技术创想 | 反序列化
导语:近两年来黑客攻击频繁不断,关于反序列化的攻击持续增多。反序列化漏洞在每年的国家护网之中充当着尖刀的角色,是各个红队攻克目标系统或者拿到外网口子的重要武器。
01.反序列化漏洞简介
Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在文件、内存、数据库中,ObjectOutputStream
类的writeObject()
方法可以实现序列化。
反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream
类的readObject()
方法用于反序列化。
序列化和反序列化本身并不存在问题。主要问题在于,如果 Java 应用对用户输入不做限制,即对不可信数据做了反序列化处理,那么攻击者就可以通过构造恶意代码,让反序列化产生非预期的对象,非预期对象在产生过程中就有可能带来任意代码执行,即我们所说的 RCE(Remove code Exection)。如果在ObjectInputStream
在反序列化时候设置 Java 类型的白名单,那么就可以减少一定的影响
一般特别要注意非预期的对象,正因为此,Java 标准库以及大量第三方公共类库成为反序列化漏洞利用的关键,例如commons-collections Collection
可以实现任意代码执行
在真实的场景中,一般不会有人直接写一句执行命令的代码在readObject
中,这样写的开发基本上都会拉出去祭天。所以在反序列化漏洞通常会需要 Java 的一些特性进行配合比如反射。然后就是利用链的寻找。所以反序列化漏洞需要三个东西
反序列化入口
目标方法
利用链(gadget chain)
在挖掘反序列化漏洞中基本上都回去寻找重写了readObject
方法的类,配合上 Java 的反射机制,构造利用链,形成了 Java 中特色的反序列化攻击。
【一点废话】
这次简要讲解 Shiro 反序列化漏洞的分析,因为个人理解 Shiro 反序列化属于特征比较明显,利用起来比较方便。主要说明为什么会形成反序列化漏洞的原因,中间牵扯到的 gadget 链大家可以去学习一下,比较经典的 gadget 链就是commons-collections Collection
,它利用 Java 的反序序列化和反射机制创造了第一个反序列化漏洞。
02.Shiro 简介
简单说一下 Shiro 的内部架构
Subject:主体,可以看到主体可以是任何可以与应用交互的"用户";
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即说明什么情况下算用户认证通过了;
Authorizer:授权器,或者访问控制器,用来决定主体是否有权进行相应的操作;即控制者用户能访问应用中的那些功能;
Realm:可以有一个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供,注意:Shiro 不知道你的用户/权限存储在哪及何种格式存储;
SessionManager:用来管理 Session 的生命周期。Shiro 并不仅仅可以用在 Web 环境,也可以用在普通的 JavaSE 环境、EJB 等环境;所以,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互数据;这样的话,比如我们在 Web 环境使用,刚开始是一台 Web 服务器;接着又上了一台其他服务器;这时两台服务器的会话数据放到同一个地方,就可以实现自己的分布式会话。
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
CacheManager:缓存控制器,来管理用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
Cryptography:密码模块,Shiro 提供了一些常见的加密组建用于密码加密/解密的。
03.Shiro 反序列化
Shiro 为了让浏览器重启或者服务重启后用户的状态不丢失,它支持持久化的信息加密保存在了 cookie 中也就是所谓 RememberMe 字段。读取的时候服务端对 rememberMe 的 cookie 值,先进行 base64 解码然后 AES 解密再反序列化,就导致了反序列化 RCE 漏洞。
04.利用 CC 链
因为 shiro550 在小于 1.2.4 版本中使用的密钥 key 是包含在代码中的,这时候我们就可以构造一个恶意类使用该密钥加密然后进行 base64 解密,那么服务端进行反序列化的时候就会触发我们构造的恶意类,执行效果如下
所以 shiro 分序列化漏洞的主要关键点在于加密和解密两个步骤中,使用代码调试一下,看一下在加密解密中主要做些什么操作,形成了反序列化漏洞
使用的环境是:Tomcat8.0.56 + shiro1.2.4 + Java8
05.漏洞调试
01.加密
程序断点打在CookieRememberMeManager
位置
进入onSuccessfulLogin
在onSuccessfulLogin
函数中会进入到forgetIdentity
中,传入参数为subject
。subject
是 Shiro 中重要的一个概念,简要来说就是当前用户的主体,后续所进行的认证和授权所有操作都会围绕着subject
对象来进行。subject
对象中包含request
、response
等等
forgetIdentity
用来处理subject
主体的request
和response
,但是相对重要的是getCookie().removeFrom()
,里面标明了我们 cookie 中的一些相关信息,比如 rememberMe,然后将这些添加到 response 包中
结束这个过程回到onSuccesfulLogin
函数当中 ,进行下一步判断是否在登录当中选择了 rememberMe,如果选择进入重要的一步,在下一步当中会进行加密和序列化操作
进入方法后会根据Subject
等信息生成一个新的PrincipalCollection
对象,里面包含前面所说的所有信息,并将这个对象传入converPrincipalsToBytes()
方法
从图中可以很清晰看到,这个方法主要是将PrincipalCollection
对象变成一个 byte 数组,用序列化的方式去变,查看具体实现方法
进行了序列化并返回序列化后的 Byte 数组,序列化后回到converPrincipalCollection
方法,有一个getCipherService
方法用来判断加密模式是否为空,实际 shiro 已经固定好了加密形式为 AES 加密,采用 PKCS5Padding 模式进行填充。然后进入加密模块,对序列化后的 Byte 数组进行加密
在加密模块中按照我们已知的加密形式(AES-PKCS5Padding),这种加密形式首先将数据进行分块处理,最后不满规定大小的块进行填充补齐,如果刚好满足则另起一组进行填充。然后加密形式会在后面的 shiro721 中详细解释,这里就不进行跟进,就当它加密完了。我们主要目标点在于加密的密钥 key 上。在加密的时候调用getEncryptionCipherKey
,名字上来看就是个获取加密密钥
这里this.getEncryptionCipherKey
返回的是encryptionCipherKey
,这个密钥由setEncryptionCipherKey
来设置,跟踪可以发现setCipherKey
调用了setEncryptionCipherKey
传入的 key 为DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
果然加密的 key 是写在了代码中,因为 shiro 是一个开源框架,开源了这个 key 就是人尽皆知了。那么使用这个 key 加密的信息就如同明文一样了
从流程里可以看到加密后的反序列化数据进行 base64 加密然后保存在 cookie 中,这样就完成了身份信息的序列化传输。
02.解密
切入点在二次登陆的时候会有 rememberMe。getRememberedSeriallizedIdentity
跟入查看一下方法调用,如何获取记住的序列化身份认证
this.getCookie().readValue(request,response)
读取 Cookie 中的数据,从readvalue
中获取
判断是否存在 rememberMe,存在进行 base64 解码并返回,进入下一个判断conberBytesToPrincipals
中进行,这个方法主要对数据的拆解和加密数据的解析
获取密码服务,这里加解密服务就是
到了这里其实就是和加密的翻转形式,只要了解这种形式直接就可以猜到解密的流程,密钥使用的还是在代码中固定的密钥,我们直接看解密后的反序列化操作
这里实质上到了反序列化的操作
使用的是默认的反序列化操作,也并没有做白名单操作,这样我们只要拿到 key,就可以任意构造恶意 payload 传递到达 RCE 的效果
这里说明一下,就像开头所说的一样这个恶意 payload 必须是实现了 Java 序列化接口的类,通过 Java 特有的反射机制来创造,可以关注一下 ysoserial 的 gadget 链
shiro 在 1.2.4 版本后的更新补丁将固定的密钥 key 改为了随机密钥,因为采用 AES-PKCS5Padding 方式,又引申出了另一种攻击方式 Padding Oracle Attack + Shiro 的攻击形式,导致反序列化加载恶意 pay
03.Shiro721
在 721 当中主要使用 Padding Oracle 方式进行绕过,这种方式可以在没有 key 的情况下绕过密钥 key 实现对密文的加解密
拿 shiro 采用的 AES-128-CBC PKCSPadding 的加密模式
AES 代表加密方式
CBC 是分组模式
128 也就是 16 个字节
在这里采用的PKCS5Padding
填充方式,PKCS5Padding
为最后结束缺几个字节就补几个字节的几(例如缺 5 个字节,就会补 5 个字节的 5 作为填充物) ,如果最后一组是满的话,采用分组模式是必须进行填充的,满了会再充满一个新组
从网上偷一张两张图,只不过是 8 位的填充示意图,16 位的跟这个大同小异,都是同一个意思。
分组完成后对明文进行加密,加密流程如下(借的图)
每一组加密前先和一个初始向量(IV)进行异或,异或后的值叫做中间值(IMV),利用密钥 key 对中间值进行加密得到第一组密文,第一组密文作为向量与第二组密文异或得到第二个中间值,利用同样的 key 对第二组的中间值加密,以此类推所有密文都进行加密
解密流程实际就是一个翻转的流程,如下:
第一组密文块经过 key 解密后得到中间值,中间值与 IV 异或得到第一组明文,然后第一组的密文块作为 IV 与第二组异或得到第二组明文,以此类推得到所有明文
在 shiro721 的利用当中,我们需要获取到一个合法的用户身份。这就是一个比较难的点,所以在日常使用这个漏洞的时候,会配合 XSS 来利用。
因为采用 PKCS5 填充形式,在数据进行解密完成后,会验证最后的填充物是否符合相关规定,这里举例为 8 字节填充,只有以下 8 种情况
在解密后获取到中间值IMV
时,中间值时不变的。我们利用改变前一位的 iv 和 IMV 进行异或,使其得到最后一位为0x01
的数值,因为异或的可以逆性,当我们得到0x01
时,我们就可以获得IMV
的最后一位数值
然后去爆破倒数第二位,使用IMV(最后一位) XOR 0x02
得到数值,将其设置为iv
最后一位,然后去爆破倒数第二位,往后以此类推,直到将所有的IMV
爆破出来
在shiro
中获取到所有的用户认证明文数据后,需要将恶意的 payload 拼接到真实用户认证数据的后面,然后发送至server
端
加密流程如下:要加密的 payload 是hello
首先将
hello
进行分块处理,这里假设AES
分组为 8 字节,那么明文进行填充后hello 0x03 0x03 0x03
创建两组明文,内容为
0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA
另外一个分组全部为 0,将两组明文进行拼接,拼接为0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA
将两组明文添加到
shiro
合法的cookie
后面,全零的一组作为向量iv
进行修改,按照Padding Oracle
攻击的方式,直到页面不报错,此时最后一位是0x01
,以此类推进行爆破。这样我们就可以反推出 IMV 的值因为 IMV 值是完全不变的,它是我们构造的密文通过
key
解密得到的,此时我们用得到的 IMV 去异或我们想要得到的明文hello
,假设得到的全部都是0xBB
,我们如果想要服务器解析得到hello
,我们只需要把异或得到的数据和第一组全A
的数据拼接传输到服务端,数据如下:0xBB 0xBB 0xBB 0xBB 0xBB 0xBB 0xBB 0xBB 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA
这样服务器解密后就会得到hello
如果是多个分组,我们传递的恶意
payload
,我们只需要在0xBB 0xBB 0xBB 0xBB 0xBB 0xBB 0xBB 0xBB 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA 0xAA
前面添加八个字符,继续按着这样的流程下去,就会将恶意payload
全部加密,传递到服务器解密后就会执行我们的恶意代码
06.总结
因为 Java 的系统会引用大量的第三方包,在 Java 反序列化漏洞中第三方包成为了主要的 gadget 利用灾害地,在写代码的时候要谨慎重写 readObject 类,最好做好相关的限制,减少反序列化漏洞的产生。文中没有做漏洞的复现只是做了过程上的调试,有兴趣的可以自己搭建环境,docker 公开的仓库都有打包好的环境。
关于领创集团(Advance Intelligence Group)
领创集团成立于 2016 年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021 年 9 月,领创集团宣布完成超 4 亿美元 D 轮融资,融资完成后领创集团估值已超 20 亿美元,成为新加坡最大的独立科技创业公司之一。
往期回顾 BREAK AWAY
▼ 如果觉得这篇内容对你有所帮助,有所启发,欢迎点赞收藏:
1、点赞、关注领创集团,获取最新技术分享和公司动态。
2、关注我们的公众号 & 知乎号「领创集团 Advance Group」,了解更多企业动态。
版权声明: 本文为 InfoQ 作者【领创集团Advance Intelligence Group】的原创文章。
原文链接:【http://xie.infoq.cn/article/bda308a0be289ddf9632d51c2】。文章转载请联系作者。
评论