作者:iswin@360观星实验室
最近1年多基本上都在忙实验室其它的工作,基本上很少关注JAVA安全这块的内容,赶上正好周末休息,加上实验室其它小伙伴对这个漏洞比较关注索性就抽出一点时间跟实验室其它小伙伴一起研究了下,就当学习了,这套系统在一些大型的企业里面其实还是有一定使用量的(尤其是在打点过程中),所以在实战的渗透中还是有一些应用场景的。
这个漏洞在19年2月份左右被腾讯安全云鼎实验室发现并且提交Sonatype官方https://cloud.tencent.com/developer/article/1390628,根据官方的通告和云鼎披露来看基本上可以确定该漏洞是未授权的远程代码执行。
经过研究发现该漏洞是一个基于OrientDB自定义函数的任意JEXL表达式执行漏洞,由于JEXL表达式可以执行JAVA代码同时没有安全上的限制,所以间接的就成了远程代码漏洞,这里注意的是漏洞触发条件对于新搭建的环境来说稍微有一点点坑,不过通过跟踪代码应该可以看出一些规律。
由于nexus的环境如果直接用源码在idea里面编译跑起来的话有点麻烦,依赖比较多,所以漏洞环境直接利用docker搭建,然后结合源码nexus-public使用jdwp进行远程调试即可。
根据漏洞描述和披露的利用截图,我们很容易定位到漏洞的触发位置,如下图所示
根据函数的输入我们可以构造如下数据包进行漏洞流程的跟踪
然后在org.sonatype.nexus.coreui.ComponentComponent#previewAssets函数出打上断点,跟踪type参数发现nexus支持两种类型的表达式Jexl和Csel(本质上就是Jexl表达式)
在最终执行表达式之前这两种表达式会对表达式的内容做校验,主要是以下两个代码片段
if (type == JexlSelector.TYPE) {
jexlExpressionValidator.validate(expression)
}
else if (type == CselSelector.TYPE) {
cselExpressionValidator.validate(expression)
}
其实这个漏洞如果细心的人,在nexus后台界面中会看到有个功能就是通过上述的那个接口,只不过哪里的type是csel,起初我们先跟踪了下csel表达式,但是发现csel表达式在校验的时候做了一些限制,函数位置org.sonatype.nexus.selector.CselValidator#validate如下图所示
如上图红圈标识的地方,在对表达式语法进行检查之后还对表达式中解析后的函数和属性的个数做了限制(大小为2)同时对解析后的内容做了限定,这样一来csel表达式这条路明显走不通了,就只能跟踪jexl表达式了,jexl表达式的校验比较简单,只是对表达式的语法进行了检查,函数位置org.sonatype.nexus.selector.JexlSelector#JexlSelector,在表达式最后执行的时候会调用org.sonatype.nexus.selector.JexlSelector#evaluate,如下图
到这里表达式校验和流程已经走完了,关于jexl表达式弹计算器随便Google一下就知道了,例如https://appcheck-ng.com/wp-content/uploads/2018/12/traccarexploit.v1.py.txt,如果不去追究那些jexl表达式以及后续执行流程细节的话,如果你运气好或许计算器已经弹出来了。
但是在万事具备准备弹个计算器的时候,发现死活不行,最后在函数org.sonatype.nexus.selector.JexlSelector#evaluate上下断点,发现也断不下来,函数最后压根都没执行到这里,到这里调试了很久,一度让我以为漏洞的触发点找错了,但是在简单回溯之后,发现这个点漏洞触发概率相对较大一点,遂决定深入跟踪分析下表达式后续的执行流程。
继续断点跟踪,直接跟进org.sonatype.nexus.repository.browse.internal.BrowseServiceImpl#previewAssets函数,跟进这个函数我们继续跟踪jexlExpression变量的调用流程,最终我们可以发现表达式会被当做参数然后形成SQL由OrientDb进行执行,如下图
如上图所示,最终生成如下的SQL语句
SELECT FROM asset WHERE (bucket = #24:0 ) AND (contentExpression(@this, "iswin", "maven-central", {"maven-central": ["maven-central"]}) == true ) SKIP 0 LIMIT 300
到这里我们基本上确定了表达式最后是有OrientDB进行执行了,这里的contentExpression就是OrientDB的函数,Google搜索了一下发现contentExpression函数并不是OrientDB的内置函数,那么这个函数应该是nexus自定义的,关于OrientDB的内置函数以及自定义函数可以参考官方文档https://orientdb.com/docs/last/SQL-Functions.html,到这里漏洞触发的整个流程都清楚了,但是依然触发不了,根据OrientDB自定义函数的例子,我们可以找到contentExpression函数最终的JAVA代码实现类位置org.sonatype.nexus.repository.selector.internal.ContentExpressionFunction,从流程上来看,最终的函数执行如下图所示
到这里理论上的函数调用流程已经梳理完了,但是依然触发不了,这里我们就需要考虑下是不是SQL语句的执行流程上出现了问题,回到SQL语句本身,如下所示
SELECT count(*) FROM asset WHERE (bucket = #24:0 ) AND (contentExpression(@this, "iswin", "maven-central", {"maven-central": ["maven-central"]}) == true )
当时看到这个语句的时候,我就在想这个语句的执行流程到底是怎么样的?是先执行SELECT count(*) FROM asset WHERE (bucket = #24:0 )这个语句还是说先执行AND后面的contentExpression函数,如果是先执行SELECT FROM WHERE,那么会不会是由于这个语句执行完成之后没有结果导致了contentExpression函数没有被执行,如果是这样那么就简单了,先登录后台随便在仓库里面传一个文件,这样确保了SELECT count(*) FROM asset WHERE (bucket = #24:0 )语句有结果返回,发送如下图的Payload,成功触发
至于OrientDB对语句的执行流程和函数的执行流程,有兴趣的同学可以在以下截个函数上下断点com.orientechnologies.orient.core.sql.OCommandExecutorSQLSelect#executeSearch、com.orientechnologies.orient.core.sql.OCommandExecutorSQLSelect#searchInClasses(1017行),决定是否去执行contentExpression函数的关键点在于com.orientechnologies.orient.core.sql.OCommandExecutorSQLSelect#executeSearch函数中target变量是否为null,当target变量不为null的时候才回去执行fetchFromTarget(target)函数从而去触发OrientDB的自定义函数,如下图
这里就引出了我开篇说道到的在调试的时候一个坑,也反映了漏洞触发的前置条件,即确保被攻击系统仓库中有项目(当然这个要求大部分都能满足,但是对于搭漏洞环境调试人员来说的确非常坑)。
当然了如果只是纯分析这个漏洞,那么上面已经花了大量时间来说了,那么这里主要讨论下在实际攻防环境下的利用,一般情况下我们大部分的攻击环境分为两种,一种是被攻击机机器能出网,另外一种是不能出网的情况。
针对出网的环境我们可以直接弹个SHELL,然后就搞定了,这里我们主要讨论不能出网的环境,针对不能出网的环境环境主要有一下两种利用办法(当然还有其他的奇技淫巧,这里不做讨论)
在这里针对nexus系统来说,且不说能不能解析jsp或者jspx脚本,但凡能写目录都是root权限(docker环境,其它实际部署环境没考证),但是漏洞执行的点为nexus权限,所以写shell基本上没戏,况且nexus不能解析脚本文件,那么现在重点就讨论该漏洞如何回显的问题。
这里就来讨论下针对JAVA WEB系统存在远程代码执行时,如何进行回显的问题,这类的系统例如Struts2、反序列化漏洞等,那么针对表达式类的代码执行,无非就以下几种主流的方法
当然对于nexus系统来说2、3明显是不可行的,那么对于1这种方案到底行不行,一开始的时候我经过一番探索,发现这种环境下回显是不可能的,主要在于jexl表达式的类加载器无法加载到Servlet相关的类,我通过主动类加载的方式加载了相关类,同时我也找到了一个类提供了一个静态方法获取request和response,函数位置com.softwarementors.extjs.djn.servlet.ssm.WebContextManager.get(),但是一直获取到的是null,所以这种方法就放弃了。
从另外一个维度来考虑下JAVA中间件(jetty)是怎么处理请求的,针对每个请求中间件会单启动一个线程来处理,针对这个请求的参数之类的会绑定到当前线程上,这里我做了个实验,如下图所示
这里会发现在nexus的程序中jexl的表达式的线程和webapp的线程是同一个线程,那么这里在jexl表达式中就有可能获取到webapp请求中的一些变量,这里思路已经说完了,至于怎么去获取response对象,这里直接给出提示,可以直接Debug跟踪类java.lang.Thread中threadLocals变量,至于怎么实现回显,仁者见仁智者见智了,大家可以去调试下。
这里给出最终的利用截图