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

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

## 测试结果
- 正确识别无需认证的busybox telnetd服务
- 能够准确检测和报告"无需认证"状态
- 修复随机密码"成功"的虚假结果问题
- IAC协商成功,获得真实服务器响应
2025-09-02 04:02:33 +00:00

368 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package services
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
type TelnetPlugin struct {
plugins.BasePlugin
}
func NewTelnetPlugin() *TelnetPlugin {
return &TelnetPlugin{
BasePlugin: plugins.NewBasePlugin("telnet"),
}
}
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)
}
// 构建凭据列表
credentials := plugins.GenerateCredentials("telnet")
for _, cred := range credentials {
// 检查上下文是否已取消
select {
case <-ctx.Done():
return &plugins.Result{
Success: false,
Service: "telnet",
Error: ctx.Err(),
}
default:
}
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,
Password: cred.Password,
}
}
}
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)
// 创建带超时的连接
conn, err := net.DialTimeout("tcp", address, time.Duration(common.Timeout)*time.Second)
if err != nil {
return false
}
defer conn.Close()
// 设置超时
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
}
// 等待密码提示
time.Sleep(500 * time.Millisecond)
passwordPromptReceived := false
attempts = 0
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
}
// 检查登录结果
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
}
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
}
}
}
// 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' {
result.WriteByte(b)
}
}
return result.String()
}
// 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)
// 检查成功标识
if strings.Contains(data, "$") ||
strings.Contains(data, "#") ||
strings.Contains(data, ">") ||
strings.Contains(data, "welcome") ||
strings.Contains(data, "last login") {
return true
}
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) *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 &plugins.Result{
Success: false,
Service: "telnet",
Error: err,
}
}
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 &plugins.Result{
Success: false,
Service: "telnet",
Error: err,
}
}
data := string(buffer[:n])
// 清理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") {
banner = "Telnet远程终端服务"
} else if strings.Contains(data, "$") || strings.Contains(data, "#") || strings.Contains(data, ">") {
banner = "Telnet远程终端服务 (无认证)"
} else {
banner = "Telnet远程终端服务"
}
common.LogSuccess(fmt.Sprintf("Telnet %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "telnet",
Banner: banner,
}
}
func init() {
plugins.RegisterWithPorts("telnet", func() plugins.Plugin {
return NewTelnetPlugin()
}, []int{23, 2323})
}