mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
feat: stream large host scans
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
const DefaultHostBatchSize = 4096
|
||||
|
||||
type HostIterator struct {
|
||||
sources []hostSource
|
||||
current hostSource
|
||||
exclude *hostMatcher
|
||||
}
|
||||
|
||||
func NewHostIterator(host string, filename string, nohosts ...string) (*HostIterator, error) {
|
||||
var sources []hostSource
|
||||
|
||||
if filename != "" {
|
||||
fileSrc, err := newFileHostSource(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, fileSrc)
|
||||
}
|
||||
|
||||
hostSources, err := newHostSources(host)
|
||||
if err != nil {
|
||||
closeHostSources(sources)
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, hostSources...)
|
||||
|
||||
matcher := newHostMatcher()
|
||||
if len(nohosts) > 0 && strings.TrimSpace(nohosts[0]) != "" {
|
||||
if err := matcher.add(nohosts[0]); err != nil {
|
||||
closeHostSources(sources)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &HostIterator{
|
||||
sources: sources,
|
||||
exclude: matcher,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (it *HostIterator) Close() error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
var firstErr error
|
||||
if it.current != nil {
|
||||
firstErr = it.current.Close()
|
||||
it.current = nil
|
||||
}
|
||||
for _, src := range it.sources {
|
||||
if err := src.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
it.sources = nil
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (it *HostIterator) Next() (string, bool, error) {
|
||||
for {
|
||||
if it.current == nil {
|
||||
if len(it.sources) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
it.current = it.sources[0]
|
||||
it.sources = it.sources[1:]
|
||||
}
|
||||
|
||||
host, ok, err := it.current.Next()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if !ok {
|
||||
if err := it.current.Close(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
it.current = nil
|
||||
continue
|
||||
}
|
||||
if it.exclude != nil && it.exclude.match(host) {
|
||||
continue
|
||||
}
|
||||
return host, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (it *HostIterator) NextBatch(ctx context.Context, size int) ([]string, error) {
|
||||
if size <= 0 {
|
||||
size = DefaultHostBatchSize
|
||||
}
|
||||
|
||||
batch := make([]string, 0, size)
|
||||
seen := make(map[string]struct{}, size)
|
||||
for len(batch) < size {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return batch, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
host, ok, err := it.Next()
|
||||
if err != nil {
|
||||
return batch, err
|
||||
}
|
||||
if !ok {
|
||||
return batch, nil
|
||||
}
|
||||
if _, exists := seen[host]; exists {
|
||||
continue
|
||||
}
|
||||
seen[host] = struct{}{}
|
||||
batch = append(batch, host)
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
type hostSource interface {
|
||||
Next() (string, bool, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type singleHostSource struct {
|
||||
host string
|
||||
done bool
|
||||
}
|
||||
|
||||
func (s *singleHostSource) Next() (string, bool, error) {
|
||||
if s.done {
|
||||
return "", false, nil
|
||||
}
|
||||
s.done = true
|
||||
return s.host, true, nil
|
||||
}
|
||||
|
||||
func (s *singleHostSource) Close() error { return nil }
|
||||
|
||||
type cidrHostSource struct {
|
||||
current uint32
|
||||
end uint32
|
||||
done bool
|
||||
}
|
||||
|
||||
func (s *cidrHostSource) Next() (string, bool, error) {
|
||||
if s.done || s.current > s.end {
|
||||
return "", false, nil
|
||||
}
|
||||
host := uint32ToIP(s.current)
|
||||
if s.current == s.end {
|
||||
s.done = true
|
||||
} else {
|
||||
s.current++
|
||||
}
|
||||
return host, true, nil
|
||||
}
|
||||
|
||||
func (s *cidrHostSource) Close() error { return nil }
|
||||
|
||||
type fileHostSource struct {
|
||||
file *os.File
|
||||
scanner *bufio.Scanner
|
||||
current hostSource
|
||||
}
|
||||
|
||||
func newFileHostSource(filename string) (*fileHostSource, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fileHostSource{
|
||||
file: file,
|
||||
scanner: bufio.NewScanner(file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *fileHostSource) Next() (string, bool, error) {
|
||||
for {
|
||||
if s.current != nil {
|
||||
host, ok, err := s.current.Next()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if ok {
|
||||
return host, true, nil
|
||||
}
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
|
||||
if !s.scanner.Scan() {
|
||||
if err := s.scanner.Err(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
line := strings.TrimSpace(s.scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
sources, err := newHostSources(line)
|
||||
if err != nil || len(sources) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(sources) == 1 {
|
||||
s.current = sources[0]
|
||||
continue
|
||||
}
|
||||
s.current = &multiHostSource{sources: sources}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fileHostSource) Close() error {
|
||||
if s.current != nil {
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
if s.file == nil {
|
||||
return nil
|
||||
}
|
||||
err := s.file.Close()
|
||||
s.file = nil
|
||||
return err
|
||||
}
|
||||
|
||||
type multiHostSource struct {
|
||||
sources []hostSource
|
||||
current hostSource
|
||||
}
|
||||
|
||||
func (s *multiHostSource) Next() (string, bool, error) {
|
||||
for {
|
||||
if s.current == nil {
|
||||
if len(s.sources) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
s.current = s.sources[0]
|
||||
s.sources = s.sources[1:]
|
||||
}
|
||||
host, ok, err := s.current.Next()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if ok {
|
||||
return host, true, nil
|
||||
}
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *multiHostSource) Close() error {
|
||||
if s.current != nil {
|
||||
_ = s.current.Close()
|
||||
s.current = nil
|
||||
}
|
||||
closeHostSources(s.sources)
|
||||
s.sources = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHostSources(host string) ([]hostSource, error) {
|
||||
var sources []hostSource
|
||||
for _, h := range strings.Split(host, ",") {
|
||||
h = strings.TrimSpace(h)
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
src, err := newHostSource(h)
|
||||
if err != nil {
|
||||
closeHostSources(sources)
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, src)
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func newHostSource(host string) (hostSource, error) {
|
||||
switch {
|
||||
case host == "192":
|
||||
return newCIDRHostSource("192.168.0.0/16")
|
||||
case host == "172":
|
||||
return newCIDRHostSource("172.16.0.0/12")
|
||||
case host == "10":
|
||||
return newCIDRHostSource("10.0.0.0/8")
|
||||
case strings.Contains(host, "/"):
|
||||
src, err := newCIDRHostSource(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(i18n.Tr("parser_cidr_failed", host)+": %w", err)
|
||||
}
|
||||
return src, nil
|
||||
case strings.Contains(host, "-") && !strings.Contains(host, ":") && looksLikeIPRange(host):
|
||||
src, err := newRangeHostSource(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(i18n.Tr("parser_ip_range_failed", host)+": %w", err)
|
||||
}
|
||||
return src, nil
|
||||
default:
|
||||
return &singleHostSource{host: host}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newCIDRHostSource(cidr string) (hostSource, error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start, ok := ipToUint32(ipNet.IP)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
ones, bits := ipNet.Mask.Size()
|
||||
if bits != 32 {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
size := uint64(1) << uint(32-ones)
|
||||
end := start + uint32(size-1)
|
||||
if size > 2 {
|
||||
start++
|
||||
end--
|
||||
}
|
||||
return &cidrHostSource{current: start, end: end}, nil
|
||||
}
|
||||
|
||||
func newRangeHostSource(rangeStr string) (hostSource, error) {
|
||||
parts := strings.Split(rangeStr, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_range_fmt", rangeStr))
|
||||
}
|
||||
|
||||
startIPStr := strings.TrimSpace(parts[0])
|
||||
endIPStr := strings.TrimSpace(parts[1])
|
||||
startIP := net.ParseIP(startIPStr)
|
||||
if startIP == nil {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_start_ip", startIPStr))
|
||||
}
|
||||
|
||||
if len(endIPStr) < 4 || !strings.Contains(endIPStr, ".") {
|
||||
endNum, err := strconv.Atoi(endIPStr)
|
||||
if err != nil || endNum > 255 {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_end_val", endIPStr))
|
||||
}
|
||||
parts := strings.Split(startIPStr, ".")
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_ip_fmt", startIPStr))
|
||||
}
|
||||
parts[3] = strconv.Itoa(endNum)
|
||||
endIPStr = strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
start, ok := ipToUint32(startIP)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
end, ok := ipToUint32(net.ParseIP(endIPStr))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("parser_invalid_end_ip", endIPStr))
|
||||
}
|
||||
if start > end {
|
||||
return nil, fmt.Errorf("%s", i18n.GetText("parser_start_gt_end"))
|
||||
}
|
||||
return &cidrHostSource{current: start, end: end}, nil
|
||||
}
|
||||
|
||||
func closeHostSources(sources []hostSource) {
|
||||
for _, src := range sources {
|
||||
_ = src.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type hostMatcher struct {
|
||||
exact map[string]struct{}
|
||||
ranges []ipRange
|
||||
}
|
||||
|
||||
type ipRange struct {
|
||||
start uint32
|
||||
end uint32
|
||||
}
|
||||
|
||||
func newHostMatcher() *hostMatcher {
|
||||
return &hostMatcher{exact: make(map[string]struct{})}
|
||||
}
|
||||
|
||||
func (m *hostMatcher) add(input string) error {
|
||||
for _, h := range strings.Split(input, ",") {
|
||||
h = strings.TrimSpace(h)
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case h == "192":
|
||||
if err := m.addCIDR("192.168.0.0/16"); err != nil {
|
||||
return err
|
||||
}
|
||||
case h == "172":
|
||||
if err := m.addCIDR("172.16.0.0/12"); err != nil {
|
||||
return err
|
||||
}
|
||||
case h == "10":
|
||||
if err := m.addCIDR("10.0.0.0/8"); err != nil {
|
||||
return err
|
||||
}
|
||||
case strings.Contains(h, "/"):
|
||||
if err := m.addCIDR(h); err != nil {
|
||||
return err
|
||||
}
|
||||
case strings.Contains(h, "-") && !strings.Contains(h, ":") && looksLikeIPRange(h):
|
||||
if err := m.addRange(h); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
m.exact[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *hostMatcher) addCIDR(cidr string) error {
|
||||
src, err := newCIDRHostSource(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeSrc, ok := src.(*cidrHostSource)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
m.ranges = append(m.ranges, ipRange{start: rangeSrc.current, end: rangeSrc.end})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *hostMatcher) addRange(rangeStr string) error {
|
||||
src, err := newRangeHostSource(rangeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeSrc, ok := src.(*cidrHostSource)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s", i18n.GetText("parser_ipv4_only"))
|
||||
}
|
||||
m.ranges = append(m.ranges, ipRange{start: rangeSrc.current, end: rangeSrc.end})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *hostMatcher) match(host string) bool {
|
||||
if _, ok := m.exact[host]; ok {
|
||||
return true
|
||||
}
|
||||
ip, ok := ipToUint32(net.ParseIP(host))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, r := range m.ranges {
|
||||
if ip >= r.start && ip <= r.end {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ipToUint32(ip net.IP) (uint32, bool) {
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return 0, false
|
||||
}
|
||||
return uint32(ip4[0])<<24 | uint32(ip4[1])<<16 | uint32(ip4[2])<<8 | uint32(ip4[3]), true
|
||||
}
|
||||
|
||||
func uint32ToIP(v uint32) string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostIteratorCIDRBatch(t *testing.T) {
|
||||
iter, err := NewHostIterator("192.168.1.0/30", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"192.168.1.1", "192.168.1.2"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIteratorDoesNotExpandWholeRangeAtOnce(t *testing.T) {
|
||||
iter, err := NewHostIterator("10", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 3)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIteratorExcludeCIDR(t *testing.T) {
|
||||
iter, err := NewHostIterator("192.168.1.0/29", "", "192.168.1.2-192.168.1.4")
|
||||
if err != nil {
|
||||
t.Fatalf("NewHostIterator error = %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
batch, err := iter.NextBatch(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("NextBatch error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"192.168.1.1", "192.168.1.5", "192.168.1.6"}
|
||||
if !reflect.DeepEqual(batch, want) {
|
||||
t.Fatalf("batch = %#v, want %#v", batch, want)
|
||||
}
|
||||
}
|
||||
+27
-25
@@ -63,52 +63,54 @@ func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSes
|
||||
|
||||
// 执行存活探测
|
||||
s.performAliveScan(ctx, info, session)
|
||||
|
||||
// 输出统计信息
|
||||
s.outputStats(session)
|
||||
}
|
||||
|
||||
// performAliveScan 执行存活探测
|
||||
func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) {
|
||||
// 解析目标主机
|
||||
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
iter, err := parsers.NewHostIterator(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
if err != nil {
|
||||
session.LogError(i18n.Tr("parse_target_failed", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = iter.Close()
|
||||
}()
|
||||
|
||||
if len(hosts) == 0 {
|
||||
s.stats.TotalHosts = 0
|
||||
s.stats.AliveHosts = 0
|
||||
s.stats.DeadHosts = 0
|
||||
|
||||
for {
|
||||
hosts, err := iter.NextBatch(ctx, targetHostBatchSize(session.Config))
|
||||
if err != nil {
|
||||
session.LogError(i18n.Tr("parse_target_failed", err))
|
||||
return
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
s.stats.TotalHosts += len(hosts)
|
||||
aliveList := CheckLive(ctx, hosts, false, session)
|
||||
s.stats.AliveHosts += len(aliveList)
|
||||
for _, host := range aliveList {
|
||||
session.LogSuccess(fmt.Sprintf("alive %s", host))
|
||||
}
|
||||
}
|
||||
|
||||
if s.stats.TotalHosts == 0 {
|
||||
session.LogError(i18n.GetText("parse_error_no_hosts"))
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化统计信息
|
||||
s.stats.TotalHosts = len(hosts)
|
||||
s.stats.AliveHosts = 0
|
||||
s.stats.DeadHosts = 0
|
||||
|
||||
// 执行存活检测
|
||||
aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测
|
||||
|
||||
// 更新统计信息
|
||||
s.stats.AliveHosts = len(aliveList)
|
||||
s.stats.DeadHosts = s.stats.TotalHosts - s.stats.AliveHosts
|
||||
s.stats.ScanDuration = time.Since(s.startTime)
|
||||
s.stats.AliveHostList = aliveList // 存储存活主机列表
|
||||
|
||||
if s.stats.TotalHosts > 0 {
|
||||
s.stats.SuccessRate = float64(s.stats.AliveHosts) / float64(s.stats.TotalHosts) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// outputStats 输出统计信息(精简版)
|
||||
func (s *AliveScanStrategy) outputStats(session *common.ScanSession) {
|
||||
// 只输出存活主机列表,不输出冗余统计
|
||||
for _, host := range s.stats.AliveHostList {
|
||||
session.LogSuccess(fmt.Sprintf("alive %s", host))
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareTargets 存活探测不需要准备扫描目标
|
||||
func (s *AliveScanStrategy) PrepareTargets(info common.HostInfo) []common.HostInfo {
|
||||
// 存活探测不需要返回目标列表,因为它不进行后续扫描
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
const maxHostBatchSize = 65536
|
||||
|
||||
func targetHostBatchSize(config *common.Config) int {
|
||||
size := parsers.DefaultHostBatchSize
|
||||
if config != nil && config.ThreadNum > 0 {
|
||||
threadWindow := config.ThreadNum * 8
|
||||
if threadWindow > size {
|
||||
size = threadWindow
|
||||
}
|
||||
}
|
||||
if size > maxHostBatchSize {
|
||||
return maxHostBatchSize
|
||||
}
|
||||
return size
|
||||
}
|
||||
+2
-2
@@ -437,7 +437,7 @@ func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) s
|
||||
info = append(info, fmt.Sprintf("Version:%s", serviceInfo.Version))
|
||||
}
|
||||
if len(info) > 0 {
|
||||
msg.WriteString(fmt.Sprintf(" [%s]", strings.Join(info, " ||")))
|
||||
fmt.Fprintf(&msg, " [%s]", strings.Join(info, " ||"))
|
||||
}
|
||||
|
||||
// Banner 信息
|
||||
@@ -446,7 +446,7 @@ func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) s
|
||||
if len(banner) > 80 {
|
||||
banner = banner[:80] + "..."
|
||||
}
|
||||
msg.WriteString(fmt.Sprintf(" Banner:(%s)", banner))
|
||||
fmt.Fprintf(&msg, " Banner:(%s)", banner)
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
|
||||
@@ -15,7 +15,7 @@ func BytesToRegexSafeString(b []byte) string {
|
||||
for _, c := range b {
|
||||
if c < 32 || c >= 128 {
|
||||
// 控制字符和高位字节转换为 \x{NN} 格式
|
||||
result.WriteString(fmt.Sprintf("\\x{%02x}", c))
|
||||
fmt.Fprintf(&result, "\\x{%02x}", c)
|
||||
} else {
|
||||
result.WriteByte(c)
|
||||
}
|
||||
|
||||
+55
-36
@@ -145,46 +145,76 @@ func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *comm
|
||||
config := session.Config
|
||||
state := session.State
|
||||
|
||||
// 解析目标主机
|
||||
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
iter, err := parsers.NewHostIterator(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||
if err != nil {
|
||||
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = iter.Close()
|
||||
}()
|
||||
|
||||
// 主机存活检测
|
||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||
hosts = CheckLive(ctx, hosts, false, session)
|
||||
session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||
pluginsToRun, isCustomMode := s.GetPlugins(config)
|
||||
totalAlive := 0
|
||||
sawHosts := false
|
||||
|
||||
for {
|
||||
hosts, err := iter.NextBatch(ctx, targetHostBatchSize(config))
|
||||
if err != nil {
|
||||
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||
return
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
break
|
||||
}
|
||||
sawHosts = true
|
||||
|
||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||
hosts = CheckLive(ctx, hosts, false, session)
|
||||
}
|
||||
totalAlive += len(hosts)
|
||||
if len(hosts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
s.dispatchUDPPlugins(ctx, session, hosts, info, config, ch, wg)
|
||||
s.scanHostBatch(ctx, session, hosts, info, pluginsToRun, isCustomMode, ch, wg)
|
||||
}
|
||||
|
||||
if len(hosts) == 0 && len(state.GetHostPorts()) == 0 {
|
||||
if sawHosts && s.shouldReportAliveCount(config) {
|
||||
session.LogInfo(i18n.Tr("alive_hosts_count_info", totalAlive))
|
||||
}
|
||||
|
||||
if !sawHosts && len(state.GetHostPorts()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// UDP 插件并行分发:直接对存活主机发协议探测包,不走端口扫描
|
||||
if len(hosts) > 0 {
|
||||
s.dispatchUDPPlugins(ctx, session, hosts, info, config, ch, wg)
|
||||
// 合并预设的 host:port
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
merged := mergeHostPorts(nil, hostPorts)
|
||||
targets := s.convertToTargetInfos(merged, info)
|
||||
for _, target := range targets {
|
||||
for _, pluginName := range pluginsToRun {
|
||||
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.ClearHostPorts()
|
||||
}
|
||||
}
|
||||
|
||||
// 流式 channel:端口扫描发现开放端口后立即通知插件执行
|
||||
func (s *ServiceScanStrategy) scanHostBatch(ctx context.Context, session *common.ScanSession, hosts []string, info common.HostInfo, pluginsToRun []string, isCustomMode bool, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
stream := make(chan string, 64)
|
||||
|
||||
// 启动端口扫描 goroutine
|
||||
go func() {
|
||||
if len(hosts) > 0 {
|
||||
EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream)
|
||||
} else {
|
||||
close(stream)
|
||||
}
|
||||
}()
|
||||
go EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream)
|
||||
|
||||
// pipeline 消费:边收开放端口边执行插件
|
||||
pluginsToRun, isCustomMode := s.GetPlugins(config)
|
||||
cancelled := false
|
||||
for addr := range stream {
|
||||
if cancelled {
|
||||
continue // ctx 已取消,排空 stream 防止写端阻塞
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -202,21 +232,10 @@ func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *comm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 合并预设的 host:port
|
||||
hostPorts := state.GetHostPorts()
|
||||
if len(hostPorts) > 0 {
|
||||
merged := mergeHostPorts(nil, hostPorts)
|
||||
targets := s.convertToTargetInfos(merged, info)
|
||||
for _, target := range targets {
|
||||
for _, pluginName := range pluginsToRun {
|
||||
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.ClearHostPorts()
|
||||
}
|
||||
func (s *ServiceScanStrategy) shouldReportAliveCount(config *common.Config) bool {
|
||||
return !config.DisablePing
|
||||
}
|
||||
|
||||
// dispatchUDPPlugins 分发UDP协议插件,跳过TCP端口扫描链路
|
||||
|
||||
@@ -57,10 +57,10 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co
|
||||
// 成功后重新连接获取文件列表
|
||||
fileList := p.getFileListAfterAuth(info, result.Username, result.Password, config, state)
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("FTP %s %s:%s", target, result.Username, result.Password))
|
||||
fmt.Fprintf(&output, "FTP %s %s:%s", target, result.Username, result.Password)
|
||||
if len(fileList) > 0 {
|
||||
for _, file := range fileList {
|
||||
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
||||
fmt.Fprintf(&output, "\n [->] %s", file)
|
||||
}
|
||||
}
|
||||
session.LogVuln(output.String())
|
||||
@@ -205,7 +205,7 @@ func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostIn
|
||||
output.WriteString(i18n.Tr("ftp_anonymous_access_detail", target, cred.Username, cred.Password))
|
||||
if len(fileList) > 0 {
|
||||
for _, file := range fileList {
|
||||
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
||||
fmt.Fprintf(&output, "\n [->] %s", file)
|
||||
}
|
||||
}
|
||||
session.LogVuln(output.String())
|
||||
|
||||
@@ -143,11 +143,11 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
|
||||
switch config.Shellcode {
|
||||
case "bind":
|
||||
output.WriteString("\n" + i18n.GetText("ms17010_exploit_bind_hint") + "\n")
|
||||
output.WriteString(fmt.Sprintf(" nc %s 64531\n", info.Host))
|
||||
fmt.Fprintf(&output, " nc %s 64531\n", info.Host)
|
||||
case "add":
|
||||
output.WriteString("\n" + i18n.GetText("ms17010_exploit_add_hint") + "\n")
|
||||
output.WriteString(i18n.GetText("ms17010_exploit_add_credential") + "\n")
|
||||
output.WriteString(fmt.Sprintf(" RDP: mstsc /v:%s\n", info.Host))
|
||||
fmt.Fprintf(&output, " RDP: mstsc /v:%s\n", info.Host)
|
||||
case "guest":
|
||||
output.WriteString("\n" + i18n.GetText("ms17010_exploit_guest_hint") + "\n")
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Hos
|
||||
}
|
||||
output.WriteString(i18n.Tr("smb_anonymous_access_detail", target, displayUser, cred.Password))
|
||||
for _, share := range shareInfo {
|
||||
output.WriteString(fmt.Sprintf("\n%s", share))
|
||||
fmt.Fprintf(&output, "\n%s", share)
|
||||
}
|
||||
|
||||
common.LogSuccess(output.String())
|
||||
|
||||
@@ -899,7 +899,7 @@ func CheckInfoPoc(infostr string) string {
|
||||
func GetHeader(header map[string]string) string {
|
||||
var builder strings.Builder
|
||||
for name, values := range header {
|
||||
builder.WriteString(fmt.Sprintf("%s: %s\n", name, values))
|
||||
fmt.Fprintf(&builder, "%s: %s\n", name, values)
|
||||
}
|
||||
builder.WriteString("\r\n")
|
||||
return builder.String()
|
||||
|
||||
Reference in New Issue
Block a user