Java代码审计漏洞点(基础)
2025-06-10 10:34:09 # 代码审计

审计流程

Servlet生命周期

image-20250401162609929

Servlet生命周期可被定义为从创建直到毁灭的整个过程。以下是Servlet遵循的过程[1]

  • Servlet初始化后调用 init () 方法。

  • Servlet调用 service() 方法来处理客户端的请求。

  • Servlet销毁前调用 destroy() 方法。

  • 最后,Servlet是由JVM的垃圾回收器进行垃圾回收的。

简单介绍一下使用到的几种方法。

  • init()方法

init方法被设计成只调用一次。它在第一次创建Servlet时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化。

  • service()方法

service()方法是执行实际任务的主要方法。Servlet容器(即Web服务器)调用service()方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个Servlet请求时,服务器会产生一个新的线程并调用服务。service()方法检查HTTP请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用doGet、doPost、doPut,doDelete等方法

  • destroy方法

destroy()方法只会被调用一次,在Servlet生命周期结束时被调用。destroy()方法可以让您的Servlet关闭数据库连接、停止后台线程、把Cookie列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

Servlet审计

Controller文件,找mapping路由。

Springboot审计

Controller再找mapping路由。

SQL注入

Java中常见的连接数据库的方式:MybatisJDBCHibernate等。在Java中存在注入的参数类型要为字符型。

JDBC中的SQL注入

JDBC中常用的几个执行SQL语句的方式[2],分别为:

  • Statement:用于执行普通的SQL语句,不带有参数。

  • PrepareedStatement:用于执行预编译的SQL语句,可以带有参数,防止SQL注入。

  • CallableStatement:用于执行数据库存储过程。

动态拼接参数

直接将参数拼接到了SQL语句中执行。

1
2
3
//动态拼接字符串
Stringsql="select * from users where id = '"+ id +"'";
ResultSetrs= statement.executeQuery(sql);

错误使用预编译

原理:PreparedStatement不会对拼接的字符串进行预处理。

正确的使用预编译,不存在注入。

1
2
3
4
5
// 安全代码
Stringsql="select * from users where id = ?";
PreparedStatementpreparestatement= conn.prepareStatement(sql);
        preparestatement.setString(1, username);
ResultSetrs= preparestatement.executeQuery();

错误使用预编译,存在注入!!!

1
2
3
4
//没有正确使用预编译方式,SQL语句还是进行了动态拼接
Stringsql="select * from users where id = '"+ id +"'";
PreparedStatementpreparestatement= conn.prepareStatement(sql);
ResultSetrs= preparestatement.executeQuery();

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

Order By注入

在使用PreparedStatement预编译时,会将传递的任意参数预编译,进而变为字符串。而order by需要的是字段名,而不是字符串。所以,在使用order by时,就不能使用预编译了。因此,order by位置也是出现sql注入重灾区。

字段名SQL语句的一部分,而字符串不是。预编译后SQL语句就会变成ORDER BY 'username',故报错。

1
2
3
4
5
//错误使用,报错⚠️。
Stringsql="SELECT * FROM users ORDER BY ?";
PreparedStatementpreparestatement= conn.prepareStatement(sql);
        preparestatement.setString(1, username);
ResultSetrs= preparestatement.executeQuery();

所以,代码中常常使用的方法是拼接字段,拼接不当就会出现注入问题。

1
2
3
4
//order by 的sql注入。
Stringsql="select * from users"+" order by "+ id;
PreparedStatementpreparestatement= conn.prepareStatement(sql);
ResultSetrs= preparestatement.executeQuery();

实际代码应用中,常常使用白名单的方式去限制字段名,进而防止注入。

Mybatis中的SQL注入

MyBatis是一个流行的 Java 持久层框架,用于简化与数据库的交互。它通过提供一种对象关系映射(ORM)的方式,使得 Java 程序员可以更方便地执行数据库操作(如查询、插入、更新、删除)。与传统的 JDBC 相比,MyBatis 提供了更高层次的抽象,并且比 JPAHibernate 等)更灵活、易于配置。

MyBatis基础知识

MyBatis主要特点
  • SQL映射:MyBatis允许开发者使用原生的SQL语句来操作数据库。它将SQL查询与Java方法进行映射,使得开发者可以完全控制SQL执行过程,同时享受框架带来的简化和支持。
  • 动态SQL: MyBatis支持动态生成SQL查询,提供了<if><choose><foreach>等标签,可以根据不同的条件动态生成SQL,避免了冗长且复杂的条件判断逻辑。
  • 简洁的XML配置: MyBatis 使用 XML 文件来配置数据库映射,开发者通过定义 SQL 语句和映射规则,控制 Java 对象与数据库表之间的映射关系。
  • 接口和注解支持: MyBatis 提供了基于接口的API,开发者可以在 Java 接口中定义数据库操作方法,并通过注解或者 XML配置映射 SQL 语句。
  • 强大的缓存机制: MyBatis 提供了一级缓存和二级缓存。一级缓存是基于 SqlSession 级别的缓存,二级缓存是跨 SqlSession 共享的缓存,默认情况下启用。
  • 支持事物管理: MyBatis 支持与 Java 的事务管理整合,确保数据库操作的原子性和一致性。
Mybatis的工作原理
  • **SqlSessionFactory:**这是 MyBatis 的核心工厂类,负责创建 SqlSession 对象。SqlSession 是 MyBatis 中用于执行 SQL 语句的主要接口,它通过配置文件读取数据库连接信息,创建并维护数据库连接。

  • **Mapper XML文件:**在MyBatis中,SQL语句通常在XML配置文件中定义,开发者需要为每个数据库操作编写对应的Mapper文件。这个文件会包含SQL语句及其映射规则。

  • **Mapper接口:**开发者通常会创建接口,定义一些方法,这些方法通过XML或注解来与具体的SQL语句进行绑定。

  • **映射过程:**当应用程序调用 SqlSession 的方法时,MyBatis 会根据 Mapper 接口或注解提供的SQL语句信息执行相应的数据库操作,并将结果自动映射为Java对象,返回给开发者。

Mybatis的使用步骤
  • 配置Mybatis: 通过mybatis-config.xml 文件进行配置,配置数据库连接、缓存、日志等。
  • **定义Mapper: ** 创建Mapper接口或类,定义SQL操作的方法。
  • 编写Mapper XML: 为每个SQL操作编写对应的XML文件,定义SQL语句与Mapper方法的映射关
  • 创建SqlSessionFactory: 通过配置文件创建 SqlSessionFactory,获取 SqlSession
  • **执行SQL操作:**通过SqlSession执行SQL语句,获取查询结果或执行更新操作。

MyBatis的代码示例

以下是一个简单的MyBatis的使用示例。

MyBatis配置文件mybatis-config.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <mappers>
        <mapper resource="com/example/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

Mapper 接口 UserMapper.java

1
2
3
4
public interface UserMapper {
    User getUserById(int id);
    List<User> getAllUsers();
}

Mapper XML 文件 UserMapper.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.example.mapper.UserMapper">
<select id="getUserById" resultType="com.example.model.User">
        SELECT id, name, email FROM users WHEREid=#{id}
</select>

<select id="getAllUsers" resultType="com.example.model.User">
        SELECT id, name, email FROM users
</select>
</mapper>

Java 主程序 Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

publicclassMain{
publicstaticvoidmain(String[] args){
SqlSessionFactorysqlSessionFactory= newSqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
try(SqlSessionsession= sqlSessionFactory.openSession()){
UserMapperuserMapper= session.getMapper(UserMapper.class);
Useruser= userMapper.getUserById(1);
System.out.println(user);
}
}
}

MyBatis中#{}和${}区别

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

对于#{}

