There are different approaches to successfully resolve the challenge. For example, we could extract the camera’s firmware (QEMU system image) from the Docker container, and just reverse the firmware as well as those CGI binaries in it. Here we present another approach by dynamically running the camera system and do some penetration test before reversing.
From Command Injections to Get the Shell
Check Exposed Services
In the “run_docker.sh”, three TCP ports were designed to be exposed by mappings:
Internal 22 to external 2222, which seems like SSH service
Internal 8080 to external 8080, which, according the challenge’s description, is the camera’s management web service
Internet 3721 to external 3721, which is an uncommon and weird port
Although nmap may show the three ports were all open, we’re unable to build SSH connection with 2222 yet at this moment. Let’s take a look at the management interface by any browser.
Find Web Service’s Vulnerabilities
In the camera’s management web service, some common IP camera’s functionalities were provided. Check their source code, we could find three HTTP requests that accept a parameter:
Check Current IP: /get_ip.html?interface=eth0
Test Network: /ping_google.html?server=www.google.com
Update NTP Server: /set_ntp.html?server=newNTP
Command injection is a pretty popular kind of vulnerabilities in IoT device’s web services. Let’s try to manipulate these parameters and to make HTTP requests. Note that, these URLs only respond raw data rather than validate HTML pages. Hence we should either just print the content by script, or view the source code in a browser to get the responses.
In the first URI, by changing the parameter value from eth0 to “eth0;whoami;ls”, a user name “root” appeared in the response. Hence here is an injection point. The “Test Network” URI hasn’t similar issue though. And the “Update NTP Server” has similar injection, but the command’s output won’t be shown in HTTP response. Hence we’ll just use the first one.
Get root Password
Since we’ve got shell command execution in root privilege, there’re many ways to get root user’s password. For example, you could reset its password, or you could overwrite the /etc/shadow file. Let’s try a gentle way. Just cat the existing /etc/shadow file. Its first line looks like this:
Figure : Editing the GET request
Figure : Result of the injected command
After doing a simple Google search for a part of the hash we start to see some results. We are lucky enough that this is an unsalted root password. Google it and we could find the existing password is “admin123”.
Figure : Search results for the password hash
Start SSH Server
By injecting command “netstat -nlp”, we could know the SSH service is not running. Execute “cat /etc/*-release”. Aha! The system is just based on Debian 7. Hence we could use “service ssh start” to start the SSH server. Now it’s time to connect with the camera by SSH through port 2222.
Figure : Starting the SSH server
Note that, the two steps above are not necessary. Through command injection, you could also directly get a root shell by “nc”. SSH will be a little more convenient for file transferring in next steps though.
Play with the Backdoor
Locate the Python CGI Code
Now it’s time to analyze the weird service on TCP port 3721. You could get the process id by “netstat -nlp” and then check the process’s file descriptors. Another way is to check all common system initialization scripts in Linux. In /etc/rc.local, we could find these two lines:
The second line is what we’re looking for. The fwupdate.sh script executed another file /usr/sbin/fwupdate. SCP this file out and check its file type by “file”. We can find it’s a “python 2.7 byte-compiled” file.
Reverse the Obfuscated Python Code
Set up PyCDC (https://github.com/zrax/pycdc) and decompile the fwupdate binary to fwupdate.py source code. From which, we are able to know this Python CGI script was based on BaseHTTPServer library while its code was obfuscated by Opy. And there’s a do_GET() callback function that compared requested URI with two prefixes and then did something. All the constant strings could be decoded by the same function “l11l1opy”.It’s not necessary to understand this function – just run it we could get all plaintext strings.
Figure : Running function and its output
There’re some interesting plaintext strings decoded from the Python script:
Hence we could guess, the backdoor CGI accept two GET requests to /login and to /take_photo , and the requests will be handled by dnsclient and sysdiag respectively. SCP out these two files. They’re all ELF executables.
Reverse the Binaries to Get CGI Protocols
Check the two binaries’ strings. It’s easy to realize they were packed by UPX 3.08. Unpack them.
By checking strings, we could know that the dnsclient accepts two parameters “username” and “password”. If the authentication failed, an error message will be returned. Otherwise, if authentication passed, a piece of XML content which contains a token will be returned.
On the other hand, the sysdiag will accept the token and return a photo. These is also a hard-coded file path “/var/opt/log” in it. In the running camera system, this file doesn’t look like a plaintext log file, nor an image file.
Reverse Backdoor Authentication Algorithm
You may consider to bypass username/password authentication by directly feeding a token value to sysdiag. However, this won’t work – the response you will get won’t be a validate picture. It’s time to reverse the binaries now. We can use IDA Pro’s evaluation edition, which is free: https://www.hex-rays.com/products/ida/support/download_demo.shtml
After some hard reversing works, we could know how everything works:
the first 8 bytes of username and the first 8 bytes of password will be XORed one by one, and then XORed with the byte in “\x7d\x77\x7c\x7e\x65\x73\x77\x62” respectively. The result should be “backdoor”.
The token has two parts: the first 8 bytes were generated by XORing first 8 bytes of username, first 8 bytes of password, and “panwpanw”. The second 8 bytes were randomly generated. These 16 bytes were then encoded by Base64 as the token.
It seems like the token was then operated by some ways and used to decrypt data in /var/opt/log. It’s not necessary to know details of these.
Generate a Validate Token
We got two equations from previous analysis:
username XOR password XOR “\x7d\x77\x7c\x7e\x65\x73\x77\x62” == “backdoor”
username XOR password XOR “panwpanw” == token[:8]
Hence we could directly calculate a validate token, no matter what username/password to use. For example, a validate token is b3dxYnF9dmd4a21wb2Rtbg==.
Perform CGI Requests
Finally, we are able to make a HTTP GET request to /take_photo on port 3721 and get a validate PNG format photo taken by the camera.
Analyze Captured Photo
Figure : Photo that was taken by the camera
Discover Hidden ZIP File and Extract It
The photo we got contains a hint “to be a picture or not to be, that is the question” in the image. Let’s take a look if there’s anything else hidden in it:
$ binwalk photo.png DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 PNG image, 1280 x 720, 8-bit/color RGB, non-interlaced 62 0x3E Zlib compressed data, best compression **1145981** 0x117C7D **Zip archive data**, encrypted at least v1.0 to extract, compressed size: 52, uncompressed size: 40, name: flag 1146111 0x117CFF **Zip archive data**, encrypted at least v2.0 to extract, compressed size: 124, uncompressed size: 148, name: README_I_AM_NOT_ENCRYPTED 1146487 0x117E77 **End of Zip archive**, footer length: 22 That’s it. A ZIP file was appended in the file. You may plan to directly extract the files via “binwalk -Me photo.png”. But that will fail – the extracted flag looks not in plaintext. Let’s just manually extract the ZIP file: $ dd if=photo.png of=photo.zip bs=1 skip=1145981
Identify Pseudo-encrypted Entry in ZIP File
There’re two files in photo.zip: flag and README_I_AM_NOT_ENCRYPTED. Try to unzip any one of them, we’ll be asked for a password, even the second file claims itself unencrypted.
Check the ZIP file structure by 010 Editor. For the “flag” file, its ZipFileRecord’s flag type is FLAG_Encrypted and FLAG_DescriptorUsedMask, while the “README_I_AM_NOT_ENCRYPTED” has flags of FLAG_Encrypted and FLAG_CompressionFlagBit1. The README file’s flags are weird cause ZIP usually not encrypt the content and compress the content in the same time! Besides, the file’s compression type is also valid (COMP_DEFLATE).
Figure :ZIP contents comparison of flags
Here we met a technique named pseudo-encryption of ZIP file. It’s a trick that manually change an ZIP entry’s flag to be encrypted, while the compressed content was not actually encrypted at all. The standard ZIP utility or some popular tools will be cheated and think it’s encrypted. While these utilities will try to decrypt the content by any password provided by users, that will always fail. This trick was used by many Android malware in the wild in years ago.
Since we’ve know how it works, just manually change the flags in README’s ZipFileRecord and ZipDirEntry to any normal ZIP entry’s flags value, and then directly unzip the file.
Get the Flag
In the decompressed README, you’ll get the flag ZIP password “bad9ea55d2f57a3131825cb5b5c1d220378dd439” and finally get the correct flag of this challenge.