Java安全基础

学习一下java中可能出现的一些安全问题具体如何实现的。先学一下部分无安全防护的代码实现,为后面深入学习漏洞原理打个基础

Java文件上传

主要学习Multipartfile和ServletFileUpload两种方式的文件上传,还有什么文件流方式

smartupload组件上传之后再了解。先摸索一下原理

Multipartfile文件上传

Multipartfile是SpringMVC提供简化上传操作的工具类

这里我们就直接拿之前的springboot项目来整就好了

在此之前需要完善一下项目的目录结构,即在main目录下创建个webapp目录,并在webapp目录下创建WEB-INF目录

这里的WEB-INF目录是java web的安全目录,该目录只能由服务端访问,客户端无法访问,目录中应有web.xml文件

确保WEB-INF有web.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>

确保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
<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>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>3.3.1</version>
</dependency>
<!-- tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>

</dependencies>

然后在application.properties添加如下信息

1
2
3
4
5
server.port=8080
# 视图前缀
spring.mvc.view.prefix=/jsp1/
# 视图后缀
spring.mvc.view.suffix=.jsp

确保你的项目结构如下

在webapp/jsp1目录下创建index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</meta>
<title>Hello</title>
</head>
<body>
<h1>Hello!!! jsp</h1>
</body>
</html>

此时启动服务并访问有如下响应

接下来写个multipartfileUpload.jsp用来实现一个文件上传的前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>文件上传页面</title>
</head>
<body>
<h1>文件上传页面</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
选择要上传的文件:<input type="file" name="file"/>
<input type="submit" value="上传"/>
</form>
</body>
</html>

然后要在controller层写一个处理文件上传的代码multipartfileController

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.springboottest.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

@Controller
public class multipartfileController {
@Value("${file.upload.path}")
private String uploadPath;

@PostMapping("/upload")
@ResponseBody
public String create(@RequestPart MultipartFile file) throws Exception{
String fileName = file.getOriginalFilename();
String filePath = uploadPath + fileName;
// 绝对路径 toAbsolutePath()
filePath = Paths.get(filePath).toAbsolutePath().toString();

File dest = new File(filePath);
// 未作上传限制,存在任意文件写入
Files.copy(file.getInputStream(),dest.toPath());
return "Upload success: "+ dest.getAbsolutePath();
}
}

接着在根目录下创建upload文件夹

启动程序,访问/jsp1/multipartfileUpload.jsp

上传文件

查看对应的文件夹就能发现已经成功上传了

ServletFileUpload文件上传

ServletFileUpload文件上传需要依赖commons-fileupload组件

常用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
FileItemFactory 表单项工厂接口
ServletFileUpload 文件上传类,用于解析上传的数据
FileItem 表单项类,表示每一个表单项
boolean ServletFileUpload.isMultipartContent(HttpServletRequest request) 判断当前上传的数据格式是否是多段的格式,只有是多段数据,才能使用该方式
public List<FileItem> parseRequest(HttpServletRequest request) 解析上传的数据,返回包含 表单项的 List 集合

boolean FileItem.isFormField() 判断当前这个表单项,是否是普通的表单项,还是上传的文件类型,true 表示普通类型的表单项;false 表示上传的文件类型

String FileItem.getFieldName() 获取表单项的 name 属性值
String FileItem.getString() 获取当前表单项的值;
String FileItem.getName() 获取上传的文件名
void FileItem.write( file ) 将上传的文件写到 参数 file 所指向存
取的硬盘位置

关于其相关应用可以参考JAVA文件上传 ServletFileUpLoad 实例

Java文件读取与下载

文件读取和下载区别不大,都是可以用来获取到文件的内容和数据的一种方式,不同的是,文件读取是之前讲文件的内容回显输出到响应中,而文件下载则是通过浏览器下载到本地,自己再打开该文件来获取文件内容

关于下载还是读取,可以通过响应头Content-Disposition来控制,它指示了响应是以何种方式来呈现的,通过直接输出还是通过浏览器的附件下载

读取/下载方式

使用java.nio.file.Files读取文件

Files 可以用于读取文件到List,可以将较小文件全部读取到内存中,常见的方法是使用 Files 类将文件的所有内容读入字节数组。 Files 类还有一个方法可以读取所有行到字符串列表。

Files 类是在Java 7 中引入的,如果想加载所有文件内容,使用这个类是比较适合的。只有在处理小文件并且需要加载所有文件内容到内存中时才应使用此方法。

写个NioFiles的类

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
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class NioFiles {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
// Path 是传统给路径的封装层,主要是为了兼容不同的系统,并提供路径接口
Path path = Paths.get(filePath);
byte[] bytes = Files.readAllBytes(path);
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println("使用Files.readAllBytes :");
System.out.println(new String(bytes));
System.out.println("使用Files.readAllLines :");
for (String line : allLines)
{
System.out.println(line);
}


}
}

再单独运行这个java代码,两种Files的函数读取文件结果如下

image-20240720170400846

使用 java.io.FileReader 类读取文件

相较于上一种方式,java.io.FileReader不支持编码方式的指定,并使用默认编码,所有有时候它的文本读取效果不是很好。

也可以使用FileReader来获取BufferedReader,然后用它来逐行读取文件

创建如下内容的IoFileReader

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
import org.springframework.boot.autoconfigure.ssl.SslProperties;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class IoFileReader {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
File file = new File(filePath);
FileReader fileReader = new FileReader(file);
int chart;
System.out.println("使用FileReader :");
while ((chart = fileReader.read()) != -1) {
System.out.print((char) chart);
}
fileReader.close();
// 重新初始化
fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
System.out.println("使用BufferedReader :");
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
fileReader.close();
}
}

可以通过FileReader直接读取文件(int),或者也可以通过BufferedReader直接读取

使用 java.io.BufferedReader 读取文件

java.io.BufferedReader的简单使用刚才已经运用过了

BufferedReader 主要用于逐行读取文件,并且可以读取大文件
BufferedReader 是同步的,也就是说多线程可操作

