文章目录

  • 一、初识ThinkPHP
  • 1、目录文件结构
  • 2、URL与路由
  • 3、请求与响应
  • 4、数据交互
  • 二、ThinkPhp 框架审计案例1
  • 1、熟悉网站结构
  • 2、确定路由与过滤
  • 3、前台SQL注入分析
  • 三、参考资料


一、初识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)--+

就能够成功的利用报错注入,获取到系统中的用户名和数据库信息。

三、参考资料