2FA
这道题他先给了一个例子,https://henryhoggard.co.uk/blog/Paypal-2FA-Bypass
是一个绕过PayPal 2FA的案例,案例中把认证的问题字段抓包删除之后发送直接绕过了验证。
照着这个思路做他给的这道题,直接删除字段好像不行,我们先审计一下源码:
可以看到,想要返回success,需要verificationHelper.didUserLikelylCheat((HashMap) submittedAnswers)
返回False,然后verificationHelper.verifyAccount(Integer.valueOf(userId), (HashMap) submittedAnswers)
为True,跟进这两个函数看一下:
想要didUserLikelylCheat
返回0,不能提交正确的答案,他这道题就是让我们必须绕过验证,他这里检查的作弊应该是直接用源码里的验证答案~到这里删除包里的答案字段应该是可以行得通的,继续往下看为什么不可以:
想要verifyAccount
返回1,对于map的size()方法是返回键值对的个数,这里他检测了输入的键值对数量是否和答案的键值对数量是否相同,因此不能直接删除包里的答案字段了,后面两个if是检测输入的值和答案相同,最终返回true。
目前看来,我们必须输入两个答案字段(两个键值对),并且必须携带错误的答案绕过作弊检测,输入正确的答案绕过身份验证,看起来好像很矛盾,但是如果我们的键没有他要检测的这两个键就可以直接绕过身份验证的if检测:
但是似乎回显并不正确?看一看键值对怎么传进去的:
原来是需要包含secQuestion字段的键,但是str.contains()方法是检测子句是否包含,也就是说我们传入包含键名secQuestion字段的键就可以了,比如secQuestiona、secQuestionb:
通过辣:
JWT-4
关于jwt不再赘述,这里先看题
这里让修改账户的jwt使之成为管理员用户,然后投票,先抓包看看:
与此同时,我们还看见了access_token=””,很奇怪:
看见了jwt,但是不知道如何判定身份,进入源码审计一下:
这里大致看得出来是通过admin
把作为身份判定的关键字,这里可以构造
"adimn":"true"
isadmin因为是bool类型,直接传入True,发现验证还是没通过,找一下这个access_token:
先看到如果user的值包含在vaildUsers中,就会加入非admin的jwt,否则判定为未认证,再往下看:
从这一段看accessToken不能为空,否则会被限制为guest,与此同时user值不能为Guset,也不是vaildUser的子段。
跟之前差不多,似乎只要不为空即可
但是又发现:
400了,看了一下路由,发现是提交到refresh的,到这段路由看一下:
发现虽然没有解析admin,但也不至于报错,而且required = false?仔细检查发现json不同键值对之间要用,
间隔~
修改后发现:
这里应该生成token,这道题的入口应该不在这里,因为根据这段路由来看我们无法自建用户和密码通过验证,最终导致UNAUTHORIZED
其实目前看来,需要找传到这段路由的数据然后修改包:
然后抓到了这个包:
但是发现不行,突然发现有一个切换用户的地方,笑死,还是太急躁了,没有好好检查页面就开始乱审计,先点击切换用户登录,这时候就可以点击重置投票的按钮了:
这里还是得用专门的网站,不然解码有乱码,修改后加密发送很可能出问题:JSON Web Tokens - jwt.io
这里有一个坑,题目是没有签名的,所以把header和payload贴过去就可以了,后面蓝色部分是网站初始化生成的,不是我们需要的,所以我的最终payload是:
eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3MDEzMjg0ODksImFkbWluIjoidHJ1ZSIsInVzZXIiOiJUb20ifQ.
JWT-5
这题是jwt破解,找个工具用rockyou.txt跑一下就可以了,要把exp时间戳修改到未过期的时间
防护
他这里提到了一个无状态会话:
{
"token_type":"bearer",
"access_token":"XXXX.YYYY.ZZZZ",
"expires_in":10,
"refresh_token":"4a9a0b1eac1a34201b3c5659944e8b7"
}
原文:
正如你所看到的,刷新令牌是一个随机字符串,服务器可以跟踪它(在内存或数据库中),以便将刷新令牌与授予刷新令牌的用户相匹配。因此,在这种情况下,只要访问令牌仍然有效,我们就可以称之为“无状态”会话,服务器端就没有设置用户会话的负担,令牌是自包含的。当访问令牌不再有效时,服务器需要查询存储的刷新令牌,以确保该令牌不会以任何方式被阻止。 每当攻击者持有访问令牌时,该令牌仅在一定时间内有效(例如10分钟)。然后,攻击者需要刷新令牌来获取新的访问令牌。这就是为什么刷新令牌需要更好的保护。也可以使刷新令牌无状态,但这意味着查看用户是否吊销了令牌将变得更加困难。服务器完成所有验证后,必须向客户端返回一个新的刷新令牌和一个新访问令牌。客户端可以使用新的访问令牌来进行API调用。
看起来jwt只给了一个10分钟但是我们不知道是什么时候开始什么时候结束
原文:
你应该检查什么?
无论选择哪种解决方案,都应该在服务器端存储足够的信息,以验证用户是否仍然可信。你可以考虑很多事情,比如存储ip地址,跟踪刷新令牌的使用次数(在访问令牌的有效时间窗口中多次使用刷新令牌可能表明有奇怪的行为,你可以撤销所有令牌,让用户再次进行身份验证)。还要跟踪哪个访问令牌属于哪个刷新令牌,否则攻击者可能会使用攻击者的刷新令牌为其他用户获取新的访问令牌(请参阅https://emtunc.org/blog/11/2017/jwt-refresh-token-manipulation/写一篇关于这种攻击是如何运作的好文章)此外,检查用户的ip地址或地理位置也是一件好事。如果您需要发放新的令牌,请检查位置是否仍然相同(如果不撤销所有令牌并让用户再次进行身份验证)。
- 储存ip
- 跟踪刷新令牌次数
- 跟踪哪个访问令牌属于哪个刷新令牌
原文:
需要刷新令牌 在现代单页应用程序(SPA)中使用刷新令牌有意义吗?
正如我们在关于存储令牌的部分中所看到的,有两种选择:网络存储或cookie,这意味着刷新令牌就在访问令牌旁边,因此如果访问令牌被泄露,刷新令牌也可能被泄露。当然,大多数时候都是有区别的。访问令牌是在您进行API调用时发送的,刷新令牌仅在应该获得新的访问令牌时发送,在大多数情况下,该访问令牌是不同的端点。如果您最终在同一台服务器上,您可以选择只使用访问令牌。 如上所述,使用访问令牌和单独的刷新令牌为服务器提供了一些优势,使其不必反复检查访问令牌。仅当用户需要新的访问令牌时才执行检查。当然,只使用访问令牌是可能的。在服务器上,您存储的信息与为刷新令牌存储的信息完全相同,请参阅上一段。通过这种方式,您每次都需要检查令牌,但根据应用程序的不同,这可能是合适的。在存储刷新令牌以进行验证的情况下,保护这些令牌也很重要(至少使用哈希函数将它们存储在数据库中)。 JWT是个好主意吗? 有很多可用的资源对使用JWT令牌进行Cookie的客户端到服务器身份验证的用例提出了质疑。使用JWT令牌的最佳位置是服务器与服务器之间的通信。在普通的web应用程序中,您最好使用普通的旧cookie。有关详细信息,请参阅:
- 访问令牌泄露和刷新令牌泄露大多数时候是有区别的
- 使用JWT令牌的最佳位置是服务器与服务器之间的通信
可能翻译有点问题,后面或许会单独开一篇研究上面提到的文章链接
jwt-7
这题先给了一个案例here,大概是刷新令牌没有和访问令牌或者用户绑定,我可以用自己的刷新令牌刷新别人的令牌,大概过程是
{"code":0,"data":{"access_token":"XXX.YYY.ZZZ","access_token_expiration":"Thursday, November 9th, 2017, 10:27:33 PM","refresh_token":"ABC123"}}
这里收到了一个刷新令牌”refresh_token”:”ABC123”
然后发送
POST /auth/refresh HTTP/1.1
Host: auth.example.com
Content-Type: application/json
Authorization: Bearer XXX.YYY.ZZZ
{"refresh_token":"ABC123"}
最终收到了新的令牌
然后回到这道题:From a breach of last year the following logfile is available here Can you find a way to order the books but let Tom pay for them?
我们先点击链接看看日志里面有一个token,解密得到:
但是我们并没有得到刷新令牌之类的,还是审计一下源码。
有了之前的经验,这次直接找到提交的路由先进行审计:
总的来说,需要创建一个header为Authorization,然后把字符串Bearer
(这里字符串后面有一个空格)替换为空,不知道是不是必要的(后来抓包发现是自带的),处理后的字符串进行了jwt解析,也就是说传入的数据是这样的(看了wp然后又审了一遍才反应过来的,又学到了hh):
Authorization: Bearer {JWT}
然后要user值为Tom,至于alg是否置空似乎都可以success
再看看刷新token部分:
和上面重叠的比较多,这里需要user和refreshToken都不为空,refreshToken来自我们传入的请求体,是一个json,也就是jwt,同时他要存在于validRefreshTokens,追一下:
这里发现是20个随机字母,这里他或许会自己生成
当然这些只是顺带看一眼,我们按照之前log拿到的JWT,修改一下时间戳,发送试试:
ok了
jwt-8
这道题很奇怪,源码中找不到对应的路由,但是其对应的源码应该就是这一段:
这里看起来有一个很明显的sql注入,大概逻辑是header中传入一个kid然后与数据库比对,返回查询结果 ;然后payload中的username为Tom即可success,尝试:
发现返回“Not a valid JWT token”,再审审:
这里存在一个jwt解析器,之前没看明白:
JJWT使用笔记(二)—— JWT token的解析 - 简书 (jianshu.com)
这个解析器可以发现我的jwt没签名,因此返回错误,但是我怎么知道签名呢?看
这个setSigningKeyResolver
他这里跟示例不一样,使用了一个获取器里包含箭头函数,应该是特定用法,用于动态获取SigningKey,暂不深究语法。然后是sql查询根据kid找到对应的SigningKey,并进行了base64解码,为什么要解码呢?因为查询出来的SigningKey应该是base64加密的,此时我们让kid查询返回为空,然后union插入一段base64加密后的字段(SigningKey),那么我们就可以控制SigningKey了
water3 => d2F0ZXIz
这里有个小坑:
看着时post发包下意识使用#
注释,但是这里可能上下文不支持服务器会出错,需要使用--
来注释:
ok了
Password reset-6
这里让我们想办法重置Tom的密码,我们先点击忘记密码,然后输入tom的邮箱发送验证,这里抓包看一下路由,然后进入相应的源码:
我们发现这里挺奇怪的,包头的host如果发生变化,他就尝试发送邮件。这里有个坑就是这种写法是为了信息不出网和配合webwolf使用 ,可能更多的是让我们体验一下吧?
这里我们修改host的端口到webwolf,也就是9090,响应将把带着重置链接的邮件发送到9090端口,这时我们查看9090接收到request就能拿到Tom的重置链接。
这里有个两个问题:
修改了host端口为什么还能被我们的后端(服务器)正确响应并发送邮件?
这是因为spring的基于路径匹配的路由,不管host是什么,只有主机收到如何路径的url,就会触发该路由的解析,因此修改了端口也能正常发包。当然这里也可以可以使用
application.properties
或application.yml
文件来配置端口号和路由绑定,但是这道题的路由显然没有~
如果修改了host地址为与url不匹配的其他主机?
这类请求常常被称为跨域请求(Cross-Origin Request)。
在浏览器中,跨域请求可能会受到同源策略(Same-Origin Policy)的限制,不允许从一个源(域、协议、端口)向另一个源发送 AJAX 请求。但是在服务器端,使用 Python 的
requests
库发送请求时,一般不会受到同源策略的限制。但需要注意的是,在某些情况下,服务器端也可能会对请求进行同源检查或设置跨域请求的限制,这取决于服务器端的配置。例如,服务器可能会使用 CORS(跨域资源共享)来控制是否允许跨域请求。
然后我们拿到了重置链接:
然后把host改为修改前的xxxx:8080,此时显示404,我们按照正常流程重置自己密码走一遍,发现路径少了一个WebGoat,可能时这个版本没有把8080的默认路由绑定到WebGoat,所以要手动添加一下:
http://172.20.10.3:8080/WebGoat/PasswordReset/reset/reset-password/55af2d7b-c49b-43c3-8d62-a2110385036b
重置一下就ok了