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 “”, 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?

  • 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

Macintosh HD:Users:byoung:Desktop:CTF QA:Archive (1):Screen Shot
2017-05-31 at 9.45.26
AM.png 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:

  • /root/mgmt/

  • /usr/sbin/

The second line is what we’re looking for. The 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 ( and decompile the fwupdate binary to 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:

  • /login?

  • /take_photo?

  • /usr/sbin/dnsclient

  • /sbin/sysdiag

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:

After some hard reversing works, we could know how everything works:

  1. 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”.

  2. 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.

  3. 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



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:

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 bs=1 skip=1145981

Identify Pseudo-encrypted Entry in ZIP File

There’re two files in 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).

Macintosh HD:Users:byoung:Desktop:Screen Shot 2017-07-20 at 2.07.18
PM.png 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.