抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

0x00 PHP函数

记录一下学习的过程,只挑了一些自己不懂的和很重要的地方,零散的拿出来详细的写写

基于ThinkPHP的代码审计——Niushop (qq.com)

php 内置函数,新建文件并编辑权限

1
2
3
4
if (! file_exists($this->reset_file_path)) {
$mode = intval('0777', 8); // 八进制,777权限
mkdir($this->reset_file_path, $mode, true);
}

文件上传检测不严谨

只规定了上传的格式,没有规定后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private function validationFile()
{
$flag = true;
switch ($this->file_path) {
case UPLOAD_AVATOR:
// 用户头像
if (($this->file_type != "image/gif" && $this->file_type != "image/png" && $this->file_type != "image/jpeg" && $this->file_type != "image/jpg") || $this->file_size > 1000000) {
$this->return['message'] = '文件上传失败,请检查您上传的文件类型,文件大小不能超过1MB';
$flag = false;
}
break;
}
return $flag;
}

sql注入

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
public function promotionZone()
{
$platform = new Platform();
$goods = new GoodsService();
// 品牌专区广告位
$brand_adv = $platform->getPlatformAdvPositionDetailByApKeyword("goodsLabel");
$this->assign('brand_adv', $brand_adv);

if (request()->isAjax()) {
$page_index = request()->get('page', '1');
$group_id = request()->get("group_id", "");
$this->goods = new GoodsService();
$condition = "";
if (! empty($group_id)) {
$condition = "FIND_IN_SET(" . $group_id . ",ng.group_id_array)";
} else {
$condition['ng.group_id_array'] = array(
'neq',
''
);
}
$goods_list = $this->goods->getGoodsList($page_index, PAGESIZE, $condition, "", $group_id);
return $goods_list;
}
}
1
$condition = "FIND_IN_SET(" . $group_id . ",ng.group_id_array)";

没有对$group_id的内容进行过滤,并且可以通过get的方式对该内容进行传参,所以就存在sql注入。

当sql注入可以通过报错来获得回显的时候,可以通过sql语句extractvalue(1,concat(char(126),@@version)) 进行版本查询。

  • EXTRACTVALUE() 函数在MySQL中用于从XML文档片段中提取值。它通常接受两个参数:第一个是XML字符串,第二个是XPath表达式,用于定位XML中的数据。然而,在SQL注入攻击中,攻击者可能会滥用这个函数以引发错误消息,从而泄露数据库的信息。
  • CONCAT() 函数用于连接两个或多个字符串。在这种情况下,它被用来构建一个包含敏感数据(如数据库版本)的字符串。

0x01 Loader

Loader 类在ThinkPHP 5.1中扮演了重要的角色,通过提供简洁的API来加载应用的各种组件. Loader 类通过注册自己为自动加载函数,帮助PHP解析并找到正确的类文件。

thinkphp通过start.php引入的base.php定义文件夹等系统常量,然后引入Loader来加载任意类,通过自动加载使用Error类注册错误处理,以及Config类加载模式配置文件thinkphp/convention.php。做好一系列准备工作之后,执行应用 App::run()->send()

例如加载控制器:

  • 控制器通常在路由调用时自动加载,但如果需要手动加载控制器,可以使用 Loader::controller 方法。
1
2
$userController = Loader::controller('User', 'controller\index');
// 这会加载 app\index\controller\User 控制器

0x02 hook

“钩子”(Hook)允许开发者在应用程序的特定点插入自己的代码,而不需要修改主体程序。这种机制特别在插件或扩展功能开发中非常重要,因为它允许开发者扩展或修改应用程序的行为,而不直接影响主程序的稳定性和完整性。钩子本身通常不负责验证逻辑本身,而是提供一个插入自定义代码和扩展功能的点。

钩子大体可以分为两类:

  1. 动作钩子(Action Hooks):
    • 这种钩子允许你在程序的特定时刻执行一个动作。例如,在用户注册后发送欢迎邮件就是通过动作钩子实现的。
    • 动作钩子通常不返回值,它们主要用于触发事件或操作。
  2. 过滤钩子(Filter Hooks):
    • 过滤钩子允许你修改数据。例如,WordPress中的the_content钩子允许你修改文章内容,如添加额外的HTML,或是动态修改文本。
    • 这类钩子接收一个值作为输入,经过处理后返回新的值。

