Home

Github

Willgues Security Blog

Anpviz IP Camera Vulnerabilities

Affected Products: Anpviz IPC-D250, IPC-D260, IPC-B850 IPC-D850, IPC-D350, IPC-D3150, IPC-D4250, IPC-D380, IPC-D880, IPC-D280, IPC-D3180, MC800N, YM500L, YM800N_N2, YMF50B, YM800SV2, YM500L8, YM200E10 and possibly more models. Firmware appears developed by ANJVision, so ANJVision cameras likely vulnerable. GWSecurity cameras appear to be a rebranded version of Anpviz, so are also likely vulnerable

Affected Firmware Versions: V3.2.2.2 and lower

Vulnerability Types:

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

CWE-306: Missing Authentication for Critical Function

CWE-321: Use of Hard-coded Cryptographic Key

CWE-552: Files or Directories Accessible to External Parties

CWE-331: Insufficient Entropy

Descriptions:

CVE-2024-35344 - There is a hardcoded cryptographic key stored in the firmware of the device. Additionally, this key is insufficiently random, as it appears to have been generated by hand due to patterns such as "12 34 57 98" present in the key. This affects Anpviz IPC-D250, IPC-D260, IPC-B850 IPC-D850, IPC-D350, IPC-D3150, IPC-D4250, IPC-D380, IPC-D880, IPC-D280, IPC-D3180, MC800N, YM500L, YM800N_N2, YMF50B, YM800SV2, YM500L8, YM200E10 and possibly more models of IP camera. The key is shared across all firmware versions and devices.

CVE-2024-35342 - Unauthenticated users are able to modify or disable camera related settings such as microphone volume, speaker volume, LED lighting, NTP, motion detection, and more. This affects Anpviz IPC-D250, IPC-D260, IPC-B850 IPC-D850, IPC-D350, IPC-D3150, IPC-D4250, IPC-D380, IPC-D880, IPC-D280, IPC-D3180, MC800N, YM500L, YM800N_N2, YMF50B, YM800SV2, YM500L8, YM200E10 and possibly more models of IP camera.

CVE-2024-35341 - Unauthenticated users are able to download the running configuration of the device via a HTTP GET request to /ConfigFile.ini or /config.xml URIs. This configuration file contains usernames and encrypted passwords (encrypted with a hardcoded key common to all devices). This affects Anpviz IPC-D250, IPC-D260, IPC-B850 IPC-D850, IPC-D350, IPC-D3150, IPC-D4250, IPC-D380, IPC-D880, IPC-D280, IPC-D3180, MC800N, YM500L, YM800N_N2, YMF50B, YM800SV2, YM500L8, YM200E10 and possibly more models of IP camera.

CVE-2024-35343 - Unauthenticated users are able to download arbitrary files from the device's filesystem via a HTTP GET request to the /playback/ URI. This affects Anpviz IPC-D250, IPC-D260, IPC-B850 IPC-D850, IPC-D350, IPC-D3150, IPC-D4250, IPC-D380, IPC-D880, IPC-D280, IPC-D3180, MC800N, YM500L, YM800N_N2, YMF50B, YM800SV2, YM500L8, YM200E10 and possibly more models of IP camera.



I recently purchased an Anpviz IPC-D250SE IP camera, and I began analyzing the device to assess its security. I decided to primarily focus on the web interface of the device.

In this model of Anpviz camera, there is functionality that is not visible or accessible by an administrator, but is still present in the web_server binary. Many of these hidden functionalities require no authentication, and allow things such as configuration modification, downloading the running config of the device (with usernames and encrypted passwords), and downloading of arbitrary files on disk via HTTP. I was also able to ascertain that there is an AES encryption key hard coded into binaries on the device, which is the same for all customers and is used to encrypt passwords stored in the running config.

First, I started by extracting the firmware using commonly available firmware analysis tools. This gave me access to the binaries that run on the device. In the /opt/ch directory, there are several binaries, including web_server, comm_server, danale_server, hik_server, mainctrl, among others. These server binaries import critical functionality from the libtools.so library also located in /opt/ch. Most of these server binaries appear to have been developed in house.

