java中的sql注入、文件上传下载、目录穿越、ssrf的相关利用与审计
SQL 注入 各种原理啥的学php的时候就过的差不多了,这里直接跳过。
这里主要看看java场景下各类sql注入是如何呈现的
环境搭建 创建工程 在idea上创建一个sqliDemo
的SpringBoot
工程
并选择如下依赖:
web→ Spring Web
SQL→ JDBC API
SQL→Mybatis Framework
SQL→Mysql Driver
数据库初始化 创建一个 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 =sqliDemoserver.port =8088 spring.datasource.url =jdbc:mysql://localhost:3306 /sqlidemo?AllowPublicKeyRetrieval=true &useSSL=false &serverTimezone=UTCspring.datasource.username =rootspring.datasource.password =1234 qwerspring.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.Statement
:https://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);
官方文档:
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排序不生效
常见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) ; }
在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 > </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-locations =classpath:mapper/*.xmlmybatis.type-aliases-package =com.hey.sqlidemo.mybatisinjection
启动该项目
Order by 注入 在上面的代码中,由于order by的特殊原因,导致没对其使用预编译,而是使用了${}
导致sql注入的出现
注入点:
1 http:// 127.0 .0.1 :8088 /sqli/my batis/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 <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/my batis/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/my batis/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();String sql = "SELECT * FROM users WHERE name like ?" ;PreparedStatement pre = conn.prepareStatement(sql); pre.setString(1 , "%" +name+"%" );ResultSet rs = pre.executeQuery();String sql = "select * from user where id in (" ; Integer[] ids = new Integer []{1 ,2 ,3 }; StringBuilder placeholderSql = new StringBuilder (sql); for (int i = 0 , size = ids.length; i < size; i++) { placeholderSql.append("?" ); if (i != size - 1 ) { placeholderSql.append("," ); } } placeholderSql.append(")" ); PreparedStatement pre = conn.prepareStatement(placeholderSql.toString()); for (int i = 0 , size = ids.length; i < size; i++) { pre.setInt(i + 1 , ids[i]); }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 ;UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), id);String sql = "select * from user where name like ?" ;String like_name = "%" + "foo" + "%" ;UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), like_name);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 > <select id ="select" parameterType ="java.lang.String" resultMap ="BaseResultMap" > SELECT * FROM user WHERE name like CONCAT("%", #{name}, "%")</select > <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 { }UserDOMapper userMapper = session.getMapper(UserDOMapper.class);UserDOExample userExample = new UserDOExample (); UserDOExample.Criteria criteria = userExample.createCriteria(); criteria.andNameEqualTo("foo" ); criteria.andNameLike("%foo%" ); 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 <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("." )); 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.lastIndexOf ('.' ) );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文档的注释内容之中即可。 //可以用来读文件 //可以用来执行命令 //也是读文件 与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 FileOutputStreamDiskFileItemFactory 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;@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" ); 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()); @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 . = %252 e / = %252 f \ = %255 c
URL Unicode编码 1 2 3 . = %u002e / = %u2215 \ = %u2216
URL UTF-8 与 超长UTF-8 编码 1 2 3 . = %c0 %2 e, %e0 %40 %ae , %c0ae / = %c0 %af , %e0 %80 %af , %c0 %2 f \ = %c0 %5 c , %c0 %80 %5 c
空字节截断 也就是00阶段,即空字节URL编码绕过,用于对一些判断后缀名的绕过,
1 ../../ ../../ ../passwd%00 .jpg
双重 ../ 仅做一次判断删除或替换 ../
情况下,可使用 ..././
方式绕过
相关敏感文件 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/My SQL/data/ hostname.err C:/Program Files/My SQL/data/my sql-bin.log C:/Program Files/My SQL/data/my sql.err C:/Program Files/My SQL/data/my sql.log C:/Program Files/My SQL/my.ini C:/Program Files/My SQL/my.cnf C:/Program Files/My SQL/MySQL Server 5.0/ data/hostname.err C:/Program Files/My SQL/MySQL Server 5.0/ data/mysql-bin.log C:/Program Files/My SQL/MySQL Server 5.0/ data/mysql.err C:/Program Files/My SQL/MySQL Server 5.0/ data/mysql.log C:/Program Files/My SQL/MySQL Server 5.0/my .cnf C:/Program Files/My SQL/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/U nattended.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/My SQL/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/g roups/etc/g rub.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/i netd.conf/etc/i nittab/etc/i ssue/etc/ lighttpd.conf/etc/ lilo.conf/etc/ logrotate.d/ftp/etc/ logrotate.d/proftpd/etc/ logrotate.d/vsftpd.log/etc/ lsb-release/etc/m otd/etc/m odules.conf/etc/m otd/etc/m tab/etc/my .cnf/etc/my .conf/etc/my sql/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/i nterrupts/proc/i oports/proc/m eminfo/proc/m odules/proc/m ounts/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/my sql/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/m ainlog/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/my sql-bin.log/var/ log/mysql/my sql.log/var/ log/mysql/my sql-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/my sql.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/i d_dsa ~/.ssh/i d_dsa.pub ~/.ssh/i d_rsa ~/.ssh/i d_rsa.pub ~/.ssh/i dentity ~/.ssh/i dentity.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协议,可以结合其他协议进行深入的利用,比如任意文件读取
创建ssrfdemo
的Spring 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/u rlconnection/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/u rl/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/u rl/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 { URL url1 = new URL (url); Image image = ImageIO.read(url1); return image.toString(); } }
这里使用了 ImageIO.read() 方法执行了HTTP 请求
1 http:// 127.0 .0.1 :8080 /ssrfvul/im ageio/vul?url=https:/ /www.baidu.com/img /flexible/ logo/pc/ result.png
Hutool 是一个小而全的Java 工具类库,通过静态方法封装,降低相关API 的学习成本,提高工作效率,使Java 拥有函数式语言般的优雅。
在Hutool 中,也实现了HTTP 客户端,Hutool-http 针对JDK 的 HttpUrlConnection 做一层封装,简化了HTTPS 请求、文件上传、Cookie 记忆等操作
Hutool-http 的核心集中在两个类:
参考: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) { if (!Security.isHttp(url)) { log.error("[HTTPURLConnection] 非法的 url 协议:" + url); return "不允许非http/https协议!!!" ; } String ip = Security.urltoIp(url); log.info("[HTTPURLConnection] SSRF解析IP:" + ip); if (Security.isIntranet(ip)) { log.error("[HTTPURLConnection] 不允许访问内网:" + ip); return "不允许访问内网!!!" ; } 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 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(); }public static boolean isHttp (String url) { return url.startsWith("http://" ) || url.startsWith("https://" ); }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" ); 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); }public static String urltoIp (String url) { try { URI uri = new URI (url); String host = uri.getHost().toLowerCase(); 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.open Connection() ; 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); public static boolean isHttp (String url) { return url.startsWith("http://" ) || url.startsWith("https://" ); } public static String gethost (String url) { try { URI uri = new URI (url); return uri.getHost().toLowerCase(); } catch (URISyntaxException e) { return "" ; } } 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); if (!isHttp(url)) { return 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 ; } } public static boolean checkSSRFByWhitehosts (String url) { return SSRFChecker.checkURLFckSSRF(url); } @Deprecated public static boolean checkSSRF (String url) { int checkTimes = 10 ; return SSRFChecker.checkSSRF(url, checkTimes); } public static boolean checkSSRFWithoutRedirect (String url) { if (url == null ) { return false ; } return !SSRFChecker.isInternalIpByUrl(url); } public static void startSSRFHook () throws IOException { SocketHook.startHook(); } public static void stopSSRFHook () { SocketHook.stopHook(); } public static String pathFilter (String filepath) { String temp = filepath; 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; } public static String sqlFilter (String sql) { if (!FILTER_PATTERN.matcher(sql).matches()) { return null ; } return sql; } 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); if (ch >= 48 && ch <= 57 ){ sb.append(ch); } 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) { } }