一文读懂OGNL漏洞
本文首发于先知:https://xz.aliyun.com/t/10482。
0x00 前言
前段时间出现的Confluence OGNL漏洞(CVE-2021-26084)引起了我对Java OGNL表达式注入的兴趣,当时没有时间立刻研究,近期又捡起来学习和分析,用了近半月整理了本文,如有不当之处,还请批评指正。
0x01 OGNL是什么?
先来看一个例子:
1 | Class SchoolMaster{ |
创建实例学校school = new School()
、学生student = new Student()
和校长schoolMaster = new SchoolMaster()
,将学校校长指定为schoolMaster
实例-school.schoolMaster = schoolMaster
,学生的学校指定为school
实例-student.school = school
,那么三者就连接起来了形成了一个对象图,对象图基本可以理解为对象之间的依赖图。通过对象图我们可以获取到对象的属性甚至对象的方法。
那么OGNL就是实现对象图导航语言,全称Object-Graph Navigation Language。通过它我们可以存取 Java对象的任意属性、调用 Java 对象的方法以及实现类型转换等。
0x02 OGNL三元素
OGNL基本使用方法示例:
1 | // 创建Student对象 |
输出结果:
1 | xiaoming:学校-tsinghua,校长-wanghua |
不难看出,OGNL getValue需要三元素:expression表达式、context上下文及root对象。那么什么是三元素:
expression表达式:表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
root根对象:OGNL的Root对象可以理解为OGNL的操作对象。当OGNL通过表达式规定了“干什么”以后,还需要指定对谁进行操作;
context上下文对象:context以MAP的结构、利用键值对关系来描述对象中的属性以及值,称之为OgnlContext,可以理解为对象运行的上下文环境,其实就是规定OGNL的操作在哪里。
在上面示例中,根对象是student1实例,context中设置了根对象和非根对象student2,表达式有name
、school.name
、school.schoolMaster.name
和student2.name
、#student2.school.name
、student2.school.schoolMaster.name
,前三个是通过表达式获取root也就是student1对象的相关属性,后三个是通过表达式获取容器变量student2对象的相关属性。
0x03 OGNL表达式语法
符号的使用:
在上一部分我们已经接触了.
和#
符号在表达式中的使用,通过.
可以获取对象属性,#
可以获取非root的Student对象。
OGNL表达式支持Java基本运算,所以运算符+
、-
、*
、/
、%
等在OGNL都是支持的,另外还支持in
、eq
、gt
等。
除了基本运算符,.
、@
、#
在OGNL中都有特殊含义。
1、通过.
获取对象的属性或方法:
1 | student |
2、三种类型对象的获取:
静态对象、静态方法和静态变量:@
1 | @java.lang.System@getProperty("user.dir") |
非原生类型对象:#
1 | #student.name |
简单对象:直接获取
1 | "string".lenth |
3、%
符号的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值。
4、$
在配置文件中引用OGNL表达式。
集合表达式:
new
创建实例:
1 | new java.lang.String("testnew") |
{}
和[]
的用法:
在OGNL中,可以用{}
或者它的组合来创建列表、数组和map,[]
可以获取下标元素。
创建list:{value1,value2...}
1 | {1,3,5}[1] |
创建数组:new type[]{value1,value2...}
1 | new int[]{1,3,5}[0] |
创建map:#{key:value,key1:value1...}
1 | #{"name":"xiaoming","school":"tsinghua"}["school"] |
除了一些符号和集合,还支持Projection投影和Selection选择等,具体可参考官方文档:https://commons.apache.org/proper/commons-ognl/language-guide.html 附录Operators部分。
0x04 命令执行调试分析
通过上面表达式的学习我们很容易能够写出Java执行命令的表达式:
1 | @java.lang.Runtime@getRuntime().exec("calc") |
Ognl低版本:2.7.3测试
调试分析Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"calc\")", context, context.getRoot());
执行流程。下图是表达式对应的语法树(AST),下面的分析可以结合图片思考。
Ognl.getValue()处理表达式时,会先生成一个tree,这个tree本质是SimpleNode实例,树的每个节点都是一个ASTChain实例,ASTChain继承自SimpleNode。
当调用node.getValue(ognlContext, root);
时,会调用SimpleNode.getValue()
进行处理,SimpleNode.getValue()
会通过SimpleNode.evaluateGetValueBody()
计算结果
1 | public final Object getValue(OgnlContext context, Object source) throws OgnlException { |
SimpleNode.evaluateGetValueBody()
在计算非常量情况的结果时会调用子类的getValueBody,Ognl在处理节点时分为多种情况进行处理:ASTChain、ASTConst、ASTCtor、ASTInstanceof、ASTList、ASTMethod、ASTStaticField、ASTStaticMethod等。
首先这里最开始是一个ASTChain @java.lang.Runtime@getRuntime().exec("calc")
,ASTChain.getValueBody()
在处理时,会迭代调用getValue处理子节点的结果,最终还是会调用ASTXXX方法处理节点的结果。
1 | protected Object getValueBody(OgnlContext context, Object source) throws OgnlException { |
当Ognl计算@java.lang.Runtime@getRuntime()
时,由于方法时静态方法会调用ASTStaticMethod.getValueBody
。ASTStaticMethod.getValueBody
通过OgnlRuntime.callStaticMethod
处理方法的调用。
1 | protected Object getValueBody(OgnlContext context, Object source) throws OgnlException { |
ObjectMethodAccessor
1 | public class ObjectMethodAccessor implements MethodAccessor { |
通过OgnlRuntime.callAppropriateMethod()
处理方法调用,最终会调用Method.invoke()
进行方法调用并返回值。
1 | public static Object callAppropriateMethod(OgnlContext context, Object source, Object target, String methodName, String propertyName, List methods, Object[] args) throws MethodFailedException { |
同样的,Ognl计算exec("calc")
时,调用ASTMethod.getValueBody
,最终也是在OgnlRuntime.callAppropriateMethod()
中调用Method.invoke()
处理。
Ognl 3.2.18 测试
Ognl>=3.1.25、Ognl>=3.2.12配置了黑名单检测,会导致上面的实验失败,提示cannot be called from within OGNL invokeMethod() under stricter invocation mode
,在使用StricterInvocation模式下不允许执行java.lang.Runtime.getRuntime()
。
对比上面2.7.3版本,在OgnlRuntime.invokeMethod
中,添加了黑名单判断,当命中黑名单会出现上图的报错:ClassResolver
、MethodAccessor
、MemberAccess
、OgnlContext
、Runtime
、ClassLoader
、ProcessBuilder
等。
1 | public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException { |
0x05 近期三个漏洞的分析
在CVE搜索OGNL
,前三个漏洞分别是Confluence的CVE-2021-26084、Struts2的CVE-2020-17530和Apache Unomi的CVE-2020-13942,本次对这三个漏洞进行分析。
Confluence CVE-2021-26084
velocity模板引擎语法:
1、基本符号
1 | "#"标识velocity的脚本语句 |
2、示例:
1 | ## 1、变量引用 |
更多语法可参考:http://velocity.apache.org/engine/1.7/user-guide.html
漏洞分析:
confluence处理velocity模板,将velocity语法转为字符串输出到页面,其中涉及到的一些表达式计算会调用ognl.getValue()
处理。confluence处理vm文件,首先将vm内容转为AST语法树,然后分别处理每一个节点的内容,将每个节点的内容拼接输出。
Confluence的Velocity模板引擎处理vm文件流程主要在com.opensymphony.webwork.dispatcher.VelocityResult.doExecute()
,首先获取OgnlValueStack、context上下文、getTemplate获取vm文件,接下来用merge
处理合并页面结果,将结果输出给writer。
merge调用((SimpleNode)this.data).render(ica, writer);
方法处理,先将vm文件的内容转为AST语法树,便于计算每个节点的结果。
本次漏洞涉及的createpage-entervariables.vm
文件经过解析后的AST语法树如下图,每一个ASTXXX处理程序都继承自SimpleNode.
queryString在第7个节点,归属applyDecorator指令,程序处理时将applyDecorator又分为35个节点,queryString在[#tag], [ ], [(], ["Hidden"], [ ], ["name='queryString'"], [ ], ["value='$!queryString'"], [)]
节点中处理,我们重点看这个处理过程。
[#tag], [ ], [(], ["Hidden"], [ ], ["name='queryString'"], [ ], ["value='$!queryString'"], [)]
节点属于AbstractTagDirective,会调用AbstractTagDirective.render()
。
AbstractTagDirective.render()
首先调用applyAttributes(contextAdapter, node, object)
处理参数,其中AbstractTagDirective.createPropertyMap()
创建参数Map,保存property键值对。
保存后AbstractTagDirective.render()
调用AbstractTagDirective.processTag()
处理tag
通过AbstractTagDirective.processTag()
最终会调用AbstractUITag.doEndTag()
,doEndTag调用evaluateParams()
处理参数。
AbstractUITag.evaluateParams
通过addParameter()
添加name和value,value的值通过findValue()
获取具体的值。
调用getValueFinder().findValue(expr, toType)
时会先调用SafeExpressionUtil.isSafeExpression()
进行安全检查,而isSafeExpression()
会通过containsUnsafeExpression()
处理,这正是本次漏洞的关键之处。
containsUnsafeExpression()
代码如下,递归检查节点及其子节点是否包含黑名单。
1 | private static boolean containsUnsafeExpression(Node node) { |
黑名单包括静态方法、静态属性、构造方法、class、classLocader、getClass()、getClassLoader()、_memberAccess、context、request等。
1 | static { |
UNSAFE_PROPERTY_NAMES
有class
和classLoader
两个元素,不包含["class"]
,而["class"]
子节点"class"
属于ASTConst不进行检查,因此可绕过,对于方法黑名单,ASTMethod仅禁止getClass()
和getClassLoader()
,forName
、getMethod
、invoke
等不在禁止范围。
特别说明下,confluence 使用的ognl版本是2.6.5,属于较早版本,没有在invokeMethod中添加黑名单进行安全检查,因此payload在ognl中可以顺利执行。(可参考0x04-Ognl 3.2.18 测试)
另外,除了queryString
,vm中还有两个看起来可利用的参数:
1 | #tag ("Hidden" "name='queryString'" "value='$!queryString'") |
经过尝试linkCreation
同样也可以利用,跟queryString
一样的:
而templateId
不能利用,因为该参数实际需要的是int,后面强制转换会报错。
Struts2 CVE-2020-17530(S2-061)
Struts2的ognl RCE漏洞主要是添加黑名单来修复和绕过黑名单。
Struts2防护机制
如果需要绕过Struts2的历史ognl rce的修复,需要考虑三点:
1 | struts-defult.xml的struts.excludedClasses和struts.excludedPackageNames部分 |
struts2在struts-defult.xml
文件中加入了一些类和包作为黑名单:
1 | <!-- s2-061修复前的黑名单 --> |
构造ValueStack时,在com.opensymphony.xwork2.ognl.OgnlValueStack.setOgnlUtil()
中会设置SecurityMemberAccess
,将struts-defult.xml
的黑名单加载进去
我调试分析时用的struts2版本是2.5.25,该版本中用到的ognl版本是3.1.28,该版本的OgnlRuntime.invokeMethod
同样做了一些黑名单限制(同“0x04-Ognl 3.2.18 测试“)。
漏洞分析:
payload:
1 | %{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#access=#bean.get("memberAccess")).(#bean.setBean(#access)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#cmd={'whoami'}).(#execute.exec(#cmd))} |
根据Struts2 S2-061漏洞分析(CVE-2020-17530)文章进行调试分析,总结s2-061绕过s2-059的思路主要有以下几点:
1、#application
中的 org.apache.tomcat.InstanceManager.newInstance()
可以实例化无参构造的类;
2、可以通过#attr
和com.opensymphony.xwork2.util.ValueStack.ValueStack
获取valuestack。org.apache.commons.collections.BeanMap
的setBean方法设置为valuestack,这样get方法传入context
就可以调用com.opensymphony.xwork2.ognl.OgnlValueStack.getContext()
,然后将获取的context同样用setBean方法进行设置,get传入memberAccess
进行获取;(关于ValueStack、OgnlContext、memberAccess和SecurityMemberAccess的关系推荐阅读Lucifaer大佬的浅析OGNL攻防史进行了解)
3、获取到的memberAccess实际就是com.opensymphony.xwork2.ognl.SecurityMemberAccess
,再利用BeanMap的put方法将SecurityMemberAccessexcludedClasses
和excludedPackageNames
置空,这样子就绕过了struts2的黑名单;
4、需要注意第三点只是绕过了struts2黑名单,ognl黑名单没有被绕过,避开ognl黑名单,可以利用struts2的黑名单,其中freemarker.template.utility.Execute
存在无参构造,freemarker.template.utility.Execute.exec()
方法可执行命令。
Apache Unomi CVE-2020-13942
Apache Unomi CVE-2020-13942包括OGNL RCE和MVEL RCE,本文仅针对OGNL进行分析。
对比1.5.1和1.5.2版本,修复该漏洞的提交Improve scripting security ([#179])中主要对org.apache.unomi.plugins.baseplugin.conditions.PropertyConditionEvaluator.java
、SecureFilteringClassLoader.java
等进行了修改,并且增加了ExpressionFilter.java
来检查表达式。
漏洞分析:
unomi处理parameterValues主要在org.apache.unomi.plugins.baseplugin.conditions.PropertyConditionEvaluator
,getPropertyValue()
获取请求的参数值。在该方法中默认会先通过getHardcodedPropertyValue()
处理。
getHardcodedPropertyValue()
中当propertyName
不等于segments
、consents
、properties.XXX
等,会返回NOT_OPTIMIZED
,然后再通过getOGNLPropertyValue()
处理,也就是说propertyName
未遵照预设的结果时会按照ognl表达式处理。
在getOGNLPropertyValue()
中,通过accessor.get(ognlContext, item)
处理,这里accessor
就是ASTChain。
那么最终会调用ASTChain.getValue()
处理表达式。
unomi 1.5.1用的ognl版本是3.2.14,该版本在OgnlRuntime.invokeMethod
中同样存在黑名单判断。只要表达式绕过Ognl的黑名单就可以达到目的。
我们来看下表达式:
1 | (#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^ #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execmethod.invoke(#rtobj,\"touch /tmp/ognl\")) |
整个的思路是用Class和Method以及Method.invoke来绕过黑名单。
this.getClass()
是一个Class对象,Class
没有在黑名单中,因此上面Class.forName()
可以执行,同理Class.forName()
会得到一个Class对象,因此runtimeclass.getDeclaredMethods()
可以正常执行,并且返回Runtime的方法数组,Method
没有在黑名单,遍历方法名获取到getRuntime
的Method对象(不可以直接getDeclaredMethod("getRuntime")
会报错),利用invoke执行getRuntime
,同理获取exec
并执行。
最后顺便提一下unomi小于1.5.1版本存在CVE-2020-11975,查了下1.5.0使用的ognl版本是3.2.11,该版本OgnlRuntime.invokeMethod
没有黑名单,这也是Runtime的payload(#r=@java.lang.Runtime@getRuntime()).(#r.exec(\"calc\"))
可以直接运行的原因。
0x06 思考与总结
上面提到的几个OGNL漏洞的修复基本都是采用黑名单来限制OGNL注入,开发人员在使用ognl时,除了ognl需要注意使用较高版本,还要注意添加额外的防护措施。当然,使用黑名单的防护方式也许一时可以防住OGNL的RCE,但总有被绕过的风险,另外除了命令执行,文件操作、SSRF也不是没有可能。
0x07 参考链接:
1 | https://commons.apache.org/proper/commons-ognl/apidocs/index.html |