Using the file command, we can see these are 32 bit ARM binaries, which is expected from an embedded device.

We can surmise based on the binary name that web_server is likely responsible for the HTTP functionality of the device. However, we can confirm this by analyzing the behavior of the web server (such as unique strings), and searching for those artifacts in these binaries. If we send a GET request to the device, we can see the device responds with a Server HTTP header containing “gSOAP/2.8”.

If we perform a recursive grep for “gSOAP” in the /opt/ch directory, we can see that the binary file web_server matches.

Therefore, we have confirmed that the HTTP server functionality of the device is most likely handled by the web_server binary.

We can now open the web_server binary in a disassembler like Ghidra and analyze the various code paths of the web server. The primary function that handles HTTP GET requests is named by the vendor http_get_handler. In the http_get_handler function, there are various other functions called which, depending on the URI in the GET request, will perform various actions. The first function that is called, I dubbed secondary_cgi_handler, as it handles requests to some CGI URIs, but most of the endpoints handled by this function are not accessible via the web interface by a valid administrator, most do not require authentication to access, and some do not have “CGI” anywhere in the URI string.

In contrast, the http_cgi_handle function, which is actually called after secondary_cgi_handler, handles URIs that are mostly in the format /cgi-bin/<name>.cgi. In addition, the endpoints managed by http_cgi_handle all require authentication, and some of the endpoints are used by the admin web interface. There are also more total URI endpoints handled by this function. It appears the http_cgi_handle endpoints utilize authentication properly. Based upon this, I consider this the primary CGI handler function, even though it is called after the “secondary_cgi_handler”.

It is worth noting, my naming of secondary_cgi_handler (As well as some other functions) was arbitrarily determined as there was no indication of the vendor’s original function name via disassembly for some functions in the web_server binary.

Before http_cgi_handle is called directly, a wrapper function is called which then calls http_cgi_handle.

After the http_cgi_handle and secondary_cgi_handler functions are called, a function named copy_file is called several times, with an integer, the URI string supplied, and the MIME type to be returned to the client’s browser as arguments.

The functions http_cgi_handle, secondary_cgi_handler, and copy_file are where the vulnerabilities reside.

If we take a look at http_cgi_handle, we can see it call various sub functions which check if the URI string supplied by the user contains a particular CGI endpoint, and if there is a match, one of the sub functions will perform the actions of the requested CGI script (these are not real CGI scripts, it is simply functionality embedded into the web_server binary itself).

We can see towards the end of the http_cgi_handle function, that if none of the sub functions find a match for the supplied URI string, the function prints “no such cgi interface”, and returns 404.

Most of the functions in http_cgi_handle call a cgi_is_valid_uid function, which checks a username and password supplied via HTTP GET parameters, and returns a value based on the validity of the username and password supplied. This is the authentication method used to ensure only an authorized user can call these endpoints. For example, in the /cgi-bin/rtsp function:

The only URI which does not require a username and password in the http_cgi_handle function, is /cgi-bin/connect_check, which simply sends an ICMP ping request to a specified IP address or hostname. To call this endpoint, we can send a GET request to the camera’s web interface with the following URL:

http://<IP Address>/cgi-bin/connect_check?addr=8.8.8.8

This is a relatively inconsequential function to have no authentication, so lets move on to the secondary_cgi_handler function and see what it provides.

The rebootipc, factoryipc, kernVersion+fsVersion+serialNumber, /settings/system/device_info/device_type, and &setdoorbroardcast= sub functions are the only functions that require authentication in the secondary_cgi_handler function.

Starting with getptzport, we can see in the code it checks for if “/getptzport” is in the URI string. Sending a GET request to this URI returns a port number associated with PTZ operations, which are used to move where the camera is pointing (if the camera supports it):

This port number corresponds to the “Control Port” which is visible in the admin interface under “Network → Service Ports → Control Protocol”:

Next, we can see the getmacaddr_eth0.cgi looks for the string “/getmacaddr_eth0.cgi” URI string in the user’s GET request.

When we send the GET request to the camera, we can see it returns the eth0 MAC Address of the device:

If we look at the code for the /settings/audio/VolumePlay= and /settings/audio/VolumeMic= functions, it appears to allow an unauthenticated user to modify the audio playback and audio input (microphone) volume for the camera:

If we look at the current audio settings in the admin interface, we can see the microphone and audio output are both set to max volume:

If we send a GET request to the /settings/audio/VolumePlay= and /settings/audio/VolumeMic= URIs with a value between 1 and 99 appended, we can successfully change the audio output and input volumes without authentication. Note the HTTP server responds with “ok”

Now, if we check the admin interface, we can see the “Audio Input Volume” and “Audio Output Volume” have been set to 1. This appears to allow disabling audio without authentication, possibly giving rise to some Hollywood style attacks on the camera (if it supports audio recording/audio playback)

The /settings/platform/ endpoint appears to allow some sort of functionality involving a remote server. This endpoint allows an unauthenticated user to enable this functionality, setting the server, port, username, password and MAC address associated with it.

This setting can be enabled by sending a GET request to the server with the following URL, and the server will return “ok” if the request is made successfully

http://<IPAddress>/settings/platform/&enable=1&server=10.1.2.3&port=1337&user=testuser&password=1337

It appears we can dump the same information set via the previous query (/settings/platform/) via a GET request to the URI /&queryPlatformSetup . It is not known what this functionality is used for, but it appears to not be used in this particular model camera, so it may be used in other models.

The /settings/ntp/ URI endpoint allows unauthenticated modification of the NTP settings on the device, including disabling NTP.

We can set the NTP config by sending a GET request to the device with the following URI

http://<IPAddress>/settings/ntp/&enable=1&server=notarealNTPserver.nist.gov&port=123

If we now check the admin interface for the date and time settings, we can see the NTP server has been changed:

The &wallpadmac&doormac function appears to implement some sort of MAC address setting functionality related to the previous /settings/platform/ and &queryPlatformSetup endpoints, based on the JSON format of the output and similar terms such as “packetrev” and “ack_sync”. It also contains a DebugPrint statement that indicates the function name is “ko_door_get_request

The /&ledlight= URI endpoint appears to allow enabling or disabling the LED light functionality of the device without authentication.

Note that if the character after “=” is 0 or 1, it writes this out via the io_out_write function.

This can be used by sending a GET request with the following URI to the device:

http://<IPAddress>/&ledlight=0

However, I could not disable LED lighting via sending this GET request, although it does accept the request without authentication.

The /settings/motiondetect endpoint appears to allow enabling and disabling of the motion detection alarm feature of these devices.

If we look in the admin interface under the “Event” section, there is a section dedicated to various motion detection settings.

If we send a GET request to the device with the following URI , it will disable the motion detection functionality without authentication.

http://<IPAddress>/settings/motiondetect/&enable=0&blockcount=0&blockconfig=0

Now, if we check the admin interface for the motion detection settings, we can see it was disabled via the previously supplied curl command.

The /settings/getspecific endpoint reveals some version/firmware information about the device, as well as some networking information.

By sending a GET request to the following URI, we can retrieve this information without authentication.

http://<IPAddress>/settings/getspecific

This ability to modify various camera related settings without authentication has been assigned CVE-2024-35342

Unauthenticated Configuration File Download / Unauthenticated Arbitrary File Download : CVE-2024-35341 / CVE-2024-35343

Now that we have covered the operations available without authentication via the secondary_cgi_handler function, we can focus on copy_file and usage of the copy_file function, which is where things become more serious.

Firstly, if we look at the code around where copy_file is called in the original http_get_handler function, we can see it is called in many situations, depending on the URI string. The most interesting calls are if the URI string contains “/playback/”, “ConfigFile.ini”, or “config.xml”.

Note the “helloworld” MIME type returned by the server on line 242 if the URI string contains “ConfigFile.ini”. This is particularly interesting, perhaps it is a note from the developers.

Now, lets take a look at the code inside the copy_file function and see what it actually does when these strings are present in the URI.

