尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python实现的信息收集工具箱:域名解析、CDN检测与端口扫描

Python实现的信息收集工具箱:域名解析、CDN检测与端口扫描 简介这是一份基于Python开发的域名与网络资产信息收集工具源码涵盖域名解析、IP反查域名、WHOIS查询、CDN检测、端口扫描、目录扫描、子域名挖掘等常用功能适合安全测试人员、网络运维工程师及Python网络编程学习者参考实践。压缩包整体约157KB共10个文件以Python主程序为核心辅以YAML配置、字典文本、README说明及License许可文件代码结构简洁便于按模块阅读与二次开发。其中包含fuzz、subdomain等字典文件可直接用于目录探测与子域名枚举场景端口扫描模块基于nmap接口需配合本机nmap环境运行。目前已有344人学习使用对于希望快速搭建自己的域名信息收集工具箱或理解网络协议封装思路的读者来说是一份轻量且实用的入门参考。1. 基于Python实现的域名解析、IP反查域名、WHOIS查询、CDN检测、端口扫描、目录扫描、子域名挖掘到底在解决什么问题做过资产测绘或者安全评估的人都有同感拿到一个目标域名后第一轮信息收集通常要重复打开四五个工具DNS解析用一个、端口扫描用另一个、目录扫描再换一个中间结果靠手记或临时拼脚本。这套基于Python实现的域名解析、IP反查域名、WHOIS查询、CDN检测、端口扫描、目录扫描、子域名挖掘工具本质是把信息收集最常碰到的七个动作收敛到同一个命令行入口里输出统一格式的结果方便后续做关联分析和二次处理。它的价值不在于某个单点功能比专业工具强而在于你能在不受网络环境限制的前提下用一套代码把七类数据拉通A记录、PTR、WHOIS、CDN归属、开放端口、敏感目录、子域名集合。对Python开发者来说这也是一个很好的“网络编程 并发 协议交互”练习范本——socket、asyncio、dnspython、python-whois 这几个库会在同一份代码里协作。接下来从工程落地角度把每个模块怎么拆、参数怎么设、坑在哪里一条条讲清楚。2. 环境搭建与整体架构如何用最小依赖组织七合一工具2.1 依赖选型标准库为主第三方库只留三个先解决环境问题。Python版本建议3.10及以上主要用到asyncio、ipaddress、concurrent.futures等标准库不需要额外安装。第三方依赖建议控制在三个以内避免工具在目标机器上部署时被依赖问题卡住# 创建虚拟环境避免污染系统 Python python3 -m venv .venv source .venv/bin/activate # 安装三个核心依赖 pip install dnspython python-whois httpx参数说明dnspython负责A记录、CNAME、PTR等DNS记录查询用法和解析能力比socket原生函数强不少python-whois用来做WHOIS信息解析底层基于RFC 3912对.com/.net等主流后缀支持较好httpx用于目录扫描和CDN检测中的HTTP请求支持异步和超时控制。如果安装python-whois时遇到编译报错多半是setuptools版本问题先执行pip install --upgrade setuptools再装。不推荐用requests代替httpx的原因在于目录扫描通常要同时探测几百个路径httpx的异步客户端配合asyncio.Semaphore控制并发比requests搭配线程池的写法更省内存代码也更短。2.2 统一入口用 argparse 把七个模块拆成子命令一个工具如果七个功能混在一个脚本里参数维护是灾难。常见做法是用argparse的子命令模式让每个模块拥有独立参数同时保留“全量扫描”的联动入口。我把这套结构的骨架简化如下import argparse def cmd_dns(args): print(f[DNS] resolve {args.domain} - {args.type}) def cmd_ptr(args): print(f[PTR] reverse {args.ip}) def cmd_whois(args): print(f[WHOIS] query {args.domain}) def cmd_cdn(args): print(f[CDN] check {args.domain}) def cmd_port(args): print(f[PORT] scan {args.host} ports {args.ports}) def cmd_dir(args): print(f[DIR] scan {args.url} wordlist {args.wordlist}) def cmd_subdomain(args): print(f[SUBDOMAIN] brute {args.domain} dict {args.dict}) def main(): parser argparse.ArgumentParser(progrecon-cli, descriptionPython实现的信息收集工具箱) sub parser.add_subparsers(destmodule, requiredTrue) p1 sub.add_parser(dns, help域名解析) p1.add_argument(domain) p1.add_argument(--type, defaultA, choices[A, CNAME, MX, NS]) p1.set_defaults(funccmd_dns) p2 sub.add_parser(ptr, helpIP反查域名) p2.add_argument(ip) p2.set_defaults(funccmd_ptr) p3 sub.add_parser(whois, helpWHOIS查询) p3.add_argument(domain) p3.set_defaults(funccmd_whois) p4 sub.add_parser(cdn, helpCDN检测) p4.add_argument(domain) p4.set_defaults(funccmd_cdn) p5 sub.add_parser(port, help端口扫描) p5.add_argument(host) p5.add_argument(--ports, default21,22,23,25,53,80,110,135,139,143,443,445,993,995,1433,1521,3306,3389,5432,6379,8080,8443) p5.set_defaults(funccmd_port) p6 sub.add_parser(dir, help目录扫描) p6.add_argument(url) p6.add_argument(--wordlist, defaultdict_common.txt) p6.set_defaults(funccmd_dir) p7 sub.add_parser(subdomain, help子域名挖掘) p7.add_argument(domain) p7.add_argument(--dict, defaultsubnames.txt) p7.set_defaults(funccmd_subdomain) args parser.parse_args() args.func(args) if __name__ __main__: main()逻辑说明add_subparsers(destmodule, requiredTrue)让不同子命令分发到不同处理函数set_defaults(func...)是一个常用技巧把处理函数绑定到命名空间上parse之后直接调用。每个子命令的--ports、--wordlist、--dict都设计了默认值方便新手直接跑通生产使用时再按目标环境调整。这个结构的收益在于后续无论是加“结果落盘”还是“批量扫描”都只需在对应子命令里追加代码不会影响其他模块。实际开发时建议为“工具源码”项目建立modules/目录把 dns.py、port.py、subdomain.py 等按功能拆文件入口脚本只做调度。2.3 结果统一出口JSON 格式避免二次解析七个模块产出格式不统一的话后面做数据关联会非常痛苦。我一般会为每个模块定义一个scan()函数返回值统一为字典最后用json.dumps(ensure_asciiFalse, indent2)输出。这个设计配合后续的流水线串联能让端口扫描发现的高危端口、子域名挖掘出来的新域名自动作为下一轮扫描的输入目标。3. 域名解析、IP反查与WHOIS三个查询类模块的实现与坑3.1 域名解析socket 足够日常dnspython 才能看到完整记录先用简单方式实现A记录查询import socket def resolve_with_socket(domain): try: ip_list socket.getaddrinfo(domain, None) return sorted(set(item[4][0] for item in ip_list)) except socket.gaierror as e: return {error: str(e)}逻辑说明socket.getaddrinfo返回值是一个四元组列表第四个元素的第一个位置是IP字符串用集合去重后排序就能拿到域名对应的全部IP。这个方法的问题在于拿不到CNAME和MX对CDN检测来说信息不够。dnspython 的写法能看到更完整的链路import dns.resolver def resolve_full(domain, record_typeA): answers dns.resolver.resolve(domain, record_type, lifetime5) return [r.to_text() for r in answers]参数说明lifetime5是单次查询的超时时间单位秒。在内网DNS环境建议调大到10公网环境5秒足够。dns.resolver.resolve返回的每个r对象.to_text()能把A记录、CNAME、MX等不同类型的值统一转成字符串避免分别处理不同类型对象。这里有一个容易被忽略的坑dns.resolver默认读取系统/etc/resolv.conf的DNS配置如果你用公共DNS做基准测试需要显式指定nameserverresolver dns.resolver.Resolver() resolver.nameservers [8.8.8.8, 1.1.1.1] answers resolver.resolve(domain, A)实际使用时不建议硬编码DNS地址提供--dns参数让用户传入否则工具在本地位移的场景下比如公司内部DNS环境会拿到完全不同的解析结果。3.2 IP反查域名PTR记录是唯一标准答案反查域名的基础是PTR记录没有第三方接口能绕过它。实现方式import dns.reversename import dns.resolver def reverse_lookup(ip): # 构造反向查询名192.168.1.1 - 1.1.168.192.in-addr.arpa rev_name dns.reversename.from_address(ip) try: answers dns.resolver.resolve(rev_name, PTR, lifetime5) return [r.to_text().rstrip(.) for r in answers] except dns.resolver.NXDOMAIN: return {ip: ip, ptr: None, note: 无PTR记录} except dns.resolver.NoAnswer: return {ip: ip, ptr: None, note: 存在但无PTR记录}参数说明dns.reversename.from_address负责把IP转换成反向查询名省去手动拼接in-addr.arpa的麻烦。rstrip(.)去掉FQDN末尾的点保持输出美观。这个模块的坑在于IPv6的反查格式和IPv4不同IPv6用的是ip6.arpa域dns.reversename.from_address能自动识别协议版本但如果你手动拼字符串务必分协议处理。此外很多机房默认不配置PTR记录反查为空是常态工具输出里要保留“无记录”和“查询失败”两种状态别混为一谈。3.3 WHOIS查询python-whois 的解析限制和 RDAP 补充方案python-whois 的用法很简单import whois def query_whois(domain): try: w whois.query(domain) return { domain: w.name, registrar: w.registrar, creation_date: str(w.creation_date), expiration_date: str(w.expiration_date), name_servers: w.name_servers, status: w.status, org: getattr(w, org, None) } except Exception as e: return {error: str(e), domain: domain}逻辑说明whois.query返回的对象包含结构化属性getattr(w, org, None)是因为部分TLD的whois响应里没有org字段直接用w.org会抛AttributeError这里用getattr兜底。python-whois 的局限性在于对.com/.net之外的TLD如.org、.io、.cn的字段解析不完全可靠有时候creation_date会是列表而非字符串需要用str()强制转换。更规范的替代方案是RDAPRegistration Data Access Protocol直接HTTP GET请求即可不需要额外库curl -s https://rdap.org/domain/example.com | python3 -m json.tool说明RDAP是WHOIS的下一代协议输出是标准JSON字段结构比WHOIS文本解析稳定得多。如果工具面向全球域名建议同时启用两种查询方式默认走RDAP失败时回落到python-whois。4. CDN检测CNAME链分析、IP段比对、证书特征三维判断4.1 为什么不能只靠IP段判断CNAME链是更早的信号很多CDN检测方案只做了“解析IP是否在CDN厂商网段内”这一步但漏掉了DNS层更明显的特征——CNAME。接入CDN的域名解析结果通常是xxx.cloudfront.net或xxx.a.b.cdn.cloudflare.net这类CDN域名。从CNAME链入手能在IP比对之前就给出高置信度的判断。import dns.resolver def get_cname_chain(domain): chain [] current domain for _ in range(10): # 防止循环跳转限制最大链路长度 try: answers dns.resolver.resolve(current, CNAME, lifetime5) cname answers[0].to_text().rstrip(.) chain.append(cname) current cname except dns.resolver.NoAnswer: break except dns.resolver.NXDOMAIN: break return chain逻辑说明循环里每次查询当前域名的CNAME拿到后继续查下一个直到域名没有CNAME记录为止。加10次循环上限是因为少数配置错误的域名可能A和CNAME来回跳形成死循环。返回的链列表最后一项通常是真正的源站域名或直接挂在CDN节点下的记录。判断逻辑很直接如果CNAME链中任意一条记录的主域名落在已知CDN厂商域名集合内比如cloudfront.net、cdn.cloudflare.net、kunlun.com阿里云CDN、chinanetcenter.com网宿等直接判定为CDN返回命中厂商。这个集合建议单独放在cdn_domains.txt文件里方便后续补充。4.2 IP段比对用 ipaddress 标准库匹配厂商网段CNAME检测无法覆盖所有情况——某些CDN服务支持A记录直连回源或者CNAME被运营商劫持。这时候回到IP段比对import ipaddress # 模拟数据生产环境从 cdn_ip_ranges.txt 加载 CDN_RANGES { Cloudflare: [104.16.0.0/13, 172.64.0.0/13], Akamai: [104.64.0.0/10], 腾讯云CDN: [1.32.128.0/18] } def check_cdn_by_ip(ip_str): ip ipaddress.ip_address(ip_str) for vendor, cidrs in CDN_RANGES.items(): for cidr in cidrs: if ip in ipaddress.ip_network(cidr): return vendor return None参数说明ipaddress.ip_address负责校验IP格式并生成IP对象ipaddress.ip_network(cidr)把CIDR字符串转换成网络对象ip in network是标准库内置的包含判断性能和正确性都优于自己写位运算。CDN厂商的IP段文件怎么维护常见做法是定期从厂商官网下载公开的IP列表AWS、Cloudflare、阿里云等都有官方发布页或者从BGP数据源如RIPE Stat API拉取ASN对应的前缀。不建议手工罗列数据会过期。4.3 辅助特征TLS证书颁发者与HTTP响应头有时候IP段比对和CNAME链都查不出来比如域名用了国内小厂CDN或者源站本身就在云上。这时加两个辅助判断维度TLS证书的颁发者CDN厂商签发的证书通常是CN*.cloudfront.net或OLets Encrypt和源站自签证书差异明显HTTP响应头Server: cloudflare、Via: 1.1 a.b.c.d (Cloudflare)、X-Cache: HIT都是明确的CDN指纹import ssl import socket import json def check_cdn_by_cert(domain, port443, timeout5): try: ctx ssl.create_default_context() with socket.create_connection((domain, port), timeouttimeout) as sock: with ctx.wrap_socket(sock, server_hostnamedomain) as tls: cert tls.getpeercert() issuer dict(x[0] for x in cert.get(issuer, [])) return issuer.get(organizationName) or issuer.get(commonName) except Exception as e: return str(e)逻辑说明ssl.create_default_context创建带默认CA证书链的验证上下文wrap_socket完成TLS握手getpeercert()返回证书的OpenSSL格式字典。issuer字段是嵌套元组结构用dict(x[0] for x in ...)扁平化后取组织名。CDN检测综合判断的策略先跑CNAME链命中即返回否则解析全部IP逐个比对IP段仍无结果再看证书和响应头。三个维度全部跑完再说“疑似CDN”而非“确定CDN”这个分寸在报告里要体现出来。5. 端口扫描与目录扫描并发参数、超时设置、结果过滤5.1 端口扫描TCP Connect asyncio 并发控制用Python写端口扫描核心是并发和超时控制。asyncio.open_connection是底层socket的异步封装配合Semaphore控制同时打开的连接数避免触发目标防火墙的封禁策略import asyncio async def scan_port(host, port, timeout2.0): try: reader, writer await asyncio.wait_for( asyncio.open_connection(host, port), timeouttimeout ) # 尝试读取banner非阻塞 try: banner await asyncio.wait_for(reader.read(1024), timeout1.0) return port, open, banner.decode(errorsreplace).strip()[:200] except Exception: return port, open, finally: writer.close() await writer.wait_closed() except asyncio.TimeoutError: return port, filtered, except ConnectionRefusedError: return port, closed, async def port_scan(host, ports, max_concurrency200): sem asyncio.Semaphore(max_concurrency) results [] async def bounded(p): async with sem: return await scan_port(host, p) tasks [bounded(p) for p in ports] for coro in asyncio.as_completed(tasks): result await coro if result[1] ! closed: results.append(result) return results参数说明timeout2.0是连接超时局域网扫描可以降到0.3秒公网扫描建议2到3秒max_concurrency200是最大并发连接数目标机器性能差或网络延迟高时调小到50。asyncio.as_completed按完成顺序返回结果比gather更早产出有数据的端口。Python原生能实现的是TCP Connect扫描无法发SYN半开扫描包需要raw socket权限通常要用Scapy或系统nmap工具配合。对大多数服务发现场景TCP Connect足够。端口列表建议开放给用户自定义同时内置常见端口表21/22/23/25/53/80/110/135/139/143/443/445/993/995/1433/1521/3306/3389/5432/6379/8080/8443。5.2 目录扫描状态码过滤与重定向处理目录扫描的代码核心是并发HTTP请求和状态码判断。用httpx异步客户端import asyncio import httpx async def dir_scan(base_url, wordlist, max_concurrency20, timeout5): async with httpx.AsyncClient( timeouttimeout, follow_redirectsFalse, headers{User-Agent: Mozilla/5.0 (compatible; ReconBot/1.0)} ) as client: sem asyncio.Semaphore(max_concurrency) results [] async def check(path): async with sem: try: resp await client.get(base_url path) if resp.status_code in (200, 204, 301, 302, 403): return { path: path, status: resp.status_code, size: len(resp.content), location: resp.headers.get(location, ) } except Exception: pass return None paths [line.strip() for line in open(wordlist, encodingutf-8, errorsignore)] tasks [check(p) for p in paths] for coro in asyncio.as_completed(tasks): r await coro if r: results.append(r) return results参数说明follow_redirectsFalse很关键——很多目录扫描器会跟着302跳转结果把首页内容当成目录内容造成大量重复误报。这里保留301/302并记录location字段由用户判断是否跟进。max_concurrency20是保守值无CDN防护的站点可以调高到50遇到WAF必须降到5以下否则触发封IP。字典从哪里来常见的路径字典几万个词条不建议直接硬编码在代码里。开源字典如FuzzDB的top500和backup.txt可以按需取用放到独立文件里命令行传入路径。这个设计和--wordlist参数对应。5.3 两个扫描的“误判源”排查端口扫描和目录扫描合在一起跑最常见的干扰是CDN和负载均衡。域名解析到CDN节点后端口扫描看到的是CDN的端口而非源站端口目录扫描打到CDN缓存节点返回的页面可能全是200。遇到这种情况先对真实IP做端口扫描再对域名做目录扫描结果分开存放别混在同一个表里。超时参数同样需要区分场景目录扫描的超时不应低于5秒因为某些后端接口要渲染模板端口扫描的超时和并发数直接相关并发越高单个连接的超时要越低否则最长扫描时间会被拖到并发数 * 超时时间 / 端口数这个量级。6. 子域名挖掘与全量流水线最后一公里的实用技巧6.1 子域名挖掘字典爆破 crt.sh 被动收集双通道子域名挖掘两类方法建议都实现字典爆破用dnspython做A/CNAME查询命中即记录被动收集查询证书透明日志一天能捞到海量子域名且不产生主动流量。被动收集只需一条命令curl -s https://crt.sh/?q%25.example.comoutputjson | python3 -c import sys,json; datajson.load(sys.stdin); print(\n.join(sorted(set(i[name_value] for i in data if name_value in i))))参数说明%25是%的URL编码表示匹配任意子域名前缀outputjson让crt.sh返回结构化数据name_value字段里可能有*.example.com这类泛解析条目需要在处理时过滤掉。字典爆破部分的代码逻辑import asyncio import dns.asyncresolver async def brute_subdomain(domain, wordlist, concurrency50): resolver dns.asyncresolver.Resolver() resolver.nameservers [8.8.8.8, 1.1.1.1] sem asyncio.Semaphore(concurrency) found [] async def query(sub): async with sem: name f{sub}.{domain} try: answers await resolver.resolve(name, A, lifetime3) ips [r.to_text() for r in answers] found.append({subdomain: name, ips: ips}) except Exception: pass words [w.strip() for w in open(wordlist, encodingutf-8, errorsignore) if w.strip()] await asyncio.gather(*(query(w) for w in words)) return found逻辑说明dns.asyncresolver.Resolver是dnspython 2.x对asyncio的原生支持不需要额外线程池。resolver.resolve的lifetime3单独控制单次查询超时避免某个查询卡住整个任务。这里需要注意泛解析wildcard DNS会让所有不存在的子域名都返回IP导致结果全是噪声。应对方法是在爆破前先查一个随机字符串子域名如果它也有解析记录就判定目标存在泛解析此时需要对结果做“二次验证”——把拿到的IP和随机子域名的IP比对相同的直接丢弃。6.2 把七个模块串成一条命令中间结果落盘与断点续跑工具做到这一步每个模块独立跑没问题了但真正的效率提升在于串联。我会在入口脚本里增加--all参数用一条python recon-cli.py collect --domain example.com跑完整个流程def cmd_collect(args): domain args.domain results {} # 1. 基础解析 results[dns] resolve_full(domain) # 2. CDN检测 results[cdn] check_cdn(domain) # 3. 子域名挖掘 subs run_subdomain_brute(domain) results[subdomains] subs # 4. 对子域名逐个做端口扫描和目录扫描 for sub in subs: results[sub[subdomain]] { ports: run_port_scan(sub[ips][0]), dirs: run_dir_scan(fhttp://{sub[subdomain]}/) } # 5. 结果落盘 with open(f{domain}_recon.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)流水线设计上的关键点全量扫描可能耗时很长一定要把中间结果实时写入JSON文件而不是等全部跑完再写。断点续跑的技巧是扫描前先检查目标文件是否存在存在则跳过该阶段。具体实现可以在输出文件名里加上模块名比如example.com_subdomains.json下次运行时检测到文件存在就加载缓存数据只扫描缺失的模块。最后一个建议输出目录里加上report.md的生成逻辑把各阶段结果整理成可读的清单方便直接贴到工作记录里。整个工具的生命周期不在“跑一次”而在“跑完能复现、能增量扩展”——把子域名挖掘的产出自动喂给端口扫描和目录扫描当输入这才是信息收集工具链的完整闭环。本文还有配套的精品资源点击获取
返回列表