Files
fscan/plugins/services/rsync.go
T
ZacharyZcR 71b92d4408 feat: v2.1.0 核心重构与功能增强
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n

## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池

## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)

## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建

## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
  JSON 输出格式、Redis 检测、Context 超时等

## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
2026-01-11 20:16:23 +08:00

402 lines
9.1 KiB
Go

//go:build (plugin_rsync || !plugin_selective) && go1.21
package services
import (
"bufio"
"context"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
"go.ciq.dev/go-rsync/rsync"
)
// RsyncPlugin Rsync扫描插件
type RsyncPlugin struct {
plugins.BasePlugin
}
func NewRsyncPlugin() *RsyncPlugin {
return &RsyncPlugin{
BasePlugin: plugins.NewBasePlugin("rsync"),
}
}
func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
}
var findings []string
// 检测未授权访问
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
common.LogSuccess(i18n.Tr("rsync_service", target, result.Banner))
findings = append(findings, result.Banner)
}
// 生成密码字典
credentials := plugins.GenerateCredentials("rsync", config)
if len(credentials) == 0 {
if len(findings) > 0 {
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "rsync",
Banner: findings[0],
}
}
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("没有可用的测试凭据"),
}
}
// 转换凭据类型
creds := make([]Credential, len(credentials))
for i, c := range credentials {
creds[i] = Credential{Username: c.Username, Password: c.Password}
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
result := TestCredentialsConcurrently(ctx, creds, authFn, "rsync", testConfig)
if result.Success {
common.LogSuccess(i18n.Tr("rsync_credential", target, result.Username, result.Password))
return result
}
// 如果暴力破解失败但有未授权访问发现,返回该结果
if len(findings) > 0 {
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "rsync",
Banner: findings[0],
}
}
return &ScanResult{
Success: false,
Service: "rsync",
}
}
// createAuthFunc 创建Rsync认证函数
func (p *RsyncPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doRsyncAuth(ctx, info, cred, config, state)
}
}
// doRsyncAuth 执行Rsync认证
func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
// 先获取可用模块列表
conn := p.connectToRsync(ctx, info, config, state)
if conn == nil {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeNetwork,
Error: fmt.Errorf("无法连接到Rsync服务"),
}
}
modules := p.getModules(conn, config)
_ = conn.Close()
if len(modules) == 0 {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
Error: fmt.Errorf("无法获取模块列表"),
}
}
// 提取第一个模块名
firstModuleLine := modules[0]
firstModule := strings.Fields(firstModuleLine)[0]
// 使用 go-rsync 库进行认证测试
address := fmt.Sprintf("%s:%d", info.Host, info.Port)
dummyFS := &dummyStorage{}
_, err := rsync.SocketClient(
dummyFS,
address,
firstModule,
"/",
rsync.WithClientAuth(cred.Username, cred.Password),
)
if err != nil {
state.IncrementTCPFailedPacketCount()
errMsg := err.Error()
if common.ContainsAny(errMsg, "auth", "password") {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: err,
}
}
return &AuthResult{
Success: false,
ErrorType: classifyRsyncErrorType(err),
Error: err,
}
}
state.IncrementTCPSuccessPacketCount()
return &AuthResult{
Success: true,
Conn: &rsyncConnWrapper{},
ErrorType: ErrorTypeUnknown,
Error: nil,
}
}
// rsyncConnWrapper 包装Rsync连接以实现io.Closer
type rsyncConnWrapper struct{}
func (w *rsyncConnWrapper) Close() error {
return nil
}
// dummyStorage 空的 FS 实现,用于认证测试
type dummyStorage struct{}
func (d *dummyStorage) Put(fileName string, content io.Reader, fileSize int64, metadata rsync.FileMetadata) (written int64, err error) {
return 0, fmt.Errorf("not implemented")
}
func (d *dummyStorage) Delete(fileName string, mode rsync.FileMode) error {
return fmt.Errorf("not implemented")
}
func (d *dummyStorage) List() (rsync.FileList, error) {
return nil, fmt.Errorf("not implemented")
}
// classifyRsyncErrorType Rsync错误分类
func classifyRsyncErrorType(err error) ErrorType {
if err == nil {
return ErrorTypeUnknown
}
rsyncAuthErrors := []string{
"auth",
"password",
"authentication failed",
"access denied",
"unauthorized",
"invalid credentials",
}
return ClassifyError(err, rsyncAuthErrors, CommonNetworkErrors)
}
// testUnauthorizedAccess 测试未授权访问
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
conn := p.connectToRsync(ctx, info, config, state)
if conn == nil {
return nil
}
defer func() { _ = conn.Close() }()
modules := p.getModules(conn, config)
if len(modules) > 0 {
banner := fmt.Sprintf("未授权访问 - 可用模块: %s", strings.Join(modules, ", "))
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "rsync",
Banner: banner,
}
}
return nil
}
// connectToRsync 连接到Rsync服务
func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn {
target := info.Target()
connChan := make(chan net.Conn, 1)
go func() {
timeout := config.Timeout
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
connChan <- nil
return
}
state.IncrementTCPSuccessPacketCount()
_ = conn.SetDeadline(time.Now().Add(timeout))
connChan <- conn
}()
select {
case conn := <-connChan:
return conn
case <-ctx.Done():
// context 被取消,启动清理协程等待并关闭可能创建的连接
go func() {
conn := <-connChan
if conn != nil {
_ = conn.Close()
}
}()
return nil
}
}
// getModules 获取Rsync模块列表
func (p *RsyncPlugin) getModules(conn net.Conn, config *common.Config) []string {
timeout := config.Timeout
// 读取服务器版本
_ = conn.SetReadDeadline(time.Now().Add(timeout))
versionBuf := make([]byte, 256)
n, err := conn.Read(versionBuf)
if err != nil {
return nil
}
_ = string(versionBuf[:n])
// 回复客户端版本
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
if _, err := conn.Write([]byte("@RSYNCD: 31.0\n")); err != nil {
return nil
}
// 发送模块列表请求
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
if _, err := conn.Write([]byte("\n")); err != nil {
return nil
}
_ = conn.SetReadDeadline(time.Now().Add(timeout))
scanner := bufio.NewScanner(conn)
var modules []string
hasError := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if strings.HasPrefix(line, "@RSYNCD: EXIT") {
break
}
if strings.HasPrefix(line, "@RSYNCD:") {
continue
}
if strings.HasPrefix(line, "@ERROR:") {
hasError = true
break
}
modules = append(modules, line)
}
if hasError {
return nil
}
return modules
}
// identifyService Rsync服务识别
func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
target := info.Target()
conn := p.connectToRsync(ctx, info, config, state)
if conn == nil {
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("无法连接到Rsync服务"),
}
}
defer func() { _ = conn.Close() }()
timeout := config.Timeout
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
if _, err := conn.Write([]byte("\n")); err != nil {
return &ScanResult{
Success: false,
Service: "rsync",
Error: err,
}
}
_ = conn.SetReadDeadline(time.Now().Add(timeout))
response := make([]byte, 1024)
n, err := conn.Read(response)
if err != nil {
return &ScanResult{
Success: false,
Service: "rsync",
Error: err,
}
}
responseStr := string(response[:n])
var banner string
if strings.Contains(responseStr, "@RSYNCD") {
lines := strings.Split(responseStr, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "@RSYNCD:") {
banner = fmt.Sprintf("Rsync服务 (%s)", strings.TrimSpace(line))
break
}
}
if banner == "" {
banner = "Rsync文件同步服务"
}
} else {
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("无法识别为Rsync服务"),
}
}
common.LogSuccess(i18n.Tr("rsync_service", target, banner))
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "rsync",
Banner: banner,
}
}
func init() {
RegisterPluginWithPorts("rsync", func() Plugin {
return NewRsyncPlugin()
}, []int{873})
}