经验分享丨JAVA审计中常见的加密错误,你知道几个?

在代码审计中,经常会发现开发人员由于密码学知识的欠缺,造成安全函数误用。本文是i春秋论坛作者「精通linux开关机」表哥发布的文章,汇总了JAVA审计中常见的加密错误,希望对各位学习有所帮助。

经验分享丨JAVA审计中常见的加密错误,你知道几个?

 

如果在业务中安全函数误用,通常是因为逻辑设计得不够清晰,会造成相关安全措施失效,进而导致业务存在安全隐患,工作中也遇到过开发人员将RSA公钥和私钥弄混而影响工作进度的情况。

作为安全人员,如果仅仅告诉开发人员哪些事情不能做,开发人员常会反驳说安全让可行的事情变得不可行,安全并不是单纯地增加开发人员的工作负担。

 

业务逻辑

本文主题围绕JAVA审计展开,关于逻辑漏洞利用的手法不展开,如果感兴趣可以百度了解DedeCMS-V5.7密码重置漏洞,语言不同但手法相似。

 

代码逻辑

 

硬编码加密密钥

相信广大开发人员都干过硬编码密钥的事情,硬编码密码常出现在代码中或是程序依赖的外部资源(如:配置文件)中。这时一旦被反编译,密钥就存在泄漏的风险。

典型错误例子:

DriverManager.getConnection(url, "scott", "tiger");

解决办法:

如果只是几条密钥,可以将其保存在配置文件中,建议保存在数据库中,同时连接数据库的密码需要加密,加密数据库密码的密钥硬编码到项目文件中(如:Spring boot的application.properties),这样做的原理是不需要确保多个密钥(CEK)的机密性,而只需要确保一个密钥(KEK)的机密性就可以了。这和认证机构的层级化非常相似。

application.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://172.16.99.99:3306/dbkzj?characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=ENC(UGFTmet+PxuhyvlOQbPJGRCVy7mTBrPw)

 

初始化向量为固定值

这个错误Apple公司也犯过,AppleAPI告诉开发者初始向量是可选的(这显然有安全隐患),另外如果它没有提供初始向量,则使用全部是0的向量来替代。

初始向量通常缩写成IV。这个问题常常出现在密码分组链接模式(CBC模式)加密中。

CBC模式采用硬编码初始向量的方式,一般初始向量的所有元素是由0填充。

每一次加密都使用相同的初始向量而非使用随机IV,则结果密码可预测性会高得多,容易受到字典式攻击。

典型错误例子:

byte[] DESIV = {0x12,0x34,0x56,0x78,(byte)0x90,(byte)0xAB,(byte)0xCD,(byte)0xEF};
IvParameterSpec iv1 = new IvParameterSpec(DESIV);// 设置向量

解决办法:

传入随机数种子,随机产生初始化向量。

SecureRandom secureRandom = new SecureRandom();
byte[] iv = secureRandom.generateSeed(16);
IvParameterSpec iv1 = new IvParameterSpec(iv);
byte[] DESIV1 = iv1.getIV();//获取初始化向量1
byte[] DESIV2 = iv2.getIV();//获取初始化向量2

 

电码本工作模式

当你使用分组密码加密,例如高级加密标准(AES),你应该选择一个分组密码的工作模式。你能选择的最糟糕的工作模式是EBC模式(它的实现最简单,运行速度最快),它令人印象深刻的是,重复的明文将会产生重复的密文。

典型错误例子:

SecretKeySpec key = new SecretKeySpec(getKey(decryptKey), "DES");
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key);//DES对称ECB模式加密
byte decryptedData[] = cipher.doFinal(ConvertUtil.hexStringToByte(decryptString));
result = new String(decryptedData);

解决办法:

对明文格式有特殊要求的环境,可选用CFB模式,无要求用CBC模式。

使用CBC模式时必须传入IV参数,用前面的例子,使用IvParameterSpec创建iv对象。

