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 "; 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(" ", $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 "; } //将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>
我们来看看每个函数的作用:
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(" ", $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 "; } //将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 admin 1 。所以应该输入xxx admin 1,将其进行URL编码后进行提交。
换行符对应的URL编码为%0A,所以最终应该输入xxx%0Aadmin 1提交。
当然不能在网页中直接输入xxx%0Aadmin 1提交,因为会被编码成xxx%250Aadmin+1,失去了我们的本意。
正确的做法是,使用burp抓包,修改name参数值为xxx%0Aadmin 1,第一次会显示regular,因为没有文件/状态可以读取,session里还是没有Admin的,会话关闭后xxx admin 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