默认缓冲区大小为: 8KB ,因此可以安全地从多个线程完成对BufferedReader 的读取操作。

创建如下内容的BufferedReaderTest

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.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;


public class BufferedReaderTest {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath, StandardCharsets.UTF_8);
}
private static void readfile(String filePath, Charset charset) throws IOException {
File file = new File(filePath);
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, charset);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
System.out.println("使用BufferedReader :");
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();

}
}

这里先以字节流方式读取文件,在将字节流转化为字符来输出文件内容

InputStreamReader继承自Reader

  • 将输入字节流InputStream(例如:FileInputStream、Socket.getInputStream)转化为字符输入流Reader,从而可以按字符读取输入字节
  • 支持设置字符集编码。在构造InputStreamReader,可以指定编码方式,来将字节转换为相应的字符
1
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, charset);

使用 Scanner 读取文件

Scanner是基于正则来读取文件,该类不同步,不能多线程使用。

但可以用来逐行的读取文件,或者配合java正则表达式来读取文件

Scanner 类使用正则表达式作为分隔标记解析字符串,分隔符模式默认匹配空格。然后可以使用各种下一种方法将得到的标记转换成不同类型的值。Scanner 类不同步,因此不是线程安全的。

创建如下内容的ScannerTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;

public class ScannerTest {
public static void main(String[] args) throws IOException{
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
Path path = Paths.get(filePath);
Scanner scanner = new Scanner(path);
System.out.println("使用Scanner :");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
}
}

使用 RandomAccessFile 断点续传

随机流(RandomAccessFile)不属于IO流。

首先把随机访问的文件对象看作存储在文件系统中的一个大型byte数组,然后通过指向该byte数组的光标或索引(即文件指针 FilePointer)在该数组任意位置读取或写入任意数据。

断点续传原理:

  • 下载断开的时候,记录文件断点的位置 position
  • 继续下载的时候,通过RandomAccessFile 找到之前的position 位置开始下载

创建如下内容的RandomAccessFileTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileTest {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
String str;
while ((str = randomAccessFile.readLine()) != null) {
System.out.println("使用RandAccessfile断点续传 : ");
System.out.println(str);
}
randomAccessFile.close();
}
}

中文乱码是因为RandomAccessFile 读取文件的 readLine 方法默认使用 ISO-8859-1 编码

使用外部库 org.apache.commons.io.FileUtils.readFileToString()

使用commons-io库可以非常简单地实现文件读取

但需要在pom.xml添加依赖:

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

创建如下内容的ApachereadFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ApachereadFile {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
File file = new File(filePath);
System.out.println("使用common0-io读取数据 :");
System.out.println(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
}
}

使用 Files.readString 读取文本

Java 11后出现了Files.readString,也可以来读取文件

创建如下内容的ReadStringTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ReadStringTest {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\HONOR\\Desktop\\1.txt";
readfile(filePath);
}
private static void readfile(String filePath) throws IOException {
Path path = Paths.get(filePath);
String string = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("readString读取文件 : ");
System.out.println(string);
}
}

文件读取的JavaWeb工程

开一个springboot项目,添加如下依赖

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</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
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
package com.example.springboottest.controller;

import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;

@Controller
@ResponseBody
public class webreadfile {
@RequestMapping("/NioFiles")
public String NioFiles(String filename, HttpServletResponse response) throws Exception {
// 使用Files读取所有数据
Path path = Paths.get(filename);
byte[] bytes = Files.readAllBytes(path);
System.out.println("使用Files.readAllBytes读取文件中......");
// true则是以附件的方式下载,flase则是直接在浏览器中显示
boolean resset= true;
if (resset) {
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}
System.out.println(new String(bytes));
// 使用Files读取文件的时候,是读取到字节并在内存的,所以直接return就行
return new String(bytes);

}
@RequestMapping("/IoFileReader")
public void IoFileReader(String filename, HttpServletResponse response) throws Exception {
File file = new File(filename);
FileReader fileReader = new FileReader(file);
int chart;
System.out.println("使用IoFileReader读取文件 :");
boolean resset= true;
if (resset) {
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}
// 输出
PrintWriter out = response.getWriter();
while ((chart = fileReader.read()) != -1) {
out.print((char) chart);
}
fileReader.close();
// BufferedReader bufferedReader = new BufferedReader(fileReader);
// String line;
// System.out.println("使用IoFileReader读取文件 :");
// // 使用FileReader读取文件的时候,是读取组后得到一个字符流,然后将这个字符流输出到response的输出流中
// PrintWriter out = response.getWriter();
// while ((line = bufferedReader.readLine()) != null) {
// out.println(line);
// }
// bufferedReader.close();
}
@RequestMapping("/IoBufferedReader")
public void IoBufferedReader(String filename, HttpServletResponse response) throws Exception {
File file = new File(filename);
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
System.out.println("使用BufferedReader :");
boolean resset= true;
if (resset) {
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}
String line;
PrintWriter writer = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
writer.println(line);
}
bufferedReader.close();
inputStreamReader.close();
}
@RequestMapping("/ScannerTest")
public void ScannerTest(String filename, HttpServletResponse response) throws Exception {
Path path = Paths.get(filename);
Scanner scanner = new Scanner(path);
System.out.println("使用Scanner :");
boolean resset= true;
if (resset) {
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}
PrintWriter writer = response.getWriter();
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
writer.println(line);
// System.out.println(line);
}
scanner.close();
}
@RequestMapping("/ApachereadFile")
public void ApachereadFile(String filename, HttpServletResponse response) throws Exception {
File file = new File(filename);
boolean resset= true;
if (resset) {
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}
System.out.println("使用common0-io读取数据 :");
System.out.println(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
}

}

如图可以通过调用接口来成功以附件的方式下载文件

