Java安全 Web漏洞(上)

java中的sql注入、文件上传下载、目录穿越、ssrf的相关利用与审计

SQL 注入

各种原理啥的学php的时候就过的差不多了,这里直接跳过。

这里主要看看java场景下各类sql注入是如何呈现的

环境搭建

创建工程

在idea上创建一个sqliDemoSpringBoot工程

并选择如下依赖:

  • web→ Spring Web
  • SQL→ JDBC API
  • SQL→Mybatis Framework
  • SQL→Mysql Driver

这里我后面将springboot版本改成3.3.2了

数据库初始化

创建一个 sqlidemo 的数据库:

1
CREATE DATABASE sqlidemo;

创建users数据表:

1
2
3
4
5
6
7
USE `sqlidemo`;
CREATE TABLE IF NOT EXISTS `users`(
`id` INT UNSIGNED AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

在表中添加数据

1
2
3
4
INSERT INTO `users` VALUES (1, 'admin', 'admin');
INSERT INTO `users` VALUES (2, 'admin1', 'admin1');
INSERT INTO `users` VALUES (3, 'test', 'test');
INSERT INTO `users` VALUES (4, 'hey', '1234qwer');

配置Spring boot

application.properties 中添加如下数据

1
2
3
4
5
6
7
8
spring.application.name=sqliDemo
#访问端口号
server.port=8088
#数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/sqlidemo?AllowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234qwer
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

JDBC中的SQL注入

动态拼接

后端代码将前端获取的参数动态直接拼接到SQL 语句中使用java.sql.Statement 执行SQL 语句从而导致SQL 注入漏洞的出现

这里造成sql注入的原因有两个关键:

  • 动态拼接参数
  • 使用java.sql.Statement 执行SQL 语句

同php中的注入差不多,代码缺少预编译等其他措施就容易造成可动态拼接

java.sql.Statement

Statement 对象用于执行一条静态的 SQL 语句并获取它的结果

createStatement() :创建一个 Statement 对象,之后可使用executeQuery() 方法执行 SQL 语句。

executeQuery(String sql) :执行指定的 SQL 语句,返回单个ResultSet 对象。

官方文档:

java.sql.Statementhttps://docs.oracle.com/javase/7/docs/api/java/sql/Statement.html

createStatement()方法https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#createStatement--

executeQuery()方法https://docs.oracle.com/javase/8/docs/api/java/sql/Statement.html#executeQuery-java.lang.String

示例

jdbcinjection 包中创建一个JdbcDynamicController

写入示例代码:

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
package com.hey.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.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

@RestController
@RequestMapping("/sqli")
public class JdbcDynamicController {
private static String driver = "com.mysql.jdbc.Driver";

@Value("${spring.datasource.url}")
private String Url;

@Value("${spring.datasource.username}")
private String username;

@Value("${spring.datasource.password}")
private String password;

@RequestMapping("/jdbc/dynamic")
public String jdbcDynamic(@RequestParam("id") String id) throws Exception {
// 一个可变字符串对象
StringBuilder stringBuilder = new StringBuilder();
// 注册驱动
Class.forName(driver);
// 获得连接
Connection connection = DriverManager.getConnection(Url, username, password);
Statement statement = connection.createStatement();
// 拼接字符串
String sql = "select * from users where id = '" + id + "'";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String info = String.format("%s: %s\n" , username, password);
stringBuilder.append(info);
}
resultSet.close();
statement.close();
return stringBuilder.toString();

}
}

运行程序后,测试sql注入

错误的预编译

在动态拼接中是使用statement执行sql语句,但如果使用PreparedStatement预编译参数化查询是能够防止SQL注入的。但如果没有正确的使用PreparedStatement预编译还是会存在SQL注入的风险

java.sql.PreparedStatement

PreparedStatement 是继承Statement 的子接口。它会对SQL 语句进行预编译,不论输入什么,经过预编译后
全都以字符串来执行 SQL语句

PreparedStatement会先使用 ? 作为占位符将SQL语句进行预编译,确定语句结构,再传入参数执行查询。如下述代码:

1
2
3
4
String sql = "select * from users where id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
// 这里的1是指 SQL 查询中参数占位符 ?的位置索引

官方文档:

https://docs.oracle.com/javase/7/docs/api/java/sql/PreparedStatement.html

https://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html

示例

由于开发人员疏忽或经验不足等原因,虽然使用了预编译PreparedStatement,但没有根据标准流程对参数进行标记,依旧使用了动态拼接SQL 语句的方式,进而造成SQL 注入漏洞。

jdbcinjection 下新建一个名为JdbcPrepareStatement 的Java 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
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
package com.hey.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.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

@RestController
@RequestMapping("/sqli2")
public class JdbcPrepareStatement {
private static String driver = "com.mysql.jdbc.Driver";
@Value("${spring.datasource.rul}")
private String url;

@Value("${spring.datasource.username}")
private String username;

@Value("${spring.datasource.password}")
private String password;

@RequestMapping("/jdbc/sec")
public String jdbcSec(@RequestParam("id") String id) throws Exception{
StringBuilder stringBuilder = new StringBuilder();
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
// 预编译代码
String sql = "select * from users where id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String info = String.format("%s: %s\n" , username, password);
stringBuilder.append(info);
}
resultSet.close();
connection.close();
return stringBuilder.toString();
}
@RequestMapping("/jdbc/preparedstatement")
public String jdbcPreparedStatement(@RequestParam("id") String id) throws Exception{
StringBuilder stringBuilder = new StringBuilder();
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
// 错误的预编译
String sql = "select * from users where id = '" + id + "'";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String info = String.format("%s: %s\n" , username, password);
stringBuilder.append(info);
}
resultSet.close();
connection.close();
return stringBuilder.toString();
}

}

这里在接口/sqli2/jdbc/sec?id=1 /sqli2/jdbc/preparedstatement?id=1分别给出了预编译的正确使用和错误使用两种情况

Order By 注入

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

其实不是不能在Order By上使用预编译,而是使用后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
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.hey.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.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

@RestController
@RequestMapping("/sqli3")
public class jdbcOrderby {
private static String driver = "com.mysql.jdbc.Driver";

@Value("${spring.datasource.url}")
private String url;

@Value("${spring.datasource.username}")
private String username;

@Value("${spring.datasource.password}")
private String password;

@RequestMapping("/jdbc/orderby1")
public String jdbcOrderby1(@RequestParam("id") String id) throws Exception{
StringBuilder stringBuilder = new StringBuilder();

Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "select * from users order by " + id;
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String info = String.format("%s: %s\n" , username, password);
stringBuilder.append(info);
}
resultSet.close();
connection.close();
return stringBuilder.toString();
}

@RequestMapping("/jdbc/orderby2")
public String jdbcOrderby2(@RequestParam("id") String id) throws Exception{
StringBuilder stringBuilder = new StringBuilder();

Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "select * from users order by ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String username = resultSet.getString("username");
String password =resultSet.getString("password");
String info = String.format("%s: %s\n" , username, password);
stringBuilder.append(info);
}
resultSet.close();
connection.close();
return stringBuilder.toString();
}
}

这里orderby1是错误的预编译仍采用动态拼接SQL 语句,导致order by注入

orderby2是将传入的参也预编译了,导致被解析成字符串,导致order by排序不生效

image-20241117105804039

常见order by注入手法

1
2
3
4
order by if(表达式,1,sleep(1))
order by rand(表达式)
order by updatexml(1,if(1=2,1,(表达式)),1)
order by extractvalue(1,if(1=2,1,(表达式)));

当初一个绕雷池waf的payload

不能完全注出数据,只是确认漏洞存在,判定当前数据库是否为sqlidemo

