Skip to content

Commit

Permalink
Merge pull request #92850 from tallclair/netexec
Browse files Browse the repository at this point in the history
Enhance agnhost netexec for SSRF E2Es
  • Loading branch information
k8s-ci-robot committed Nov 3, 2020
2 parents 43edb40 + c05a350 commit 17376e6
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 25 deletions.
7 changes: 6 additions & 1 deletion test/images/agnhost/README.md
Original file line number Diff line number Diff line change
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
101 changes: 77 additions & 24 deletions test/images/agnhost/netexec/netexec.go
Original file line number Diff line number Diff line change
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")
}

// 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

0 comments on commit 17376e6

Please sign in to comment.