rest() 方法的主要作用是清空缓冲区中的任何数据,重置响应头,并且为响应设置一些默认的状态。具体来说,它做了以下几件事情:

  • 清空缓冲区: 如果之前已经通过 getWriter() 或getOutputStream() 获取了输出流并写入了一些数据, reset() 会清空这些数据,以确保响应是一个干净的、空白的状态。
  • 重置响应头: reset() 方法会清除之前设置的响应头,将响应头恢复为默认状态。
  • 设置默认状态: reset() 会将响应状态码重置为默认值,通常是 200 OK

Nio或io的区别

Java 中的 I/O(Input/Output)包含传统的 I/O(使用 java.io 包)和 NIO(New I/O,使用 java.nio 包)。以下是它们之间的主要区别:

1.阻塞 vs 非阻塞:

  • I/O( ): 传统的 I/O 操作是阻塞的。当一个线程执行读/写操作时,它会被阻塞,直到数据完全读取或写入完成。

  • NIO(java.nio): NIO 提供了非阻塞的 I/O 操作。这意味着线程可以在等待数据就绪的同时执行其他任务,而不必一直等待。

2.通道与缓冲区:

  • I/O: 传统的 I/O 使用流(InputStream 和 OutputStream)。它们是单向的,而且通常是字节流或字符流。
  • NIO: NIO 引入了通道(Channel)和缓冲区(Buffer)的概念。通道是双向的,而缓冲区可以读取和写入数据。

3.选择器(Selectors):

NIO: NIO 提供了选择器(Selectors),允许单个线程同时管理多个通道。通过
选择器,可以实现单线程处理多个连接的高并发性能。

4.内存映射文件:

NIO: NIO 提供了对文件进行内存映射的功能。这意味着可以直接在内存中操作文
件,而不必通过传统的读取和写入方法。

5.性能和扩展性:

NIO: 由于非阻塞和选择器的机制,NIO 在处理大量连接时通常比传统 I/O 更具有性能和扩展性。

6.适用场景:

  • I/O: 适用于较简单的同步 I/O 操作,适合于连接数较少的场景。
  • NIO: 适用于需要处理大量连接并实现高并发的场景,例如网络编程、服务器编程等。

总的来说,NIO 提供了更为灵活和高效的 I/O 操作,特别适用于需要处理大量并发连接的应用场景。但对于简单的 I/O 操作,传统的 I/O 也是足够的。选择使用哪种取决于具体的应用需求。

Java命令执行

同PHP中的systemeval等函数差不多,Java中也有JDK原生提供的命令执行方法,它们分别是:

  • java.lang.Runtime
  • java.lang.ProcessBuilder
  • java.lang.UNIXProcess/ProcessImpl (需要反射调用)

java.lang :https://docs.oracle.com/javase/7/docs/api/java/lang/package-summary.html

java.lang.Runtime

Runtime 是java.lang 中的一个类,主要是与操作系统交互执行操作命令。
而在java.lang.Runtime 中的exec() 方法,可以用来执行具体的命令,而执行 exec()方法有以下六种形式(重载),如下图所示:

这里主要关注exec(String command)和exec(String[] cmdarray)这两种执行方式

exec(String command)

看代码就懂了,无需多言

1
2
String command = "calc";
Runtime.getRuntime().exec(command);

exec(String[] cmdarray)

1
2
String[] command = {"cmd","/c","whoami"};
Runtime.getRuntime().exec(command);

构造回显

同php中的命令执行函数不同的是,这些java函数都没有回显,实战中经常需要通过dnslog等外带来判断该漏洞是否可以执行命令,得到命令的输出

为了直观地看到命令的结果,这里将结果通过getInputStream 和getErrorStream 用 BufferedReader 生成输出流,然后输出

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class RuntimeTest {
public static void main(String[] args) throws IOException {
String cmdString = "cmd /c dir";
String cmdArgs[] = {"cmd", "/c", "ping", "www.baidu.com"};

cmdStringExec(cmdString);
cmdArgsExec(cmdArgs);
}
private static void cmdStringExec(String cmdString) throws IOException {
String line;
Runtime runtime = Runtime.getRuntime();
Process exec = runtime.exec(cmdString);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream(), "GBK"));
BufferedReader bufferedErrorReader = new BufferedReader(new InputStreamReader(exec.getErrorStream(), "GBK"));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = bufferedErrorReader.readLine()) != null) {
System.out.println(line);
}
}
private static void cmdArgsExec(String[] cmdArgs) throws IOException {
String line;
Runtime runtime = Runtime.getRuntime();
Process exec = runtime.exec(cmdArgs);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream(), "GBK"));
BufferedReader bufferedErrorReader = new BufferedReader(new InputStreamReader(exec.getErrorStream(), "GBK"));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = bufferedErrorReader.readLine()) != null) {
System.out.println(line);
}
}
}

使用 BufferedReader 的时候,命令并不是等待执行完之后得到的,而是一边执行,一边输出的。

以执行ping为例,它是逐渐输出结果的,而不是等代码执行完成才全部输出

java.lang.ProcessBuilder

java.lang.ProcessBuilder也可以来执行命令

如图,它有两种传参方式一种是传入字符串,一种是字符串列表

示例代码:

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class BuilderTest {
public static void main(String[] args) throws IOException{
List<String> cmdlists = new ArrayList<String>();
cmdlists.add("cmd");
cmdlists.add("/c");
cmdlists.add("ping www.baidu.com");
ProcessBuilder builder = new ProcessBuilder(cmdlists);
// ProcessBuilder builder = new ProcessBuilder().command("calc.exe");
Process process = builder.start();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
BufferedReader bufferErrorReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "GBK"));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = bufferErrorReader.readLine()) != null) {
System.out.println(line);
}
}
}

java.lang.UNIXProcess/ProcessImpl

再JDK9的时候UNIXProcess被合并到了 ProcessImpl 当中

同时,相比上面两种方法,这种方法太底层了,无法直接调用,需要使用反射来调用

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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Map;