1
order=DATABASE() like 'sqlidemo' and sleep(10)

Mybatis

相关文档:mybatis – MyBatis 3 | 简介

#{} 和${} 区别

在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 也可能存在注入风险

示例

mybatisinjection包下创建一个User的实体类,方便后续与数据表做映射

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
package com.hey.sqlidemo.mybatisinjection;

public class User {
private int id;
private String username;
private String 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;
}

public int getId() {
return id;
}

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

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

接着在mybatisinjection文件下新建UserMapper的Java Interface

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

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface UserMapper {
@Select("select * from users order by ${sort}")
List<User> orderbyInjection(@Param("sort") String sort);
@Select("select * from users where id in (${params})")
List<User> inInjection(@Param("params") String params);
@Select("select * from users where username like '%${username}%' ")
List<User> likeInjection(@Param("username") String username);
// Mybatis查询SQL语句的另一种使用注解方式,这也是存在SQL注入的
// @Select("select * from users where username = '${username}'")
// List<User> likeInjection(@Param("username") String username);
}

resource中创建mapper文件夹。然后写入UserMapper.xml的配置文件,与事先的UserMapper做好绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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.hey.sqlidemo.mybatisinjection.UserMapper">
<resultMap type="com.hey.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>

然后在mybatisinjection 下创建一个MybatisController的类

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.hey.sqlidemo.mybatisinjection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/sqli")
public class MybatisController {
@Autowired
private UserMapper userMapper;

@RequestMapping("/mybatis/order")
public List<User> orderSql(String sort) {
return userMapper.orderbyInjection(sort);
}

@RequestMapping("/mybatis/in")
public List<User> inSql(String params) {
return userMapper.inInjection(params);
}

@RequestMapping("/mybatis/like")
public List<User> likeSql(String username) {
return userMapper.likeInjection(username);
}
}

最后在application.properties 中添加如下配置

1
2
3
4
# 指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mapper/*.xml
# 指定Mybatis的实体目录
mybatis.type-aliases-package=com.hey.sqlidemo.mybatisinjection

启动该项目

Order by 注入

在上面的代码中,由于order by的特殊原因,导致没对其使用预编译,而是使用了${}导致sql注入的出现

注入点:

1
http://127.0.0.1:8088/sqli/mybatis/order?sort=username

In 注入

in语句常用于where 表达式中,其作用是查询某个范围内的数据,例如:

1
select * from where field in (value1,value2,value3,…);

如上述代码,in在查询某个范围时可能会用到不止一个参数,在mybatis中如果直接使用占位符#{}将这些参数value1,value2看作一个整体 ,导致查询报错

因此开发可能为了完成正常功能而不引起报错直接使用拼接符${}进行查询,从而出现sql注入,例如:

1
select * from users where id in (${params})

正确做法是使用foreach配合占位符#{}实现in查询

1
2
3
4
5
6
7
<!-- where in 查询正确方式-->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
select * from users where id in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>

再看之前写的代码,明显没进行预编译导致SQL注入

注入点在:

1
http://127.0.0.1:8088/sqli/mybatis/in?params=2
1
http://127.0.0.1:8088/sqli/mybatis/in?params=if(ascii(mid((select database()),1,1))<96,1,2)

Like 注入

like语句一般用来在一个字符型字段列中检索包含对应字串的,例如:

1
select * from users where username like admin

同样的,使用like语句进行查询时也不能使用占位符#{},不然查询会出现报错

1
select * from users where username like '%#{username}%'

因此开发可能为了完成正常功能而不引起报错直接使用拼接符${}进行查询,从而出现sql注入,例如:

正常的做法如下:

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

注入点

1
http://127.0.0.1:8088/sqli/mybatis/like?username=admin

SQL注入相关漏洞修复

原文:https://gist.github.com/retanoj/5fd369524a18ab68a4fe7ac5e0d121e8

表,字段名称

(Select,Order by,Group by等)

1
2
3
4
5
6
7
8
9
10
11
12
// 插入数据用户可控时,应使用白名单处理

String orderBy = "{user input}";
String orderByField;
switch (orderBy) {
case "name":
orderByField = "name";break;
case "age":
orderByField = "age"; break;
default:
orderByField = "id";
}

JDBC

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
String name = "foo";

// 一般查询场景
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, name);
ResultSet rs = pre.executeQuery();

// like 模糊查询场景
String sql = "SELECT * FROM users WHERE name like ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, "%"+name+"%");
ResultSet rs = pre.executeQuery();

// where in 查询场景
String sql = "select * from user where id in (";

// 定义一个 Integer 数组,包含需要查询的 id 值
Integer[] ids = new Integer[]{1,2,3};

// 使用 StringBuilder 构建动态 SQL,以提高字符串拼接的效率
StringBuilder placeholderSql = new StringBuilder(sql);

// 遍历 ids 数组,动态构建 SQL 中的占位符部分
for(int i = 0, size = ids.length; i < size; i++) {
// 添加一个占位符 '?'
placeholderSql.append("?");

// 如果当前不是最后一个元素,则添加逗号分隔符
if (i != size - 1) {
placeholderSql.append(",");
}
}

// 遍历完成后,补上右括号,完成 SQL 的构造
placeholderSql.append(")");

// 使用动态生成的 SQL 创建一个 PreparedStatement 对象
PreparedStatement pre = conn.prepareStatement(placeholderSql.toString());

// 再次遍历 ids 数组,将每个 id 的值绑定到 SQL 的对应占位符上
for(int i = 0, size = ids.length; i < size; i++) {
// `setInt` 方法用于设置 SQL 中的占位符值,索引从 1 开始(数据库规范)
pre.setInt(i + 1, ids[i]);
}

// 执行 SQL 查询,将结果存储到 ResultSet 对象中
ResultSet rs = pre.executeQuery();

Spring-JDBC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
JdbcTemplate jdbcTemplate = new JdbcTemplate(app.dataSource());

// 一般查询场景
String sql = "select * from user where id = ?";
Integer id = 1;
// 使用 Spring 的 JdbcTemplate 执行查询,将结果映射为 UserDO 实例
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), id);

// like 模糊查询场景
String sql = "select * from user where name like ?";
String like_name = "%" + "foo" + "%";
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), like_name);

// where in 查询场景
NamedParameterJdbcTemplate namedJdbcTemplate = new NamedParameterJdbcTemplate(app.dataSource());

MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("names", Arrays.asList("foo", "bar"));

String sql = "select * from user where name in (:names)";
List<UserDO> users = namedJdbcTemplate.query(sql, parameters, BeanPropertyRowMapper.newInstance(UserDO.class));

Mybatis XML Mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 一般查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name = #{name}
</select>

