mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-01-31 15:53:10 +08:00
feat(js): adds RSYNC module (#6410)
* adding min auth support * adding unauth list modules + auth list files in module * example * adding rsync test * bump go.mod --------- Co-authored-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
@@ -15,6 +15,7 @@ var jsTestcases = []TestCaseInfo{
|
||||
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
|
||||
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/rsync-test.yaml", TestCase: &javascriptRsyncTest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
|
||||
{Path: "protocols/javascript/oracle-auth-test.yaml", TestCase: &javascriptOracleAuthTest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
|
||||
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}},
|
||||
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
|
||||
@@ -30,6 +31,7 @@ var (
|
||||
vncResource *dockertest.Resource
|
||||
postgresResource *dockertest.Resource
|
||||
mysqlResource *dockertest.Resource
|
||||
rsyncResource *dockertest.Resource
|
||||
pool *dockertest.Pool
|
||||
defaultRetry = 3
|
||||
)
|
||||
@@ -124,7 +126,7 @@ func (j *javascriptOracleAuthTest) Execute(filePath string) error {
|
||||
results := []string{}
|
||||
var err error
|
||||
_ = pool.Retry(func() error {
|
||||
//let ssh server start
|
||||
// let oracle server start
|
||||
time.Sleep(3 * time.Second)
|
||||
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
|
||||
return nil
|
||||
@@ -258,6 +260,38 @@ func (j *javascriptNoPortArgs) Execute(filePath string) error {
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
type javascriptRsyncTest struct{}
|
||||
|
||||
func (j *javascriptRsyncTest) Execute(filePath string) error {
|
||||
if rsyncResource == nil || pool == nil {
|
||||
// skip test as rsync is not running
|
||||
return nil
|
||||
}
|
||||
tempPort := rsyncResource.GetPort("873/tcp")
|
||||
finalURL := "localhost:" + tempPort
|
||||
defer purge(rsyncResource)
|
||||
errs := []error{}
|
||||
for i := 0; i < defaultRetry; i++ {
|
||||
results := []string{}
|
||||
var err error
|
||||
_ = pool.Retry(func() error {
|
||||
//let rsync server start
|
||||
time.Sleep(3 * time.Second)
|
||||
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return multierr.Combine(errs...)
|
||||
}
|
||||
|
||||
// purge any given resource if it is not nil
|
||||
func purge(resource *dockertest.Resource) {
|
||||
if resource != nil && pool != nil {
|
||||
@@ -397,4 +431,20 @@ func init() {
|
||||
if err := mysqlResource.Expire(30); err != nil {
|
||||
log.Printf("Could not expire mysql resource: %s", err)
|
||||
}
|
||||
|
||||
// setup a temporary rsync server
|
||||
rsyncResource, err = pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "alpine",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"sh", "-c", "apk add --no-cache rsync shadow && useradd -m rsyncuser && echo 'rsyncuser:mysecret' | chpasswd && echo 'rsyncuser:MySecret123' > /etc/rsyncd.secrets && chmod 600 /etc/rsyncd.secrets && echo -e '[data]\\n path = /data\\n comment = Local Rsync Share\\n read only = false\\n auth users = rsyncuser\\n secrets file = /etc/rsyncd.secrets' > /etc/rsyncd.conf && mkdir -p /data && exec rsync --daemon --no-detach --config=/etc/rsyncd.conf"},
|
||||
Platform: "linux/amd64",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Could not start Rsync resource: %s", err)
|
||||
return
|
||||
}
|
||||
// by default expire after 30 sec
|
||||
if err := rsyncResource.Expire(30); err != nil {
|
||||
log.Printf("Could not expire Rsync resource: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -53,6 +53,7 @@ require (
|
||||
github.com/DataDog/gostackparse v0.7.0
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
|
||||
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697
|
||||
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883
|
||||
github.com/alexsnet/go-vnc v0.1.0
|
||||
@@ -269,6 +270,7 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
|
||||
github.com/kataras/jwt v0.1.10 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -87,6 +87,8 @@ github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub
|
||||
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4=
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8=
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
|
||||
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 h1:54I+OF5vS4a/rxnUrN5J3hi0VEYKcrTlpc8JosDyP+c=
|
||||
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697/go.mod h1:yNqYRqxYkSROY1J+LX+A0tOSA/6soXQs5m8hZSqYBac=
|
||||
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 h1:+Is1AS20q3naP+qJophNpxuvx1daFOx9C0kLIuI0GVk=
|
||||
@@ -634,6 +636,8 @@ github.com/k14s/difflib v0.0.0-20201117154628-0c031775bf57 h1:CwBRArr+BWBopnUJhD
|
||||
github.com/k14s/difflib v0.0.0-20201117154628-0c031775bf57/go.mod h1:B0xN2MiNBGWOWi9CcfAo9LBI8IU4J1utlbOIJCsmKr4=
|
||||
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 h1:4bcRTTSx+LKSxMWibIwzHnDNmaN1x52oEpvnjCy+8vk=
|
||||
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368/go.mod h1:lKGj1op99m4GtQISxoD2t+K+WO/q2NzEPKvfXFQfbCA=
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 h1:IAukUBAVLUWBcexOYgkTD/EjMkfnNos7g7LFpyIdHJI=
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166/go.mod h1:T4xUEny5PVedYIbkMAKYEBjMyDsOvvP0qK4s324AKA8=
|
||||
github.com/kataras/jwt v0.1.10 h1:GBXOF9RVInDPhCFBiDumRG9Tt27l7ugLeLo8HL5SeKQ=
|
||||
github.com/kataras/jwt v0.1.10/go.mod h1:xkimAtDhU/aGlQqjwvgtg+VyuPwMiyZHaY8LJRh0mYo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
|
||||
21
integration_tests/protocols/javascript/rsync-test.yaml
Normal file
21
integration_tests/protocols/javascript/rsync-test.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
id: rsync-test
|
||||
|
||||
info:
|
||||
name: Rsync Test
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
const rsync = require('nuclei/rsync');
|
||||
rsync.IsRsync(Host, Port);
|
||||
|
||||
args:
|
||||
Host: "{{Host}}"
|
||||
Port: "873"
|
||||
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "success == true"
|
||||
|
||||
@@ -21,6 +21,7 @@ func init() {
|
||||
|
||||
// Objects / Classes
|
||||
"IsRsyncResponse": gojs.GetClassConstructor[lib_rsync.IsRsyncResponse](&lib_rsync.IsRsyncResponse{}),
|
||||
"RsyncClient": gojs.GetClassConstructor[lib_rsync.RsyncClient](&lib_rsync.RsyncClient{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,61 @@ export function IsRsync(host: string, port: number): IsRsyncResponse | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RsyncClient is a client for RSYNC servers.
|
||||
* Internally client uses https://github.com/gokrazy/rsync driver.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const rsync = require('nuclei/rsync');
|
||||
* const client = new rsync.RsyncClient();
|
||||
* ```
|
||||
*/
|
||||
export class RsyncClient {
|
||||
|
||||
// Constructor of RsyncClient
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Connect establishes a connection to the rsync server with authentication.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const rsync = require('nuclei/rsync');
|
||||
* const client = new rsync.RsyncClient();
|
||||
* const connected = client.Connect('acme.com', 873, 'username', 'password', 'backup');
|
||||
* ```
|
||||
*/
|
||||
public Connect(host: string, port: number, username: string, password: string, module: string): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListModules lists available modules on the rsync server.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const rsync = require('nuclei/rsync');
|
||||
* const client = new rsync.RsyncClient();
|
||||
* const modules = client.ListModules('acme.com', 873, 'username', 'password');
|
||||
* log(toJSON(modules));
|
||||
* ```
|
||||
*/
|
||||
public ListModules(host: string, port: number, username: string, password: string): string[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListFilesInModule lists files in a specific module on the rsync server.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const rsync = require('nuclei/rsync');
|
||||
* const client = new rsync.RsyncClient();
|
||||
* const files = client.ListFilesInModule('acme.com', 873, 'username', 'password', 'backup');
|
||||
* log(toJSON(files));
|
||||
* ```
|
||||
*/
|
||||
public ListFilesInModule(host: string, port: number, username: string, password: string, module: string): string[] | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IsRsyncResponse is the response from the IsRsync function.
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
rsynclib "github.com/Mzack9999/go-rsync/rsync"
|
||||
|
||||
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
|
||||
"github.com/praetorian-inc/fingerprintx/pkg/plugins/services/rsync"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
)
|
||||
|
||||
type (
|
||||
// RsyncClient is a client for RSYNC servers.
|
||||
// Internally client uses https://github.com/gokrazy/rsync driver.
|
||||
// @example
|
||||
// ```javascript
|
||||
// const rsync = require('nuclei/rsync');
|
||||
// const client = new rsync.RsyncClient();
|
||||
// ```
|
||||
RsyncClient struct{}
|
||||
|
||||
// IsRsyncResponse is the response from the IsRsync function.
|
||||
// this is returned by IsRsync function.
|
||||
// @example
|
||||
@@ -25,8 +38,30 @@ type (
|
||||
IsRsync bool
|
||||
Banner string
|
||||
}
|
||||
|
||||
// ListSharesResponse is the response from the ListShares function.
|
||||
// this is returned by ListShares function.
|
||||
// @example
|
||||
// ```javascript
|
||||
// const rsync = require('nuclei/rsync');
|
||||
// const client = new rsync.RsyncClient();
|
||||
// const listShares = client.ListShares('acme.com', 873);
|
||||
// log(toJSON(listShares));
|
||||
RsyncListResponse struct {
|
||||
Modules []string
|
||||
Files []string
|
||||
Output string
|
||||
}
|
||||
)
|
||||
|
||||
func connectWithFastDialer(executionId string, host string, port int) (net.Conn, error) {
|
||||
dialer := protocolstate.GetDialersWithId(executionId)
|
||||
if dialer == nil {
|
||||
return nil, fmt.Errorf("dialers not initialized for %s", executionId)
|
||||
}
|
||||
return dialer.Fastdialer.Dial(context.Background(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
}
|
||||
|
||||
// IsRsync checks if a host is running a Rsync server.
|
||||
// @example
|
||||
// ```javascript
|
||||
@@ -44,11 +79,7 @@ func isRsync(executionId string, host string, port int) (IsRsyncResponse, error)
|
||||
resp := IsRsyncResponse{}
|
||||
|
||||
timeout := 5 * time.Second
|
||||
dialer := protocolstate.GetDialersWithId(executionId)
|
||||
if dialer == nil {
|
||||
return IsRsyncResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
|
||||
}
|
||||
conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
conn, err := connectWithFastDialer(executionId, host, port)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
@@ -59,7 +90,7 @@ func isRsync(executionId string, host string, port int) (IsRsyncResponse, error)
|
||||
rsyncPlugin := rsync.RSYNCPlugin{}
|
||||
service, err := rsyncPlugin.Run(conn, timeout, plugins.Target{Host: host})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
return resp, nil
|
||||
}
|
||||
if service == nil {
|
||||
return resp, nil
|
||||
@@ -68,3 +99,115 @@ func isRsync(executionId string, host string, port int) (IsRsyncResponse, error)
|
||||
resp.IsRsync = true
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListModules lists the modules of a Rsync server.
|
||||
// @example
|
||||
// ```javascript
|
||||
// const rsync = require('nuclei/rsync');
|
||||
// const client = new rsync.RsyncClient();
|
||||
// const listModules = client.ListModules('acme.com', 873, 'username', 'password');
|
||||
// log(toJSON(listModules));
|
||||
// ```
|
||||
func (c *RsyncClient) ListModules(ctx context.Context, host string, port int, username string, password string) (RsyncListResponse, error) {
|
||||
executionId := ctx.Value("executionId").(string)
|
||||
return listModules(executionId, host, port, username, password)
|
||||
}
|
||||
|
||||
// ListShares lists the shares of a Rsync server.
|
||||
// @example
|
||||
// ```javascript
|
||||
// const rsync = require('nuclei/rsync');
|
||||
// const client = new rsync.RsyncClient();
|
||||
// const listShares = client.ListFilesInModule('acme.com', 873, 'username', 'password', '/');
|
||||
// log(toJSON(listShares));
|
||||
// ```
|
||||
func (c *RsyncClient) ListFilesInModule(ctx context.Context, host string, port int, username string, password string, module string) (RsyncListResponse, error) {
|
||||
executionId := ctx.Value("executionId").(string)
|
||||
return listFilesInModule(executionId, host, port, username, password, module)
|
||||
}
|
||||
|
||||
func listModules(executionId string, host string, port int, username string, password string) (RsyncListResponse, error) {
|
||||
fastDialer := protocolstate.GetDialersWithId(executionId)
|
||||
if fastDialer == nil {
|
||||
return RsyncListResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
|
||||
}
|
||||
|
||||
address := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
// Create a bytes buffer for logging
|
||||
var logBuffer bytes.Buffer
|
||||
|
||||
// Create a custom slog handler that writes to the buffer
|
||||
logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
})
|
||||
|
||||
// Create a logger that writes to our buffer
|
||||
logger := slog.New(logHandler)
|
||||
|
||||
sr, err := rsynclib.ListModules(address,
|
||||
rsynclib.WithClientAuth(username, password),
|
||||
rsynclib.WithLogger(logger),
|
||||
rsynclib.WithFastDialer(fastDialer.Fastdialer),
|
||||
)
|
||||
if err != nil {
|
||||
return RsyncListResponse{}, fmt.Errorf("connect failed: %v", err)
|
||||
}
|
||||
|
||||
result := RsyncListResponse{
|
||||
Modules: make([]string, len(sr)),
|
||||
Output: logBuffer.String(),
|
||||
}
|
||||
|
||||
for i, item := range sr {
|
||||
result.Modules[i] = string(item.Name)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func listFilesInModule(executionId string, host string, port int, username string, password string, module string) (RsyncListResponse, error) {
|
||||
fastDialer := protocolstate.GetDialersWithId(executionId)
|
||||
if fastDialer == nil {
|
||||
return RsyncListResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
|
||||
}
|
||||
|
||||
address := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
// Create a bytes buffer for logging
|
||||
var logBuffer bytes.Buffer
|
||||
|
||||
// Create a custom slog handler that writes to the buffer
|
||||
logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
})
|
||||
|
||||
// Create a logger that writes to our buffer
|
||||
logger := slog.New(logHandler)
|
||||
|
||||
sr, err := rsynclib.SocketClient(nil, address, module, ".",
|
||||
rsynclib.WithClientAuth(username, password),
|
||||
rsynclib.WithLogger(logger),
|
||||
rsynclib.WithFastDialer(fastDialer.Fastdialer),
|
||||
)
|
||||
if err != nil {
|
||||
return RsyncListResponse{}, fmt.Errorf("connect failed: %v", err)
|
||||
}
|
||||
|
||||
// Try to list files to test authentication
|
||||
list, err := sr.List()
|
||||
if err != nil {
|
||||
return RsyncListResponse{}, fmt.Errorf("authentication failed: %v", err)
|
||||
}
|
||||
|
||||
result := RsyncListResponse{
|
||||
Files: make([]string, len(list)),
|
||||
Output: logBuffer.String(),
|
||||
}
|
||||
|
||||
for i, item := range list {
|
||||
result.Files[i] = string(item.Path)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user