Because the writer of the Cow Say What?
problem from this 12 months’s BSidesSF CTF, I
bought lots of questions on it after the CTF ended. It’s each surprisingly
straight-forward but additionally a really little-known concern.
The problem was an internet problem – when you visited the service, you bought a web page
offering a textarea for enter to the cowsay
program, in addition to a drop down for the fashion of the cow saying one thing
(plain, stoned, lifeless, and so forth.). There was a hyperlink to the supply code, reproduced
right here:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
bundle predominant
import (
"fmt"
"html/template"
"io"
"log"
"internet/http"
"os"
"os/exec"
"regexp"
)
const (
COWSAY_PATH = "/usr/video games/cowsay"
)
var (
modeRE = regexp.MustCompilePOSIX("^-(b|d|g|p|s|t|w)$")
)
// Be aware: mode should be validated previous to working this!
func cowsay(mode, message string) (string, error) {
cowcmd := fmt.Sprintf("%s %s -n", COWSAY_PATH, mode)
log.Printf("Working cowsay as: %s", cowcmd)
cmd := exec.Command("/bin/sh", "-c", cowcmd)
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
go func() {
defer stdin.Shut()
io.WriteString(stdin, message)
}()
outbuf, err := cmd.Output()
if err != nil {
return "", err
}
return string(outbuf), nil
}
func checkMode(mode string) error {
if mode == "" {
return nil
}
if !modeRE.MatchString(mode) {
return fmt.Errorf("Mode should match regexp: %s", modeRE.String())
}
return nil
}
const cowTemplateSource = `
<!doctype html>
<html>
<h1>Cow Say What?</h1>
<p>I really like <a href="https://www.mankier.com/1/cowsay">cowsay</a> a lot that
I needed to convey it to the online. Take pleasure in!</p>
{{if .Error}}
<p><b>{{.Error}}</b></p>
{{finish}}
<type technique="POST" motion="https://systemoverlord.com/">
<choose identify="mode">
<possibility worth="">Plain</possibility>
<possibility worth="-b">Borg</possibility>
<possibility worth="-d">Lifeless</possibility>
<possibility worth="-g">Grasping</possibility>
<possibility worth="-p">Paranoid</possibility>
<possibility worth="-s">Stoned</possibility>
<possibility worth="-t">Drained</possibility>
<possibility worth="-w">Wired</possibility>
</choose><br />
<textarea identify="message" placeholder="message" cols="60" rows="10">{{.Message}}</textarea><br />
<enter kind="submit" worth="Say"><br />
</type>
{{if .CowSay}}
<pre>{{.CowSay}}</pre>
{{finish}}
<p>Take a look at <a href="http://systemoverlord.com/cowsay.go">the way it works</a>.</p>
</html>
`
var cowTemplate = template.Should(template.New("cowsay").Parse(cowTemplateSource))
kind tmplVars struct {
Error string
CowSay string
Message string
}
func cowsayHandler(w http.ResponseWriter, r *http.Request) {
vars := tmplVars{}
if r.Technique == http.MethodPost {
mode := r.FormValue("mode")
message := r.FormValue("message")
vars.Message = message
if err := checkMode(mode); err != nil {
vars.Error = err.Error()
} else {
if mentioned, err := cowsay(mode, message); err != nil {
log.Printf("Error working cowsay: %v", err)
vars.Error = "An error occurred working cowsay."
} else {
vars.CowSay = mentioned
}
}
}
cowTemplate.Execute(w, vars)
}
func sourceHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "cowsay.go")
}
func predominant() {
addr := "0.0.0.0:6789"
if len(os.Args) > 1 {
addr = os.Args[1]
}
http.HandleFunc("/cowsay.go", sourceHandler)
http.HandleFunc("https://systemoverlord.com/", cowsayHandler)
log.Deadly(http.ListenAndServe(addr, nil))
}
There’s a couple of issues to unpack right here, however most likely most important is that
the cowsay output is produced by invoking an exterior program. Notably, it
passes the message through stdin
, and the mode as an argument to this system. The
whole program is invoked through sh -c
, which makes this much like the
system(3)
libc
operate.
The mode is validated through a daily expression. As Jamie Zawinski was opined
(and Jeff Atwood has commented
on):
Some folks, when confronted with an issue, assume “I do know, I’ll use
common expressions.” Now they’ve two issues.
Nicely, it seems we do have two issues. Our common expression is given by
the assertion:
1
modeRE = regexp.MustCompilePOSIX("^-(b|d|g|p|s|t|w)$")
We will use a software like regex101.com to play
round with our expression. Particularly, it seems that it ought to encompass
a -
adopted by one of many characters separated by pipes throughout the
parentheses. At first, this seems fairly limiting, nonetheless, if we look at the
Go regexp
documentation, we would
discover a couple of oddities. Particularly, ^
is outlined as “at starting of textual content or
line (flag m=true)” and $
as “at finish of textual content … or line (flag m=true)”. So
apparently two of our particular characters have totally different meanings relying on
some “flags”.
There are not any flags in our common expression, so we’re utilizing regardless of the
defaults are. Trying on the documentation for
Flags, we see that there are
two default units of flags: Perl
and POSIX
. Barely surprisingly, the
constants use an inverted which means for the m
flag: OneLine
, which causes the
common expression engine to “deal with ^ and $ as solely matching at starting and
finish of textual content”. This flag is not included in POSIX
(in reality, no flags are),
so in a POSIX RE, ^
and $
match the start and finish of traces
respectively.
Our check for the Regexp to match is
MatchString
, which is
documented as:
MatchString experiences whether or not the string s comprises any match of the common
expression re.
Be aware that the check is “comprises any match”. If ^
and $
matched starting
and finish of enter, that will require all the string to match, however since
they’re matching starting and finish of line, as long as the enter comprises a
line matching the common expression, then MatchString
will return true.
This now means we are able to go arbitrary enter through the mode
parameter, which
will probably be immediately interpolated into the string handed to sh -c
. Put one other
manner, we now have a Command
Injection vulnerability.
We simply must additionally embrace a line that matches our common expression.
To ship a parameter containing a newline, we merely must URL encode
(typically referred to as p.c encoding) the character, leading to %0A
. This may
be exploited with a easy cURL command:
1
2
3
curl 'https://cow-say-what-473bf31e.challenges.bsidessf.internet/'
-H 'Content material-Kind: utility/x-www-form-urlencoded'
--data-raw 'mode=-dpercent0acat flag.txt #&message=foo'
The -dpercent0a
matches the common expression, then now we have a command injected
(cat flag.txt
) and begin a remark (#
) to simply ignore the remainder of the
command.
1
2
3
4
5
6
7
8
9
_____
< foo >
-----
^__^
(xx)_______
(__) )/
U ||----w |
|| ||
CTF{dont_have_a_cow_have_a_flag}