查看原文
其他

实战之巧用验证码校验接口

听风安全 2023-11-28

The following article is from goddemon的小屋 Author goddemon

免责声明由于传播、利用本公众号听风安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号听风安全及作者不为承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!

公众号现在只对常读和星标的公众号才展示大图推送,

建议大家把听风安全设为星标,否则可能就看不到啦!

----------------------------------------------------------------------

Part1 前言:

被朋友吐槽了一手排版,说排版有点丑,就换了个排版。 

(虽然还是丑,手动狗头)

本篇的话,可能稍微有点多,主要是分析整个过程中的思路,请耐心看完。

Part2 案例:

某证书站,无意遇到的,就看了下,获取到的验证码是4位数,可以进行爆破验证码但是由于存在图片验证码模块,即每验证一次验证码也会验证一次图形验证码有师傅说可能会说基于ocr图片验证码模块结合打,基于可以倒是可以,但是从攻击成本来说基本上很高

(而且后面经过验证这种方法也不行,具体看下文,下文会提出来)

当时走到这里就没有接着尝试这个接口的爆破洞,又进行尝试多手机号的思路(因为主要是想获取到验证码进行任意密码重置,就没有去测短信轰炸等等这些漏洞)

即同时指定两个手机号验证码,尝试多种手法均无果

老实回头看看该系统中存在验证码的功能点 

存在一个用户注册功能点,且验证码处可以进行枚举,且不存在图片验证码模块。

即经过测试确实是可以进行验证码爆破,且爆破成功,获取到任意用户注册漏洞 

(但是这里的核心最舒服的不是这个,而是这里发觉到的一个特性,就是注册这里爆破出来的验证码,只要不注册激活成功且验证码在一定时间内,该验证码就不会失效,即可以理解为该验证码可进行复用)最开始想法是利用注册处的验证码爆破出后,复用该验证码去重置密码

遇到了如下的特性,该手机已注册,建议取回密码,即后端写法先判断手机号是否注册,未进行注册即进行发送验证码。利用+8614XXXXXXXXX成功绕过校验,获取到验证码,本来以为到这里就可以完美重置密码了

但是发觉很有意思的,即注册处的验证码与重置处的验证码无法复用,即开发后台写了参数判断,判断是哪个地方的验证码,导致无法重置。

即单独使用注册处的模块去进行重置密码思路,走到这里又走不通。

整理下以具备的条件 

①注册处可以进行爆破出验证码,且该验证码可复用,但只能复用验证码到注册处,重置密码处有参数校验。


②密码重置处可以发送验证,但是无法爆破。走到这里就可以大概整理出一个思路,密码重置处发送验证码,注册处去爆破出验证码,然后将爆破出的验证码拿到密码重置处去进行复用。

这里就需要去确定二个逻辑了 

第一,就是注册和重置密码的验证码到底是如何做的校验注册处与重置密码的参数的,如果只是密码重置处根据后端参数做的校验,而注册处没有校验,即可能有机会可以绕过,如果是发送短信接口不同或者说是注册处也有校验,则必然不可能。

第二,重置密码处的验证码是否会出现基于时间限制的写法,如果这两个的条件均可以满足,该漏洞即可满足。

第一个逻辑的验证方法很简单,重置密码处发送一个验证码,然后利用去利用注册码的验证接口进行验证验证码如果可以返回success即说明没问题。

如图,很幸运成功了,说明接口相同,且只有重置密码处具备参数校验。第二个逻辑直接登录爆破fuzz尝试,看看多久的时间可以复用,这里最好的校验模式,就是直接爆破这个注册点,爆破出后复用到重置处,尝试是否可以成功即可知道。

很幸运猜想和自己想的差不多,开发是基于时间去做的,爆破成功,快速将验证码填写到重置密码处 (这里的时间经过测试是一分钟,一分钟之内的即可,超过一分钟的即为失败)重置成功最后也是通过了 ,不过只给了一个低 理由是因为该验证码可爆破问题被提交过很多次了,虽然提交的都只是被利用为任意用户注册之类的,但是考虑到核心还是验证码爆破,所以给的低,不过无所谓啦,人嘛开心最好。

Part3 分析:

验证码验证功能开发:

基于开发的角度探究验证码的验证方式

最简单demo:

验证码的验证逻辑:最简单的验证码逻辑,也是最正确的,生成一个验证码,然后进行验证,当验证通过后,去除该验证码。

用户输入手机号和收到的验证码。
后端将用户输入的手机号和验证码与存储的验证码进行比对。
如果匹配成功,则验证通过;如果不匹配,则验证失败。

具体代码:(这里的demo将验证码设置为固定的123456)

@RestController
public class VerificationController {
    private Map<String, String> verificationCodes = new HashMap<>();

    @PostMapping("/verify")
    public String verifyCode(@RequestBody VerificationRequest verificationRequest) {
        String phoneNumber = verificationRequest.getPhoneNumber();
        String code = verificationRequest.getCode();

        if (isValidVerificationCode(phoneNumber, code)) {
            // 验证成功后,将验证码从缓存中删除
            removeVerificationCode(phoneNumber);
            return "Verification successful";
        } else {
            return "Invalid verification code";
        }
    }

    private boolean isValidVerificationCode(String phoneNumber, String code) {
        String storedCode = getStoredVerificationCode(phoneNumber);
        return storedCode != null && storedCode.equals(code);
    }

    private String getStoredVerificationCode(String phoneNumber) {
        return verificationCodes.get(phoneNumber);
    }

    private void removeVerificationCode(String phoneNumber) {
        verificationCodes.remove(phoneNumber);
    }

    @PostMapping("/generate-code")
    public String generateCode(@RequestBody PhoneNumberRequest phoneNumberRequest) {
        String phoneNumber = phoneNumberRequest.getPhoneNumber();
        String code = generateVerificationCode();

        // 将验证码与手机号关联并存储到缓存中
        storeVerificationCode(phoneNumber, code);

        return "Verification code generated successfully";
    }

    private String generateVerificationCode() {
        // 生成随机的验证码逻辑...
        // 返回生成的验证码字符串
        return "123456"// 假设生成的验证码为固定的 "123456"
    }

    private void storeVerificationCode(String phoneNumber, String code) {
        verificationCodes.put(phoneNumber, code);
    }
}

基于时间的验证码验证

为什么会有这种写法的原因是因为很多时候,开发为了修复短信轰炸之类的漏洞 

一般会基于三种修复方案 

方案1,记录次数去限制发送:一种是代码中或者验证码接口进行去做限制,记录相关手机号到数据库取判断一日是否大于10次。

方案2,基于时间去限制发送:就如下面这种代码,这种思路相对来说比较鸡肋。

方案3,增加验证码机制,如图形验证码以及人机验证机制。(这种这里不在阐述,上述的重置密码即采用该接口模块进行修复)

@RestController
public class VerificationController {
    private Map<String, VerificationCode> verificationCodes = new HashMap<>();

    @PostMapping("/verify")
    public String verifyCode(@RequestBody VerificationRequest verificationRequest) {
        String phoneNumber = verificationRequest.getPhoneNumber();
        String code = verificationRequest.getCode();

        if (isValidVerificationCode(phoneNumber, code)) {
            // 验证成功后,将验证码从缓存中删除
            removeVerificationCode(phoneNumber);
            return "Verification successful";
        } else {
            return "Invalid verification code";
        }
    }

    private boolean isValidVerificationCode(String phoneNumber, String code) {
        VerificationCode storedCode = getStoredVerificationCode(phoneNumber);
        if (storedCode != null && storedCode.getCode().equals(code)) {
            long currentTime = System.currentTimeMillis() / 1000// 当前时间(秒)
            return currentTime - storedCode.getCreationTime() <= 60;
        }
        return false;
    }

    private VerificationCode getStoredVerificationCode(String phoneNumber) {
        return verificationCodes.get(phoneNumber);
    }

    private void removeVerificationCode(String phoneNumber) {
        verificationCodes.remove(phoneNumber);
    }

    @PostMapping("/generate-code")
    public String generateCode(@RequestBody PhoneNumberRequest phoneNumberRequest) {
        String phoneNumber = phoneNumberRequest.getPhoneNumber();
        String code = generateVerificationCode();

        // 将验证码与手机号关联并存储到缓存中
        storeVerificationCode(phoneNumber, code);

        return "Verification code generated successfully";
    }

    private String generateVerificationCode() {
        // 生成随机的验证码逻辑...
        // 返回生成的验证码字符串
        return "123456"// 假设生成的验证码为固定的 "123456"
    }

    private void storeVerificationCode(String phoneNumber, String code) {
        long currentTime = System.currentTimeMillis() / 1000// 当前时间(秒)
        VerificationCode verificationCode = new VerificationCode(code, currentTime);
        verificationCodes.put(phoneNumber, verificationCode);
    }

    private static class VerificationCode {
        private String code;
        private long creationTime;

        public VerificationCode(String code, long creationTime) {
            this.code = code;
            this.creationTime = creationTime;
        }

        public String getCode() {
            return code;
        }

        public long getCreationTime() {
            return creationTime;
        }
    }
}

Part4 分析总结:

根据上面的分析可知,对于验证码的逻辑 

如果是第一种手法,无法进行相关绕过,但是这种可能会导致短信轰炸漏洞,可以尝试测试短信轰炸漏洞。

对于第二种 如果采用时间这种修复方案的话,且存在验证码爆破的话,攻击思路,我们可以基于结合其他验证接口,尝试是否可能存在验证码复用绕过防御,该点触发尤其是对于注册功能处,尤其多。


不可错过的往期推荐哦


从0到1深入浅出学习SQL注入

渗透实战|两个0day漏洞挖掘案例

记一次细得不行的账户权限提升

三个bypass案例分享

记一次Oracle注入漏洞提权的艰难过程

U盘植马之基于arduino的badusb实现及思考

APT是如何杜绝软件包被篡改的

利用sqlserver agent job实现权限维持

SRC挖掘葵花宝典

点击下方名片,关注我们
觉得内容不错,就点下“”和“在看
如果不想错过新的内容推送可以设为星标


继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存