Skip to content

config: propagate errors through Config context (#513)#560

Open
Christbowel wants to merge 1 commit intovulncheck-oss:mainfrom
Christbowel:feature/513-error-propagation
Open

config: propagate errors through Config context (#513)#560
Christbowel wants to merge 1 commit intovulncheck-oss:mainfrom
Christbowel:feature/513-error-propagation

Conversation

@Christbowel
Copy link
Copy Markdown

Problem

DoRequest returns only a bool on failure, discarding the underlying
error. This causes false-negatives with fire-and-forget payloads: the
server receives the payload, stops responding while executing it, and the
HTTP client gets a timeout or EOF, which the framework incorrectly
records as a failed exploit.

This pattern is visible in the wild in vulncheck-oss/cve-2025-55182,
where the terminal output shows:

level=ERROR   msg="HTTP request error: context deadline exceeded
              (Client.Timeout exceeded while awaiting headers)"
level=SUCCESS msg="Webshell installed!"

The exploit succeeded, but without a way to inspect the error type,
RunExploit had no choice but to return false.

Solution

Rather than changing DoRequest (which would break the existing API),
this PR adds two small, independent pieces:

config/config.go — a new Errors []error field on Config and
five methods to populate and query it:

Method Description
AddError(err) Accumulate an error from a protocol call
HasErrors() bool True if any errors are present
HasTimeoutError() bool True for context.DeadlineExceeded or net.Error.Timeout()
HasEOFError() bool True for io.EOF — server closed connection after receiving payload
ClearErrors() Reset between retries

protocol/httphelper.go — a new DoRequestErr() function that
mirrors DoRequest but returns the raw error instead of a bool,
so the exploit can pass it to conf.AddError and inspect it.

DoRequest is completely untouched — no existing exploit breaks.

Usage

client, req, ok := protocol.CreateRequest("POST", url, payload, true)
if !ok {
    return false
}

_, _, err := protocol.DoRequestErr(client, req)
if err != nil {
    conf.AddError(err)
    if conf.HasTimeoutError() || conf.HasEOFError() {
        return true // payload delivered — timeout/EOF is expected
    }
    return false
}

return true

Tests

16 new unit tests in config/config_errors_test.go cover:

  • zero-value config (no panics)
  • plain non-timeout errors
  • context.DeadlineExceeded direct and via expired context
  • net.Error with Timeout() == true and false
  • net.OpError wrapping context.DeadlineExceeded (real *url.Error shape)
  • io.EOF direct and wrapped in net.OpError
  • ClearErrors on empty and non-empty configs
  • error identity preservation via errors.Is
go test ./...         # all packages pass, zero regressions
golangci-lint run     # zero new issues introduced

Closes #513
Relates to #415

Add Errors []error field to Config with five methods:
  - AddError(err)         accumulate an error from a protocol call
  - HasErrors() bool      true if any errors are present
  - HasTimeoutError()     true for context.DeadlineExceeded / net.Error.Timeout
  - HasEOFError()         true for io.EOF (server closed connection after payload)
  - ClearErrors()         reset the error list between retries

Add DoRequestErr() to protocol/httphelper.go which returns the raw
error instead of a bool, allowing callers to inspect and accumulate it
via conf.AddError before deciding whether the failure is expected.

This fixes the false-negative described in vulncheck-oss#415 where a fire-and-forget
payload causes the HTTP client to time out (or receive EOF) while the
exploit is actually succeeding on the target.

Closes vulncheck-oss#513
Relates to vulncheck-oss#415
Copy link
Copy Markdown
Collaborator

@terrorbyte terrorbyte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your first contribution! This is pretty much exactly what I was thinking about doing in the issues you linked. My only ask is to maybe add an additional State struct to config that can hold the Rhost specific state requests so that the errors can be cleared as they iterate and to maybe add the new error handling to the core protocol HTTP calls.

Comment thread config/config.go
// Errors accumulates errors from protocol calls so exploit implementations
// can inspect the nature of failures after the fact.
// Use AddError to populate and HasTimeoutError/HasEOFError/HasErrors to query.
Errors []error
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment these error states don't get cleared here:

go-exploit/framework.go

Lines 578 to 600 in 3845e7e

for index, host := range conf.RhostsNTuple {
// setup the conf for the downstream exploit
conf.Rhost = host.Rhost
conf.Rport = host.Rport
switch host.SSL {
case config.SSLDisabled:
conf.SSL = false
conf.DetermineSSL = false
case config.SSLEnabled:
conf.SSL = true
conf.DetermineSSL = false
case config.SSLAutodiscover:
conf.SSL = false
conf.DetermineSSL = true
}
output.PrintFrameworkStatus("Starting target", "index", index, "host", conf.Rhost,
"port", conf.Rport, "ssl", conf.SSL, "ssl auto", conf.DetermineSSL)
if !doScan(sploit, conf) {
return
}
globalWG.Wait()
}

This will make it so that the errors continue through to multiple rhosts targets.

I was mulling this exact problem over recently and I think I've come to the conclusion that maybe we should add a Config.State struct that can hold the rhost specific state that also clears on a new iteration of conf.RhostsNTuple that also just clears that state for each iteration.

Comment thread config/config.go
// return false
// }
// return true
func (conf *Config) HasTimeoutError() bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome!

Comment thread protocol/httphelper.go
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add the error propagation directly into the protocol calls? I think I'd like to make this accessible uniformly across all HTTP calls!

Comment thread protocol/httphelper.go
//
// Use this variant for fire-and-forget payloads where a timeout or EOF is
// expected and should not be treated as a failure.
func DoRequestErr(client *http.Client, req *http.Request) (*http.Response, string, error) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might end up being a good place to apply #547 !

@Christbowel
Copy link
Copy Markdown
Author

Thanks for the detailed feedback! I've read through #547 and understand the direction.

For the State struct, I'll move Errors into a Config.State that gets reset in the RhostsNTuple loop in framework.go so errors don't leak across targets.

For the uniform HTTP propagation, would it make sense to wait until #547 is merged and align DoRequestErr with the new httpclient package? Happy to proceed either way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Propagate Errors Through config.Config

2 participants