fscan/plugins/services/telnet.go
ZacharyZcR a23c82142d refactor: 重构SMTP和Telnet插件使用统一发包控制
- 修改SMTP插件,在多个连接点添加发包控制
- 修改Telnet插件,在identifyService中使用SafeTCPDial包装器
- 保持现有功能不变,统一发包控制逻辑
2025-09-02 11:43:42 +00:00

380 lines
9.5 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)
// 检查发包限制
if canSend, reason := common.CanSendPacket(); !canSend {
common.LogError(fmt.Sprintf("Telnet连接 %s 受限: %s", address, reason))
return false
}
// 创建带超时的连接
conn, err := net.DialTimeout("tcp", address, time.Duration(common.Timeout)*time.Second)
if err == nil {
common.IncrementTCPSuccessPacketCount()
} else {
common.IncrementTCPFailedPacketCount()
}
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)
// 使用统一TCP包装器
conn, err := common.SafeTCPDial(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})
}