JAVA代码审计05: WEBGOAT 认证缺陷(上)

0x00 前言

走个形式~

0x01 Authentication Bypasses

Password Reset

权限绕过,我们首先看到他给了一个样例,在进行安全校验对时候直接将对应请求包中的参数删掉就可以绕过了

image-20201109162548587

当然我们的案例不可能和上面一样直接删除就好了,我们直接从源码层面来进行分析

首先我们简单发一个请求,发现请求了后端的一个接口/auth-bypass/verify-account

image-20201109162946023

全局搜索接口名来到如下页面

可以看到在接受请求之前会进行一个判断,判断我们的参数名中是否包含secQuestion

这也就是为什么我们直接把参数删掉是不行的

image-20201109163146394

下面那个判断是为了防止我们看源码进行作弊的 2333

可以看到源码中含有我们的答案,如果我们输入的答案和源码中一样的话,like 就会为true(这里的like相当于标志符)

同时返回让我们不要作弊的消息233

image-20201109163335735

继续往下来,核心的代码处是这里,如果这里if可以通过那么我们就可以绕过我们的校验了

我们来看一下这个校验账户的功能

image-20201109163620539

直接进行一个跟进

发现有3个if,任意一个if符合条件了就会返回false

我们来看一下这三个if

第一个if 主要是判断了我们前端传过来的校验参数是否是两个

第二个和第三个if都是判断数值是否相等和我们源码中的答案

但是这里明显逻辑是有疏忽的,可以看到他获取的是 secQuestion0 和 secQuestion1 参数对应的数值进行一个比对

那么如果我们传入的参数含有 secQuestion 但是又不是 secQuestion0 或 secQuestion1 不就可以对这个校验进行一个绕过了吗

image-20201109163639090

我们来打断点测试一下,红框中是我们修改后的参数

image-20201109163925074

可以看到前三个if全都校验通过了,直接return true

image-20201109164014411

return true之后就进入到了我们校验成功的if语句中了,所以就可以绕过校验了

image-20201109164103401

image-20201109164122755

思考以及修复建议

首先上面的这个权限绕过我觉得可以运用在平时漏洞挖掘的过程中,有的时候的一些验证码校验很有可能删除就可以通过了,之前还无法理解后端是怎么写的,现在后端想想其实就是后端只获取了对应参数的数值类似verify ,所以我们在burp的过程中删去这个参数就可以绕过了

username=test&password=test&verify=1233 改成 username=test&password=test 就可以了

修复起来其实也比较简单,我们后端限定前端传递过来的参数一定要含有verify,如果没有就进行报错或者显示不合法即可

当然利用签名也是一个很好的方法,签名能有效的防止传递过来的参数被修改,但是签名也有被破解的风险,所以最关键的还是后端要对传递过来的参数进行一个校验

0x02 JWT Tokens (未完待续)

Jwt Token 简介

jwt的全称是JSON Web Token,主要是由三个部分组成 分别是header(头)、payload(载体)、signature(签名),现在主要是用在跨域请求中,因为jwt中会携带我们的身份信息,所以我们在跨域请求的过程中会直接携带上

header 中定义我们签名校验的算法

payload 中是我们的一些信息

signature 签名主要是防止在传输过程中信息载体被篡改

通常的jwt sign 生成是如下

1、EncodeString = Base64(header).Base64(payload)
2、最终token = HS256(EncodeString,"秘钥")

https://jwt.io/ 这个网站是一个非常好用的jwt破解的工具

image-20201110140512417

JWT signing

首先我们来看一下这个题目

题目的意思是要我们来重置这个投票,不过当前的用户是guest我们不能进行任何操作

image-20201110141239089

我们切换用户到tom,不过执行删除的过程中发现仍然无法进行删除,因为我们此时还不是admin用户

image-20201110141358973

我们burp看一下数据包,我们可以看到我们cookie中的access_token ,这个token 格式为 xxxx . xxxxx . xxxxx

发现是jwttoken 我们来到之前的那个网站 这样清楚一些

image-20201110141600475