In the top highlighted snippet, we can see that if the URI does not contain “playback/”, but if it contains “ConfigFile.ini” or “config.xml”, it will do a call to popen, starting a subprocess that executes “cp /mnt/nand/config.xml /mnt/nand/ConfigFile.ini”. It then jumps to LAB_0001e57c, which opens the file at /mnt/nand/ConfigFile.ini, reads the bytes from it, then jumps to LAB_0001e5b0, which writes the contents of that file in response to the HTTP GET request.

Based upon this code, we should be able to download the config file stored at /mnt/nand/config.xml without authentication, simply by being able to access the web interface of the device. We should be able to do this with any URI string that contains either “config.xml” or “ConfigFile.ini”.

In the bottom highlighted snippet, we can see that if none of the previous gotos or jumps are called (no matches for ConfigFile.ini or config.xml), it will copy everything after the first 8 bytes (exactly matches “playback”) of the URI into acStack_118, and open and read a file’s bytes, with the file name based on the contents of acStack_118. It will then jump to LAB_0001e5b0, which as previously noted, writes the output of that file in response to the HTTP GET request. Based upon this code, Upon specifying a URI containing "/playback" followed by additional characters, those subsequent characters will be treated as a complete filepath and subsequently utilized by fopen() function. The resulting resource obtained via fopen() will then be transmitted as the response payload for an incoming HTTP GET request.

This means we can arbitrarily read any file on the filesystem that is readable by the user the web_server binary is running under the context of.

First, lets test the downloading of the /mnt/nand/config.xml file, by using both “config.xml” and “ConfigFile.ini” in our GET request.

First, http://<IPAddress>/config.xml:

As we can see, an XML config file is returned.

Now lets try http://<IPAddress>/ConfigFile.ini :

Now if we save and analyze the contents of these XML files, we can determine that these appear to be the running config of the device. This includes encrypted passwords.

Now, lets test the /playback/ URI and attempt to download arbitrary files from the filesystem. Based on firmware analysis, we know the actual password for these devices is stored at /etc/passwd_sys.

So lets try http://<IPAddress>/playback/etc/passwd_sys

As we can see, we have successfully extracted an arbitrary file from the filesystem. Using this, we can even dump /dev/mtd volumes and dump the entire root filesystem image, as well as many other things.

You might be wondering how the password encryption is implemented in the config file that is able to be extracted. Based on the string “EncryptPwd” being in the config file, we can search for this string and see where it is utilized, in order to look for the encryption function.

If we grep for “EncryptPwd” in the /opt/ch directory, we can see the binary file libtools.so matches.

Use of Hardcoded Weak Cryptographic Key: CVE-2024-35344

Lets open up the libtools.so in Ghidra and analyze which function uses that particular string.

Here is a function which uses the “EncryptPwd=%s” string in a call to snprintf(), and the text that is placed into “%s” is a var local_450, which shortly before is passed to a function called StringEncrypt()

Based on this contextual information, we can surmise that the StringEncrypt function is the function responsible for encrypting passwords stored in /mnt/nand/config.xml.

We can assume the input string is auStack_410 (param_1), the output is local_450 (param_2), and a length (64, param_3) is supplied as the third argument.

In the StringEncrypt() function, we can see local_224 is initialized with the first 16 characters of the input string (line 35). These first 16 characters are passed as the input to a crypto_aes_expand_key() function, which has 3 arguments; an output variable (auStack_1fc), an encryption key (memory location DAT_000b3adc), and a key size (32 bytes, or 256 bits).

The expanded key is then passed to aes_encrypt_(), which takes the expanded key, an output variable, and the string to be encrypted (first 16 characters of the input string).

After encryption, the encrypted data is passed to a hexdataToHexStr() function, which hex encodes the encrypted string.

Lets take a look at the data stored at DAT_000b3adc, and see if there is a discernible encryption key.

We can see the data stored at DAT_000b3adc appears to contain 32 bytes of data, with each byte separated by a null byte (00). This is because it is stored as an array of uint_32t, and on most CPUs, the size of a uint32_t variable is 4 bytes, so padding bytes are inserted by the compiler.

We can extract the full 32 byte (or 256 bit) encryption key by removing the padding bytes, so the full encryption key (in hex bytes) is as follows:

