0x00 前言
现在已经是自由人了,不用再被繁琐的工作拖累了,所以有了更多时间可以学习。说到OGNL注入,你肯定第一个想到的是Struts2,但是我这里并不打算直接从其入手。一是,光是分析他的修复历史以及绕过就要一大篇文章去写;二是,我想锻炼一下自己的漏洞分析能力,而Struts2系列,很多师傅已经写过了,所以本文我分析的漏洞基本上都是比较新的CVE。当然,我后面还是会专门写一篇Struts2漏洞文章的。
- CVE-2022-26134漏洞分析
0x01 OGNL
表达式
OGNL是Java中常见的一种表达式,在Java安全中,我们之后还能见到EL、SPEL、JEXL表达式、(其实在之前的Nexus文章我已经见过了,但是没深入了解),懒狗预计后面会各自写一篇文章。
什么是表达式呢?其实通俗的说就是一个帮助我们更方便操作Bean等对象的式子,比如下面这个例子:
可以看到,通过表达式,可以简化我们对类的操作。
OGNL三要素
OGNL有以下三要素:
- Expression 表达式
- root 根对象,即操作对象
- context 上下文,用来保存对象运行的属性及其值,有点类似于运行环境的意思,保存了环境变量
以下便是一个OGNL的实例,大家可以先感受一下:
OGNL语法
.
操作符:如上所示,可以调用对象的属性和方法,hacker.name
,且上一个节点的结果作为下一个节点的上下文,如(#a=new java.lang.String("calc")).(@java.lang.Runtime@getRuntime().exec(#a))
,也可以换成逗号(#a=new java.lang.String("calc")),(@java.lang.Runtime@getRuntime().exec(#a))
@
操作符:用于调用静态对象、静态方法、静态变量,@java.lang.Math@abs(-10)
#
操作符:a)用于调用非root对象
b)创建Map
c)定义变量
$
操作符:一般用于配置文件,<param name="name">${name}</param>
%
操作符:计算其中的OGNL表达式,%{hacker.name}
List:直接使用
{"green", "red", "blue"}
创建对象创建:
new java.lang.String[]{"foobar"}
0x02 OGNL注入
简介
上文对OGNL的简单了解后,我们可以看到,利用OGNL可以用来操作类并进行相关调用。而所谓OGNL注入,即是当解析OGNL的函数(如上文的getValue)中参数可控时,传入恶意的OGNL语句,从而实现RCE等恶意操作。
OGNL RCE的测试
在pom.xml中加入如下的依赖(OGNL有2和3两个大版本,此处使用2):
然后,我们根据上面学习的OGNL语法,写下如下的代码:
运行后,成功弹出计算器。具体里面怎么实现的,我这里就不跟进了,其实我感觉很多文章跟进,意义也不大……
OGNL 高版本下的黑名单
OGNL在>=3.1.25、>=3.2.12的版本中增加了黑名单。我们将依赖更新为3.1.25,然后再次运行,就会得到一个报错信息,如下:
根据报错信息,跟进到OgnlRuntime#invokeMethod,可以看到如下的黑名单:
0x03 CVE-2022-26134 Confluence
漏洞信息
官方通告:https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html
根据通告,我们确定了漏洞包为:xwork-1.0.3-atlassian-8.jar(点击下载);漏洞修复包为:xwork-1.0.3-atlassian-10.jar(点击下载)
漏洞点
将上面下载的源码解压,然后放进IDEA中,然后进行比较,找到了补丁的修复点如下(ActionChainResult#execute):
代码如下:
环境搭建
修改vulhub里的靶场(可以随便一个低版本的Confluence即可,这里我是用的对应CVE的靶场)的docker-compose.yml,添加上调试端口:
使用docker-compose up -d将vulhub里的靶场启动(最好挂上代理来拉取),之后访问127.0.0.1:8090,即可看到如下的安装界面(原谅我从奇安信社区的文章截了一个图,因为我当时直接下一步,忘记截图了):
复制上图中的Server ID,然后访问:https://my.atlassian.com/license/evaluation,进行如下的选择,并填入我们刚刚复制的ID:
点击生成证书后,就会跳转到对应界面。在这里,我们可以复制key:
把key复制到靶场对应位置,之后就可以开始安装了,在此页面填入数据库信息(根据https://github.com/vulhub/vulhub/tree/master/confluence/CVE-2022-26134进行填写即可):
之后就没什么值得注意的了,自己随便点点就能完成了。不过我们还需要下载一份代码用于我们调试,访问https://www.atlassian.com/software/confluence/download-archives,下载与Vulhub中相同的版本:7.13.6。接着,用IDEA将其打开,并将/confluence/WEB-INF中lib文件夹、atlassian-bundled-plugins-setup、atlassian-bundled-plugins添加到依赖之中(如果不加,之后远程调试时,服务会启动失败):
下一步,就是设置JVM远程调试,并开启调试:
进入docker容器,并在其配置文件中加入参数:
然后重启服务:
在上文的补丁处,设下断点(这里还可以通过IDEA将代码源选择为之前下载的源码),访问127.0.0.1:8090,成功在断点中止:
漏洞分析
简单跟进后,我们在translateVariables()方法中发现了和OGNL相关的代码如下:
继续跟进findValue()方法,可以看到调用了我们在上文讲过的getValue:
而纵观整个过程,都不存在过滤,所以当ActionChainResult.namespace和ActionChainResult.actionName可控时,就可以成功通过OGNL来实现RCE(并且通过源码,我们可以发现Confluence使用的OGNL版本为2.6.5,所以不存在黑名单)。因此,下一步,我们的目标就是找到this.namespace和this.actionName可控的地方。
向上追溯,发现ActionChainResult是在DefaultActionInvocation#executeResult中通过createResult创建,并调用的execute,传入的是DefaultActionInvocation:
跟进DefaultActionInvocation#createResult:
从这里可以看到,他先获得一个Map,然后从map中根据resultCode取出对应的resultConfig ,最后通过向buildResult传入resultConfig 将其实例化。这里我们的resultCode为“notpermitted”:
在map中找到对应的值如下图所示:
在其中的resultConfig 记录了className和params,并且actionName为“notpermitted”,跟进buildResult就会发现,他就是用过这个设置来实例化的:
所以,我们的actionName便是由proxy.getConfig().getResults()所决定的,更简单的说,是由DefaultActionInvocation.proxy决定的。而回过头去看漏洞点处的代码,会发现namespace同样是由他决定(在不改变proxy中的Map时,这里namespace一定会因为null,进入if,因为Map中params只有actionName的值):
这里的invocation.getProxy()即会获得DefaultActionInvocation.proxy。因此我们的目标转换为了,DefaultActionInvocation.proxy在何时可控?
继续向上追溯,发现在ConfluenceServletDispatcher#serviceAction中进行了proxy的创建,并调用execute:
熟悉代理的同学,就会知道,这里proxy其实就是DefaultActionInvocation.proxy,proxy.execute()会去调用invocation的invoke:
所以我们只要找到ConfluenceServletDispatcher#serviceAction中的namespace和actionName何时可控就可以控制proxy的创建了。继续向上追溯,发现其由ServletDispatcher#service调用:
看到ServletDispatcher,我们就知道,已经快接近最终的可控点了。查看getNameSpace:
先使用getServletPath获取请求路径,然后传入getNamespaceFromServletPath,继续跟进:
只是单纯的把/
去除。至此,我们似乎已经可以通过URL来控制namespace,从而触发漏洞了。访问http://127.0.0.1:8090/${6+6}/,发现成功触发漏洞:
那actionName是否可控呢?首先看actionName是什么,跟进上文的 this.getActionName(request):
因此,actionName实际就是我们访问的文件名,如hacker.action
,actionName即为hacekr
。再查看DefaultActionProxy的构造函数:
可以看到config实际是从配置中获取的,当我们尝试修改时,会因为找不到,而抛出错误。
POC
由上文的分析,我们编写POC如下:
通过响应时间来判断是否存在漏洞。
Confluence高版本绕过
在7.15 及以上的版本的findValue中增加了检查,懒狗将在更深入学习了OGNL注入后,补上这一部分。