Files
fscan/plugins/services/rsync.go
T
ZacharyZcR 6d61b661f4
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
fix: 修复实机测试发现的可靠性问题 (v2.2.0-rc.1)
- UDP 插件在 -p 指定端口时被跳过
- Redis exploit 无超时保护 / readReply 吞没非超时错误
- service_probe 连接丢失后静默成功
- SNMP 探测成功但终端无输出
- SSH 爆破不稳定 (并发过高 + 自适应超时过短 + 限流误判)
- 进度条 isActive 竞态

新增 Config.ModuleTimeout() 协议级超时下限 (≥3s)
新增 ErrorTypeThrottle 限流错误分类
2026-06-14 22:23:52 +08:00

422 lines
9.4 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, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, session)
}
var findings []string
// 检测未授权访问
if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success {
session.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("%s", i18n.GetText("service_no_credentials")),
}
}
// 转换凭据类型
creds := make([]Credential, len(credentials))
for i, c := range credentials {
creds[i] = Credential{Username: c.Username, Password: c.Password}
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, creds, authFn, "rsync", testConfig)
if result.Success {
session.LogVuln(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, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doRsyncAuth(ctx, info, cred, session)
}
}
// doRsyncAuth 执行Rsync认证
func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
// 先获取可用模块列表
conn := p.connectToRsync(ctx, info, session)
if conn == nil {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeNetwork,
Error: fmt.Errorf("%s", i18n.GetText("rsync_connect_failed")),
}
}
modules := p.getModules(conn, session.Config)
_ = conn.Close()
if len(modules) == 0 {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
Error: fmt.Errorf("%s", i18n.GetText("rsync_modules_failed")),
}
}
// 提取第一个模块名
var firstModule string
for _, moduleLine := range modules {
if fields := strings.Fields(moduleLine); len(fields) > 0 {
firstModule = fields[0]
break
}
}
if firstModule == "" {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
Error: fmt.Errorf("%s", i18n.GetText("rsync_modules_failed")),
}
}
// 使用 go-rsync 库进行认证测试
address := info.Target()
dummyFS := &dummyStorage{}
_, err := rsync.SocketClient(
dummyFS,
address,
firstModule,
"/",
rsync.WithClientAuth(cred.Username, cred.Password),
)
if err != nil {
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,
}
}
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, session *common.ScanSession) *ScanResult {
conn := p.connectToRsync(ctx, info, session)
if conn == nil {
return nil
}
defer func() { _ = conn.Close() }()
modules := p.getModules(conn, session.Config)
if len(modules) > 0 {
banner := i18n.Tr("rsync_unauth_modules", 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, session *common.ScanSession) net.Conn {
target := info.Target()
timeout := session.Config.ModuleTimeout()
connChan := make(chan net.Conn, 1)
go func() {
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
connChan <- nil
return
}
_ = conn.SetDeadline(time.Now().Add(timeout))
connChan <- conn
}()
select {
case conn := <-connChan:
return conn
case <-ctx.Done():
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.ModuleTimeout()
// 读取服务器版本
_ = conn.SetReadDeadline(time.Now().Add(timeout))
if _, err := readRsyncLine(conn, 256); err != nil {
return nil
}
// 回复客户端版本
_ = 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, session *common.ScanSession) *ScanResult {
target := info.Target()
conn := p.connectToRsync(ctx, info, session)
if conn == nil {
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("%s", i18n.GetText("rsync_connect_failed")),
}
}
defer func() { _ = conn.Close() }()
timeout := session.Config.ModuleTimeout()
_ = 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))
responseStr, err := readRsyncLine(conn, 1024)
if err != nil {
return &ScanResult{
Success: false,
Service: "rsync",
Error: err,
}
}
var banner string
if strings.Contains(responseStr, "@RSYNCD") {
lines := strings.Split(responseStr, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "@RSYNCD:") {
banner = i18n.Tr("rsync_service_info", strings.TrimSpace(line))
break
}
}
if banner == "" {
banner = i18n.GetText("rsync_file_sync_service")
}
} else {
return &ScanResult{
Success: false,
Service: "rsync",
Error: fmt.Errorf("%s", i18n.Tr("service_not_identified", "Rsync")),
}
}
session.LogSuccess(i18n.Tr("rsync_service", target, banner))
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "rsync",
Banner: banner,
}
}
func readRsyncLine(conn interface {
Read([]byte) (int, error)
}, max int) (string, error) {
var line strings.Builder
var b [1]byte
for line.Len() < max {
if _, err := io.ReadFull(conn, b[:]); err != nil {
if err == io.EOF && line.Len() > 0 {
return line.String(), nil
}
return "", err
}
line.WriteByte(b[0])
if b[0] == '\n' {
return line.String(), nil
}
}
return line.String(), nil
}
func init() {
RegisterPluginWithPorts("rsync", func() Plugin {
return NewRsyncPlugin()
}, []int{873})
}