<!-- like 查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name like CONCAT("%", #{name}, "%")
</select>

<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>

Mybatis Criteria

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UserDO {
private Integer id;
private String name;
private Integer age;
}

public class UserDOExample {
// auto generate by Mybatis
}

UserDOMapper userMapper = session.getMapper(UserDOMapper.class);
UserDOExample userExample = new UserDOExample();
UserDOExample.Criteria criteria = userExample.createCriteria();

// 一般查询场景
criteria.andNameEqualTo("foo");

// like 模糊查询场景
criteria.andNameLike("%foo%");

// where in 查询场景
criteria.andIdIn(Arrays.asList(1,2));

List<UserDO> users = userMapper.selectByExample(userExample);

文件上传与下载

文件上传的审计

对于文件上传功能进行代码审计时,主要关注整个上传流程对所上传文件做了什么操作,有没有相应的限制

需要关注的几点:

  • SpringBoot对JSP的限制
  • 文件后缀名是否存在白名单
  • 文件类型是否存在白名单
  • 所保存的路径是否能解析JSP
  • 文件头检测

这里直接拿之前写得文件上传demo代码来继续学习,分析代码视角如何审计该功能

SpringBoot 对 JSP 的限制

常见的springboot项目应该都是不适配jsp的,多少也无法解析jsp文件,因为官方也不提倡springboot使用jsp,并对此做了相关限制。

要想在SpringBoot中使用JSP,需要引入相关的依赖,自建WEB-INF,web.xml 等操作

bug可以参考:https://blog.csdn.net/weixin_43122090/article/details/103866174

不过这样操作也相应地失去了一些springboot的特性

当我们进行代码审计想快速知道项目是否能解析JSP时,可以查看例如pom.xml相关文件是否引入了相关的jsp依赖

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

如果目标项目存在jsp依赖并且有文件上传漏洞,就可以通过文件上传漏洞上传jsp木马获取webshell

校验文件类型

文件名后缀检验

如果后端没有对后缀名进行限制,或者各种黑白名单存在可绕过的缺陷,这样可能有利于我们进行进一步的漏洞利用,上传jsp木马等

各种可能的限制情景的绕过与php中的文件上传绕过原理都大差不差

当然也可能出现没有任何文件名后缀校验的情况

例如之前写得multipartfileController.java就是直接通过getOriginalFilename() 方法获取上传时的文件名,直接和path 路径拼接,并没有判断后缀名。

文件后缀名校验黑白名单

有时后端会对后缀名启用了黑白名单

对于白名单:除了代码写得check逻辑可能出现问题,导致不符合白名单的后缀也可以绕过,不然就只能遵循白名单上传指定的后缀名文件了

对于黑名单:可以通过fuzz,得到具体ban了哪些后缀,之后尝试找到能够正常解析代码的后缀例如jsp、asp等后缀,绕过黑白单限制去获取webshell

语言 可解析后缀
asp/aspx asp,aspx,asa,asax,ascx,ashx,asmx,cer,aSp,aSpx,aSa,aSax,aScx,aShx,aSmx,cEr
php php,php5,php4,php3,php2,pHp,pHp5,pHp4,pHp3,pHp2,html,htm,phtml,pht,Html,Htm,pHtml
jsp jsp,jspa,jspx,jsw,jsv,jspf,jtml,jSp,jSpx,jSpa,jSw,jSv,jSpf,jHtml

参考文章:构造优质上传漏洞fuzz字典 | 回忆飘如雪

1
2
3
4
// 获取文件后缀名
String Suffix = fileName.substring(fileName.lastIndexOf("."));
//黑or白名单判断
String[] SuffixList = {".jpg", ".png", ".jpeg", ".gif"};

MIME type 检测

校验文件类型还有一种方式检测MIME Type。也就是我们在请求中常见的 Content-Type 字段。

如果项目中使用 MIME type 黑白名单检测文件类型,可以分析黑白名单中是否有遗漏的敏感文件类型。

常见MIME类型:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/MIME_types/Common_types

相关文章:https://blog.csdn.net/qq_42764468/article/details/121522645

https://www.cnblogs.com/bojuetech/p/5907910.html

文件名操作

常见的情况是后端直接接受保存我们上传的文件名,也有可能是后端自定义的文件名比如使用UUID

1
2
3
4
String originalFileName = file.getOriginalFilename();
String extension = originalFileName.substring(originalFileName.la
stIndexOf('.'));
String fileName = UUID.randomUUID() + extension;

还有的会根据上传时的时间戳来命名上传文件

虽然将文件名随机命名可以增加一些攻击利用难度。但并没有直接修复任意文件上传漏洞。

有时候上传后的文件路径和文件名会直接在相应包给出来

记一次实战中碰到的案例

文件上传木马后,响应包未给出相应路径,通过svn泄露,尝试脱下部分代码及路径,根据泄露的路径推断出文件上传保存的路径,结合上传文件名,成功获取webshell

保存路径

如果木马能顺利上传,我们还需要关注木马是否保存在本地,还是云端,保存路径能否被解析成对应的语言

是否保存在本地

现在很多项目可以说是都在向云服务器迁移,并且对数据,文件做了隔离。不同的的场景应用不同的存储服务器。

后端在对上传文件保存时无非要么是保存在服务器本地,要么保存在相关云存储服务器。比如:阿里云oss等

如果文件是保存在oss存储桶的话是无法解析你上传的webshell的,顶多上传html打xss或者覆盖原有页面钓session,除了html,还有一种文件在特殊场景下会被利用呢?那就是shtml

参考http://pirogue.org/2017/09/29/aliyunoss/

这个文章已经是17年的文章了,手法不确定过时没有,但可以作为一种尝试

1
2
3
4
shtml用的是SSI指令,SSI是为WEB服务器提供的一套命令,这些命令只要直接嵌入到HTML文档的注释内容之中即可。
<!--#include file="/home/www/user7511/nav_foot.htm"--> //可以用来读文件
<!--#exec cmd="ifconfig"--> //可以用来执行命令
<!--#include virtual="/includes/header.html" --> //也是读文件 与FILE不同他支持绝对路径和../来跳转到父目录 而file只能读取当前目录下的

通过代码审计是否保存文件的话,查看处理上传逻辑的那段代码,应该不难判断

是否解析

例如当/aaa目录可以解析jsp文件,那当我们将jsp木马上传到该目录下,并且能成功访问,就可以获取到webshell

还有可能是上传到了本地,但该路径不会被解析,同样也是无法获取webshell的

如果所保存文件的地址可能是一个不可执行不可解析权限非常低的目录,尽管我们将WebShell 上传到了目标服务器,那么也因无法解析执行而无功而返。

路径是否可控

在获取文件名后,大多会进行路径拼接操作。在这里我们可以检查拼接路径是有相关防护,如果没有限制 ../

么极有可能存在目录穿越漏洞。

如果保存图片的地址是非解析目录,我们可以配合目录穿越漏洞操作WebShell 存储到其他地方,尝试执行

FileUploadDemo 项目中路径使用了直接拼接的方式,并且没有任何防护,代码如下:

1
2
String fileName = file.getOriginalFilename();
String filePath = path + fileName;

大家可以启动运行该项目自行调试,将上传的文件名改为 ../../../../../../../test.txt 后观察结果。

文件上传功能关键字

可以通过 查看需求文档 ,查看Controller 层 ,部署后通过前端定位功能点 , 全局搜索关键字 等等来定位上传功能

文件上传关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
File
FileUpload
FileUploadBase
FileItemIteratorImpl
FileItemStreamImpl
FileUtils
UploadHandleServlet
FileLoadServlet
FileOutputStream
DiskFileItemFactory
MultipartRequestEntity
MultipartFile
com.oreilly.servlet.MultipartRequest
......

任意文件上传漏洞修复

参考:https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

  • 列出允许的扩展。只允许业务功能的安全和关键扩展
  • 确保在验证扩展名之前应用输入验证。
  • 验证文件类型,不要相信Content-Type 头,因为它可以被欺骗。
  • 将文件名改为由应用程序生成的文件名
  • 设置一个文件名的长度限制。如果可能的话,限制允许的字符
  • 设置一个文件大小限制
  • 只允许授权用户上传文件
  • 将文件存储在不同的服务器上。如果不可能,就把它们存放在webroot 之外。
  • 在公众访问文件的情况下,使用一个处理程序,在应用程序中被映射到文件名(someid -> file.ext)。
  • 通过杀毒软件或沙盒(如果有的话)运行文件,以验证它不包含恶意数据。
  • 确保任何使用的库都是安全配置的,并保持最新。
  • 保护文件上传免受CSRF 攻击

任意文件读取/下载漏洞代码审计

代码审计流程大致分为下面几步:

首先是确定功能是否存在文件读取/下载功能,其次是分析文件参数是否可控,再其次分析路径是否可控,如果存在路径限制则尝试绕过,最终经过一系列分析确定是否存在任意文件读取/下载漏洞。

任意文件读取/下载漏洞代码审计本身不难,确定了功能点后,如果后端直接接受前端传来的文件名,没有对路径做限制,那大概率存在任意文件读取/下载漏洞。当然具体情况还得具体分析。

如果存在路径限制,这部分属于目录穿越漏洞范畴了。

确定功能点

确定目标系统是否存在读取或下载功能方式很多。可以通过阅读使用手册,官方文档,部署环境后前端定位功能,后端关键字查找。

相关关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.FileOnputStream
java.io.FileSystem/Win32FileSystem/WinNTFileSystem/UnixFileSystem
sun.nio.fs.UnixFileSystemProvider/WindowsFileSystemProvider
java.io.RandomAccessFile
sun.nio.fs.UnixChannelFactory
sun.nio.fs.WindowsChannelFactory
java.nio.channels.AsynchronousFileChannel
FileUtil/IOUtil
BufferedReader
readAllBytes
scanner

这些关键字不仅仅能定位到文件读取或下载操作,可能还会涉及到一些比如文件删除,文件移动,文件遍历等操作

文件参数可控

以我们前面写的webreadfile.java为例,该代码的70、71行写了,后端接受前端传来的文件名,并未有其他的处理逻辑,这意味着文件名可由我们前端输入来控制

路径无限制

部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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();
}