public class ProcessImplExecTest {
public static void main(String[] args) throws Exception {
String[] cmds = {"cmd.exe","/c","ping www.baidu.com"};
Class c = Class.forName("java.lang.ProcessImpl");
Method m = c.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
m.setAccessible(true);
Process invoke = (Process) m.invoke(null, cmds, null, ".", null, true);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(invoke.getInputStream(), "GBK"));
BufferedReader buffErrorReader = new BufferedReader(new InputStreamReader(invoke.getErrorStream(), "GBK"));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
while ((line = buffErrorReader.readLine()) != null) {
System.out.println(line);
}
}
}

但在Java 9之后,Oracle引入了一个新的系统,称为Java Platform Module System(JPMS),它使得Java核心库内部的类和接口不再对默认情况下公开可见,除非明确声明为”open”。这是为了提供更好的封装和安全性。

也意味着通过反射来访问 java.lang.ProcessImpl.start 方法,这在Java 9及以上版本中是不允许的,因为 java.lang 包没有明确声明为”open”。

需要添加一个命令行选项到您的JVM启动参数,来开放这个模块:

1
--add-opens java.base/java.lang=ALL-UNNAMED

否则就会报错:

命令执行的JavaWeb工程

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

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;

@Controller
@ResponseBody
public class webruntime {
@RequestMapping("/execRuntimeString")
public void execRuntimeString(String cmd, HttpServletResponse response) throws IOException {
String line;
Process exec = Runtime.getRuntime().exec(cmd);
response.setContentType("text/html;charset=GBK");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream(), "GBK"));
BufferedReader bufferedErrorReader = new BufferedReader(new InputStreamReader(exec.getErrorStream(), "GBK"));
PrintWriter writer = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
writer.println(line);
System.out.println(line);
}
while ((line = bufferedErrorReader.readLine()) != null) {
writer.println(line);
System.out.println(line);
}
}
}

Java数据库操作

Java中常见的数据库操作方式有:

  • JDBC :比较原生繁琐
  • Mybatis:比较便捷主流

JDBC使用

Java 数据库连接,(Java Database Connectivity,简称JDBC)是Java 语言中用来规范客户端程序如何来访问数据库的应用程序接口(位于jdk 的java.sql 中)。

通常说的JDBC 是面向关系型数据库的,提供了诸如查询、更新、删除、增加数据库中数据的方法。在使用时候需要导入具体的jar 包,不同数据库需要导入的jar 包不同。

JDBC 与MySQL 进行连接交互,通常为以下6 个流程:

  • 注册驱动 (仅仅做一次)

  • 建立连接(Connection)

  • 创建运行 SQL 的语句(Statement)

  • 运行语句

  • 处理运行结果(ResultSet)

  • 释放资源

先创建个数据库:

1
2
3
4
5
6
7
8
9
10
11
CREATE DATABASE jdbcdemo;
USE jdbcdemo;
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
`username` varchar(25) NOT NULL COMMENT '用户名',
`password` varchar(25) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO user(id,username,password) VALUES (1, "root1", "roo1");
INSERT INTO user(id,username,password) VALUES (2, "root", "root");
INSERT INTO user(id,username,password) VALUES (3, "root2", "root2");

新建个spring项目,勾上依赖SQL->JDBC API,MYsql Driver

注册驱动:

在注册数据库驱动时,虽然

1
DriverManager.registerDriver(newcom.mysql.jdbc.Driver()) 

方法可以完成,但会使数据库驱动被注册两次。这是因为Driver 类的源码中,已经在静态代码块中完成了数据库驱动的注册。所以,为了避免数据库驱动被重复注册,我们只需要在程序中加载驱动类即可,具体加载方式如下所示:

1
Class.forName("com.mysql.jdbc.Driver");

示例代码如下:

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.jdbcdemo;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.*;

public class JdbcDemo {
public static void main(String[] args) throws SQLException, ClassNotFoundException{
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/jdbcdemo?useUnicode=true&characterEncoding=utf8";
String username = "root";
String password = "1234qwer";
Connection conn = DriverManager.getConnection(url, username, password);
Statement statement = conn.createStatement();
String sql = "SELECT * FROM user";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()){
System.out.println("=============================");
System.out.println(resultSet.getInt("id"));
System.out.println(resultSet.getString("username"));
System.out.println(resultSet.getString("password"));
}
resultSet.close();
statement.close();
conn.close();
}
}

Mybatis使用

依赖项:

Web → Spring boot

SQL → JDBC API

SQL → Mybatis Framework

SQL→ Mysql Driver

数据库就沿用之前的就好了

在项目文件夹(Application 的路径)下创建如下文件夹:

  • controller :Controller 层
  • service :业务层

​ impl : service 层的实现

  • entity : 实体类,用来和数据做映射
  • mapper : 数据操作层 DAO

如图:

同时在resources 下也创建一个mapper 文件夹

1.在entity 下建立一个 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
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.jdbcdemo.entity;
import java.security.PrivateKey;
public class User {
private int id;
private String username;
private String password;

public int getId() {
return id;
}

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

public void setId(int id) {
this.id = id;
}


public void setUsername(String username) {
this.username = username;
}


public void setPassword(String password) {
this.password= password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}

2.在mapper 中建立 UserMapper的接口

使用 @Mapper 注解使得 MyBatis 在运行时自动创建该接口的实现。

调用该接口时,就会使用与xml 对应的语句,然后返回结果。

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

import com.example.jdbcdemo.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
//添加用户
public boolean addUser(User user);
//删除用户
public boolean delUser(String username);
//修改用户
public boolean updateUser(User user);
//查询用户
public User getUser(int id);
//查询所有用户
public List<User> getAllUser();

}

3.在src.main.resources.mapper 中创建UserMapper.xml 文件

