认证绕过
POST /WebGoat/auth-bypass/verify-account
secQuestion0=test&secQuestion1=test&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746
按照上面例子移除,没有成功,直接看代码了
这里会先调用下面的parseSecQuestions()
处理,将名字包含secQuestion
的参数转为HashMap类型。
然后会进入verificationHelper.didUserLikelylCheat()
中进行判断参数的secQuestion0
和secQuestion1
是否为数据库中的数据,是的话就返回True,判定作弊了
下面的verificationHelper.verifyAccount()
才是重头戏
这里先判断submittedQuestions的参数数量,然后是重点
submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))
翻译翻译
存在参数secQuestion0 && !参数等于数据库中的参数
才返回认证失败
还有点绕?再换种说法,存在参数secQuestion0
并且参数不等于数据库中的参数
才认证失败,其他情况默认认证成功
我画了一个流程图(不大方便想出来就画出来),左边是正常的情况,右边是这里的写法。应该默认认证失败,只有包含secQuestion0
和secQuestion1
并比对成功才认证成功。
下面是我认为的一种写法
或者直接去掉containKey,只留下两个判断qeuals的。
JWT tokens
什么是JWT token
JSON Web Token(JWT)是一种开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。可以验证和信任此信息,因为它是数字签名的。JWTs 可以使用 secret (使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
jsonweb令牌用于携带与客户机的身份和特征(声明)相关的信息。此“容器”由服务器签名,以避免客户端为了更改(例如)身份或任何特征(例如:将角色从简单用户更改为管理员或更改客户端登录名)而对其进行篡改。此令牌在身份验证期间创建(在身份验证成功的情况下提供),并在任何处理之前由服务器验证。应用程序使用它来允许客户端向服务器提供代表其“身份证”的令牌(包含有关其所有用户信息的容器),并允许服务器以安全的方式验证令牌的有效性和完整性,所有这些都采用无状态和可移植的方法(可移植的方式是客户端和服务器技术可以不同,包括传输通道,即使HTTP是最常用的)
JWT token结构
图片来自WebGoat,token经过base64编码,包含三部分:
- header
- claims / Payload
- signature
认证过程
JWT 签名安全性问题
在RFC specification 中定义了alg: none
是一个合法的选项,可以使用不安全的JWT,这样生成的token,若后端按照headers中的算法来计算signature而不是按照自己设定的来计算,就会导致绕过校验这部分
这里直接解析层JWT的格式,然后就直接获取声明/payload中的数据,判断是否为admin了,没有进行校验。
跳转到Jwt parse()
中,由于代码过长,只截取其中一段
这一部分就是负责校验,但是由于我们设置为空,所以直接跳过了。并且,阅读后发现,就算header中alg
设置为HS512,只要base64UrlEncodedDigest
为null,就会跳过校验。
Refreshing a token
通常有两种类型的令牌:访问令牌和刷新令牌。访问令牌用于对服务器进行API调用。访问令牌有一个有限的生命周期。一旦访问令牌不再有效,刷新令牌就可以向服务器发出请求,来获取新的访问令牌。刷新令牌可能会过期,但其过期更长。这解决了用户必须再次使用其凭据进行身份验证的问题。
应该存储用户的ip或者地理位置来判断refresh token的使用情况
Assignment
这个其实比较绕,不看源码的话感觉是弄不出来的
这里要先通过/JWT/refresh/login
登录,获得我们的access_token和refresh_token。这里的user和password都是硬编码的。
拿到refresh_token之后,因为我们目的是让Tom买单,所以访问/JWT/refresh/newToken
,使用refresh_token,再附带上这@RequestHeader(value = "Authorization", required = false)
要求的,Header里的Authorization,内容自然是Tom的JWT token。
这样拿到access_token之后,再去checkout
并且,由于parse代码和上一个实验是一样的,所以这里alg
是否为none都没有所谓。
同上,这里没有先根据算法判断是否需要校验,而是没有校验的base64的话直接就不校验了。并且,refresh_token这里也没有判断绑定哪位用户,而是可以用来获取所有用户的access_token
修复的话,不用parse,使用parseClaimsJws即可。
final
POST /WebGoat/JWT/final/delete
Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
这一行开始,setSigningKeyResolver
的作用是设置用来HMACSHA256算法中的密钥(JSON Web Tokens - jwt.io里的 your-256-bit-secret)
那现在就清楚了,因为这里可以注入,所以我们可以控制这里的密钥,来让我们绕过校验。这里使用base64编码后的,是因为return 时候用了BASE64.decode.
当然,这里的union,在mysql中的话不添加from information_schema.tables
也可以返回数据,但是在这个题目中就会报错,具体原因,可能是webgoat用的HSQLDB,或者是java的问题(溜)。报错的话,两种都尝试一下就好了
建议用法
- 固定算法,不要允许客户切换算法
- 确定使用了合适的密钥长度
- 确定token不包含个人信息,如果需要传递私密信息,再加密
- 添加更多测试用例
- 看看RFC中的使用方法https://tools.ietf.org/html/rfc8725#section-2
密码重置
功能点:输入邮箱部分,若输入不存在邮箱,也应该统一返回Email Sent,否则可以用来探测账户是否存在
安全问题
密码重设链接应满足以下问题
- 一个有随机token的独一无二链接
- 只可以被用一次
- 这个链接只是短时间内有效
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付