整个代码对于路径并没有任何额外的处理,可以确定存在任意文件读取漏洞

但在实际环境,可能会遇到各种限制及验证,例如判断文件名是否为空,是否存在该文件和限制可读取文件的路径等操作

有时候会以下列方式设置读取/下载文件目录:

1
2
String path = "C:\\Users\\hey\\Desktop\\";
String filePath = path + fileName;

但这样如果没有过滤../还是可以结合目录穿越来达到任意文件读取的

任意文件读取/下载漏洞验证

启动项目,执行payload即可

1
http://127.0.0.1:8080/IoBufferedReader?filename=C:/Windows/win.ini

可以看到成功下载了该文件,这里是任意文件下载是因为后端代码的缘故,改成读取也是一样的效果

任意文件读取/下载漏洞修复

  • 限定允许读取目录,必要情况后端写死指定读取文件,视具体功能而定
  • 做好读取白名单
  • 过滤./.等目录穿越payload黑名单
  • 进行鉴权,权限划分,避免越权读取文件

目录穿越

原理就不多讲了,本质就是没有对传入的文件名进行过滤,从而导致攻击者可通过使用 ../ 等方式进行目录穿越

示例代码

示例一:https://github.com/j3ers3/Hello-Java-Sec/blob/master/src/main/java/com/best/hello/controller/Traversal.java

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

import com.best.hello.util.Security;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

/**
* @date 2021/07/15
*/
@Api("目录遍历")
@RestController
@RequestMapping("/Traversal")
public class Traversal {
Logger log = LoggerFactory.getLogger(Traversal.class);

@ApiOperation(value = "vul:任意文件下载")
@GetMapping("/download")
public String download(String filename, HttpServletResponse response) {
// 下载的文件路径
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
log.info("[vul] 任意文件下载:" + filePath);

try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)))) {
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setContentLength((int) Files.size(Paths.get(filePath)));
response.setContentType("application/octet-stream");

// 使用 Apache Commons IO 库的工具方法将输入流中的数据拷贝到输出流中
IOUtils.copy(inputStream, response.getOutputStream());
log.info("文件 {} 下载成功,路径:{}", filename, filePath);
return "下载文件成功:" + filePath;
} catch (IOException e) {
log.error("下载文件失败,路径:{}", filePath, e);
return "未找到文件:" + filePath;
}
}

@ApiOperation(value = "vul:任意路径遍历")
@GetMapping("/list")
public String fileList(String filename) {
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
log.info("[vul] 任意路径遍历:" + filePath);
StringBuilder sb = new StringBuilder();

File f = new File(filePath);
File[] fs = f.listFiles();

if (fs != null) {
for (File ff : fs) {
sb.append(ff.getName()).append("<br>");
}
return sb.toString();
}
return filePath + "目录不存在!";
}


@ApiOperation(value = "safe:过滤../")
@GetMapping("/download/safe")
public String safe(String filename) {
if (!Security.checkTraversal(filename)) {
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
return "安全路径:" + filePath;
} else {
return "检测到非法遍历!";
}
}
}

代码中并未对输入的文件名进行相关过滤,也无相关黑白名单配置,仅仅是将它与路径直接拼接,导致了整个路径可控,从而触发目录穿越

1
String filePath = System.getProperty("user.dir") + "/logs/" + filename;

示例二:https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/PathTraversal.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
package org.joychou.controller;

import org.apache.commons.codec.binary.Base64;
import org.joychou.security.SecurityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

@RestController
public class PathTraversal {

protected final Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd
*/
@GetMapping("/path_traversal/vul")
public String getImage(String filepath) throws IOException {
return getImgBase64(filepath);
}

@GetMapping("/path_traversal/sec")
public String getImageSec(String filepath) throws IOException {
if (SecurityUtil.pathFilter(filepath) == null) {
logger.info("Illegal file path: " + filepath);
return "Bad boy. Illegal file path.";
}
return getImgBase64(filepath);
}

private String getImgBase64(String imgFile) throws IOException {

logger.info("Working directory: " + System.getProperty("user.dir"));
logger.info("File path: " + imgFile);

File f = new File(imgFile);
if (f.exists() && !f.isDirectory()) {
byte[] data = Files.readAllBytes(Paths.get(imgFile));
return new String(Base64.encodeBase64(data));
} else {
return "File doesn't exist or is not a file.";
}
}

public static void main(String[] argv) throws IOException {
String aa = new String(Files.readAllBytes(Paths.get("pom.xml")), StandardCharsets.UTF_8);
System.out.println(aa);
}
}

这里同样也是传入的文件路径可控导致目录穿越漏洞

1
2
3
4
@GetMapping("/path_traversal/vul")
public String getImage(String filepath) throws IOException {
return getImgBase64(filepath);
}

目录穿越相关绕过

目录穿越的payload遇到waf一般都是寄中寄,但对于后端写了相关限制的黑盒场景下还是可以试试如下的payload的,说不定有意想不到的惊喜

URL 编码

单次的URL 编码, ../ 结果为: ..%2F%2E%2E%2F

URL 双重编码

1
2
3
. = %252e
/ = %252f
\ = %255c

URL Unicode编码

1
2
3
. = %u002e
/ = %u2215
\ = %u2216

URL UTF-8 与 超长UTF-8 编码

1
2
3
. = %c0%2e, %e0%40%ae, %c0ae
/ = %c0%af, %e0%80%af, %c0%2f
\ = %c0%5c, %c0%80%5c

空字节截断

也就是00阶段,即空字节URL编码绕过,用于对一些判断后缀名的绕过,

1
../../../../../passwd%00.jpg

双重 ../

仅做一次判断删除或替换 ../ 情况下,可使用 ..././ 方式绕过

1
%u002e%u002e%u2215

相关敏感文件

