session会话机制介绍如下
http是无状态协议。服务器靠cookie和session来记住用户。$_SESSION 和 $_GET等一样,是超全局变量。
后台脚本里面会写: session() start 。会话开始。所以,当浏览器访问一个页面时,session机制就开始了。这个时候机制会生成一个PHPSESSID. 存放在cookie当中,当在这个网站post,get 请求数据的时候,会带着这个ID。这个ID是随机的,是由PHP决定的。在注册时,这个PHPSSID可能和注册时间,毫秒,用户名,密码哈希值等有关,所以爆破碰撞PHPSSID难度很大。
在服务器后台,会创建一个以PHPSSID命名文件,里面会储存用户的信息。服务器根据用户发送过来的PHPSSID,读取文件里面的信息。
上面的和这道题没有太大关系。
题目源码打包地址: 120.27.32.227/www.zip
这是一道代码审计。
下面是相关代码示例。
<?php
define("DB_NAME", "xian");
define("DB_HOST", "localhost");
define("DB_USER", "root");
define("DB_PASSWD", "");
define("DSN", "mysql:host=".DB_HOST.";dbname=".DB_NAME);
ini_set("display_errors", "On");
error_reporting(0);
foreach (array('_COOKIE','_POST','_GET') as $_request)
{
foreach ($$_request as $_key=>$_value)
{
$$_key= $_value;
}
}
session_start();
?>
这是common.php里面的代码,连接数据库,取出超全局变量里面的变量和值,根据顺序,先取出cookie,再试post和get。++如果这些变量里面有重名的变量,根据先后顺序,get具有决定性。接着就是SESSION会话机制开始++。由于$_SESSION也是一个超全局变量,如果碰到和前面那三个有重名的,$_SESSION具有决定性。
下面看do_register.php。
<?php
include_once("common.php");
$dbh = new PDO(DSN, DB_USER, DB_PASSWD);
$sql = "select * from user where username = :username";
$sth = $dbh->prepare($sql);
$sth->execute(array(':username'=>$username));
$res = $sth->fetch(PDO::FETCH_ASSOC);
if($res !== false) {
header("Location: error.php?msg=username%20has%20been%20used!");
die();
}
$sql = "insert into user (username, password, role) values(:username, :password, 1)";
$sth = $dbh->prepare($sql);
$res = $sth->execute(array(':username'=>$username, ':password'=>$password));
if($res === false) {
header("Location: error.php?msg=register%20error!");
die();
}
$sql = "select * from user where username = :username and password = :password";
$sth = $dbh->prepare($sql);
$sth->execute(array(':username'=>$username, ':password' => $password));
$res = $sth->fetch(PDO::FETCH_ASSOC);
if($res === false) {
header("Location: error.php?msg=register%20error!");
die();
}
$userinfo["id"] = $res["id"];
$userinfo["username"] = $username;
$userinfo["password"] = $password;
$_SESSION["userinfo"] = $userinfo;
$userinfo["role"] = $res["role"];
header("Location: index.php");
?>
看这段代码的最后几行,是直接把userinfo的信息写进了session里面。下面再看do_changepassword.php。
<?php
include_once("common.php");
if(!isset($_SESSION["userinfo"])) {
header("Location: login.php");
die();
}
$userinfo = $_SESSION["userinfo"];
if($old_pass = $userinfo['password']) {
if($userinfo["id"] == 1) {
echo "flag{xxx}";
die();
}
$dbh = new PDO(DSN, DB_USER, DB_PASSWD);
$sql = "update user set password = :password where id=:id";
$sth = $dbh->prepare($sql);
$res = $sth->execute((array(':password'=>$new_pass, ':id'=>$userinfo['id'])));
var_dump $new_pass;
echo "<br>";
var_dump $userinfo['id'];
if($res === false) {
header("Location: error.php?msg=changepass%20error!");
die();
}
$userinfo["password"] = $new_pass;
$_SESSION['userinfo'] = $userinfo;
header("Location: index.php");
} else {
header("Location: error.php?msg=invalid%20old%20pass!");
die();
}
?>
这里看到,要得到flag,id必须为1。
所以正常来说,我们会想到在超全局变量里面传入参数userinfo['id']=1。
这是一个数组,所以我们请求应该这么写。 http://localhost/do_changepass.php?userinfo[id]=1
但是并没有用,因为$_SESSION这个超全局变量里面有和这个重名的,最后这个变量的值是由session决定的。
所以,这题,只要在post,get,cookie这三个里面传入和session里面重名的变量,都会被session给覆盖,也就是传入参数数组的方式行不通。
解这题的正确姿势,还是要用到“php是最好的语言这个特性”---弱类型。
所以这道题,在register的时候,传入参数,userinfo=1 。然后访问do_changepass ,改密码,就出flag了。解释如下。
传入参数 userinfo=1, 在register页面,
$userinfo["id"] = $res["id"];
$userinfo["username"] = $username;
$userinfo["password"] = $password;
$_SESSION["userinfo"] = $userinfo;
$userinfo["role"] = $res["role"];
header("Location: index.php");
这几行代码之前,session为空。所以这里调用的userinfo 是我们传进去的参数,而我们传进去的参数是一个字符串 , $userinfo["id"]
会把我们的字符串先转化为字符数组,但是是一个数字索引数组 。里面没有id这个键,所以 $userinf["id"] 就与 userinfo[0] 等同。我们的userinfo就变成了userinfo="$rs["id"]"。只改变了第一个字符。接下来的变量赋值,都只改变了第一个字符。所以最后第一个字符是由 $res["role"]决定的,值为1。
我们传入变量的时候,只传入一个字符,同样能出flag。
if($userinfo["id"] == 1) {
echo "flag{xxx}";
die();
}
这里userinfo["id"]=userinf[0]=$res["role"]=1。
总结:这一道题考察了 变量覆盖,php弱类型:字符和数组的转化,变量重名。还有观察力。