diff --git a/Common/i18n/messages/plugins.go b/Common/i18n/messages/plugins.go index 2dc5044..3f3bab0 100644 --- a/Common/i18n/messages/plugins.go +++ b/Common/i18n/messages/plugins.go @@ -882,4 +882,22 @@ var PluginMessages = map[string]map[string]string{ LangZH: "SNMP认证失败: %v", LangEN: "SNMP authentication failed: %v", }, + + // ========================= Telnet插件消息 ========================= + "telnet_weak_password_success": { + LangZH: "Telnet弱密码: %s 用户名:%s 密码:%s", + LangEN: "Telnet weak password: %s username:%s password:%s", + }, + "telnet_unauthorized_access": { + LangZH: "Telnet无需认证: %s", + LangEN: "Telnet unauthorized access: %s", + }, + "telnet_connection_failed": { + LangZH: "Telnet连接失败: %v", + LangEN: "Telnet connection failed: %v", + }, + "telnet_auth_failed": { + LangZH: "Telnet认证失败: %v", + LangEN: "Telnet authentication failed: %v", + }, } \ No newline at end of file diff --git a/Core/Registry.go b/Core/Registry.go index 4e818a0..d4b3bdf 100644 --- a/Core/Registry.go +++ b/Core/Registry.go @@ -27,6 +27,7 @@ import ( _ "github.com/shadow1ng/fscan/plugins/services/smtp" _ "github.com/shadow1ng/fscan/plugins/services/snmp" _ "github.com/shadow1ng/fscan/plugins/services/ssh" + _ "github.com/shadow1ng/fscan/plugins/services/telnet" ) // ============================================================================= diff --git a/Plugins/services/telnet/connector.go b/Plugins/services/telnet/connector.go new file mode 100644 index 0000000..b53e5a7 --- /dev/null +++ b/Plugins/services/telnet/connector.go @@ -0,0 +1,523 @@ +package telnet + +import ( + "bytes" + "context" + "fmt" + "net" + "regexp" + "strings" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// TelnetConnector Telnet服务连接器 +type TelnetConnector struct{} + +// NewTelnetConnector 创建新的Telnet连接器 +func NewTelnetConnector() *TelnetConnector { + return &TelnetConnector{} +} + +// Connect 连接到Telnet服务 +func (c *TelnetConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 创建TCP连接 + conn, err := common.WrapperTcpWithContext(ctx, "tcp", target) + if err != nil { + return nil, fmt.Errorf(i18n.GetText("telnet_connection_failed"), err) + } + + // 创建Telnet客户端 + client := &TelnetClient{ + IPAddr: info.Host, + Port: info.Ports, + conn: conn, + } + + // 初始化连接 + client.init() + + return client, nil +} + +// Authenticate 认证Telnet服务 +func (c *TelnetConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + client, ok := conn.(*TelnetClient) + if !ok { + return fmt.Errorf("invalid connection type") + } + + // 检查上下文是否已取消 + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // 设置凭据 + client.UserName = cred.Username + client.Password = cred.Password + + // 判断服务器类型 + client.ServerType = client.MakeServerType() + + // 处理无需认证的情况 + if client.ServerType == UnauthorizedAccess { + return nil // 认证成功 + } + + // 尝试登录 + return client.Login() +} + +// Close 关闭连接 +func (c *TelnetConnector) Close(conn interface{}) error { + if client, ok := conn.(*TelnetClient); ok && client != nil { + client.Close() + } + return nil +} + +// TelnetClient Telnet客户端结构体 +type TelnetClient struct { + IPAddr string // 服务器IP地址 + Port string // 服务器端口 + UserName string // 用户名 + Password string // 密码 + conn net.Conn // 网络连接 + LastResponse string // 最近一次响应内容 + ServerType int // 服务器类型 +} + +// init 初始化Telnet连接 +func (c *TelnetClient) init() { + // 启动后台goroutine处理服务器响应 + go func() { + for { + // 读取服务器响应 + buf, err := c.read() + if err != nil { + // 处理连接关闭和EOF情况 + if strings.Contains(err.Error(), "closed") || + strings.Contains(err.Error(), "EOF") { + break + } + break + } + + // 处理响应数据 + displayBuf, commandList := c.SerializationResponse(buf) + + if len(commandList) > 0 { + // 有命令需要回复 + replyBuf := c.MakeReplyFromList(commandList) + c.LastResponse += string(displayBuf) + _ = c.write(replyBuf) + } else { + // 仅保存显示内容 + c.LastResponse += string(displayBuf) + } + } + }() + + // 等待连接初始化完成 + time.Sleep(time.Second * 2) +} + +// WriteContext 写入数据到Telnet连接 +func (c *TelnetClient) WriteContext(s string) { + // 写入字符串并添加回车及空字符 + _ = c.write([]byte(s + "\x0d\x00")) +} + +// ReadContext 读取Telnet连接返回的内容 +func (c *TelnetClient) ReadContext() string { + // 读取完成后清空缓存 + defer func() { c.Clear() }() + + // 等待响应 + if c.LastResponse == "" { + time.Sleep(time.Second) + } + + // 处理特殊字符 + c.LastResponse = strings.ReplaceAll(c.LastResponse, "\x0d\x00", "") + c.LastResponse = strings.ReplaceAll(c.LastResponse, "\x0d\x0a", "\n") + + return c.LastResponse +} + +// Close 关闭Telnet连接 +func (c *TelnetClient) Close() { + if c.conn != nil { + c.conn.Close() + } +} + +// SerializationResponse 解析Telnet响应数据 +func (c *TelnetClient) SerializationResponse(responseBuf []byte) (displayBuf []byte, commandList [][]byte) { + for { + // 查找IAC命令标记 + index := bytes.IndexByte(responseBuf, IAC) + if index == -1 || len(responseBuf)-index < 2 { + displayBuf = append(displayBuf, responseBuf...) + break + } + + // 获取选项字符 + ch := responseBuf[index+1] + + // 处理连续的IAC + if ch == IAC { + displayBuf = append(displayBuf, responseBuf[:index]...) + responseBuf = responseBuf[index+1:] + continue + } + + // 处理DO/DONT/WILL/WONT命令 + if ch == DO || ch == DONT || ch == WILL || ch == WONT { + commandBuf := responseBuf[index : index+3] + commandList = append(commandList, commandBuf) + displayBuf = append(displayBuf, responseBuf[:index]...) + responseBuf = responseBuf[index+3:] + continue + } + + // 处理子协商命令 + if ch == SB { + displayBuf = append(displayBuf, responseBuf[:index]...) + seIndex := bytes.IndexByte(responseBuf, SE) + if seIndex != -1 && seIndex > index { + commandList = append(commandList, responseBuf[index:seIndex+1]) + responseBuf = responseBuf[seIndex+1:] + continue + } + } + + break + } + + return displayBuf, commandList +} + +// MakeReplyFromList 处理命令列表并生成回复 +func (c *TelnetClient) MakeReplyFromList(list [][]byte) []byte { + var reply []byte + for _, command := range list { + reply = append(reply, c.MakeReply(command)...) + } + return reply +} + +// MakeReply 根据命令生成对应的回复 +func (c *TelnetClient) MakeReply(command []byte) []byte { + // 命令至少需要3字节 + if len(command) < 3 { + return []byte{} + } + + verb := command[1] // 动作类型 + option := command[2] // 选项码 + + // 处理回显(ECHO)和抑制继续进行(SGA)选项 + if option == ECHO || option == SGA { + switch verb { + case DO: + return []byte{IAC, WILL, option} + case DONT: + return []byte{IAC, WONT, option} + case WILL: + return []byte{IAC, DO, option} + case WONT: + return []byte{IAC, DONT, option} + case SB: + // 处理子协商命令 + if len(command) >= 4 { + modifier := command[3] + if modifier == ECHO { + return []byte{IAC, SB, option, BINARY, IAC, SE} + } + } + } + } else { + // 处理其他选项 - 拒绝所有请求 + switch verb { + case DO, DONT: + return []byte{IAC, WONT, option} + case WILL, WONT: + return []byte{IAC, DONT, option} + } + } + + return []byte{} +} + +// read 从Telnet连接读取数据 +func (c *TelnetClient) read() ([]byte, error) { + var buf [2048]byte + // 设置读取超时为2秒 + _ = c.conn.SetReadDeadline(time.Now().Add(time.Second * 2)) + n, err := c.conn.Read(buf[0:]) + if err != nil { + return nil, err + } + return buf[:n], nil +} + +// write 向Telnet连接写入数据 +func (c *TelnetClient) write(buf []byte) error { + // 设置写入超时 + _ = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 3)) + + _, err := c.conn.Write(buf) + if err != nil { + return err + } + // 写入后短暂延迟,让服务器有时间处理 + time.Sleep(TIME_DELAY_AFTER_WRITE) + return nil +} + +// Login 根据服务器类型执行登录 +func (c *TelnetClient) Login() error { + switch c.ServerType { + case Closed: + return fmt.Errorf("service is disabled") + case UnauthorizedAccess: + return nil + case OnlyPassword: + return c.LogBaserOnlyPassword() + case UsernameAndPassword: + return c.LogBaserUsernameAndPassword() + default: + return fmt.Errorf("unknown server type") + } +} + +// MakeServerType 通过分析服务器响应判断服务器类型 +func (c *TelnetClient) MakeServerType() int { + responseString := c.ReadContext() + + // 空响应情况 + if responseString == "" { + return Closed + } + + response := strings.Split(responseString, "\n") + if len(response) == 0 { + return Closed + } + + lastLine := strings.ToLower(response[len(response)-1]) + + // 检查是否需要用户名和密码 + if containsAny(lastLine, []string{"user", "name", "login", "account", "用户名", "登录"}) { + return UsernameAndPassword + } + + // 检查是否只需要密码 + if strings.Contains(lastLine, "pass") { + return OnlyPassword + } + + // 检查是否无需认证的情况 + if isNoAuthRequired(lastLine) || c.isLoginSucceed(responseString) { + return UnauthorizedAccess + } + + return Closed +} + +// LogBaserOnlyPassword 处理只需密码的登录 +func (c *TelnetClient) LogBaserOnlyPassword() error { + c.Clear() // 清空之前的响应 + + // 发送密码并等待响应 + c.WriteContext(c.Password) + time.Sleep(time.Second * 2) + + // 验证登录结果 + responseString := c.ReadContext() + if c.isLoginFailed(responseString) { + return fmt.Errorf("login failed") + } + if c.isLoginSucceed(responseString) { + return nil + } + + return fmt.Errorf("login failed") +} + +// LogBaserUsernameAndPassword 处理需要用户名和密码的登录 +func (c *TelnetClient) LogBaserUsernameAndPassword() error { + // 发送用户名 + c.WriteContext(c.UserName) + time.Sleep(time.Second * 2) + c.Clear() + + // 发送密码 + c.WriteContext(c.Password) + time.Sleep(time.Second * 3) + + // 验证登录结果 + responseString := c.ReadContext() + if c.isLoginFailed(responseString) { + return fmt.Errorf("login failed") + } + if c.isLoginSucceed(responseString) { + return nil + } + + return fmt.Errorf("login failed") +} + +// Clear 清空最近一次响应 +func (c *TelnetClient) Clear() { + c.LastResponse = "" +} + +// 登录失败的关键词列表 +var loginFailedString = []string{ + "wrong", + "invalid", + "fail", + "incorrect", + "error", +} + +// isLoginFailed 检查是否登录失败 +func (c *TelnetClient) isLoginFailed(responseString string) bool { + responseString = strings.ToLower(responseString) + + // 空响应视为失败 + if responseString == "" { + return true + } + + // 检查失败关键词 + for _, str := range loginFailedString { + if strings.Contains(responseString, str) { + return true + } + } + + // 检查是否仍在要求输入凭证 + patterns := []string{ + "(?is).*pass(word)?:$", + "(?is).*user(name)?:$", + "(?is).*login:$", + } + for _, pattern := range patterns { + if regexp.MustCompile(pattern).MatchString(responseString) { + return true + } + } + + return false +} + +// isLoginSucceed 检查是否登录成功 +func (c *TelnetClient) isLoginSucceed(responseString string) bool { + // 空响应视为失败 + if responseString == "" { + return false + } + + // 获取最后一行响应 + lines := strings.Split(responseString, "\n") + if len(lines) == 0 { + return false + } + + lastLine := lines[len(lines)-1] + + // 检查命令提示符 + if regexp.MustCompile("^[#$>].*").MatchString(lastLine) || + regexp.MustCompile("^<[a-zA-Z0-9_]+>.*").MatchString(lastLine) { + return true + } + + // 检查last login信息 + if regexp.MustCompile("(?:s)last login").MatchString(responseString) { + return true + } + + // 发送测试命令验证 + c.Clear() + c.WriteContext("?") + time.Sleep(time.Second * 2) + responseString = c.ReadContext() + + // 检查响应长度 + if strings.Count(responseString, "\n") > 6 || len([]rune(responseString)) > 100 { + return true + } + + return false +} + +// 辅助函数:检查字符串是否包含任意给定子串 +func containsAny(s string, substrings []string) bool { + for _, sub := range substrings { + if strings.Contains(s, sub) { + return true + } + } + return false +} + +// 辅助函数:检查是否无需认证 +func isNoAuthRequired(line string) bool { + patterns := []string{ + `^/ #.*`, + `^<[A-Za-z0-9_]+>`, + `^#`, + } + + for _, pattern := range patterns { + if regexp.MustCompile(pattern).MatchString(line) { + return true + } + } + return false +} + +// Telnet协议常量定义 +const ( + // 写入操作后的延迟时间 + TIME_DELAY_AFTER_WRITE = 300 * time.Millisecond + + // Telnet基础控制字符 + IAC = byte(255) // 解释为命令(Interpret As Command) + DONT = byte(254) // 请求对方停止执行某选项 + DO = byte(253) // 请求对方执行某选项 + WONT = byte(252) // 拒绝执行某选项 + WILL = byte(251) // 同意执行某选项 + + // 子协商相关控制字符 + SB = byte(250) // 子协商开始(Subnegotiation Begin) + SE = byte(240) // 子协商结束(Subnegotiation End) + + // 特殊功能字符 + NULL = byte(0) // 空字符 + EOF = byte(236) // 文档结束 + SUSP = byte(237) // 暂停进程 + ABORT = byte(238) // 停止进程 + REOR = byte(239) // 记录结束 + + // Telnet选项代码 + BINARY = byte(0) // 8位数据通道 + ECHO = byte(1) // 回显 + SGA = byte(3) // 禁止继续 + + // 服务器类型常量定义 + Closed = iota // 连接关闭 + UnauthorizedAccess // 无需认证 + OnlyPassword // 仅需密码 + UsernameAndPassword // 需要用户名和密码 +) \ No newline at end of file diff --git a/Plugins/services/telnet/exploiter.go b/Plugins/services/telnet/exploiter.go new file mode 100644 index 0000000..a7eeac8 --- /dev/null +++ b/Plugins/services/telnet/exploiter.go @@ -0,0 +1,25 @@ +package telnet + +import ( + "context" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// TelnetExploiter Telnet服务利用器 +// 遵循新架构设计模式,当前为空实现 +type TelnetExploiter struct{} + +// NewTelnetExploiter 创建新的Telnet利用器 +func NewTelnetExploiter() *TelnetExploiter { + return &TelnetExploiter{} +} + +// Exploit 执行Telnet服务利用 +// 当前为空实现,遵循其他插件的一致性设计 +func (e *TelnetExploiter) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + // 空实现 - 遵循新架构中其他服务插件的模式 + // 主要功能集中在连接器和插件主体中实现 + return nil, nil +} \ No newline at end of file diff --git a/Plugins/services/telnet/plugin.go b/Plugins/services/telnet/plugin.go new file mode 100644 index 0000000..a32cc5a --- /dev/null +++ b/Plugins/services/telnet/plugin.go @@ -0,0 +1,184 @@ +package telnet + +import ( + "context" + "fmt" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// TelnetPlugin Telnet服务插件 +type TelnetPlugin struct { + *base.ServicePlugin + exploiter *TelnetExploiter +} + +// NewTelnetPlugin 创建Telnet插件 +func NewTelnetPlugin() *TelnetPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "telnet", + Version: "2.0.0", + Author: "fscan-team", + Description: "Telnet远程终端协议服务检测和弱口令扫描", + Category: "service", + Ports: []int{23}, // Telnet默认端口 + Protocols: []string{"tcp"}, + Tags: []string{"telnet", "remote-access", "weak-password", "unauthorized-access"}, + } + + // 创建连接器和服务插件 + connector := NewTelnetConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建Telnet插件 + plugin := &TelnetPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewTelnetExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapUnauthorized, + }) + + return plugin +} + +// init 自动注册Telnet插件 +func init() { + // 创建插件工厂 + metadata := &base.PluginMetadata{ + Name: "telnet", + Version: "2.0.0", + Author: "fscan-team", + Description: "Telnet远程终端协议服务检测和弱口令扫描", + Category: "service", + Ports: []int{23}, + Protocols: []string{"tcp"}, + Tags: []string{"telnet", "remote-access", "weak-password", "unauthorized-access"}, + } + + factory := base.NewSimplePluginFactory(metadata, func() base.Plugin { + return NewTelnetPlugin() + }) + + base.GlobalPluginRegistry.Register("telnet", factory) +} + +// Scan 重写扫描方法,进行Telnet服务扫描 +func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 如果禁用了暴力破解,只进行服务识别 + if common.DisableBrute { + return p.performServiceIdentification(ctx, info) + } + + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 生成凭据进行暴力破解 + credentials := p.generateCredentials() + if len(credentials) == 0 { + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("no credentials available"), + }, nil + } + + // 遍历凭据进行测试 + for _, cred := range credentials { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 检查是否无需认证 + if result.Extra != nil && result.Extra["type"] == "unauthorized-access" { + common.LogSuccess(i18n.GetText("telnet_unauthorized_access", target)) + } else { + // 认证成功 + common.LogSuccess(i18n.GetText("telnet_weak_password_success", target, cred.Username, cred.Password)) + } + + return &base.ScanResult{ + Success: true, + Service: "Telnet", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "Telnet", + "port": info.Ports, + "username": cred.Username, + "password": cred.Password, + "type": "weak-password", + }, + }, nil + } + } + + // 没有找到有效凭据 + return &base.ScanResult{ + Success: false, + Service: "Telnet", + Error: fmt.Errorf("authentication failed for all credentials"), + }, nil +} + +// performServiceIdentification 执行服务识别 +func (p *TelnetPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 尝试连接到服务进行基本识别 + conn, err := p.GetServiceConnector().Connect(ctx, info) + if err != nil { + return &base.ScanResult{ + Success: false, + Error: err, + }, nil + } + defer p.GetServiceConnector().Close(conn) + + // 服务识别成功 + return &base.ScanResult{ + Success: true, + Service: "Telnet", + Banner: "Telnet service detected", + Extra: map[string]interface{}{ + "service": "Telnet", + "port": info.Ports, + "type": "service-identification", + }, + }, nil +} + +// generateCredentials 生成Telnet认证凭据 +func (p *TelnetPlugin) generateCredentials() []*base.Credential { + var credentials []*base.Credential + + // 获取用户名字典 + usernames := common.Userdict["telnet"] + if len(usernames) == 0 { + // 使用默认用户名 + usernames = []string{"admin", "root", "user", "test", "guest"} + } + + // 获取密码字典 + passwords := common.Passwords + if len(passwords) == 0 { + // 使用默认密码 + passwords = []string{"", "admin", "root", "123456", "password", "test", "guest"} + } + + // 生成凭据组合 + for _, username := range usernames { + for _, password := range passwords { + // 处理密码中的用户名占位符 + actualPassword := strings.Replace(password, "{user}", username, -1) + + credentials = append(credentials, &base.Credential{ + Username: username, + Password: actualPassword, + }) + } + } + + return credentials +} \ No newline at end of file diff --git a/TestDocker/Telnet/docker-compose.yml b/TestDocker/Telnet/docker-compose.yml new file mode 100644 index 0000000..90253aa --- /dev/null +++ b/TestDocker/Telnet/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.8' + +services: + telnet: + build: . + ports: + - "23:23" + container_name: telnet_test + restart: unless-stopped \ No newline at end of file