mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-26 21:21:53 +08:00
fix(proxy): 修复代理模式下服务识别错误和端口漏扫问题
- port_scan.go: 验证通过后重建干净连接,避免HTTP GET探测污染服务识别 - port_scan.go: 优化验证策略,用轻量CRLF探测替代HTTP GET,超时从2.2s降至0.6s - manager.go: 修正ProbeProxyBehavior判断逻辑,超时应视为代理正常转发
This commit is contained in:
+11
-11
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -368,6 +367,13 @@ func (s *socks5Dialer) updateAverageConnectTime(duration time.Duration) {
|
|||||||
// ProbeProxyBehavior 探测代理是否存在"全回显"问题
|
// ProbeProxyBehavior 探测代理是否存在"全回显"问题
|
||||||
// 通过连接一个几乎肯定不可达的地址,并尝试发送数据来判断代理行为
|
// 通过连接一个几乎肯定不可达的地址,并尝试发送数据来判断代理行为
|
||||||
// 返回 true 表示代理可靠,false 表示代理存在全回显问题
|
// 返回 true 表示代理可靠,false 表示代理存在全回显问题
|
||||||
|
//
|
||||||
|
// 判断标准:
|
||||||
|
// - 连接失败 → 可靠(代理正确拒绝不可达目标)
|
||||||
|
// - 写入失败 → 可靠(代理在数据传输时报告错误)
|
||||||
|
// - 读取超时 → 可靠(代理转发了请求,目标没响应是正常的)
|
||||||
|
// - 读取错误 → 可靠(代理正确报告了目标不可达)
|
||||||
|
// - 收到数据 → 不可靠(代理伪造了响应)
|
||||||
func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
|
func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
|
||||||
// 使用 RFC 5737 保留的测试 IP (TEST-NET-1) + 高端口
|
// 使用 RFC 5737 保留的测试 IP (TEST-NET-1) + 高端口
|
||||||
// 192.0.2.1 是文档专用地址,保证不会路由到真实主机
|
// 192.0.2.1 是文档专用地址,保证不会路由到真实主机
|
||||||
@@ -402,14 +408,8 @@ func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
|
|||||||
n, readErr := conn.Read(buf)
|
n, readErr := conn.Read(buf)
|
||||||
|
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
// 读取超时或错误 = 目标不可达,但代理行为正常
|
// 读取超时或错误 = 目标不可达,代理行为正常
|
||||||
// 说明代理没有伪造响应
|
// 超时说明代理正确转发了请求,目标没有响应是正常的
|
||||||
errStr := readErr.Error()
|
|
||||||
if strings.Contains(errStr, "timeout") {
|
|
||||||
// 超时可能意味着代理接受了连接但没有伪造响应
|
|
||||||
// 这是边界情况,暂时认为不可靠
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 其他错误(reset, refused等)说明代理正确报告了目标不可达
|
// 其他错误(reset, refused等)说明代理正确报告了目标不可达
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -419,6 +419,6 @@ func ProbeProxyBehavior(dialer Dialer, timeout time.Duration) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代理接受连接且不报错也不响应 = 全回显行为
|
// 无错误且无数据 = EOF,说明连接被正常关闭,代理行为正常
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-22
@@ -109,6 +109,63 @@ func (f *failedPortCollector) Count() int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preResolveDomains 预解析域名列表
|
||||||
|
// 将域名解析为 IP,解析失败的域名保留原样
|
||||||
|
// 这样可以避免扫描过程中大量并发 DNS 查询导致系统资源耗尽
|
||||||
|
func preResolveDomains(hosts []string, timeout time.Duration) []string {
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有需要解析的域名
|
||||||
|
needResolve := false
|
||||||
|
for _, host := range hosts {
|
||||||
|
if net.ParseIP(host) == nil {
|
||||||
|
needResolve = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needResolve {
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("[DNS] 开始预解析 %d 个主机", len(hosts)))
|
||||||
|
|
||||||
|
result := make([]string, 0, len(hosts))
|
||||||
|
resolved := 0
|
||||||
|
failed := 0
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
// 如果已经是 IP,直接添加
|
||||||
|
if net.ParseIP(host) != nil {
|
||||||
|
result = append(result, host)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析域名
|
||||||
|
ips, err := net.LookupHost(host)
|
||||||
|
if err != nil || len(ips) == 0 {
|
||||||
|
// 解析失败,保留原域名(后续连接时会再次尝试解析)
|
||||||
|
common.LogDebug(fmt.Sprintf("[DNS] 解析失败: %s - %v", host, err))
|
||||||
|
result = append(result, host)
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用第一个解析结果
|
||||||
|
result = append(result, ips[0])
|
||||||
|
resolved++
|
||||||
|
common.LogDebug(fmt.Sprintf("[DNS] %s -> %s", host, ips[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolved > 0 || failed > 0 {
|
||||||
|
common.LogDebug(fmt.Sprintf("[DNS] 预解析完成: 成功=%d, 失败=%d", resolved, failed))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// estimateScanTime 估算扫描时间
|
// estimateScanTime 估算扫描时间
|
||||||
// 参数: totalTasks - 总任务数, threads - 线程数, timeout - 超时时间(秒)
|
// 参数: totalTasks - 总任务数, threads - 线程数, timeout - 超时时间(秒)
|
||||||
// 返回: 估算的扫描时间(秒)
|
// 返回: 估算的扫描时间(秒)
|
||||||
@@ -134,12 +191,18 @@ func estimateScanTime(totalTasks int, threads int, timeout int64) int64 {
|
|||||||
// EnhancedPortScan 高性能端口扫描函数
|
// EnhancedPortScan 高性能端口扫描函数
|
||||||
// 使用滑动窗口调度 + 自适应线程池 + 流式迭代器
|
// 使用滑动窗口调度 + 自适应线程池 + 流式迭代器
|
||||||
func EnhancedPortScan(hosts []string, ports string, timeout int64, config *common.Config, state *common.State) []string {
|
func EnhancedPortScan(hosts []string, ports string, timeout int64, config *common.Config, state *common.State) []string {
|
||||||
|
common.LogDebug(fmt.Sprintf("[PortScan] 开始: %d个主机, 线程数=%d", len(hosts), config.ThreadNum))
|
||||||
|
|
||||||
|
// 预解析域名,避免扫描过程中大量 DNS 查询导致问题
|
||||||
|
hosts = preResolveDomains(hosts, time.Duration(timeout)*time.Second)
|
||||||
|
|
||||||
// 解析端口和排除端口
|
// 解析端口和排除端口
|
||||||
portList := parsers.ParsePort(ports)
|
portList := parsers.ParsePort(ports)
|
||||||
if len(portList) == 0 {
|
if len(portList) == 0 {
|
||||||
common.LogError(i18n.Tr("invalid_port", ports))
|
common.LogError(i18n.Tr("invalid_port", ports))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
|
||||||
|
|
||||||
// 使用config中的排除端口配置
|
// 使用config中的排除端口配置
|
||||||
excludePorts := parsers.ParsePort(config.Target.ExcludePorts)
|
excludePorts := parsers.ParsePort(config.Target.ExcludePorts)
|
||||||
@@ -156,16 +219,28 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
|||||||
// 创建流式迭代器(O(1) 内存,端口喷洒策略)
|
// 创建流式迭代器(O(1) 内存,端口喷洒策略)
|
||||||
iter := NewSocketIterator(hosts, portList, exclude)
|
iter := NewSocketIterator(hosts, portList, exclude)
|
||||||
totalTasks := iter.Total()
|
totalTasks := iter.Total()
|
||||||
|
common.LogDebug(fmt.Sprintf("[PortScan] 总任务数: %d", totalTasks))
|
||||||
|
|
||||||
// 使用传入的配置
|
// 使用传入的配置
|
||||||
threadNum := config.ThreadNum
|
threadNum := config.ThreadNum
|
||||||
|
|
||||||
|
// 大规模扫描警告和线程数自动调整
|
||||||
|
if totalTasks > 100000 {
|
||||||
|
common.LogBase(fmt.Sprintf("[*] 大规模扫描: %d 个目标 (%d主机 × %d端口)", totalTasks, len(hosts), len(portList)))
|
||||||
|
// 如果任务数超过100万且线程数大于300,自动降低线程数
|
||||||
|
if totalTasks > 1000000 && threadNum > 300 {
|
||||||
|
oldThreadNum := threadNum
|
||||||
|
threadNum = 300
|
||||||
|
common.LogBase(fmt.Sprintf("[*] 自动调整线程数: %d -> %d (大规模扫描优化)", oldThreadNum, threadNum))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化端口扫描进度条
|
// 初始化端口扫描进度条
|
||||||
if totalTasks > 0 && config.Output.ShowProgress {
|
if totalTasks > 0 && config.Output.ShowProgress {
|
||||||
description := fmt.Sprintf("端口扫描中(%d线程)", threadNum)
|
description := fmt.Sprintf("端口扫描中(%d线程)", threadNum)
|
||||||
common.InitProgressBar(int64(totalTasks), description)
|
common.InitProgressBar(int64(totalTasks), description)
|
||||||
}
|
}
|
||||||
|
common.LogDebug("[PortScan] 进度条初始化完成")
|
||||||
|
|
||||||
// 初始化并发控制
|
// 初始化并发控制
|
||||||
to := time.Duration(timeout) * time.Second
|
to := time.Duration(timeout) * time.Second
|
||||||
@@ -174,6 +249,7 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
|||||||
failedCollector := &failedPortCollector{}
|
failedCollector := &failedPortCollector{}
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("[PortScan] 开始创建线程池, size=%d", threadNum))
|
||||||
// 创建自适应线程池(支持动态调整)
|
// 创建自适应线程池(支持动态调整)
|
||||||
pool, err := NewAdaptivePool(threadNum, func(task interface{}) {
|
pool, err := NewAdaptivePool(threadNum, func(task interface{}) {
|
||||||
taskInfo, ok := task.(portScanTask)
|
taskInfo, ok := task.(portScanTask)
|
||||||
@@ -193,10 +269,13 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
|||||||
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
common.LogDebug("[PortScan] 线程池创建成功")
|
||||||
defer pool.Release()
|
defer pool.Release()
|
||||||
|
|
||||||
|
common.LogDebug("[PortScan] 开始滑动窗口调度")
|
||||||
// 滑动窗口调度:维护固定数量的"飞行中"任务
|
// 滑动窗口调度:维护固定数量的"飞行中"任务
|
||||||
slidingWindowSchedule(iter, pool, &wg, threadNum)
|
slidingWindowSchedule(iter, pool, &wg, threadNum)
|
||||||
|
common.LogDebug("[PortScan] 滑动窗口调度完成")
|
||||||
|
|
||||||
// 收集结果
|
// 收集结果
|
||||||
aliveAddrs := collector.GetAll()
|
aliveAddrs := collector.GetAll()
|
||||||
@@ -361,6 +440,18 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 步骤1.6:如果使用了代理且进行了数据交互,需要重建连接
|
||||||
|
// 因为验证阶段可能读取了Banner或发送了HTTP GET探测,污染了连接状态
|
||||||
|
if common.IsProxyEnabled() && verifyMethod != "direct" {
|
||||||
|
_ = conn.Close()
|
||||||
|
// 重新建立干净的连接用于服务识别
|
||||||
|
conn, err = connectWithRetry(addr, timeout, 3, state)
|
||||||
|
if err != nil {
|
||||||
|
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 步骤2:记录开放端口
|
// 步骤2:记录开放端口
|
||||||
atomic.AddInt64(count, 1)
|
atomic.AddInt64(count, 1)
|
||||||
collector.Add(addr)
|
collector.Add(addr)
|
||||||
@@ -387,62 +478,60 @@ func isTimeoutError(err error) bool {
|
|||||||
return err != nil && strings.Contains(err.Error(), "i/o timeout")
|
return err != nil && strings.Contains(err.Error(), "i/o timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用(4阶段验证)
|
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用
|
||||||
// 防止透明代理/全回显代理的假连接问题
|
// 防止透明代理/全回显代理的假连接问题
|
||||||
// 返回: (是否有效, 验证方式)
|
// 返回: (是否有效, 验证方式)
|
||||||
|
//
|
||||||
|
// 优化策略:
|
||||||
|
// 1. 快速 Banner 检测 (100ms) - 大部分服务会主动发送数据
|
||||||
|
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
||||||
|
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
||||||
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||||
// 如果没有使用代理,跳过验证
|
// 如果没有使用代理,跳过验证
|
||||||
if !common.IsProxyEnabled() {
|
if !common.IsProxyEnabled() {
|
||||||
return true, "direct"
|
return true, "direct"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段1: 读取 Banner (200ms)
|
|
||||||
// 某些服务会主动发送欢迎消息
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
|
||||||
buf := make([]byte, 256)
|
buf := make([]byte, 256)
|
||||||
|
|
||||||
|
// 阶段1: 快速读取 Banner (100ms)
|
||||||
|
// 大部分服务(SSH、FTP、SMTP、MySQL等)会主动发送欢迎消息
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||||
n, _ := conn.Read(buf)
|
n, _ := conn.Read(buf)
|
||||||
_ = conn.SetReadDeadline(time.Time{}) // 重置 deadline
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
// 收到数据,检查是否为代理错误响应
|
|
||||||
if isProxyErrorResponse(buf[:n]) {
|
if isProxyErrorResponse(buf[:n]) {
|
||||||
common.LogDebug(fmt.Sprintf("代理返回错误响应 %s", addr))
|
common.LogDebug(fmt.Sprintf("代理返回错误响应 %s", addr))
|
||||||
return false, "proxy_error"
|
return false, "proxy_error"
|
||||||
}
|
}
|
||||||
// 收到有效 Banner,确认端口开放
|
|
||||||
return true, "banner"
|
return true, "banner"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段2: 发送探测数据 (HTTP GET with Host header)
|
// 阶段2: 轻量探测 - 发送 CRLF 触发响应
|
||||||
// 使用完整的 HTTP 请求以获得服务器响应
|
// 使用 \r\n 而非 HTTP GET,因为:
|
||||||
// 从 addr 提取主机名作为 Host 头(HTTP 服务器需要正确的 Host 才会响应)
|
// - 不会污染大部分协议状态
|
||||||
host := addr
|
// - 某些服务(如 HTTP)会返回 400 Bad Request
|
||||||
if colonIdx := strings.LastIndex(addr, ":"); colonIdx > 0 {
|
// - 速度快,只需极短超时
|
||||||
host = addr[:colonIdx]
|
|
||||||
}
|
|
||||||
probeReq := fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", host)
|
|
||||||
_ = conn.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
|
_ = conn.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
|
||||||
_, writeErr := conn.Write([]byte(probeReq))
|
_, writeErr := conn.Write([]byte("\r\n"))
|
||||||
_ = conn.SetWriteDeadline(time.Time{}) // 重置 deadline
|
_ = conn.SetWriteDeadline(time.Time{})
|
||||||
|
|
||||||
if writeErr != nil && isConnectionClosed(writeErr) {
|
if writeErr != nil && isConnectionClosed(writeErr) {
|
||||||
// 写入失败(broken pipe/reset)= 连接已被拒绝
|
|
||||||
common.LogDebug(fmt.Sprintf("探测写入失败 %s: %v", addr, writeErr))
|
common.LogDebug(fmt.Sprintf("探测写入失败 %s: %v", addr, writeErr))
|
||||||
return false, "write_failed"
|
return false, "write_failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段3: 等待探测响应 (2s - 需要足够时间让 HTTP 服务器响应)
|
// 阶段3: 等待探测响应 (500ms)
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
_ = conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||||
n, readErr := conn.Read(buf)
|
n, readErr := conn.Read(buf)
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
// 收到响应,检查是否为代理错误
|
|
||||||
if isProxyErrorResponse(buf[:n]) {
|
if isProxyErrorResponse(buf[:n]) {
|
||||||
common.LogDebug(fmt.Sprintf("代理探测返回错误 %s", addr))
|
common.LogDebug(fmt.Sprintf("代理探测返回错误 %s", addr))
|
||||||
return false, "proxy_error"
|
return false, "proxy_error"
|
||||||
}
|
}
|
||||||
// 收到有效响应,确认端口开放
|
|
||||||
return true, "probe"
|
return true, "probe"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user