Author:LJ(知道创宇404安全实验室)

Date: 2017-03-14

0x00 概述

凌晨12点,Struts2 官方发布了 S2-045 的漏洞公告,仔细阅读描述,发现是友商安恒的 Nike Zheng 小伙伴提交的,所以赶紧跟进分析了一把。

0x01 漏洞场景还原

首先,我们获取最新的 Struts2 源代码,并编译出存在漏洞环境的 demo ,这里我选择了官方教程推荐的 struts2-showcase , 具体操作指令如下:

git clone https://github.com/apache/Struts.git  
cd Struts  
git checkout STRUTS_2_5_10  
mvn package  

国内的网络,你懂的...,最后在 apps/showcase/target 目录下生成了 struts2-showcase.war 文件,将其拷贝至 tomcat 的webapps 目录下,为了输入方便,这里将文件名重命名为 showcase.war 。启动tomcat,访问 http://localhost:8080/showcase/ 可以看到漏洞环境的界面如下:

漏洞环境首页

下面是使用网上流出 PoC 的关键部分打一下看到的效果:

漏洞效果

从上面的效果图可以看出:

  • 发起请求的时候并不需要找到具体的上传点,只要是有效的 URL 就可以

  • 发起请求不需要 POST 方法也可以触发,因为 curl 默认发起请求就是 GET

0x02 漏洞分析

先进入刚才获取到的 Struts 源代码目录,并查看diff后的代码,具体操作如下:

git diff STRUTS_2_5_10 STRUTS_2_5_10_1 > s2-45.diff  

在去除了一些配置和测试文件信息后,下面这部份代码引起了笔者的注意:

diff代码

从删除的代码上可以看出专门对 validation 做了不为空的校验,进一步跟进 LocalizedTextUtilfindText 函数,发现这个函数被重载:

LocalizedTextUtil代码

在392行代码中 ActionContext 中获取了 ValueStack ,这里可以简单认为是从请求的上下文环境当中获取了所有的数据,然后调用重载的下一个 findText ,在729行代码中看到 TextParseUtil 类对 valueStack 做了转换处理,做进一步的跟进,具体代码如下:

TextParseUtil代码

translateVariables 的多次调用后,最终调用了第152行的 translateVariables 函数,根据此函数的注释,就是在这里做了由值到对象并执行的转换,最终触发漏洞。

但是这里有一个问题,就是具体是在哪里触发的漏洞,这个是由 Struts2 的机制引起的,在 struts2 的框架当中,内置了许多的拦截器,主要用于对框架的功能扩展,此次漏洞的 FileUploadInterceptor 就是对框架文件上传的功能扩展。

0x03 动态分析

在进一步的跟进调试前,我们先梳理下 Struts2 整体的框架结构,以便于理解 request 请求流转的过程,这里借用 struts 官方的设计架构图,并做个整体架构的简述:

Struts2 官方架构图

我们先不看 Struts2 自带的蓝色核心部分,可以看到 request 请求大部分都是在浅黄色的过滤器和绿色的拦截器中间流转。

在有了具体的业务逻辑需要后台处理时,需要将 request 发送到对应的 Action 来处理,图中画的是 FilterDispatcher ,但在 Struts 2.5 以上已经将这个换成了 StrutsPrepareAndExecuteFilter

看到上图,去除 Struts2自带的核心部分,可以看到大部分的过程都是在过滤器和拦截器中间流转,而且对流转的Action并没有加以区分。

简单来说,过滤器对所有的请求都起到作用,主要用来对请求添加,修改或者分派转发至Action处理业务逻辑,图中的FilterDispatcher 就是起到这个作用的。

拦截器能对配置文件中匹配的 request 进行处理,并能获取 request 当中的上下文环境及数据。

我们先来对 FileUploadInterceptor 下断点来调试:

其中第264行是对错误的request进行验证处理,我们跟进findText 看一下:

这里 request 获取ValueStack对象,我们可以看到 defaultMessage 的值如下图:

进一步跟进 findText 函数,就发现这个函数被重载了。重载的函数始于448行,然后是循环操作。跳出循环单步跟进到573行,我们发现这里又调用了 GetdefaultMessage 方法:

于是继续跟进 GetDefaultMessage方法并定位到729行:

接着跟进 translateVariables 方法,expression 就是传入的错误信息: 注意到上图使用了 ognl"$""%"标签,两者都能告诉执行环境 ${}%{} 中的内容为ognl表达式PoC中使用的是 "%",使用 "$" 也能触发漏洞。 再次跟进translateVariables方法,在这里提取出ognl表达式并调用evaluate方法执行。

最后程序在 parserevaluate方法中执行了 ognl表达式。

综上,漏洞触发后程序的调用栈先后顺序如下:

1.intercept:264,FileUploadInterceptor           文件上传拦截器处理报错信息  
2.findText:393,LocalizedTextUtil  
3.findText:573,LocalizedTextUtil                提取封装的Action中的值栈  
4.getDefaultMessage:729,LocalizedTextUtil        值到对象转换  
5.translateVariables:45,TextParseUtil  
6.translateVariables:123,TextParseUtil  
7.translateVariables:166,TextParseUtil          提取出ognl表达式并执行  
8.evaluate:13,OgnlTextParser                    最后执行ognl表达式

0x04 总结

回顾struts2历史RCE漏洞,形成原因与ognl表达式执行相关的漏洞层出不穷。 当阅读这些历史漏洞的分析文章时,有些作者以 ”这里竟然执行了ognl表达式“ 这句话对漏洞点进行吐槽。 然而这次的漏洞成因则更为奇怪,解析出错误信息的ognl表达式并执行。 最后,当动态调试此类漏洞时,前往 ognl表达式执行的方法处下断点调试,马上就能一目了然的看到漏洞触发的完整调用栈。

0x05 参考链接