前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java代码审计 -- 失效的身份验证

Java代码审计 -- 失效的身份验证

原创
作者头像
Gh0st1nTheShel
修改2022-01-23 14:33:27
1.1K0
修改2022-01-23 14:33:27
举报
文章被收录于专栏:网络空间安全网络空间安全

欢迎关注我的微信公众号《壳中之魂》,查看更多网安文章

WebGoat

使用docker搭建环境

代码语言:javascript
复制
docker run -it -p 127.0.0.1:80:8888 -p 127.0.0.1:8080:8080 -p 127.0.0.1:9090:9090 -e TZ=Europe/Amsterdam webgoat/goatandwolf:v8.2.2 

在这里有个小问题,由于后面需要用到burpsuite,但是burpsuite抓不到本地包,这个环境又不能使用本机IP登录,所以最好把127.0.0.1换成本机ip,如10.10.10.10

随后打开页面进行注册

http://192.168.3.25:8080/WebGoat/login#

如需审计源码则可以去github下载

Release v8.2.2 · WebGoat/WebGoat · GitHub

身份验证绕过(逻辑漏洞)

题目要求我们回答安全问题来重置密码,然而答案明显不知道,先随便输入然后抓包

首先确定了接口,为/WebGoat/auth-bypass/verify-account

将包发送出去,查看结果

先要绕过只需要把secQuestion0/1换为secQuestion2/3即可

查看源码

代码语言:javascript
复制
@PostMapping(path = "/auth-bypass/verify-account", produces = {"application/json"})
@ResponseBody
public AttackResult completed(@RequestParam String userId, @RequestParam String verifyMethod, HttpServletRequest req) throws ServletException, IOException {
    AccountVerificationHelper verificationHelper = new AccountVerificationHelper();
    Map<String, String> submittedAnswers = parseSecQuestions(req);
    if (verificationHelper.didUserLikelylCheat((HashMap) submittedAnswers)) {
        return failed(this)
                .feedback("verify-account.cheated")
                .output("Yes, you guessed correctly, but see the feedback message")
                .build();
    }

    // else
    if (verificationHelper.verifyAccount(Integer.valueOf(userId), (HashMap) submittedAnswers)) {
        userSessionData.setValue("account-verified-id", userId);
        return success(this)
                .feedback("verify-account.success")
                .build();
    } else {
        return failed(this)
                .feedback("verify-account.failed")
                .build();
    }

}

当首先看到有两个if体

第一个if体:

这个if体是当用户输入了正确答案,正如我所说,这两个问题的密码正常情况下是回答不出来的,除非读了源码,所以didUserLikelylCheat方法就是判断输入的内容是否是源码的答案,若是则提醒用户作弊

重点在第二个if体内

第二个if题对用户输入进行比对,若用户成功绕过则显示verify-account.success的内容,否则显示verify-account.failed的内容

而问题出在verifyAccount方法,步入方法

代码语言:javascript
复制
public boolean verifyAccount(Integer userId, HashMap<String, String> submittedQuestions) {
    //short circuit if no questions are submitted
    if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
        return false;
    }

    if (submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))) {
        return false;
    }

    if (submittedQuestions.containsKey("secQuestion1") && !submittedQuestions.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {
        return false;
    }

    // else
    return true;

}

可以很轻易地看出,判断的语句是获取secQuestion0&1,然后和答案进行比对吗,若比对错误则返回false,除此以外任何情况都返回true,所以我们传入secQuestion2&3不在判断范围内,所以不会进入secQuestion0&1的if判断,直接就返回true

JWT Token漏洞

失效的身份验证会导致攻击者破译密码、密钥或者会话令牌或者利用其他开发漏洞暂时或长久地冒充其他用户的身份,导致攻击者可以执行受害者用户的任何操作。

什么是JWT

Json Web Token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519

该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景,是目前最流行的跨域认证解决方案。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{ "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分" }

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的数据结构

实际当中 JWT 长这个样子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkNURkh1YiIsImlhdCI6MTUxNjIzOTAyMn0.Y2PuC-D6SfCRpsPN19_1Sb4WPJNkJr7lhG6YzA8-9OQ

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的

JWT 的三个部分依次如下:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

写成一行,就是下面的样子。

Header.Payload.Signature

每个部分最后都会使用 base64URLEncode方式进行编码

#!/usr/bin/env python function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,以上面的例子,使用 base64decode 之后:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

{ "alg": "HS256", "typ": "JWT" }

header部分最常用的两个字段是alg和typ。

alg属性表示token签名的算法(algorithm),最常用的为 HMAC SHA256(写成 HS256)和RSA算法

typ属性表示这个token的类型(type),JWT 令牌统一写为JWT。

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段,还可以在这个部分定义私有字段,以上面的例子为例,将 payload 部分解 base64 之后:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkNURkh1YiIsImlhdCI6MTUxNjIzOTAyMn0

{ "sub": "1234567890", "name": "CTFHub", "iat": 1516239022 }

注意:JWT 默认是不会对 Payload 加密的,也就意味着任何人都可以读到这部分JSON的内容,所以不要将私密的信息放在这个部分

Signature

Signature 部分是对前两部分的签名,防止数据篡改

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

参考链接

https://jwt.io/introduction/

https://en.wikipedia.org/wiki/JSON_Web_Token

https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

JWT Token

失效的身份验证的靶场为(A2)Broken Authentication中的JWT tokens关卡,打开关卡,可以看到题目的要求

(尝试修改你的token以获得管理员权限,并重置投票)

首先先以guest的身份进行重置投票

进行抓包

可以发现,access_token的值为空

把身份切换为tom,尝试重置投票

再次进行抓包