来到我们的网站 ,发现 payload 信息载体中 我们的admin 对应的是 false ,所以我们的目标就是要把这里的false 改成true

但是由于第三段的sign是防止篡改的

image-20201110141718874

说实话我在做这个的时候还是比较懵逼的,因为我发现并没有给我secret,所以第一次做的时候 直接看了源码中的secret ,相当于作弊做出来了,但是后面发现好像有存在不校验sign的情况

简单的来说就是将flase 修改成 true ,然后将头部的alg改成了None,同时删除第三段即可

image-20201111205348692

结果发现就成功了

image-20201110142743754

之前研究了很久怎么都想不明白为什么直接删除可以(后面自己上手写了个jwt验证才明白了,由于这题和下面那题的原理差不多都是通过修改头部算法为None 然后删除签名,具体分析我们在下面一起分析,就不重复分析了

Refreshing a token

首先来看我们的题目,简单的讲一下

这里有logfile,能看到路由记录,路由记录里能看到tom 过期的token

image-20201110213508845

如下图

image-20201110214017303

首先我们来直接结算一下发现果然是不行的

image-20201110214100639

那我们如果带上过期了的token呢 我们再试试 ,看到我们的回显发现已经显示我们的token已经过期了

image-20201110214209526

后面尝试了之前的方法把 jwt token 中的header中的算法置为None发现也不行,后面去看了一下国外老哥是咋说的

https://pvxs.medium.com/webgoat-jwt-tokens-7-f4af043eb49b

国外老哥好像提到,看http history中会看到一个登录的请求包,奈何我本地也找不到,没办法直接看源码

我们直接全局搜索 /JWT/refresh/checkout

定位到如下的文件

image-20201110214529090

看了一下上下文,发现找到了之前外国老哥说的账号密码

这里的登录是 json的格式,后端会对用户名密码进行一个校验,如果校验通过的话就生成jwt token 并且进行一个返回

由于这里账号密码都是写死的所以也就没有sql注入的可能性了

image-20201110214615468

然后我们就根据源码进行请求的发送,这里注意修改content-type

image-20201110214754315

发现返回了正常的access_token

来到我们的jwt.io 进行解码,并且修改user对应的数值为tom

由于该网站算法如果改成none的话就没有数据了,所以我们讲数据复制到burp自行进行一个jwt加密

image-20201110214922836

由于我们把算法已经置为none了所以最后一段的校验位也就自然而然的没有了

image-20201110215015818

最后添加进去即可

image-20201110215129418

Jwt Token 空加密算法绕过 代码分析

那么我们现在来看一下这一题和上一题为什么会出现jwt token 算法头中的alg 置 None 就行了

其实本质的问题就是,后端jwt的某个函数允许空加密算法,由于之前怎么都弄不清楚所以用springboot 简单的写了一个jwt 校验的例子

项目的结构如下

image-20201111210538598

Controller 层

package com.example.Controller;

import com.example.Jwt.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@ResponseBody
public class ProjController {

    @Autowired
    JwtUtils jwtUtils;

    // 来获取我们token中的数值
    @RequestMapping("/token")
    public String getToken(HttpServletRequest request,HttpServletResponse response){
        String token = request.getHeader("Authorization");
        if (token == null){
            return "testfail";
        }
        return "testsuccess";
    }

    @GetMapping("/generate")
    public String generateToken(HttpServletRequest request,HttpServletResponse response){
        String token = jwtUtils.generateToken();
        return token;
    }

    @RequestMapping("/valid")
    public String validToken(HttpServletRequest request){
        String token = request.getHeader("Authorization");
        String res = jwtUtils.validToken(token);
        return res;
    }
}

JwtUtils

package com.example.Jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.TextCodec;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class JwtUtils {
    public static final String JWT_PASSWORD = TextCodec.BASE64.encode("victory");

    public String generateToken(){
        String jws = Jwts.builder()
                .setIssuer("me")
                .setSubject("Bob")
                .setAudience("you")
                .setId(UUID.randomUUID().toString()).signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD).compact(); //just an example id
        System.out.println(jws);
        return jws;
    }

    public String validToken(String jwttoken){
        try {
            Jwt claimsJws = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(jwttoken);
            //OK, we can trust this JWT
            return "success";
        } catch (JwtException e) {
            //don't trust the JWT!
            e.printStackTrace();
        }
        System.out.println("error");
        return "fail";
    }
}

由于是测试项目所以很简陋233,就只是一个校验jwt是否有效的测试代码

这是我们有效的jwt token

image-20201111210938119

我们删除一位试试看是否是success ,这里我删了最后一位A 结果就是 fail 了

image-20201111211135736

然后我们直接将我们的签名进行删除

image-20201111211036947

发现照样是成功的

这里主要问题就出在这里,校验过程中的函数 parse

如果开发不小心手抖用成了parse,那么这时候校验jwt的时候将会允许Header头部的算法为None,那么自然签名就没有了

(不过我这边直接删除签名也可以,应该是签名没有默认算法为None了吧)

image-20201111211203600

那么如何修复呢?

其实也很简单,我们只需要将函数修改成 parseClaimsJws 就可以了,这样的话后端就会对签名进行校验了

image-20201111211613652

那么我们重新来看一下webgoat中的源代码

不出所料就是用了parse导致的

前三个challenge 由于都用了 parse,所以在解决方法上都可以通过删除签名同时置Header头中的alg为None来解决

image-20201111211816968

image-20201111212151594

Final challenges

这题讲道理蛮有意思的,如果让我黑盒做然后不根据提示估计大概率做不出来orz,所以就直接结合代码审计来做了

我们首先点一下delete,发现发送了一个请求,并且当前我们jerry的token也是时间过期的状态

image-20201111233653465

第一步是一样的,直接代码中全局进行搜索,定位到了如下文件

首先来看第一个箭头的位置,很明显这里进行了一个sql注入的拼接,那么说明肯定存在sql注入

看一下代码的意思发现这次的secret是直接从数据库中进行读取 这个sql语句查询的就是我们jwt的secret

然后再来看第二个箭头的地方 之前说过 如果运用了 parseClaimsJws 这个方法的话我们就不能用 None算法来绕过我们的签名校验了

image-20201111233755464

所以这里我们要利用sql注入来使得jwt的secret是被我们控制的,这样的话整个jwt也可以被我们控制,我们就可以修改username了

首先来看一下之前那个token的结构,我们可以看到header头中的kid参数就是有sql注入的位置

image-20201111234213530

所以我们要利用这个sql注入,那么如何利用呢

首先我们来看一个简单的例子

在原有的查询语句基础上我们可以通过union来进行拼接,但是拼接的语句有讲究

就是我们 select 'xxxxx' from 表,这个表一定要存在,这样的话回显才会回显 xxxxx,不然的话是会报错的

image-20201111234258248

所以通过这个例子我们就可以利用sql语句来控制sql查询出来的结果为我们自己设定的结果了

我这里利用如下语句 ,YWFhYQ为aaaa的base64编码

asdf' union select 'YWFhYQ==' from information_schema.tables; --

image-20201111234446486

这里为什么要进行base64编码呢,我们来打断点看一下就知道了

首先我们看到我们的sql代码已经进来了,同时也符合结构正常闭合前面的了

接下来看我们的while语句

rs.next()也就是我们查询出来的信息,根据我们刚才的分析查询出来的结果应该是 我们的jwt secret

然后他会执行一次base64编码,所以我们之前才要对secret进行一次base64编码

image-20201111234713237

0x03 总结

上半部分的一个简单的总结,首先是密码重置这个题目,这题主要是后端没有做好对前端传递过来参数的校验,导致我们可以修改前端参数从而绕过的问题,在实际过程中也经常有这样的问题存在,比如前端验证码参数我们直接删除就可以进行一个绕过了

再一个就是jwt,jwt token 除了最后一个练习其实前面都是可以通过空算法来绕过的,主要还是后端函数问题允许空算法,最后一个比较有意思利用了sql注入,同时也提供了宝贵的思路,说不定实际情况中真的存在jwt头部有sql注入的问题,不过存在的可能性非常小啦

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像