#{} 占位符用于预编译参数绑定MyBatis会把 #{} 中的值作为参数传递给数据库查询或更新语句,在执行 SQL时由数据库驱动进行处理。这样做的好处是,参数会被视为数据,进行转义和预编译,不会直接拼接到SQL语句中,防止了SQL注入攻击。

  • 使用场景: #{} 适用于所有情况,尤其是对于动态条件、查询参数等。比如查询条件、更新值等。

示例:

查询的xml

1
2
3
<select id="getUserById" resultType="com.example.User">
    SELECT * FROM users WHERE id = #{id}
</select>

对应的Java代码

1
User user = userMapper.getUserById(1);

哪么最后在执行时,MyBatis 会将 id 的值 1 绑定到 SQL 查询中,生成的SQL语句会是:

1
SELECT * FROM users WHERE id = ?

如果传入的数值为1,那么最终会被解析成

1
SELECT * FROM users WHERE id = "1"
对于${}:

${} 用于直接拼接字符串MyBatis 在解析 SQL 时,${} 不会对传入的参数不做处理,而是直接拼接,MyBatis会把 ${} 中的内容直接替换为对应的参数值,而不进行任何转义或预处理,进而会造成SQL注入漏洞。

  • 使用场景${} 一般用于动态表名、列名、排序字段等场景,通常情况下不应将用户输入的参数直接传入 ${} 中,以防止SQL注入。

存在SQL注入漏洞的${}

假设我们允许用户通过输入标名查询数据,并且直接用${}来拼接SQL

Mapper XML文件:

1
2
3
<select id="findByTable" resultType="com.example.User">
    SELECT * FROM ${tableName} WHERE id = #{id}
</select>

这里的tableNameSQL语句的一部分,直接拼接可导致SQL注入问题。假设注入的数据为users;select database();-- -,查询数据库的名称。

1
SELECT * FROM users;select database();-- -where id = ?

不存在SQL注入的${}用法

Mapper XML文件

1
2
3
<select id="findByColumn" resultType="com.example.User">
    SELECT * FROM users WHERE ${column} = #{value}
</select>

对应的Java代码

1
2
3
String column = "age"; //用户不可控
int value = 25; //用户可控
List<User> users = userMapper.findByColumn(column, value);

最后在执行时,MyBatis 会将 ${column} 替换成 age,而 #{value} 仍然会用 25 作为参数绑定。最终生成的 SQL 语句为:

1
SELECT * FROM users WHERE age = ?

总之,防止SQL注入的关键点是,作为SQL语句中的**字段部分(为SQL语句的一部分,不使用预编译)**不是用户可以任意控制的,这样就可以有效的防止SQL注入了。例如,上述的案例中age是写死的或者写一个白名单数组,只允许传入白名单数组内的内容,这样用户不可任意控制了也就防止了SQL注入。

ORDER BY注入

ORDER BY语句 :用于对查询结果的排序,asc为升序,desc为降序。默认为升序。

比如:

1
select * from users order by username asc;

JDBC中的ORDER BY注入问题,前面已经说过了。

JDBC预编译中ORDER BY注入一样,在ORDER BY语句后面需要是字段名或者字段位置。因此也不能使用Mybatis中预编译的方式。

存在注入的ORDER BY查询,如下UserMapper.xml,这里的sort要可控为任意字符。

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

Java代码如下:

1
2
3
4
@GetMapping("/mybatis/orderby")
public List<User> orderbySql(@RequestParam("sort") String sort){
return userMapper.orderbysqli(sort);
}

IN注入

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

比如:

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

如上所示,IN在查询某个范围数据是会用到多个参数,在Mybtis中如果直接使用占位符 #{}进行查询会将这些参数看做一个整体,查询会报错。 因此很多开发人员可能会使用拼接符${}对参数进行查询,从而造成了SQL注入漏洞。

例如:

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

正确的做法是需要使用foreach配合占位符 #{} 实现IN查询。比如:

1
2
3
4
5
6
7
8
<!-- where in 查询场景 -->
</select>
<select id="findUsers" resultType="User">
    SELECT * FROM users WHERE id IN 
    <foreach item="id" collection="ids" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

存在SQL注入漏洞的代码。

1
2
3
<select id="findUsers" resultType="User">
    SELECT * FROM users WHERE id IN (${ids})
</select>

对应的Java代码如下:

1
2
3
4
@GetMapping("/mybatis/injection")
publicList<User>INuser(@RequestParam("id") String id){
return userMapper.INuser(id);
}

LIKE注入

LIKE 子句用于在 SQL 查询中进行模糊匹配。当使用用户输入的内容作为 LIKE 条件时,应该非常小心,因为恶意输入可能导致SQL注入。使用LIKE语句进行查询时如果使用占位符#{}查询时程序会报错。

因此经验不足的开发人员可能会直接使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。

这里username任意可控,存在SQL注入的代码。

1
2
3
<select id="like2" parameterType="String" resultMap="User">
        select * from users where username like '%${username}%'
</select>

正确的用法,不存在SQL注入漏洞。

1
2
3
<select id="getUserListLikeConcat" resultType="org.example.User">
    SELECT * FROM user WHERE name LIKE concat ('%', #{name}, '%')
</select>

或者

1
2
3
4
5
<select id="getUserListLike" resultType="org.example.User">
<bind name="pattern" value="'%' + name + '%'" />
SELECT * FROM user
WHERE name LIKE #{pattern}
</select>

SQL相关函数点

看了前面的内容,我们可以知道,代码存在SQL注入,其实本质就是将用户的输入当作了SQL语句的字段内容去执行了。想要发现SQL注入漏洞,就要注意在代码中进行SQL语句操作的位置点,仔细查看代码是否正常使用预编译或者是否过滤完全。

下面给出了涉及SQL语句操作的词语,通过这些词可以快速定位代码中的操作SQL的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Statement
createStatement
PrepareStatement
like '%${
in(${
in (${
select
update
insert
delete
${
setObject(
setInt(
setString(
setSQLXML(
createQuery(
createSQLQuery(
createNativeQuery(

XXE

常见的XXEpayload如下:

测试回显的XXE

1
2
3
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://dnslog.com/"> ]>
<x>&xxe;</x>

无回显XXE

1
2
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY % xxe SYSTEM "https://dnslog.com"> %xxe;]>

漏洞相关库

常见的可能存在XXE的相关包[3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javax.xml.parsers.DocumentBuilder
javax.xml.parsers.SAXParser
javax.xml.parsers.SAXParserFactory
javax.xml.transform.TransformerFactory
javax.xml.validation.Validator
javax.xml.validation.SchemaFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.transform.sax.SAXSource
org.xml.sax.XMLReader
org.xml.sax.helpers.XMLReaderFactory
org.dom4j.io.SAXReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.bind.Unmarshaller
javax.xml.xpath.XpathExpression
javax.xml.stream.XMLStreamReader
org.apache.commons.digester3.Digester

测试实例

org.xml.sax.XMLReader

存在XXE漏洞的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.x2n.vultest;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

public class XMLReaderXXE {
public static void main(String[] args) {
try {
// 创建XMLReader实例
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
// 构建一个包含XXE漏洞的XML字符串
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<!DOCTYPE x [ <!ENTITY xxe SYSTEM \"http://dnslog.cn\"> ]>\n" +
"<x>&xxe;</x>";
// 使用InputSource包装XML字符串
InputSource inputSource = new InputSource();
inputSource.setCharacterStream(new java.io.StringReader(xml));
// 解析XML
xmlReader.parse(inputSource);
} catch (Exception e) {
e.printStackTrace();
}
}
}

不存在XXE漏洞,参考前辈文章[4]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建XMLReader实例
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
//在xmlReader后进行setFeature
//第一个是关键的setFeature
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// 构建一个包含XXE漏洞的XML字符串
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<!DOCTYPE x [ <!ENTITY xxe SYSTEM \"http://dnslog.cn\"> ]>\n" +
"<x>&xxe;</x>";
// 使用InputSource包装XML字符串
InputSource inputSource = new InputSource();
inputSource.setCharacterStream(new java.io.StringReader(xml));
// 解析XML
xmlReader.parse(inputSource);

这么一看,其实修复XXE漏洞很简单,只要加上setFeature即可。反之,审计的时候,定位相关的xml处理函数,查看是否进行了setFeature操作,即可得到是否存在XXE漏洞。

org.jdom2.input.SAXBuilder

SAXBuilder如果使用默认配置就会触发XXE漏洞。

存在XXE漏洞的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.x2n.vultest;
import org.jdom2.input.SAXBuilder;
import java.io.StringReader;

public class SAXBuilderXXE {
public static void main(String[] args) {
try {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<!DOCTYPE x [ <!ENTITY xxe SYSTEM \"http://dnslog.cn\"> ]>\n" +
"<x>&xxe;</x>";
SAXBuilder builder = new SAXBuilder();
builder.build(new StringReader(xml));
} catch (Exception e) {
e.printStackTrace();
}
}
}

pom.xml中引入依赖

1
2
3
4
5
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>2.0.6</version>
</dependency>

不存在XXE漏洞的情况。

  • 方式一
1
2
SAXBuilder builder = new SAXBuilder(true);
Document doc = builder.build(InputSource);
  • 方式二
1
2
3
4
5
6
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document doc = builder.build(InputSource);

org.dom4j.io.SAXReader

在默认情况下会出现XXE漏洞。

1
2
SAXReader saxReader = new SAXReader();
saxReader.read(InputSource);

不存在XXE漏洞的情况。

1
2
3
4
5
6
SAXReader saxReader = new SAXReader();
saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
saxReader.read(InputSource);

javax.xml.parsers.DocumentBuilder

存在XXE漏洞的测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.x2n.vultest;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.StringReader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class DocumentBuilderFactoryXXE {
public static void main(String[] args) {
try {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<!DOCTYPE x [ <!ENTITY xxe SYSTEM \"http://dnslog.cn\"> ]>\n" +
"<x>&xxe;</x>";
// 创建DocumentBuilderFactory实例
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 创建DocumentBuilder实例
DocumentBuilder builder = dbf.newDocumentBuilder();
// 解析XML字符串
Document document = builder.parse(new InputSource(new StringReader(xml)));
document.getDocumentElement().normalize();
// 获取根元素
Element root = document.getDocumentElement();
System.out.println("Root element: " + root.getTagName());
// 遍历子节点
NodeList nodeList = root.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
System.out.println("Node: " + node.getNodeName() + " , Text: " + node.getTextContent());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (SAXException e) {
throw new RuntimeException(e);
}
}
}

错误的修复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
String FEATURE = null;
FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing";
dbf.setFeature(FEATURE, true);
FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-general-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
dbf.setFeature(FEATURE, false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
// 读取xml文件内容
FileInputStream fis = new FileInputStream("path/to/xxexml");
InputSource is = new InputSource(fis);
builder.parse(is);

看似设置得很很全面,但是直接仍然会被攻击,原因就是在于DocumentBuilder builder = dbf.newDocumentBuilder();这行代码需要在dbf.setFeature()之后才能够生效;⚠️⚠️⚠️

正确的修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String FEATURE = null;
FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing";
dbf.setFeature(FEATURE, true);
FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-general-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
dbf.setFeature(FEATURE, false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder builder = dbf.newDocumentBuilder();
// 读取xml文件内容
FileInputStream fis = new FileInputStream("path/to/xxexml");
InputSource is = new InputSource(fis);
Document doc = builder.parse(is);

注意DocumentBuilder builder = dbf.newDocumentBuilder();在两种不同的位置的差异性。

其它同理

通过对不同的XML解析库的修复方式可以发现,XXE的防护只需要限制带外实体的注入就可以了,修复方式也简单,需要设置几个选项为false即可,可能少许的几个库可能还需要设置一些其他的配置,但是都是类似的。

总体来说修复方式都是通过setFeature的方式来防御XXE

  • 方法一
1
2
3
4
"http://apache.org/xml/features/disallow-doctype-decl", true 
"http://apache.org/xml/features/nonvalidating/load-external-dtd", false
"http://xml.org/sax/features/external-general-entities", false
"http://xml.org/sax/features/external-parameter-entities", false
  • 方式二
1
2
XMLConstants.ACCESS_EXTERNAL_DTD, ""
XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""

本质上XXE的问题就是一个配置不当的问题,即容易发现也容易防御,但是前提是需要知道这个漏洞。很多程序员并不知道XXE,所以才会写出含有漏洞的代码。

SSRF

SSRF漏洞其实就是代码中有一个可以访问内网的接口,用户可以通过控制这个请求的接口,进而访问内网。SSRF不只是仅限于代码中请求URL的函数,XXE也可以作为SSRF的接口的,还有一些代码可以解析xmlhtml等文件,通过控制这些文件的内容也是可以实现SSRF

Java中支持的协议类型

1
http https file ftp mailto jar netdoc

可能存在问题的函数

代码中常用的可能存在SSRF的函数如下:

1
2
3
4
5
6
7
HttpURLConnection.getInputStream
URLConnection.getInputStream
HttpVlient.execute
Request.Get.execute
Request.Post.execute
URL.openStream
ImageIO.read

根据函数名称,快速定位函数,分析函数的参数是否可控,进而确认是否存在SSRF漏洞。

测试实例

HttpURLConnection.getInputStream

就是URL可控就行了,比较简单。有时候会存在对URL过滤的情况,这时候需要进行绕过,可以参考我之前写的SSRF绕过内容[5]

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.x2n.ssrf;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpURLConnectionSSRF {
public static void main(String[] args) {
try{
//假设这里是用户可控的URL
String targetUrl = "http://127.0.0.1";
URL url = new URL(targetUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//获取输入流,并读取响应
try(BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))){
String inputLine;
StringBuilder content = new StringBuilder();
while((inputLine = in.readLine()) != null){
content.append(inputLine);
}
System.out.println(content.toString());
}
//关闭连接
connection.disconnect();
}catch(Exception e){
e.printStackTrace();
}
}
}

HttpCLient.execute

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
package com.x2n.ssrf;

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

public class HttpClientSSRF {
public static void main(String[] args) throws IOException {
//创建HttpClient实例
try(CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建HttpGet实例
HttpGet http = new HttpGet("http://127.0.0.1:8787");
// 执行请求并获取响应
try(CloseableHttpResponse response = httpClient.execute(http)) {
//检查响应状态码
if(response.getStatusLine().getStatusCode()==200){
//读取响应内容
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String line;
StringBuilder body = new StringBuilder();
while((line = reader.readLine()) != null){
body.append(line);
}
System.out.println("response body: "+body.toString());
}else{
System.out.println("request failed: "+response.getStatusLine().getStatusCode());
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

其它同理

比较简单,这里就不再举例。

RCE

Java中的RCE中的方式两种:

aaa

参考


  1. https://www.runoob.com/servlet/servlet-life-cycle.html ↩︎

  2. https://mp.weixin.qq.com/s/9w4Rzr-P1ph41VSCmmc_Hg ↩︎

  3. https://r17a-17.github.io/2021/09/04/Java-XXE漏洞总结/ ↩︎

  4. https://blog.spoock.com/2018/10/23/java-xxe/ ↩︎

  5. https://x2nn.github.io/2024/07/25/SSRF漏洞篇/#SSRF的绕过 ↩︎