当前位置: 首页 > 工具软件 > hsycms > 使用案例 >

PHP代码审计16—ThinkPHP代码审计入门

钮才哲
2023-12-01

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

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

三、参考资料

 类似资料: