vlambda博客
学习文章列表

CSRF漏洞之Token防御原理解析

作者:无名安全团队成员-火之高兴



    最近刚好学到了CSRF漏洞,对于CSRF漏洞的防御策略之一就是在页面中引入token,其流程为:

  1. 将CSRF Token输出到页面中

    • 用户打开页面的时候,服务器需要给这个用户生成一个Token,该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合.

    • 将token放在session中

  2. 页面提交的请求携带这个Token

  3. 服务器验证Token是否正确

因此对于token的生成与认证产生了兴趣,在这里以Json web token (JWT)为例进行分析。

JWT的结构

JWt主要由三部分组成,他们之间用圆点(.)连接。这三部分分别是

  1. Header

  2. Payload

  3. Signature

Header

jwt的头部主要有两部分信息:

  • token的类型

  • 加密算法的名称 例如 HMAC、SHA256、SRA

完整的头部信息如下所示:

ps:SH256是HMAC的一种

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

将头部进行base64加密构成了token的第一部分

payload

payload是存放有效信息的地方,主要包含三个部分:

  1. 标准中注册的声明

    • iss:jwt签发者

    • sub:jwt所面向的用户

    • aud: 接收jwt的一方

    • exp: jwt的过期时间,这个过期时间必须要大于签发时间

    • nbf: 定义在什么时间之前,该jwt都是不可用的.

    • iat: jwt的签发时间

    • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  2. 公共的声明

    • 可随意定义一般添加用户的相关信息与其他必要信息

  3. 私有的声明

    • 生产者与消费者共有的声明

一般如下所示:

{
 "sub": "1234567890",
 "name": "Happy Fire",
 "iat": 1631980800     --> unix时间
}

对payload进行Base64编码就得到JWT的第二部分,由于payload、header都是使用base64进行加密的,因此可以很轻易的进行解密,尽量不要在这两个地方放入敏感信息。

Signature

Signature是JWT的最后一个部分,Signature由三部分组成

  • header(base64后)

  • payload(base64后)

  • secret

将base64加密后的header和base64加密后的payload使用 "." 连接字符串,然后使用header中声明的加密方式进行加salt(secret)加密的方式进行加密构成了JWT的第三部分,将这三部分组合起来就形成了最终的JWT。

我们通过官方的一张图可以直观的反映出JWT的加密:

CSRF漏洞之Token防御原理解析

代码分析

老规矩先引入依赖:

<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

设置密钥和过期时间:

 //密钥
public static final String SECRET = "sdjhakdhajdklsl;o653632";
//过期时间:秒
public static final int EXPIRE = 5;

生成token:

public static String createToken(String userId, String userName) throws Exception {
       Calendar nowTime = Calendar.getInstance();
       nowTime.add(Calendar.SECOND, EXPIRE);        
       Date expireDate = nowTime.getTime();      ---->  过期时间

       Map<String, Object> map = new HashMap<>();     ---> header头 设置token类型以及加密算法
       map.put("alg", "HS256");
       map.put("typ", "JWT");

       String token = JWT.create()
              .withHeader(map)//头
              .withClaim("userId", userId)
              .withClaim("userName", userName)
              .withSubject("测试")//
              .withIssuedAt(new Date())//签名时间
              .withExpiresAt(expireDate)//过期时间
              .sign(Algorithm.HMAC256(SECRET));//签名
       return token;
  }

我们来详细的分析一下创建的这段代码:

String token = JWT.create()
              .withHeader(map)//头
              .withClaim("userId", userId)
              .withClaim("userName", userName)
              .withSubject("测试")//
              .withIssuedAt(new Date())//签名时间
              .withExpiresAt(expireDate)//过期时间
              .sign(Algorithm.HMAC256(SECRET));//签名

首先进入create方法:

public static JWTCreator.Builder create() {
       return JWTCreator.init();
  }

我们可以看到,主要是借助JWTCreator的builder方法来对JWT进行构建,最初调用init()初始化:

static JWTCreator.Builder init() {
       return new Builder();
  }

Init()方法会新建一个builder对象,该对象首先构建两个对象来存放header和payload对象:

private final Map<String, Object> payloadClaims;
       private final Map<String, Object> headerClaims;

       Builder() {
           this.payloadClaims = new HashMap<>();
           this.headerClaims = new HashMap<>();
      }

接下来调用Builder对象的withHeader方法来生成header:

public Builder withHeader(Map<String, Object> headerClaims) {
           if (headerClaims == null) {
               return this;
          }

           for (Map.Entry<String, Object> entry : headerClaims.entrySet()) {
               if (entry.getValue() == null) {
                   this.headerClaims.remove(entry.getKey());
              } else {
                   this.headerClaims.put(entry.getKey(), entry.getValue());
              }
          }

           return this;
      }

给headerClaims传入值并返回该对象:


接下来使用调用withClaim方法放入userID与userName

public Builder withClaim(String name, String value) throws IllegalArgumentException {
           assertNonNull(name);
           addClaim(name, value);
           return this;
      }

首先会对name进行非空判断,如果传入的不是空值那么调用addClaim方法:

private void addClaim(String name, Object value) {
           if (value == null) {
               payloadClaims.remove(name);
               return;
          }
           payloadClaims.put(name, value);
      }
  }

接下来对值进行非空判断,如果值为空值,则将该键从payload中移除并返回,如果值也不为空值,那么就讲该键值对放入payloadClaims中

同理为Subject、签名时间、过期时间赋值,内部都掉用addClaim方法:

public Builder withIssuedAt(Date issuedAt) {
           addClaim(PublicClaims.ISSUED_AT, issuedAt);
           return this;
      }

最后开始生成签名并生成token

.sign(Algorithm.HMAC256(SECRET));//签名

先掉用HMAC256加密算法对SECRET进行加密,然后掉用sign方法生成

/**
Creates a new JWT and signs is with the given algorithm
Params:
algorithm – used to sign the JWT
Returns:
a new JWT token
Throws:
IllegalArgumentException – if the provided algorithm is null.
JWTCreationException – if the claims could not be converted to a valid JSON or there was a problem with the signing key
*/
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
           if (algorithm == null) {
               throw new IllegalArgumentException("The Algorithm cannot be null.");
          }
           headerClaims.put(PublicClaims.ALGORITHM, algorithm.getName());
           if (!headerClaims.containsKey(PublicClaims.TYPE)) {
               headerClaims.put(PublicClaims.TYPE, "JWT");
          }
           String signingKeyId = algorithm.getSigningKeyId();
           if (signingKeyId != null) {
               withKeyId(signingKeyId);
          }
           return new JWTCreator(algorithm, headerClaims, payloadClaims).sign();
      }

首先判断在header中是否声明了token的type,如果已声明则跳过,如果未声明则将typ与JWT放入header中,其中publicClaims接口声明了所有JWT支持的header和payload字段:

public interface PublicClaims {

   //Header
   String ALGORITHM = "alg";
   String CONTENT_TYPE = "cty";
   String TYPE = "typ";
   String KEY_ID = "kid";

   //Payload
   String ISSUER = "iss";
   String SUBJECT = "sub";
   String EXPIRES_AT = "exp";
   String NOT_BEFORE = "nbf";
   String ISSUED_AT = "iat";
   String JWT_ID = "jti";
   String AUDIENCE = "aud";

}
   private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String,        Object> payloadClaims) throws JWTCreationException {
       this.algorithm = algorithm;
       try {
           ObjectMapper mapper = new ObjectMapper();
           SimpleModule module = new SimpleModule();
           module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
           mapper.registerModule(module);
           mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
           headerJson = mapper.writeValueAsString(headerClaims);
           payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
      } catch (JsonProcessingException e) {
           throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", e);
      }
  }

