4. 信息收集与资产测绘
说实话,做Web安全测试这么多年,我最大的体会就是:数据清洗比漏洞挖掘更磨人。你想想看,从目标网站抓回来的原始HTML,那叫一个乱——标签嵌套、多余空格、隐藏字符,什么妖魔鬼怪都有。如果不做预处理,后续的分析根本没法看。
我个人习惯把数据清洗分成三步走:解析结构、提取关键、去重格式化。今天咱们就聊聊这三板斧。
一、HTML解析:用BeautifulSoup把乱麻理成线
原始HTML就像一团乱麻。BeautifulSoup就是那把梳子。它能帮我们把标签、属性、文本内容拆得清清楚楚。
核心思路:把HTML字符串转成可遍历的文档树,然后用选择器精准定位目标元素。
我常用的解析器是 html.parser,Python自带的,够用。如果遇到特别乱的页面,我会换 lxml,速度更快,容错性也更好。
from bs4 import BeautifulSoup
# 原始HTML(模拟从目标站点抓取的内容)
raw_html = """
<html>
<body>
<div class="vuln-item">
<span class="type">XSS</span>
<a href="/detail/123">反射型XSS漏洞</a>
<p class="desc">参数未过滤</p>
</div>
<div class="vuln-item">
<span class="type">SQLi</span>
<a href="/detail/456">SQL注入漏洞</a>
<p class="desc">未使用预编译</p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(raw_html, 'html.parser')
# 提取所有漏洞条目
items = soup.find_all('div', class_='vuln-item')
for item in items:
vuln_type = item.find('span', class_='type').text
vuln_name = item.find('a').text
vuln_desc = item.find('p', class_='desc').text
print(f'{vuln_type}: {vuln_name} - {vuln_desc}')
我的小技巧:用 soup.select() 配合CSS选择器,比 find_all 更简洁。比如 soup.select('div.vuln-item > a') 直接定位到链接。
二、正则表达式:从文本里“抠”出关键信息
BeautifulSoup擅长处理结构化标签,但有些信息藏在文本里。比如URL参数、IP地址、邮箱、或者特定的漏洞编号。这时候就得靠正则表达式了。
我记得有一次做渗透测试,目标站点的响应里混着大量日志信息。日志里藏着SQL语句和错误堆栈。用BeautifulSoup根本拿不到,因为那些文本不在标签里。最后我用正则把 SELECT.*FROM 和 Error:.* 的模式全捞出来了。
import re
# 模拟从响应中提取的混合文本
response_text = """
[2025-01-15] GET /search?q=test&page=1 HTTP/1.1
Error: You have an error in your SQL syntax; check the manual...
SELECT * FROM users WHERE id = 1 OR 1=1
Contact: admin@example.com
IP: 192.168.1.100
"""
# 提取SQL语句
sql_pattern = r'SELECT\s+.*?;'
sql_matches = re.findall(sql_pattern, response_text, re.IGNORECASE | re.DOTALL)
# 提取邮箱
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
email_matches = re.findall(email_pattern, response_text)
# 提取IP地址
ip_pattern = r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
ip_matches = re.findall(ip_pattern, response_text)
print(f'SQL语句: {sql_matches}')
print(f'邮箱: {email_matches}')
print(f'IP地址: {ip_matches}')
避坑指南:我曾经因为正则写得太贪婪,把整个页面内容都匹配进去了。后来养成了习惯——先写小范围测试,再放大。用 re.findall 之前,先用 re.search 验证一下模式对不对。
三、数据去重与格式化:让数据“规规矩矩”
从不同页面、不同接口抓回来的数据,经常有重复。比如同一个漏洞,在列表页和详情页各出现一次。如果不做去重,统计结果就虚高了。
我一般用两种方式去重:
- 基于哈希值:对每条记录计算MD5或SHA1,存到集合里。新记录如果哈希已存在,直接跳过。
- 基于关键字段:比如漏洞的URL+参数组合,作为唯一标识。
import hashlib
import json
# 模拟原始数据(有重复)
raw_data = [
{'url': '/detail/123', 'type': 'XSS', 'desc': '反射型XSS'},
{'url': '/detail/456', 'type': 'SQLi', 'desc': 'SQL注入'},
{'url': '/detail/123', 'type': 'XSS', 'desc': '反射型XSS'}, # 重复
{'url': '/detail/789', 'type': 'XSS', 'desc': '存储型XSS'},
]
# 方法1:基于哈希去重
seen_hashes = set()
unique_data = []
for item in raw_data:
# 将字典转为有序字符串,保证哈希一致
item_str = json.dumps(item, sort_keys=True)
item_hash = hashlib.md5(item_str.encode()).hexdigest()
if item_hash not in seen_hashes:
seen_hashes.add(item_hash)
unique_data.append(item)
print(f'去重前: {len(raw_data)} 条')
print(f'去重后: {len(unique_data)} 条')
# 方法2:基于关键字段去重(更灵活)
seen_urls = set()
unique_by_url = []
for item in raw_data:
# 用URL+类型作为唯一键
key = f"{item['url']}_{item['type']}"
if key not in seen_urls:
seen_urls.add(key)
unique_by_url.append(item)
print(f'基于URL去重后: {len(unique_by_url)} 条')
格式化这块,我习惯把数据统一成JSON或CSV。JSON适合后续程序处理,CSV方便导入Excel做人工审核。
import csv
# 将去重后的数据导出为CSV
with open('vulnerabilities.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['url', 'type', 'desc'])
writer.writeheader()
writer.writerows(unique_data)
print('数据已导出到 vulnerabilities.csv')
我的习惯:去重之后,我会再加一步字段标准化。比如把所有漏洞类型统一成大写(XSS、SQLI),描述里的多余空格去掉。这样后续做统计或生成报告时,不会因为大小写不一致而出错。
四、实战中的常见坑
嗯,这里要提醒几个我踩过的坑:
- 编码问题:有些页面是GBK编码,直接用BeautifulSoup解析会乱码。我一般先
response.encoding = 'utf-8'或者用chardet自动检测。 - 动态加载的内容:BeautifulSoup只能解析静态HTML。如果目标页面是JS渲染的,得先用Selenium或Playwright拿到渲染后的源码。
- 正则性能:别在长文本上反复用
re.findall。我习惯先截取关键区域,再跑正则。比如先找到<div id="content">到</div>之间的内容,再提取细节。 - 去重粒度:别只看URL。同一个URL可能因为参数不同而代表不同漏洞。我建议用
URL + 参数名 + 漏洞类型作为唯一键。
数据清洗这件事,说白了就是把脏活累活干漂亮。你想想看,如果原始数据里藏着10个漏洞,但因为没清洗干净只发现了8个,那后面的分析报告就全偏了。所以别嫌麻烦,这一步值得花时间。