-
-
[原创] 调试httpd通过fork+execute调用的cgibin程序
-
2023-3-14 10:55 2449
-
在调试dlink的httpd时,漏洞可能发生在httpd通过fork+execute调用的cgibin中,其中httpd解析网络请求中的字段,并且以环境变量的形式传递给cgibin进行处理。那么要调试cgibin就有两种方式:
- 设置环境变量,然后直接调试cgibin。
- 了解httpd是如何调用cgibin的,然后通过设置gdb调试子进程、catch exec,调试cgibin。
方法1的优点是简单直接,缺点是需要了解httpd是如何处理、传递数据到环境变量,以免使用了实际上通不过httpd校验的环境变量;方法2的优点是直观,但是需要熟练掌握gdb调试子进程相关的知识。
httpd调用CGI上下文说明
和许多典型的httpdserver一样,传入到函数process_cgi中的参数a1是httpd所定义的数据结构,其中包含了一个网络请求的数据集合,例如请求方式REQUEST_METHOD、URI、SESSION等等。a1传递到process_cgi函数中进行处理,获取到需要调用的cgi,以及将需要处理的数据转化成环境变量集合到cgi中。在dir 850l的固件中,几乎所有的cgi都是通过链接的形式到程序cgibin,cgibin根据请求的不同来采用不同的接口(函数)进行处理。
1 2 3 4 5 6 | int __fastcall process_cgi(_DWORD * a1) { ...... v77 = spawn( * filename, filename, argv, v79, v75, v9, v8, a1 + 992 ); ...... } |
spawn函数则是一个典型的封装了fork+execve的函数,通过fork函数创建子进程,设置子进程的进程组、资源限制、重定向输入输出等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | __pid_t __fastcall spawn(const char * filename, char * const * argv, char * const * envp, int a4, int a5, int a6, int a7, char * path) { __pid_t result; / / $v0 int v13; / / $a0 int v14[ 4 ]; / / [sp + 18h ] [ - 18h ] BYREF __pid_t v15; / / [sp + 28h ] [ - 8h ] result = fork(); / / 创建子进程,此时的子进程依旧是httpd的程序镜像 if ( result = = - 1 ) { v15 = - 1 ; lerror( "spawn: failed to create child process" ); goto LABEL_6; } if ( !result ) { setpgid( 0 , 0 ); / / 改变子进程的进程组 sub_409C9C( 13 , 0 ); if ( coredir ) { v14[ 3 ] = 0 ; v14[ 2 ] = 0 ; v14[ 1 ] = 0 ; v14[ 0 ] = 0 ; setrlimit64( 4 , v14); / / 设置资源限制 } dup2(a4, 0 ); / / 复制文件描述符:标准输入和标准输出 dup2(a4, 1 ); if ( a5 ! = - 1 ) dup2(a5, 2 ); if ( chdir(path) = = - 1 ) { v13 = 5 ; } else { execve(filename, argv, envp); / / 执行cgibin,将cgibin加载并替换掉子进程的httpd v13 = 6 ; } exit(v13); } + + dword_42350C; if ( debug ) { v15 = result; log_d( "child process %d created" , result); LABEL_6: result = v15; } return result; } |
那么到此,回想之前的标题:如何调试httpd通过execute调用的cgibin,该问题就可以抽象为:如何调试子进程中通过execute调用的可执行文件。
调试原理
默认情况下,gdb在调试多进程的时候,只会追踪父进程,例如执行完fork函数,fork的返回值是子进程的pid,gdb中实际上在调试的是父进程。如果要调试子进程,则需要在gdb中使用如下的命令:
1 | set follow - fork - mode child |
上面命令解决了让gdb调试到子进程,但是有时候还需要同时调试父进程和子进程,如果仅仅是gdb在子进程中,父进程依旧会正常运行。那么,就可以使用如下的命令,使得在调试子进程的时候,父进程也暂停处于挂起的状态:
1 | set detach - on - fork off # 默认是on |
上面两条命令结合起来就实现了同时调试父进程和子进程,那么这个时候也还有一个问题:在子进程中,execute调用cgibin是作为一个函数来实现的,单步步过该函数达不到调试目的,步进该函数更加容易陷入到函数的细节实现中。
对于调试cgibin,可以通过catch exec
命令,来捕获执行新进程的事件。当进程使用execute重新执行一个程序时,gdb会中断程序的运行,到ld加载器start函数中。除此之外,还可以使用例如catch exec /bin/ls
来指定需要捕获的具体进程加载程序事件。
综上所述,调试通过fork+execute调用的程序,可以使用如下步骤:
- 在fork的时候,执行gdb命令:
set follow-fork-mode child
,使得gdb开始调试子进程。 - 同时,可以执行gdb命令:
set detach-on-fork off
,让在调试子进程的同时,父进程挂起。这样执行完子进程也可以返回到父进程中。 - 执行gdb命令:
catch exec
,捕获子进程通过execute加载执行新程序的事件。
调试过程
现在回归到具体的调试过程中。
当httpd执行到调用cgi的spawn函数中,此时准备执行fork函数,可以看到只有httpd一个进程,也就是父进程PID=1444:
1 2 3 | pwndbg> info inferiors Num Description Executable * 1 process 1444 / home / utest / app / FirmAE / firmwares / _DIR850L_FW115KRb07. bin .extracted / squashfs - root / sbin / httpd |
第一步需要保证在执行到spawn的时候,fork之后是进入了子进程,
1 2 | 0x409d2c <spawn + 56 > move $s3, $a2 ► 0x409d30 <spawn + 60 > jalr $t9 <fork> |
此时,执行gdb命令:
1 2 | set follow - fork - mode child set detach - on - fork off |
再查看进程信息,也可以确认此时gdb处于子进程中。此时也可以通过inferiors Num
切换到父进程。
1 2 3 4 | pwndbg> info inferiors Num Description Executable 1 process 1444 / home / utest / app / FirmAE / firmwares / _DIR850L_FW115KRb07. bin .extracted / squashfs - root / sbin / httpd * 2 process 31740 / home / utest / app / FirmAE / firmwares / _DIR850L_FW115KRb07. bin .extracted / squashfs - root / sbin / httpd |
继续在子进程中执行到execute函数,可以看到此时正准备加载执行/htdocs/web/session.cgi
,该cgi实际上是一个链接到cgibin。
1 2 3 4 5 6 7 | 0x409e68 <spawn + 372 > addiu $a0, $zero, 5 0x409e6c <spawn + 376 > lw $t9, - 0x7d7c ($gp) 0x409e70 <spawn + 380 > move $a0, $s2 ► 0x409e74 <spawn + 384 > jalr $t9 <execve> path: 0x7ed330 ◂— '/htdocs/web/session.cgi' argv: 0x4280f0 —▸ 0x7ed330 ◂— '/htdocs/web/session.cgi' envp: 0x0 |
那么此时就应该执行gdb命令:catch exec
,用来捕获子进程加载cgi的事件:
这个时候可以查看栈,来看httpd传递给cgi哪些环境变量(也就是需要处理的数据):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | pwndbg> stack 30 00 : 0000 │ fp sp 0x7fe2b4a0 ◂— 0x1 01 : 0004 │ 0x7fe2b4a4 —▸ 0x7fe2bd23 ◂— '/htdocs/web/session.cgi' 02 : 0008 │ 0x7fe2b4a8 ◂— 0x0 03 : 000c │ 0x7fe2b4ac —▸ 0x7fe2bd3b ◂— 'HTTP_HOST=192.168.0.1' 04 : 0010 │ 0x7fe2b4b0 —▸ 0x7fe2bd51 ◂— 'HTTP_USER_AGENT=Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0' 05 : 0014 │ 0x7fe2b4b4 —▸ 0x7fe2bdb0 ◂— 'HTTP_ACCEPT=*/*' 06 : 0018 │ 0x7fe2b4b8 —▸ 0x7fe2bdc0 ◂— 'HTTP_ACCEPT_LANGUAGE=en-US,en;q=0.5' 07 : 001c │ 0x7fe2b4bc —▸ 0x7fe2bde4 ◂— 'HTTP_ACCEPT_ENCODING=gzip, deflate' 08 : 0020 │ 0x7fe2b4c0 —▸ 0x7fe2be07 ◂— 'HTTP_ORIGIN=http://192.168.0.1' 09 : 0024 │ 0x7fe2b4c4 —▸ 0x7fe2be26 ◂— 'HTTP_REFERER=http://192.168.0.1/index.php' 0a : 0028 │ 0x7fe2b4c8 —▸ 0x7fe2be50 ◂— 'HTTP_COOKIE=uid=bjtsPYnEJz' 0b : 002c │ 0x7fe2b4cc —▸ 0x7fe2be6b ◂— 'GATEWAY_INTERFACE=CGI/1.1' 0c : 0030 │ 0x7fe2b4d0 —▸ 0x7fe2be85 ◂— 'CONTENT_LENGTH=31' 0d : 0034 │ 0x7fe2b4d4 —▸ 0x7fe2be97 ◂— 'CONTENT_TYPE=application/x-www-form-urlencoded' 0e : 0038 │ 0x7fe2b4d8 —▸ 0x7fe2bec6 ◂— 'SCRIPT_FILENAME=/htdocs/web/session.cgi' 0f : 003c │ 0x7fe2b4dc —▸ 0x7fe2beee ◂— 'REQUEST_URI=/session.cgi' 10 : 0040 │ 0x7fe2b4e0 —▸ 0x7fe2bf07 ◂— 'REMOTE_ADDR=192.168.0.2' 11 : 0044 │ 0x7fe2b4e4 —▸ 0x7fe2bf1f ◂— 'REMOTE_PORT=45050' 12 : 0048 │ 0x7fe2b4e8 —▸ 0x7fe2bf31 ◂— 'REQUEST_METHOD=POST' 13 : 004c │ 0x7fe2b4ec —▸ 0x7fe2bf45 ◂— 'SCRIPT_NAME=/session.cgi' 14 : 0050 │ 0x7fe2b4f0 —▸ 0x7fe2bf5e ◂— 'SERVER_NAME=192.168.0.1' 15 : 0054 │ 0x7fe2b4f4 —▸ 0x7fe2bf76 ◂— 'SERVER_ADDR=192.168.0.1' 16 : 0058 │ 0x7fe2b4f8 —▸ 0x7fe2bf8e ◂— 'SERVER_PORT=80' 17 : 005c │ 0x7fe2b4fc —▸ 0x7fe2bf9d ◂— 'SERVER_SOFTWARE=Mathopd/1.6b9' 18 : 0060 │ 0x7fe2b500 —▸ 0x7fe2bfbb ◂— 'SERVER_ID=LAN-1' 19 : 0064 │ 0x7fe2b504 —▸ 0x7fe2bfcb ◂— 'SERVER_PROTOCOL=HTTP/1.1' 1a : 0068 │ 0x7fe2b508 ◂— 0x0 |
以前做毕设的时候,要实现对cgi的模糊测试,就是将AFL生产的数据通过设计的数据结构转换成环境变量到cgi 中去执行。
小结
如何调试httpd使用fork+execute执行的cgibin,本质上可以抽象为:如何调试通过fork+execute调用的程序,办法是:
- 执行到fork函数,执行gdb命令:
set follow-fork-mode child
,使得gdb调试到子进程中。 - 执行gdb命令:
set detach-on-fork off
,使得gdb在调试子进程的时候挂起父进程,这样也方便并行调试。 - 执行到execute函数处,执行gdb命令:
catch exec
,捕获子进程通过execute调用执行cgi的事件。