在所有验证都完成后,将headerClaims、payloadClaims由HashMap对象转成JSON对象,最终掉用init方法生成token,

  private String sign() throws SignatureGenerationException {
       String header = Base64.encodeBase64URLSafeString(headerJson.getBytes(StandardCharsets.UTF_8));
       String payload = Base64.encodeBase64URLSafeString(payloadJson.getBytes(StandardCharsets.UTF_8));

       byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
       String signature = Base64.encodeBase64URLSafeString((signatureBytes));

       return String.format("%s.%s.%s", header, payload, signature);
  }

先将header与payload使用base64加密算法进行加密,然后对加密后的header与payload使用HS256算法进行加盐加密,加密后的值在进行一次base64加密从而生成signature,最终将加密后的header、payload、signature使用"."连接生成token

token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLmtYvor5UiLCJ1c2VyTmFtZSI6IndhbmdibyIsImV4cCI6MTYzMjEwNjU2MiwidXNlcklkIjoiMTIzNDUiLCJpYXQiOjE2MzIxMDY1NTd9.ebUt4h9OU-dxhdkCfoa_KDXfzVXADmF-q4kw3rVF79I

token验证

对token进行验证时,只需要重新生成一个signature,并对比signature与token中的是否一致即可。

/**
* The JWTVerifier class holds the verify method to assert that a given Token has not only a     * proper JWT format, but also it's signature matches.
* <p>
* This class is thread-safe.
*/
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();

生成一个JWTVerifier对象对token进行验证,使用JWT.require方法创建该对象,该方法带有加密的算法以及密钥


   /**
    * Returns a {@link JWTVerifier} builder with the algorithm to be used to validate token signature.
    *
    * @param algorithm that will be used to verify the token's signature.
    * @return {@link JWTVerifier} builder
    * @throws IllegalArgumentException if the provided algorithm is null.
    */
   public static Verification require(Algorithm algorithm) {
       return JWTVerifier.init(algorithm);
  }

使用verifier对象的verify方法对token进行验证

 jwt = verifier.verify(token);

verfier方法先对token进行解析

DecodedJWT jwt = new JWTDecoder(parser, token);


JWTDecoder(JWTParser converter, String jwt) throws JWTDecodeException {
       parts = TokenUtils.splitToken(jwt);
       String headerJson;
       String payloadJson;
       try {
           headerJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[0]));
           payloadJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[1]));
      } catch (NullPointerException e) {
           throw new JWTDecodeException("The UTF-8 Charset isn't initialized.", e);
      } catch (IllegalArgumentException e){
           throw new JWTDecodeException("The input is not a valid base 64 encoded string.", e);
      }
       header = converter.parseHeader(headerJson);
       payload = converter.parsePayload(payloadJson);
  }

解析token得到header和payload的JSON对象并生成header和payload从而生成一个新的DecodedJWT对象(此时还未解析signature),接下来使用DecodedJWT对象的verify方法开始验证

 public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException {
       verifyAlgorithm(jwt, algorithm);     ---> 验证加密算法是否一致
       algorithm.verify(jwt);               --->验证signature
       verifyClaims(jwt, claims);
       return jwt;
  }
 public void verify(DecodedJWT jwt) throws SignatureVerificationException {
       byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature());--->解密signature

       try {
           boolean valid = crypto.verifySignatureFor(getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes);  -->生成新的signature并对比是否一致
           if (!valid) {
               throw new SignatureVerificationException(this);
          }
      } catch (IllegalStateException | InvalidKeyException | NoSuchAlgorithmException e) {
           throw new SignatureVerificationException(this, e);
      }
  }

    首先对token中的signature中的signature进行解密,接下来使用token中的header和payload与本地的secret生成新的signature,并判断与token中的signature是否相同,如果不同则token解析未通过,如果相同则解析其他claims如过期时间等,查看该token是否过期,如果都通过则验证成功。