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 ezshell