From b86ee9535b3c2b4cab9083a18880a91c6f2113b7 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Thu, 20 Nov 2025 18:24:54 -0700 Subject: [PATCH 1/4] Add ASP.NET State Helpers --- aspnet/aspnet.go | 143 ++++++++++++++++++++++++++++++++++++++++++ aspnet/aspnet_test.go | 126 +++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 aspnet/aspnet.go create mode 100644 aspnet/aspnet_test.go diff --git a/aspnet/aspnet.go b/aspnet/aspnet.go new file mode 100644 index 0000000..b48ac3b --- /dev/null +++ b/aspnet/aspnet.go @@ -0,0 +1,143 @@ +// Package aspnet provides helper functions to deal with ASP.NET and C# applications that utilize the state preserving hidden fields. These are notoriously annoying to automate and require multiple requests per action and often simulate user interaction clicks. The ASPState type helps speed up development of those requests. +// +// The package can be used to facility chains of go-exploit requests to ASP.NET applications like so: +// +// state := aspnet.State{} +// resp, body, ok := protocol.HTTPSendAndRecvWith("GET", conf.GenerateURL("/management/AdminDatabase.aspx"), "") +// if !ok { +// output.PrintError("Could not retrieve to the admin database endpoint") +// +// return false +// } +// +// state.Update(body) +// +// // Now only the parameters that are required can be utilized and no special body parsing +// // for __VIEWSTATE and friends is required. +// p := state.MergeParams(map[string]string{ +// "__EVENTTARGET": "ctl00$MainContent$DatabaseType", +// "ctl00%24MainContent%24DatabaseType": "psql", +// }) +// params := protocol.CreateRequestParamsEncoded(p) +// headers["Content-Type"] = "application/x-www-form-urlencoded" +// resp, body, ok = protocol.HTTPSendAndRecvWithHeaders("POST", conf.GenerateURL("/management/AdminDatabase.aspx"), params, headers) +// if !ok { +// output.PrintError("Could not POST to the admin database endpoint") +// +// return false +// } +// +// // Update the state from the previous POST response, this time we only want the states and have no content +// state.Update(body) +// params := protocol.CreateRequestParamsEncoded(state.AsParams()) +// resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("POST", conf.GenerateURL("/management/AdminDatabase.aspx"), params, headers) +// if !ok { +// output.PrintError("Could not POST to the admin database endpoint") +// +// return false +// } +package aspnet + +import ( + "maps" + "strings" + + "github.com/antchfx/htmlquery" +) + +// State represents the current state of the steps in a request chain for a ASP.NET application. The state should have all possible ASP.NET common state values represented and if they are not set in the current request state will be nil. This state structt only covers __VIEWSTATE, __VIEWSTATEGENERATOR, __EVENTVALIDATION, __EVENTARGUMENT, __EVENTTARGET, and __LASTFOCUS. The __EVENTTARGET and __EVENTARGUMENT are purposefully not omitted as there are often multiple or non-state required targets, so ensure they are set to the specific target. +type State struct { + ViewState *string + ViewStateGenerator *string + EventTarget *string + EventValidation *string + EventArgument *string + LastFocus *string +} + +// xPathQuiet is similar to search.XPath, but does not trigger framework errors as these can be expected to be empty. +func xPathQuiet(document, path string) (string, bool) { + doc, err := htmlquery.Parse(strings.NewReader(document)) + if err != nil { + return "", false + } + n := htmlquery.FindOne(doc, path) + if n == nil { + return "", false + } + + return htmlquery.InnerText(n), true +} + +// AsParams creates a map structure for use with the protocol package HTTP helpers or in their raw map form. If the last process state did not have one of the parameters it will not be set, but empty string values are preserved. +func (state *State) AsParams() map[string]string { + u := map[string]string{} + if state.ViewState != nil { + u["__VIEWSTATE"] = *state.ViewState + } + if state.ViewStateGenerator != nil { + u["__VIEWSTATEGENERATOR"] = *state.ViewStateGenerator + } + if state.EventValidation != nil { + u["__EVENTVALIDATION"] = *state.EventValidation + } + if state.EventArgument != nil { + u["__EVENTARGUMENT"] = *state.EventArgument + } + if state.EventTarget != nil { + u["__EVENTTARGET"] = *state.EventTarget + } + if state.LastFocus != nil { + u["__LASTFOCUS"] = *state.LastFocus + } + + return u +} + +// MergeParams merges the hand written or custom parameters and the ASP.NET state parameters to allow for a single call to protocol.CreateRequestParamsEncoded for both the current state and any modifications that are necessary. The same rules for parameter empty vs not found exist as AsParams. The parameters passed in the function will override the underlying state values if they are passed. +func (state *State) MergeParams(p map[string]string) map[string]string { + params := state.AsParams() + maps.Copy(params, p) + + return params +} + +// Update the State to extract the supported state values and reset the parameters that are not found. This should be called after each HTTP request that requires state updates. This update only works on the first matched state document and if multiple states are set on the expected page manual updating may be required. +func (state *State) Update(body string) { + v, hasMatch := xPathQuiet(body, `//input[@name="__VIEWSTATE"]/@value`) + if hasMatch { + state.ViewState = &v + } else { + state.ViewState = nil + } + vg, hasMatch := xPathQuiet(body, `//input[@name="__VIEWSTATEGENERATOR"]/@value`) + if hasMatch { + state.ViewStateGenerator = &vg + } else { + state.ViewStateGenerator = nil + } + ev, hasMatch := xPathQuiet(body, `//input[@name="__EVENTVALIDATION"]/@value`) + if hasMatch { + state.EventValidation = &ev + } else { + state.EventValidation = nil + } + et, hasMatch := xPathQuiet(body, `//input[@name="__EVENTTARGET"]/@value`) + if hasMatch { + state.EventTarget = &et + } else { + state.EventTarget = nil + } + ea, hasMatch := xPathQuiet(body, `//input[@name="__EVENTARGUMENT"]/@value`) + if hasMatch { + state.EventTarget = &ea + } else { + state.EventTarget = nil + } + lf, hasMatch := xPathQuiet(body, `//input[@name="__LASTFOCUS"]/@value`) + if hasMatch { + state.LastFocus = &lf + } else { + state.LastFocus = nil + } +} diff --git a/aspnet/aspnet_test.go b/aspnet/aspnet_test.go new file mode 100644 index 0000000..42ae288 --- /dev/null +++ b/aspnet/aspnet_test.go @@ -0,0 +1,126 @@ +package aspnet_test + +import ( + "testing" + + "github.com/vulncheck-oss/go-exploit/aspnet" +) + +var pageState1 = ` + + + Gladinet Cloud Cluster + + +
+
+ + + + +
+
+ + +
+ +` + +var pageState2 = ` + + + Gladinet Cloud Cluster + + + +
+ + + + +
+
+ + +
+ + +` + +func TestState_Full(t *testing.T) { + state := aspnet.State{} + p := state.AsParams() + if len(p) != 0 { + t.Error("Parameters should not have state currently") + } + + state.Update(pageState1) + p = state.AsParams() + if len(p) == 0 { + t.Error("Parameters should have state currently") + } + if len(p) != 5 { + t.Errorf("First state should only have 5 values: %d - %#v", len(p), p) + } + + value, exists := p["__VIEWSTATE"] + if !exists { + t.Error("ViewState should be set on first request state update") + } + if value != `/wEPDwULLTE4OTcxMDA5NzIPZBYCZg9kFgQCAw8WAh4EVGV4dGVkAgUPZBYIAgYPZBYCAjsPEGQPFgRmAgECAgIDFgQQBRREZWZhdWx0IC0gYWxsIGluIG9uZQUHZGVmYXVsdGcQBQZNeSBTcWwFBW15c3FsZxAFClNRTCBTZXJ2ZXIFA3NxbGcQBQpQb3N0Z3JlU1FMBQRwc3FsZxYBZmQCCA8PFgIeC05hdmlnYXRlVXJsBSVodHRwOi8vd3d3LmdsYWRpbmV0LmNvbS9wL2NvbnRhY3QuaHRtZGQCCQ8PFgIfAQUjaHR0cDovL3d3dy5nbGFkaW5ldC5jb20vcC90ZXJtcy5odG1kZAIKDw8WAh8BBSVodHRwOi8vd3d3LmdsYWRpbmV0LmNvbS9wL3ByaXZhY3kuaHRtZGRkhIVOv1laSf4FVfKCihTCvPyajtM=` { + t.Error("ViewState on first update is unexpected") + } + + value, exists = p["__LASTFOCUS"] + if !exists { + t.Error("LastFocus should not be nil") + } + if value != `` { + t.Error("LastFocus should be set but is an empty string") + } + if state.ViewStateGenerator == nil { + t.Errorf("ViewStateGenerator should not be nil on first request: %#v", state.ViewStateGenerator) + } + + state.Update(pageState2) + p = state.AsParams() + if len(p) == 0 { + t.Error("Parameters should have state currently at state 2") + } + if len(p) != 4 { + t.Errorf("Second state should only have 4 values: %d - %#v", len(p), p) + } + if state.ViewStateGenerator != nil { + t.Errorf("ViewStateGenerator should be nil on second request: %#v", state.ViewStateGenerator) + } + if state.ViewState == nil { + t.Errorf("ViewState should be not be nil on second request: %#v", state.ViewStateGenerator) + } + if *state.ViewState != `/wEPDwULLTE4OTcxMDA5NzIPZBYCZg9kFgQCAw8WAh4EVGV4dGVkAgUPZBYIAgYPZBYGAjsPEGQPFgRmAgECAgIDFgQQBRREZWZhdWx0IC0gYWxsIGluIG9uZQUHZGVmYXVsdGcQBQZNeSBTcWwFBW15c3FsZxAFClNRTCBTZXJ2ZXIFA3NxbGcQBQpQb3N0Z3JlU1FMBQRwc3FsZxYBAgNkAj0PDxYCHgdWaXNpYmxlaGRkAkUPDxYCHwFnZGQCCA8PFgIeC05hdmlnYXRlVXJsBSVodHRwOi8vd3d3LmdsYWRpbmV0LmNvbS9wL2NvbnRhY3QuaHRtZGQCCQ8PFgIfAgUjaHR0cDovL3d3dy5nbGFkaW5ldC5jb20vcC90ZXJtcy5odG1kZAIKDw8WAh8CBSVodHRwOi8vd3d3LmdsYWRpbmV0LmNvbS9wL3ByaXZhY3kuaHRtZGQYAQUeX19Db250cm9sc1JlcXVpcmVQb3N0QmFja0tleV9fFgEFIGN0bDAwJE1haW5Db250ZW50JFBTUUxDaGtTU0xNb2Rlt1OAugQHTFQSO9InFhq1a4zTB6w=` { + t.Error("ViewState on second update is unexpected") + } +} + +func TestState_Merge(t *testing.T) { + state := aspnet.State{} + p := state.AsParams() + if len(p) != 0 { + t.Error("Parameters should not have state currently") + } + + state.Update(pageState1) + p = state.AsParams() + if len(p) == 0 { + t.Error("Parameters should have state currently") + } + if len(p) != 5 { + t.Errorf("State should only have 5 values: %d - %#v", len(p), p) + } + v := map[string]string{ + "STUFF": "THINGS", + } + merged := state.MergeParams(v) + if len(merged) != 6 { + t.Errorf("State should have 6 values: %d - %#v", len(p), p) + } +} From fe5a0c13836e480b85cc9f567294420a7f4775d1 Mon Sep 17 00:00:00 2001 From: Jacob Baines <113205286+j-baines@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:08:23 -0500 Subject: [PATCH 2/4] Update aspnet/aspnet.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnet/aspnet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnet/aspnet.go b/aspnet/aspnet.go index b48ac3b..9cd33ed 100644 --- a/aspnet/aspnet.go +++ b/aspnet/aspnet.go @@ -45,7 +45,7 @@ import ( "github.com/antchfx/htmlquery" ) -// State represents the current state of the steps in a request chain for a ASP.NET application. The state should have all possible ASP.NET common state values represented and if they are not set in the current request state will be nil. This state structt only covers __VIEWSTATE, __VIEWSTATEGENERATOR, __EVENTVALIDATION, __EVENTARGUMENT, __EVENTTARGET, and __LASTFOCUS. The __EVENTTARGET and __EVENTARGUMENT are purposefully not omitted as there are often multiple or non-state required targets, so ensure they are set to the specific target. +// State represents the current state of the steps in a request chain for a ASP.NET application. The state should have all possible ASP.NET common state values represented and if they are not set in the current request state will be nil. This state struct only covers __VIEWSTATE, __VIEWSTATEGENERATOR, __EVENTVALIDATION, __EVENTARGUMENT, __EVENTTARGET, and __LASTFOCUS. The __EVENTTARGET and __EVENTARGUMENT are purposefully not omitted as there are often multiple or non-state required targets, so ensure they are set to the specific target. type State struct { ViewState *string ViewStateGenerator *string From 8e256642d7aa546f3f8ac95045448d9789a2949b Mon Sep 17 00:00:00 2001 From: Jacob Baines <113205286+j-baines@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:08:33 -0500 Subject: [PATCH 3/4] Update aspnet/aspnet.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnet/aspnet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnet/aspnet.go b/aspnet/aspnet.go index 9cd33ed..516c223 100644 --- a/aspnet/aspnet.go +++ b/aspnet/aspnet.go @@ -1,6 +1,6 @@ // Package aspnet provides helper functions to deal with ASP.NET and C# applications that utilize the state preserving hidden fields. These are notoriously annoying to automate and require multiple requests per action and often simulate user interaction clicks. The ASPState type helps speed up development of those requests. // -// The package can be used to facility chains of go-exploit requests to ASP.NET applications like so: +// The package can be used to facilitate chains of go-exploit requests to ASP.NET applications like so: // // state := aspnet.State{} // resp, body, ok := protocol.HTTPSendAndRecvWith("GET", conf.GenerateURL("/management/AdminDatabase.aspx"), "") From 92be04a4c6ac61600742b67ae5cacde1d428fe90 Mon Sep 17 00:00:00 2001 From: terrorbyte Date: Tue, 25 Nov 2025 09:27:06 -0700 Subject: [PATCH 4/4] Fix parameter mix up. Thanks robot. --- aspnet/aspnet.go | 4 +-- aspnet/aspnet_test.go | 75 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/aspnet/aspnet.go b/aspnet/aspnet.go index b48ac3b..8c7ae56 100644 --- a/aspnet/aspnet.go +++ b/aspnet/aspnet.go @@ -130,9 +130,9 @@ func (state *State) Update(body string) { } ea, hasMatch := xPathQuiet(body, `//input[@name="__EVENTARGUMENT"]/@value`) if hasMatch { - state.EventTarget = &ea + state.EventArgument = &ea } else { - state.EventTarget = nil + state.EventArgument = nil } lf, hasMatch := xPathQuiet(body, `//input[@name="__LASTFOCUS"]/@value`) if hasMatch { diff --git a/aspnet/aspnet_test.go b/aspnet/aspnet_test.go index 42ae288..4b77af9 100644 --- a/aspnet/aspnet_test.go +++ b/aspnet/aspnet_test.go @@ -59,8 +59,8 @@ func TestState_Full(t *testing.T) { if len(p) == 0 { t.Error("Parameters should have state currently") } - if len(p) != 5 { - t.Errorf("First state should only have 5 values: %d - %#v", len(p), p) + if len(p) != 6 { + t.Errorf("First state should only have 6 values: %d - %#v", len(p), p) } value, exists := p["__VIEWSTATE"] @@ -87,8 +87,8 @@ func TestState_Full(t *testing.T) { if len(p) == 0 { t.Error("Parameters should have state currently at state 2") } - if len(p) != 4 { - t.Errorf("Second state should only have 4 values: %d - %#v", len(p), p) + if len(p) != 5 { + t.Errorf("Second state should only have 5 values: %d - %#v", len(p), p) } if state.ViewStateGenerator != nil { t.Errorf("ViewStateGenerator should be nil on second request: %#v", state.ViewStateGenerator) @@ -101,6 +101,65 @@ func TestState_Full(t *testing.T) { } } +func TestState_Each(t *testing.T) { + state := aspnet.State{} + p := state.AsParams() + if len(p) != 0 { + t.Error("Parameters should not have state currently") + } + + state.Update(pageState1) + p = state.AsParams() + if len(p) == 0 { + t.Error("Parameters should have state currently") + } + if len(p) != 6 { + t.Errorf("First state should only have 6 values: %d - %#v", len(p), p) + } + + value, exists := p["__VIEWSTATE"] + if !exists { + t.Error("ViewState should be set on first request state update") + } + if value != `/wEPDwULLTE4OTcxMDA5NzIPZBYCZg9kFgQCAw8WAh4EVGV4dGVkAgUPZBYIAgYPZBYCAjsPEGQPFgRmAgECAgIDFgQQBRREZWZhdWx0IC0gYWxsIGluIG9uZQUHZGVmYXVsdGcQBQZNeSBTcWwFBW15c3FsZxAFClNRTCBTZXJ2ZXIFA3NxbGcQBQpQb3N0Z3JlU1FMBQRwc3FsZxYBZmQCCA8PFgIeC05hdmlnYXRlVXJsBSVodHRwOi8vd3d3LmdsYWRpbmV0LmNvbS9wL2NvbnRhY3QuaHRtZGQCCQ8PFgIfAQUjaHR0cDovL3d3dy5nbGFkaW5ldC5jb20vcC90ZXJtcy5odG1kZAIKDw8WAh8BBSVodHRwOi8vd3d3LmdsYWRpbmV0LmNvbS9wL3ByaXZhY3kuaHRtZGRkhIVOv1laSf4FVfKCihTCvPyajtM=` { + t.Error("ViewState on first update is unexpected") + } + + value, exists = p["__LASTFOCUS"] + if !exists { + t.Error("LastFocus should not be nil") + } + if value != `` { + t.Error("LastFocus should be set but is an empty string") + } + value, exists = p["__VIEWSTATEGENERATOR"] + if !exists { + t.Error("ViewStateGenerator should not be nil") + } + if value != `C73717A7` { + t.Error("ViewStateGenerator on first update is unexpected") + } + value, exists = p["__EVENTVALIDATION"] + if !exists { + t.Error("EventValidation should not be nil") + } + if value != `/wEdAAdexv6/qKqWdd7V9UzkVbKnzivrZbTfl5HxflMl0WEimkj+n3ntyqDMPWej+FjsRo61P6Uqwq7GZ15buFg7WHqF4VZwC+5O3u0TMTTYeToUrXDySQQEwxvyin+PIQ6Xt1JpqJ+bt/0dmbPhJrKioUwF82Mylv8B1bqOz6F0llEnG94eilk=` { + t.Error("EventValidation on first update is unexpected") + } + if state.EventArgument == nil { + t.Errorf("EventArgument should not be nil on second request: %#v", state.ViewStateGenerator) + } + if *state.EventArgument != "" { + t.Errorf("EventArgument should be empty string on second request: %#v", state.ViewStateGenerator) + } + if state.EventTarget == nil { + t.Errorf("EventTarget should not be nil on second request: %#v", state.ViewStateGenerator) + } + if *state.EventTarget != "" { + t.Errorf("EventTarget should be empty string on second request: %#v", state.ViewStateGenerator) + } +} + func TestState_Merge(t *testing.T) { state := aspnet.State{} p := state.AsParams() @@ -113,14 +172,14 @@ func TestState_Merge(t *testing.T) { if len(p) == 0 { t.Error("Parameters should have state currently") } - if len(p) != 5 { - t.Errorf("State should only have 5 values: %d - %#v", len(p), p) + if len(p) != 6 { + t.Errorf("State should only have 6 values: %d - %#v", len(p), p) } v := map[string]string{ "STUFF": "THINGS", } merged := state.MergeParams(v) - if len(merged) != 6 { - t.Errorf("State should have 6 values: %d - %#v", len(p), p) + if len(merged) != 7 { + t.Errorf("State should have 7 values: %d - %#v", len(p), p) } }