钩子的工作流程通常包括三个基本步骤:

  1. 定义钩子位置:
    • 开发主程序的开发者在软件的关键位置放置钩子。这些位置通常是执行操作或返回数据之前的点,例如数据保存到数据库之前。
    • 在PHP框架中,这通常通过调用预留的函数或方法来实现,如调用do_action()apply_filters()
  2. 附加函数(Hooking Functions):
    • 外部开发者或插件开发者编写自定义函数来“挂钩”到这些预定义的钩子上。这通常通过调用如add_action()add_filter()的函数来实现,这些函数让你指定当钩子被触发时应该执行哪个函数。
  3. 执行钩子:
    • 当到达程序中定义钩子的位置时,所有附加到该钩子的函数都会按照指定的顺序执行。这允许动态地修改程序的行为或数据。

举个例子

标准登录流程(无钩子):

  1. 用户提交用户名和密码:用户在登录表单填写信息后提交。
  2. 系统验证凭据:系统将用户提交的信息与数据库中的数据进行比对。
  3. 处理验证结果
    • 如果验证成功(即用户名和密码匹配),用户被认为已成功登录,系统可能会跳转到主页或用户仪表板。
    • 如果验证失败,可能会显示一个错误消息。

引入钩子的登录流程:

在有钩子的情况下,流程可能会包含一些额外的步骤,允许自定义或扩展功能:

  1. 用户提交用户名和密码。
  2. 前置钩子:在验证用户凭据之前执行。这可以用于记录尝试登录的日志、检查账号是否被锁定等预处理操作。
  3. 系统验证凭据:与标准流程相同。
  4. 后置钩子
    • 如果凭据正确,登录成功钩子可以触发,例如用于触发欢迎邮件的发送、记录登录成功的日志、加载用户的个性化设置等。
    • 如果凭据不正确,登录失败钩子可以触发,例如用于记录登录失败尝试、增加账户的安全级别等。

在验证登录过程中,利用钩子执行额外的操作或检查。钩子本身通常不负责验证逻辑本身,而是提供一个插入自定义代码和扩展功能的点。

0x03 input 函数获取请求参数

在ThinkPHP框架中,input 助手函数是一个非常重要的功能,它用于获取客户端传递来的数据(如GET、POST等请求数据)。这个助手函数主要用于简化和加强数据获取和验证的过程。

获取数据

  • 可以指定要获取的数据类型和变量名。如果不指定,默认为 input('param.'),它将从默认的请求变量中获取数据。
1
2
$name = input('get.name'); // 获取GET请求中的name变量
$age = input('post.age'); // 获取POST请求中的age变量
  • 判断有没有传递某个参数可以用
1
2
input('?get.id');
input('?post.name');

数据过滤和验证

  • input 函数允许您指定过滤函数或者验证规则。这是通过传递第二个参数来实现的,可以是PHP的内置过滤函数,也可以是自定义的回调函数。
1
$email = input('post.email', '', 'trim,strtolower,email'); // 使用多个过滤器

设置默认值

  • 如果指定的变量不存在,可以通过传递第三个参数来设置一个默认值。
1
$status = input('post.status', 0); // 如果post中没有status变量,则默认为0

获取整个数组

  • 如果想获取整个GET或POST数组,可以省略变量名。
1
2
$allGetVars = input('get.'); // 获取所有GET变量
$allPostVars = input('post.'); // 获取所有POST变量

函数段解析thinkphp/helper.php:121

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function input($key = '', $default = null, $filter = '')
{
if (0 === strpos($key, '?')) {
$key = substr($key, 1);
$has = true;
}
if ($pos = strpos($key, '.')) {
// 指定参数来源
list($method, $key) = explode('.', $key, 2);
if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) {
$key = $method . '.' . $key;
$method = 'param';
}
} else {
// 默认为自动判断
$method = 'param';
}
if (isset($has)) {
return request()->has($key, $method, $default);
} else {
return request()->$method($key, $default, $filter);
}
}

  • $key: 可选参数,用于指定要获取的数据的键(key),同时可以通过在键前加前缀来指定数据来源(例如 post.name 表示从 POST 数据获取 name 键的值)。如果键以 ? 开头,表示检查该键是否存在。
  • $default: 可选参数,如果指定的键不存在,则返回这个默认值。
  • $filter: 可选参数,用于指定应用于数据的过滤器。

检查键是否存在

1
2
3
4
if (0 === strpos($key, '?')) {
$key = substr($key, 1);
$has = true;
}
  • 这段代码检查 $key 是否以 ? 开始。如果是,移除 ? 并设置一个标记 $has,表示后续操作应检查键是否存在而非直接获取值。

解析数据来源

1
2
3
4
5
6
7
8
9
if ($pos = strpos($key, '.')) {
list($method, $key) = explode('.', $key, 2);
if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) {
$key = $method . '.' . $key;
$method = 'param';
}
} else {
$method = 'param';
}
  • 这段代码检查 $key 中是否包含点 (.),用来分隔数据来源和实际的键名。例如 post.name
  • 如果存在点分隔符,explode 函数用来分离数据来源和键名。
  • 检查分离出的来源是否在预定义的来源列表中,如果不在,则重设 $methodparam(表示自动判断来源),并将原始的方法和键重新组合成 $key

