RCTF2021 EasyPHP

Posted bfengj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RCTF2021 EasyPHP相关的知识,希望对你有一定的参考价值。

Easyphp

先分析一下nginx.conf,也是一个一个查的来好好的学一波。

listen:监听设置。

server_name:设置虚拟主机的名称,域名,可以通配符、正则。

如果当一个名称匹配多个 server 的是时候,匹配优先级如下:

  1. 确切的名称
  2. 以 * 开头的最长的通配符名称
  3. 以 * 结尾的最长通配符名称
  4. 第一个匹配的正则表达式

location:根据 URI 进行配置设置。

location [ = | ~ | ~* | ^~ ] uri  ... 
  • none,如果没有修饰符,则将该位置解释为前缀匹配。这意味着给定的位置将根据请求URI的开头进行匹配,以确定匹配
  • =,代表精确匹配,完全相等即匹配
  • ~,区分大小写的正则表达式匹配
  • ~*,不区分大小写的正则表达式匹配
  • ^~,普通字符匹配,如果该选项匹配,只匹配该选项

所以本题中的都是None,即为前缀匹配,Z3ratu1师傅就是利用了这个前缀匹配的漏洞对/admin进行了绕过。

allow和deny:指令是由ngx_http_access_module模块提供,用于访问控制,用于ip控制。本题里面肯定就是匹配/admin前缀的只允许ip为127.0.0.1的访问。

try_files:其作用是按顺序检查文件是否存在,返回第一个找到的文件或文件夹(结尾加斜线表示为文件夹),如果所有的文件或文件夹都找不到,会进行一个内部重定向到最后一个参数。

在这里其实就是,先检查$document_root$uri是否存在,如果不存在就内部重定向到@phpfpm

    location @phpfpm 
        include        fastcgi_params;
        fastcgi_split_path_info ^(.+?\\.php)(/.*)$;
        fastcgi_pass   php:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root/index.php;
        fastcgi_param  REQUEST_URI  $uri;
    

它最上面设置了root /var/www/html;,而$url就是请求那部分了,比如请求/admin?hello=world,那么nginx这里的$url/admin

所以这里很明显是被转交给@phpfpm处理。

fastcgi_pass:address为后端的fastcgi server的地址。

fastcgi_index:fastcgi默认的主页资源。

fastcgi_param:传递给FastCGI服务器的参数值,可以是文本,变量或组合。

需要注意到这个REQUEST_URI$uri,这个就比较重要了,后面会利用到。

接下来开始本题的解题。

看一下index.php里面的路由,各种操作:

<?php
session_start();
require 'flight/autoload.php';
use flight\\Engine;
$app = new Engine();

$username = "admin";
$password = uniqid("asdfsadf",true)."YouWillNerveKnow";// you will never know the password

function isdanger($v)
    if(is_array($v))
        foreach($v as $k=>$value)
            if(isdanger($k)||isdanger($value))
                return true;
            
        
    else
        if(strpos($v,"../")!==false)
            return true;
        
    
    return false;


$app->before("start",function()
    foreach([$_GET,$_POST,$_COOKIE,$_FILES] as $value)
        if(isdanger($value))
            die("go away hack");
        
    
);
$app->route('/*', function()
    global $app;
    $request = $app->request();
    $app->render("head",[],"head_content");
    if(stristr($request->url,"login")!==FALSE)
        return true;
    else
        if($_SESSION["user"])
            return true;
        
        $app->redirect("/login");
    

    
);


$app->route('/admin', function()
    global $app;
    $request = $app->request();
    $app->render("admin",["data"=>"./".$request->query->data],"body_content");
    $app->render("template",[]);
);

$app->route("GET /login",function()
    global $app;
    $request = $app->request();
    $app->render("login",["fail"=>$request->query->fail],"body_content");
    $app->render("template",[]);
);

$app->route("POST /login",function()
    global $username,$password,$app;
    $request  = $app->request();
    if($request->data->username === $username && $request->data->password === $password)
        $_SESSION["user"] = $username;
        $app->redirect("/");
        return;
    
    $app->redirect("/login?fail=1");
);

$app->route("GET /",function()
    global $app;
    $app->render("index",[],"body_content");
    $app->render("template",[]);
);

$app->start();

在admin视图里面发现了任意文件读取:

<h3>File List:</h3>
<script>
</script>
<div class="bg-light border rounded-3" style="white-space: pre-line">
    <?php
        $dir = pathinfo($data?$data:".",PATHINFO_DIRNAME);
        foreach(scandir($dir) as $v)
            echo "<a href=\\"/admin?data=$dir/$v\\">$v</a><br />";
        
    ?>
</div>
<?php if ($data)  ?><h3><?= $data . ":" ?></h3>
    <div class="bg-light border rounded-3"><code style="white-space: pre-line"><?php echo file_get_contents($data); ?></code></div><?php  ?>

所以利用点就是/admin路由了。

所以第一步就是/admin路由的绕过,因为nginx对这里做了只允许本地访问。

首先还是先简单的分析一下flight框架的整个流程吧。

