文章目录
一、初识ThinkPHP
1、目录文件结构
├─application 应用目录(可设置)
│ ├─common 公共模块目录(可更改)
│ ├─index 模块目录(可更改)
│ │ ├─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 框架系统目录
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件
2、URL与路由
基本路由:
- ROOT_PATH => application
- THINK_PATH => thiinkphp
- EXTEND_PATH => extend
- VENDER_PATH => vender
URL访问:
www.xxx.com/index.php/index/index/index ,访问的位置为application目录下的index模块下的从contraller目录下的index文件下的index函数。
传入参数:
方式一:
www.xxx.com/index.php/hello/index/hello/name/word/city/chengdu,对于这种传入参数的方式,表示访问hello模块下的index文件下的hello函数,传入的参数1为name,传入的值为word,传入的第二个参数为city,传入的值为cehngdu,对于这两个参数的传入没有顺序要求,比如请求URL为 www.xxx.com/index.php/hello/index/hello/city/chengdu/name/word也是一样的效果。
方式2:
www.xxx.com/index.php/hello/index/hello/name=word&citychengdu,这就是常见的传参方式,相对容易理解一些。
3、请求与响应
没有使用传统的$_GET
,$_POST
,$_COOKIE
,$_SESSION
等全局变量,而是提供了Request对象进行调用。
ThinkPHP5的request对象由think\Request类完成。
在thinkphp5中,通过reques对象获取请求内容的方法有下面这几种:
- 继承think\Controller
- 自动注入请求对象
- 使用助手函数
获取请求变量:
-
param()获取请求变量
$request->param()方法,用于获取所有的变量,对于变量的获取,具有一定的优先级,优先级情况如下: 路由变量 > 当前请求变量($_POST变量) > $_GET变量 使用示例:echo $request->param('name','yujun','stryolower') 详解:该示例表示获取name变量的值,如果没有获取到,默认为yujun,如果获取到了使用strtolower()函数转换为小写。
-
get()获取$_GET变量
示例:echo $request->get('name') 使用助手函数示例:echo input('get.name') // 表示获取get请求的name变量的值,如果使用input('get.')的方式,表示获取所有get请求的变量
-
post()获取$_POST变量
-
file()获取$_FILE的内容
-
ip()获取请求的IP地址
-
method()获取请求的方法
-
pathInfo()获取控制器和方法名的路径信息
示例:请求www.xxxx.com/index.php/index/index/hello echo $request—>pathinfo() //结果为index/index/hello
-
rootInfo()获取路由信息
响应:
响应内容的输出,包含以下方式:
-
自动输出
在config.ph中设置default_return_type 即可更改默认返回类型,达到自动输出的效果
-
手动输出
输出json类型:return json($data) 输出json类型,并设置响应码和http头: 方法1:return json($data,201,['set_cookie'=>'test_cookie']) 方法2:return json($data)->code(201)->gheader(['set_cookie'=>'test_cookie']) 对于其他的输出类型,只需要更换为xml或者html等函数即可。
-
页面跳转
示例: public function hello($name){ if($name==='thinkphp'){ $this->success("hello,you are thinkphp","admin")} else{ $this->error("ou error!!","test")} } public function admin(){ return "hello,your right"; } public function test(){ return "your are error!!"; } 此时,我们请求hello方法,传入$name=thinkphp,则会跳转到admin()方法,如果传入错误,则会输出错误信息后,跳转到test()方法。
-
页面重定向
示例: public function hello($name){ if($name==='thinkphp'){ $this->redicret("http://www.baidu.com"); }else{ $this->redict("http://www.163.com")}; } 利用的是302功能码的重定向功能。 也可以设置跳转的功能码,比如设置为301: $this->redicret("http://www.baidu.com",301)
4、数据交互
数据库的的基本配置,在database.php中。
查询表达式:
-
selec * from table_bame where id='$id'
方法1:$result=Db::query("select * from test_table where id='$id'") 方法2:$result=Db::name("test_table")->where('id',1)->find(); 方法3:$result=Db::name("table_name")->where('id',$id)->select; 方法4(参数绑定):$result=Db::name('table_name')->where("id=:id",["id"=>$id])->select()
-
select * from table_name where id>'$id' limit 0,10
方法1:$result=Db::query("select * from test_table where id >'$id' limit 0,10") 方法2:$result=Db::name("test_table")->where('id','>',$id)->limit(10)->find(); 方法3:$result=Db::name("table_name")->where('id','>',$id)->limit(10)->select();
-
select * from table_name wheere id='$id' and password='$passwd'
方法1:$result=Db::query("select * from table_name wheere id='$id' and password='$passwd'") 方法2:$result=Db::name("test_table")->where('id',$id)->where('password',$passwd)->find(); 方法3:$result=Db::name("test_table")->where(['id'=>[$id],'passwd'=>[$passwd]])->find();
-
select user_name from table_name where id='$id'
方法1: $result=Db::name(table_name)->column('user_name')->where('id',$id)->find() 方法2(参数绑定):$result::Db::name(table_name)-culomn('user_name')->where("id=:id",["id"=>$id])->select();
二、ThinkPhp 框架审计案例1
审计系统:hsyCMS v3.0
涉及漏洞:XSS、SQL注入、文件删除。
1、熟悉网站结构
熟悉网站结构,需要做到一下几点:
- 了解网站目录结构
- 了解系统功能
- 前台功能
- 后台功能
- 分析可能存在的测试点
- 前台测试点分析
- 留言
- 搜索
- 后台测试点分析
- 登陆、注册、密码找回
- 文件上传、下载、读取
- SQL注入、http头注入、代码注入
- 前台测试点分析
2、确定路由与过滤
路由: app/route.php
use think\Route;
//前端路由配置
if (is_file(APP_PATH.'common/install.lock')) {
$routeNav = db('nav')->field('entitle')->order('sort,id')->select(); //从nav表中查询entitle
$routeCate = db('cate')->field('entitle')->order('sort,id')->select(); //从cate表中查询entitle
Route::rule('search','index/Search/index'); //将search 路由到 index模块的Search控制器下的index方法下
foreach ($routeNav as $key=>$v) {
Route::rule($v['entitle'],'index/Article/index'); //将从nav表中查询出的entitle循环路由到index/Article/index
Route::rule($v['entitle'].'/:id','index/Show/index');
}
foreach ($routeCate as $key=>$v) {
Route::rule($v['entitle'],'index/Article/index'); //将从cate表中查询出的rntitle循环路由到index/Artitle/index
}
}
参数过滤情况:
需要了解的参数过滤情况:
- 原生参数:GET、POST、RERUEST
- 系统外部变量获取函数:get()、post()、Request()
Requet类函数分析:libs\libray\thibk\Request.php
-
get()
public function get($name = '', $default = null, $filter = '') { if (empty($this->get)) { $this->get = $_GET; //将$_GET中的参数赋值到$this—>get变量 } if (is_array($name)) { //如果传入的$name是数组 $this->param = []; $this->mergeParam = false; return $this->get = array_merge($this->get, $name); //将传入的GET参数和$name合并为一个数组 } return $this->input($this->get, $name, $default, $filter); //调用input函数 }
-
input()
public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { // 获取原始数据 return $data; } $name = (string) $name; if ('' != $name) { // 解析name if (strpos($name, '/')) { //如果name中存在“/” list($name, $type) = explode('/', $name);//将$name拆分为$name和$type } else { $type = 's'; } foreach (explode('.', $name) as $val) { // 按.拆分成多维数组进行判断 if (isset($data[$val])) { $data = $data[$val]; } else { return $default; // 无输入数据,返回默认值 } } if (is_object($data)) { return $data; } } $filter = $this->getFilter($filter, $default);// 调用解析过滤器,$default为空 if (is_array($data)) { //如果输入的数据是数组,调用array_walk_recursive()并使用$filter作为过滤器 array_walk_recursive($data, [$this, 'filterValue'], $filter); reset($data); } else { $this->filterValue($data, $name, $filter); //调用filtervalue() } if (isset($type) && $data !== $default) { //没有设置$type,也就是$name中不存在“/” $this->typeCast($data, $type); // 强制类型转换 } return $data; }
-
getFilter()
protected function getFilter($filter, $default) { if (is_null($filter)) { //默认为空,所以并不会进行过滤 $filter = []; } else { //不为空, $filter = $filter ?: $this->filter; if (is_string($filter) && false === strpos($filter, '/')) { $filter = explode(',', $filter); } else { $filter = (array) $filter; } } $filter[] = $default; return $filter; }
-
post()
public function post($name = '', $default = null, $filter = '') { if (empty($this->post)) { $content = $this->input; if (empty($_POST) && false !== strpos($this->contentType(), 'application/json')) { $this->post = (array) json_decode($content, true); } else { $this->post = $_POST; } } if (is_array($name)) { $this->param = []; $this->mergeParam = false; return $this->post = array_merge($this->post, $name); } return $this->input($this->post, $name, $default, $filter); //调用inout函数 }
-
Request()
public function request($name = '', $default = null, $filter = '') { if (empty($this->request)) { $this->request = $_REQUEST; //获取$_request } if (is_array($name)) { //如果$name为数组,返回合并后的数组 $this->param = []; $this->mergeParam = false; return $this->request = array_merge($this->request, $name); } return $this->input($this->request, $name, $default, $filter); //调用inout }
可以看到,我们的get()、post()、request()函数都调用了input()方法进行参数检查,但是我们传入的filter都是为空,也就是默认不进行检查,所以并不安全。下面就简单的分析几个例子看看.
3、前台SQL注入分析
漏洞描述:
在prevNext()函数中,未经过任何过滤就将参数直接拼接到了SQL语句中,造成了SQL注入。
漏洞分析:
首先进入漏洞所在代码位置:/app/index/common.php的preNext()函数
//获取上下篇
function prevNext($id,$entitle,$one){
//上一篇
$prev=db('article')->field("id,title")->where("id < {
$id} and nid={
$one['nid']} and cid={
$one['cid']}")->order('id desc')->limit('1')->find();
//执行的Sql语句: select id,title from sy_article where ( id < $id and uid=$one['nid'] and cid=$one['cid'] ) oeder by id desc limit 0,1
if($prev){
$prev['url'] = '/'.$entitle.'/'.$prev['id'].'.html';
}else{
$prev['url'] = "javascript:void(0)";
$prev['title'] = "没有了";
}
$data['prev'] = $prev;
//下一篇
$next = db('article')->field("id,title")->where("id > {
$id} and nid= {
$one['nid']} and cid = {
$one['cid']}")->order('id asc')->limit('1')->find();
if($next){
$next['url'] = '/'.$entitle.'/'.$next['id'].'.html';
}else{
$next['url'] = "javascript:void(0)";
$next['title'] = "没有了";
}
$data['next'] = $next;
return $data;
}
可见,在执行SQL语句的时候,通过where函数执行设定了判断条件,将id等参数拼接到了sql语句中,所以存在SQl注入的风险。
然后我们逆向查找一下,发现在app/index/controller/Show.php里面的index()方法调用了此方法,我们进入分析一下:
public function index()
{
$id = input('id'); //通过input助手函数获取传入的参数id(并没有经过过滤)
$one = db('article')->where('id',$id)->find();
if(empty($one)){
exit("文章不存在");}
$navrow = db('nav')->where('id',$one['nid'])->find();
//省略n行..........
if($data['showcate']==1){
//省略n行......
$data['pn'] = prevNext($id,$navrow['entitle'],$one);
}
$data['one'] = $one;
$data['nid'] = $one['nid'];
$data['site'] = getseo($one['nid'],$id,$one['cid']);
$this->assign($data);
//省略n行......
由于该系统没有对传入的参数做进行过滤,所以在这里就可以直接构造sql注入语句进行注入。比如构造这样一个payload:
http://www.xxx.com/index.php/index/show/index?id=123) and (select 1 from (select count(*),concat(user(),0x7e,database(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
就能够成功的利用报错注入,获取到系统中的用户名和数据库信息。