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


    一、初识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 命令行入口文件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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()函数转换为小写。
      
      • 1
      • 2
      • 3
      • 4
    • get()获取$_GET变量

      示例:echo $request->get('name')
      使用助手函数示例:echo input('get.name') // 表示获取get请求的name变量的值,如果使用input('get.')的方式,表示获取所有get请求的变量
      
      • 1
      • 2
    • post()获取$_POST变量

    • file()获取$_FILE的内容

    • ip()获取请求的IP地址

    • method()获取请求的方法

    • pathInfo()获取控制器和方法名的路径信息

      示例:请求www.xxxx.com/index.php/index/index/hello
      echo $request>pathinfo()  //结果为index/index/hello
      
      • 1
      • 2
    • rootInfo()获取路由信息

    响应:

    响应内容的输出,包含以下方式:

    • 自动输出

      在config.ph中设置default_return_type 即可更改默认返回类型,达到自动输出的效果
      
      • 1
    • 手动输出

      输出json类型:return json($data)
      输出json类型,并设置响应码和http头:
      方法1return json($data,201,['set_cookie'=>'test_cookie'])
      方法2return json($data)->code(201)->gheader(['set_cookie'=>'test_cookie'])
      对于其他的输出类型,只需要更换为xml或者html等函数即可。
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 页面跳转

      示例:
      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()方法。
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 页面重定向

      示例:
      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)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

    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()
      
      • 1
      • 2
      • 3
      • 4
    • 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();
      
      • 1
      • 2
      • 3
    • 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();
      
      • 1
      • 2
      • 3
    • 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();
      
      • 1
      • 2

    二、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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    参数过滤情况:

    需要了解的参数过滤情况:

    • 原生参数: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函数
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 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;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
    • 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;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    • 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函数
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    • 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
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

    可以看到,我们的get()、post()、request()函数都调用了input()方法进行参数检查,但是我们传入的filter都是为空,也就是默认不进行检查,所以并不安全。下面就简单的分析几个例子看看.

    3、前台SQL注入分析

    漏洞描述

    在prevNext()函数中,未经过任何过滤就将参数直接拼接到了SQL语句中,造成了SQL注入。
    
    • 1

    漏洞分析:

    首先进入漏洞所在代码位置:/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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可见,在执行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行......
       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    由于该系统没有对传入的参数做进行过滤,所以在这里就可以直接构造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)--+
    
    • 1

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

    三、参考资料

  • 相关阅读:
    QUALITY-GATED CONVOLUTIONAL LSTM FOR ENHANCING COMPRESSED VIDEO
    论文笔记:SAITS: SELF-ATTENTION-BASED IMPUTATION FOR TIMESERIES
    大数据HBASE的详细使用
    『CV学习笔记』文本识别算法CRNN&SVTR介绍
    C++学习 --文件
    Java设计模式之桥接模式
    除了labview你还知道哪些工业控制领域的软件?
    “一人负债,全家背锅”,严厉打击信用卡套现欺诈
    pytorch安装教程
    GO语言篇之CGO
  • 原文地址:https://blog.csdn.net/qq_45590334/article/details/126485532