...
SecretKeySpec key = new SecretKeySpec(getKey(decryptKey), "DES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = secureRandom.generateSeed(16);
IvParameterSpec iv1 = new IvParameterSpec(iv);
byte[] DESIV1 = iv1.getIV();//获取初始化向量1
cipher.init(Cipher.DECRYPT_MODE, key,DESIV1);//DES对称CBC模式加密
byte decryptedData[] = cipher.doFinal(ConvertUtil.hexStringToByte(decryptString));
result = new String(decryptedData);
...

 

不安全的加密算法

不得不提MD5,MD5在10年前已被破解,在IDEA中使用MD5会产生警告,但是部分安全意识淡薄的开发还会认为MD5是安全的。

典型错误例子:

MessageDigest md = MessageDigest.getInstance("MD5"); //MD5
md.update(passwordToHash.getBytes());
byte[] bytes = md.digest();
StringBuilder sb = new StringBuilder();for(int i=0; i< bytes.length ;i++){
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}
generatedPassword = sb.toString();

解决办法:

不要使用MD5、SHA1等过时的算法,用SHA-256和SHA-512等强算法。

MessageDigest md = MessageDigest.getInstance("SHA-256");//SHA-256
md.update(passwordToHash.getBytes());
byte[] bytes = md.digest();
StringBuilder sb = new StringBuilder();for(int i=0; i< bytes.length ;i++){
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}

 

密钥长度太短

密钥长度太短是特指非对称秘钥。开发人员常常会有疑问,密码和加密秘钥之间有什么区别?

密码长度不一,密钥长度固定,并且通常密钥复杂度更高包含不可打印字符。密钥的熵值比前者高得多。

回到密钥长度太短上,通常开发人员在选择对称加密的密钥长度上基本会选128位以上的。错误发生在选择非对称加密的密钥长度上,在RSA、DAS、DH和相似的算法中,像RSA这种大数分解的密钥长度即使到达512位也是不安全的,1024或2048位才是主流。

椭圆曲线ECC可以用短密钥,有很好的发展空间,但是目前开发人员仍不愿意尝试用椭圆曲线的算法。

典型错误例子:

KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");  //基于RSA算法生成对象
keyPairGen.initialize(256,new SecureRandom());  //密钥大小为96-4096位
KeyPair keyPair = keyPairGen.generateKeyPair();  // 生成一个密钥对,保存在keyPair中  
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();   // 得到私钥  
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();  // 得到公钥  
String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));  
String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
keyMap.put(0,publicKeyString);  //0表示公钥
keyMap.put(1,privateKeyString);  //1表示私钥

解决办法:

将密钥大小调为2048位,或用更先进的椭圆曲线ECC加密算法,其密钥长度只有256位长度,只有RSA加密算法同等加密强度的密钥长度(3072位),运算速度更快,更安全。

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ECCEnum.ALGORITHM.value(),
                ECCEnum.PROVIDER.value());
keyPairGenerator.initialize(256, new SecureRandom());// 这里是椭圆曲线只有256位长度。
KeyPair kp = keyPairGenerator.generateKeyPair();
ECPublicKey publicKey = (ECPublicKey) kp.getPublic();
ECPrivateKey privateKey = (ECPrivateKey) kp.getPrivate();
Map<String,String> map = new HashMap<>();
map.put(ECCEnum.PRIVATE_KEY.value(), BASE64Encoder.encodeBuffer(privateKey.getEncoded()));
map.put(ECCEnum.PUBLIC_KEY.value(), BASE64Encoder.encodeBuffer(publicKey.getEncoded()));

 

不安全的随机数

如果开发安全意识淡薄,使用java.util.Random类生成一个Web应用程序的会话标记。

我已获得会话标记,那么可以预测下一个用户和前一个用户的会话标记进而劫持他们的会话。

典型错误例子:

String token = (new Random().nextInt(99999)) + "";  
MessageDigest md = MessageDigest.getInstance("md5");  
byte md5[] =  md.digest(token.getBytes());  
BASE64Encoder encoder = new BASE64Encoder();  
result = encoder.encode(md5);  

解决办法:

使用SecureRandom类产生会话标记,种子很重要,不要设置特定值作为种子。

StringBuilder buf = new StringBuilder();
SecureRandom sr = new SecureRandom();
for( int i=0; i<6; i++ ) {// log2(52^6)=34.20... so, this is about 32bit strong.
        boolean upper = sr.nextBoolean();
        char ch = (char)(sr.nextInt(26) + 'a');
        if(upper)   ch=Character.toUpperCase(ch);
        buf.append(ch);
}
result = buf.toString();

 

