diff --git a/go.mod b/go.mod index 17e096c..5f7e722 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( github.com/satori/go.uuid v1.2.0 github.com/schollz/progressbar/v3 v3.13.1 github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8 + github.com/ziutek/telnet v0.1.0 + go.mongodb.org/mongo-driver v1.17.4 golang.org/x/crypto v0.31.0 golang.org/x/net v0.32.0 golang.org/x/sync v0.10.0 @@ -63,7 +65,6 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.mongodb.org/mongo-driver v1.17.4 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 9162df2..c015c5a 100644 --- a/go.sum +++ b/go.sum @@ -30,7 +30,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo= @@ -86,7 +85,6 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -136,7 +134,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -146,6 +143,8 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/ziutek/telnet v0.1.0 h1:Fds2AqweYyoRHX/5X8ikiyqIcSl156Sf2xCvURfqXHA= +github.com/ziutek/telnet v0.1.0/go.mod h1:3M/h4qudUBZA8n+N4ywQIu2auiHUJNdqLUIKDAbG2M4= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -241,7 +240,6 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/plugins/services/telnet.go b/plugins/services/telnet.go index 600cb27..b22abbd 100644 --- a/plugins/services/telnet.go +++ b/plugins/services/telnet.go @@ -21,34 +21,31 @@ func NewTelnetPlugin() *TelnetPlugin { } } - - -func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { +func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) if common.DisableBrute { return p.identifyService(ctx, info) } - if result := p.testUnauthAccess(ctx, info); result != nil && result.Success { - common.LogSuccess(fmt.Sprintf("Telnet %s 未授权访问", target)) - return result - } - - credentials := GenerateCredentials("telnet") - if len(credentials) == 0 { - return &ScanResult{ - Success: false, - Service: "telnet", - Error: fmt.Errorf("没有可用的测试凭据"), - } - } - + // 构建凭据列表 + credentials := plugins.GenerateCredentials("telnet") + for _, cred := range credentials { - if p.testCredential(ctx, info, cred) { - common.LogSuccess(fmt.Sprintf("Telnet %s %s:%s", target, cred.Username, cred.Password)) + // 检查上下文是否已取消 + select { + case <-ctx.Done(): + return &plugins.Result{ + Success: false, + Service: "telnet", + Error: ctx.Err(), + } + default: + } - return &ScanResult{ + if p.testTelnetCredential(ctx, info, cred) { + common.LogSuccess(fmt.Sprintf("Telnet %s %s:%s", target, cred.Username, cred.Password)) + return &plugins.Result{ Success: true, Service: "telnet", Username: cred.Username, @@ -57,135 +54,259 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRes } } - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "telnet", Error: fmt.Errorf("未发现弱密码"), } } - -func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult { - target := fmt.Sprintf("%s:%s", info.Host, info.Ports) +// testTelnetCredential 测试telnet凭据 +func (p *TelnetPlugin) testTelnetCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool { + address := fmt.Sprintf("%s:%s", info.Host, info.Ports) - conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) + // 创建带超时的连接 + conn, err := net.DialTimeout("tcp", address, time.Duration(common.Timeout)*time.Second) if err != nil { - return nil + return false } defer conn.Close() - - conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - - buffer := make([]byte, 1024) - n, err := conn.Read(buffer) - if err != nil { - return nil - } - - welcome := string(buffer[:n]) - if strings.Contains(welcome, "$") || strings.Contains(welcome, "#") || strings.Contains(welcome, ">") { - return &ScanResult{ - Success: true, - Service: "telnet", - Banner: "未授权访问", - } - } - - return nil + // 设置超时 + deadline := time.Now().Add(time.Duration(common.Timeout) * time.Second) + conn.SetDeadline(deadline) + + // 简单的telnet认证流程 + return p.performSimpleTelnetAuth(conn, cred.Username, cred.Password) } -func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { - target := fmt.Sprintf("%s:%s", info.Host, info.Ports) - - conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) - if err != nil { - return false - } - defer conn.Close() - - conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) - +// performSimpleTelnetAuth 执行简单的telnet认证 +func (p *TelnetPlugin) performSimpleTelnetAuth(conn net.Conn, username, password string) bool { buffer := make([]byte, 1024) - n, err := conn.Read(buffer) + + // 处理IAC协商并等待真正的登录提示 + loginPromptReceived := false + attempts := 0 + maxAttempts := 10 // 最多尝试10次读取 + + for attempts < maxAttempts && !loginPromptReceived { + attempts++ + + // 设置较短的读取超时 + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := conn.Read(buffer) + if err != nil { + common.LogDebug(fmt.Sprintf("第%d次读取失败: %v", attempts, err)) + time.Sleep(200 * time.Millisecond) + continue + } + + response := string(buffer[:n]) + + // 处理IAC协商 + p.handleIACNegotiation(conn, buffer[:n]) + + // 清理响应 + cleaned := p.cleanResponse(response) + common.LogDebug(fmt.Sprintf("第%d次响应[%s:%s]: %q -> %q", attempts, username, password, response, cleaned)) + + // 检查是否为shell提示符(无需认证) + if p.isShellPrompt(cleaned) { + common.LogDebug(fmt.Sprintf("检测到shell提示符,无需认证")) + return true + } + + // 检查是否收到登录提示 + if strings.Contains(strings.ToLower(cleaned), "login") || + strings.Contains(strings.ToLower(cleaned), "username") || + strings.Contains(cleaned, ":") { // 简单的提示符检测 + loginPromptReceived = true + common.LogDebug(fmt.Sprintf("检测到登录提示")) + break + } + + time.Sleep(200 * time.Millisecond) + } + + if !loginPromptReceived { + common.LogDebug(fmt.Sprintf("未在%d次尝试中检测到登录提示", maxAttempts)) + return false + } + + // 发送用户名 + common.LogDebug(fmt.Sprintf("发送用户名: %s", username)) + _, err := conn.Write([]byte(username + "\r\n")) if err != nil { return false } - - data := string(buffer[:n]) - data = p.cleanTelnetData(data) - if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") { - conn.Write([]byte(cred.Username + "\r\n")) - time.Sleep(500 * time.Millisecond) + // 等待密码提示 + time.Sleep(500 * time.Millisecond) + passwordPromptReceived := false + attempts = 0 + + for attempts < 5 && !passwordPromptReceived { + attempts++ - n, err = conn.Read(buffer) + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := conn.Read(buffer) if err != nil { + common.LogDebug(fmt.Sprintf("读取密码提示第%d次失败: %v", attempts, err)) + time.Sleep(200 * time.Millisecond) + continue + } + + response := string(buffer[:n]) + cleaned := p.cleanResponse(response) + common.LogDebug(fmt.Sprintf("密码提示第%d次响应: %q -> %q", attempts, response, cleaned)) + + if strings.Contains(strings.ToLower(cleaned), "password") || + strings.Contains(cleaned, ":") { + passwordPromptReceived = true + common.LogDebug(fmt.Sprintf("检测到密码提示")) + break + } + + time.Sleep(200 * time.Millisecond) + } + + if !passwordPromptReceived { + common.LogDebug(fmt.Sprintf("未检测到密码提示")) + return false + } + + // 发送密码 + common.LogDebug(fmt.Sprintf("发送密码: %s", password)) + _, err = conn.Write([]byte(password + "\r\n")) + if err != nil { + return false + } + + // 检查登录结果 + time.Sleep(1000 * time.Millisecond) + attempts = 0 + + for attempts < 5 { + attempts++ + + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := conn.Read(buffer) + if err != nil { + common.LogDebug(fmt.Sprintf("读取登录结果第%d次失败: %v", attempts, err)) + time.Sleep(200 * time.Millisecond) + continue + } + + response := string(buffer[:n]) + cleaned := p.cleanResponse(response) + common.LogDebug(fmt.Sprintf("登录结果第%d次响应: %q -> %q", attempts, response, cleaned)) + + // 检查登录成功或失败 + if p.isLoginSuccess(cleaned) { + common.LogDebug(fmt.Sprintf("登录成功!")) + return true + } + + if p.isLoginFailed(cleaned) { + common.LogDebug(fmt.Sprintf("登录失败!")) return false } - data = string(buffer[:n]) - if strings.Contains(strings.ToLower(data), "password") { - conn.Write([]byte(cred.Password + "\r\n")) - time.Sleep(1 * time.Second) - - n, err = conn.Read(buffer) - if err != nil { - return false - } - - result := string(buffer[:n]) - result = p.cleanTelnetData(result) - - return p.isLoginSuccessful(result) - } + time.Sleep(200 * time.Millisecond) } - + + common.LogDebug(fmt.Sprintf("无法确定登录结果")) return false } +// handleIACNegotiation 处理IAC协商 +func (p *TelnetPlugin) handleIACNegotiation(conn net.Conn, data []byte) { + for i := 0; i < len(data); i++ { + if data[i] == 255 && i+2 < len(data) { // IAC + cmd := data[i+1] + opt := data[i+2] + + // 简单响应策略:拒绝所有选项 + switch cmd { + case 251: // WILL + // 回应DONT + conn.Write([]byte{255, 254, opt}) + common.LogDebug(fmt.Sprintf("IAC响应: DONT %d", opt)) + case 253: // DO + // 回应WONT + conn.Write([]byte{255, 252, opt}) + common.LogDebug(fmt.Sprintf("IAC响应: WONT %d", opt)) + } + i += 2 + } + } +} -func (p *TelnetPlugin) cleanTelnetData(data string) string { - cleaned := "" +// cleanResponse 清理telnet响应中的IAC命令 +func (p *TelnetPlugin) cleanResponse(data string) string { + var result strings.Builder for i := 0; i < len(data); i++ { b := data[i] + // 跳过IAC命令序列 if b == 255 && i+2 < len(data) { i += 2 continue } - if b >= 32 && b <= 126 || b == '\r' || b == '\n' { - cleaned += string(b) + // 保留可打印字符 + if (b >= 32 && b <= 126) || b == '\r' || b == '\n' { + result.WriteByte(b) } } - return cleaned + return result.String() } -func (p *TelnetPlugin) isLoginSuccessful(data string) bool { +// isShellPrompt 检查是否为shell提示符 +func (p *TelnetPlugin) isShellPrompt(data string) bool { + data = strings.ToLower(data) + return strings.Contains(data, "$") || + strings.Contains(data, "#") || + strings.Contains(data, ">") +} + +// isLoginSuccess 检查登录是否成功 +func (p *TelnetPlugin) isLoginSuccess(data string) bool { data = strings.ToLower(data) - successIndicators := []string{"$", "#", ">", "welcome", "last login"} - for _, indicator := range successIndicators { - if strings.Contains(data, indicator) { - return true - } - } - - failIndicators := []string{"incorrect", "failed", "denied", "invalid", "login:"} - for _, indicator := range failIndicators { - if strings.Contains(data, indicator) { - return false - } + // 检查成功标识 + if strings.Contains(data, "$") || + strings.Contains(data, "#") || + strings.Contains(data, ">") || + strings.Contains(data, "welcome") || + strings.Contains(data, "last login") { + return true } return false } -func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { +// isLoginFailed 检查登录是否失败 +func (p *TelnetPlugin) isLoginFailed(data string) bool { + data = strings.ToLower(data) + + // 检查失败标识 + if strings.Contains(data, "incorrect") || + strings.Contains(data, "failed") || + strings.Contains(data, "denied") || + strings.Contains(data, "invalid") || + strings.Contains(data, "login:") || // 重新出现登录提示 + strings.Contains(data, "username:") { + return true + } + + return false +} + +func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) if err != nil { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "telnet", Error: err, @@ -198,7 +319,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "telnet", Error: err, @@ -206,7 +327,21 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf } data := string(buffer[:n]) - data = p.cleanTelnetData(data) + // 清理telnet数据 + cleaned := "" + for i := 0; i < len(data); i++ { + b := data[i] + // 跳过telnet控制字符 + if b == 255 && i+2 < len(data) { + i += 2 + continue + } + // 只保留可打印字符和换行符 + if (b >= 32 && b <= 126) || b == '\r' || b == '\n' { + cleaned += string(b) + } + } + data = cleaned var banner string if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") { @@ -219,7 +354,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf common.LogSuccess(fmt.Sprintf("Telnet %s %s", target, banner)) - return &ScanResult{ + return &plugins.Result{ Success: true, Service: "telnet", Banner: banner, @@ -227,8 +362,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf } func init() { - // 使用高效注册方式:直接传递端口信息,避免实例创建 - RegisterPluginWithPorts("telnet", func() Plugin { + plugins.RegisterWithPorts("telnet", func() plugins.Plugin { return NewTelnetPlugin() }, []int{23, 2323}) -} +} \ No newline at end of file