-
Notifications
You must be signed in to change notification settings - Fork 51
config: propagate errors through Config context (#513) #560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,12 @@ package config | |
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
| "io" | ||
| "net" | ||
| "strings" | ||
| "text/template" | ||
|
|
||
|
|
@@ -123,6 +127,10 @@ type Config struct { | |
| FileTemplateData string | ||
| // File format exploit output | ||
| FileFormatFilePath string | ||
| // 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 | ||
| } | ||
|
|
||
| // Convert ExploitType to String. | ||
|
|
@@ -545,3 +553,70 @@ func (conf *Config) HasCustomPayload() bool { | |
|
|
||
| return len(conf.CustomPayload) > 0 | ||
| } | ||
|
|
||
| // AddError appends err to the accumulated error list on the config context. | ||
| // It is intended to be called by the exploit implementation after receiving | ||
| // a failed protocol call, so the nature of the failure can be inspected later. | ||
| func (conf *Config) AddError(err error) { | ||
| conf.Errors = append(conf.Errors, err) | ||
| } | ||
|
|
||
| // HasErrors returns true if at least one error has been accumulated. | ||
| func (conf *Config) HasErrors() bool { | ||
| return len(conf.Errors) > 0 | ||
| } | ||
|
|
||
| // HasTimeoutError returns true if any accumulated error is a network timeout. | ||
| // This covers both context.DeadlineExceeded and net.Error.Timeout(), which | ||
| // are the two forms observed in practice (see issue #415). | ||
| // | ||
| // Typical use — fire-and-forget payload where the server stops responding | ||
| // because it is executing the payload: | ||
| // | ||
| // 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() { | ||
| // return true // timeout expected: payload executing on target | ||
| // } | ||
| // return false | ||
| // } | ||
| // return true | ||
| func (conf *Config) HasTimeoutError() bool { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is awesome! |
||
| for _, err := range conf.Errors { | ||
| if errors.Is(err, context.DeadlineExceeded) { | ||
| return true | ||
| } | ||
|
|
||
| var netErr net.Error | ||
| if errors.As(err, &netErr) && netErr.Timeout() { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // HasEOFError returns true if any accumulated error is an io.EOF. | ||
| // This typically means the server closed the connection after receiving | ||
| // the payload without sending an HTTP response — a common pattern in | ||
| // exploits that cause the target process to restart or crash. | ||
| func (conf *Config) HasEOFError() bool { | ||
| for _, err := range conf.Errors { | ||
| if errors.Is(err, io.EOF) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // ClearErrors removes all accumulated errors from the config context. | ||
| // Useful when an exploit retries after a handled error. | ||
| func (conf *Config) ClearErrors() { | ||
| conf.Errors = nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| package config_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "io" | ||
| "net" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/vulncheck-oss/go-exploit/config" | ||
| ) | ||
|
|
||
| // Sentinelles pour err113 — pas d'errors.New() inline dans les fonctions. | ||
| var ( | ||
| errFirst = errors.New("first") | ||
| errSecond = errors.New("second") | ||
| errConnRefused = errors.New("connection refused") | ||
| errSomethingElse = errors.New("something else") | ||
| errOne = errors.New("one") | ||
| errSentinel = errors.New("sentinel") | ||
| ) | ||
|
|
||
| func newTestConfig() *config.Config { | ||
| return &config.Config{} | ||
| } | ||
|
|
||
| // timeoutNetError implémente net.Error avec Timeout() == true. | ||
| type timeoutNetError struct{} | ||
|
|
||
| func (timeoutNetError) Error() string { return "fake timeout" } | ||
| func (timeoutNetError) Timeout() bool { return true } | ||
| func (timeoutNetError) Temporary() bool { return true } | ||
|
|
||
| // nonTimeoutNetError implémente net.Error avec Timeout() == false. | ||
| type nonTimeoutNetError struct{} | ||
|
|
||
| func (nonTimeoutNetError) Error() string { return "fake net error" } | ||
| func (nonTimeoutNetError) Timeout() bool { return false } | ||
| func (nonTimeoutNetError) Temporary() bool { return false } | ||
|
|
||
| // --- AddError / HasErrors --- | ||
|
|
||
| func TestAddError_AccumulatesErrors(t *testing.T) { | ||
| conf := newTestConfig() | ||
|
|
||
| if conf.HasErrors() { | ||
| t.Fatal("HasErrors() doit être false sur un Config vide") | ||
| } | ||
|
|
||
| conf.AddError(errFirst) | ||
| conf.AddError(errSecond) | ||
|
|
||
| if !conf.HasErrors() { | ||
| t.Fatal("HasErrors() doit être true après AddError") | ||
| } | ||
|
|
||
| if len(conf.Errors) != 2 { | ||
| t.Fatalf("attendu 2 erreurs, obtenu %d", len(conf.Errors)) | ||
| } | ||
| } | ||
|
|
||
| func TestHasErrors_FalseOnEmpty(t *testing.T) { | ||
| conf := newTestConfig() | ||
| if conf.HasErrors() { | ||
| t.Fatal("HasErrors() doit être false sur un Config vide") | ||
| } | ||
| } | ||
|
|
||
| // --- HasTimeoutError --- | ||
|
|
||
| func TestHasTimeoutError_FalseOnEmpty(t *testing.T) { | ||
| conf := newTestConfig() | ||
| if conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être false sans erreurs") | ||
| } | ||
| } | ||
|
|
||
| func TestHasTimeoutError_FalseOnPlainError(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(errConnRefused) | ||
| if conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être false pour une erreur générique") | ||
| } | ||
| } | ||
|
|
||
| func TestHasTimeoutError_TrueOnContextDeadline(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(context.DeadlineExceeded) | ||
| if !conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être true pour context.DeadlineExceeded") | ||
| } | ||
| } | ||
|
|
||
| func TestHasTimeoutError_TrueOnExpiredContext(t *testing.T) { | ||
| conf := newTestConfig() | ||
| ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) | ||
| defer cancel() | ||
| conf.AddError(ctx.Err()) | ||
| if !conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être true pour un contexte expiré") | ||
| } | ||
| } | ||
|
|
||
| func TestHasTimeoutError_TrueOnNetErrorTimeout(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(timeoutNetError{}) | ||
| if !conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être true pour net.Error.Timeout()=true") | ||
| } | ||
| } | ||
|
|
||
| func TestHasTimeoutError_FalseOnNetErrorNonTimeout(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(nonTimeoutNetError{}) | ||
| if conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être false pour net.Error.Timeout()=false") | ||
| } | ||
| } | ||
|
|
||
| func TestHasTimeoutError_TrueOnURLErrorWrappingDeadline(t *testing.T) { | ||
| conf := newTestConfig() | ||
| wrapped := &net.OpError{ | ||
| Op: "read", | ||
| Err: context.DeadlineExceeded, | ||
| } | ||
| conf.AddError(wrapped) | ||
| if !conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être true pour net.OpError wrappant DeadlineExceeded") | ||
| } | ||
| } | ||
|
|
||
| // --- HasEOFError --- | ||
|
|
||
| func TestHasEOFError_FalseOnEmpty(t *testing.T) { | ||
| conf := newTestConfig() | ||
| if conf.HasEOFError() { | ||
| t.Fatal("HasEOFError() doit être false sans erreurs") | ||
| } | ||
| } | ||
|
|
||
| func TestHasEOFError_TrueOnIOEOF(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(io.EOF) | ||
| if !conf.HasEOFError() { | ||
| t.Fatal("HasEOFError() doit être true pour io.EOF") | ||
| } | ||
| } | ||
|
|
||
| func TestHasEOFError_TrueOnWrappedEOF(t *testing.T) { | ||
| conf := newTestConfig() | ||
| wrapped := &net.OpError{ | ||
| Op: "read", | ||
| Err: io.EOF, | ||
| } | ||
| conf.AddError(wrapped) | ||
| if !conf.HasEOFError() { | ||
| t.Fatal("HasEOFError() doit être true pour net.OpError wrappant io.EOF") | ||
| } | ||
| } | ||
|
|
||
| func TestHasEOFError_FalseOnNonEOF(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(errSomethingElse) | ||
| if conf.HasEOFError() { | ||
| t.Fatal("HasEOFError() doit être false pour une erreur non-EOF") | ||
| } | ||
| } | ||
|
|
||
| // --- ClearErrors --- | ||
|
|
||
| func TestClearErrors_ResetsSlice(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(errOne) | ||
| conf.AddError(context.DeadlineExceeded) | ||
| conf.AddError(io.EOF) | ||
|
|
||
| conf.ClearErrors() | ||
|
|
||
| if conf.HasErrors() { | ||
| t.Fatal("HasErrors() doit être false après ClearErrors") | ||
| } | ||
| if conf.HasTimeoutError() { | ||
| t.Fatal("HasTimeoutError() doit être false après ClearErrors") | ||
| } | ||
| if conf.HasEOFError() { | ||
| t.Fatal("HasEOFError() doit être false après ClearErrors") | ||
| } | ||
| if conf.Errors != nil { | ||
| t.Fatalf("Errors doit être nil après ClearErrors, obtenu %v", conf.Errors) | ||
| } | ||
| } | ||
|
|
||
| func TestClearErrors_SafeOnEmpty(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.ClearErrors() | ||
| if conf.HasErrors() { | ||
| t.Fatal("HasErrors() doit rester false après ClearErrors sur config vide") | ||
| } | ||
| } | ||
|
|
||
| // --- Préservation de l'identité des erreurs --- | ||
|
|
||
| func TestErrorIdentityPreserved(t *testing.T) { | ||
| conf := newTestConfig() | ||
| conf.AddError(errSentinel) | ||
|
|
||
| if !errors.Is(conf.Errors[0], errSentinel) { | ||
| t.Fatal("errors.Is doit retrouver l'erreur originale dans conf.Errors") | ||
| } | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to add the error propagation directly into the |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -206,6 +206,26 @@ func DoRequest(client *http.Client, req *http.Request) (*http.Response, string, | |
| return resp, string(bodyBytes), true | ||
| } | ||
|
|
||
| // DoRequestErr behaves identically to DoRequest but returns the raw error | ||
| // instead of a bool on failure. This allows the calling exploit to inspect | ||
| // the error (e.g. via conf.AddError + conf.HasTimeoutError) rather than | ||
| // receiving only a success/failure signal. | ||
| // | ||
| // 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might end up being a good place to apply #547 ! |
||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| return resp, "", fmt.Errorf("HTTP request error: %w", err) | ||
| } | ||
|
|
||
| defer resp.Body.Close() | ||
|
|
||
| bodyBytes, _ := io.ReadAll(resp.Body) | ||
|
|
||
| return resp, string(bodyBytes), nil | ||
| } | ||
|
|
||
| // Turns net/http []*Cookie into a string for adding to the Cookie header. | ||
| // | ||
| // if resp.StatusCode == 302 { | ||
|
|
||
There was a problem hiding this comment.
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
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.Statestruct that can hold therhostspecific state that also clears on a new iteration ofconf.RhostsNTuplethat also just clears that state for each iteration.