一个完全可复现的实验环境,以及一个手工编写的原始套接字漏洞利用程序,针对
CVE-2010-4221 —— ProFTPD 的 pr_netio_telnet_gets() 中的预认证栈缓冲区溢出,
作为漏洞研究与漏洞利用开发的学习练习而构建。
每一次失败都有记录。一帆风顺的路径是谎言;弯路才是教训。
本仓库是一个教育性产物。它的存在是为了让那些负担不起导师或培训的人, 能够了解内存破坏漏洞利用究竟是如何诞生的:从补丁开始,经历失败, 最终在你自己拥有的实验环境中形成可用的概念验证。
红队工作 —— 真正的、专业的进攻性安全 —— 由一个词定义:授权。 专业人员所做的一切都发生在书面协议之内:一份签署的《交战规则》文件, 其中明确了范围、目标、允许使用的技术、时间窗口以及批准人员。 没有这份文件,完全相同的按键操作就不再是职业行为 —— 在地球上几乎 所有司法管辖区,这都是犯罪。
因此,以下是本仓库不可协商的契约:
这门技艺值得学习。但只有伴随着随之而来的纪律,这门技艺才有价值。
ProFTPD 在 FTP 控制通道上处理 TELNET 转义序列。在 TELNET 中,
0xFF(IAC,"Interpret As Command",解释为命令)是转义字节;
字面量 0xFF 以 0xFF 0xFF 的形式发送。
pr_netio_telnet_gets() 将客户端字节复制到栈缓冲区
(pr_cmd_read 的 char buf[PR_DEFAULT_CMD_BUFSZ+1],在 glibc 的
MAXPATHLEN=4096 下为 4104 字节),并通过 buflen —— 一个
size_t,无符号类型 —— 跟踪剩余空间。
漏洞路径(1.3.3a,netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // 写入 #1
buflen--; // 递减 #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // 写入 #2
buflen--; // 递减 #2 <-- 两者之间没有检查
两次写入,两次递减,两者之间没有零值检查。当 buflen
恰好为 1 时,这对操作将其递减到 0,然后下溢到
SIZE_MAX(1800 京)。循环现在认为缓冲区是无限的,
并持续将攻击者控制的字节写入栈的上方 —— 覆盖保存的寄存器、
保存的 RBP 以及返回地址。
预认证。 该函数在 USER/PASS 被处理之前就已运行。
修复(提交
3cc69b8388,
"Bug#3521 - Telnet IAC processing stack overflow",发布于 1.3.3c)
只有十二行。整个安全边界是:
if (buflen == 0) {
break;
}
参见 patch.diff。阅读补丁能告诉你伤口在哪里 ——
这就是技能所在。
Dockerfile 从历史 Debian 快照源码编译 ProFTPD 1.3.3a, 故意设置为不安全状态(这就是 2010 年的样子):
-fno-stack-protector — 无金丝雀(canary)-z execstack — 可执行栈(无 NX)-no-pie — 固定二进制地址gdb 下运行,gdb 默认禁用 ASLR → 确定性栈docker build -t proftpd-133a .
docker rm -f lab133 2>/dev/null
docker run -d --name lab133 --cap-add SYS_PTRACE \
--security-opt seccomp=unconfined -p 127.0.0.1:2122:21 \
proftpd-133a sh -c 'gdb -batch -ex "set follow-fork-mode child" \
-ex "run" -ex "continue" --args /usr/local/sbin/proftpd -n -d1 \
> /tmp/gdb.txt 2>&1; sleep 600'
python3 exploit.py
预期输出:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(Shellcode 写入文件描述符 0、1 和 2,因为我们不想依赖 知道哪一个承载控制通道 —— 其中两个会响应。)
"SITE " + NOP 滑板 + shellcode + [\xff\xff 洪水] + 填充 + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
shellcode 位于命令缓冲区内部 溢出尾部仅
—— 无人触碰的区域 传递一个地址
buf[4102] 被触碰(截断 NUL)。帧的活动局部变量
之下的所有内容都是平静的。buflen 驱动到下溢(关于奇偶性问题,参见"旅程")。pr_cmd_read 在解析后命中 return 0 时,CPU 落入滑板
并滑入 shellcode。这种倒置架构 —— 载荷在前,洪水居中,地址最后 ——
已针对规范的 Metasploit 模块(proftp_telnet_iac)进行了验证,
该模块使用相同的布局。他们的目标启用了 NX,因此需要带有
res 指针"四重解引用"的 ROP 链;我们的实验环境具有可执行栈,
因此单次直接返回就足够了。
最终的漏洞利用程序只有 60 行。它的代价是:
盲目的 \xff 洪水 → 毫无效果。 服务器礼貌地关闭了会话。
根本原因:buflen 从 4102(偶数)开始,每对 IAC 递减 2 ——
它干净地落在 0 上,永远不会落在 1 上。下溢需要打破奇偶性。
教训:阅读状态机胜过盲目喷洒。
错误的缓冲区大小。 第一次校准尝试假设缓冲区为 1024 字节。
实际大小在 Linux/glibc 上是 MAXPATHLEN+8 = 4104。洪水在距离
目标 3KB 处停止。教训:测量目标,不要假设目标。
第一次 SIGSEGV。 循环 De Bruijn 模式(Aa0Aa1...)将返回槽位
定位在 buf+4152,经过两次交叉验证(帧数学 + 模式偏移)。
教训:循环模式是测量尺,不是漏洞利用。
RIP 控制。 将槽位设置为 0x4141414141414141 导致 ret
指令本身崩溃 —— x86-64 拒绝非规范地址,故障落在 ret 上,
而我们的值在回溯中等待。教训:ret 上崩溃且帧中有你的值 = 已控制。
ret 槽位上方的 shellcode → 被破坏。 8 个字节被堆指针
(0x4d7838 —— 后来被识别为 cmd_rec 池分配)覆盖。
槽位上方的栈帧属于在着陆与劫持之间仍在工作的函数。
教训:溢出不是最后一次写入;程序在你刚刚破坏的栈上继续运行。
槽位下方的"死区" → 也被涂写。 pr_cmd_read 自身的局部变量
(cmd、buflen、cp)恰好位于那里,并在解析期间持续被存储。
硬件监视点取证。 在 gdb 中使用 watch *(long*)ADDR
将谜团变成了摄像机:对被破坏地址的每次写入,都带有回溯,
按顺序呈现。教训:当问题是"谁写了这块内存?"时,
答案就在一条 gdb 命令之遥。
先阅读参考资料,然后理解它。 规范模块确认了倒置架构。 在建立自己的心智模型之后阅读他人的漏洞利用是学习; 在此之前阅读则是抄袭。
size_t 永远不会变负 —— 它会变得巨大。 剩余空间计数器中的
整数下溢就是带额外步骤的栈溢出。\x0a
(结束读取)并能在 \xff(TELNET 转义)下存活。argv[0] 中 11 字节的差异
(构建树二进制 vs 已安装二进制)将整个栈偏移了 0x40,
并悄然使一个完美的漏洞利用失效。modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)由 diegslva 构建,公开学习 —— 从"从未写过漏洞利用"到 使用手工编写的 shellcode 实现预认证 RCE,在一个有记录的日内完成。 如果本仓库让你学到了什么,请将这份知识传递下去。