Windows

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
C:/Users/Administrator/NTUser.dat
C:/Documents and Settings/Administrator/NTUser.dat
C:/apache/logs/access.log
C:/apache/logs/error.log
C:/apache/php/php.ini
C:/boot.ini
C:/inetpub/wwwroot/global.asa
C:/MySQL/data/hostname.err
C:/MySQL/data/mysql.err
C:/MySQL/data/mysql.log
C:/MySQL/my.cnf
C:/MySQL/my.ini
C:/php4/php.ini
C:/php5/php.ini
C:/php/php.ini
C:/Program Files/Apache Group/Apache2/conf/httpd.conf
C:/Program Files/Apache Group/Apache/conf/httpd.conf
C:/Program Files/Apache Group/Apache/logs/access.log
C:/Program Files/Apache Group/Apache/logs/error.log
C:/Program Files/FileZilla Server/FileZilla Server.xml
C:/Program Files/MySQL/data/hostname.err
C:/Program Files/MySQL/data/mysql-bin.log
C:/Program Files/MySQL/data/mysql.err
C:/Program Files/MySQL/data/mysql.log
C:/Program Files/MySQL/my.ini
C:/Program Files/MySQL/my.cnf
C:/Program Files/MySQL/MySQL Server 5.0/data/hostname.err
C:/Program Files/MySQL/MySQL Server 5.0/data/mysql-bin.log
C:/Program Files/MySQL/MySQL Server 5.0/data/mysql.err
C:/Program Files/MySQL/MySQL Server 5.0/data/mysql.log
C:/Program Files/MySQL/MySQL Server 5.0/my.cnf
C:/Program Files/MySQL/MySQL Server 5.0/my.ini
C:/Program Files (x86)/Apache Group/Apache2/conf/httpd.conf
C:/Program Files (x86)/Apache Group/Apache/conf/httpd.conf
C:/Program Files (x86)/Apache Group/Apache/conf/access.log
C:/Program Files (x86)/Apache Group/Apache/conf/error.log
C:/Program Files (x86)/FileZilla Server/FileZilla Server.xml
C:/Program Files (x86)/xampp/apache/conf/httpd.conf
C:/WINDOWS/php.ini C:/WINDOWS/Repair/SAM
C:/Windows/repair/system C:/Windows/repair/software
C:/Windows/repair/security
C:/WINDOWS/System32/drivers/etc/hosts
C:/Windows/win.ini
C:/WINNT/php.ini
C:/WINNT/win.ini
C:/xampp/apache/bin/php.ini
C:/xampp/apache/logs/access.log
C:/xampp/apache/logs/error.log
C:/Windows/Panther/Unattend/Unattended.xml
C:/Windows/Panther/Unattended.xml
C:/Windows/debug/NetSetup.log
C:/Windows/system32/config/AppEvent.Evt
C:/Windows/system32/config/SecEvent.Evt
C:/Windows/system32/config/default.sav
C:/Windows/system32/config/security.sav
C:/Windows/system32/config/software.sav
C:/Windows/system32/config/system.sav
C:/Windows/system32/config/regback/default
C:/Windows/system32/config/regback/sam
C:/Windows/system32/config/regback/security
C:/Windows/system32/config/regback/system
C:/Windows/system32/config/regback/software
C:/Program Files/MySQL/MySQL Server 5.1/my.ini
C:/Windows/System32/inetsrv/config/schema/ASPNET_schema.xml
C:/Windows/System32/inetsrv/config/applicationHost.config
C:/inetpub/logs/LogFiles/W3SVC1/u_ex[YYMMDD].log

Linux

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
/etc/passwd
/etc/shadow
/etc/aliases
/etc/anacrontab
/etc/apache2/apache2.conf
/etc/apache2/httpd.conf
/etc/at.allow
/etc/at.deny
/etc/bashrc
/etc/bootptab
/etc/chrootUsers
/etc/chttp.conf
/etc/cron.allow
/etc/cron.deny
/etc/crontab
/etc/cups/cupsd.conf
/etc/exports
/etc/fstab
/etc/ftpaccess
/etc/ftpchroot
/etc/ftphosts
/etc/groups
/etc/grub.conf
/etc/hosts
/etc/hosts.allow
/etc/hosts.deny
/etc/httpd/access.conf
/etc/httpd/conf/httpd.conf
/etc/httpd/httpd.conf
/etc/httpd/logs/access_log
/etc/httpd/logs/access.log
/etc/httpd/logs/error_log
/etc/httpd/logs/error.log
/etc/httpd/php.ini
/etc/httpd/srm.conf
/etc/inetd.conf
/etc/inittab
/etc/issue
/etc/lighttpd.conf
/etc/lilo.conf
/etc/logrotate.d/ftp
/etc/logrotate.d/proftpd
/etc/logrotate.d/vsftpd.log
/etc/lsb-release
/etc/motd
/etc/modules.conf
/etc/motd
/etc/mtab
/etc/my.cnf
/etc/my.conf
/etc/mysql/my.cnf
/etc/network/interfaces
/etc/networks
/etc/npasswd
/etc/passwd
/etc/php4.4/fcgi/php.ini
/etc/php4/apache2/php.ini
/etc/php4/apache/php.ini
/etc/php4/cgi/php.ini
/etc/php4/apache2/php.ini
/etc/php5/apache2/php.ini
/etc/php5/apache/php.ini
/etc/php/apache2/php.ini
/etc/php/apache/php.ini
/etc/php/cgi/php.ini
/etc/php.ini
/etc/php/php4/php.ini
/etc/php/php.ini
/etc/printcap
/etc/profile
/etc/proftp.conf
/etc/proftpd/proftpd.conf
/etc/pure-ftpd.conf
/etc/pureftpd.passwd
/etc/pureftpd.pdb
/etc/pure-ftpd/pure-ftpd.conf
/etc/pure-ftpd/pure-ftpd.pdb
/etc/pure-ftpd/putreftpd.pdb
/etc/redhat-release
/etc/resolv.conf
/etc/samba/smb.conf
/etc/snmpd.conf
/etc/ssh/ssh_config
/etc/ssh/sshd_config
/etc/ssh/ssh_host_dsa_key
/etc/ssh/ssh_host_dsa_key.pub
/etc/ssh/ssh_host_key
/etc/ssh/ssh_host_key.pub
/etc/sysconfig/network
/etc/syslog.conf
/etc/termcap
/etc/vhcs2/proftpd/proftpd.conf
/etc/vsftpd.chroot_list
/etc/vsftpd.conf
/etc/vsftpd/vsftpd.conf
/etc/wu-ftpd/ftpaccess
/etc/wu-ftpd/ftphosts
/etc/wu-ftpd/ftpusers
/logs/pure-ftpd.log
/logs/security_debug_log
/logs/security_log
/opt/lampp/etc/httpd.conf
/opt/xampp/etc/php.ini
/proc/cpuinfo
/proc/filesystems
/proc/interrupts
/proc/ioports
/proc/meminfo
/proc/modules
/proc/mounts
/proc/stat
/proc/swaps
/proc/version
/proc/self/net/arp
/root/anaconda-ks.cfg
/usr/etc/pure-ftpd.conf
/usr/lib/php.ini
/usr/lib/php/php.ini
/usr/local/apache/conf/modsec.conf
/usr/local/apache/conf/php.ini
/usr/local/apache/log
/usr/local/apache/logs
/usr/local/apache/logs/access_log
/usr/local/apache/logs/access.log
/usr/local/apache/audit_log
/usr/local/apache/error_log
/usr/local/apache/error.log
/usr/local/cpanel/logs
/usr/local/cpanel/logs/access_log
/usr/local/cpanel/logs/error_log
/usr/local/cpanel/logs/license_log
/usr/local/cpanel/logs/login_log
/usr/local/cpanel/logs/stats_log
/usr/local/etc/httpd/logs/access_log
/usr/local/etc/httpd/logs/error_log
/usr/local/etc/php.ini
/usr/local/etc/pure-ftpd.conf
/usr/local/etc/pureftpd.pdb
/usr/local/lib/php.ini
/usr/local/php4/httpd.conf
/usr/local/php4/httpd.conf.php
/usr/local/php4/lib/php.ini
/usr/local/php5/httpd.conf
/usr/local/php5/httpd.conf.php
/usr/local/php5/lib/php.ini
/usr/local/php/httpd.conf
/usr/local/php/httpd.conf.ini
/usr/local/php/lib/php.ini
/usr/local/pureftpd/etc/pure-ftpd.conf
/usr/local/pureftpd/etc/pureftpd.pdn
/usr/local/pureftpd/sbin/pure-config.pl
/usr/local/www/logs/httpd_log
/usr/local/Zend/etc/php.ini
/usr/sbin/pure-config.pl
/var/adm/log/xferlog
/var/apache2/config.inc
/var/apache/logs/access_log
/var/apache/logs/error_log
/var/cpanel/cpanel.config
/var/lib/mysql/my.cnf
/var/lib/mysql/mysql/user.MYD
/var/local/www/conf/php.ini
/var/log/apache2/access_log
/var/log/apache2/access.log
/var/log/apache2/error_log
/var/log/apache2/error.log
/var/log/apache/access_log
/var/log/apache/access.log
/var/log/apache/error_log
/var/log/apache/error.log
/var/log/apache-ssl/access.log
/var/log/apache-ssl/error.log
/var/log/auth.log
/var/log/boot
/var/htmp
/var/log/chttp.log
/var/log/cups/error.log
/var/log/daemon.log
/var/log/debug
/var/log/dmesg
/var/log/dpkg.log
/var/log/exim_mainlog
/var/log/exim/mainlog
/var/log/exim_paniclog
/var/log/exim.paniclog
/var/log/exim_rejectlog
/var/log/exim/rejectlog
/var/log/faillog
/var/log/ftplog
/var/log/ftp-proxy
/var/log/ftp-proxy/ftp-proxy.log
/var/log/httpd/access_log
/var/log/httpd/access.log
/var/log/httpd/error_log
/var/log/httpd/error.log
/var/log/httpsd/ssl.access_log
/var/log/httpsd/ssl_log
/var/log/kern.log
/var/log/lastlog
/var/log/lighttpd/access.log
/var/log/lighttpd/error.log
/var/log/lighttpd/lighttpd.access.log
/var/log/lighttpd/lighttpd.error.log
/var/log/mail.info
/var/log/mail.log
/var/log/maillog
/var/log/mail.warn
/var/log/message
/var/log/messages
/var/log/mysqlderror.log
/var/log/mysql.log
/var/log/mysql/mysql-bin.log
/var/log/mysql/mysql.log
/var/log/mysql/mysql-slow.log
/var/log/proftpd
/var/log/pureftpd.log
/var/log/pure-ftpd/pure-ftpd.log
/var/log/secure
/var/log/vsftpd.log
/var/log/wtmp
/var/log/xferlog
/var/log/yum.log
/var/mysql.log
/var/run/utmp
/var/spool/cron/crontabs/root
/var/webmin/miniserv.log
/var/www/log/access_log
/var/www/log/error_log
/var/www/logs/access_log
/var/www/logs/error_log
/var/www/logs/access.log
/var/www/logs/error.log
~/.atfp_history
~/.bash_history
~/.bash_logout
~/.bash_profile
~/.bashrc
~/.gtkrc
~/.login
~/.logout
~/.mysql_history
~/.nano_history
~/.php_history
~/.profile
~/.ssh/authorized_keys
~/.ssh/id_dsa
~/.ssh/id_dsa.pub
~/.ssh/id_rsa
~/.ssh/id_rsa.pub
~/.ssh/identity
~/.ssh/identity.pub
~/.viminfo
~/.wm_style
~/.Xdefaults
~/.xinitrc
~/.Xresources
~/.xsession

