WebGoat代码审计其4-失效的身份认证

Posted by Mr.Be1ieVe on Saturday, July 3, 2021

认证绕过

POST /WebGoat/auth-bypass/verify-account

secQuestion0=test&secQuestion1=test&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746

按照上面例子移除,没有成功,直接看代码了

这里会先调用下面的parseSecQuestions()处理,将名字包含secQuestion的参数转为HashMap类型。

然后会进入verificationHelper.didUserLikelylCheat()中进行判断参数的secQuestion0secQuestion1是否为数据库中的数据,是的话就返回True,判定作弊了

下面的verificationHelper.verifyAccount()才是重头戏

这里先判断submittedQuestions的参数数量,然后是重点 submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))

翻译翻译

存在参数secQuestion0 && !参数等于数据库中的参数才返回认证失败

还有点绕?再换种说法,存在参数secQuestion0并且参数不等于数据库中的参数才认证失败,其他情况默认认证成功

我画了一个流程图(不大方便想出来就画出来),左边是正常的情况,右边是这里的写法。应该默认认证失败,只有包含secQuestion0secQuestion1并比对成功才认证成功。

下面是我认为的一种写法

或者直接去掉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的独一无二链接
  • 只可以被用一次
  • 这个链接只是短时间内有效

「真诚赞赏,手留余香」

Mr.Be1ieVe's Treasure

真诚赞赏,手留余香

使用微信扫描二维码完成支付