返回数据

1
2
3
4
5
if (isset($has)) {
return request()->has($key, $method, $default);
} else {
return request()->$method($key, $default, $filter);
}
  • 如果设置了 $has(查询键是否存在),调用 has 方法来检查键是否存在,方法名动态从 $method 变量获取(利用 PHP 的变量函数特性)。
  • 如果没有设置 $has,直接通过动态方法名获取键的值,传递 $key, $default, 和 $filter 作为参数。

0x04 路由机制

image

thinkphp 采用的是多路由方式

定义方式 定义格式
方式1:路由到模块/控制器 ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’
方式2:路由到重定向地址 ‘外部地址’(默认301重定向) 或者 [‘外部地址’,‘重定向代码’]
方式3:路由到控制器的方法 ‘@[模块/控制器/]操作’
方式4:路由到类的方法 ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’
方式5:路由到闭包函数 闭包函数定义(支持参数传入)

路由到模块/控制器

这是最常用的一种路由方式,把满足条件的路由规则路由到相关的模块、控制器和操作,然后由App类调度执行相关的操作。

在通过route/route.php 来自定义路由 路由地址 · ThinkPHP5.1完全开发手册 · 看云 (kancloud.cn)

thinkphp采用的时MVC的开发模式,C代表controller,是控制器,用于进行路由的转发。index控制器在index/controller/Index.php

1
Route::get('hello/:name', 'index/hello');

这条路由定义指明了当客户端向服务器发起一个GET请求,且URL匹配hello/:name模式时,请求将被转发到index模块的index控制器的hello方法处理。:name是一个动态参数,它会捕获URL中相应位置的值并传递给控制器方法。

URL 的访问设计:URL访问 · ThinkPHP5.1完全开发手册 · 看云 (kancloud.cn),可以去访问hello方法

如果不支持PATHINFO的服务器可以使用兼容模式访问如下:

1
http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...\]

所以访问hello方法的时候,构造路由http://thinkphp5:8090/public/index.php?s=index/index/hello/name

可以传参进去

还可以支持路由到动态的模块、控制器或者操作,例如:

1
2
3
4
// action变量的值作为操作方法传入
Route::get(':action/blog/:id', 'index/blog/:action');
// 变量传入index模块的控制器和操作方法
Route::get(':c/:a', 'index/:c/:a');

路由到重定向地址

旧的页面URL,如 /old-page,想要将所有访问这个旧页面的请求重定向到新页面 /new-page,下面是如何设置路由规则的例子:

1
2
3
4
5
6
7
use think\Route;

// 使用redirect方法进行重定向
Route::get('old-page', 'redirect:/new-page');

// 或者使用完整URL进行重定向
Route::get('old-blog', 'redirect:http://example.com/new-blog');

这些路由规则意味着:

  • 访问 /old-page 时,用户会被自动重定向到 /new-page
  • 访问 /old-blog 时,用户会被自动重定向到 http://example.com/new-blog

动态传递的路由参数

1
2
// 动态重定向
Route::get('user/:id', 'redirect:/new-user/:id');

在这个例子中,任何形如 /user/123 的URL将会被重定向到 /new-user/123,其中 :id 是动态传递的。

redirect方法

V5.1.3+版本开始,可以直接使用redirect方法注册一个重定向路由

1
Route::redirect('blog/:id','http://blog.thinkphp.cn/read/:id',302);

0x05 控制器

控制器定义 · ThinkPHP5.1完全开发手册 · 看云 (kancloud.cn)

搞清楚功能所在文件的绝对路径与访问URL之间的关系

控制器类

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\index\controller;

use think\Controller;

class Index extends Controller
{
public function index()
{
return 'index';
}
}

控制器类文件的实际位置是

1
application\index\controller\Index.php

访问URL地址是(假设没有定义路由的情况下)

1
http://localhost/index.php/index

在ThinkPHP中,URL访问地址通常通过路由来定义,路由决定了哪些URL可以访问哪些控制器中的方法。基于上面的Index控制器例子,如果想通过URL访问index方法,需要在路由配置文件中定义一个路由,如:

1
2
3
4
// 路由定义可能位于 route.php 文件中
use think\Route;

Route::get('hello', 'index/index'); // 当访问 /hello 时,调用 index 控制器的 index 方法

这意味着当用户访问http://yourdomain.com/hello时,将会触发Index控制器的index方法,并显示返回结果Hello, ThinkPHP!

