使用DNS名称作为安全性依据的漏洞优化

  • 2019 年 10 月 5 日
  • 筆記

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/weixin_38004638/article/details/100605199

漏洞

问题描述:程序中采用DNS名称进行安全认证,但DNS名称是容易被攻击者进行欺骗的。

许多 DNS 服务器都很容易被攻击者欺骗,所以应考虑到某天软件有可能会在有问题的 DNS 服务器环境下运行。如果允许攻击者进行 DNS 更新(有时称为 DNS 缓存中毒),则他们会通过自己的机器路由您的网络流量,或者让他们的 IP 地址看上去就在您的域中。勿将系统安全寄托在 DNS 名称上。

例如:下面代码片段中,如果发生DNS欺骗,会绕过安全验证。

String ip = request.getRemoteAddr();

InetAddress inetAddress = InetAddress.getByName(ip);

if (inetAddress.getCanonicalHostName().endsWith("demo.com")) {

//Verification passed

}

修复建议: 不要依赖DNS名称进行安全认证。

IP 地址相比 DNS 名称而言更为可靠,但也还是可以被欺骗的。攻击者可以轻易修改要发送的数据包的源 IP 地址,但是响应数据包会返回到修改后的 IP 地址。为了看到响应的数据包,攻击者需要在受害者机器与修改的 IP 地址之间截取网络数据流。为实现这个目的,攻击者通常会尝试把自己的机器和受害者的机器部署在同一子网内。攻击者可能会巧妙地采取源地址路由的方法来回避这一要求,但是在今天的互联网上通常会禁止源地址路由。总而言之,核实 IP 地址是一种有用的 authentication 方式,但不应仅使用这一种方法进行 authentication。

解决方案:如果通过域名检查的方式可以确保主机接受和发送的 DNS 记录的一致性,您可以更加信任这一方式。攻击者如若不能控制目标域的域名服务器,就无法同时欺骗接受和发送的 DNS 记录。虽然这种方法并不简单,但是:攻击者也许可以说服域注册者把域移交给一个恶意的域名服务器。依赖于 DNS 记录的 authentication 是有风险的。 虽然没有十分简单的 authentication 机制,但是还有比基于主机的 authentication 更好的方法。密码系统提供了比较不错的安全性,但是这种安全性却易受密码选择不当、不安全的密码传送和 password management 失误的影响。类似于 SSL 的方法值得考虑,但是通常这样的方法过于复杂,以至于使用时会有运行出错的风险,而关键资源也随时面临着被窃取的危险。在大多数情况下,包括一个物理标记的多重 authentication 可以在合理的代价范围内提供最大程度的安全保障。 Tips: 1. 检查 DNS 信息的使用情况。除了考虑程序员的 authentication 机制能否起作用以外,还应该考虑在社会工程攻击中是如何利用 DNS 欺骗的。例如,如果攻击者可以使自己发出的数据包看上去像是来自内部机器的,他们是否可以通过验证程序获得信任呢?

修复方案1:添加ip校验

ip = ipMatch(IpUtil.getIpAddress().getHostAddress());
private static String ipMatch(String ip){      if (Pattern.matches("[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?", ip)) {          return ip;      }      return "0";  }

对于IP

可以简单的用JDK提供的方法进行校验: try { InetAddress.getByName(ip); } catch (UnknownHostException uhe) { throw new Exception("Ip address " +ip + " is invalid!"); }

getByName方法原本是传入host name,解析成IP返回,但是也支持传入IP,返回IP,同时对该IP做了校验。

再对IP进行正则表达式判断:

一个IP字串,由四段组成,每一段是0~255的数字,段与段之间用小数点隔开,比如61.139.2.69就是一个合法的IP字串。

如果正则表达式写成/d{1,3}(/./d{1,3}){3}无疑是不负责的,因为它可以匹配300.400.555.666这样的非法IP字串。

要匹配一个0~255之间的数,有几种匹配方式,下面是其中一种:

匹配 正则表达式 说明 0~9 /d 单个数字 10~99 [1-9]/d 两位数 100~199 1/d/d 百位为1的三位数 200~249 2[0-4]/d 三位数,百位是2,十位是0~9 250~255 25[0-5] 三位数,百位是2,十位是5,个位是0~5 写成正则表达式,即:(/d|([1-9]/d)|(1/d/d)|(2[0-4]/d)|(25[0-5])),但是这样的正则表达式在匹配254这样的字串时,会分别匹配2、5、4,得到3个匹配,达不到预期效果,正确做法是将次序颠倒为((25[0-5])|(2[0-4]/d)|(1/d/d)|([1-9]/d)|/d),因为在(xxx|yyy)这种匹配行为中,是从左向右搜索的。

完整的正则表达式是:

((25[0-5])|(2[0-4]/d)|(1/d/d)|([1-9]/d)|/d)(/.((25[0-5])|(2[0-4]/d)|(1/d/d)|([1-9]/d)|/d)){3} 按:

象061这样的高位为0的数是不能被匹配的。 太麻烦,不如自己写一小段代码解析来得容易,呵呵 一个完整的域名,由根域、顶级域、二级、三级……域名构成,每级域名之间用点分开,每级域名由字母、数字和减号构成(第一个字母不能是减号),不区分大小写,长度不超过63。

很显然,单独的名字可以由正则表达式[a-zA-Z0-9][-a-zA-Z0-9]{0,62}来匹配,而完整的域名至少包括两个名字(比如google.com,由google和com构成),最后可以有一个表示根域的点(在规范中,最后有一个点的才是完整域名,但一般认为包括两个以上名字的域名也是完整域名,哪怕它后面没有点)。

匹配完整域名的正则表达式:

[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?

对于HOSTNAME

用正则表达式: 这里顺便把校验IPV4和V6的REG也写出来: public static String REG_IPV4 = "\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}\b"; public static String REG_IPV6 = "^(((?=(?>.*?::)(?!.*::)))(::)?([0-9A-F]{1,4}::?){0,5}|([0-9A-F]{1,4}:){6})(\2([0-9A-F]{1,4}(::?|$)){0,2}|((25[0-5]|(2[0-4]|1\d|[1-9])?\d)(\.|$)){4}|[0-9A-F]{1,4}:[0-9A-F]{1,4})(?<![^:]:|\.)\z";

public static String VALID_HOST_REGEX = "^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?$";

用的时候很简单: Pattern pattern = Pattern.compile(VALID_HOST_REGEX, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(host); if(!matcher.find()) {

throw new Exception("Invalid gateway host"); }

https://www.jb51.net/article/72601.htm

https://www.baidu.com/link?url=jZ_QuN3XINVhP2iN13WjrQFL69NeNecQwCYHcJ5LQwVcNCtg16wkSHA6ByDaKJXxWV6f0UsbLw_NxbWqp5xmuf6cP3QgQgdlXkLAP5Z8Bsm&wd=&eqid=b8fbbc510016d604000000035d73709b

https://blog.csdn.net/seawave/article/details/1520988

修复方案2:更换获取IP的方法

https://www.iteye.com/blog/lafecat-2229184

https://www.jianshu.com/p/6c445ea8211b

参考:

https://www.cnblogs.com/eyesmoon/p/7421477.html