0x00 PHP函数
记录一下学习的过程,只挑了一些自己不懂的和很重要的地方,零散的拿出来详细的写写
基于ThinkPHP的代码审计——Niushop (qq.com)
php 内置函数,新建文件并编辑权限
1 | if (! file_exists($this->reset_file_path)) { |
文件上传检测不严谨
只规定了上传的格式,没有规定后缀
1 | private function validationFile() |
sql注入
1 | public function promotionZone() |
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 | $userController = Loader::controller('User', 'controller\index'); |
0x02 hook
“钩子”(Hook)允许开发者在应用程序的特定点插入自己的代码,而不需要修改主体程序。这种机制特别在插件或扩展功能开发中非常重要,因为它允许开发者扩展或修改应用程序的行为,而不直接影响主程序的稳定性和完整性。钩子本身通常不负责验证逻辑本身,而是提供一个插入自定义代码和扩展功能的点。
钩子大体可以分为两类:
- 动作钩子(Action Hooks):
- 这种钩子允许你在程序的特定时刻执行一个动作。例如,在用户注册后发送欢迎邮件就是通过动作钩子实现的。
- 动作钩子通常不返回值,它们主要用于触发事件或操作。
- 过滤钩子(Filter Hooks):
- 过滤钩子允许你修改数据。例如,WordPress中的
the_content
钩子允许你修改文章内容,如添加额外的HTML,或是动态修改文本。 - 这类钩子接收一个值作为输入,经过处理后返回新的值。
- 过滤钩子允许你修改数据。例如,WordPress中的
钩子的工作流程通常包括三个基本步骤:
- 定义钩子位置:
- 开发主程序的开发者在软件的关键位置放置钩子。这些位置通常是执行操作或返回数据之前的点,例如数据保存到数据库之前。
- 在PHP框架中,这通常通过调用预留的函数或方法来实现,如调用
do_action()
或apply_filters()
。
- 附加函数(Hooking Functions):
- 外部开发者或插件开发者编写自定义函数来“挂钩”到这些预定义的钩子上。这通常通过调用如
add_action()
或add_filter()
的函数来实现,这些函数让你指定当钩子被触发时应该执行哪个函数。
- 外部开发者或插件开发者编写自定义函数来“挂钩”到这些预定义的钩子上。这通常通过调用如
- 执行钩子:
- 当到达程序中定义钩子的位置时,所有附加到该钩子的函数都会按照指定的顺序执行。这允许动态地修改程序的行为或数据。
举个例子
标准登录流程(无钩子):
- 用户提交用户名和密码:用户在登录表单填写信息后提交。
- 系统验证凭据:系统将用户提交的信息与数据库中的数据进行比对。
- 处理验证结果
- 如果验证成功(即用户名和密码匹配),用户被认为已成功登录,系统可能会跳转到主页或用户仪表板。
- 如果验证失败,可能会显示一个错误消息。
引入钩子的登录流程:
在有钩子的情况下,流程可能会包含一些额外的步骤,允许自定义或扩展功能:
- 用户提交用户名和密码。
- 前置钩子:在验证用户凭据之前执行。这可以用于记录尝试登录的日志、检查账号是否被锁定等预处理操作。
- 系统验证凭据:与标准流程相同。
- 后置钩子
- 如果凭据正确,登录成功钩子可以触发,例如用于触发欢迎邮件的发送、记录登录成功的日志、加载用户的个性化设置等。
- 如果凭据不正确,登录失败钩子可以触发,例如用于记录登录失败尝试、增加账户的安全级别等。
在验证登录过程中,利用钩子执行额外的操作或检查。钩子本身通常不负责验证逻辑本身,而是提供一个插入自定义代码和扩展功能的点。
0x03 input 函数获取请求参数
在ThinkPHP框架中,input
助手函数是一个非常重要的功能,它用于获取客户端传递来的数据(如GET、POST等请求数据)。这个助手函数主要用于简化和加强数据获取和验证的过程。
获取数据:
- 可以指定要获取的数据类型和变量名。如果不指定,默认为
input('param.')
,它将从默认的请求变量中获取数据。
1 | $name = input('get.name'); // 获取GET请求中的name变量 |
- 判断有没有传递某个参数可以用
1 | input('?get.id'); |
数据过滤和验证:
input
函数允许您指定过滤函数或者验证规则。这是通过传递第二个参数来实现的,可以是PHP的内置过滤函数,也可以是自定义的回调函数。
1 | $email = input('post.email', '', 'trim,strtolower,email'); // 使用多个过滤器 |
设置默认值:
- 如果指定的变量不存在,可以通过传递第三个参数来设置一个默认值。
1 | $status = input('post.status', 0); // 如果post中没有status变量,则默认为0 |
获取整个数组:
- 如果想获取整个GET或POST数组,可以省略变量名。
1 | $allGetVars = input('get.'); // 获取所有GET变量 |
函数段解析thinkphp/helper.php:121
1 | function input($key = '', $default = null, $filter = '') |
$key
: 可选参数,用于指定要获取的数据的键(key),同时可以通过在键前加前缀来指定数据来源(例如post.name
表示从 POST 数据获取name
键的值)。如果键以?
开头,表示检查该键是否存在。$default
: 可选参数,如果指定的键不存在,则返回这个默认值。$filter
: 可选参数,用于指定应用于数据的过滤器。
检查键是否存在
1 | if (0 === strpos($key, '?')) { |
- 这段代码检查
$key
是否以?
开始。如果是,移除?
并设置一个标记$has
,表示后续操作应检查键是否存在而非直接获取值。
解析数据来源
1 | if ($pos = strpos($key, '.')) { |
- 这段代码检查
$key
中是否包含点 (.
),用来分隔数据来源和实际的键名。例如post.name
。 - 如果存在点分隔符,
explode
函数用来分离数据来源和键名。 - 检查分离出的来源是否在预定义的来源列表中,如果不在,则重设
$method
为param
(表示自动判断来源),并将原始的方法和键重新组合成$key
。
返回数据
1 | if (isset($has)) { |
- 如果设置了
$has
(查询键是否存在),调用has
方法来检查键是否存在,方法名动态从$method
变量获取(利用 PHP 的变量函数特性)。 - 如果没有设置
$has
,直接通过动态方法名获取键的值,传递$key
,$default
, 和$filter
作为参数。
0x04 路由机制
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 | // action变量的值作为操作方法传入 |
路由到重定向地址
旧的页面URL,如 /old-page
,想要将所有访问这个旧页面的请求重定向到新页面 /new-page
,下面是如何设置路由规则的例子:
1 | use think\Route; |
这些路由规则意味着:
- 访问
/old-page
时,用户会被自动重定向到/new-page
。 - 访问
/old-blog
时,用户会被自动重定向到http://example.com/new-blog
。
动态传递的路由参数
1 | // 动态重定向 |
在这个例子中,任何形如 /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 |
|
控制器类文件的实际位置是
1 | application\index\controller\Index.php |
访问URL地址是(假设没有定义路由的情况下)
1 | http://localhost/index.php/index |
在ThinkPHP中,URL访问地址通常通过路由来定义,路由决定了哪些URL可以访问哪些控制器中的方法。基于上面的Index
控制器例子,如果想通过URL访问index
方法,需要在路由配置文件中定义一个路由,如:
1 | // 路由定义可能位于 route.php 文件中 |
这意味着当用户访问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
)确定模块名的方法:
查看项目结构:模块名通常是
application
目录下的子目录名。你可以查看这个目录来确定你的应用包含哪些模块。配置文件:ThinkPHP的应用配置文件通常位于
application/config.php
,在这个文件中可以查看默认模块的设置,或者是模块的相关配置。例如:1
2
3
4
5
6
7
8return [
// 应用设置
'app' => [
'default_module' => 'index', // 默认模块名
'deny_module_list' => ['common'], // 禁止访问的模块
],
...
];URL访问:在未配置特殊路由的情况下,URL的结构通常反映了模块的使用。例如,访问
http://yourdomain.com/index/controller/action
中的index
可能就是模块名。
controller - 控制器名
action - 控制器中的方法名
如果你的控制器是HelloWorld
,并且定义如下:
1 |
|
控制器类文件的实际位置是
1 | application\index\controller\HelloWorld.php |
访问url
如果不指定方法名,ThinkPHP 默认会调用控制器中的 index
方法。因此,即使 URL 中没有明确指出要调用 index
方法,系统也会自动寻找并执行 HelloWorld
控制器中的 index()
方法。
1 | http://localhost/index.php/index/hello_world/index |
tips:控制器命名和URL映射
在ThinkPHP中,控制器的命名规范是大驼峰(PascalCase),即每个单词的首字母大写。但在URL中,控制器名通常会自动转换为小写,并且驼峰命名中的大写字母会通过下划线(_
)进行分隔。所以HelloWorld
控制器在URL中被表示为 hello_world
的原因。
也可以关闭自动转化,以原来的名字Helloworld来进行访问
1 | // 是否自动转换URL中的控制器和操作名 |
0x07 参考文章
模块设计 · ThinkPHP5.1完全开发手册 · 看云 (kancloud.cn)
0x08 碎碎念
写这篇文章时的碎碎念 👇
我发现我的脑子就像我的笔记一样。脑子里的内容稀碎时,笔记也是。
学习知识是一个闭环的过程。我在学习thinkphp框架的过程是:先把thinkphp的框架文档过了一遍,脑子很乱,但是能僵硬的换出来一些框架流程图。可是生涩不理解,脑子里构建不出来一副完整的画面。学着学着,有点没方向了,我又从路由看起。但是路由进去的时候,走controller。然后我就忽然,脑子灵光一闪,明白了框架的入口到底是个什么意思了。我之前没明白框架入口到底是个什么意思,现在忽然懂了,程序的入口不是route而是框架入口。它规定了路由是怎么走的。然后我就又忽然明白了,thinkphp多路由到底是个什么东西。
学习的时候,如果给你一个“意义”和目的,你就会自己推着自己向前走了。不过这也可能是我忽然探索明白了自己的学习模式。我学习的话,需要一个“意义驱动”。有了这样的驱动,我就有了自己的学习模式和方向。
学习代码审计的时候,我觉得特别没有方向,然后我看见p神说,要从框架底层看起。我就去看文档,不明白意义何在,看的云里雾里。后来我就去问chat森森,为什么要学框架,我想要代码审计,但我不明白我该从哪个方向看起,又要学到什么程度。后来chat森森跟我说“熟悉ThinkPHP或任何其他目标框架的目录结构、核心组件以及其工作方式。这包括了解其路由机制、控制器、模型、视图、中间件和服务提供者”。于是这个时候我的方向就是看路由机制是怎么运行的。看着看着,我自己就找到了方向,自己倒推到了“程序入口”和thinkphp的多路由机制。然后我就想到了y4er的文章。他就是从框架入口start.php 文件开始讲的。 那一刻!我完成了闭环,虽然我还没完全弄懂,但是我有了方向,继续看下去,我就理解了到底是怎么回事。再抽象一点回顾这个学习的过程。我发现这才是我“私人定制”的一种学习方式。我做事情一直都喜欢先把框架定出来,然后再有方向性的填内容进去。