自定义加密算法

密码学中一个无漏洞的密钥系统的安全性只取决于其所采取的密钥强度,不依赖于其实现的细节,事实上加密流程是公开的。在一些产品的激活流程中,我常看到自定义加密算法的身影。

这些自定义加密算法基本都有一个特点,过多地需要保密内部的实现细节,一旦流程和处理细节被曝光,这种加密方式将可被攻击者通过流程总结出公式计算出来。

典型错误例子:

String ACTIVECODE_RAW = "2020-04-21" + "WWW.HACKER.COM";  
MessageDigest md = MessageDigest.getInstance("md5");  
byte md5[] =  md.digest(ACTIVECODE_RAW.getBytes());  
BASE64Encoder encoder = new BASE64Encoder();  
result_ACTIVECODE = encoder.encode(md5);  

解决办法:

不要靠内部隐蔽细节来实现安全,要将安全转移到密钥的安全强度上,建议使用非对称密码体系完成产品激活,例如Navicat的激活(使用RSA)。

byte[] keyBytes = Base64Utils.decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64Utils.decode(sign));

 

开发人员绕过二次验证

这种行为让安全人员哭笑不得,开发人员为了省事,密码验证居然采用了复制的方式。

典型错误例子:

String password=request.getParameter("password");
DefaultUser user = (DefaultUser) ESAPI.authenticator().createUser(username, password, password);

解决办法:

不要省事,老老实实接收参数完成赋值。

String password=request.getParameter("password");
String confirmpassword=request.getParameter("confirmpassword");
DefaultUser user = (DefaultUser) ESAPI.authenticator().createUser(username, password, confirmpassword);

 

过于广泛的信任证书

即使有CA签名的证书也不应该信任,证书颁发机构(CA)为每个公开密钥发放一个数字证书,证书对于通用网络通信工具是必需的,而盗用证书颁发机构的数量正在不断增加,这导致即使由CA签名的证书也可能是恶意的。

当程序默认接受由CA颁发的证书而屏蔽了安全校验逻辑,拥有盗用证书的攻击者可能会拦截这些CA的SSL/TLS信息流进行中间人攻击。

典型错误例子:

URL url = new URL("https://www.ABC.com");
URLConnection urlConnection = url.openConnection();
InputStream inputStream = urlConnection.getInputStream();

解决办法:

不要直接使用默认的URLConnection 建立SSL/TLS连接,建议使用HttpsURLConnection 进行替代,并对证书进行判断和处理。

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, new SecureRandom());
URL url = new URL("https://www.ABC.com");
HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
InputStream inputStream = httpsURLConnection.getInputStream();

 

PBE(基于密码的加密)用于使用密码加密数据

PBE加密跳出了DES和AES的加密模式,综合对称加密、信息摘要算法的优势,形成了一个对称加密的特例。

但是使用其他一定要注意迭代次数,如果PBE迭代次数过少(少于1千次),会增加被攻击的可能性。

典型错误例子:

final int SALT_COUNT = 50;
Key k = stringToKey(key);
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, SALT_COUNT);  
Cipher cipher = Cipher.getInstance(KEY_PBE);  
cipher.init(Cipher.ENCRYPT_MODE, k, parameterSpec);

解决办法:

PBE使用PBEParameterSpec时,应使用非常量salt,至少迭代1000次。使用 PBEKeySpec时,应使用非常量salt,至少迭代10000次。

final int SALT_COUNT = 100000;
Key k = stringToKey(key);
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, SALT_COUNT);  
Cipher cipher = Cipher.getInstance(KEY_PBE);  
cipher.init(Cipher.ENCRYPT_MODE, k, parameterSpec);

 

总结

代码审计可以拉近开发与安全人员的关系,安全人员提供给开发人员更好用的加密API,同时教会开发人员正确使用,这是安全工程师必须具备的工作技能。

细化后主要有以下三点:

1、保证这个API能够加密功能更简单;

2、保证这些API在默认的情况下是安全的;

3、文档应该非常清晰的记录可能会发生的问题。

微软的C#语言在这方面做得比JAVA好很多,希望广大JAVA开发人员工作中多注意安全编码技能的提高。

posted @ 2020-04-24 11:01  i春秋  阅读(1005)  评论(0编辑  收藏  举报