前言
上篇文章中,我们讨论了端口扫描器的实现,编码实现了一个简单的多线程端口扫描器,从子域名挖掘到端口扫描,主机发现部分暂时结束了,今后遇到更好用的主机发现技术再作补充,接下来开始服务的识别工作。
通过主机发现技术,我们知道某个域名、某些IP下有哪些目标主机,指导了目标在哪,接下来就需要了解目标是什么,这就是服务识别技术。
服务识别的核心,是指纹识别,指纹的构建方式千奇百怪,识别速度和准确度也各有千秋,但万变不离其宗,目的都是通过服务的唯一特征,来确定该服务,本文重点从网络特征入手,基于前文实现的主机发现函数,重点分析和实现以网络特征为基础构建的指纹识别器。
阅读本篇文章,你需要:
正文
本篇文章中,我们主要讨论基于网络特征的指纹构建,所谓网络特征,主要指端口特征、数据包特征、以及主机的Internet特征,这些方式常用在操作系统识别、CDN判断以及常用服务初筛上。
端口特征
这里提到的端口特征,主要指TCP熟知端口,在基于C/S模式的服务中,为了方便连接,服务端的端口配置常常是默认端口,这就给了我们通过开放端口粗略识别服务的机会,这里我收集了一份常用端口对应的服务列表:
{
"21": "ftp",
"22": "ssh",
"23": "telnet",
"25": "smtp",
"53": "dns",
"68": "dhcp",
"80": "http",
"69": "tftp",
"995": "pop3",
"135": "rpc",
"139": "netbios",
"143": "imap",
"443": "https",
"161": "snmp",
"489": "ldap",
"445": "smb",
"465": "smtps",
"512": "linuxrrpe",
"513": "linuxrrlt",
"514": "linuxrcmd",
"873": "rsync",
"993": "imaps",
"1080": "proxy",
"1099": "javarmi",
"1352": "lotus",
"1433": "mssql",
"1521": "oracle",
"1723": "pptp",
"2082": "cpanel",
"2083": "cpanel",
"2181": "zookeeper",
"2222": "dircetadmin",
"2375": "docker",
"2604": "zebra",
"3306": "mysql",
"3312": "kangle",
"3389": "rdp",
"3690": "svn",
"4440": "rundeck",
"4848": "glassfish",
"5432": "postgresql",
"5632": "pcanywhere",
"5900": "vnc",
"5984": "couchdb",
"6082": "varnish",
"6379": "redis",
"9001": "weblogic",
"7778": "kloxo",
"10050": "zabbix",
"8291": "routeros",
"9200": "elasticsearch",
"11211": "memcached",
"27017": "mongodb",
"27018": "mongodb",
"50070": "hadoop",
"888": "宝塔",
"8080": "http"
}
这里使用Json来保存,我们先来读取这份列表:
def initFingerprint(fingermode = 1):
try:
# 模式1:打开端口指纹文件
if fingermode == 1:
return open(portfingerfile,"r",encoding="utf8")
except Exception as e:
print(e)
return None
通过这份列表,我们就可以实现一个粗略的服务识别函数,其中使用了前文中提到的端口扫描函数:
# 通过端口识别服务:
import json
def PortAnalyze(ip):
results = []
try:
portjson = initFingerprint(1)
portdict = json.load(portjson)
ports = Scan(ip,0,65535)
for result in ports:
if str(result) in portdict:
dict = {
"port":result,
"service":portdict[str(result)]
}
results.append(dict)
except Exception as e:
print(e)
return results
接下来,我们仍然找东南某省的设备来测试:
匹配一下端口试试:
限于常见端口列表,只能粗略估计所存在的服务。
Internet特征
Internet特征主要通过的主机所在的网络位置识别服务,多用于识别网络基础设施。比如CDN或路由器,我们收集到了一份网络常见CDN的网络位置,文件放在了项目源代码中。接下来我们基于这个文件来实现CDN的识别。
首先,加载指纹文件:
def initFingerprint(fingermode = 1):
try:
# 模式1:打开端口指纹文件
if fingermode == 1:
return open(portfingerfile,"r",encoding="utf8")
# 模式2:打开CDN指纹文件
if fingermode == 2:
return open(cdnfingerfile,"r",encoding="utf8")
except Exception as e:
print(e)
return None
我们通过CIDR子网来判断,这里使用内置的ipaddress模块实现:
# 通过ip所在子网判断:
import ipaddress
def IpIsCDN(ip,cdn_list):
try:
for network in cdn_list:
if ipaddress.ip_address(ip) in ipaddress.ip_network(network):
return True
except Exception as e:
print(e)
return False
也可以通过主机CNAME解析来判断,这里使用dnspython模块完成:
# 通过CNAME判断:
from dns import resolver
def CnameIsCDN(domain,cdn_list):
try:
results = resolver.resolve(domain,"CNAME")
for result in results.response.answer:
for cdn in cdn_list:
if cdn in result.to_text():
return True
except Exception as e:
print(e)
return False
通过自治系统号(Autonomous System Number)判断,这里需要使用GeoIP加载,数据库地址,注册账号并登陆后选择GeoLite2-ASN下的mmdb格式,使用pip引入geoip2后,先来编写代码加载GeoIP数据库:
# geoip引入
import geoip2.database
def initGeoip():
try:
return geoip2.database.Reader(geodb)
except Exception as e:
print(e)
return None
接下来编写ASN判断逻辑:
# 通过ASN判断:
def ASNIsCDN(ip,cdn_list):
geo = initGeoip()
try:
result = geo.asn(ip)
if str(result.autonomous_system_number) in cdn_list:
return True
except Exception as e:
print(e)
return False
识别逻辑完成,接下来编写CDN识别主函数,使用了前文中提到的域名解析函数:
# CDN识别主函数
def CheckCDN(domain):
cdnjson = initFingerprint(2)
cdnfinger = json.load(cdnjson)
if CnameIsCDN(domain,cdnfinger["all_CNAME"]):
return True
iplist = domainToip(domain)
for ip in iplist:
if IpIsCDN(ip,cdnfinger["cdns_ip"]):
return True
elif ASNIsCDN(ip,cdnfinger["ASNS"]):
return True
return False
我们拿一个使用CDN优化过的单人博客来测试:
由于CDN厂商很少变换自己的网络位置,可以粗略使用这种方式进行CDN的识别判断。
数据包特征识别
由于不同操作系统对IP协议栈的实现不同,可以通过数据包特征识别对应的操作系统信息,其中最简单的特征是TTL,Windows的默认是128,Linux/Unix的常见TTL值是64,有些特殊的Unix会将TTL设置为255。
我们先来写一个简单的练练手,仍然使用Scapy对目标主机数据包进行探测:
# 通过数据包特征简单识别OS:
from scapy.layers.inet import TCP, IP
from scapy.sendrecv import sr1, send
def checkOS(ip,port):
result = {
"OS":None
}
try:
# 发送一个握手包
ans = sr1(IP(dst=ip) /
TCP(dport=port, flags="S"),
timeout=1, verbose=False)
# 回显判断
if ans.haslayer(IP):
ttl = ans.getlayer(IP).ttl
if ttl <= 64:
result["OS"] = "Linux/Unix"
elif ttl <= 128:
result["OS"] = "Windows"
elif ttl <= 255:
result["OS"] = "Unix"
# 发送挥手包
send(IP(dst=ip) /
TCP(dport=port, flags='R'),
verbose=False)
except Exception as e:
print(e)
return result
实际上,一些成熟的服务商都会伪装自己的TTL,此时需要更精确的方式来识别网络特征。这里,我们来看看Nmap是怎么处理的,源代码可以在Nmap官网找到,在nmap-os-db中包含了2600多已知系统的指纹特征,我们先挑选一条来看看:
#Windows 7 Professional Version 6.1 Build 7600
Fingerprint MicrosoftWindows 7 Professional
ClassMicrosoft | Windows | 7 | general purpose
CPEcpe:/o:microsoft:windows_7::professional
SEQ(SP=FC-106%GCD=1-6%ISR=108-112%TI=I%II=I%SS=S%TS=7)
OPS(O1=M5B4NW8ST11%O2=M5B4NW8ST11%O3=M5B4NW8NNT11%O4=M5B4NW8ST11%O5=M5B4NW8ST11%O6=M5B4ST11)
WIN(W1=2000%W2=2000%W3=2000%W4=2000%W5=2000%W6=2000)
ECN(R=Y%DF=Y%T=7B-85%TG=80%W=2000%O=M5B4NW8NNS%CC=N%Q=)
T1(R=Y%DF=Y%T=7B-85%TG=80%S=O%A=S+%F=AS%RD=0%Q=)
T2(R=N)
T3(R=N)
T4(R=N)
T5(R=Y%DF=Y%T=7B-85%TG=80%W=2000%S=Z%A=S+%F=AR%O=%RD=0%Q=)
T6(R=N)
T7(R=N)
U1(DF=N%T=7B-85%TG=80%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)
IE(DFI=N%T=7B-85%TG=80%CD=Z)
来分析一下各行的含义:
- 第一行为注释行,说明此指纹对应的操作系统与版本。
- Fingerprint关键字定义一个新的指纹,紧随其后的是指纹名字Microsoft Windows 7Professional。
- Class行用于指定该指纹所属的类别,依次指定该系统的vendor(生产厂家), OS family(系统类别), OS generation(第几代操作系统), 以及device type(设备类型)。此处Vendor为Microsoft, OS family为Windows,OS generation为7,设备类型为通用设备(普通PC或服务器)。
- CPE行,以标准的CPE(Common Platform Enumeration,通用平台枚举)格式来描述操作系统类型,便于Nmap与外界信息的交换,可以很快从网上开源数据库查找到CPE描述的操作系统具体信息。Nmap使用的标准格式为:
cpe:/<part>:<vendor>:<product>:<version>:<update>:<edition>:<language>
- SEQ描述顺序产生方式
- OPS描述TCP包中可选字段的值
- WIN描述TCP包的初始窗口大小
- ECN(Explicit Congestion Notification)描述TCP明确指定拥塞通知时的特征
- T1-T7描述TCP回复包的字段特征
- U1描述向关闭的UDP发包产生的回复的特征
- IE描述向目标机发送ICMP包产生的特征
Nmap正是通过定义复杂的数据包特征来达到高精度识别操作系统的结果,我们来看一下主要的识别函数:
/* Performs the OS detection for IPv4 hosts. This method should not be called
* directly. os_scan() should be used instead, as it handles chunking so
* you don't do too many targets in parallel */
///IPv4的操作系统探测的实现函数,由os_scan()来调用。
int OSScan::os_scan_ipv4(vector<Target *> &Targets) {
int itry = 0;
/* Hosts which haven't matched and have been removed from incompleteHosts because
* they have exceeded the number of retransmissions the host is allowed. */
list<HostOsScanInfo *> unMatchedHosts; ///记录超时或超过最大重传而未匹配的主机扫描信息
/* Check we have at least one target*/
if (Targets.size() == 0) {
return OP_FAILURE;
}
perf.init();///初始化扫描性能变量
///操作系统扫描的管理对象,维护未完成扫描列表std::list<HostOsScanInfo *> incompleteHosts;
OsScanInfo OSI(Targets);
if (OSI.numIncompleteHosts() == 0) {
/* no one will be scanned */
return OP_FAILURE;
}
///设置起始时间与超时
OSI.starttime = o.TimeSinceStart();
startTimeOutClocks(&OSI);
///创建HOS对象,负责管理单个主机的具体扫描过程
HostOsScan HOS(Targets[0]);
/* Initialize the pcap session handler in HOS */
///打开libpcap,设置对应的BPF filter,以便接收目标的回复包
begin_sniffer(&HOS, Targets);
while (OSI.numIncompleteHosts() != 0) {
if (itry > 0)
sleep(1);
if (itry == 3)
usleep(1500000); /* Try waiting a little longer just in case it matters */
if (o.verbose) {
char targetstr[128];
bool plural = (OSI.numIncompleteHosts() != 1);
if (!plural) {
(*(OSI.incompleteHosts.begin()))->target->NameIP(targetstr, sizeof(targetstr));
} else Snprintf(targetstr, sizeof(targetstr), "%d hosts", (int) OSI.numIncompleteHosts());
log_write(LOG_STDOUT, "%s OS detection (try #%d) against %s\n", (itry == 0)? "Initiating" : "Retrying", itry + 1, targetstr);
log_flush_all();
}
///准备第itry轮的OS探测:删除陈旧信息、初始化必要变量
startRound(&OSI, &HOS, itry);
///执行顺序产生测试(发送6个TCP探测包,每隔100ms一个)
doSeqTests(&OSI, &HOS);
///执行TCP/UDP/ICMP探测包测试
doTUITests(&OSI, &HOS);
///对该轮探测的结果做指纹对比,获取OS扫描信息
endRound(&OSI, &HOS, itry);
///将超时未匹配的主机移动到unMatchedHosts列表中
expireUnmatchedHosts(&OSI, &unMatchedHosts);
itry++;
}
/* Now move the unMatchedHosts array back to IncompleteHosts */
///对没有找到匹配的主机,将之移动的未完成列表,并查找出最接近的指纹(以概率形式展现给用户)
if (!unMatchedHosts.empty())
OSI.incompleteHosts.splice(OSI.incompleteHosts.begin(), unMatchedHosts);
if (OSI.numIncompleteHosts()) {
/* For hosts that don't have a perfect match, find the closest fingerprint
* in the DB and, if we are in debugging mode, print them. */
findBestFPs(&OSI);
if (o.debugging > 1)
printFP(&OSI);
}
return OP_SUCCESS;
}
总结一下,主要识别步骤如下:
- 根据进行的扫描次数,适当休眠
- 准备该轮扫描的所需的环境,清理垃圾数据并初始化必要的变量
- 提取指纹的SEQ/OPS/WIN/T1几行数据,并进行顺序产生测试(Sequence generationtests)。
- TCP/UDP/ICMP综合探测,拟合指纹数据。
- 处理此轮探测的结果,匹配相应的系统指纹,移除已完成的扫描任务。
- 移除超时不匹配的主机到unMatchedHosts列表中。
接下来,我们利用Nmap来实现指纹的探测,Nmap中文网提供了各种版本Nmap安装包,安装完成后,在Python环境中添加python-nmap模块,接下来编写OS指纹探测函数:
import nmap
# 利用Nmap识别OS:
def checkOS_nmap(ip):
# 创建扫描对象
scanner = nmap.PortScanner()
result = {
"OS":None
}
try:
# 开始扫描
response = scanner.scan(hosts=ip,arguments="-O")
if response["scan"][ip]["osmatch"]:
result["OS"] = response["scan"][ip]["osmatch"][0]
except Exception as e:
print(e)
return result
仍然找一个某省主机做测试:
测试一下两种探测结果:
可以看到,两种方式都可以识别出操作系统类型,但Nmap的指纹库回显信息显然更加详细,关于Nmap指纹库的详细细节,可以查阅官网说明
结语
在本篇文章中我们列举了几种常见的网络特征指纹设计和识别方式,并分析了Nmap的OS指纹库和用来识别设备指纹的主要函数,需要读者有一定的文档和源码阅读能力,这里列出文中编写的完整代码地址。
网络特征只是指纹构建的一种分支,这是一种基于规则库的专家系统思路。以这种方式匹配到的指纹一旦成功,精度较高,但对未收录在指纹库中的系统无能为力。
接下来的文章中,我们将总结并实现通过文本特征构建的指纹识别方式,并以SqlMap等主流工具的指纹库为例编码实现服务识别