创建UserMapper.xml 文件,用于和 dao 层映射绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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.jdbcdemo.mapper.UserMapper" >
<!-- 添加用户 -->
<insert id="addUser" parameterType="com.example.jdbcdemo.entity.User" >
INSERT INTO user(id,name,password) VALUES (#{id},#{userName},#{password})
</insert>
<!-- 删除用户 -->
<delete id="delUser" parameterType="String" >
DELETE FROM user WHERE name=#{name}
</delete>
<!-- 修改用户 -->
<update id="updateUser" parameterType="com.example.jdbcdemo.entity.User" >
UPDATE user SET name=#{userName},password=#{password} WHERE id=#{id}
</update>
<!-- 根据id查用户 -->
<select id="getUser" parameterType="int" resultType="com.example.jdbcdemo.entity.User" >
SELECT * FROM user WHERE id=#{id}
</select>
<!-- 查询全部数据 -->
<select id="getAllUser" resultType="com.example.jdbcdemo.entity.User" >
SELECT * FROM user
</select>
</mapper>

4.在Service 中创建一个 UserService 的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.jdbcdemo.service.impl;

import com.example.jdbcdemo.entity.User;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public interface UserService {
public boolean addUser(User user);
public boolean delUser(String username);
public boolean updateUser(User user);
public User getUser(int id);
public List<User> getAllUser();
}

然后在impl 中新建一个UserServiceImpl 的对象,实现UserService 接口

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.jdbcdemo.service.impl;

import com.example.jdbcdemo.entity.User;
import com.example.jdbcdemo.mapper.UserMapper;
import com.example.jdbcdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}

@Override
public boolean addUser(User user) {
boolean flag = false;
flag = userMapper.addUser(user);
return flag;
}

@Override
public boolean delUser(String username) {
boolean flag = false;
flag = userMapper.delUser(username);
return flag;
}

@Override
public boolean updateUser(User user) {
boolean flag = false;
flag = userMapper.updateUser(user);
return flag;
}
@Override
public User getUser(int id) {
User user = null;
user = userMapper.getUser(id);
return user;
}
@Override
public List<User> getAllUser() {
List<User> list = null;
list = userMapper.getAllUser();
return list;
}

}

5.在Controller 层中创建UserController 的类

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

import com.example.jdbcdemo.entity.User;
import com.example.jdbcdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RestController
public class UserController {
private final UserService service;
@Autowired
public UserController(UserService service) {
this.service = service;
}

// 添加用户
@RequestMapping(value = "/addUser", method = RequestMethod.POST)
@ResponseBody
public String addUser(User user) {
boolean flag = service.addUser(user);
if (flag) {
return "Success";
} else {
return "False";
}
}

// 删除用户
@RequestMapping(value = "/delUser", method = RequestMethod.POST)
@ResponseBody
public String delUser(String username) {
boolean flag = service.delUser(username);
if (flag) {
return "Success";
} else {
return "False";
}
}

// 修改用户
@RequestMapping(value = "/updateUser", method = RequestMethod.POST)
@ResponseBody
public String updateUser(User user) {
boolean flag = service.updateUser(user);
if (flag) {
return "Success";
} else {
return "False";
}
}
// 获取用户
@RequestMapping(value = "/getUser/{id}", method = RequestMethod.GET)
@ResponseBody
public User getUser(@PathVariable("id") int id) {
return service.getUser(id);
}
// 获取所有用户
@RequestMapping(value = "/getAllUser", method = RequestMethod.GET)
@ResponseBody
public List<User> getAllUser() {
return service.getAllUser();
}
}

6.最后在application.properties 中添加配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.application.name=JDBCdemo
server.port=8080

# mybatis 配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.jdbcdemo.entity

spring.datasource.url=jdbc:mysql://localhost:3306/jdbcdemo?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=1234qwer
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.log-prefix=[Mybatis]

Java反射基础

反射-1-forName

不同于php等其他语言,java拥有反射这一特性,Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。

p牛对反射的解释:

对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语⾔言附加上动态特性。

通过代码来简单理解理解~

1
2
3
4
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
  • 获取类的方法: forName
  • 实例例化类对象的方法: newInstance
  • 获取函数的方法:getMethod
  • 执行函数的方法: invoke

如何获取到类

  • obj.getClass() 如果上下文中存在某个类的实例obj ,那么我们可以直接通过obj.getClass() 来获取它的类。准确的说是返回调用该方法的对象的运行时类对象(Runtime Class Object)。也就是说,它返回的是调用这个方法的对象所属的类的 Class 对象。

  • Test.class 如果你已经加载了某个类,只是想获取到它的java.lang.Class 对象,那么就直接

    拿它的class 属性即可。这个方法其实不不属于反射。

  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使用forName 来获取

同时,ClassLoader.getSystemClassLoader().loadClass(“java.lang.Runtime”) 类似的利用类加载机制,也可以获取 Class 对象

获取Runtime类Class对象代码片段:

1
2
3
4
String className     = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象了

后面会提到:反射调用内部类的时候需要使用$来代替.,如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.anbai.Test$Hello

forName

forName有两个函数重载:

1
2
Class<?> forName(String name)
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)

第一个就是我们最常见的获取class的方式,其实可以理理解为第二种方式的一个封装:

1
2
3
Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

默认情况下, forName 的第⼀个参数是类名;第二个参数表示是否初始化;第三个参数就
是ClassLoader 。
ClassLoader 是什么呢?它就是⼀个“加载器器”,告诉Java虚拟机如何加载这个类。

Java默认的ClassLoader 就是根据类名来加载类,这个类名是类完整路路径,如java.lang.Runtime

获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:

1
2
Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class

类的初始化顺序

不妨先运行一下p牛给的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
public static void main(String[] args) {
TrainPrint train = new TrainPrint(); // 创建 TrainPrint 类的新实例
}
}

可以看到,首先调用的是static {} ,其次是{} ,最后是构造函数
其中, static {} 就是在“类初始化”的时候调用的,而{} 中的代码会放在构造函数的super() 后面,但在当前构造函数内容的前面。
所以说, forName 中的initialize=true 其实就是告诉Java虚拟机是否执行”类初始化“。

Person p = new Person(“zhangsan”,20); 该句话都做了什么事情?

1,因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。