0x06 路由和控制器之间的关系

如果没有明确地定义路由规则,那么URL的访问和解析会依赖于框架的默认路由规则。

ThinkPHP默认的URL格式遵循以下结构:

1
http://[hostname]/[entry_script]/[module]/[controller]/[action]
  • hostname - 服务器的地址(例如 localhost)

  • entry_script - 入口文件(通常是 index.php

  • module - 应用模块名(通常是 index

    确定模块名的方法:

    1. 查看项目结构:模块名通常是application目录下的子目录名。你可以查看这个目录来确定你的应用包含哪些模块。

    2. 配置文件:ThinkPHP的应用配置文件通常位于application/config.php,在这个文件中可以查看默认模块的设置,或者是模块的相关配置。例如:

      1
      2
      3
      4
      5
      6
      7
      8
      return [
      // 应用设置
      'app' => [
      'default_module' => 'index', // 默认模块名
      'deny_module_list' => ['common'], // 禁止访问的模块
      ],
      ...
      ];
    3. URL访问:在未配置特殊路由的情况下,URL的结构通常反映了模块的使用。例如,访问http://yourdomain.com/index/controller/action中的index可能就是模块名。

  • controller - 控制器名

  • action - 控制器中的方法名

如果你的控制器是HelloWorld,并且定义如下:

1
2
3
4
5
6
7
8
9
10
<?php
namespace app\index\controller;

class HelloWorld
{
public function index()
{
return 'hello,world!';
}
}

控制器类文件的实际位置是

1
application\index\controller\HelloWorld.php

访问url

如果不指定方法名,ThinkPHP 默认会调用控制器中的 index 方法。因此,即使 URL 中没有明确指出要调用 index 方法,系统也会自动寻找并执行 HelloWorld 控制器中的 index() 方法。

1
2
http://localhost/index.php/index/hello_world/index 
http://localhost/index.php/index/hello_world/

tips:控制器命名和URL映射

在ThinkPHP中,控制器的命名规范是大驼峰(PascalCase),即每个单词的首字母大写。但在URL中,控制器名通常会自动转换为小写,并且驼峰命名中的大写字母会通过下划线(_)进行分隔。所以HelloWorld 控制器在URL中被表示为 hello_world 的原因。

也可以关闭自动转化,以原来的名字Helloworld来进行访问

1
2
// 是否自动转换URL中的控制器和操作名
'url_convert' => false,

0x07 参考文章

Thinkphp 源码阅读 - Y4er的博客

模块设计 · ThinkPHP5.1完全开发手册 · 看云 (kancloud.cn)

0x08 碎碎念

写这篇文章时的碎碎念 👇

我发现我的脑子就像我的笔记一样。脑子里的内容稀碎时,笔记也是。

学习知识是一个闭环的过程。我在学习thinkphp框架的过程是:先把thinkphp的框架文档过了一遍,脑子很乱,但是能僵硬的换出来一些框架流程图。可是生涩不理解,脑子里构建不出来一副完整的画面。学着学着,有点没方向了,我又从路由看起。但是路由进去的时候,走controller。然后我就忽然,脑子灵光一闪,明白了框架的入口到底是个什么意思了。我之前没明白框架入口到底是个什么意思,现在忽然懂了,程序的入口不是route而是框架入口。它规定了路由是怎么走的。然后我就又忽然明白了,thinkphp多路由到底是个什么东西。

学习的时候,如果给你一个“意义”和目的,你就会自己推着自己向前走了。不过这也可能是我忽然探索明白了自己的学习模式。我学习的话,需要一个“意义驱动”。有了这样的驱动,我就有了自己的学习模式和方向。

学习代码审计的时候,我觉得特别没有方向,然后我看见p神说,要从框架底层看起。我就去看文档,不明白意义何在,看的云里雾里。后来我就去问chat森森,为什么要学框架,我想要代码审计,但我不明白我该从哪个方向看起,又要学到什么程度。后来chat森森跟我说“熟悉ThinkPHP或任何其他目标框架的目录结构、核心组件以及其工作方式。这包括了解其路由机制、控制器、模型、视图、中间件和服务提供者”。于是这个时候我的方向就是看路由机制是怎么运行的。看着看着,我自己就找到了方向,自己倒推到了“程序入口”和thinkphp的多路由机制。然后我就想到了y4er的文章。他就是从框架入口start.php 文件开始讲的。 那一刻!我完成了闭环,虽然我还没完全弄懂,但是我有了方向,继续看下去,我就理解了到底是怎么回事。再抽象一点回顾这个学习的过程。我发现这才是我“私人定制”的一种学习方式。我做事情一直都喜欢先把框架定出来,然后再有方向性的填内容进去。

评论