最近看到了一个国外高中生的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. 树状图展示终端目录内容-tree

    以树状图列出目录的内容,让你一目了然 执行 tree 指令,它会列出指定目录下的所有文件,包括子目录里的文件. 安装 我们通过包管理工具可以方便的安装它 mac - brew install tree ...

  2. JS中escape()、encodeURI()、encodeURIComponent()区别详解

    avaScript中有三个可以对字符串编码的函数,分别是: escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decode ...

  3. XCTF-WEB-高手进阶区-NaNNaNaNNaN-Batman-笔记

    上来直接百度先搜下Batman -_-|| 不存在的传令兵么 本身是下载下来了一个文件web100 打开发现是如下内容 可以看出这个是个脚本语言,因此尝试修改后缀为html,发现是一个OK框 现在是想 ...

  4. Java—增强for循环与for循环的区别/泛型通配符/LinkedList集合

    增强for循环 增强for循环是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的. 它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作. ...

  5. java 的API及Object类

    一 Java的API Java 的API(API: Application(应用) Programming(程序) Interface(接口)) Java API就是JDK中提供给我们使用的类,这些类 ...

  6. 栈及其简单应用(python代码)

    栈属于线性结构(Linear Struncture),要搞清楚这个概念,首先要明白”栈“原来的意思,如此才能把握本质."栈“者,存储货物或供旅客住宿的地方,可引申为仓库.中转站,所以引入到计 ...

  7. C#LeetCode刷题之#258-各位相加(Add Digits)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3860 访问. 给定一个非负整数 num,反复将各个位上的数字相加 ...

  8. C#LeetCode刷题之#867-转置矩阵(Transpose Matrix)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3756 访问. 给定一个矩阵 A, 返回 A 的转置矩阵. 矩阵的 ...

  9. JavaScript map+parseInt 容易产生的误区

    map /** * 语法: * var new_array = arr.map(function callback(currentValue[,index[,array]]){ * // return ...

  10. ROS 八叉树地图构建 - 给 octomap_server 增加半径滤波器!

    为了在每帧点云中滤除噪声点,选择了半径滤波器,也用高斯滤波器测试过,但是没有半径效果好,这里记录下在 octomap_server 中增加半径滤波器的步骤,并在 launch 中配置滤波器参数. 一. ...