fscan/plugins/services/webtitle.go
ZacharyZcR 6cf5719e8a refactor: 彻底清理插件系统,消除虚假利用功能
- 删除整个legacy插件系统(7794行代码)
- 完成所有插件向单文件架构迁移
- 移除19个插件的虚假Exploit功能,只保留真实利用:
  * Redis: 文件写入、SSH密钥注入、计划任务
  * SSH: 命令执行
  * MS17010: EternalBlue漏洞利用
- 统一插件接口,简化架构复杂度
- 清理临时文件和备份文件

重构效果:
- 代码行数: -7794行
- 插件文件数: 从3文件架构→单文件架构
- 真实利用插件: 从22个→3个
- 架构复杂度: 大幅简化
2025-08-26 11:43:48 +08:00

321 lines
7.6 KiB
Go

package services
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/shadow1ng/fscan/common"
)
// WebTitlePlugin Web标题获取插件 - 收集Web应用信息和指纹识别
type WebTitlePlugin struct {
name string
ports []int
}
// NewWebTitlePlugin 创建WebTitle插件
func NewWebTitlePlugin() *WebTitlePlugin {
return &WebTitlePlugin{
name: "webtitle",
ports: []int{80, 443, 8080, 8443, 8000, 8888, 9000, 9090}, // 常见Web端口
}
}
// GetName 实现Plugin接口
func (p *WebTitlePlugin) GetName() string {
return p.name
}
// GetPorts 实现Plugin接口
func (p *WebTitlePlugin) GetPorts() []int {
return p.ports
}
// Scan 执行WebTitle扫描 - Web服务识别和指纹获取
func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 检查是否为Web端口
webPorts := map[string]bool{
"80": true, "443": true, "8080": true, "8443": true,
"8000": true, "8888": true, "9000": true, "9090": true,
"8001": true, "8081": true, "8008": true, "8086": true,
"9001": true, "8089": true, "8090": true, "8082": true,
}
if !webPorts[info.Ports] {
return &ScanResult{
Success: false,
Service: "webtitle",
Error: fmt.Errorf("非Web端口"),
}
}
// 获取Web信息
webInfo, err := p.getWebInfo(ctx, info)
if err != nil {
return &ScanResult{
Success: false,
Service: "webtitle",
Error: err,
}
}
if !webInfo.Valid {
return &ScanResult{
Success: false,
Service: "webtitle",
Error: fmt.Errorf("未发现有效的Web服务"),
}
}
// 记录Web信息发现
msg := fmt.Sprintf("WebTitle %s", target)
if webInfo.Title != "" {
msg += fmt.Sprintf(" [%s]", webInfo.Title)
}
if webInfo.StatusCode != 0 {
msg += fmt.Sprintf(" %d", webInfo.StatusCode)
}
if webInfo.Server != "" {
msg += fmt.Sprintf(" %s", webInfo.Server)
}
common.LogSuccess(msg)
return &ScanResult{
Success: true,
Service: "webtitle",
Banner: webInfo.Summary(),
}
}
// WebInfo Web信息结构
type WebInfo struct {
Valid bool
URL string
StatusCode int
Title string
Server string
ContentType string
ContentLength int64
Technologies []string
SecurityHeaders map[string]string
}
// Summary 返回Web信息摘要
func (wi *WebInfo) Summary() string {
if !wi.Valid {
return "Web服务检测失败"
}
var parts []string
if wi.StatusCode != 0 {
parts = append(parts, fmt.Sprintf("HTTP %d", wi.StatusCode))
}
if wi.Title != "" {
title := wi.Title
if len(title) > 30 {
title = title[:30] + "..."
}
parts = append(parts, fmt.Sprintf("[%s]", title))
}
if wi.Server != "" {
parts = append(parts, wi.Server)
}
return strings.Join(parts, " ")
}
// getWebInfo 获取Web服务信息
func (p *WebTitlePlugin) getWebInfo(ctx context.Context, info *common.HostInfo) (*WebInfo, error) {
webInfo := &WebInfo{
Valid: false,
SecurityHeaders: make(map[string]string),
Technologies: []string{},
}
// 确定协议
protocol := "http"
if info.Ports == "443" || info.Ports == "8443" {
protocol = "https"
}
webInfo.URL = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
// 创建HTTP客户端
client := &http.Client{
Timeout: time.Duration(common.WebTimeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
},
}
// 发送HTTP请求
req, err := http.NewRequestWithContext(ctx, "GET", webInfo.URL, nil)
if err != nil {
return webInfo, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置User-Agent
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
resp, err := client.Do(req)
if err != nil {
return webInfo, fmt.Errorf("HTTP请求失败: %v", err)
}
defer resp.Body.Close()
webInfo.Valid = true
webInfo.StatusCode = resp.StatusCode
// 获取基本响应头信息
if server := resp.Header.Get("Server"); server != "" {
webInfo.Server = server
}
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
webInfo.ContentType = contentType
}
webInfo.ContentLength = resp.ContentLength
// 检测安全头
p.detectSecurityHeaders(resp, webInfo)
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return webInfo, fmt.Errorf("读取响应内容失败: %v", err)
}
// 提取页面标题
webInfo.Title = p.extractTitle(string(body))
// 检测Web技术
p.detectTechnologies(resp, string(body), webInfo)
return webInfo, nil
}
// extractTitle 提取页面标题
func (p *WebTitlePlugin) extractTitle(html string) string {
// 正则表达式匹配title标签
titleRe := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
matches := titleRe.FindStringSubmatch(html)
if len(matches) > 1 {
title := strings.TrimSpace(matches[1])
// 移除多余的空白字符
title = regexp.MustCompile(`\s+`).ReplaceAllString(title, " ")
// 长度限制
if len(title) > 100 {
title = title[:100] + "..."
}
// 确保是有效的UTF-8
if utf8.ValidString(title) {
return title
}
}
return ""
}
// detectSecurityHeaders 检测安全头
func (p *WebTitlePlugin) detectSecurityHeaders(resp *http.Response, webInfo *WebInfo) {
securityHeaders := []string{
"X-Frame-Options",
"X-XSS-Protection",
"X-Content-Type-Options",
"Strict-Transport-Security",
"Content-Security-Policy",
"X-Powered-By",
}
for _, header := range securityHeaders {
if value := resp.Header.Get(header); value != "" {
webInfo.SecurityHeaders[header] = value
}
}
}
// detectTechnologies 检测Web技术
func (p *WebTitlePlugin) detectTechnologies(resp *http.Response, body string, webInfo *WebInfo) {
// 基于响应头检测
if xPoweredBy := resp.Header.Get("X-Powered-By"); xPoweredBy != "" {
webInfo.Technologies = append(webInfo.Technologies, fmt.Sprintf("X-Powered-By: %s", xPoweredBy))
}
// 基于Server头检测
if server := resp.Header.Get("Server"); server != "" {
if strings.Contains(strings.ToLower(server), "apache") {
webInfo.Technologies = append(webInfo.Technologies, "Apache HTTP Server")
} else if strings.Contains(strings.ToLower(server), "nginx") {
webInfo.Technologies = append(webInfo.Technologies, "Nginx")
} else if strings.Contains(strings.ToLower(server), "iis") {
webInfo.Technologies = append(webInfo.Technologies, "Microsoft IIS")
}
}
// 基于HTML内容检测
bodyLower := strings.ToLower(body)
// 检测常见框架
if strings.Contains(bodyLower, "wordpress") {
webInfo.Technologies = append(webInfo.Technologies, "WordPress")
}
if strings.Contains(bodyLower, "joomla") {
webInfo.Technologies = append(webInfo.Technologies, "Joomla")
}
if strings.Contains(bodyLower, "drupal") {
webInfo.Technologies = append(webInfo.Technologies, "Drupal")
}
// 检测JavaScript框架
if strings.Contains(bodyLower, "jquery") {
webInfo.Technologies = append(webInfo.Technologies, "jQuery")
}
if strings.Contains(bodyLower, "angular") {
webInfo.Technologies = append(webInfo.Technologies, "AngularJS")
}
if strings.Contains(bodyLower, "react") {
webInfo.Technologies = append(webInfo.Technologies, "React")
}
// 检测Web服务器相关
if strings.Contains(bodyLower, "apache tomcat") || strings.Contains(bodyLower, "tomcat") {
webInfo.Technologies = append(webInfo.Technologies, "Apache Tomcat")
}
if strings.Contains(bodyLower, "jetty") {
webInfo.Technologies = append(webInfo.Technologies, "Eclipse Jetty")
}
}
// init 自动注册插件
func init() {
RegisterPlugin("webtitle", func() Plugin {
return NewWebTitlePlugin()
})
}