mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

## 主要修复 - 重写Telnet插件认证逻辑,修复"未发现弱密码"错误 - 实现完整IAC协商处理,确保与telnet服务器正常通信 - 改进登录提示和认证流程检测,支持多轮数据读取 - 优化shell提示符检测,准确识别无需认证的服务 - 添加详细调试日志,方便问题排查 ## 技术改进 - 实现handleIACNegotiation函数处理telnet协议协商 - 改进cleanResponse函数清理IAC控制命令 - 增强performSimpleTelnetAuth多阶段认证检测 - 分离isLoginSuccess和isLoginFailed判断逻辑 - 优化超时处理和错误恢复机制 ## 测试结果 - 正确识别无需认证的busybox telnetd服务 - 能够准确检测和报告"无需认证"状态 - 修复随机密码"成功"的虚假结果问题 - IAC协商成功,获得真实服务器响应
368 lines
9.2 KiB
Go
368 lines
9.2 KiB
Go
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})
|
||
} |