Natas20 Writeup(Session登录,注入参数)

Natas20:

读取源码,发现把sessionID存到了文件中,按键值对存在,以空格分隔,如果$_SESSION["admin"]==1,则成功登陆,得到flag。并且通过查询所提交的参数,也会被存到文件中,因此,可以采取注入键值对admin 1的方式来实现修改。
使用burp抓包,将name参数修改为:name=xxx%0Aadmin 1,得到flag。

源码解析:

<html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas20", "pass": "<censored>" };</script></head>
<body>
<h1>natas20</h1>
<div id="content">
<?

//debug($msg)表示打开了调试信息,可以通过在URL的末尾添加 /index.php?debug来查看调试消息 $msg
function debug($msg) { /* {{{ */
    //php中预定义的 $_GET 变量用于收集来自 method="get" 的表单中的值。
    //array_key_exists(key,array)函数检查键名是否存在于数组中,如果键名存在则返回 TRUE,如果键名不存在则返回 FALSE。
    if(array_key_exists("debug", $_GET)) {
        print "DEBUG: $msg<br>";
    }
}
/* }}} */
function print_credentials() { /* {{{ */
    //主要功能就是判断session[admin]=1后显示密码
    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"); 
    //strspn(string,charlist):返回在字符串string中包含charlist中字符的数目
    //判断sid是否是由数字/字母/-组成,如果不是,则无效
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return "";
    }
    //session_save_path() - 返回当前会话的保存路径。
    $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();
    //使用换行符分割data数据
    foreach(explode("\n", $data) as $line) {
        debug("Read [$line]");
    //使用空格符分割line数据,返回的数组包含2个元素,最后那个元素包含line的剩余部分
    $parts = explode(" ", $line, 2);
    if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1];
    }
    //session_encode — 将当前会话数据编码为一个字符串
    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!!
    //判断sid是否是由数字/字母/-组成,如果不是,则无效
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return;
    }
    $filename = session_save_path() . "/" . "mysess_" . $sid;
    $data = "";
    debug("Saving in ". $filename);
    //ksort — 对数组按照键名排序
    ksort($_SESSION);
    //foreach 遍历给定的数组。每次循环中,当前单元的键名被赋给$key、当前单元的值被赋给$value,并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。
    foreach($_SESSION as $key => $value) {
        debug("$key => $value");
        //给data赋值
        $data .= "$key $value\n";
    }
    //将data存在文件中
    file_put_contents($filename, $data);
    //改变文件模式:Read and write for owner, nothing for everybody else
    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"];
}

?>

<form action="index.php" method="POST">
Your name: <input name="name" value="<?=$name?>"><br>
<input type="submit" value="Change name" />
</form>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>
natas20-sourcecode.html

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

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

访问之后将看到一些提示信息:

DEBUG: MYWRITE 9l3s4uq5gqk1u5c3mk8gti5sr6 name|s:5:"admin";
DEBUG: Saving in /var/lib/php5/sessions//mysess_9l3s4uq5gqk1u5c3mk8gti5sr6
DEBUG: name => admin

DEBUG: MYREAD 9l3s4uq5gqk1u5c3mk8gti5sr6
DEBUG: Reading from /var/lib/php5/sessions//mysess_9l3s4uq5gqk1u5c3mk8gti5sr6
DEBUG: Read [name admin]
DEBUG: Read []

mywrite和myread是两个关键函数,它们的作用是管理会话状态。

function myread($sid) { 
    debug("MYREAD $sid"); 
    //strspn(string,charlist):返回在字符串string中包含charlist中字符的数目
    //判断sid是否是由数字/字母/-组成,如果不是,则无效
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return "";
    }
    //session_save_path() - 返回当前会话的保存路径。
    $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();
    //使用换行符分割data数据
    foreach(explode("\n", $data) as $line) {
        debug("Read [$line]");
    //使用空格符分割line数据,返回的数组包含2个元素,最后那个元素包含line的剩余部分
    $parts = explode(" ", $line, 2);
    if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1];
    }
    //session_encode — 将当前会话数据编码为一个字符串
    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!!
    //判断sid是否是由数字/字母/-组成,如果不是,则无效
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return;
    }
    $filename = session_save_path() . "/" . "mysess_" . $sid;
    $data = "";
    debug("Saving in ". $filename);
    //ksort — 对数组按照键名排序
    ksort($_SESSION);
    //foreach 遍历给定的数组。每次循环中,当前单元的键名被赋给$key、当前单元的值被赋给$value,并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。
    foreach($_SESSION as $key => $value) {
        debug("$key => $value");
        //给data赋值
        $data .= "$key $value\n";
    }
    //将data存在文件中
    file_put_contents($filename, $data);
    //改变文件模式:Read and write for owner, nothing for everybody else
    chmod($filename, 0600);
}

简单来说,myread首先对sid(第一次由服务器自动生成并保存在cookie中)进行校验,若非字母/数字则不返回会话状态。

若sid合法,则进入相关目录寻找/读取文件,若是老的会话/文件已经删除会新建文件保存会话,文件读取完后将session的最后一对键值覆盖到第一的位置。

mywrite则会在会话结束的时候重新读取session,并对session进行一次ksort,将排序后的键值对重新写入文件。

print_credentials()函数的主要功能,是判断$_SESSION["admin"] == 1后显示密码。

由于源码里面没有向$SESSION里面添加admin的键值对,默认情况下,$_SESSION中唯一的key是name,其值通过/index.php中的表单提交进行设置。

我们可以通过对name键值对进行注入:将data里面的值变为:name xxx\nadmin 1\n。所以应该输入xxx\nadmin 1,将其进行URL编码后进行提交。

换行符对应的URL编码为%0A,所以最终应该输入xxx%0Aadmin 1提交。

当然不能在网页中直接输入xxx%0Aadmin 1提交,因为会被编码成xxx%250Aadmin+1,失去了我们的本意。

正确的做法是,使用burp抓包,修改name参数值为xxx%0Aadmin 1,第一次会显示regular,因为没有文件/状态可以读取,session里还是没有Admin的,会话关闭后xxx\nadmin 1就会被写入到状态中,下次登录后session就会加入admin 1了。

flag:IFekPyrQXftziDEsUr3x21sYuahypdgJ


参考:
https://www.cnblogs.com/ichunqiu/p/9554885.html
https://www.cnblogs.com/liqiuhao/p/6882068.html
https://www.freebuf.com/column/182518.html

 

posted @ 2020-03-06 22:48  zhengna  阅读(507)  评论(0编辑  收藏  举报