php框架入门

php框架入门

因为是安全相关,主要是讲解一下:

  1. 一个框架中有那些重要的文件和文件夹
  2. 运维的粗心会导致哪些问题
  3. 框架的类加载机制

前言

这是我于2020.11.8的例会分享内容,如有错误还请指正!

先从文件和文件夹讲起

thinkphp

laravel

yii2

public/web目录

我们先康康不同框架中,public/web文件夹里面有什么东西。

thinkphp-public

laravel-public

yii-web

可以看道三个文件夹共有的特征便是——都拥有index.php文件。文件内容都大致如以下伪代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
define("...", "...");
//... // 定义了一些常量,例如yii是定义其debug是否开启等...

require __DIR__.'/../vendor/autoload.php';
// 导入autoload.php来自动加载类,这个之后在讲

$config = require '../config/config.php';
require ...;
// 导入一些其它文件,比如config,引导文件等...

(new app($config))->run();
=================
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);
=================
...
// 启动框架

可见index.php便是整个框架的入口文件了。那么public/web文件夹应该是被作为网站根目录来使用的。

对于渗透人员来说,如果运维人员配置错误,会发生什么?例如以下场景。

Q: 有时候我们会发现,一个网站的主页并不是http://vps.com/,而是http://vps.com/public/,甚至是http://vps.ip/app/public/时,这说明了什么?

A: 很明显,懒狗并没有按照框架的要求将public/web目录设置为网站主目录。这里分开讨论:

  1. 对于http://vps.com/public/的情况,运维应该是将框架目录设为了网站主目录,这种情况下,我们就可以根据不同的文件夹去读取在public/web下读不到的东西。
  2. 对于http://vps.ip/app1/public/的情况,那就是大奖了。很有可能是以下情况:
1
2
3
4
5
var--www--app1
-app2
-app3
-...
-index.php

在这个服务器上存在多个网站,但运维懒狗到懒到不想为每个目录独立配置服务器,设置了更上一层的目录为网站根目录,还“贴心”设置了index.php引导用户跳转到不同文件夹下的不同目录。对于这种设置,我们就可以读到更多原来读不到的文件。

对于其它的public/web下的其它文件,就说一下yii2下的web/assets文件夹吧。根据官网文档,这个文件夹的作用是如下:

如果资源包放在 Web 不能访问的目录, 当视图注册资源时资源会被拷贝到一个 Web 可访问的目录中, 这个过程称为资源发布。资源包即为那些css/js文件。php将会在assets创建一个资源包文件的链接。

这个功能是默认开启的,但当assets没有www-data的写权限时,yii会爆出无法写入文件的错误,解决途径有两个,一是关闭资源发布,二是给其写权限。如果是没有经验的运维,会老老实实根据yii爆出的错误,给予该文件夹写权限。那么如果有任意目录的文件上传,你知道该上传到哪里了吧?

vendor目录

thinkphp-vendor

laravel-vendor

yii-vendor

这个目录里装载了一个框架所需的依赖包,如果开发需要一些包作为辅助时,也会安装到这个文件夹。

这里不得不谈的是php的类加载机制。

autoload.php文件

在入口文件中,我们看到,其导入了vendor/autoload.php文件,而其又导入了composer/autoload_real.php文件。我们来看看autoload_real.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// autoload_real.php @generated by Composer

class ComposerAutoloaderInite66911f1406e2749477b48d048bd8aca
{
// 加载器
private static $loader;

// 加载加载器文件
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

// 获取加载器
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
// 单例模式
if (null !== self::$loader) {
return self::$loader;
}

// 平台检查
require __DIR__ . '/platform_check.php';

// 调用方法获取加载器
spl_autoload_register(array('ComposerAutoloaderInite66911f1406e2749477b48d048bd8aca', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInite66911f1406e2749477b48d048bd8aca', 'loadClassLoader'));

// 当符合以下条件,就使用静态初始化,否则使用接口初始化
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';

call_user_func(
\Composer\Autoload\ComposerStaticInite66911f1406e2749477b48d048bd8aca::getInitializer($loader)
);
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}

$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}

$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}

// 对加载器进行注册
// 其内部的大致流程是: classMap->psr-4->psr-0
$loader->register(true);

// 对辅助函数类直接进行加载
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInite66911f1406e2749477b48d048bd8aca::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
// 直接加载
composerRequiree66911f1406e2749477b48d048bd8aca($fileIdentifier, $file);
}

// 返回加载器
return $loader;
}
}

function composerRequiree66911f1406e2749477b48d048bd8aca($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;

$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

可以看到,php的自动类加载遵循了psr-4规范。

对于java来说,当一个文件存在多个类时,会自动将多出来的类放入新的class文件内,而不是两个类共存同一个文件。而php的标准对于这个就无能为力了。

在WMCTF2020中有一道webweb的反序列化链便存在这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ws.php // Agent.php
<?php
namespace CLI;

class WS {
//...
}

class Agent {
//...
public function __destruct() {
// 危险代码
}
}

这道题使用fatfree框架,我们的起点__destructCLI\Agent下,但此类和CLI\WS共存在ws.php上,根据上面的加载规则,Agent类是加载不到的。

这是这个题的一个坑点,如果我们想要加载这个类,就要提前去导入ws.php。做法也简单,在整个payload外层包裹一层CLI\WS即可。这样,类加载器会根据最外层的CLI\WS一类,首先导入ws.php,然后解析器会同时解析CLI\WSCLI\Agent,再导入CLI\Agent类时,就成功了。

其它

还有一些零散的文件可以注意一下,这里简单进行一个概况

如果出现了在上面public文件夹出现的问题可以注意一下:

  1. .env 系统环境变量,可能会有数据库地址和密码,以及类似于laravel中的密钥
  2. README.md 获得框架名称、版本,以便对框架进行代码审计
  3. composer.lock 获得当前框架所有依赖名

如果当前目录下有assets文件夹,大概率是yii2框架

最后

感谢懒狗运维\开发给我们饭恰(bushi)

Comments