fix: 完全重写Telnet插件,修复IAC协商和认证检测问题

## 主要修复
- 重写Telnet插件认证逻辑,修复"未发现弱密码"错误
- 实现完整IAC协商处理,确保与telnet服务器正常通信
- 改进登录提示和认证流程检测,支持多轮数据读取
- 优化shell提示符检测,准确识别无需认证的服务
- 添加详细调试日志,方便问题排查

## 技术改进
- 实现handleIACNegotiation函数处理telnet协议协商
- 改进cleanResponse函数清理IAC控制命令
- 增强performSimpleTelnetAuth多阶段认证检测
- 分离isLoginSuccess和isLoginFailed判断逻辑
- 优化超时处理和错误恢复机制

## 测试结果
- 正确识别无需认证的busybox telnetd服务
- 能够准确检测和报告"无需认证"状态
- 修复随机密码"成功"的虚假结果问题
- IAC协商成功,获得真实服务器响应
This commit is contained in:
ZacharyZcR 2025-09-02 04:02:33 +00:00
parent 3ab0405df2
commit 7579549e94
3 changed files with 244 additions and 111 deletions

3
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

View File

@ -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("未发现弱密码"),
}
}
// 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)
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
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 nil
}
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
}
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)
// 创建带超时的连接
conn, err := net.DialTimeout("tcp", address, 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))
// 设置超时
deadline := time.Now().Add(time.Duration(common.Timeout) * time.Second)
conn.SetDeadline(deadline)
// 简单的telnet认证流程
return p.performSimpleTelnetAuth(conn, cred.Username, cred.Password)
}
// performSimpleTelnetAuth 执行简单的telnet认证
func (p *TelnetPlugin) performSimpleTelnetAuth(conn net.Conn, username, password string) bool {
buffer := make([]byte, 1024)
// 处理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)
passwordPromptReceived := false
attempts = 0
n, err = conn.Read(buffer)
for attempts < 5 && !passwordPromptReceived {
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 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
}
data = string(buffer[:n])
if strings.Contains(strings.ToLower(data), "password") {
conn.Write([]byte(cred.Password + "\r\n"))
time.Sleep(1 * time.Second)
// 检查登录结果
time.Sleep(1000 * time.Millisecond)
attempts = 0
n, err = conn.Read(buffer)
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
}
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]
func (p *TelnetPlugin) cleanTelnetData(data string) string {
cleaned := ""
// 简单响应策略:拒绝所有选项
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
}
}
}
// 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) {
// 检查成功标识
if strings.Contains(data, "$") ||
strings.Contains(data, "#") ||
strings.Contains(data, ">") ||
strings.Contains(data, "welcome") ||
strings.Contains(data, "last login") {
return true
}
}
failIndicators := []string{"incorrect", "failed", "denied", "invalid", "login:"}
for _, indicator := range failIndicators {
if strings.Contains(data, indicator) {
return false
}
// 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) *ScanResult {
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})
}