19 42 50 15 53 88 ab cd 88 77 66 55 44 33 de ef 12 34 57 98 0a ba cd fe dd cc 99 55 66 88 3f fe

Based on the pattern “ab cd 88 77 66 55 44 33 de ef 12 34 57 98” included in the key, it appears this key was not randomly generated, but typed by hand. The fact that this key is embedded directly within every libtools.so binary on all devices implies that attackers could potentially extract and utilize this key to decrypt the saved passwords present in the running config of any device, given they manage to obtain access to the binary.

Combined with the previous unauthenticated config file download vulnerability, an attacker could gain administrative access to every device that contains these issues.

I was able to analyze multiple different firmware images from different camera models, as well as extracting the libtools.so binary from a live device, and was able to confirm the same key is hardcoded across all devices.

Enabling of Debug Server / Telnet Server

In the secondary_cgi_handler() function, the first sub function that is called is /openslog(), which appears to allow enabling of some sort of syslog functionality. However, there is an authenticated component of this function that searches for the URI string “/cgi-bin/console.cgi?”. If this URI string is present, along with a valid username, password, and “enable” value supplied via the GET request, a user can enable whats known as the “debug server”, which is a separate binary located at /opt/ch/dbg_server. When this debug server is running, it listens on port 9999, and can be accessed via netcat.

First, lets check to see if port 9999 is listening on the device via netcat:

As shown, the connection was refused on that port.

Now, if we send the following GET request (note these are the default credentials, but this is an authenticated CGI endpoint), we can enable the debug server on port 9999:

http://<IPAddress>/cgi-bin/console.cgi?enable=1&username=admin&password=123456

Now, if we run a netcat command to connect to that port, we can interact with the debug server. There are many different functions available in the debug server, lets first try running “help” to see what commands are available.

If we type in “telnet” into the debug server, it accepts the command and indicates a return code of 0x00000000 (0), which typically indicates success.

Now, we can try to telnet to the device.

The credentials used for the admin interface are not accepted, so the credentials used here are presumably for root, and whatever the MD5 hash corresponds to that is available from /etc/passwd_sys. Based on open source research of the hash $1$yFuJ6yns$33Bk0I91Ji0QMujkR/DPi1 , this hash is used in multiple devices.

Conclusion

Based on these findings, an unauthenticated attacker who has unrestricted network access to a device could download the running configuration of the device, use the knowledge of the hardcoded key and AES implementation to decrypt the admin password, which would then allow enabling of the debug server and telnet. Then, the attacker could use the unauthenticated arbitrary file download vulnerability to retrieve the root password hash, crack the hash, and then log into the device as root via telnet, allowing full compromise of the affected devices.

I attempted to report this vulnerability to Anpviz, ANJVision, and GWSecurity. After multiple attempts to reach out, I only received a response from one ANJVision employee, who simply asked if I reverse engineered the firmware. I requested a timeline for when a patch would be available to end users on numerous occasions, and that I wished to coordinate responsible disclosure when a patch was available. I continued to receive no response

February 19 - Initial email to Anpviz, ANJVision, and GWSecurity with vulnerability details

February 20 - Follow up email to Anpviz, ANJVision, and GWSecurity, asking for confirmation that they had received the report

Feburary 21 - Second follow up email to Anpviz, ANJVision, and GWSecurity, asking for confirmation that they had received the report, and that I planned on coordinating with US-CERT if I was unable to make contact

February 21 - Received email from ANJVision employee, asking if I decompiled the firmware. The individual also asked me to provide a firmware image, because some of the issues may have been fixed in a new version. I provded answers to the questions, and provided the links to firmware images.

March 24 - Followed up again and requested a timeline for patched firmware, and that I planned to disclose the issues after new firmware was available

March 26 - Followed up again and requested a timeline for patched firmware, and that I planned to disclose the issues after new firmware was available

April 4 - Followed up again and requested a timeline for patched firmware, and that I planned to disclose the issues after new firmware was available

April 22 - Opened report with US-CERT about vulnerability

May 4 - Disclosed