文件
websafe-kb/04-server-security/scanning/tools/port-scanner.py

292 行
8.3 KiB
Python

#!/usr/bin/env python3
"""
Port Scanner - 多线程端口扫描工具
支持:
- TCP Connect 扫描
- SYN 扫描 (需要 root)
- 服务指纹识别
- 多线程扫描
- 自定义端口范围
Usage:
python3 port-scanner.py -H 192.168.1.1 -p 1-1000
python3 port-scanner.py -H 192.168.1.1 -p 80,443,8080
python3 port-scanner.py -H 192.168.1.1 --top-ports 100
授权边界:
- 仅用于自有资产、测试环境或已明确授权的目标
- 允许公网验证,但建议缩小到明确主机和必要端口范围
- 不面向无授权第三方网站或泛互联网枚举
"""
import argparse
import socket
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Tuple, Optional
import sys
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from tool_contract import add_common_args, emit_report, ensure_authorized, make_report, write_evidence # noqa: E402
class Colors:
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
END = "\033[0m"
BOLD = "\033[1m"
class PortScanner:
def __init__(self, threads: int = 100, timeout: float = 1.0):
self.threads = threads
self.timeout = timeout
self.open_ports = []
self.lock = threading.Lock()
self.service_banners = {
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
135: "RPC",
139: "NetBIOS",
143: "IMAP",
443: "HTTPS",
445: "SMB",
993: "IMAPS",
995: "POP3S",
1433: "MSSQL",
1521: "Oracle",
3306: "MySQL",
3389: "RDP",
5432: "PostgreSQL",
5900: "VNC",
6379: "Redis",
8080: "HTTP-Proxy",
8443: "HTTPS-Alt",
8888: "HTTP-Alt",
9000: "PHP-FPM",
9200: "Elasticsearch",
27017: "MongoDB",
}
self.top_ports = [
21,
22,
23,
25,
53,
80,
110,
111,
135,
139,
143,
443,
445,
993,
995,
1433,
1434,
1723,
3306,
3389,
5432,
5900,
6379,
8000,
8080,
8443,
8888,
9000,
9090,
9200,
27017,
]
def print_result(self, level: str, msg: str):
colors = {
"INFO": Colors.BLUE,
"SUCCESS": Colors.GREEN,
"WARNING": Colors.YELLOW,
"ERROR": Colors.RED,
"OPEN": Colors.GREEN + Colors.BOLD,
}
print(f"{colors.get(level, '')}[{level}]{Colors.END} {msg}")
def parse_ports(self, port_str: str) -> List[int]:
"""解析端口字符串"""
ports = set()
for part in port_str.split(","):
if "-" in part:
start, end = part.split("-")
ports.update(range(int(start), int(end) + 1))
else:
ports.add(int(part))
return sorted(ports)
def scan_port(self, host: str, port: int) -> Tuple[int, str, Optional[str]]:
"""扫描单个端口"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
result = sock.connect_ex((host, port))
if result == 0:
service = self.service_banners.get(port, "Unknown")
banner = None
try:
sock.send(b"HEAD / HTTP/1.0\r\n\r\n")
banner = sock.recv(1024).decode("utf-8", errors="ignore").strip()
except:
pass
sock.close()
return port, "open", banner or service
sock.close()
except Exception:
pass
return port, "closed", None
def scan_host(
self, host: str, ports: List[int], verbose: bool = True
) -> List[Dict]:
"""扫描主机"""
results = []
total = len(ports)
if verbose:
self.print_result("INFO", f"开始扫描 {host}: {total} 个端口")
start_time = time.time()
with ThreadPoolExecutor(max_workers=self.threads) as executor:
futures = {
executor.submit(self.scan_port, host, port): port for port in ports
}
completed = 0
for future in as_completed(futures):
port, status, banner = future.result()
completed += 1
if verbose and completed % 100 == 0:
print(
f"\r{Colors.CYAN}[*]{Colors.END} 进度: {completed}/{total}",
end="",
flush=True,
)
if status == "open":
result = {
"port": port,
"status": status,
"service": self.service_banners.get(port, "Unknown"),
"banner": banner,
}
results.append(result)
with self.lock:
self.open_ports.append((host, port))
if verbose:
print(
f"\n{Colors.GREEN}[OPEN]{Colors.END} {host}:{port} - {banner[:50] if banner else self.service_banners.get(port, 'Unknown')}"
)
if verbose:
print()
elapsed = time.time() - start_time
if verbose:
self.print_result(
"INFO", f"扫描完成: {len(results)} 个开放端口, 耗时 {elapsed:.2f}s"
)
return results
def main():
parser = argparse.ArgumentParser(description="Port Scanner")
parser.add_argument("-H", "--host", required=True, help="目标主机")
parser.add_argument(
"-p", "--ports", default="1-1000", help="端口范围 (例: 1-1000, 80,443,8080)"
)
parser.add_argument("--top-ports", type=int, help="扫描最常用的 N 个端口")
parser.add_argument("-t", "--threads", type=int, default=100, help="线程数")
parser.add_argument("--timeout", type=float, default=1.0, help="超时时间")
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
add_common_args(parser, include_network=False)
args = parser.parse_args()
ensure_authorized(args, parser)
scanner = PortScanner(threads=args.threads, timeout=args.timeout)
if args.format != "text":
scanner.print_result = lambda *_args, **_kwargs: None # type: ignore[assignment]
if args.top_ports:
ports = scanner.top_ports[: args.top_ports]
else:
ports = scanner.parse_ports(args.ports)
scanner.print_result("INFO", f"目标: {args.host}")
scanner.print_result("INFO", f"端口: {len(ports)}")
scanner.print_result("INFO", f"线程: {args.threads}")
results = scanner.scan_host(args.host, ports, args.verbose)
evidence_refs = []
ref = write_evidence(args, "port-scan-results.json", {"results": results, "ports": ports})
if ref:
evidence_refs.append(ref)
status = "verified" if results else "needs-review"
severity = "medium" if results else "info"
report = make_report(
tool="port-scanner",
mode="minimal-port-scan",
target=args.host,
status=status,
severity=severity,
payload_or_probe={"ports": ports, "open_ports": results},
request_summary={"threads": args.threads, "timeout": args.timeout},
evidence_refs=evidence_refs,
destructive_risk="low",
args=args,
)
text_lines = [
"=" * 60,
"Port Scanner",
"=" * 60,
f"Target: {args.host}",
f"Ports Checked: {len(ports)}",
f"Open Ports: {len(results)}",
f"Status: {status}",
]
emit_report(args, report, text_lines)
if __name__ == "__main__":
main()