RCTF2021 EasyPHP
Posted bfengj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RCTF2021 EasyPHP相关的知识,希望对你有一定的参考价值。
Easyphp
先分析一下nginx.conf,也是一个一个查的来好好的学一波。
listen:监听设置。
server_name:设置虚拟主机的名称,域名,可以通配符、正则。
如果当一个名称匹配多个 server 的是时候,匹配优先级如下:
- 确切的名称
- 以 * 开头的最长的通配符名称
- 以 * 结尾的最长通配符名称
- 第一个匹配的正则表达式
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);
$name
是start
,得到了一个$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