在调试dlink的httpd时,漏洞可能发生在httpd通过fork+execute调用的cgibin中,其中httpd解析网络请求中的字段,并且以环境变量的形式传递给cgibin进行处理。那么要调试cgibin就有两种方式:
方法1的优点是简单直接,缺点是需要了解httpd是如何处理、传递数据到环境变量,以免使用了实际上通不过httpd校验的环境变量;方法2的优点是直观,但是需要熟练掌握gdb调试子进程相关的知识。
和许多典型的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调用的程序,可以使用如下步骤:
set follow-fork-mode child
,使得gdb开始调试子进程。set detach-on-fork off
,让在调试子进程的同时,父进程挂起。这样执行完子进程也可以返回到父进程中。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调用的程序,办法是:
set follow-fork-mode child
,使得gdb调试到子进程中。set detach-on-fork off
,使得gdb在调试子进程的时候挂起父进程,这样也方便并行调试。catch exec
,捕获子进程通过execute调用执行cgi的事件。更多【 调试httpd通过fork+execute调用的cgibin程序】相关视频教程:www.yxfzedu.com