成功抓到access_token的值,把access_token的值拿去解密,可以用webgoat自带的,也可以用JSON Web Tokens - jwt.io

可以看到在paylaod中存在admin的属性,同时被赋予了false

通过bp抓包,发现重置投票的接口为/WebGoat/JWT/votings,知道了接口的位置,直接在源代码中搜索

代码如下所示

代码语言:javascript
复制
@PostMapping("/JWT/votings")
@ResponseBody
public AttackResult resetVotes(@CookieValue(value = "access_token", required = false) String accessToken) {
    if (StringUtils.isEmpty(accessToken)) {
        return failed(this).feedback("jwt-invalid-token").build();
    } else {
        try {
            Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
            Claims claims = (Claims) jwt.getBody();
            boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
            if (!isAdmin) {
                return failed(this).feedback("jwt-only-admin").build();
            } else {
                votes.values().forEach(vote -> vote.reset());
                return success(this).build();
            }
        } catch (JwtException e) {
            return failed(this).feedback("jwt-invalid-token").output(e.toString()).build();
        }
    }
}

稍加观察就可以发现,boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));这句话获取了claims中的admin参数的值,如果为admin则可以重置投票,否则显示jwt-only-admin

Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);通过这行代码我们知道,加密的密钥为JWT_PASSWORD,追踪JWT_PASSWORD,查看值

代码语言:javascript
复制
public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory"); 

可以看到了是吧victory进行base64的加密,所以理论上我们想要进行重置投票,只需要将admin的属性置为true即可

事先说明,这样是错的,原因处在了Secret key上,再回到重置投票的代码

步入方法,发现竟然又把传入的值解码了,所以最后的密钥其实就是JWT_PASSWORD本身

代码语言:javascript
复制
public JwtParser setSigningKey(String base64EncodedKeyBytes) {
    Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty.");
    this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes);
    return this;
}

最终答案

eyJhbGciOiJIUzUxMiJ9.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2NDE1MjMwMDUsDQogICJ1c2VyIiA6ICJUb20iDQp9._YNHC6_Ts4CPo3DWL94ErcAiVtgxgOCpuAGJCQQKc7fTZrlYxTfNMtUvfI5QHTAxpzMj3rGfN92oiDFLFEl-Pw

然而当我们不知道密钥或者猜解不出来怎么办呢?webgoat其实给了我们另一种解决方法

We can change the admin claim to false but then signature will become invalid. How do we end up with a valid signature? Looking at the RFC specification alg: none is a valid choice and gives an unsecured JWT. Let’s change our token:

简单来说,就是RFC标准允许我们把加密方式设置为none,那么不需要知道密钥,只需要把admin设置为true即可

但是后面最后一行有提示,说要在最后面加上一个.,否则会显示jwt非法

eyJhbGciOiJub25lIn0.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2NDE1MjMwMDUsDQogICJ1c2VyIiA6ICJUb20iDQp9.

通过上述分析可以发现,若研发人员在Web应用中对基于JWT的身份认证方案设计不当,攻击者可通过猜解、爆破等方式获取JWT Token,进而使身份认证方案防御失效。

防御

  • 始终执行算法验证

签名算法的验证固定在后端,不以 JWT 里的算法为标准。假设每次验证 JWT ,验证算法都靠读取 Header 里面的 alg 属性来判断的话,攻击者只要签发一个 "alg: none" 的 JWT ,就可以绕过验证了。

  • 选择合适的算法

具体场景选择合适的算法,例如分布式场景下,建议选择 RS256 。

  • HMAC 算法的密钥安全

除了需要保证密钥不被泄露之外,密钥的强度也应该重视,防止遭到字典攻击。

  • 避免敏感信息保存在 JWT 中

JWS 方式下的 JWT 的 Payload 信息是公开的,不能将敏感信息保存在这里,如有需要,请使用 JWE 。

  • JWT 的有效时间尽量足够短

JWT 过期时间建议设置足够短,过期后重新使用 refresh_token 刷新获取新的 token 。

参考文章:你可能没那么了解 JWT (baidu.com)

用户名爆破漏洞

密码重置界面

重置密码需要输入用户名和密保问题,当输入的用户名错误时则会显示非法用户,因此可以对用户名进行爆破

查看源码

代码语言:javascript
复制
if (validAnswer == null) {
    return failed(this).feedback("password-questions-unknown-user").feedbackArgs(username).build();
} else if (validAnswer.equals(securityQuestion)) {
    return success(this).build();
}

很好理解,就是验证发现当用户不存在即告诉用户用户名非法,想要修复也很简单,只告诉用户用户名或者密码错误即可

越权漏洞

登录页面,要求我们以tom的身份登录,然而我们不知道tom的密码,先进行密码找回

密码找回要求我们输入要找回的邮箱

发一个正常的密码重置包给我们的邮箱

发现url似乎存在身份码,猜测如果我们能够获得tom的身份码就可以代替tom进行水平越权的密码重置

我们输入tom的邮箱,然后抓包

发现包被发往了了红框内的地址(也就是8080端口),通过修改发送的地址为我们自己的服务器(也就是webwolf的9090端口)

然而我们不是tom,所以是收不到此邮件的,但是我们可以收到此http包

bfda7dcb-bdec-41a6-8913-40ecee20e720

将此身份码替换url中的,然后输入新的密码tom

使用密码tom登录,成功

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • WebGoat
相关产品与服务
多因子身份认证
多因子身份认证(Multi-factor Authentication Service,MFAS)的目的是建立一个多层次的防御体系,通过结合两种或三种认证因子(基于记忆的/基于持有物的/基于生物特征的认证因子)验证访问者的身份,使系统或资源更加安全。攻击者即使破解单一因子(如口令、人脸),应用的安全依然可以得到保障。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档