代码审计(Java)

概述

由于工作需要,开始接触代码审计的相关工作。因为之前对这方面并没有很清晰的概念,所以特地学习下

Web应用安全的核心问题

用户提交的数据不可信是Web应用程序核心安全问题。

SQL注入

原理

合法输入

1
2
id=1
SELECT * FROM users WHRER id='1';

恶意输入

1
2
id=1' or '1'='1
SELECT * FROM users WHRER id='1' or 'a'='a';

Java代码分析

不合规代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class SQLInject {
public static void main(String[] args)throws Exception{
//正常输入
select("1");
// 恶意输入
select("' or 'a'='a");
}
public static void select(String id){
if (id!=null&&!id.isEmpty()){
//声明Connection对象
Connection con;
//驱动程序名
String driver = "com.mysql.jdbc.Driver";
//URL指向要访问的数据库名mydata
String url = "jdbc:mysql://localhost:3306/test";
//MySQL配置时的用户名
String user = "root";
//MySQL配置时的密码
String password = "123456";
//遍历查询结果集
try {
//加载驱动程序
Class<?> aClass = Class.forName(driver);
//1.getConnection()方法,连接MySQL数据库!!
con = DriverManager.getConnection(url,user,password);
if(!con.isClosed()) {
//2.创建statement类对象,用来执行SQL语句!!
Statement statement = con.createStatement();
//要执行的SQL语句
String sql = "select * from users where id='" + id + "'";
//3.ResultSet类,用来存放获取的结果集!!
ResultSet rs = statement.executeQuery(sql);
System.out.println("-----------------");
System.out.println("执行Sql语句: "+sql);
System.out.println("执行结果如下所示:");
String name, uname;
while (rs.next()) {
//获取stuname这列数据
uname = rs.getString("uname");
//获取stuid这列数据
name = rs.getString("name");
//输出结果
System.out.println(uname + "\t" + name);
res.add(new String[]{"id: "+rs.getString("id")
+ ",uname:"+rs.getString("uname")
+ ",passwd:"+rs.getString("passwd")
+ ",name:"+rs.getString("name")}
);
}
rs.close();
con.close();
}
} catch(ClassNotFoundException e) {
//数据库驱动类异常处理
e.printStackTrace();
} catch(SQLException e) {
//数据库连接失败异常处理
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
result.put("data",res);
result.put("success",true);
return DataUtil.toJson(result);
}
}
}
1
2
3
4
5
6
SQL Paramter:1
-----------------
执行Sql语句: select * from users where id='1'
执行结果如下所示:
test Alice
-----------------
1
2
3
4
5
6
7
8
SQL Paramter:' or 'a'='a
-----------------
执行Sql语句:select * from users where id='1' or 'a'='a'
执行结果如下所示:
test Alice
admin Orleven
user Bob
-----------------

合规代码(参数化查询)

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
public class SQLFormat {
public static void main(String[] args)throws Exception{
select("1");
select("' or 'a'='a");
}
public static void select(String id){
if (id!=null&&!id.isEmpty()){
//声明Connection对象
Connection con;
//驱动程序名
String driver = "com.mysql.jdbc.Driver";
//URL指向要访问的数据库名mydata
String url = "jdbc:mysql://localhost:3306/test";
//MySQL配置时的用户名
String user = "root";
//MySQL配置时的密码
String password = "123456";
//遍历查询结果集
try {
//加载驱动程序
Class<?> aClass = Class.forName(driver);
//1.getConnection()方法,连接MySQL数据库!!
con = DriverManager.getConnection(url,user,password);
if(!con.isClosed()) {
//要执行的SQL语句
String sql = "select * from users where id=?";
//创建statement类对象,ResultSet类,用来存放获取的结果集!!
PreparedStatement stmt = con.prepareStatement(sql);
stmt.setString(1, id);
ResultSet rs = stmt.executeQuery();
System.out.println("-----------------");
System.out.println("执行Sql语句: "+stmt.toString());
System.out.println("执行结果如下所示:");
String uname,name;
while (rs.next()) {
//获取stuname这列数据
uname = rs.getString("uname");
//获取stuid这列数据
name = rs.getString("name");
//输出结果
System.out.println(uname + "\t" + name);
}
rs.close();
con.close();
}
} catch(ClassNotFoundException e) {
//数据库驱动类异常处理
e.printStackTrace();
} catch(SQLException e) {
//数据库连接失败异常处理
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
result.put("data",res);
result.put("success",true);
return DataUtil.toJson(result);
}
}
}
1
2
3
4
5
SQL Paramter:1
-----------------
执行Sql语句: select * from users where id='1'
执行结果如下所示:
test Alice

至于为什么还有1个查询结果,那就是Mysql的字符特性的问题了,这里不作多的解释。

1
2
3
4
5
SQL Paramter:' or 'a'='a
-----------------
执行Sql语句: select * from users where id='1\' or \'a\'=\'a'
执行结果如下所示:
test Alice

防范建议

  1. 采用参数查询即预编译方式(首选)
  2. 字符串过滤

XXE

原理

合法输入

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<users>
<user>
<id>1</id>
<uname>test</uname>
<passwd>e10adc3949ba59abbe56e057f20f883e</passwd>
<name>Alice</name>
</user>
</users>
1
{"id":"1","uname":"test","passwd":"e10adc3949ba59abbe56e057f20f883e","name":"Alice"}

恶意输入

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE xdsec [
<!ELEMENT methodname ANY >
<!ENTITY xxe SYSTEM "http://localhost/text.txt" >]>
<methodcall>
<methodname>%26xxe;</methodname>
</methodcall>

本地80端口WEB服务器收到如下请求:

1
2
3
4
5
127.0.0.1 - - [08/Jan/2018:17:03:27 +0800] "GET /text.txt HTTP/1.1" 404 1053 "-" "Java/1.8.0_51"
127.0.0.1 - - [08/Jan/2018:17:03:28 +0800] "GET /text.txt HTTP/1.1" 404 1053 "-" "Java/1.8.0_51"
127.0.0.1 - - [08/Jan/2018:17:03:28 +0800] "GET /text.txt HTTP/1.1" 404 1053 "-" "Java/1.8.0_51"
127.0.0.1 - - [08/Jan/2018:17:03:28 +0800] "GET /text.txt HTTP/1.1" 404 1053 "-" "Java/1.8.0_51"
127.0.0.1 - - [08/Jan/2018:17:03:28 +0800] "GET /text.txt HTTP/1.1" 404 1053 "-" "Java/1.8.0_51"

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
public class XMlXXEFormat {
public static void main(String[] args)throws Exception{
String data = "";
XMlXXEInject(data);
}
public String XMlXXEInject(String data){
Map<String,Object> result = new HashMap<>();
List<User> res = new ArrayList();
result.put("success",false);
if (data!=null&&!data.isEmpty()){
// 获取基于SAX的解析器的实例
try {
System.out.println(data);
SAXParserFactory factory = SAXParserFactory.newInstance();
// 2.创建一个SAXParser实例
SAXParser saxParser = factory.newSAXParser();
// 3.解析
MySaxHandler myhandler = new MySaxHandler();
saxParser.parse(new ByteArrayInputStream(data.getBytes()), myhandler);
res = myhandler.getUserList();
}catch (Exception e){
e.printStackTrace();
}
}
result.put("data", res);
System.out.println(DataUtil.toJson(result));
}
}

合规代码

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
public class XMlXXEFormat {
public static void main(String[] args)throws Exception{
String data = "";
XMlXXEInject(data);
}
public String XMlXXEInject(String data){
Map<String,Object> result = new HashMap<>();
List<User> res = new ArrayList();
result.put("success",false);
if (data!=null&&!data.isEmpty()){
// 获取基于SAX的解析器的实例
try {
System.out.println(data);
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// 2.创建一个SAXParser实例
SAXParser saxParser = factory.newSAXParser();
// 3.解析
MySaxHandler myhandler = new MySaxHandler();
saxParser.parse(new ByteArrayInputStream(data.getBytes()), myhandler);
res = myhandler.getUserList();
}
catch (Exception e){
e.printStackTrace();
}
}
result.put("data", res);
System.out.println(DataUtil.toJson(result));
}
}

报错如下:

1
org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 50; 将功能 "http://apache.org/xml/features/disallow-doctype-decl" 设置为“真”时, 不允许使用 DOCTYPE。

防范建议

  1. 使用开发语言提供的禁用外部实体的方法(首选)
  2. 过滤用户提交的XML数据,关键词:<!DOCTYPE<!ENTITY,或者SYSTEMPUBLIC

命令注入

原理

合法输入

1
2
8.8.8.8
ping -n/-c 1 8.8.8.8

恶意输入

1
2
8.8.8.8 & whoami
ping -n/-c 1 8.8.8.8 & whoami

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
public class ExecTestController {
public static void main(String[] args)throws Exception{
String data = "8.8.8.8 & ipconfig/all ";
ExecInject(data);
}
public String ExecInject(HttpServletRequest request){
String command = request.getParameter("command");
Map<String,Object> result = new HashMap<>();
result.put("success",false);
StringBuffer buffer = null;
if (command!=null&&!command.isEmpty()){
// 1.获取基于SAX的解析器的实例
try {
System.out.println("Command: ping"+ command);
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("cmd.exe /C ping "+command);
// Process proc = rt.exec(new String [] {"sh", "-c", "ls "+order});
int res = proc.waitFor();
if(res !=0){
System.out.println("process error: "+ res);
}
InputStream in = (res == 0)? proc.getInputStream() : proc.getErrorStream();
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
buffer=new StringBuffer();
String line;
while((line = reader.readLine())!=null){
buffer.append(line+"\n");
}
}catch (Exception e){
e.printStackTrace();
}
}
System.out.print(buffer.toString());
}
}

合规代码

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
public class ExecTestController {
public static void main(String[] args)throws Exception{
String data = "8.8.8.8 & ipconfig/all ";
ExecFormat(data);
}
public String ExecFormat(HttpServletRequest request){
String command = request.getParameter("command");
Map<String,Object> result = new HashMap<>();
result.put("success",false);
StringBuffer buffer = null;
if (command!=null&&!command.isEmpty()){
// 1.获取基于SAX的解析器的实例
try {
// 严格校验参数
if (!Pattern.matches("[A-Za-z@.]+", command)){ ;
result.put("message", "Error!");
return DataUtil.toJson(result);
}
System.out.println("Command: ping"+ command);
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("cmd.exe /C ping "+command);
// Process proc = rt.exec(new String [] {"sh", "-c", "ls "+order});
int res = proc.waitFor();
if(res !=0){
System.out.println("process error: "+ res);
}
InputStream in = (res == 0)? proc.getInputStream() : proc.getErrorStream();
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
buffer=new StringBuffer();
String line;
while((line = reader.readLine())!=null){
buffer.append(line+"\n");
}
}catch (Exception e){
e.printStackTrace();
}
}
System.out.print(buffer.toString());
}
}

防范建议

  1. 白名单
  2. 严格权限限制等等
  3. 严格校验参数

任意文件下载/目录遍历

原理

在JAVA web程序的下载文件相关的代码中,若不对HTTP请求中的待下载文件名进行检查,则有可能产生任意文件下载漏洞。
目录遍历与任意文件下载的原理相似,这里就简单列个文件下载好了

合法输入

1
2
path=users.docs
config/users.docs

恶意输入

1
2
path=../../../../Windows/System32/drivers/etc/HOSTS
config/../../../../Windows/System32/drivers/etc/HOSTS

Java代码分析

不合规代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class FileTestController {
public void FileFileDownVul(HttpServletRequest request,HttpServletResponse response){
String path = request.getParameter("path");
Map<String,Object> result = new HashMap<>();
result.put("success",false);
if (path!=null&&!path.isEmpty()){
// 1.获取基于SAX的解析器的实例
try {
path = "config/"+path;
File file = new File(path);
System.out.println(path);
response.setHeader("Content-Disposition", "attachment;filename=\""
+ new String(path.getBytes(), "ISO8859-1") + "\"");
response.setContentLength((int) file.length());
byte[] buffer = new byte[4096];// 缓冲区
BufferedOutputStream output = null;
BufferedInputStream input = null;
try {
output = new BufferedOutputStream(response.getOutputStream());
input = new BufferedInputStream(new FileInputStream(file));
int n = -1;
//遍历,开始下载
while ((n = input.read(buffer, 0, 4096)) > -1) {
output.write(buffer, 0, n);
}
output.flush(); //不可少
response.flushBuffer();//不可少
} catch (Exception e) {
//异常自己捕捉
} finally {
//关闭流,不可少
if (input != null)
input.close();
if (output != null)
output.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}

合规代码

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
public class FileTestController {
public void FileFileDownFormat(HttpServletRequest request,HttpServletResponse response){
String path = request.getParameter("path");
Map<String,Object> result = new HashMap<>();
result.put("success",false);
if (path!=null&&!path.isEmpty()){
try {
if (!Pattern.matches("^([a-z0-9]{32})$", path)){ ;
return ;
}
path = "config/"+path+".docx";
File file = new File(path);
System.out.println(path);
response.setHeader("Content-Disposition", "attachment;filename=\""
+ new String(path.getBytes(), "ISO8859-1") + "\"");
response.setContentLength((int) file.length());
byte[] buffer = new byte[4096];// 缓冲区
BufferedOutputStream output = null;
BufferedInputStream input = null;
try {
output = new BufferedOutputStream(response.getOutputStream());
input = new BufferedInputStream(new FileInputStream(file));
int n = -1;
//遍历,开始下载
while ((n = input.read(buffer, 0, 4096)) > -1) {
output.write(buffer, 0, n);
}
output.flush(); //不可少
response.flushBuffer();//不可少
} catch (Exception e) {
//异常自己捕捉
} finally {
//关闭流,不可少
if (input != null)
input.close();
if (output != null)
output.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}

防范建议

  1. 严格的权限限制。
  2. 控制路径后缀,对文件名进行检查等。
  3. 采用getCanonicalPath、getAbsolutePath等方法。

任意文件上传

原理

当攻击者利用恶意文件上传漏洞时,通常会向服务器上传jsp木马并访问,可以直接控制服务器。

合法输入

1
2
3
4
5
6
7
8
9
10
11
-----------------------------11922861918696
Content-Disposition: form-data; name="name"
test.txt
-----------------------------11922861918696
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test
-----------------------------11922861918696--

恶意输入

1
2
3
4
5
6
7
8
9
10
11
12
13
-----------------------------11922861918696
Content-Disposition: form-data; name="name"
../test.jsp
-----------------------------11922861918696
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
...
webshell
....
-----------------------------11922861918696--

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
public class FileTestController {
@RequestMapping("/Index")
public String FileFileUpload(Map<String,Object> map){
return "Fileup";
}
@ResponseBody
@RequestMapping(value= "/File/FileUpVul",method= RequestMethod.POST)
public String FileFileUpVul(HttpServletRequest request,@RequestParam("file") MultipartFile file){
String name = request.getParameter("name");
if (!file.isEmpty()) {
try {
byte[] bytes = file.getBytes();
BufferedOutputStream stream =
new BufferedOutputStream(new FileOutputStream(new File("config/"+name)));
stream.write(bytes);
stream.close();
return "You successfully uploaded " + name + " into config/" + name;
} catch (Exception e) {
return "You failed to upload " + name + " => " + e.getMessage();
}
} else {
return "You failed to upload " + name + " because the file was empty.";
}
}
}

合规代码

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
public class FileTestController {
@RequestMapping("/Index")
public String FileFileUpload(Map<String,Object> map){
return "Fileup";
}
@ResponseBody
@RequestMapping(value= "/File/FileUpVul",method= RequestMethod.POST)
public String FileFileUpVul(HttpServletRequest request,@RequestParam("file") MultipartFile file){
String name = DataUtil.getTimeStamp()+".jpg";
if (!file.isEmpty()) {
try {
// 判断大小等等,只要做好配置,并不需要检查文件头。
if(file.getSize()<1024*1024*10){
byte[] bytes = file.getBytes();
BufferedOutputStream stream =
new BufferedOutputStream(new FileOutputStream(new File("config/"+name)));
stream.write(bytes);
stream.close();
return "You successfully uploaded " + name + " into config/" + name;
}
} catch (Exception e) {
return "You failed to upload " + name + " => " + e.getMessage();
}
}
return "You failed to upload " + name + " because the file was empty.";
}

防范建议

  1. 控制文件名
  2. 控制文件内容
  3. 控制上传目录的权限

XSS

原理

这里仅仅拿了个反射的XSS做例子。

合法输入

1
2
alice
hello,alice.

恶意输入

1
2
1<script>alert(1)</script>
hello,1<script>alert(1)</script>.

Java代码分析

不合规代码

1
2
3
4
5
6
7
8
9
10
11
12
public class XSSTestController {
public String XXSXXSInject(HttpServletRequest request){
String data = request.getParameter("data");
if (data!=null&&!data.isEmpty()){
return "hello,"+data+".";
}
return "404";
}
}

合规代码

1
2
3
4
5
6
7
8
9
public class XSSTestController {
public String XXSXXSInject(HttpServletRequest request){
String data = request.getParameter("data");
if (data!=null&&!data.isEmpty()){
return "hello,"+HtmlUtils.htmlEscape(data)+".";
}
return "404";
}
}
1
2
1<script>alert(1)</script>
hello,1&lt;script&gt;alert(1)&lt;/script&gt;.

防范建议

  1. 对用户的输入进行html编码。

其他场景

这些场景和前面提到的类似或者比较浅显易懂,就不贴代码了。

压缩文件问题

没有检测压缩后的大小等细节。

其他未净化输入导致日志等处被污染

顾名思义,应用程序的日志被污染,如果输出到页面上也容易触发XSS。

合法输入

1
2
3
orleven
正常用户登录失败,记录日志.....
User Login Successed for: orleven

恶意输入

1
2
3
4
orleven\r\nUser Login Successed for: administrator
恶意用户登录失败,记录日志.....
User Login Failed for: orleven
User Login Successed for: administrator

正则表达式的拼接

原理与SQL注入基本相同。

合法输入

1
2
search=error
(.*? +public\\[\\d+\\]+.*error.*)

恶意输入

1
2
search=.*)|(.*
(.*? +public\\[\\d+\\]+.*.*)|(.*.*)

XML注入的拼接

原理与SQL注入基本相同。

合法输入

1
2
3
4
5
6
quantity=1
<item>
<name>apple</name>
<price>500.0</price>
<quantity>1</quantity>
<item>

恶意输入

1
2
3
4
5
6
quantity=1</quantity><price>5.0</price><quantity>1
<item>
<name>apple</name>
<price>500.0</price>
<quantity>1</quantity><price>5.0</price><quantity>1</quantity>
<item>

总结

想要学好代码审计,就必须多接触主流的的框架,例如Spring等,在了解各各类框架之后,才能更快的上手。

相关测试代码
参考文章