Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Args struct {
PoCType string
ReportFormat string
HarFilePath string
CustomBlindXSSPayloadFile string
Timeout int
Delay int
Concurrence int
Expand Down
15 changes: 10 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&args.Cookie, "cookie", "C", "", "Add custom cookies to the request. Example: -C 'sessionid=abc123'")
rootCmd.PersistentFlags().StringVarP(&args.Data, "data", "d", "", "Use POST method and add body data. Example: -d 'username=admin&password=admin'")
rootCmd.PersistentFlags().StringVar(&args.CustomPayload, "custom-payload", "", "Load custom payloads from a file. Example: --custom-payload 'payloads.txt'")
rootCmd.PersistentFlags().StringVar(&args.CustomBlindXSSPayloadFile, "custom-blind-xss-payload", "", "Load custom blind XSS payloads from a file. Example: --custom-blind-xss-payload 'payloads.txt'")
rootCmd.PersistentFlags().StringVar(&args.CustomAlertValue, "custom-alert-value", "1", "Set a custom alert value. Example: --custom-alert-value 'document.cookie'")
rootCmd.PersistentFlags().StringVar(&args.CustomAlertType, "custom-alert-type", "none", "Set a custom alert type. Example: --custom-alert-type 'str,none'")
rootCmd.PersistentFlags().StringVar(&args.UserAgent, "user-agent", "", "Set a custom User-Agent header. Example: --user-agent 'Mozilla/5.0'")
Expand Down Expand Up @@ -152,11 +153,12 @@ func initConfig() {
options = model.Options{
Header: args.Header,
Cookie: args.Cookie,
UniqParam: args.P,
BlindURL: args.Blind,
CustomPayloadFile: args.CustomPayload,
CustomAlertValue: args.CustomAlertValue,
CustomAlertType: args.CustomAlertType,
UniqParam: args.P,
BlindURL: args.Blind,
CustomPayloadFile: args.CustomPayload,
CustomBlindXSSPayloadFile: args.CustomBlindXSSPayloadFile,
CustomAlertValue: args.CustomAlertValue,
CustomAlertType: args.CustomAlertType,
Data: args.Data,
UserAgent: args.UserAgent,
OutputFile: args.Output,
Expand Down Expand Up @@ -225,6 +227,9 @@ func initConfig() {
if args.CustomPayload == "" && cfgOptions.CustomPayloadFile != "" {
options.CustomPayloadFile = cfgOptions.CustomPayloadFile
}
if args.CustomBlindXSSPayloadFile == "" && cfgOptions.CustomBlindXSSPayloadFile != "" {
options.CustomBlindXSSPayloadFile = cfgOptions.CustomBlindXSSPayloadFile
}
if args.CustomAlertValue == DefaultCustomAlertValue && cfgOptions.CustomAlertValue != "" {
options.CustomAlertValue = cfgOptions.CustomAlertValue
}
Expand Down
1 change: 1 addition & 0 deletions pkg/model/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Options struct {
// Feature Options
BlindURL string `json:"blind,omitempty"`
CustomPayloadFile string `json:"custom-payload-file,omitempty"`
CustomBlindXSSPayloadFile string `json:"custom-blind-xss-payload-file,omitempty"`
CustomAlertValue string `json:"custom-alert-value,omitempty"`
CustomAlertType string `json:"custom-alert-type,omitempty"`
OnlyDiscovery bool `json:"only-discovery,omitempty"`
Expand Down
54 changes: 54 additions & 0 deletions pkg/scanning/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,60 @@
printing.DalLog("SYSTEM", "Added blind XSS payloads with callback URL: "+options.BlindURL, options)
}

// Custom Blind XSS Payloads from file
if options.CustomBlindXSSPayloadFile != "" {
fileInfo, statErr := os.Stat(options.CustomBlindXSSPayloadFile)
if os.IsNotExist(statErr) {
printing.DalLog("SYSTEM", "Failed to load custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" (file not found)", options)
} else if statErr != nil {
printing.DalLog("SYSTEM", "Failed to load custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" ("+statErr.Error()+")", options)

Check warning on line 441 in pkg/scanning/scan.go

View check run for this annotation

Codecov / codecov/patch

pkg/scanning/scan.go#L441

Added line #L441 was not covered by tests
} else if fileInfo.IsDir() {
printing.DalLog("SYSTEM", "Failed to load custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" (path is a directory)", options)

Check warning on line 443 in pkg/scanning/scan.go

View check run for this annotation

Codecov / codecov/patch

pkg/scanning/scan.go#L443

Added line #L443 was not covered by tests
} else {
// File exists and is not a directory, proceed to read it
payloadLines, readErr := voltFile.ReadLinesOrLiteral(options.CustomBlindXSSPayloadFile)
if readErr != nil {
printing.DalLog("SYSTEM", "Failed to read custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" ("+readErr.Error()+")", options)

Check warning on line 448 in pkg/scanning/scan.go

View check run for this annotation

Codecov / codecov/patch

pkg/scanning/scan.go#L448

Added line #L448 was not covered by tests
} else {
var bcallback string
if options.BlindURL != "" {
if strings.HasPrefix(options.BlindURL, "https://") || strings.HasPrefix(options.BlindURL, "http://") {
bcallback = options.BlindURL

Check warning on line 453 in pkg/scanning/scan.go

View check run for this annotation

Codecov / codecov/patch

pkg/scanning/scan.go#L453

Added line #L453 was not covered by tests
} else {
bcallback = "//" + options.BlindURL
}
}

addedPayloadCount := 0
for _, customPayload := range payloadLines {
if customPayload != "" {
addedPayloadCount++
actualPayload := customPayload
if options.BlindURL != "" { // Only replace if BlindURL is set
actualPayload = strings.Replace(customPayload, "CALLBACKURL", bcallback, -1)
}

for k, v := range params {
if optimization.CheckInspectionParam(options, k) {
ptype := ""
for _, av := range v.Chars {
if strings.Contains(av, "PTYPE:") {
ptype = GetPType(av)
}

Check warning on line 474 in pkg/scanning/scan.go

View check run for this annotation

Codecov / codecov/patch

pkg/scanning/scan.go#L472-L474

Added lines #L472 - L474 were not covered by tests
}
// Use only NaN encoder to avoid encoding issues with custom payloads
tq, tm := optimization.MakeRequestQuery(target, k, actualPayload, "toBlind"+ptype, "toBlind", NaN, options)
tm["payload"] = "toBlind"
query[tq] = tm
}
}
}
}
printing.DalLog("SYSTEM", "Added "+strconv.Itoa(addedPayloadCount)+" custom blind XSS payloads from file: "+options.CustomBlindXSSPayloadFile, options)
}
}
}

// Remote Payloads
if options.RemotePayloads != "" {
rp := strings.Split(options.RemotePayloads, ",")
Expand Down
198 changes: 198 additions & 0 deletions pkg/scanning/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package scanning

import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"time"

"github.com/hahwul/dalfox/v2/pkg/model"
"github.com/logrusorgru/aurora"
"github.com/stretchr/testify/assert"
)

// mockServer creates a test server that reflects query parameters and path in its response
Expand Down Expand Up @@ -79,6 +85,198 @@ func Test_shouldIgnoreReturn(t *testing.T) {
}
}

// createTempPayloadFile creates a temporary file with the given content.
// It returns the path to the temporary file and a cleanup function.
func createTempPayloadFile(t *testing.T, content string) (string, func()) {
t.Helper()
tmpFile, err := ioutil.TempFile("", "test-payloads-*.txt")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
os.Remove(tmpFile.Name())
t.Fatalf("Failed to write to temp file: %v", err)
}
if err := tmpFile.Close(); err != nil {
os.Remove(tmpFile.Name())
t.Fatalf("Failed to close temp file: %v", err)
}
return tmpFile.Name(), func() { os.Remove(tmpFile.Name()) }
}

// captureOutput captures stdout and stderr during the execution of a function.
func captureOutput(f func()) (string, string) {
oldStdout := os.Stdout
oldStderr := os.Stderr
rOut, wOut, _ := os.Pipe()
rErr, wErr, _ := os.Pipe()
os.Stdout = wOut
os.Stderr = wErr

f()

wOut.Close()
wErr.Close()
os.Stdout = oldStdout
os.Stderr = oldStderr

var outBuf, errBuf strings.Builder
// Use a WaitGroup to wait for copying to finish
var wg sync.WaitGroup
wg.Add(2)

go func() {
defer wg.Done()
io.Copy(&outBuf, rOut)
}()
go func() {
defer wg.Done()
io.Copy(&errBuf, rErr)
}()

wg.Wait()
return outBuf.String(), errBuf.String()
}

func TestGeneratePayloads_CustomBlindXSS(t *testing.T) {
server := mockServerForScanTest()
defer server.Close()

baseOptions := model.Options{
Concurrence: 1,
Format: "plain",
Silence: false, // Set to false to capture logs
NoSpinner: true,
CustomAlertType: "none",
AuroraObject: aurora.NewAurora(false), // Assuming NoColor is true for tests
Scan: make(map[string]model.Scan),
PathReflection: make(map[int]string),
Mutex: &sync.Mutex{},
}

params := map[string]model.ParamResult{
"q": {
Name: "q",
Type: "URL",
Reflected: true,
Chars: []string{},
},
}
policy := map[string]string{"Content-Type": "text/html"}
pathReflection := make(map[int]string)

t.Run("Valid custom blind payload file with --blind URL", func(t *testing.T) {
payloadContent := "blindy1<script>CALLBACKURL</script>\nblindy2<img src=x onerror=CALLBACKURL>"
payloadFile, cleanup := createTempPayloadFile(t, payloadContent)
defer cleanup()

options := baseOptions
options.CustomBlindXSSPayloadFile = payloadFile
options.BlindURL = "test-callback.com"
options.UniqParam = []string{"q"} // Ensure params are processed

var generatedQueries map[*http.Request]map[string]string
var logOutput string

stdout, stderr := captureOutput(func() {
generatedQueries, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
})
logOutput = stdout + stderr // Combine stdout and stderr

assert.Contains(t, logOutput, "Added 2 custom blind XSS payloads from file: "+payloadFile)

foundPayload1 := false
foundPayload2 := false
expectedPayload1 := strings.Replace("blindy1<script>CALLBACKURL</script>", "CALLBACKURL", "//"+options.BlindURL, -1)
expectedPayload2 := strings.Replace("blindy2<img src=x onerror=CALLBACKURL>", "CALLBACKURL", "//"+options.BlindURL, -1)

for req, meta := range generatedQueries {
if meta["type"] == "toBlind" && meta["payload"] == "toBlind" { // Check our specific type for these payloads
// Check if the payload in the query matches one of our expected transformed payloads
// This requires knowing how MakeRequestQuery structures the request.
// Assuming payload is in query parameter 'q' for this test.
queryValues := req.URL.Query()
if queryValues.Get("q") == expectedPayload1 {
foundPayload1 = true
}
if queryValues.Get("q") == expectedPayload2 {
foundPayload2 = true
}
}
}
assert.True(t, foundPayload1, "Expected payload 1 not found or not correctly transformed")
assert.True(t, foundPayload2, "Expected payload 2 not found or not correctly transformed")
})

t.Run("Custom blind payload file with CALLBACKURL but no --blind flag", func(t *testing.T) {
payloadContent := "blindy3<a href=CALLBACKURL>"
payloadFile, cleanup := createTempPayloadFile(t, payloadContent)
defer cleanup()

options := baseOptions
options.CustomBlindXSSPayloadFile = payloadFile
options.BlindURL = "" // No blind URL
options.UniqParam = []string{"q"}

var generatedQueries map[*http.Request]map[string]string
stdout, stderr := captureOutput(func() {
generatedQueries, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
})
logOutput := stdout + stderr // Combine stdout and stderr

assert.Contains(t, logOutput, "Added 1 custom blind XSS payloads from file: "+payloadFile)
foundPayload := false
expectedPayload := "blindy3<a href=CALLBACKURL>" // CALLBACKURL should not be replaced

for req, meta := range generatedQueries {
if meta["type"] == "toBlind" && meta["payload"] == "toBlind" {
if req.URL.Query().Get("q") == expectedPayload {
foundPayload = true
break
}
}
}
assert.True(t, foundPayload, "Expected payload with unreplaced CALLBACKURL not found")
})

t.Run("Invalid non-existent custom blind payload file", func(t *testing.T) {
options := baseOptions
options.CustomBlindXSSPayloadFile = "nonexistentfile.txt"
options.UniqParam = []string{"q"}

stdout, stderr := captureOutput(func() {
_, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
})
logOutput := stdout + stderr // Combine stdout and stderr

assert.Contains(t, logOutput, "Failed to load custom blind XSS payload file: nonexistentfile.txt")
// Check that no payloads of type "toBlind" were added due to this specific file error
// (assuming other payload generation might still occur)
customBlindPayloadsFound := false
assert.False(t, customBlindPayloadsFound, "Queries should not include payloads from a non-existent file if logic prevents it after error")
})

t.Run("Empty custom blind payload file", func(t *testing.T) {
payloadFile, cleanup := createTempPayloadFile(t, "")
defer cleanup()

options := baseOptions
options.CustomBlindXSSPayloadFile = payloadFile
options.UniqParam = []string{"q"}

stdout, stderr := captureOutput(func() {
_, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
})
logOutput := stdout + stderr // Combine stdout and stderr

assert.Contains(t, logOutput, "Added 0 custom blind XSS payloads from file: "+payloadFile)
// Verify no queries were generated specifically from this empty file.
// Similar to the above, this assumes no other "toBlind" payloads would be generated,
// or relies on the specific log message for confirmation.
})
}

func Test_generatePayloads(t *testing.T) {
// Create a mock server
server := mockServerForScanTest()
Expand Down
Loading