内网设备存在弱密码,对于企业来说,是个极大的安全隐患。而如何快速地发现企业网络中各种设备的弱密码,并有效地进行处置,便成了摆在安全部门面前的难题。

笔者所在的企业,光是OA网络就有10万+的活跃IP,分布于总部及数十个分支机构中,设备类型也很繁杂。为了能够每周都对全体设备做一次弱密码审计并定期输出扫描报告,我基于开源工具有针对性地编写了一些辅助工具(脚本)来做这件事情,取得了不错的效果。因而我在此将自己的经验和代码分享出来,希望对大家有所帮助。

在本篇文章中分享的是ssh弱密码审计工具。我会具体讲解一下它的开发思路、使用环境和以及使用方法。

一、ssh弱密码审计工具的执行流程

下面是我做一次ssh弱密码审计的执行流程:

(1)使用开源工具zmap对全网进行ssh端口扫描,发现所有潜在的ssh主机;

(2)使用自编写的工具sshbrute对上述的ssh主机进行弱密码审计;

(3)使用自编写的脚本sshverify对一些特殊的设备进行二次验证和过滤;

(4)输出弱密码审计的结果。

二、相关的资源和环境

本文的工具均基于Linux进行开发和运行。在我的企业环境中,只允许使用红帽企业版,所以我是基于RHEL 6/7来开发的。但其它的Linux发行版应该也没有什么问题。同时你还需要准备以下的软件或工具:

(1)zmap,请参考其主页(URL:https://github.com/zmap/zmap)自行编译安装;

(2)编译工具,主要是gcc/g++/make/cmake/libtool等;

(3)第三方库,主要是libssh2(URL:https://www.libssh2.org/ ),以及plog和getoptpp,均安装在/usr/local目录下;

(4)python 2.7,以及paramiko库(URL:https://pypi.python.org/pypi/paramiko/)。

(5)我的工具代码sshbrute.cpp和sshverify.py,链接地址https://github.com/penoxcn/SshWeakPasswordAudit.git

其中sshbrute.cpp使用以下命令编译即可:

g++ --std=c++11 sshbrute.cpp -o sshbrute-pthread -I /usr/local/include/ -lssh2  -L/usr/local/lib

三、ssh主机的发现

在我的企业OA网络中使用10.0.0.0/8的A类地址,每个分支机构使用一个或多个16位掩码的地址段,地址空间比较大。端口扫描工具有很多,开源的就有nmap、zmap、masscan等。相对来说,nmap扫描速度慢,准确度高,适合精细的扫描,如OS识别、服务识别等;masscan和zmap速度快,适合于进行大规模(如整个Internet地址空间)的端口扫描。而在企业内网环境中,我觉得zmap更易用一些,在漏报率和误报率方面比masscan表现要好一些。zmap最大的不便是一次扫描只支持扫描一个端口,如果有多个端口要扫描,需要多次启动程序进行扫描。

要使用zmap扫描网络中的ssh主机,使用以下命令即可:

zmap -M tcp_synscan -p 22 -wipsubnets.txt  -o ssh_hosts_found.txt -B50M -i eth0 --disable-syslog 

其中:

-M tcp_synscan指定扫描方式为TCP SYN扫描;

-p 参数指定要扫描的端口,本例中是ssh的默认端口22;

-w ipsubnets.txt 指定要扫描的目标地址从文件ipsubnets.txt中加载;

-o ssh_hosts_found.txt指定扫描的结果输出到文件ssh_hosts_found.txt中;

-B 50M 指定扫描的速率为50Mbps;

-i eth0指定服务器的网卡接口,没有指定zmap会自动查找默认的接口;

–disable-syslog 是禁止发送syslog日志。

影响扫描效率和扫描时间的变量是目标地址空间(-w参数)的大小和扫描速率(-B)参数。扫描的地址空间越大,耗时就越多;扫描速度越高,耗时就越短。扫描时间与速率的关系可以大约估算:1Mbps的速率大约相当于每秒2400个SYN包,所以以xMbps的速率扫描y个IP地址,总时间=y/(x*2400)。例如8Mbps速率扫描一个B段65536个地址,只需要3.4秒(实际还要再加上几秒的等待时间)。

在ipsubnets.txt中直接填入10.0.0.0/8自然最简单,但也很粗暴,因为其中包含大量的无效地址范围。稍为高效一点做法的是将每个分支机构的地址范围写入ipsubnets.txt中,如10.0.0.0/16,10.1.0.0/16,10.2.0.0/16…,在我的企业中大概共有70个16位掩码的地址段,仅此便可以将扫描的范围缩减超过7成。

扫描的速率需要依照企业实际环境来设定的,不是越大越好。在总部范围内是同一个局域网,速度大一点没有关系。但对于异地甚至跨国的分支结构来说,大多是通过仅有几M带宽的专线进行连接的,扫描速率就不能超过专线速度,不然会影响生产使用。高速率也可能会意外地影响网络设备的运行,曾经有个分行向我反映,在扫描的时间段内,他们的核心交换机CPU严重超过阈值,后来分析是因为扫描时探测了大量不存在的地址(段),交换机在寻找路由时耗费了过多的CPU资源。因此在实际中,我的做法是,将各机构的IP地址范围精确到C段(通过每周定期全IP范围ping探测得到),大部分分支机构的地址范围能够缩减六七成;然后针对总部使用高速扫描,针对分支机构使用低速扫描。通过这种方式,一方面大幅降低了扫描总时间,另一方面也大大减少了扫描对企业网络正常运行的影响。

zmap输出的结果是开放了ssh端口的ip地址集合,每行一个ip地址。由于sshbrute工具接受的输入文件是每行一个ip:port的形式,因此需要进行一下转换,用sed命令即可完成:

sed "s/$/:22/g" ssh_hosts_found.txt > ssh_targets.txt

如扫描的是非标准ssh端口的主机,使用实际的端口替换22即可。

四、使用sshbrute工具进行弱密码审计

虽然有诸如thc-hydra、ncrack等开源工具可以做ssh的暴力破解工作,但是此类通用工具一般编译和使用要求都比较高,况且我们仅做最常见的一些弱密码的审计,不是要做高强度的暴力破解,这时候定制化的小工具反而更灵活,实现起来也不难。原理很简单:针对每一个IP地址,循环遍历密码字典中的“用户名:密码”组合进行ssh登录尝试,找到成功的登录尝试,记录结果即可。一开始我是用Python实现的,因为其它协议的弱密码审计均是用Python实现的,但是发现Python的“伪多线程”特性使得它无法有效利用多个CPU,另外在多线程环境下,Python的ssh库paramiko总会出现各种难以排查的问题(例如挂死在某些连接上),因此我用C++基于libssh2编写了一个多线程的弱密码审计程序,不到500行代码搞定,具体代码参见sshbrute.cpp。

sshbrute.cpp中最主要的函数是SshTryLogin,根据IP地址、用户名、密码等参数完成一次ssh验证过程。在主程序中,针对每个IP地址,程序会先进行预测试PreTest,在PreTest时使用一个随机的用户名和密码调用SshTryLogin。预测试的作用有二:一是检测目标主机是否是可连接的ssh主机,如果不可连接,则后续不再尝试登陆了;二是探测是否不需要密码,或任意密码就可以登录的主机。在我的企业中,曾经发现过许多不需要认证或者任意用户名和密码都可以登录的交换机设备,以及视频会议设备,此种情况只需要报告结果,也不需要再进行尝试了。预测试之后,则是循环地对每一个用户名,依次使用字典中的密码进行登录,如果登录成功则运行uname -a命令获得系统的信息并保存,然后结束当前用户的登录尝试,继续进行另一个用户的尝试,直到字典遍历完了再继续探测下一个IP地址。

sshbrute的具体用法如下:

./sshbrute -i ssh_targets.txt -dssh_userpasswords.txt -o ssh_weakpasswd_found.txt -l sshbrute.log -t 500

其中:

-i ssh_targets.txt指定了要探测的主机目标,每行一个,ip:port 的形式;

-d ssh_userpasswords.txt 指定了字典文件,每行一个username:password组合;如果一个用户名要探测多个密码,则需要分别写上这些组合;

-o ssh_weakpasswd_found.txt 指定了找到的ssh弱密码记录输出文件;

-l sshbrute.log 指定了日志记录文件;

-t 500 指定了线程数目。

影响整个审计任务时间的因素主要是目标的数目,弱密码的组合数,以及探测的线程数。为了尽可能地发挥多线程的优势,机器的性能也建议配置高一点(例如16G以上RAM,8核以上CPU)。我的企业环境中,全网约2.2w个ssh地址目标,弱密码字典62条记录,开800个线程,1个小时可以跑完。

弱密码字典中的条目选择也很关键,应该少而精,最好不要超过100条组合。我们是做全网弱密码审计,不是做暴力破解。我挑选的原则是:(1)各种设备的默认用户和密码、初始安装密码,例如IBM HMC初始用户和密码是hscroot:abc123,IBM虚拟化环境里VIOS默认用户和密码padmin:padmin,F5设备root用户默认密码default,Juniper交换机root:root123,某类型光纤存储使用superuser:passw0rd,监控摄像头root:12345等等;(2)网络中常见的TOP弱密码,与常见管理员用户名的组合,如top 20的弱密码,如123456、password、12345678、qwerty、123123等等。常见管理员用户root/admin/db2admin等;(3)带有企业环境特色的密码,公共密码:例如有些人喜欢用公司英文名称小写作为密码;或者公司缩写+@年份作为密码,这类密码在开发环境大家喜欢使用,因为便于共享;(4)一些特定用户名和密码相同的组合,如root:root,admin:admin,test:test以及网络管理员喜欢用的cisco:cisco等;(5)其它,例如曾经我们虚拟化环境的模板就使用了固定的管理员和密码,量产了很大一批的Linux虚拟机,这种也需要加入到字典里进行检查。

经过精心选择的字典,能够发现99%以上的弱密码问题。剩下的少量未知的,则认为是可以接受的残余风险了。

五、使用sshverify工具对上述结果进行二次验证

对于常见的Linux/Unix等设备,sshbrute发现的结果是精确的,它通过uname -a命令获得了发行版本信息,或者特定shell的错误信息(例如cisco IOS会报告无法识别该命令),但也有一些特殊或者说奇怪的设备,用libssh2显示用户验证成功,但用ssh客户端连接提示验证失败;又或者如某些视频会议设备这样,ssh验证成功,但实际无shell又无法ssh tunnel的情况;又或者如浪潮服务器的IPMI管理界面,libssh2无法获得banner信息的,等等。这些情况我们需要进行二次的验证,以过滤误报(验证失败)、干扰(视频设备),正确识别风险(认证成功的)。这便是sshverify.py程序要做的事情。

sshverify.py会首先筛选sshbrute输出的弱密码记录,条件是banner字段为空白或者包含IPHONE关键字(代表IP电话之类型的)的,此两类类型的需要再次验证,其它记录则不再验证直接复制到输出文件中。因为此两种类型记录不多,大概是100多个,所以程序没有使用多线程等并发机制。实现很简单,就是使用paramiko的SSHClient类依次连接筛选出来的每一个地址,成功连接后获取banner信息,根据banner判断目标的类型,然后再决定是否忽略该条弱密码记录。而在libssh2中,是无法正常使用它的api来获取到这个banner信息的。paramiko使用了不同的实现机制,却可以正常地获取到,正好可以和libssh2的形成了互补。

sshverify.py的具体用法如下:

python sshverify.py -l SshVerify.log -issh_weakpasswd_found.txt -o ssh_weakpasswd_verified.txt

其中:

-l SshVerify.log 指定了日志文件的路径;

-i ssh_weakpasswd_found.txt 是上一步sshbrute输出的弱密码记录;

-o ssh_weakpasswd_verified.txt 指定了验证过滤后的最终记录。

本程序中的验证案例是依据我公司环境定制的,大家可以根据实际使用环境的情况,对非常规的设备进行分析,修改sshverify.py程序,添加关键字进行验证和过滤。

六、经验总结

在上文中,我展示了可以用于企业实际环境中进行ssh弱密码审计的核心程序和脚本。如果要做成定期任务,不需要那么多手工参与的,而且要有统计分析的,则还需要自己再编写一些脚本,进行扫描程序的调用、与数据库的连接及数据存取、进行统计分析等等。另外的工作还包括:根据实际环境,对扫描的参数进行一些优化和调整,对网段范围和密码字典进行更新等等。

实际上对于领导来说,如何发现弱密码并不是最重要的一步,使用什么工具去发现弱密码他们并不关心,他们关心的是如何持续地运营弱密码发现和修复这个过程,能够说明你的成效的是一幅逐渐递降的“弱密码发现和整改数量趋势图”。最重要的工作其实是整改。在一个传统的大企业里,首先找到IP地址的使用人就颇费力气;找到责任人然后通知对方了,对方鸟不鸟你还不一定;即使对方鸟你了,懂不懂IT有没有能力整改也不一定。最终,连断网的威胁发出了,可能都没有效果。只有上级国家监管机构来检查了,我们才能借机强势一把。那时候啊,整改曲线就会有一个的斜率很抖的降落,像高台跳水运动一样漂亮。不说了,说多都是泪。

最后,希望这个工具对大家有用,如有不当的地方,敬请指出,不胜感激!后面有时间,我再慢慢整理一下自己编写的各种安全工具,与大家分享交流。

*本文作者:ipenox