2,执行该类中的static代码块,如果有的话,给Person.class类进行初始化。

3,在堆内存中开辟空间,分配内存地址。

4,在堆内存中建立对象的特有属性。并进行默认初始化。

5,对属性进行显示初始化。

6,对对象进行构造代码块初始化。

7,对对象进行对应的构造函数初始化。

8,将内存地址付给栈内存中的p变量。

简单利用

那么,假设我们有如下函数,其中函数的参数name可控:

1
2
3
public void ref(String name) throws Exception {
Class.forName(name);
}

我们就可以编写⼀个恶意类,将恶意代码放置在static {} 中,从而执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

通过反射无需实例调用

1
2
3
4
5
6
public class hey {
public String name="Hey";
public void get_name(){
System.out.print("good job! "+name+", You've successfully used reflections\n");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class test {
public static void main(String[] args) throws Exception{
execute("hey", "get_name");
}

public static void execute(String a, String b) throws Exception{
Class clazz = null;
clazz = Class.forName(a);
clazz.getMethod(b).invoke(clazz.newInstance());
}
}

反射-2-单例模式中静态方法利用

我们可以使用forName加载任意类,而不需要import,这样对于我们的攻击者来说就十分有利。

class.newInstance() 可以调用类中的无参构造函数 但是有些情况是无法使用的

  • 因为可能使用的类没有无参构造函数
  • 构造函数是私有的(单例情况

加载内部类

有时候会看到类名的部分包含$符号,它的作用是查找内部类

Java的普通类C1 中支持编写内部类C2 ,而在编译的时候,会生成两个文件: C1.classC1$C2.class ,我们可以把他们看作两个无关的类,通过Class.forName("C1$C2") 即可加载这个内部类。

newInstance()

class.newInstance() 可以调用类中的无参构造函数 ,但是有些情况是无法使用的:

  • 因为可能使用的类没有无参构造函数
  • 构造函数是私有的(单例情况)

当类的构造方法是私有时,但它是单例模式(以Runtime为例),我们无法成功利用下面的代码来执行命令

比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连
接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来
获取:

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

那就没有办法去执行私有方法了吗?还可以利用 getMethodinvoke 方通过Runtime.getRuntime() (静态方法)来获取到Runtime 对象

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

拆分来看就是:

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类
  • 这也比较好理解了,我们正常执行方法是[1].method([2], [3], [4]…) ,其实在反射里就是method.invoke([1], [2], [3], [4]…)
  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了,我们正常执行方法是[1].method([2], [3], [4]...) ,其实在反射里就是method.invoke([1], [2], [3], [4]...)

反射-3-参数的构造

getConstructor

getConstructor可以帮助我们解决:如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类
呢? 该问题

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)
clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

相比之前的

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

这里用来执行命令的方式ProcessBuilder,它需要调用start() 来执行命令

而这里getConstructor作用和getMethod类似,它接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。

1
2
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)

前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

通过getMethod("start") 获取到start方法,然后invoke 执行, invoke 的第一个参数就是ProcessBuilder Object

  • public ProcessBuilder(String… command)

也可以用这种方式来构造

... 这样的语法来表示“这个函数的参数个数是可变的,String...也就是可变长参数(varargs)

对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):

1
2
public void hello(String[] names) {}
public void hello(String...names) {}

那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。所以,我们将字符串数组的类String[].class 传给getConstructor ,获取ProcessBuilder 的第二种构造函数:

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();

getDeclared

这个可以用来解决:如果一个方法或构造方法是私有方法,我们是否能执行它呢?

它与普通的getMethod 、getConstructor 区别是:

1
2
getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

getDeclaredMethod 的具体用法和getMethod 类似, getDeclaredConstructor 的具体用法和getConstructor 类似

前文我们说过Runtime这个类的构造函数是私有的,我们需要用Runtime.getRuntime() 来获取对象。其实现在我们也可以直接用getDeclaredConstructor 来获取这个私有的构造方法来实例化对象,进而执行命令:

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

这里使用了一个方法setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用setAccessible 修改它的作用域,否则仍然不能调用。

反射Runtime执行本地命令代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);

// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(org.apache.commons.io.IOUtils.toString(in, "UTF-8"));

反射调用Runtime实现本地命令执行的流程如下:

  1. 反射获取Runtime类对象(Class.forName("java.lang.Runtime"))。
  2. 使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()),因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))。
  3. 获取Runtime类的exec(String)方法(runtimeClass1.getMethod("exec", String.class);)。
  4. 调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。

Java序列化与反序列化

序列化

序列化就是将对象变成字符序列的过程,这样有利于保存和传输。

ObjectOutputStream类的writeObject()方法可以用来实现序列化

writeObject():将指定对象写入ObjectOutputStream

ObjectOutputStream (Java Platform SE 8 )

一个对象序列化的必要条件:

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

反序列化

反序列化就是将字符序列变成对象的过程

使用的是ObjectInputStream类的readObject()方法

ObjectInputStream (Java Platform SE 8 )

I/O流

常见输入输出流:

1.文件字节流:

1
2
FileInputStream fis = new FileInputStream("D:\\test.txt");
FileOutputStream fos = new FileOutputStream("D:\\test2.txt");

2.缓冲字节流:

1
2
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\test.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\test2.txt"));

3.缓存字符流:

1
2
BufferedReader br = new BufferedReader(new FileReader("D:\\test.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\test2.txt"));

4.字节流

1
2
3
byte[] byteArray = new byte[1024];
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
ByteArrayOutputStream baos = new ByteArrayOutputStream();

5.序列化和反序列化流

1
2
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\test.txt"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\test.txt"));

6.CharArrayReader和CharArrayWriter:用于在内存中读写字符数组

1
2
3
char[] charArray = new char[1024];
CharArrayReader car = new CharArrayReader(charArray);
CharArrayWriter caw = new CharArrayWriter();

7.StringReader和StringWriter:用于在内存中读写字符串

