“Integrated Security” from the “Intro to Cybersecurity” course by pwn.college

The last series from the “intro” course is to wrap up all the knowledge you’ve learned so far. This last step nicely combines all the previous ones you’ve (or should’ve) made, testing your knowledge in different areas at once.

0x01 and 0x02: ECB-to-Win

These tasks test your understanding of the vulnerable ECB mode in the AES cipher and your knowledge of basic buffer overflow techniques.

You have two programs that encrypt and decrypt data using an unknown key. To verify that the data was encrypted with the “correct” script, the encryption part prepends two values ​​to the plaintext: the constant VERIFIED and the length of the following plaintext so that the decrypting part could check them. Furthermore, both parts limit the plaintext to only 16 bytes.

The decryptor, written in C, suffers from a classic buffer overflow: it checks the header and its size but doesn’t verify the plaintext’s size, decrypting it into a fixed-size stack buffer. Therefore, your task is to find a way to “trick” the decryptor, pass the header length check, and force it to decrypt plaintext that exceeds the buffer size.

You probably remember from the “Cryptography” course that ECB mode uses fixed-size blocks that are independent of each other. That is, it simply splits the data into fixed-size blocks, adds padding to the last block if necessary, and encrypts each block separately.

Given that the entire check is built around the header and fits perfectly into one block, nothing prevents you from putting any necessary data there and using it with plaintext longer than the size encoded in the header.

After that, everything should be simple: there is no stack canary in the binary, PIE is disabled so that you can provide the hardcoded address of the win function as the return address, and that’s it.

0x03 and 0x04: ECB-to-Shellcode

Everything is the same as before, but now you need to write actual shellcode, rather than specifying the correct address of the win function. If you completed the final tasks from the “Binary Exploitation” course, this challenge should be pretty straightforward, since the same approach can be applied here.

0x05: CIMG Screenshots

This challenge further develops the CIMG file format presented in the “Reverse Engineering” module. It adds a directive to create a “screenshot” of the frame buffer and use it as a separate sprite. This is an excellent format change that would make it easier to solve the problems in the above module.

Unfortunately (or fortunately), the implementation of this directive results in a classic buffer overflow: it reads data from the framebuffer back into a fixed-size buffer on the stack without bounds checking, so you can overflow it and use any exploits you want to read the flag.

Just in case you wanted to cut corners and exploit the same vulnerability you used with the load_sprite_from_file directive to read the flag directly, it won’t work since that vulnerability is patched.

0x06: CIMG Screenshots 2

In this version of the CIMG renderer, the load_sprite_from_file implementation has been removed, so you can no longer load anything from a file. Luckily, you don’t need to load the actual shellcode, because there’s a win function that does everything for you. You need to find a way to overflow the buffer and “patch” the return address so that it points to the win function. The only problem is that the framebuffer must contain only printable characters, so you won’t be able to construct any address you want.

But if you inspect the win function itself, you’ll find that the code inside has been copied and pasted multiple times, so you can easily find the address you need that passes all the checks. However, keep in mind that since you overwrote the stack, the rbp register now contains garbage, so you need to find a place in the function where it can somehow be “restored” first.

0x07: Wily Webserver

Another standard buffer overflow vulnerability, but now it’s hidden in an HTTP web server written in C (oh my!).

The server’s concept is quite simple: its only task is to search for the requested files at the specified path in the /challenge/files directory and send back their contents if found. It also performs a few simple checks, such as verifying the file type and size, and ensuring the content doesn’t contain the string pwn.college. So, unfortunately, simply requesting a flag file back won’t work.

The server suffers from a classic “path traversal” problem: even if it tries to find a file in a fixed directory, it doesn’t check the path itself, so nothing is stopping you from requesting something like /../../../../etc/passwd. Now you can request the /flag, but since it contains a forbidden string, the server will return an 403 error.

If you look at the implementation of the send_file function, you’ll see that there’s a fixed-size buffer on the stack that’s used to read files from the file system. The server verifies that the file shouldn’t exceed the buffer size, but then uses the same buffer to form the HTTP response: it writes the headers and the file contents to the same buffer, effectively violating its own verification. Therefore, your goal is to create a file smaller than 8 KB, but it still has to overwrite the buffer because previously written headers occupy space.

The rest is as usual: just put the shellcode into this buffer and find the correct buffer address in memory to redirect the code execution path back to your shellcode.

0x08: The Watering Hole

This is a slight variation on the previous challenge: you have the same vulnerable web server, but its environment has been modified. Specifically, it performs the correct security trick: after binding to port 80, it lowers privileges to nobody:nobody, so you won’t be able to read the flag even if you hijack the process. The flag itself is “hidden” in the cookie value that the /challenge/victim script sends to the server. Therefore, you need to find a way to recover it from another client connection using your exploit.

What I decided to do (and I know it’s not very elegant) is inject the exploit that accepts the next connection, dup2 the client’s socket to stdout, and execute execve('/bin/cat', NULL, NULL). This causes the server to print the subsequent incoming request to stdout, revealing the flag.

0x09: Secure Chat 1

Now, things are not as interesting (in my opinion) because the current task concerns only the Web and SQL. So no more disassemblers.

You’ll receive two Python scripts that set up the environment, launch a “secure” chat on top of it, and simulate the operation by sending multiple requests from different “users.” Ultimately, you’ll have a shell and a working web server responding to HTTP requests. Your task is to trick the chat application and reveal the flag.