SSRF

服务端请求伪造漏洞介绍

SSRF 漏洞,全称 Server Side Request Forgery (服务端请求伪造),该漏洞允许攻击者利用ssrf使服务器端应用程序向其他资源发起请求(对内网,或者互联网其他网站)。

ssrf常用于探测内网服务、端口、利用各种协议读文件,打redis等操作

常出现在后端存在向其他服务器发起请求的功能点,且未作好过滤,请求的目标参数直接或间接地可控

Java 中支持的协议

Java 网络请求支持的协议包括:http,https,file,ftp,mailto,jar,netdoc

https://github.com/frohoff/jdk8u-jdk/tree/master/src/share/classes/sun/net/www/protocol

值得一提的是,JDK1.7开始就不支持gopher协议了,所以java中的ssrf,可能不像php中有那么多利用面

1
2
3
4
5
| PHP     | --wite-curlwrappers且php版本至少为5.3 |
| Java | 小于JDK1.7 |
| Curl | 低版本不支持 |
| Perl | 支持 |
| ASP.NET | 小于版本3 |

示例代码

在实际项目中,该部分功能代码多协助工具类中,方便调用,以下示例的方法有些支持多个协议,包括file协议,可以结合其他协议进行深入的利用,比如任意文件读取

创建ssrfdemoSpring boot 工程项目,选择Spring Initializer、Java 8、Maven 项目、 Spring web

HttpClient

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

HttpClient 实现了 HTTP1.0 和 HTTP1.1。也实现了 HTTP 全部的方法,如: GET,POST, PUT,DELETE, HEAD, OPTIONS, TRACE

官方文档:https://hc.apache.org/httpcomponents-client-5.3.x/index.html

相关实现:

可以在pom.xml中引入HttpClient依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>

重新加载Maven变更后创建一个HttpClientController的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
package com.example.ssrfdemo.Controller;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.InputStreamReader;

@RestController
@RequestMapping("/ssrfvul")
public class HttpClientController {
@RequestMapping("/httpclient/vul")
public String httpclientvul(String url) throws Exception {
StringBuilder sb = new StringBuilder();

CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = client.execute(httpGet);

BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();

}
}

该示例中,使用了 execute() 方法执行了 HTTP 请求

payload:

1
http://127.0.0.1:8080/ssrfvul/httpclient/vul?url=https://www.baidu.com

HttpAsyncClient

HttpAsyncClient 是一个异步的 HTTP 客户端开发包,基于 HttpCore NIO 和 HttpClient 组件。

HttpAsyncClient 的出现并不是为了替换 HttpClient,而是作为一个补充用于需要大量并发连接,对性能要求非常高的基于 HTTP 的原生数据通信,而且提供了事件驱动的 API。

官方文档:https://hc.apache.org/httpcomponents-asyncclient-4.1.x/index.html

相关实现

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.1.3</version>
</dependency>

创建HttpAsyncClientController

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.ssrfdemo.Controller;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.swing.text.html.parser.Entity;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