1
2
StringReader sr = new StringReader("hello world");
StringWriter sw = new StringWriter();

Demo

编写一个redrock 的类,然后将其序列化/反序列化到/从文件/base64中

四个函数:

  • 将对象序列化到文件
  • 从文件内容反序列化到对象
  • 将对象序列化后base64 输出
  • 将base64 解码然后反序列化
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
package com.example.jdbcdemo;

import java.io.*;
import java.util.Base64;

public class serdemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 初始化类
Redrock redrock = new Redrock();
redrock.Org = "SRE";
redrock.Num = 114514;
System.out.println("打印原版Redrock:");
System.out.println(redrock);
String fileName = "./RedrockSRE.bin";
SerialToFile(redrock, fileName);
Redrock redrockFromFile = (Redrock) UnserialFromFile(fileName);
System.out.println("反序列化后Redrock:");
System.out.println(redrockFromFile);
System.out.println("反序列化后Redrock并base64编码:");
String RedrockSer = SerialToBase64(redrock);
System.out.println("将base64编码的文件反序列化后:");
System.out.println(UnserialFromBase64(RedrockSer));
}

private static Object UnserialFromBase64(String base64Msg) throws IOException, ClassNotFoundException{
Base64.Decoder decoder = Base64.getDecoder();
byte[] msg = decoder.decode(base64Msg);
ByteArrayInputStream bis = new ByteArrayInputStream(msg);
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}

private static String SerialToBase64(Object o) throws IOException{
// 创建一个byteArrayOutputStream对象,是对应内存而言,是一个往外写的输出字符流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 创建一个对象输出流,然后把对象输出到内存中(即fileOutputStream中)
ObjectOutputStream oos = new ObjectOutputStream(bos);
// 将对象写入到对象输出流中
oos.writeObject(o);
Base64.Encoder encoder = Base64.getEncoder();
byte[] msg = encoder.encode(bos.toByteArray());
System.out.println(new String(msg));
return new String(msg);
}

private static Object UnserialFromFile(String fileName) throws IOException, ClassNotFoundException{
// 打开文件流,然后输入
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
//读取这个输入流,然后返回
return ois.readObject();
}

private static void SerialToFile(Object o, String fileName) throws IOException{
// 准备写入的文件输出流
FileOutputStream fos = new FileOutputStream(fileName);
// 文件流的内容来自于类输出流
ObjectOutputStream oos = new ObjectOutputStream(fos);
// 之后就是在输出流中放入对象
oos.writeObject(o);
}

}

在创建一个Redrock类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.jdbcdemo;

import java.io.Serializable;

public class Redrock implements Serializable {

public String Org;
public int Num;

@Override
public String toString() {
return "Redrock{" +
"Org='" + Org + '\'' +
", Num=" + Num +
'}';
}
}

base64 编码后反序列化特征为rO0AB,二进制头部为:ACED

Java RMI基础

​ RMI(Remote Method Invocation,远程方法调用)是Java 的一组拥护开发分布式应用程序的API。RMI 使用 Java 语言接口定义了远程对象,它集合了Java 序列化和Java 远程方法协议(Java Remote MethodProtocol)

简单地说,原先的程序仅能在同一操作系统的方法调用,通过RMI 可以变成在不同操作系统之间对程序中方法的调用。
RMI 依赖的通信协议是 JRMP。RMI 对象是通过序列化方式进行传输的。

JRMP: Java 远程方法协议(Java Remote Method Protocol,JRMP),是特定于Java 技术的、用于查找和引用远程对象的协议。

RMI 说明

既然RMI需要进行远程通信,那就不可避免的会涉及到客户端与服务端这两类对象,通常情况下,服务端通过监听一个端口,然后客户端去通过RMI的通信协议去访问该端口,服务端接受客户端的请求并作出响应,以此来达成远程方法调用的目的

实际在使用的时候,服务端可能需要有多个服务分别监听对应的多个端口,这时候就需要有个可以管理每个服务(方法)对应哪一端口的服务,也就是注册中心。每个服务(方法)都在注册中心注册与端口的对应关系,之后客户端便能通过注册中心,使用方法名称就可以得到这个服务监听的端口

通常服务端与注册中心是在同一个主机上的,所以注册中心只需要指定端口的对应服务即可

RMI 中的三个角色

RMI 中的三个角色:

  • 服务端 Server :负责将远程对象绑定至注册中心
  • 客户端 Client :服务端会将远程对象绑定至此。客户端会向注册中心查询绑定的远程对象
  • 注册中心 Registry :与注册中心和服务端交互

具体关系如图:

  • 存根/桩(Stub):客户端侧的代理,每个远程对象都包含一个代理对象stub,当运行在本地Java 虚拟机上的程序调用运行在远程Java 虚拟机上的对象方法时,它首先在本地创建该对象的代理对象stub, 然后调用代理对象上匹配的方法。
  • 骨架(Skeleton):服务端侧的代理,用于读取stub 传递的方法参数,调用服务器方的
    实际对象方法, 并接收方法执行后的返回值。

Demo

先实现一个Server ,也就是先写一个对外开放的接口,供客户端调用:
IRemoteObj.java

1
2
3
4
5
6
7
8
9
package com.example.rmidemo;

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

public interface IRemoteObj extends Remote {
public String sayHello(String name) throws RemoteException;
}

然后根据RMI的要求实现这个接口,具体就是写个类,需要继承UnicastRemoteObject

IremoteObjImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.rmidemo;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IRemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
protected IRemoteObjImpl() throws RemoteException{

}
@Override
public String sayHello(String name) throws RuntimeException {
String upward = name.toUpperCase();
System.out.println(upward);
return upward;
}
}

然后就写个注册中心,并将对象绑定上去

在下面第一步写好实现类时,就已经申请端口并监听端口了

在RMI中,远程对象通常会使用RMI系统自动分配的端口进行通信