关键的$app是个Engine类:$app = new Engine();。这部分的初始化可以跳过,唯一需要注意的就是初始化中某些默认的设置(比如路由默认不区分大小写)。

接下来的就是$app->before(xxxx,xxxx)还有$app->$app->route(xxxx,xxxx)了,before这里就大致知道,在路径处理之前调用那个回调函数即可。看一下route()的运作,比如第一个:

$app->route('/*', function()
    global $app;
    $request = $app->request();
    $app->render("head",[],"head_content");
    if(stristr($request->url,"login")!==FALSE)
        return true;
    else
        if($_SESSION["user"])
            return true;
        
        $app->redirect("/login");
    
);

第一个参数是路由(也可能包含请求方法),第二个参数就是访问这个路由时调用的回调函数(也就是相应的处理了)。

对于route,调用的函数其实是Engine.php_route()

    public function _route($pattern, $callback, $pass_route = false) 
        $this->router()->map($pattern, $callback, $pass_route);
    

router()产生router类的对象,然后调用它的map()方法:

    public function map($pattern, $callback, $pass_route = false) 
        $url = $pattern;
        $methods = array('*');

        if (strpos($pattern, ' ') !== false) 
            list($method, $url) = explode(' ', trim($pattern), 2);
            $url = trim($url);
            $methods = explode('|', $method);
        

        $this->routes[] = new Route($url, $callback, $methods, $pass_route);
    

其实就是根据提供的参数创建Route对象,添加到$this>routes[]里面,Route对象里面包括请求的url(可能包含通配符),回调函数,请求的方式(Get,Post)等。

创建这么多个Route对象之后,最后一行$app->start();。虽然它这里实际上调用的是_start()方法,但是中间的处理还是需要跟进一下的,因为之前的那个before()设置的waf就是在这里调用到的。

先是进去__call()方法:

    public function __call($name, $params) 
        $callback = $this->dispatcher->get($name);

        if (is_callable($callback)) 
            return $this->dispatcher->run($name, $params);
        

        if (!$this->loader->get($name)) 
            throw new \\Exception("$name must be a mapped method.");
        

        $shared = (!empty($params)) ? (bool)$params[0] : true;

        return $this->loader->load($name, $shared);
    

$namestart,得到了一个$callback,然后进入$this->dispatcher->run($name, $params);

    public function run($name, array $params = array()) 
        $output = '';

        // Run pre-filters
        if (!empty($this->filters[$name]['before'])) 
            $this->filter($this->filters[$name]['before'], $params, $output);
        

        // Run requested method
        $output = $this->execute($this->get($name), $params);

        // Run post-filters
        if (!empty($this->filters[$name]['after'])) 
            $this->filter($this->filters[$name]['after'], $params, $output);
        

        return $output;
    

可以发现那个Run pre-filters,就是调用之前waf的那个回调函数,就不细跟了,知道那个waf在这里调用的就可以了:

    foreach([$_GET,$_POST,$_COOKIE,$_FILES] as $value)
        if(isdanger($value))
            die("go away hack");
        
    

执行完pre-filters之后就是$output = $this->execute($this->get($name), $params);,跟进:

    public static function execute($callback, array &$params = array()) 
        if (is_callable($callback)) 
            return is_array($callback) ?
                self::invokeMethod($callback, $params) :
                self::callFunction($callback, $params);
        
        else 
            throw new \\Exception('Invalid callback specified.');
        
    

进入self::invokeMethod($callback, $params) :,最终调用:

        switch (count($params)) 
            case 0:
                return ($instance) ?
                    $class->$method() :
                    $class::$method();

进入_start()

    public function _start() 
        $dispatched = false;
        $self = $this;
        $request = $this->request();
        $response = $this->response();
        $router = $this->router();

        // Allow filters to run
        $this->after('start', function() use ($self) 
            $self->stop();
        );

        // Flush any existing output
        if (ob_get_length() > 0) 
            $response->write(ob_get_clean());
        

        // Enable output buffering
        ob_start();

        // Route the request
        while ($route = $router->route($request)) 
            $params = array_values($route->params);

            // Add route info to the parameter list
            if ($route->pass) 
                $params[] = $route;
            

            // Call route handler
            $continue = $this->dispatcher->execute(
                $route->callback,
                $params
            );

            $dispatched = true;

            if (!$continue) break;

            $router->next();

            $dispatched = false;
        

        if (!$dispatched) 
            $this->notFound();
        
    

考虑到后面需要取$request->query->data,还需要关注一下request的产生:$request = $this->request();,继续跟进一下就可以看到是在这里调用了newInstance

            if ($shared) 
                $obj = ($exists) ?
                    $this->getInstance($name) :
                    $this->newInstance($class, $params);

然后创建实例:

        switch (count($params)) 
            case 0:
                return new $class();

关注Request类对象的query属性的产生:

    public function __construct($config = array()) 
        // Default properties
        if RCTF2021 EasyPHP

RCTF2021 EasyPHP

刷题记录:[RCTF 2019]Nextphp

RCTF2015 PWN400 分析

[RCTF2015]EasySQL

RCTF 2018线上赛 writeup