Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion gobusterfuzz/gobusterfuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *l
url := *d.options.URL
url.Fragment = strings.ReplaceAll(url.Fragment, FuzzKeyword, word)
url.Host = strings.ReplaceAll(url.Host, FuzzKeyword, word)
url.Path = strings.ReplaceAll(url.Path, FuzzKeyword, word)
// Replace FUZZ in the encoded path string (EscapedPath returns RawPath
// when set, otherwise the percent-encoded form of Path) and re-assign
// via the helper so percent sequences in the wordlist word are not
// double-encoded as %25XX. See issue #618.
libgobuster.SetURLPathPreservingEncoding(&url, strings.ReplaceAll(url.EscapedPath(), FuzzKeyword, word))
url.Scheme = strings.ReplaceAll(url.Scheme, FuzzKeyword, word)

query := url.Query()
Expand Down
19 changes: 19 additions & 0 deletions libgobuster/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ import (
"errors"
"fmt"
"io"
"net/url"
"os"
"regexp"
"strconv"
"strings"
)

// SetURLPathPreservingEncoding assigns rawPath to u as both the encoded
// (RawPath) and decoded (Path) forms. When u.String() serializes the URL it
// emits RawPath verbatim, which preserves percent-encoded sequences in
// rawPath instead of double-encoding them (e.g. %2e -> %252e). This matters
// for fuzzing wordlists that contain pre-encoded payloads. If rawPath is not
// a valid percent-encoded path the function falls back to the previous
// behavior of assigning it to Path only.
func SetURLPathPreservingEncoding(u *url.URL, rawPath string) {
decoded, err := url.PathUnescape(rawPath)
if err != nil {
u.Path = rawPath
u.RawPath = ""
return
}
u.Path = decoded
u.RawPath = rawPath
}

// Set is a set of Ts
type Set[T comparable] struct {
Set map[T]bool
Expand Down
66 changes: 66 additions & 0 deletions libgobuster/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package libgobuster
import (
"errors"
"io"
"net/url"
"os"
"reflect"
"strconv"
Expand All @@ -11,6 +12,71 @@ import (
"testing/iotest"
)

func TestSetURLPathPreservingEncoding(t *testing.T) {
t.Parallel()

cases := []struct {
name string
startURL string
rawPath string
wantURL string
wantPath string
wantRaw string
}{
{
name: "percent_encoded_word_is_not_double_encoded",
startURL: "http://example/cgi-bin/FUZZ",
rawPath: "/cgi-bin/%2e%2e/opt/passwords",
wantURL: "http://example/cgi-bin/%2e%2e/opt/passwords",
wantPath: "/cgi-bin/../opt/passwords",
wantRaw: "/cgi-bin/%2e%2e/opt/passwords",
},
{
name: "plain_path_is_unchanged_on_the_wire",
startURL: "http://example/",
rawPath: "/admin/login",
wantURL: "http://example/admin/login",
wantPath: "/admin/login",
wantRaw: "/admin/login",
},
{
name: "preexisting_encoded_segment_round_trips",
startURL: "http://example/dir%20space/FUZZ",
rawPath: "/dir%20space/admin",
wantURL: "http://example/dir%20space/admin",
wantPath: "/dir space/admin",
wantRaw: "/dir%20space/admin",
},
{
name: "invalid_percent_sequence_falls_back_to_path_only",
startURL: "http://example/",
rawPath: "/literal-%-sign",
wantURL: "http://example/literal-%25-sign",
wantPath: "/literal-%-sign",
wantRaw: "",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
u, err := url.Parse(tc.startURL)
if err != nil {
t.Fatalf("parse: %v", err)
}
SetURLPathPreservingEncoding(u, tc.rawPath)
if got := u.String(); got != tc.wantURL {
t.Errorf("u.String() = %q, want %q", got, tc.wantURL)
}
if u.Path != tc.wantPath {
t.Errorf("u.Path = %q, want %q", u.Path, tc.wantPath)
}
if u.RawPath != tc.wantRaw {
t.Errorf("u.RawPath = %q, want %q", u.RawPath, tc.wantRaw)
}
})
}
}

func TestNewSet(t *testing.T) {
t.Parallel()
if NewSet[string]().Set == nil {
Expand Down