Pwn-la-Chapelle

Pwn-la-Chapelle

Popping Shells and Stealing Flags at RWTH Aachen University

Writeup - Deutsche Hacking Meisterschaft 2024: Cute Big Cats

by Trayshar - - Estimated reading time: 6 minutes

A crypto challenge by 0x4D5A and Rugo, solved by Euph0r14 and Trayshar

Challenge

Who doesn’t love Cute Big Cats? But the most beloved cat is the “flag cat”, only visibile to the admin user. Go grab it!

We were given a C# ASP.NET Core web application that implements a website showing pictures of big, fluffy cats. Our job is to get a picture of the elusive “flag cat”, which only the administrator account is said to be capable of…

Landing Page

Exploration

To get any cat pics at all, we first have to register a new account. Then we can view one of the five fluffy-but-not-interesting kitties, selected at random.

The ability to reset one’s password caught our attention: Given a username, a link is generated that changes that users password to an default! Obviously, it refuses to do so for the administrator account.

Each password reset link has a token in it’s URL that encodes the time at which it was issued and the username to reset:

1public string GeneratePWResetLink(string username) {
2    var jsonString = JsonConvert.SerializeObject(new PWResetRequest() {
3        Timestamp = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeMilliseconds(),
4        Username = username
5    });
6
7    return EncryptStringToAes(jsonString).TrimEnd('=').Replace('+', '-').Replace('/', '_');
8}

This data is put into a json object and then encypted using AES128 in CBC-Mode. Since the integrity of the message is not checked, we can meddle with it. But first, let’s make sure we’re on the same page about CBC-Mode decryption.

Cipher Block Chaining Mode

AES is a block cypher, meaning it can only handle a fixed length of data. To encrypt/decrypt a message that is longer, different modes of encyption can be used, one being Cipher Block Chaining Mode (CBC-Mode).

CBC-Mode Decryption

In this mode, when decrypting a message, the previous block’s ciphertext is XORed into the plaintext. Hence, bit flips in one block directly translate to changes in the decrypted plaintext of the next block! But doing this will render the plaintext of the modified block utterly unintelligible…

A First Try

So, we only need to flip some bits in the 16 byte block that is XORed with the block containing the username. Easy enough. Now, let’s examine what those blocks look like in plaintext (for user Admim):

{"Timestamp":172
1063019009,"User
name":"Admim"}

As we have to change the last block, the middle block will become complete nonsense, e.g.

{"Timestamp":172
$Y1x�H�M���&%;G
name:"Admin"}

Sadly, this is not a valid token because the raw JSON contains invalid Unicode, as indicated by an error message. So, what can we do? We cannot influence the decrypted output from the manipulated block; it will always be gibberish. Maybe we could try to contain it inside a string value at least? But how would we be able to do that, given that we cannot use the username and we don’t have any more fields we can utilize? It’s not like we can just add additional fields….

Sloppy JSON Parsing

So, we limit-tested the JSON parser that was used. Luckily for us, it was Newtonsoft, the de-facto standard for this job in C#, and it’s quite lenient with syntax.

We noticed that we can mix double and single quotation marks within the JSON, like {"Num":12,'User':"Guy"}, and it still parses! Since the JSON standard only allows double quotes for defining strings, JSON serializers will escape them but not single quotes inside strings, making single quotes valid unescaped characters for usernames.

The last piece of the puzzle was that in case of recurring keys in the JSON, only the last one will be parsed. Hence, {"User":"IGNORED",'User':"Guy"} will be parsed like {"User":"Guy"}.

We can work with that to craft a payload username!

The payload

We add padding to the username such that all of the corrupted data induced by our bit flips are inside the first username value. After that comes a second username key-value-pair, which refers to the admin account. We change some characters to quotation marks to separate the two key-value pairs using bit flips. We can do this easily since the length and structure of the message are known beforehand.

The result should look like this:

{"Timestamp":172
0641298062,"User
name":"aaaaaaaaa
<corrupted data>
",'Username': "a
dmin"}

Here is our crafted username: aaaaaaaaaaaaaaaaaaaaaaaaa!,'Username': !admin

This chunks to aaaaaaaaa aaaaaaaaaaaaaaaa !,'Username': !a dmin in the final JSON.

As you see, the !,'Username': !a in the username corresponds exactly to the ",'Username': "a in the payload. Most importantly, this means that all the characters we need to change are in a single block, and the block before that is inside a string value, allowing it to be safely corrupted.

The Flag Cat

We register that username, generate a password reset token, and finally flip two bits in the encrypted message so that the exclamation marks become quotation marks.

Now we can redeem the crafted token to reset the admin’s password!

Doing this allows us to finally view the flag cat: Flag Cat

Final Thoughts

This challenge was a lot of fun to work on. We later realized that the “single quote exploit” wasn’t even necessary, as we only used them inside the block we had full control over anyway.

This demonstrates once again that encryption alone, even when done correctly using secure methods, is insufficient for such applications. Always add integrity protection if the client’s data is to be trusted! And don’t use CBC mode!

Solve Script

 1#!/usr/bin/env python3
 2
 3import base64
 4import requests
 5
 6USERNAME = "aaaaaaaaaaaaaaaaaaaaaaaaa!,'Username': !admin"
 7HOST = "https://de36e0786d37faf124dbcca0-1024-cute-big-cats.challenges.dhm-ctf.de"
 8TOKEN = "o7lu5PCQOBvpwyTPeA5e1zObIzReTtV7H7C_l_y3uKHyWjPd_j_OUMroJa6SPvFs96Uii02Dxxhjq3EVqRbFuYWFA73CyHuhwAfLqEq_stcXF0B-WRuOC7OurYGGjm8R"
 9
10def encode(input: bytes) -> str:
11    token: str = base64.b64encode(input).decode()
12    return token.replace("=", "").replace("+", "-").replace("/", "_")
13
14def decode(token: str) -> bytes:
15    match len(token) % 4:
16        case 2: token += "=="
17        case 3: token += "="
18    return base64.b64decode(token, "-_")
19
20assert TOKEN == encode(decode(TOKEN))
21
22# …
rssfacebooktwittergithubyoutubemailspotifylastfminstagramlinkedingooglegoogle-pluspinterestmediumvimeostackoverflowredditquoraquora