用Python复刻Shodan:4000行代码自建网络空间搜索引擎
当你在Shodan上搜索"Nginx country:CN"时,背后发生了什么?端口扫描、Banner抓取、指纹匹配、地理定位、数据索引——这些核心能力,能否用纯Python在本地复现?本文带你用4000+行代码,从零构建一个功能完整的网络空间搜索引擎。
一、为什么自建?
Shodan是全球最大的网络空间搜索引擎,它持续扫描互联网上所有可达设备,索引开放端口、服务指纹、地理位置等信息。对于安全研究人员而言,它是资产发现、威胁情报的利器。但Shodan的免费API有严格限制,且数据不在本地,无法定制。
自建一个"迷你Shodan"有三个核心价值:
数据自主:扫描结果存储在本地SQLite数据库中,可离线查询、二次分析,不受API配额限制。
定制灵活:可以针对特定目标深度扫描,添加自定义漏洞检测规则,集成到内部安全平台。
技术深度:通过亲手实现端口扫描、Banner解析、指纹匹配、CMS识别等模块,真正理解网络空间搜索引擎的工作原理。
我们的目标是:用纯Python(asyncio异步架构)实现一个4000+行的工具,覆盖端口扫描、15+协议Banner抓取、200+服务指纹识别、30+ CMS检测、20+漏洞检测、IP地理定位、SQLite存储、HTML可视化报告生成等完整功能链。
二、整体架构
整个工具采用模块化设计,核心模块分工明确:
searcher.py (主控制器)
├── modules/port_scanner.py — 异步TCP端口扫描
├── modules/banner_grab.py — 多协议Banner抓取
├── modules/fingerprint.py — 服务指纹识别引擎
├── modules/web_detect.py — CMS/WAF/技术栈识别
├── modules/vuln_check.py — 常见漏洞检测
├── modules/geo_lookup.py — IP地理定位
├── modules/crawler.py — 网页爬虫
├── storage.py — SQLite存储引擎
├── report.py — HTML报告生成器
├── utils.py — 工具函数
└── db/fingerprints.json — 指纹特征库
扫描流程分为五个阶段:
- 端口扫描 → 发现目标IP上的开放端口
- Banner抓取+指纹识别 → 获取服务响应,匹配软件名和版本
- Web应用检测 → 对HTTP/HTTPS端口识别CMS、WAF、Title
- 漏洞检测 → 检查未授权访问、默认口令等常见漏洞
- 地理定位 → 查询IP的国家、城市、ISP、ASN信息
每个阶段都是异步执行的,通过asyncio信号量控制并发数,在保证速度的同时避免对目标造成过大压力。
三、异步端口扫描:速度的秘密
端口扫描是整个工具的基础。传统的串行扫描一个端口需要等待TCP连接结果(超时通常2-3秒),扫描一台主机的70个常用端口就需要2-3分钟。
使用asyncio可以并发发起数百甚至数千个TCP连接尝试,将扫描时间压缩到几秒。
核心实现非常简洁:
async def scan_port(self, ip: str, port: int) -> PortResult:
result = PortResult(ip=ip, port=port, state="closed")
start = time.monotonic()
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(ip, port),
timeout=self.timeout
)
elapsed = (time.monotonic() - start) * 1000
result.state = "open"
result.latency_ms = round(elapsed, 2)
writer.close()
await writer.wait_closed()
except asyncio.TimeoutError:
result.state = "filtered"
except ConnectionRefusedError:
result.state = "closed"
return result
asyncio.open_connection() 是Python标准库提供的异步TCP连接函数,内部使用操作系统的非阻塞socket。当数百个这样的协程并发运行时,它们共享同一个事件循环,由操作系统epoll/kqueue统一调度IO,效率远高于多线程方案。
并发控制是关键。如果同时发起数万个连接,可能会耗尽文件描述符或触发目标的防护机制。我们使用asyncio.Semaphore进行信号量控制:
self.semaphore = asyncio.Semaphore(self.concurrency) # 默认500
async def _scan_with_semaphore(self, ip: str, port: int):
async with self.semaphore:
return await self.scan_port(ip, port)
同时采用分批执行策略,每批创建concurrency * 2个任务,用asyncio.gather()并发执行,避免一次性创建过多协程导致内存爆炸。
实测性能:扫描一个/24网段(254个IP)的70个常用端口(约18000个目标),使用200并发,耗时约25秒,扫描速率约700端口/秒。
四、Banner抓取:读懂服务的"名片"
发现开放端口只是第一步,更重要的是知道端口上运行的是什么服务。Banner是服务在连接建立时主动或被动返回的欢迎信息,通常包含软件名称和版本号。
不同协议的Banner获取方式差异很大,我们实现了15+种协议的专用抓取器:
SSH:连接后直接读取第一行,格式为SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1,可以直接解析出协议版本和软件信息。
HTTP/HTTPS:发送HEAD / HTTP/1.1请求,从响应头的Server字段获取Web服务器信息。HTTPS还需要处理TLS握手和证书信息提取。
FTP:连接后服务端会主动发送欢迎信息,如220 ProFTPD 1.3.5e Server。还可以发送SYST命令获取操作系统类型。
MySQL:MySQL的握手协议比较特殊,服务端发送一个二进制协议包。我们需要解析包头(4字节长度+序列号),跳过协议版本字节,读取以null结尾的版本字符串。
Redis:发送INFO server命令,返回的文本中包含redis_version、redis_mode、os等丰富信息。
VNC:VNC连接后会发送RFB协议版本,如RFB 003.008,可以判断VNC服务版本。
协议猜测逻辑也很重要。当通用TCP连接没有返回有效banner时,我们会根据端口号进行协议推测(如22=SSH,3306=MySQL),然后尝试该协议的专用握手方式获取更精确的信息。
五、指纹识别:从Banner到软件名
拿到Banner后,需要精确识别出软件名称和版本号。这是指纹匹配引擎的核心工作。
我们的指纹数据库db/fingerprints.json包含200+条规则,每条规则是一个正则表达式模式,匹配成功后可以从捕获组中提取版本号。
例如,识别Apache HTTP Server的规则:
{
"service": "http",
"pattern": "Server:\\s*Apache/([\\d.]+)",
"software": "Apache HTTP Server",
"group": 1,
"product": "Apache",
"cpe": "cpe:/a:apache:http_server"
}
当Banner中出现Server: Apache/2.4.54时,正则匹配成功,捕获组1提取出版本号2.4.54。
识别引擎会遍历所有规则,计算每条匹配的置信度。置信度由多个因素决定:
- 基础置信度:0.5(匹配成功即有)
- 端口匹配加分:+0.2(如3306端口匹配MySQL规则)
- 已知服务匹配加分:+0.15
- 有软件名加分:+0.1
- 有版本捕获组加分:+0.05
最终选择置信度最高的匹配结果。当没有规则匹配成功时,回退到启发式识别——根据Banner中的关键词(如"ssh-"、"http/1")和端口号进行粗略分类。
指纹库覆盖的服务类型包括:
- 远程访问:OpenSSH、Dropbear、Cisco SSH、MikroTik
- Web服务器:Apache、Nginx、IIS、LiteSpeed、Caddy、Tengine、OpenResty
- FTP:vsftpd、ProFTPD、FileZilla、Pure-FTPd、Serv-U
- 邮件:Postfix、Exim、Sendmail、Exchange、Dovecot
- 数据库:MySQL、MariaDB、PostgreSQL、MongoDB、Redis、Elasticsearch
- 中间件:Tomcat、WebLogic、JBoss、Jetty、Gunicorn
- IoT设备:Hikvision、Dahua摄像头、HP打印机、Synology NAS
- 安全设备:pfSense、FortiGate、Palo Alto、SonicWall
六、Web应用识别:30+ CMS指纹
对于HTTP/HTTPS端口,仅知道Web服务器是不够的。还需要识别后端运行的是什么CMS(内容管理系统)、使用了什么框架、是否有WAF(Web应用防火墙)。
CMS识别的原理是基于页面特征匹配。每种CMS都有独特的"指纹",比如:
- WordPress:页面中出现
wp-content/、wp-includes/、wp-json等路径 - ThinkPHP:页面或响应头中出现
ThinkPHP标识,或存在thinkphp_show_page_trace - Laravel:Cookie中出现
laravel_session - Spring Boot:出现
Whitelabel Error Page或X-Application-Context响应头 - Shiro:Cookie中出现
rememberMe - GitLab:Cookie中出现
_gitlab_session - Jenkins:响应头中出现
X-Jenkins或X-Hudson
WAF检测也基于类似的特征匹配。例如Cloudflare的特征是响应头包含cf-ray,AWS WAF的特征是x-amzn-requestid。
此外,我们还会提取页面Title、favicon hash(类似Shodan的http.favicon.hash查询)、技术栈(jQuery、Bootstrap、PHP、ASP.NET等)。
七、漏洞检测:20+未授权访问检查
在扫描过程中,我们同时对已知端口进行常见漏洞的快速检测。重点关注的是"未授权访问"类漏洞——这类漏洞危害大、检测简单、误报率低。
以Redis未授权访问检测为例:
async def _check_redis(self, ip, port):
reader, writer = await asyncio.wait_for(
asyncio.open_connection(ip, port), timeout=self.timeout
)
writer.write(b"INFO server\r\n")
await writer.drain()
data = await asyncio.wait_for(reader.read(8192), timeout=self.timeout)
text = data.decode("utf-8", errors="replace")
writer.close()
await writer.wait_closed()
if "redis_version" in text:
return VulnResult(
vuln_name="Redis Unauthorized Access",
severity="critical", cvss=9.8
)
原理很简单:如果Redis没有设置密码认证,发送INFO server命令就能获取服务器信息,返回中包含redis_version字段。这意味着攻击者可以直接执行任意Redis命令,包括写入crontab、SSH公钥等,实现远程代码执行。
类似地,MongoDB可以通过Wire Protocol发送listDatabases命令检测;Elasticsearch可以通过GET /获取集群信息;Kubernetes API可以通过/api/v1/namespaces获取命名空间列表;Docker API可以通过/version获取版本信息。
我们实现了20+种漏洞检测,覆盖Redis、MongoDB、Elasticsearch、Memcached、Kubernetes、Docker、ZooKeeper、CouchDB、Consul、etcd、MinIO、Hadoop YARN、Spark、Jupyter Notebook、ActiveMQ、RabbitMQ、Nacos、Prometheus、Grafana、Solr、Druid、Portainer等常见组件。
每种漏洞都关联了CVSS评分和详细描述,检测结果直接存储到数据库的vulns表中。
八、SQLite存储:轻量高效的本地数据库
所有扫描结果都存储在SQLite数据库中。选择SQLite而非MySQL/PostgreSQL的原因:
- 零配置:不需要安装额外的数据库服务,Python内置支持
- 单文件:整个数据库就是一个文件,方便备份和迁移
- 性能足够:对于千万级以下的数据量,SQLite的查询性能完全够用
- 并发支持:通过
check_same_thread=False配置,支持多线程读写
数据库包含三张核心表:
hosts表:存储每个开放端口的完整信息,包括IP、端口、服务、Banner、软件版本、地理位置、Web信息、漏洞统计等。以(ip, port)作为唯一约束,实现自动去重和更新。
vulns表:存储检测到的漏洞详情,包括漏洞ID、名称、严重级别、CVSS评分、证据等。
scan_history表:记录每次扫描的元数据,便于追踪扫描历史。
我们在多个字段上建立了索引(ip、port、service、country_code、cms、software),确保搜索查询的效率。
九、HTML可视化报告
report.py模块生成的HTML报告是整个工具的亮点之一。报告采用暗色主题设计,包含:
- 统计概览:开放端口数、独立IP数、服务类型数、各严重级别漏洞数
- 地图可视化:使用Leaflet.js在OpenStreetMap上标注所有发现的主机位置,点击标记可查看IP、端口、服务、国家信息
- 漏洞列表:按严重级别高亮显示,包含CVSS评分和漏洞描述
- 按服务分类:Tab切换不同服务类型的主机列表
- 搜索过滤:支持关键词实时搜索过滤表格内容
报告是纯静态HTML文件,不依赖后端服务,可以直接在浏览器中打开查看。
十、使用示例
安装和使用非常简单:
# 安装
git clone <repo> && cd net-search
pip install -r requirements.txt
# 扫描单个IP
python searcher.py --ip 192.168.1.1
# 扫描网段
python searcher.py --ip 10.0.0.0/24 --port 22,80,443,3306,6379
# 大规模扫描(500并发)
python searcher.py --ip 172.16.0.0/16 --threads 500
# 搜索结果
python searcher.py --search "nginx" --output json
python searcher.py --service redis --country CN --output html
# 查看数据库统计
python searcher.py --stats
命令行工具支持--ip(目标IP/CIDR/范围)、--port(端口)、--search(关键词搜索)、--service/--country/--cms(过滤条件)、--output(输出格式)、--threads(并发数)等参数。
十一、性能与局限
实测性能参考:
| 场景 | 规模 | 耗时 |
|---|---|---|
| 单IP常用端口 | 1×70 | ~3秒 |
| /24网段 | 254×70 | ~30秒 |
| /16网段Top20 | 65536×20 | ~5分钟 |
相比Shodan,我们的工具存在明显局限:
- 无分布式架构:单机运行,无法像Shodan那样持续扫描整个互联网
- 无持久化索引:SQLite适合中小规模数据,无法支撑Shodan那样的数十亿条记录
- 无历史快照:Shodan可以查看某个IP的历史变化,我们的工具只能记录当前状态
- 扫描被动性:需要主动触发扫描,不像Shodan那样持续自动更新
但对于安全研究、内部资产管理和渗透测试场景,这些局限并不影响工具的核心价值。
总结
通过4000+行Python代码,我们实现了一个功能完整的网络空间搜索引擎原型。它涵盖了端口扫描、Banner抓取、指纹识别、CMS检测、漏洞检测、地理定位、数据存储和可视化报告的完整技术栈。
这个项目不仅是一个实用的安全工具,更是一次深入理解网络协议、异步编程和安全检测技术的实践。每一个模块——从MySQL握手包解析到Redis INFO命令,从正则指纹匹配到异步信号量控制——都蕴含着网络空间安全领域的核心知识。
完整源码已开源,欢迎Star和PR。
评论