diff --git a/lib/agent/go.mod b/lib/agent/go.mod index bc6b67d45..2bbd9ba12 100644 --- a/lib/agent/go.mod +++ b/lib/agent/go.mod @@ -1,21 +1,21 @@ module main -go 1.23.0 +go 1.24.0 -toolchain go1.23.3 +toolchain go1.24.8 require ( github.com/stretchr/testify v1.9.0 - google.golang.org/grpc v1.74.2 + google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.6 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/lib/agent/go.sum b/lib/agent/go.sum index 8c68dd312..dbf5b3672 100644 --- a/lib/agent/go.sum +++ b/lib/agent/go.sum @@ -24,6 +24,10 @@ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= @@ -32,6 +36,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= @@ -40,6 +46,10 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= @@ -48,6 +58,10 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= @@ -58,6 +72,10 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= @@ -67,4 +85,4 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/lib/request-processor/context/event_getters.go b/lib/request-processor/context/event_getters.go index 513479fdf..5492bef0b 100644 --- a/lib/request-processor/context/event_getters.go +++ b/lib/request-processor/context/event_getters.go @@ -49,6 +49,9 @@ func GetModule() string { func getHostNameAndPort(urlCallbackId int) (string, uint32) { // urlcallbackid is the type of data we request, eg C.OUTGOING_REQUEST_URL urlStr := Context.Callback(urlCallbackId) + // remove all control characters (< 32) and 0x7f(DEL) also replace \@ with @ and remove all whitespace + // url.Parse fails if the url contains control characters + urlStr = helpers.NormalizeRawUrl(urlStr) urlParsed, err := url.Parse(urlStr) if err != nil { return "", 0 diff --git a/lib/request-processor/helpers/normalizeRequestUrl.go b/lib/request-processor/helpers/normalizeRequestUrl.go new file mode 100644 index 000000000..fe35b1a30 --- /dev/null +++ b/lib/request-processor/helpers/normalizeRequestUrl.go @@ -0,0 +1,90 @@ +package helpers + +import ( + "net/url" + "regexp" + "strings" +) + +// remove all control characters (< 32) and 0x7f(DEL) + whitespace +func removeCTLByte(urlStr string) string { + for i := 0; i < len(urlStr); i++ { + if urlStr[i] <= ' ' || urlStr[i] == 0x7f { + urlStr = urlStr[:i] + urlStr[i+1:] + } + } + return urlStr +} + +func removeUserInfo(raw string) string { + schemeEnd := strings.Index(raw, "://") + if schemeEnd == -1 { + // No scheme, can't safely identify authority + return raw + } + + scheme := raw[:schemeEnd+3] + rest := raw[schemeEnd+3:] + + // Authority is up to first '/', '?', or '#' (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2) + authorityEnd := len(rest) + for _, sep := range []string{"/", "?", "#"} { + if idx := strings.Index(rest, sep); idx != -1 && idx < authorityEnd { + authorityEnd = idx + } + } + + authority := rest[:authorityEnd] + path := rest[authorityEnd:] + + // Remove userinfo if present + if at := strings.LastIndex(authority, "@"); at != -1 { + authority = authority[at+1:] + } + + return scheme + authority + path +} + +func UnescapeUrl(urlStr string) string { + unescapedUrl, err := url.QueryUnescape(urlStr) + if err != nil { + return urlStr + } + return unescapedUrl +} + +// ConvertIPv6Mapped converts IPv6-mapped IPv4 (only if it contains ::ffff:) +// Example: "http://[::ffff:10.0.0.1]" -> "http://10.0.0.1" +func convertIPv6Mapped(input string) string { + // Return immediately if not IPv6-mapped form + if !strings.Contains(input, "::ffff:") { + return input + } + + // Extract URL scheme if present (http://, https://, etc.) + scheme := "" + if strings.Contains(input, "://") { + parts := strings.SplitN(input, "://", 2) + scheme = parts[0] + "://" + input = parts[1] + } + + // Strip brackets + input = strings.TrimPrefix(input, "[") + input = strings.TrimSuffix(input, "]") + + // Replace ::ffff:x.x.x.x -> x.x.x.x + re := regexp.MustCompile(`::ffff:(\d+\.\d+\.\d+\.\d+)`) + ip := re.ReplaceAllString(input, "$1") + + return scheme + ip +} + +func NormalizeRawUrl(urlStr string) string { + urlStr = UnescapeUrl(urlStr) + urlStr = removeCTLByte(urlStr) + urlStr = FixURL(urlStr) + urlStr = removeUserInfo(urlStr) + urlStr = convertIPv6Mapped(urlStr) + return urlStr +} diff --git a/lib/request-processor/helpers/normalizeRequestUrl_test.go b/lib/request-processor/helpers/normalizeRequestUrl_test.go new file mode 100644 index 000000000..a179e5320 --- /dev/null +++ b/lib/request-processor/helpers/normalizeRequestUrl_test.go @@ -0,0 +1,24 @@ +package helpers + +import "testing" + +func TestNormalizeRequestUrl(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"http://localhost:4000", "http://localhost:4000"}, + {"http://localhost:4000 ", "http://localhost:4000"}, + {"http://localhost:4000" + "\x00", "http://localhost:4000"}, + {"http://\\@localhost:4000", "http://localhost:4000"}, + {"http://127.1.1.1:4000\\\\\\@127.0.0.1:80/", "http://127.0.0.1:80/"}, + {"https:/localhost:4000", "https://localhost:4000"}, + {"http://127%2E0%2E0%2E1:4000", "http://127.0.0.1:4000"}, + } + for _, test := range tests { + result := NormalizeRawUrl(test.input) + if result != test.expected { + t.Errorf("For input '%s', expected %v but got %v", test.input, test.expected, result) + } + } +} diff --git a/lib/request-processor/helpers/tryParseURL.go b/lib/request-processor/helpers/tryParseURL.go index 648b59074..b1323de0a 100644 --- a/lib/request-processor/helpers/tryParseURL.go +++ b/lib/request-processor/helpers/tryParseURL.go @@ -1,14 +1,16 @@ package helpers import ( + "net" "net/url" + "strconv" "golang.org/x/net/idna" ) func TryParseURL(input string) *url.URL { - parsedURL, err := url.ParseRequestURI(input) - if err != nil { + parsedURL, err := url.Parse(input) + if err != nil || parsedURL.Host == "" { return nil } @@ -17,5 +19,26 @@ func TryParseURL(input string) *url.URL { if err == nil { parsedURL.Host = parsedHost } + // If the port is not present, we need to add it based on the scheme + if parsedURL.Port() == "" { + port := 0 + switch parsedURL.Scheme { + case "http": + port = 80 + case "https": + port = 443 + } + parsedURL.Host = parsedURL.Host + ":" + strconv.Itoa(int(port)) + } + host, port, err := net.SplitHostPort(parsedURL.Host) + if err == nil { + ip := net.ParseIP(host) + if ip != nil { + parsedURL.Host = ip.String() + ":" + port + } else { + parsedURL.Host = host + ":" + port + } + } + return parsedURL } diff --git a/lib/request-processor/helpers/tryParseURL_test.go b/lib/request-processor/helpers/tryParseURL_test.go index 940a57ec6..23e6bfde5 100644 --- a/lib/request-processor/helpers/tryParseURL_test.go +++ b/lib/request-processor/helpers/tryParseURL_test.go @@ -14,7 +14,7 @@ func TestTryParseURL_InvalidURL(t *testing.T) { } func TestTryParseURL_ValidURL(t *testing.T) { - input := "https://example.com" + input := "https://example.com:443" expected, _ := url.Parse(input) result := TryParseURL(input) diff --git a/lib/request-processor/vulnerabilities/ssrf/containsPrivateIPAddress_test.go b/lib/request-processor/vulnerabilities/ssrf/containsPrivateIPAddress_test.go index 84339124d..d7ddc6e1a 100644 --- a/lib/request-processor/vulnerabilities/ssrf/containsPrivateIPAddress_test.go +++ b/lib/request-processor/vulnerabilities/ssrf/containsPrivateIPAddress_test.go @@ -142,8 +142,6 @@ var privateIPs = []string{ "0000:0000:0000:0000:0000:0000:0000:0000", "::", "::1", - "::ffff:0.0.0.0", - "::ffff:127.0.0.1", "fe80::", "fe80::1", "fe80::abc:1", diff --git a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go index 24eb55307..f5c4d90ac 100644 --- a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go +++ b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput.go @@ -2,28 +2,50 @@ package ssrf import ( "main/helpers" + "main/log" + "net/url" + "strconv" "strings" ) -func findHostnameInUserInput(userInput string, hostname string, port uint32) bool { +func getVariants(userInput string) []string { + variants := []string{userInput, "http://" + userInput, "https://" + userInput} + decodedUserInput, err := url.QueryUnescape(userInput) + if err == nil && decodedUserInput != userInput { + variants = append(variants, decodedUserInput, "http://"+decodedUserInput, "https://"+decodedUserInput) + } + return variants +} +func findHostnameInUserInput(userInput string, hostname string, port uint32) bool { + log.Debugf("findHostnameInUserInput: userInput: %s, hostname: %s, port: %d", userInput, hostname, port) if len(userInput) <= 1 { return false } + // if hostname contains : we need to add the [ and ] to the hostname (ipv6) + if strings.Contains(hostname, ":") { + hostname = "[" + hostname + "]" + } - hostnameURL := helpers.TryParseURL("http://" + hostname) + hostnameURL := helpers.TryParseURL("http://" + hostname + ":" + strconv.Itoa(int(port))) if hostnameURL == nil { return false } userInput = helpers.ExtractResourceOrOriginal(userInput) - variants := []string{userInput, "http://" + userInput, "https://" + userInput} + userInput = helpers.NormalizeRawUrl(userInput) + + variants := getVariants(userInput) for _, variant := range variants { userInputURL := helpers.TryParseURL(variant) + if userInputURL == nil { + continue + } + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 // "The host subcomponent is case-insensitive." - if userInputURL != nil && strings.EqualFold(userInputURL.Hostname(), hostnameURL.Hostname()) { + if strings.EqualFold(userInputURL.Hostname(), hostnameURL.Hostname()) { userPort := helpers.GetPortFromURL(userInputURL) if port == 0 { diff --git a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go index 17e75ee0c..91229bb9b 100644 --- a/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go +++ b/lib/request-processor/vulnerabilities/ssrf/findHostnameInUserInput_test.go @@ -11,6 +11,25 @@ func TestFindHostnameInUserInput(t *testing.T) { port uint32 expected bool }{ + {"aa:@localhost:8080", "localhost", 8080, true}, + {"http://127.1.1.1:4000∖@127.0.0.1:80/", "127.0.0.1", 80, true}, + {"http://127.1.1.1:4000\\\\@127.0.0.1:8080/", "127.0.0.1", 8080, true}, + {"http://[0:0:0:0:0:ffff:127.0.0.1]/thefile", "0:0:0:0:0:ffff:127.0.0.1", 80, true}, + {"http://[0000:0000:0000:0000:0000:0000:0000:0001]:4000", "0000:0000:0000:0000:0000:0000:0000:0001", 4000, true}, + {"http://127.0.0.1:8080#\\@127.2.2.2:80/ ", "127.0.0.1", 8080, true}, + {"http://1.1.1.1 &@127.0.0.1:4000# @3.3.3.3/", "127.0.0.1", 4000, true}, + {"http://127.1.1.1:4000:\\@@127.0.0.1:8080/", "127.0.0.1", 8080, true}, + {"http://127.1.1.1:4000\\@127.0.0.1:8080/", "127.0.0.1", 8080, true}, + {"http://127.0.0.1:8080/@/127.1.1.1:4000", "127.0.0.1", 8080, true}, + {"http://%31%32%37.%30.%30.%31:4000", "127.0.0.1", 4000, true}, + {"http://%30:4000", "0", 4000, true}, + {"http://127%2E0%2E0%2E1:4000", "127.0.0.1", 4000, true}, + {"http://[::ffff:127.0.0.1]:4000", "::ffff:127.0.0.1", 4000, true}, + {"http://[0:0:0:0:0:0:0:1]:4000", "::1", 4000, true}, + {"http://[::]:4000", "::", 4000, true}, + {"http://[0000:0000:0000:0000:0000:0000:0000:0001]:4000", "::1", 4000, true}, + {"http://[::1]:4000", "::1", 4000, true}, + {"http://[0:0::1]:4000", "::1", 4000, true}, {"https://m%C3%BCnchen.de", "münchen.de", 0, true}, {"https://münchen.de", "xn--mnchen-3ya.de", 0, true}, {"https://xn--mnchen-3ya.de", "münchen.de", 0, true}, diff --git a/tests/cli/ssrf/test_ssrf_obfuscated_host.phpt b/tests/cli/ssrf/test_ssrf_obfuscated_host.phpt new file mode 100644 index 000000000..519195396 --- /dev/null +++ b/tests/cli/ssrf/test_ssrf_obfuscated_host.phpt @@ -0,0 +1,62 @@ +--TEST-- +Test ssrf - obfuscated host + +--ENV-- +AIKIDO_LOG_LEVEL=INFO +AIKIDO_BLOCK=1 + +--POST-- +test=http://127.1.1.1:4000\@127.0.0.1:4000 + +--FILE-- + ['pipe', 'r'], + 1 => ['file', '/dev/null', 'a'], + 2 => ['file', '/dev/null', 'a'] +]; + +try { + // Start PHP server + $process = proc_open("php -S $host:$port", $descriptorspec, $pipes); + if (!is_resource($process)) { + throw new RuntimeException("Failed to start PHP server."); + } + + $status = proc_get_status($process); + $pid = $status['pid']; + + // Wait a moment to ensure server starts + sleep(1); + + // Perform the cURL request + $ch1 = curl_init("http://127.0.0.1:4000"); + curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch1, CURLOPT_FOLLOWLOCATION, true); + $response = curl_exec($ch1); + curl_close($ch1); + + echo "Response:\n$response\n"; + +} catch (Throwable $e) { + echo "Error: " . $e->getMessage() . "\n"; +} finally { + // Ensure the server is killed if started + if ($pid) { + exec("kill -9 $pid"); + } + if (isset($process) && is_resource($process)) { + proc_close($process); + } +} + + + + +--EXPECTREGEX-- +.*Aikido firewall has blocked a server-side request forgery.*