在代码审计中,经常会发现开发人员由于密码学知识的欠缺,造成安全函数误用。本文是i春秋论坛作者「精通linux开关机」表哥发布的文章,汇总了JAVA审计中常见的加密错误,希望对各位学习有所帮助。
注:公众号旨在为大家提供更多的学习方法与技能技巧,文章仅供学习参考。
如果在业务中安全函数误用,通常是因为逻辑设计得不够清晰,会造成相关安全措施失效,进而导致业务存在安全隐患,工作中也遇到过开发人员将RSA公钥和私钥弄混而影响工作进度的情况。
作为安全人员,如果仅仅告诉开发人员哪些事情不能做,开发人员常会反驳说安全让可行的事情变得不可行java rsa加密算法 经验分享丨JAVA审计中常见的加密错误,你知道几个?,安全并不是单纯地增加开发人员的工作负担。
业务逻辑
本文主题围绕JAVA审计展开,关于逻辑漏洞利用的手法不展开,如果感兴趣可以百度了解-V5.7密码重置漏洞,语言不同但手法相似。
代码逻辑
硬编码加密密钥
相信广大开发人员都干过硬编码密钥的事情,硬编码密码常出现在代码中或是程序依赖的外部资源(如:配置文件)中。这时一旦被反编译,密钥就存在泄漏的风险。
典型错误例子:
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">DriverManager.getConnection(url, "scott", "tiger");
</pre>
解决办法:
如果只是几条密钥,可以将其保存在配置文件中,建议保存在数据库中,同时连接数据库的密码需要加密,加密数据库密码的密钥硬编码到项目文件中(如: boot的.),这样做的原理是不需要确保多个密钥(CEK)的机密性,而只需要确保一个密钥(KEK)的机密性就可以了。这和认证机构的层级化非常相似。
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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)
</pre>
初始化向量为固定值
这个错误也犯过java rsa加密算法,告诉开发者初始向量是可选的(这显然有安全隐患),另外如果它没有提供初始向量,则使用全部是0的向量来替代。
初始向量通常缩写成IV。这个问题常常出现在密码分组链接模式(CBC模式)加密中。
CBC模式采用硬编码初始向量的方式,一般初始向量的所有元素是由0填充。
每一次加密都使用相同的初始向量而非使用随机IV,则结果密码可预测性会高得多,容易受到字典式攻击。
典型错误例子:
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">byte[] DESIV = {0x12,0x34,0x56,0x78,(byte)0x90,(byte)0xAB,(byte)0xCD,(byte)0xEF}; IvParameterSpec iv1 = new IvParameterSpec(DESIV);// 设置向量
</pre>
解决办法:
传入随机数种子,随机产生初始化向量。
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">SecureRandom secureRandom = new SecureRandom(); byte[] iv = secureRandom.generateSeed(16); IvParameterSpec iv1 = new IvParameterSpec(iv); byte[] DESIV1 = iv1.getIV();//获取初始化向量1 byte[] DESIV2 = iv2.getIV();//获取初始化向量2
</pre>
电码本工作模式
当你使用分组密码加密,例如高级加密标准(AES),你应该选择一个分组密码的工作模式。你能选择的最糟糕的工作模式是EBC模式(它的实现最简单,运行速度最快),它令人印象深刻的是,重复的明文将会产生重复的密文。
典型错误例子:
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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);
</pre>
解决办法:
对明文格式有特殊要求的环境,可选用CFB模式,无要求用CBC模式。
使用CBC模式时必须传入IV参数,用前面的例子,使用创建iv对象。
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">... 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); ...
</pre>
不安全的加密算法
不得不提MD5,MD5在10年前已被破解,在IDEA中使用MD5会产生警告,但是部分安全意识淡薄的开发还会认为MD5是安全的。
除了MD5还有DES、3DES、RC4(弱的流密码)、RC5。其中RC4最常用于无线路由器WEP加密(2010年以前的路由器),因为RC4密码存在缺陷,利用RC4中的统计偏差,导致可对加密信息中的一些伪随机字节能进行猜测。
只要收集到数量足够多的通信数据包即可破解密码(在密集发包的WIFI环境下15分钟即可完成破解)。
典型错误例子:
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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();
</pre>
解决办法:
不要使用MD5、SHA1等过时的算法,用SHA-256和SHA-512等强算法。
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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)); }
</pre>
密钥长度太短
密钥长度太短是特指非对称秘钥。开发人员常常会有疑问,密码和加密秘钥之间有什么区别?
密码长度不一,密钥长度固定,并且通常密钥复杂度更高包含不可打印字符。密钥的熵值比前者高得多。
回到密钥长度太短上,通常开发人员在选择对称加密的密钥长度上基本会选128位以上的。错误发生在选择非对称加密的密钥长度上,在RSA、DAS、DH和相似的算法中,像RSA这种大数分解的密钥长度即使到达512位也是不安全的,1024或2048位才是主流。
椭圆曲线ECC可以用短密钥,有很好的发展空间,但是目前开发人员仍不愿意尝试用椭圆曲线的算法。
典型错误例子:
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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表示私钥
</pre>
解决办法:
将密钥大小调为2048位,或用更先进的椭圆曲线ECC加密算法,其密钥长度只有256位长度,只有RSA加密算法同等加密强度的密钥长度(3072位)java rsa加密算法,运算速度更快,更安全。
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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 map = new HashMap(); map.put(ECCEnum.PRIVATE_KEY.value(), BASE64Encoder.encodeBuffer(privateKey.getEncoded())); map.put(ECCEnum.PUBLIC_KEY.value(), BASE64Encoder.encodeBuffer(publicKey.getEncoded()));
</pre>
不安全的随机数
如果开发安全意识淡薄,使用java.util.类生成一个Web应用程序的会话标记。
我已获得会话标记,那么可以预测下一个用户和前一个用户的会话标记进而劫持他们的会话。
典型错误例子:
<pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">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);
</pre>
解决办法:
使用类产生会话标记,种子很重要,不要设置特定值作为种子。
这里2^32中可能的会话标记(有40多亿),同时注意不要截取,过短可能被爆破token。
<p><pre style="overflow-wrap: break-word;margin-top: 15px;margin-bottom: 15px;padding: 2px;background-color: rgb(248, 248, 248);border-width: 1px;border-style: solid;border-color: rgb(204, 204, 204);border-radius: 3px;overflow: auto;color: rgb(81, 81, 81);font-size: 14px;text-align: start;">StringBuilder buf = new StringBuilder(); SecureRandom sr = new SecureRandom(); for( int i=0; i