This level is easy because the chat has a classic SQL injection vulnerability. You just need to log in as “sharon” and see what she sent to “bob.”

0x0a: Secure Chat 2

While SQL injection remains, the previous vulnerability in which the flag was passed in clear text has been fixed. After Sharon realized she had posted the flag, she deleted her account altogether.

But hey, Bob still knows the flag and wants to share that knowledge with Alice, so we need to intercept that message somehow.

If you analyze the logic, you can see that Alice waits for a message from Mallory by using the “real” browser, rather than simply making raw HTTP calls. Furthermore, the /chat endpoint has a classic XSS vulnerability, allowing any JavaScript to be injected into the message. Therefore, we need first to send Alice a malicious message to obtain her credentials, and then initiate communication with Bob on her behalf.

If we have Alice’s session cookie, communicating with Bob is simple: just copy and paste the same code from the script the challenge uses, and you’ll be able to reveal the flag.

0x0b: Secure Chat 3

So, from my perspective, now the complexity has increased significantly. It’s necessary to apply several methods simultaneously, find bugs and vulnerabilities in the application, crack the encryption, and finally, reveal the flag. I spent several days just thinking about all this and experimenting, and here are my thoughts. I actually think there is a more elegant way to solve this problem, which I haven’t found.

So, first off, the difference between this task and the previous one is that Bob no longer shares the flag. He says that Sharon did it and what her username was before she deleted her account. So, the original message is gone, and Bob no longer shares the flag. What should we do?

If you look closely at the chat-server logic, you’ll notice that there’s a bug (or feature) in the account deletion logic: first, it updates the username by setting the encrypted_username_1 or encrypted_username_2 fields to NULL, and then deletes messages where both usernames are NULL. In the logic emulated by the run script, this isn’t possible, so the message from Sharon to Bob is still in the database, but you can’t access it anymore because one of the username fields is NULL. So the first task is to find a way to get it back.

It seems there’s no way to retrieve the unencrypted message, but there is a way to retrieve the encrypted version. I did this by abusing a database query that updates the username (next to the “delete” clause). It has the same SQL injection vulnerability as the others in the script, allowing us to exploit it. My solution was to update the username with a subquery that selects the encrypted chat and use that value as the username we want to set. This way, the next time you log in, you’ll see the encrypted message as a greeting.

So, now we have the ciphertext. How do we decrypt it? Given that it’s AES ECB, you should know from the “crypto” module that you’ll need one of the following options:

  • a “decryption oracle” that reveals padding errors
  • an “encryption oracle” that allows data to be appended to the original plaintext

Unfortunately, there’s no “decryption oracle,” otherwise everything would be pretty straightforward. But there is an “encryption oracle”: when you change your username, the “oracle” re-encrypts your messages, changing the username in the original plaintext. Essentially, this means you can change your username in any way you like, “adding” any data to it and affecting the ciphertext.

But how can you controllably influence the message Sharon sent to Bob? This is where you’ll need Sharon’s correct username to pull off the following trick: the deleted message still belongs to Bob’s account, but since Sharon sent it, you can’t change the message unless you change Bob’s username to Sharon’s! So, when you “become” Sharon and change your username back to something else, you will also affect the ciphertext.

Given all this, you need to construct the exploit logic (can it really be called an exploit?) in such a way that it allows you to brute-force the ciphertext by determining the block boundaries, and decrypt the plaintext byte by byte. Just as you did in the “AES ECB prefix” challenges in the “crypto” module.

0x0c: Secure Chat 4

Administrator accounts now have additional security: any administrative actions require a PIN. To verify the PIN, the chat-server application runs a separate executable that verifies it. This is a standard binary and has a standard buffer overflow vulnerability. Therefore, you need to create the correct payload to send and call the “win” function, as before. The binary has no security and is compiled as no-PIE, so it’s pretty simple.

0x0d: Secure Chat 5

One line of code changes everything. Now all cookies set by chat-server app are HttpOnly, so it’s impossible to log out the admin session as I did before.

Luckily, XSS and SQL injection are still there, so you can still send any requests on top of Alice’s session. The only problem is that Alice “reads” Mallory’s messages every 5 seconds, so the previous brute-force solution I used to find the flag doesn’t work either: it’s too slow. But if sending one request at a time is too slow, let’s send them in bulk!

My encrypt_data function now encrypts (or rather, adds data to cleartext) an array of required strings instead of just one. The idea I implemented is as follows:

  • I register multiple new usernames for each value I need to encrypt, where the new name is pwn_{value}
  • I generate and send to Alice a JavaScript that
    • for each item to be encrypted, it appends it to Sharon’s username and sets her username to this value, and then, using SQL injection, changes one of the pwn usernames to pwn_{value}_{ciphertext}
    • changes Sharon’s username back to its original value so that it can be used again on the next call
  • Get the new pwn‘s username for each created user, extract the ciphertext, and return a dictionary [value:ciphertext]

Given that I can now encrypt multiple strings simultaneously, the logic for brute-forcing the ciphertext has also changed: I generate a lookup table for each character to be decrypted, then look up the desired value in the resulting dictionary. Furthermore, whereas I previously decrypted the entire message, to reduce time, I now skip the part that is already known (Sharon’s username and the message “the flag is pwn.college”).

It’s still not very fast at decrypting data, taking about 45 minutes to reveal the flag, but it’s much faster than the previous solution.

Have fun!

The challenges are here, and my solutions are here.