@RestController
@RequestMapping("/ssrfvul")
public class HttpAsyncClientController {
@GetMapping("/httpasync/vul")
public String httpasyncvul(String url) throws Exception{
CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
try {
client.start();
HttpGet httpget = new HttpGet(url);
Future<HttpResponse> future = client.execute(httpget, null);
HttpResponse response = future.get();
return EntityUtils.toString(response.getEntity());
} catch (IOException | ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
try {
client.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

依旧使用execute() 方法执行HTTP 请求

payload:

1
http://127.0.0.1:8080/ssrfvul/httpasync/vul?url=https://www.baidu.com

java.net.URLConnection

java.net.URLConnection,是Java 原生的HTTP 请求方法。URLConnection 类包含了许多方法可以让你的 URL 在网络上通信。此类的实例既可用于读取URL 所引用的资源,也可用于写入 URL 所引用资源。

参考:https://docs.oracle.com/javase/8/docs/api/java/net/URLConnection.html

由于是java原生的方法,所以已经封装在jdk中,不需要额外引入依赖

相关实现

创建UrlConnectionController类,写入如下代码

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.ssrfdemo.Controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

@RestController
@RequestMapping("/ssrfvul")
public class UrlConnectionController {
@RequestMapping("/urlconnection/vul")
public String urlconnectionvul(String url) throws Exception {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
URLConnection conn = url1.openConnection();
conn.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
return result.toString();
}
}


使用 openConnection() 方法执行HTTP 请求

1
http://127.0.0.1:8080/ssrfvul/urlconnection/vul?url=https://www.baidu.com

java.net.HttpURLConnection

HttpURLConnection 继承自 URLConnection。可以向指定网站发起GET 或POST请求

https://docs.oracle.com/javase/8/docs/api/java/net/HttpURLConnection.html

同样也是不需要额外引入依赖

相关实现

创建HttpUrlConnectionController类,写入如下代码

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 com.example.ssrfdemo.Controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

@RestController
@RequestMapping("/ssrfvul")
public class HttpUrlConnectionController {
@RequestMapping("/httpurlconnection/vul")
public String httpurlconnectionvul(String url) throws Exception{
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
}
return result.toString();
}
}

使用 openConnection() 方法执行HTTP 请求

1
http://127.0.0.1:8080/ssrfvul/httpurlconnection/vul?url=https://www.baidu.com

java.net.URL

java.net.URL包中定义了 URL 类,该类用来处理有关 URL 的内容。通过使用 URL 对象的 openStream()方法创建打开指定 URL 链接,以获取输入流资源内容。

参考:https://docs.oracle.com/javase/8/docs/api/java/net/URL.html

同样不需要额外引入依赖

相关实现

创建UrlController类,写入如下代码

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;

@RestController
@RequestMapping("/ssrfvul")
public class UrlController {
@GetMapping("/url/vul")
public String urlvul(String url) throws Exception {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
BufferedReader reader = new BufferedReader(new InputStreamReader(url1.openStream()));
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
return result.toString();
}
}

使用 openStream() 方法执行HTTP 请求

1
http://127.0.0.1:8080/ssrfvul/url/vul?url=https://www.baidu.com

java.net.Socket

java.net.Socket 是 Java 套接字编程使用的类。提供了两台计算机之间的通信机制。

在Java 代码审计中,可能会遇见使用 Socket 判断IP 与端口连通性的代码。如果IP 和端口接受外部
输入,那么极有可能存在SSRF 漏洞。

参考:https://docs.oracle.com/javase/8/docs/api/java/net/Socket.html

相关实现

不需要额外引入依赖

创建SocketController类,写入如下代码

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;

@RestController
@RequestMapping("/ssrfvul")
public class SocketController {
@GetMapping("/socket/vul")
public String socketvul(String url, int port) throws Exception{
StringBuilder result = new StringBuilder();
Socket socket = new Socket(url, port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
return result.toString();
}
}

使用getInputStream方法去执行请求,通过该接口可以去探测内网的其他资产端口开放情况

1
http://127.0.0.1:8080/ssrfvul/url/vul?url=172.22.10.1&port=80

测试:假设你的虚拟机(可与本机互通)ip为172.22.10.1,起个nc监听80端口

1
echo "hello world" | nc -lvvp 80

通过执行上述的payload,可以在nc关闭后接收到”hello world”字符串

OkHttp

OKHttp 是一个网络请求框架,OKHttp 会为每个客户端创建自己的连接池和线程池。重用连接和线程可以减少延迟并节省内存。

OkHttp 中请求方式分为同步请求(client.newCall(request).execute() )和异步请求(client.newCall(request).enqueue() )两种

参考:https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/

相关实现

在pom.xml引入相关依赖

1
2
3
4
5
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>

新建一个OkHttpClientController类,写入如下代码

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 com.example.ssrfdemo.Controller;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/ssrfvul")
public class OkHttpClientController {
@RequestMapping("/okhttpclient/vul")
public String okhttpclientvul(String url) throws Exception{
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);
}
}
}

1
http://127.0.0.1:8080/ssrfvul/okhttpclient/vul?url=https://www.baidu.com

ImageIO

ImageIO 是Java 读写图片操作的一个类。在代码审计中,如果目标使用了

ImageIO.read 读取图片,且读取的图片地址可控的话,可能会存在SSRF 漏洞

参考:https://docs.oracle.com/javase/8/docs/api/javax/imageio/ImageIO.html

同样的javax.imageio.ImageIO 也已封装在JDK 中,不需要额外引入依赖

相关实现

新建ImageIOController类,写入如下代码

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import java.awt.*;
import java.net.URL;

@RestController
@RequestMapping("/ssrfvul")
public class ImageIOController {
@GetMapping("/imageio/vul")
public String imageiovul(String url) throws Exception {
// StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
Image image = ImageIO.read(url1);
return image.toString();
}
}

这里使用了 ImageIO.read() 方法执行了HTTP 请求

1
http://127.0.0.1:8080/ssrfvul/imageio/vul?url=https://www.baidu.com/img/flexible/logo/pc/result.png

Hutool

Hutool 是一个小而全的Java 工具类库,通过静态方法封装,降低相关API 的学习成本,提高工作效率,使Java 拥有函数式语言般的优雅。

在Hutool 中,也实现了HTTP 客户端,Hutool-http 针对JDK 的 HttpUrlConnection 做一层封装,简化了HTTPS 请求、文件上传、Cookie 记忆等操作

Hutool-http 的核心集中在两个类:

  • HttpRequest

  • HttpResponse

参考:http概述

相关实现

引入 Hutool 依赖

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.13</version>
</dependency>

新建HutoolController类,并写入如下代码

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

import cn.hutool.http.HttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/ssrfvul")
public class HutoolController {
@GetMapping("/hutool/vul")
public String hutoolvul(String url) throws Exception {
HttpRequest httpReuest = HttpRequest.get(url);
return httpReuest.execute().body();
}
}

同样使用 execute() 方法执行了HTTP 请求

1
http://127.0.0.1:8080/ssrfvul/hutool/vul?url=https://www.baidu.com

Jsoup

Jsoup 是基于 Java 的 HTML 解析器,可以从指定的 URL 中解析 HTML 内容

参考:https://jsoup.org/

相关实现

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.1</version>
</dependency>

新建JsoupController类,写入如下代码

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

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

@RestController
@RequestMapping("/ssrfvul")
public class JsoupController {
@GetMapping("/jsoup/vul")
public String jsoupvul(String url) throws Exception{
Document document = Jsoup.connect(url).get();
return document.toString();
}
}

使用 Jsoup.connect() 方法执行HTTP 请求

1
http://127.0.0.1:8080/ssrfvul/jsoup/vul?url=https://www.baidu.com

RestTemplate

RestTemplate 是从Spring3.0 开始支持的一个HTTP 请求工具,它提供了常见的 REST 请求方案的模版,
例如GET 请求、POST 请求、PUT 请求等等。

从名称上来看,是更针对RESTFUL风格API 设计的。但通过他调用普通的HTTP 接口也是可以的。

参考:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html

相关实现

引入依赖RestTemplate

其实就RestTemplate包含在spring-web 这个包下面,引入依赖spring-web就可以了

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

新建RestTemplateController类,并写入如下代码

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/ssrfvul")
public class RestTemplateController {
@GetMapping("/resttempate/vul")
public String resttempatevul(String url) throws Exception {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(url, String.class);
}
}

使用getForObject方法执行http请求

1
http://127.0.0.1:8080/ssrfvul/resttempate/vul?url=https://www.baidu.com

SSRF 代码审计

下列是一些可能涉及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
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

SSRF 修复

参考该项目:https://github.com/j3ers3/Hello-Java-Sec/blob/master/src/main/java/com/best/hello/controller/SSRF.java

  • 白名单限制http/https协议
  • 黑名单限制非内网地址
  • 先解析域名在判断ip,避免域名解析为内网ip进行绕过
  • 不允许重定向
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
@GetMapping("/HTTPURLConnection/safe")
public String HTTPURLConnection(String url) {
// 校验 url 是否以 http 或 https 开头
if (!Security.isHttp(url)) {
log.error("[HTTPURLConnection] 非法的 url 协议:" + url);
return "不允许非http/https协议!!!";
}

// 解析 url 为 IP 地址
String ip = Security.urltoIp(url);
log.info("[HTTPURLConnection] SSRF解析IP:" + ip);

// 校验 IP 是否为内网地址
if (Security.isIntranet(ip)) {
log.error("[HTTPURLConnection] 不允许访问内网:" + ip);
return "不允许访问内网!!!";
}

// 访问 url
try {
return HttpClientUtils.HTTPURLConnection(url);
} catch (Exception e) {
log.error("[HTTPURLConnection] 访问失败:" + e.getMessage());
return "访问失败,请稍后再试!!!";
}
}
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
/**
* 判断是否为内网IP
*
* @return True or False
*/
public static boolean isIntranet(String ip) {
log.info("isIntranet: " + ip);
Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|^(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|^(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|^(192\\.168\\.\\d{1,3}\\.\\d{1,3})$");
Matcher match = reg.matcher(ip);
return match.find();
}

/**
* 判断是否是http类型
*
* @return True or False
*/
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}

/**
* 判断url是否在白名单内
*/
public static boolean isWhite(String url) {
List<String> url_list = new ArrayList<String>();
url_list.add("baidu.com");
url_list.add("www.baidu.com");
url_list.add("oa.baidu.com");

// 从url转换host
URI uri = null;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
System.out.print(e);
}
assert uri != null;
String host = uri.getHost().toLowerCase();

return url_list.contains(host);
}

/**
* url转IP
*/
public static String urltoIp(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost().toLowerCase();
// 判断 URL 是否是 IP 地址
if (InetAddressUtils.isIPv4Address(host)) {
return host;
} else {
InetAddress ip = Inet4Address.getByName(host);
return ip.getHostAddress();
}
} catch (Exception e) {
return "127.0.0.1";
}
}
1
2
3
4
// 不允许跳转或判断跳转
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setInstanceFollowRedirects(false); // 不允许重定向或者对重定向后的地址做二次判断
conn.connect();

参考该项目:https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/SecurityUtil.java

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

import org.joychou.config.WebConfig;
import org.joychou.security.ssrf.SSRFChecker;
import org.joychou.security.ssrf.SocketHook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.regex.Pattern;


public class SecurityUtil {

private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
private final static Logger logger = LoggerFactory.getLogger(SecurityUtil.class);


/**
* Determine if the URL starts with HTTP.
*
* @param url url
* @return true or false
*/
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}


/**
* Get http url host.
*
* @param url url
* @return host
*/
public static String gethost(String url) {
try {
URI uri = new URI(url);
return uri.getHost().toLowerCase();
} catch (URISyntaxException e) {
return "";
}
}


/**
* 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。
* 优先判断黑名单,如果满足黑名单return null。
*
* @param url the url need to check
* @return Safe url returns original url; Illegal url returns null;
*/
public static String checkURL(String url) {

if (null == url){
return null;
}

ArrayList<String> safeDomains = WebConfig.getSafeDomains();
ArrayList<String> blockDomains = WebConfig.getBlockDomains();

try {
String host = gethost(url);

// 必须http/https
if (!isHttp(url)) {
return null;
}

// 如果满足黑名单返回null
if (blockDomains.contains(host)){
return null;
}
for(String blockDomain: blockDomains) {
if(host.endsWith("." + blockDomain)) {
return null;
}
}

// 支持多级域名
if (safeDomains.contains(host)){
return url;
}

// 支持一级域名
for(String safedomain: safeDomains) {
if(host.endsWith("." + safedomain)) {
return url;
}
}
return null;
} catch (NullPointerException e) {
logger.error(e.toString());
return null;
}
}


/**
* 通过自定义白名单域名处理SSRF漏洞。如果URL范围收敛,强烈建议使用该方案。
* 这是最简单也最有效的修复方式。因为SSRF都是发起URL请求时造成,大多数场景是图片场景,一般图片的域名都是CDN或者OSS等,所以限定域名白名单即可完成SSRF漏洞修复。
*
* @author JoyChou @ 2020-03-30
* @param url 需要校验的url
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFByWhitehosts(String url) {
return SSRFChecker.checkURLFckSSRF(url);
}


/**
* 解析URL的IP,判断IP是否是内网IP。如果有重定向跳转,循环解析重定向跳转的IP。不建议使用该方案。
* 存在的问题:
* 1、会主动发起请求,可能会有性能问题
* 2、设置重定向跳转为第一次302不跳转,第二次302跳转到内网IP 即可绕过该防御方案
* 3、TTL设置为0会被绕过
*
* @param url check的url
* @return 安全返回true,危险返回false
*/
@Deprecated
public static boolean checkSSRF(String url) {
int checkTimes = 10;
return SSRFChecker.checkSSRF(url, checkTimes);
}


/**
* 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且TTL默认不为0。
* 存在问题:
* 1、TTL为0会被绕过
* 2、使用重定向可绕过
*
* @param url The url that needs to check.
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFWithoutRedirect(String url) {
if(url == null) {
return false;
}
return !SSRFChecker.isInternalIpByUrl(url);
}

/**
* Check ssrf by hook socket. Start socket hook.
*
* @author liergou @ 2020-04-04 02:15
*/
public static void startSSRFHook() throws IOException {
SocketHook.startHook();
}

/**
* Close socket hook.
*
* @author liergou @ 2020-04-04 02:15
**/
public static void stopSSRFHook(){
SocketHook.stopHook();
}



/**
* Filter file path to prevent path traversal vulns.
*
* @param filepath file path
* @return illegal file path return null
*/
public static String pathFilter(String filepath) {
String temp = filepath;

// use while to sovle multi urlencode
while (temp.indexOf('%') != -1) {
try {
temp = URLDecoder.decode(temp, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.info("Unsupported encoding exception: " + filepath);
return null;
} catch (Exception e) {
logger.info(e.toString());
return null;
}
}

if (temp.contains("..") || temp.charAt(0) == '/') {
return null;
}

return filepath;
}


public static String cmdFilter(String input) {
if (!FILTER_PATTERN.matcher(input).matches()) {
return null;
}

return input;
}


/**
* 过滤mybatis中order by不能用#的情况。
* 严格限制用户输入只能包含<code>a-zA-Z0-9_-.</code>字符。
*
* @param sql sql
* @return 安全sql,否则返回null
*/
public static String sqlFilter(String sql) {
if (!FILTER_PATTERN.matcher(sql).matches()) {
return null;
}
return sql;
}

/**
* 将非<code>0-9a-zA-Z/-.</code>的字符替换为空
*
* @param str 字符串
* @return 被过滤的字符串
*/
public static String replaceSpecialStr(String str) {
StringBuilder sb = new StringBuilder();
str = str.toLowerCase();
for(int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
// 如果是0-9
if (ch >= 48 && ch <= 57 ){
sb.append(ch);
}
// 如果是a-z
else if(ch >= 97 && ch <= 122) {
sb.append(ch);
}
else if(ch == '/' || ch == '.' || ch == '-'){
sb.append(ch);
}
}

return sb.toString();
}

public static void main(String[] args) {
}

}

Java安全 Web漏洞(上)
https://www.smal1.black/Java安全 Web漏洞(上).html
作者
Small Black
发布于
2024年11月23日
许可协议