RmiRegistry.java

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

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiRegistry {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
// 申请一个需要运行的实例,用接口表示
IRemoteObjImpl iRemoteObj = new IRemoteObjImpl();
// Remote stub = UnicastRemoteObject.exportObject(iRemoteObj, 5000); // 指定5000为服务端口
// 申请注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定一个名字给这个对象
registry.bind("IRemoteObj", iRemoteObj);
// registry.bind("IRemoteObj", stub); // 绑定已导出的远程对象
}
}

最后在编写一下客户端,调用一下服务端

RmiClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.jdbcdemo;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiClient {
public static void main(String[] args) throws Exception{
// 通过端口得到一个注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj iRemoteObj = (IRemoteObj) registry.lookup("IRemoteObj");
String result = iRemoteObj.sayHello("World");
System.out.printf(result);
}
}

先起服务端,在起客户端调用

Java JNDI基础

什么是JNDI

JNDI(Java Naming and Directory Interface,Java 命名和目录接口)是一种标准的Java 命名系统接口。

JNDI 提供统一的客户端API,通过不同的访问提供者接口 JNDI 服务供应接口(SPI)的实现,由管理者将JNDI API 映射为特定的命名服务和目录系统,使得Java 应用程序可以和这些命名服务和目录服务之间进行交互。

目录服务是命名服务的一种自然扩展。JNDI 可以访问的目录及服务,比如:DNS、LDAP、CORBA 对象服务、RMI 等等。

例如 对外提供了服务的RMI,JNDI 可以通过相关API 可以链接处理这些服务。

JNDI 的五个包

javax.naming

它包含了命名服务的类和接口。比如其中定义了Context 接口,可以用于查找、绑定/解除绑定、重命名对象以及创建和销毁子上下文等操作。

  • 查找

    最常用的操作是lookup(),向lookup()提供想要查找的对象的名称,它会返回与该名称绑定的对象

  • 绑定

    listBindings():返回一个名字到对象的绑定的枚举。绑定是一个元组,包含绑定对象的名称、对象的类的名称和对象本身

  • 列表

    list()listBindings()类似,只是它返回一个包含对象名称和对象类名称的名称枚举

    list()对于诸如浏览器等想要发现上下文中绑定的对象的信息但又不需要所有实际对象的应用程序来说非常有用

  • 引用

    在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储

InitialContext 类

构造方法:

1
2
3
4
5
6
InitialContext()
// 构建初始上下文
InitialContext(boolean lazy)
// 构建一个初始上下文,并选择不初始化它
InitialContext(Hashtable<?,?> environment)
// 使用提供的环境构建初始上下文

常用方法:

1
2
3
4
5
bind(Name name, Object obj) // 将名称绑定到对象
list(String name) // 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
lookup(String name) // 检索命名对象
rebind(String name, Object obj) // 将名称绑定到对象,覆盖任何现有绑定
unbind(String name) //取消绑定命名对象

Reference 类

构造方法:

1
2
3
4
5
6
7
8
Reference(String className)
// 为类名为"className"的对象构造一个新的引用
Reference(String className, RefAddr addr)
// 为类名为“className”的对象和地址构造一个新引用
Reference(String className, RefAddr addr, String factory, String factoryLocation)
// 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用
Reference(String className, String factory, String factoryLocation)
//

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void add(int posn, RefAddr addr)
// 将地址添加到索引posn的地址列表中
void add(RefAddr addr)
// 将地址添加到地址列表末尾
void clear()
// 从此引用中删除所有地址
RedAddr get(int posn)
// 检索索引posn上的地址
RefAddr get(String addrType)
// 检索地址类型为“addrType”的第一个地址
Enumeration<RefAddr> getAll()
// 检索本参考文献中地址的列举
String getClassName()
// 检索引用引用的对象的类名
String getFactoryClassLocation()
// 检索此引用引用的对象的工厂位置
String getFactoryClassName()
// 检索此引用引用对象的工厂的类名。
Object remove(int posn)
// 从地址列表中删除索引posn上的地址
int size()
// 检索此引用中的地址数
String toString()
// 生成此引用的字符串表示形式

官方文档介绍:Naming Package (The Java™ Tutorials > Java Naming and Directory Interface > Overview of JNDI)

javax.naming.directory

继承了javax.naming,提供了除命名服务外访问目录服务的功能。

可以参考:Directory and LDAP Packages (The Java™ Tutorials > Java Naming and Directory Interface > Overview of JNDI)

javax.naming.ldap

继承了javax.naming,提供了访问LDAP 的能力。

可以参考:Directory and LDAP Packages (The Java™ Tutorials > Java Naming and Directory Interface > Overview of JNDI)

javax.naming.event

包含了用于支持命名和目录服务中的事件通知的类和接口。

Event and Service Provider Packages (The Java™ Tutorials > Java Naming and Directory Interface > Overview of JNDI)

javax.naming.spi

允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI 可以访问相关服务。

Event and Service Provider Packages (The Java™ Tutorials > Java Naming and Directory Interface > Overview of JNDI)

JNDI 操作RMI

直接在之前RMI基础的代码中进行修改就好了

注意要将IremoteObjImpl.java的protect IRemoteObjImpl()改成public

创建一个jndidemo的包,并创建jndiserverjndiclient 的类

jndiserver.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.rmidemo.jndidemo;

import com.example.rmidemo.IRemoteObjImpl;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class jndiserver {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
System.setProperty("java.rmi.server.hostname", "127.0.0.1");
initialContext.rebind("rmi://127.0.0.1:1099/IRemoteObj", new IRemoteObjImpl());
System.out.println("JNDI Server ready");

}

}

jndiclient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.rmidemo.jndidemo;

import com.example.rmidemo.IRemoteObj;

import javax.naming.InitialContext;

public class jndiclient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/IRemoteObj");
System.out.println(remoteObj.sayHello("JNDI Client Hello"));
}
}

RMIserve -> jndiServer -> JndiClient 依次运行


Java安全基础
https://www.smal1.black/Java安全基础.html
作者
Small Black
发布于
2024年10月21日
许可协议