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
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.PHONY: install build

TARGET_DIR = $(HOME)/.nomore403/payloads
REPO_URL = https://raw.githubusercontent.com/R0X4R/nomore403/main/payloads
FILES = endpaths headers httpmethods ips midpaths simpleheaders useragents

# Dynamically resolve GOBIN using Go's environment variables.
# If GOBIN is empty, Go defaults to using GOPATH/bin.
GOBIN := $(shell go env GOBIN)
namespace :;
ifeq ($(GOBIN),)
GOBIN := $(shell go env GOPATH)/bin
endif

install:
@echo "[*] ENSURING GLOBAL CONFIGURATION DIRECTORIES EXIST"
@mkdir -p $(TARGET_DIR)
@echo "[*] DOWNLOADING ASSET PAYLOADS FROM GITHUB REPOSITORY"
@for file in $(FILES); do \
curl -sSL "$(REPO_URL)/$$file" -o "$(TARGET_DIR)/$$file"; \
done
@echo "[*] INSTALLING nomore403"
@go install github.com/R0X4R/nomore403@latest
@echo "[+] SUCCESS! nomore403 IS INSTALLED."
@$(GOBIN)/nomore403 -h

build:
@echo "[*] ENSURING GLOBAL CONFIGURATION DIRECTORIES EXIST"
@mkdir -p $(TARGET_DIR)
@echo "[*] DEPLOYING LOCAL PAYLOADS"
@cp -r payloads/* $(TARGET_DIR)/
@echo "[*] BUILDING LOCAL BINARY"
@go build -o nomore403 main.go
@$(HOME)/go/bin/nomore403 -h
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,22 @@ This tool does not "break authentication" by itself. It helps find differences b

## Installation

### Build from source
### Global Installation (Zero Download)

If you want to install the tool globally without cloning the repository or downloading any files manually, run this one-liner. It pulls the setup configuration, deploys the required asset payloads to your home directory, and installs the binary system-wide.

```bash
git clone https://github.com/devploit/nomore403
cd nomore403
go build
curl -sSL https://raw.githubusercontent.com/R0X4R/nomore403/main/Makefile | make -f - install
```

### Install with Go
### Local Build from Source

Use this method if you want to inspect the source code, modify the project, or build a local executable binary inside your current working directory.

```bash
go install github.com/devploit/nomore403@latest
git clone https://github.com/R0X4R/nomore403 && cd nomore403 && make build
```

If you install with `go install`, the `payloads/` directory is not installed automatically. Clone the repository and point the tool to that directory with `-f` if needed.

## Requirements

- Go 1.24 or later to build from source
Expand Down
41 changes: 8 additions & 33 deletions cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func isTransientError(err error) bool {
"connection reset",
"EOF",
"temporary failure",
"no such host", // DNS can be transient
"no such host",
}
for _, pattern := range transientPatterns {
if strings.Contains(strings.ToLower(errStr), strings.ToLower(pattern)) {
Expand All @@ -178,11 +178,6 @@ func isTransientError(err error) bool {
return false
}

// request makes a single HTTP request using headers `headers` and proxy `proxy`.
func request(method, uri string, headers []header, proxy *url.URL, timeout int, redirect bool) (ResponseInfo, error) {
return requestBody(method, uri, headers, "", proxy, timeout, redirect)
}

func requestBody(method, uri string, headers []header, body string, proxy *url.URL, timeout int, redirect bool) (ResponseInfo, error) {
if method == "" {
method = "GET"
Expand All @@ -192,8 +187,6 @@ func requestBody(method, uri string, headers []header, body string, proxy *url.U
proxy = nil
}

// net/http and url.Parse do not accept legacy IIS-style %uXXXX escapes.
// Route those requests through the raw client so the request target is sent as-is.
if strings.Contains(strings.ToLower(uri), "%u") && proxy == nil && !redirect {
return rawRequest(method, uri, rawRequestTarget(uri), headers, body, timeout)
}
Expand All @@ -202,9 +195,6 @@ func requestBody(method, uri string, headers []header, body string, proxy *url.U

parsedURL, err := url.Parse(uri)
if err != nil || parsedURL == nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
// Fallback for non-standard encoding (e.g., %u002f unicode escapes)
// that url.Parse rejects. Extract scheme/host manually and preserve
// the raw path so the server receives it as-is.
parsedURL, err = parseRawURL(uri)
if err != nil {
return ResponseInfo{}, fmt.Errorf("invalid URL: %q", uri)
Expand All @@ -222,8 +212,6 @@ func requestBody(method, uri string, headers []header, body string, proxy *url.U
req.Header = make(http.Header)

for _, header := range headers {
// Go's net/http ignores req.Header["Host"] — it uses req.Host instead.
// Set req.Host directly so Host header variations are actually sent.
if strings.EqualFold(header.key, "Host") {
req.Host = header.value
} else {
Expand Down Expand Up @@ -261,7 +249,6 @@ func requestBody(method, uri string, headers []header, body string, proxy *url.U

// loadFlagsFromRequestFile parse an HTTP request and configure the necessary flags for an execution
func loadFlagsFromRequestFile(requestFile string, schema bool, verbose bool, techniques []string, redirect bool) {
// Read the content of the request file
content, err := os.ReadFile(requestFile)
if err != nil {
log.Printf("[!] Error reading request file: %v", err)
Expand All @@ -274,7 +261,6 @@ func loadFlagsFromRequestFile(requestFile string, schema bool, verbose bool, tec
return
}

// Down HTTP/2 to HTTP/1.1 (handles both "HTTP/2" and "HTTP/2.0")
firstLine := temp[0]
if strings.Contains(firstLine, "HTTP/2.0") {
firstLine = strings.Replace(firstLine, "HTTP/2.0", "HTTP/1.1", 1)
Expand Down Expand Up @@ -304,7 +290,6 @@ func loadFlagsFromRequestFile(requestFile string, schema bool, verbose bool, tec

uri := httpSchema + req.Host + req.RequestURI

// Extract headers from the request
var reqHeaders []string
for k, v := range req.Header {
reqHeaders = append(reqHeaders, k+": "+strings.Join(v, ""))
Expand All @@ -320,10 +305,8 @@ func runAutocalibrate(options RequestOptions) (int, int) {
calibrationPaths := []string{"calibration_test_123456", "calib_nonexist_789xyz", "zz_calibrate_000"}
var samples []int

baseURI := options.uri
if !strings.HasSuffix(baseURI, "/") {
baseURI += "/"
}
// Unconditionally ensure there is exactly one trailing slash
baseURI := strings.TrimSuffix(options.uri, "/") + "/"

var lastStatusCode int
for _, path := range calibrationPaths {
Expand All @@ -342,7 +325,6 @@ func runAutocalibrate(options RequestOptions) (int, int) {
return 0, 0
}

// Calculate average and max deviation
sum := 0
for _, s := range samples {
sum += s
Expand All @@ -360,19 +342,13 @@ func runAutocalibrate(options RequestOptions) (int, int) {
}
}

// Use tolerance = max(calibrationTolerance, maxDeviation*2) to handle dynamic content
tolerance := calibrationTolerance
if maxDeviation*2 > tolerance {
tolerance = maxDeviation * 2
}

// Fragment calibration: request URI#fragment to baseline fragment-stripped responses.
// Since # is a fragment separator, the server receives the parent path instead of the
// target path. This catches false positives from any payload that accidentally creates
// a fragment URL (e.g., midpath "#" → domain.com/#path → requests domain.com/).
parsedURI, parseErr := url.Parse(options.uri)
if parseErr == nil && parsedURI.Path != "" && parsedURI.Path != "/" {
// Build parent path: /api/admin → /api/
parentPath := parsedURI.Path
if strings.HasSuffix(parentPath, "/") {
parentPath = parentPath[:len(parentPath)-1]
Expand All @@ -392,7 +368,7 @@ func runAutocalibrate(options RequestOptions) (int, int) {
if getFragmentCl() > 0 {
summary += fmt.Sprintf(" | frag %db", getFragmentCl())
}
fmt.Printf("\n%s %s\n", color.New(color.FgHiBlack, color.Bold).Sprint("calib:"), color.New(color.FgHiBlack).Sprint(summary))
fmt.Printf("\n%s %s\n", color.New(color.FgHiYellow, color.Bold).Sprint("CALIB:"), color.New(color.FgHiYellow).Sprint(summary))

return avgCl, tolerance
}
Expand Down Expand Up @@ -425,18 +401,17 @@ func parseRawURL(rawURI string) (*url.URL, error) {
return nil, fmt.Errorf("missing host")
}

// Split raw path and query
rawQuery := ""
if qIdx := strings.Index(rawPath, "?"); qIdx >= 0 {
rawQuery = rawPath[qIdx+1:]
rawPath = rawPath[:qIdx]
}

return &url.URL{
Scheme: scheme,
Host: host,
Opaque: rawPath,
RawQuery: rawQuery,
Scheme: scheme,
Host: host,
Opaque: rawPath,
RawQuery: rawQuery,
}, nil
}

Expand Down
26 changes: 13 additions & 13 deletions cmd/requester.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func colorizeStatusTransition(result Result) string {
if statusPriority(result.statusCode) > statusPriority(base) {
arrow = color.GreenString("=>")
} else if statusPriority(result.statusCode) < statusPriority(base) {
arrow = color.HiBlackString("->")
arrow = color.HiWhiteString("->")
}

return fmt.Sprintf("%s%s%s", colorizeStatusCode(base), arrow, colorizeStatusCode(result.statusCode))
Expand Down Expand Up @@ -1077,7 +1077,7 @@ func printFindingGroup(findings []Result, limit int, includeCurl bool) {
limit = len(collapsed)
}
for i, f := range collapsed[:limit] {
scoreColor := color.New(color.FgHiBlack)
scoreColor := color.New(color.FgHiMagenta)
techColor := color.New(color.FgWhite, color.Bold)
marker := " "
switch f.likelihood {
Expand Down Expand Up @@ -1159,10 +1159,10 @@ func colorizeWhyText(text string) string {
"location changed", color.CyanString("location changed"),
"redirect anomaly", color.CyanString("redirect anomaly"),
"type changed", color.MagentaString("type changed"),
"server changed", color.HiBlackString("server changed"),
"server changed", color.HiWhiteString("server changed"),
"len Δ", color.BlueString("len Δ"),
"unstable replay", color.MagentaString("unstable replay"),
"minor variation", color.HiBlackString("minor variation"),
"minor variation", color.HiWhiteString("minor variation"),
)
return replacer.Replace(text)
}
Expand Down Expand Up @@ -1245,8 +1245,8 @@ func printSilentTechniqueSummary() {
return
}
fmt.Printf("\n%s %s\n",
color.New(color.FgHiBlack, color.Bold).Sprint("no visible results:"),
color.New(color.FgHiBlack).Sprintf("%d techniques", len(silent)),
color.New(color.FgHiWhite, color.Bold).Sprint("no visible results:"),
color.New(color.FgHiWhite).Sprintf("%d techniques", len(silent)),
)
}

Expand Down Expand Up @@ -1480,7 +1480,7 @@ func formatPrintedResult(result Result) string {
itemWidth := terminalWidth() - 2 - techWidth - 1 - scoreWidth - 1 - codeWidth - 1 - sizeWidth - 2
item := truncateForDisplay(result.line, itemWidth)
if result.defaultReq {
techDefault := color.New(color.FgHiBlack, color.Bold).Sprintf("%-*s", techWidth, "default")
techDefault := color.New(color.FgHiWhite, color.Bold).Sprintf("%-*s", techWidth, "default")
scoreBlank := strings.Repeat(" ", scoreWidth)
return fmt.Sprintf(" %s %s %s %s %s", techDefault, scoreBlank, statusLabel, clStr, item)
}
Expand All @@ -1495,7 +1495,7 @@ func scoreColPlaceholder() string {

func formatCompactScore(score int, likelihood string) string {
marker := "."
style := color.New(color.FgHiBlack)
style := color.New(color.FgHiWhite)
switch likelihood {
case "medium":
marker = "+"
Expand Down Expand Up @@ -1606,16 +1606,16 @@ func showInfo(options RequestOptions) {
if targetWidth < 36 {
targetWidth = 36
}
labelStyle := color.New(color.FgHiBlack, color.Bold).SprintFunc()
labelStyle := color.New(color.FgHiWhite, color.Bold).SprintFunc()
valueStyle := color.New(color.FgWhite, color.Bold).SprintFunc()
meta := []string{
labelStyle("target:") + " " + valueStyle(truncateForDisplay(options.uri, targetWidth)),
labelStyle("method:") + " " + valueStyle(options.method),
labelStyle("TARGET:") + " " + valueStyle(truncateForDisplay(options.uri, targetWidth)),
labelStyle("METHOD:") + " " + valueStyle(options.method),
}
if len(options.frontendHints) > 0 {
meta = append(meta, labelStyle("frontend:")+" "+valueStyle(strings.Join(options.frontendHints, ", ")))
meta = append(meta, labelStyle("FRONTEND:")+" "+valueStyle(strings.Join(options.frontendHints, ", ")))
}
meta = append(meta, labelStyle("payloads:")+" "+valueStyle(options.folder))
meta = append(meta, labelStyle("PAYLOADS:")+" "+valueStyle(options.folder))
fmt.Println(strings.Join(meta, " "))
}

Expand Down
Loading