Skip to content
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

Enhance agnhost netexec for SSRF E2Es #92850

Merged
merged 5 commits into from Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion test/images/agnhost/README.md
Expand Up @@ -393,7 +393,7 @@ Starts a HTTP(S) server on given port with the following endpoints:
- `protocol`: The protocol which will be used when making the request. Default value: `http`.
Acceptable values: `http`, `udp`, `sctp`.
- `tries`: The number of times the request will be performed. Default value: `1`.
- `/echo`: Returns the given `msg` (`/echo?msg=echoed_msg`)
- `/echo`: Returns the given `msg` (`/echo?msg=echoed_msg`), with the optional status `code`.
- `/exit`: Closes the server with the given code and graceful shutdown. The endpoint's parameters
are:
- `code`: The exit code for the process. Default value: 0. Allows an integer [0-127].
Expand All @@ -407,6 +407,8 @@ Starts a HTTP(S) server on given port with the following endpoints:
it exited.
- `/hostname`: Returns the server's hostname.
- `/hostName`: Returns the server's hostname.
- `/redirect`: Returns a redirect response to the given `location`, with the optional status `code`
(`/redirect?location=/echo%3Fmsg=foobar&code=307`).
- `/shell`: Executes the given `shellCommand` or `cmd` (`/shell?cmd=some-command`) and
returns a JSON containing the fields `output` (command's output) and `error` (command's
error message). Returns `200 OK` if the command succeeded, `417 Expectation Failed` if not.
Expand All @@ -419,6 +421,9 @@ If `--tls-cert-file` is added (ideally in conjunction with `--tls-private-key-fi
will be upgraded to HTTPS. The image has default, `localhost`-based cert/privkey files at
`/localhost.crt` and `/localhost.key` (see: [`porter` subcommand](#porter))

If `--http-override` is set, the HTTP(S) server will always serve the override path & options,
ignoring the request URL.

It will also start a UDP server on the indicated UDP port that responds to the following commands:

- `hostname`: Returns the server's hostname
Expand Down
2 changes: 1 addition & 1 deletion test/images/agnhost/VERSION
@@ -1 +1 @@
2.24
2.25
101 changes: 77 additions & 24 deletions test/images/agnhost/netexec/netexec.go
Expand Up @@ -40,13 +40,14 @@ import (
)

var (
httpPort = 8080
udpPort = 8081
sctpPort = -1
shellPath = "/bin/sh"
serverReady = &atomicBool{0}
certFile = ""
privKeyFile = ""
httpPort = 8080
udpPort = 8081
sctpPort = -1
shellPath = "/bin/sh"
serverReady = &atomicBool{0}
certFile = ""
privKeyFile = ""
httpOverride = ""
)

// CmdNetexec is used by agnhost Cobra.
Expand All @@ -69,7 +70,7 @@ var CmdNetexec = &cobra.Command{
- "protocol": The protocol which will be used when making the request. Default value: "http".
Acceptable values: "http", "udp", "sctp".
- "tries": The number of times the request will be performed. Default value: "1".
- "/echo": Returns the given "msg" ("/echo?msg=echoed_msg")
- "/echo": Returns the given "msg" ("/echo?msg=echoed_msg"), with the optional status "code".
- "/exit": Closes the server with the given code and graceful shutdown. The endpoint's parameters
are:
- "code": The exit code for the process. Default value: 0. Allows an integer [0-127].
Expand All @@ -83,6 +84,8 @@ var CmdNetexec = &cobra.Command{
it exited.
- "/hostname": Returns the server's hostname.
- "/hostName": Returns the server's hostname.
- "/redirect": Returns a redirect response to the given "location", with the optional status "code"
("/redirect?location=/echo%3Fmsg=foobar&code=307").
- "/shell": Executes the given "shellCommand" or "cmd" ("/shell?cmd=some-command") and
returns a JSON containing the fields "output" (command's output) and "error" (command's
error message). Returns "200 OK" if the command succeeded, "417 Expectation Failed" if not.
Expand All @@ -91,6 +94,13 @@ var CmdNetexec = &cobra.Command{
Returns a JSON with the fields "output" (containing the file's name on the server) and
"error" containing any potential server side errors.

If "--tls-cert-file" is added (ideally in conjunction with "--tls-private-key-file", the HTTP server
will be upgraded to HTTPS. The image has default, "localhost"-based cert/privkey files at
"/localhost.crt" and "/localhost.key" (see: "porter" subcommand)

If "--http-override" is set, the HTTP(S) server will always serve the override path & options,
ignoring the request URL.

It will also start a UDP server on the indicated UDP port that responds to the following commands:

- "hostname": Returns the server's hostname
Expand All @@ -112,6 +122,7 @@ func init() {
"File containing an x509 private key matching --tls-cert-file")
CmdNetexec.Flags().IntVar(&udpPort, "udp-port", 8081, "UDP Listen Port")
CmdNetexec.Flags().IntVar(&sctpPort, "sctp-port", -1, "SCTP Listen Port")
CmdNetexec.Flags().StringVar(&httpOverride, "http-override", "", "Override the HTTP handler to always respond as if it were a GET with this path & params")
tallclair marked this conversation as resolved.
Show resolved Hide resolved
}

// atomicBool uses load/store operations on an int32 to simulate an atomic boolean.
Expand All @@ -135,7 +146,21 @@ func (a *atomicBool) get() bool {

func main(cmd *cobra.Command, args []string) {
exitCh := make(chan shutdownRequest)
addRoutes(exitCh)
if httpOverride != "" {
mux := http.NewServeMux()
addRoutes(mux, exitCh)

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
overrideReq, err := http.NewRequestWithContext(r.Context(), "GET", httpOverride, nil)
if err != nil {
http.Error(w, fmt.Sprintf("override request failed: %v", err), http.StatusInternalServerError)
return
}
mux.ServeHTTP(w, overrideReq)
})
} else {
addRoutes(http.DefaultServeMux, exitCh)
}

go startUDPServer(udpPort)
if sctpPort != -1 {
Expand All @@ -150,22 +175,24 @@ func main(cmd *cobra.Command, args []string) {
}
}

func addRoutes(exitCh chan shutdownRequest) {
http.HandleFunc("/", rootHandler)
http.HandleFunc("/clientip", clientIPHandler)
http.HandleFunc("/echo", echoHandler)
http.HandleFunc("/exit", func(w http.ResponseWriter, req *http.Request) { exitHandler(w, req, exitCh) })
http.HandleFunc("/hostname", hostnameHandler)
http.HandleFunc("/shell", shellHandler)
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/dial", dialHandler)
http.HandleFunc("/healthz", healthzHandler)
func addRoutes(mux *http.ServeMux, exitCh chan shutdownRequest) {
mux.HandleFunc("/", rootHandler)
mux.HandleFunc("/clientip", clientIPHandler)
mux.HandleFunc("/dial", dialHandler)
mux.HandleFunc("/echo", echoHandler)
mux.HandleFunc("/exit", func(w http.ResponseWriter, req *http.Request) { exitHandler(w, req, exitCh) })
mux.HandleFunc("/healthz", healthzHandler)
mux.HandleFunc("/hostname", hostnameHandler)
mux.HandleFunc("/redirect", redirectHandler)
mux.HandleFunc("/shell", shellHandler)
mux.HandleFunc("/upload", uploadHandler)
// older handlers
http.HandleFunc("/hostName", hostNameHandler)
http.HandleFunc("/shutdown", shutdownHandler)
mux.HandleFunc("/hostName", hostNameHandler)
mux.HandleFunc("/shutdown", shutdownHandler)
}

func startServer(server *http.Server, exitCh chan shutdownRequest, fn func() error) {
log.Printf("Started HTTP server on port %d", httpPort)
go func() {
re := <-exitCh
ctx, cancelFn := context.WithTimeout(context.Background(), re.timeout)
Expand All @@ -190,8 +217,18 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("GET /echo?msg=%s", r.FormValue("msg"))
fmt.Fprintf(w, "%s", r.FormValue("msg"))
msg := r.FormValue("msg")
codeString := r.FormValue("code")
log.Printf("GET /echo?msg=%s&code=%s", msg, codeString)
if codeString != "" {
code, err := strconv.Atoi(codeString)
if err != nil && codeString != "" {
fmt.Fprintf(w, "argument 'code' must be an integer or empty, got %q\n", codeString)
return
}
w.WriteHeader(code)
}
fmt.Fprintf(w, "%s", msg)
}

func clientIPHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -488,6 +525,22 @@ func hostNameHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, getHostName())
}

func redirectHandler(w http.ResponseWriter, r *http.Request) {
location := r.FormValue("location")
codeString := r.FormValue("code")
log.Printf("%s /redirect?msg=%s&code=%s", r.Method, location, codeString)
code := http.StatusFound
if codeString != "" {
var err error
code, err = strconv.Atoi(codeString)
if err != nil && codeString != "" {
fmt.Fprintf(w, "argument 'code' must be an integer or empty, got %q\n", codeString)
return
}
}
http.Redirect(w, r, location, code)
}

// udp server supports the hostName, echo and clientIP commands.
func startUDPServer(udpPort int) {
serverAddress, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", udpPort))
Expand All @@ -497,7 +550,7 @@ func startUDPServer(udpPort int) {
defer serverConn.Close()
buf := make([]byte, 2048)

log.Printf("Started UDP server")
log.Printf("Started UDP server on port %d", udpPort)
// Start responding to readiness probes.
serverReady.set(true)
defer func() {
Expand Down