曝光过程 2016年8月11日，有人在seclists上公布了这个漏洞的POC，但是针对的是latest.php这个文件的注入，zabbix官方已经在7月22日发布了补丁，在3.0.4版本进行了修复，但是仅修复了latest.php的注入问题 紧接着8月12日，有人在seclists上进行回复，发现另一处jsrpc.php的注入，但两个漏洞的原理是一样的，3.0.4是否修补jsrpc.php的注入不得而知。 Right, it’s the same vuln, just in different places. It was fixed in 3.0.4. 漏洞分析 我们以jsrpc.php的注入为例进行分析，采用zabbix的3.0.3版本，源码编译安装。先看一下POC： http://127.0.0.1:8001/jsrpc.php? type=9& method=screen.get& timestamp=1471403798083& pageFile=history.php& profileIdx=web.item.graph& profileIdx2=1+or+updatexml%281,md5%280x11%29,1%29+or+1=1%29%23& updateProfile=true& period=3600& stime=20160817050632& resourcetype=17 注入点在profileIdx2处，根据POC的结果可以看出来是insert类型的注入。 首先查看jsrpc.php文件，根据method参数，直接来到180行： case 'screen.get': $result = ''; $screenBase = CScreenBuilder::getScreen($data); if ($screenBase !== null) { $screen = $screenBase->get(); if ($data['mode'] == SCREEN_MODE_JS) { $result = $screen; } else { if (is_object($screen)) { $result = $screen->toString(); } } } break; 首先跟进CScreenBuilder::getScreen($data)函数，其中$data参数就是$_REQUEST，在31行被赋值。 在getScreen函数中，根据请求的resourcetype不同，会初实例化不同的类，在POC中看到resourcetype的值为17，从include/defines.inc.php中可以看到17对应的为SCREEN_RESOURCE_HISTORY。 define('SCREEN_RESOURCE_HOSTGROUP_TRIGGERS',14); define('SCREEN_RESOURCE_SYSTEM_STATUS', 15); define('SCREEN_RESOURCE_HOST_TRIGGERS', 16); // used in Monitoring > Latest data > Graph (history.php) define('SCREEN_RESOURCE_HISTORY', 17); define('SCREEN_RESOURCE_CHART', 18); define('SCREEN_RESOURCE_LLD_SIMPLE_GRAPH', 19); define('SCREEN_RESOURCE_LLD_GRAPH', 20); 紧接着我们就可以从getScreen函数中找到对应的处理代码： case SCREEN_RESOURCE_HISTORY: return new CScreenHistory($options); 返回的是CScreenHistory类的实例，继续跟进CScreenHistory类，看到该类是继承自CScreenBase的，传入的参数其实还是$_REQUEST。简单的通读一下这个类的代码加上搜索功能，定位到了下面这段代码： // Calculate timeline. if ($this->required_parameters['timeline'] && $this->timeline === null) { $this->timeline = $this->calculateTime([ 'profileIdx' => $this->profileIdx, 'profileIdx2' => $this->profileIdx2, 'updateProfile' => $this->updateProfile, 'period' => array_key_exists('period', $options) ? $options['period'] : null, 'stime' => array_key_exists('stime', $options) ? $options['stime'] : null ]); } 发现在构造函数中，只有这里用到了profileIdx2参数，于是继续跟进calculateTime函数。在这个函数中，有两个函数用到了$options["profileIdx2"]，分别是： CProfile::get() // 从CProfile类的$profiles数组中获取数据。 CProfile::update() // 更新CProfile类的$profiles,$insert,$update数组。 到现在已经走完了实例化的过程，在jsrpc.php中要继续调用get()方法，不过这个函数通读下来并没有什么与漏洞相关的信息，然后继续在jsrpc.php中向下跟，到最后的部分会看到： require_once dirname(__FILE__).'/include/page_footer.php'; 所以我们跟到这个文件中去，在38行左右有如下代码： if (CProfile::isModified()) { DBstart(); $result = CProfile::flush(); DBend($result); } 继续进入CProfile::flush()函数，看到如下代码： foreach (self::$insert as $idx => $profile) { foreach ($profile as $idx2 => $data) { $result &= self::insertDB($idx, $data['value'], $data['type'], $idx2); } } self::$insert就是我们之前使用CProfile::update()方法设置过的，可以看到又取出了idx2，并送入了self::insertDB函数，继续跟到self::insertDB函数中去： private static function insertDB($idx, $value, $type, $idx2) { $value_type = self::getFieldByType($type); $values = [ 'profileid' => get_dbid('profiles', 'profileid'), 'userid' => self::$userDetails['userid'], 'idx' => zbx_dbstr($idx), $value_type => zbx_dbstr($value), 'type' => $type, 'idx2' => $idx2 ]; return DBexecute('INSERT INTO profiles ('.implode(', ', array_keys($values)).') VALUES ('.implode(', ', $values).')'); } 在最后的DBexecute函数中，插入了我们的profileIdx2，没有进行任何过滤，导致了INSERT类型的SQL注入出现。另外一种latest.php的注入原理与这个比较相近，各位可以自行分析一下，这里就不再赘述了。
有时候总是会不小心把存有账号密码之类的敏感文件push到github上。如果这次commit只有这一个文件，并且发现的及时，那还是很好解决的，删除commit然后强推就可以了。 但是不幸的是，总有那么些时候发现的比较晚，并且那次commit中含有其他的文件。 方法1： 试试BFG这个工具吧。 http://rtyley.github.io/bfg-repo-cleaner/ 方法2： 使用git filter-branch命令处理。 step 1 clone项目回来。 step 2 执行以下命令： $ git filter-branch --force --index-filter \ 'git rm --cached --ignore-unmatch PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA' \ --prune-empty --tag-name-filter cat -- --all 记得把其中的PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA替换成实际包含敏感信息的文件。 step 3 这个时候该文件其实已经删掉了，但是为了以防万一，现在你可以把它加到.gitignore文件中。或是做一些其他的必要操作。 step 4 强推回去： $ git push origin --force --all 记得把tag也强推一遍，如果没有打过tag，可以不用。 $ git push origin --force --tags step 5 检查一下有没有出现其他的问题，例如少文件，文件内容不对等。没有的话就可以做最后的操作了： $ git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin $ git reflog expire --expire=now --all $ git gc --prune=now PS 如果有其他人和你一起维护这个项目，那么应该让他们用rebase来获取最新的内容，千万不要使用merge，不然历史记录中还会出现那些敏感信息的，因为他们的本地log中还存在这些内容，merge的话会重新推上去。 参考资料：https://help.github.com/articles/remove-sensitive-data/
CI提供了钩子这种特性，可以在不修改核心文件的情况下用来修改框架内部执行流程。 全局来看，是否开启钩子可以在APPPATH/config/config.php中进行修改，而hooks的配置则在APPPATH/config/hooks.php中进行定义，具体如何实现可以参考官方文档：http://codeigniter.org.cn/user_guide/general/hooks.html?highlight=hooks 这里简单讲一下hooks的定义方式，有助于后面的源码理解。 第一种定义方式，使用数组定义的形式： $hook['pre_controller'] = array( 'class' => 'MyClass', 'function' => 'Myfunction', 'filename' => 'Myclass.php', 'filepath' => 'hooks', 'params' => array('beer', 'wine', 'snacks') ); 另一种出现在PHP5.3以后，可以使用lambda的闭包或是匿名函数作为钩子： $hook['post_controller'] = function() { /* do something here */ }; 如果想在同一个挂钩点添加多个钩子，将其改为二维数组即可。看一下Hooks.php的精简版代码，具体的解析写在了代码的注释中。 <?php defined('BASEPATH') OR exit('No direct script access allowed'); class CI_Hooks { // 钩子是否启用 public $enabled = FALSE; // 所有的hook数组 public $hooks = array(); // 存储hooks的类 protected $_objects = array(); // 运行标志，防止死循环 protected $_in_progress = FALSE; public function __construct() { // 加载配置文件 $CFG =& load_class('Config', 'core'); log_message('info', 'Hooks Class Initialized'); // 检测配置文件中是否存在enable_hooks选项，没有的话直接返回 if ($CFG->item('enable_hooks') === FALSE) { return; } // 查找hooks文件 if (file_exists(APPPATH.'config/hooks.php')) { include(APPPATH.'config/hooks.php'); } if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/hooks.php')) { include(APPPATH.'config/'.ENVIRONMENT.'/hooks.php'); } // 没有hooks，结束。 if ( ! isset($hook) OR ! is_array($hook)) { return; } // 更改相关状态 $this->hooks =& $hook; $this->enabled = TRUE; } public function call_hook($which = '') { // 如果hooks没有启用，或没有传入合法的hooks，返回FALSE if ( ! $this->enabled OR ! isset($this->hooks[$which])) { return FALSE; } // 如果hooks的定义方式为数组，并且没有指定function的时候，依次运行 if (is_array($this->hooks[$which]) && ! isset($this->hooks[$which]['function'])) { foreach ($this->hooks[$which] as $val) { $this->_run_hook($val); } } else // 对于PHP5.3以上，采用匿名函数的时候进入else分支 { $this->_run_hook($this->hooks[$which]); } return TRUE; } protected function _run_hook($data) { // 检测参数是否可调用 if (is_callable($data)) { is_array($data) ? $data[0]->{$data[1]}() : $data(); return TRUE; } elseif ( ! is_array($data)) { // 如果不可调用、也不是数组，返回FALSE return FALSE; } // 如果正在运行，则返回 if ($this->_in_progress === TRUE) { return; } // 检查是否设置了相关文件名和路径 if ( ! isset($data['filepath'], $data['filename'])) { return FALSE; } // 连接路径 $filepath = APPPATH.$data['filepath'].'/'.$data['filename']; // 检测文件合法性 if ( ! file_exists($filepath)) { return FALSE; } // Determine and class and/or function names $class = empty($data['class']) ? FALSE : $data['class']; $function = empty($data['function']) ? FALSE : $data['function']; $params = isset($data['params']) ? $data['params'] : ''; if (empty($function)) { return FALSE; } // Set the _in_progress flag $this->_in_progress = TRUE; // Call the requested class and/or function if ($class !== FALSE) { // The object is stored? if (isset($this->_objects[$class])) { if (method_exists($this->_objects[$class], $function)) { $this->_objects[$class]->$function($params); } else { return $this->_in_progress = FALSE; } } else { class_exists($class, FALSE) OR require_once($filepath); if ( ! class_exists($class, FALSE) OR ! method_exists($class, $function)) { return $this->_in_progress = FALSE; } // Store the object and execute the method $this->_objects[$class] = new $class(); $this->_objects[$class]->$function($params); } } else { function_exists($function) OR require_once($filepath); if ( ! function_exists($function)) { return $this->_in_progress = FALSE; } $function($params); } $this->_in_progress = FALSE; return TRUE; } } 结合注释来看，整体比较简单。
基准测试类是系统自动加载的，无需用户手动加载。 先贴上代码精简版的Benchmark类 <?php defined('BASEPATH') OR exit('No direct script access allowed'); class CI_Benchmark { public $marker = array(); public function mark($name) { $this->marker[$name] = microtime(TRUE); } public function elapsed_time($point1 = '', $point2 = '', $decimals = 4) { if ($point1 === '') { return '{elapsed_time}'; } if ( ! isset($this->marker[$point1])) { return ''; } if ( ! isset($this->marker[$point2])) { $this->marker[$point2] = microtime(TRUE); } return number_format($this->marker[$point2] - $this->marker[$point1], $decimals); } public function memory_usage() { return '{memory_usage}'; } } 整个类比较简单，有一个$marker变量，三个函数，分别来看一下。 marker变量 存储所有的benchmark点的信息。 mark($name) 这个函数只是简单的调用了microtime函数，并把基准点的名称存入了marker数组中。 elapsed_time($point1 = '', $point2 = '', $decimals = 4) 这个函数是计算两个基准点直接的时间差，接受三个参数。 有个注意点是当$point1为空的时候直接返回了一个字符串，这个字符串其实在后面的输出时被替换成了其他的东西，暂时不管。 memory_usage() 同上，只是返回了一个字符串，也是在后面输出时被替换掉了。
在包含完common.php中的函数后，开始真正的进行一些列的工作。 先是做一些安全方面的处理问题，如果PHP的版本<=5.4，那么会进行防注册全局变量的操作，因为注册全局变量的功能在PHP5.3开始被废弃，并且在PHP5.4以后被移除了。 紧接着设置了错误处理： // 设置错误处理 set_error_handler('_error_handler'); set_exception_handler('_exception_handler'); register_shutdown_function('_shutdown_handler'); 接下来检查了核心类扩展的前缀问题： if ( ! empty($assign_to_config['subclass_prefix'])) { get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix'])); } 通常情况下$assign_to_config变量应当在index.php中设置，subclass_prefix的值在APPPATH/config/config.php文件中设置。 这里通过$assign_to_config['subclass_prefix']指定用户的扩展核心的前缀名。 注意一点，index.php中的subclass_prefix具有更高的优先权。 紧接着检查了是否应当使用Composer autoloader来进行加载。暂时忽略这部分。 后面的部分就是加载了一系列的组件来帮助CI完成更多的功能，并进行一系列的初始化操作。 基准测试类BM（Benchmark），用于计时、性能测试等功能。 扩展组件EXT（Hooks），CI的扩展组件，类似于钩子，可以在不改变CI核心代码的情况下增加功能。然后调用了pre_system这个挂钩点，如果存在的话。 配置文件管理CFG（Config），用于加载配置文件，获取配置项等。加载完成后也处理了一下$assign_to_config的变量。 UNI（Utf-8），负责处理UTF-8相关的工作。 URI和RTR，前者负责解析URI并获取参数等，后者是路由组件 OUT（output），输出组件，所有的输出均是通过该组件输出的。 SEC（Security），负责处理安全相关的问题。 IN(Input），负责处理输入相关的东西。 LANG（Lang），处理语言相关的东西。 这些组件的加载方式都是通过调用load_class函数来处理的，第二个参数指文件所在的目录。
文件的一开始就有如下代码： defined('BASEPATH') OR exit('No direct script access allowed'); 这个是为了防止直接访问文件而写的，这样可以保证是从index.php访问过来的。 接下来两段代码引入了两个文件： /* * ------------------------------------------------------ * Load the framework constants * ------------------------------------------------------ */ // 引入全局变量文件 // 包括文件权限、open的标志位、是否显示backtrace、退出状态码 if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/constants.php')) { require_once(APPPATH.'config/'.ENVIRONMENT.'/constants.php'); } require_once(APPPATH.'config/constants.php'); /* * ------------------------------------------------------ * Load the global functions * ------------------------------------------------------ */ // 引入全局函数 require_once(BASEPATH.'core/Common.php'); 我在注释中已经写明了这两个文件的作用，constants.php这个文件比较普通，没啥需要注意的，我们看看Common.php这个文件。 这个文件中定义了一些常用的公共函数，而且定义的方式也是比较特别，采用了如下的形式： if (! function_exists('function_name')) { function function_name () { //TODO } } 很明显这是为了防止函数名重复而采取的容错措施，所以用户在自己定义系统级的全局函数的时候也应该采用这种方式，防止函数名重复。 我们依次每个函数看一下。 is_php 很明显这个函数是用于判断版本号的，函数内部有一个static的$_is_php变量，用于缓存version_compare的结果，这样可以每次在使用is_php函数的时候提高效率。 /** * Determines if the current version of PHP is equal to or greater than the supplied value * * @param string * @return bool TRUE if the current version is $version or higher */ function is_php($version) { static $_is_php; $version = (string) $version; if ( ! isset($_is_php[$version])) { $_is_php[$version] = version_compare(PHP_VERSION, $version, '>='); } return $_is_php[$version]; } is_really_writable 这个函数是用于判断某个文件或目录是否可写，本质上该函数是is_writable()的封装，至于为何要封装，有两点原因： 在windows系统中，如果设置了read-only属性，is_writable()会返回true，但是实际上该文件、目录并不可写。 在unix系统中，如果safe_mode的值为on，那么is_writable()的结果也会出现问题。 因此，is_really_writable()需要对这两种情况进行特殊判断，如果在Unix系统上，而且safe_mode的值为off，直接调用is_writable()。否则的话，如果目标是目录，则尝试写入一个文件来判断是否可写，如果目标是文件，则尝试使用fopen来判断是否可写。 /** * Tests for file writability * * is_writable() returns TRUE on Windows servers when you really can't write to * the file, based on the read-only attribute. is_writable() is also unreliable * on Unix servers if safe_mode is on. * * @link https://bugs.php.net/bug.php?id=54709 * @param string * @return bool */ function is_really_writable($file) { // If we're on a Unix server with safe_mode off we call is_writable if (DIRECTORY_SEPARATOR === '/' && (is_php('5.4') OR ! ini_get('safe_mode'))) { return is_writable($file); } /* For Windows servers and safe_mode "on" installations we'll actually * write a file then read it. Bah... */ if (is_dir($file)) { $file = rtrim($file, '/').'/'.md5(mt_rand()); if (($fp = @fopen($file, 'ab')) === FALSE) { return FALSE; } fclose($fp); @chmod($file, 0777); @unlink($file); return TRUE; } elseif ( ! is_file($file) OR ($fp = @fopen($file, 'ab')) === FALSE) { return FALSE; } fclose($fp); return TRUE; } load_class 这个函数比较特殊，返回的是类实例的引用，（参考资料：http://www.nowamagic.net/academy/detail/1205559） 该函数接受三个参数，分别是要加载的类名，该类文件所在的目录，以及传递给该类的构造函数的参数。 该函数的内部有一个$_classes数组，用于缓存已经实例化的类。如果需要加载的类已经加载过了，则直接从$_classes数组中进行返回。 对于类文件的寻找，会先寻找application/libraries目录，如果没有找到则会寻找system/libraries目录，如果这两个目录都没有找到的话，再继续从用户提供的$directory目录进行寻找。 查找完成后，继续查找扩展目录，如果找到的话把扩展文件也包含进来。 如果没有找到的话则返回503并退出。 接下来调用is_loaded()函数将其加入已经加载类的列表中，然后调用构造函数，并返回类的实例。 /** * Class registry * * This function acts as a singleton. If the requested class does not * exist it is instantiated and set to a static variable. If it has * previously been instantiated the variable is returned. * * @param string the class name being requested * @param string the directory where the class should be found * @param string an optional argument to pass to the class constructor * @return object */ function &load_class($class, $directory = 'libraries', $param = NULL) { static $_classes = array(); // Does the class exist? If so, we're done... if (isset($_classes[$class])) { return $_classes[$class]; } $name = FALSE; // Look for the class first in the local application/libraries folder // then in the native system/libraries folder foreach (array(APPPATH, BASEPATH) as $path) { if (file_exists($path.$directory.'/'.$class.'.php')) { $name = 'CI_'.$class; if (class_exists($name, FALSE) === FALSE) { require_once($path.$directory.'/'.$class.'.php'); } break; } } // Is the request a class extension? If so we load it too if (file_exists(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php')) { $name = config_item('subclass_prefix').$class; if (class_exists($name, FALSE) === FALSE) { require_once(APPPATH.$directory.'/'.$name.'.php'); } } // Did we find the class? if ($name === FALSE) { // Note: We use exit() rather then show_error() in order to avoid a // self-referencing loop with the Exceptions class set_status_header(503); echo 'Unable to locate the specified class: '.$class.'.php'; exit(5); // EXIT_UNK_CLASS } // Keep track of what we just loaded is_loaded($class); $_classes[$class] = isset($param) ? new $name($param) : new $name(); return $_classes[$class]; } is_loaded 这个函数比较简单，注意下同样也是返回的引用。 /** * Keeps track of which libraries have been loaded. This function is * called by the load_class() function above * * @param string * @return array */ function &is_loaded($class = '') { static $_is_loaded = array(); if ($class !== '') { $_is_loaded[strtolower($class)] = $class; } return $_is_loaded; } get_config 该函数主要是读取主要的config.php文件，可以在Config类没有实例化的情况下收集配置信息。注意该函数也是返回引用的。 这个函数只会加载主配置文件，如果存在对应环境的配置文件，则会加载对应的配置文件，因为在整个框架配置完成前（config实例化之前），需要该函数来读取对应的配置文件。 这里有一次动态替换配置信息的机会，只需要传递对应的数组即可，只有一次机会的原因在下个函数中说明。 /** * Loads the main config.php file * * This function lets us grab the config file even if the Config class * hasn't been instantiated yet * * @param array * @return array */ function &get_config(Array $replace = array()) { static $config; if (empty($config)) { $file_path = APPPATH.'config/config.php'; $found = FALSE; if (file_exists($file_path)) { $found = TRUE; require($file_path); } // Is the config file in the environment folder? if (file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/config.php')) { require($file_path); } elseif ( ! $found) { set_status_header(503); echo 'The configuration file does not exist.'; exit(3); // EXIT_CONFIG } // Does the $config array exist in the file? if ( ! isset($config) OR ! is_array($config)) { set_status_header(503); echo 'Your config file does not appear to be formatted correctly.'; exit(3); // EXIT_CONFIG } } // Are any values being dynamically added or replaced? foreach ($replace as $key => $val) { $config[$key] = $val; } return $config; } config_item 这个函数用于获取特定的配置项目，这里有一个$_config变量，用做类似缓存的作用，所以下面的get_config函数只有一次执行机会，这也是为何上面的函数说只有一次动态修改变量的机会。因为引用类型不能直接赋值给静态变量，所以这里使用数组的方式赋值。 /** * Returns the specified config item * * @param string * @return mixed */ function config_item($item) { static $_config; if (empty($_config)) { // references cannot be directly assigned to static variables, so we use an array $_config[0] =& get_config(); } return isset($_config[0][$item]) ? $_config[0][$item] : NULL; } get_mimes 这个文件也比较简单，就是返回了mimes.php文件中定义的MIME类型。 /** * Returns the MIME types array from config/mimes.php * * @return array */ function &get_mimes() { static $_mimes; if (empty($_mimes)) { if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/mimes.php')) { $_mimes = include(APPPATH.'config/'.ENVIRONMENT.'/mimes.php'); } elseif (file_exists(APPPATH.'config/mimes.php')) { $_mimes = include(APPPATH.'config/mimes.php'); } else { $_mimes = array(); } } return $_mimes; } is_https/is_cli 这两个函数都比较简单，没啥好说的。 show_error/show_404 这俩函数没啥好说的，都会终止代码的执行，并显示相关的错误信息。 log_message 调用Log class来记录log信息，代码比较简单。 据说如果主配置文件中log_threshold被设置为0，则不会记录任何Log信息。 set_status_header CI框架允许用户自定义HTTP协议的头信息，CI的Output组件提供了set_status_header接口，通过这个接口可以设置头信息。 _error_handler/_exception_handler/_shutdown_handler 一系列的错误处理函数，没有什么很特别的地方，与其他框架的类似。 remove_invisible_characters 这个函数比较简单，去除$str中的不可见字符，根据是否设置了$url_encoded参数来满足不同情况下的需求。 /** * Remove Invisible Characters * * This prevents sandwiching null characters * between ascii characters, like Java\0script. * * @param string * @param bool * @return string */ function remove_invisible_characters($str, $url_encoded = TRUE) { $non_displayables = array(); // every control character except newline (dec 10), // carriage return (dec 13) and horizontal tab (dec 09) if ($url_encoded) { $non_displayables[] = '/%0[0-8bcef]/'; // url encoded 00-08, 11, 12, 14, 15 $non_displayables[] = '/%1[0-9a-f]/'; // url encoded 16-31 } $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 do { $str = preg_replace($non_displayables, '', $str, -1, $count); } while ($count); return $str; } html_escape 这个函数是个递归调用，对传入参数数组中的每一个元素进行htmlspecialchars操作。 /** * Returns HTML escaped variable. * * @param mixed $var The input string or array of strings to be escaped. * @param bool $double_encode $double_encode set to FALSE prevents escaping twice. * @return mixed The escaped string or array of strings as a result. */ function html_escape($var, $double_encode = TRUE) { if (empty($var)) { return $var; } if (is_array($var)) { return array_map('html_escape', $var, array_fill(0, count($var), $double_encode)); } return htmlspecialchars($var, ENT_QUOTES, config_item('charset'), $double_encode); } _stringify_attributes 这个函数也比较简单，将传进来的数组展开为key=val的形式再传出，第二参数可以指定val的部分是否有双引号包裹。 /** * Stringify attributes for use in HTML tags. * * Helper function used to convert a string, array, or object * of attributes to a string. * * @param mixed string, array, object * @param bool * @return string */ function _stringify_attributes($attributes, $js = FALSE) { $atts = NULL; if (empty($attributes)) { return $atts; } if (is_string($attributes)) { return ' '.$attributes; } $attributes = (array) $attributes; foreach ($attributes as $key => $val) { $atts .= ($js) ? $key.'='.$val.',' : ' '.$key.'="'.$val.'"'; } return rtrim($atts, ','); } function_usable 用于检测某个函数是否可用，因为如果加载了Suhosin扩展的话，会造成一些影响。注释中解释的比较明白了。 /** * Function usable * * Executes a function_exists() check, and if the Suhosin PHP * extension is loaded - checks whether the function that is * checked might be disabled in there as well. * * This is useful as function_exists() will return FALSE for * functions disabled via the *disable_functions* php.ini * setting, but not for *suhosin.executor.func.blacklist* and * *suhosin.executor.disable_eval*. These settings will just * terminate script execution if a disabled function is executed. * * The above described behavior turned out to be a bug in Suhosin, * but even though a fix was commited for 0.9.34 on 2012-02-12, * that version is yet to be released. This function will therefore * be just temporary, but would probably be kept for a few years. * * @link http://www.hardened-php.net/suhosin/ * @param string $function_name Function to check for * @return bool TRUE if the function exists and is safe to call, * FALSE otherwise. */ function function_usable($function_name) { static $_suhosin_func_blacklist; if (function_exists($function_name)) { if ( ! isset($_suhosin_func_blacklist)) { if (extension_loaded('suhosin')) { $_suhosin_func_blacklist = explode(',', trim(ini_get('suhosin.executor.func.blacklist'))); if ( ! in_array('eval', $_suhosin_func_blacklist, TRUE) && ini_get('suhosin.executor.disable_eval')) { $_suhosin_func_blacklist[] = 'eval'; } } else { $_suhosin_func_blacklist = array(); } } return ! in_array($function_name, $_suhosin_func_blacklist, TRUE); } return FALSE; }
目前阅读的版本为稳定版3.0 这个文件作为CI的入口文件，主要做了一些初始化之类的操作，保证后续的正常执行。 运行环境 define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development'); 在文件的开始就需要定义一个运行环境，CI默认提供了development、testing、production三种环境，用户可以定义自己的运行环境，后面在很多的文件中都用到了这个变量。很明显后面就根据这个环境设置错误报告的级别，如果用户定义了自己的运行环境，也需要在这里进行相应的修改。 目录设置 接下来就是设置了几个目录，当然允许用户也可以自己更改system、application的名称。 $system_path = 'system'; $application_folder = 'application'; $view_folder = ''; 需要注意最后没有slash。 默认控制器 这里给用户一个定义默认控制器的功能，正常情况下是在路由中设置默认控制器的，但是可以在这里强制硬编码一个控制器。这样做的结果就是你的web程序只能有一个特定的控制器，所以没有特殊需要的话留空即可。 到这里用户可以自定义的配置文件部分就完成了，接下来通常情况下是不需要用户改动的。 配置CLI if (defined('STDIN')) { chdir(dirname(__FILE__)); } if (($_temp = realpath($system_path)) !== FALSE) { $system_path = $_temp.'/'; } else { // Ensure there's a trailing slash $system_path = rtrim($system_path, '/').'/'; } // Is the system path correct? if ( ! is_dir($system_path)) { header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); echo 'Your system folder path does not appear to be set correctly. Please open the following file and correct this: '.pathinfo(__FILE__, PATHINFO_BASENAME); exit(3); // EXIT_CONFIG } 在这里设置一些东西，可以让CLI模式正常运行，比较简单。 定义路径常量 接下来就是定义一些常量了，像什么SELF、BASEPATH等等。在后面的源码阅读中都是比较常用的，建议简单记一部分。 最后引入了 BASEPATH中的core/CodeIgniter.php文件，这个文件可以算是整个CI框架执行流程的核心部分，被称作bootstrap文件。 最后看一下整个CI框架的执行流程
注：这个主防只支持x86系统。。本人技术略渣，x64还没搞出来。。。。 注注：这篇文章是很久以前发在旧博客里的，无意间翻到了觉得还不错，复制过来了，毕竟也是用心做的东西。 这个SSDT的内容我用了之前的一个框架，网上也有很多的SSDT库。关于SSDT还不太清楚的童鞋可以看看我以前的一篇文章。 http://lightlesshome.diandian.com/post/2014-01-29/40060870666 主要就是一个主防的思路问题。我的思路比较简单，很多方面都没有考虑到，用ZwSetKeyValue这个函数来开刀。 首先肯定是要HOOK掉这个函数。之后就要考虑ring0和ring3交互的问题。这里采用的是DeviceIoControl和IOCTL的方法来进行交互的。 首先ring0进行监听，然后得到监听到事件后向ring3发出一个信号（暂且用信号这个词，其实是通知ring3），然后ring3收到通知后，向ring0请求信息，ring0会把一系列的东西传递给ring3，ring3进行处理后弹框并询问用户是否允许修改。 我参考了下这篇文章的方法，在数据量不大的时候并且不要求效率的时候可以考虑采用下面这个模型。 http://www.cnblogs.com/dflower/archive/2009/04/17/1438066.html IOCTL控制码的话主要定义了以下几个。 // 设置事件 #define IOCTL_SETEVENT \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x831, METHOD_BUFFERED, FILE_ANY_ACCESS) // 向ring0 请求数据 #define IOCTL_GETINFO \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x832, METHOD_BUFFERED, FILE_ANY_ACCESS) // 设置等待用户判断的事件 #define IOCTL_SETWAITEVENT \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x833, METHOD_BUFFERED, FILE_ANY_ACCESS) // 传递用户的判断结果 #define IOCTL_PASSUSERRES \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x834, METHOD_BUFFERED, FILE_ANY_ACCESS) 用途稍后会进行解释。 关于这个DeviceIoControl函数的用法可以参考下MSDN，比较简单，主要就是向ring0发送个控制码，然后获取数据之类的啥啥啥的。 在ring0中用这个函数。NTSTATUS DeviceIoControlDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP pIrp); 然后设置以下派遣函数 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceIoControlDispatch; DriverObject->MajorFunction[IRP_MJ_CREATE] = DeviceIoControlDispatch; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DeviceIoControlDispatch; 下面要在这个函数中获取IOCTL功能码 PIO_STACK_LOCATION irpStack; irpStack = IoGetCurrentIrpStackLocation(pIrp); irpStack->Parameters.DeviceIoControl.IoControlCode 那个IoControlCode就是功能码了。 下面就是继续switch case 来进行处理每一个功能码了。 基本上DeviceIoControlDispatch就是这样了。下面来考虑怎么在ring0和ring3中互相通知。 可以在ring3中创建一个事件，然后单独开一个线程来监视这个事件。开线程之前使用DeviceIoControl函数将这个事件的句柄传递到内核中，ring0收到这个事件后，转为句柄并保存起来，在以后需要使用的时候拿出来使用。 在ring3中： 先打开我们的驱动，其中的"\\.\RegDef"是驱动中的符号链接。 m_hDevice = CreateFile("\\\\.\\RegDef", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL); 然后创建事件 m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 下面将事件句柄传递到ring0中。 DeviceIoControl(m_hDevice, IOCTL_SETEVENT, &m_hEvent, sizeof(HANDLE), NULL, 0, &m_returnBytes, NULL); 然后开启一个新线程用来监视这个Event。我这里ring3使用的MFC，比较方便一点。 AfxBeginThread(EventThread, this); UINT CRegDefDlg::EventThread(LPVOID pParam) { CRegDefDlg *pThis = (CRegDefDlg *)pParam; DWORD dwReturned = 0; int i = 0; char szBuf[512]; char szName[128]; char szKey[128]; char szSubKey[128]; CString strTemp; while (true) { WaitForSingleObject(pThis->m_hEvent, INFINITE); DeviceIoControl(pThis->m_hDevice, IOCTL_GETINFO, NULL, 0, &pThis->m_OutBuffer, 1024, &dwReturned, NULL); //AfxMessageBox(pThis->m_OutBuffer); strTemp.Empty(); strTemp.Format("%s", pThis->m_OutBuffer); AfxMessageBox(strTemp); ZeroMemory(szBuf, 512);ZeroMemory(szName, 128);ZeroMemory(szKey, 128);ZeroMemory(szSubKey, 128); // sscanf(pThis->m_OutBuffer, "%[]", szName, szKey, szSubKey); int i = 0; int j = 0; while (pThis->m_OutBuffer[i] != '|') { szName[j] = pThis->m_OutBuffer[i]; i++;j++; } i += 1;j = 0; while (pThis->m_OutBuffer[i] != '|') { szKey[j] = pThis->m_OutBuffer[i]; i++;j++; } i += 1;j = 0; while (pThis->m_OutBuffer[i] != '|') { szSubKey[j] = pThis->m_OutBuffer[i]; i++;j++; } sprintf(szBuf, "Process Name: %s\r\nRegistry key: %s\r\nSubkey: %s\r\nWould you want this process to change the registyr?", szName, szKey, szSubKey); int t = pThis->MessageBox(szBuf,"Warning!", MB_YESNO); if (t == IDYES) { // 1表示允许修改; pThis->UserRes = 1; DeviceIoControl(pThis->m_hDevice, IOCTL_PASSUSERRES, &pThis->UserRes, 1, NULL, 0, &dwReturned, NULL); } else { // 0表示不允许修改 pThis->UserRes = 0; DeviceIoControl(pThis->m_hDevice, IOCTL_PASSUSERRES, &pThis->UserRes, 1, NULL, 0, &dwReturned, NULL); } SetEvent(pThis->m_hWaitUserRequestEvent); ResetEvent(pThis->m_hEvent); } return 0; } 写完后考虑了下用一个线程来一直监听可能会有点蛋疼。可能会出现各种bug。 里面主要的东西就是那个while(true)循环，在这个循环里，首先要进行WatiForSingleObject来等待事件的发生。一旦等待完成，就要马上发送IOCTL_GETINFO来获取信息，（这里定义的IOCTL_GETINFO控制码是用来向ring0获取信息的），然后中间一大堆字符串处理的东西请忽视。pThis->UserRes用来表示用户是否允许修改，如果允许的话就置为1，不允许就置为0，然后将这个参数传递到ring0中，这里用到的控制码是IOCTL_PASSUSERRES。之后要ResetEvent()，不然的话会挂掉。 下面来看ring0中的处理，首先要获取ring3传下来的事件句柄，并保存下来。下面这段代码在DeviceIoControlDispatch中。 case IOCTL_SETEVENT: KdPrint(("IOCTL_SETEVENT called.")); pInBuffer = pIrp->AssociatedIrp.SystemBuffer; pInbufferSize = irpStack->Parameters.DeviceIoControl.InputBufferLength; if (pInBuffer == NULL || pInbufferSize < sizeof(HANDLE)) { KdPrint(("[DeviceIoControlDispatch] Set event error!\n")); status = STATUS_INVALID_BUFFER_SIZE; break; } hEvent = *(HANDLE*)pInBuffer; KdPrint(("hEvent: %x\n", hEvent)); status = ObReferenceObjectByHandle(hEvent, GENERIC_ALL, NULL, KernelMode, (PVOID*)&gpEventObject, objHandleInfo); KdPrint(("gpEventObject: %x\n",gpEventObject)); break; 之前定义IOCTL的时候用的是BUFFER_IO方式，这里就用buffer来获取数据。关键就是这个ObReferenceObjectByHandle，之前一直是倒数第二个参数没加&符号，蓝屏了无数次。。。 hEvent是一个HANDLE类型。然后gpEventObject是PRKEVENT类型的全局变量，用于保存这个事件句柄，objHandleInfo是全局POBJECT_HANDLE_INFORMATION类型，不过好像在某些地方看到这个参数用NULL也可以。没有试验过，童鞋们可以自己尝试。这样这个句柄就保存下来了，在需要通知应用层事件的地方加上 KeSetEvent(gpEventObject, 0, FALSE); 这时ring3就会通过DeviceIoControl向驱动层请求数据。这个方法好像在竹林蹊径上也看到。 通信问题解决了之后就是考虑我们自己的HookZwSetValueKey的问题。 首先就是检测修改的注册表路径，如果是不允许修改的路径，比如启动项，那么就阻塞住ring0，并向ring3发送信息申请是否可以修改。因为同时有N多个进程会使用注册表，所以要先用一个互斥体来保证资源。随后调用KeSetEvent和KeClearEvent来通知上层应用，传递好进程的信息之后，就要KeWaitForSingleObject 来等待另一个事件，目的是等待上层应用的确认，基本上就是这样了。 完整代码请移步：https://github.com/LightIce/RegistryDefence/
好久没有写东西，这篇文章也是第一次投给乌云，赚了点外快，乌云那边已经发出来了，贴到自己博客里吧。 0x00 前言 最近上(ri)网(zhan)上(ri)多了，各种狗啊盾啊看的好心烦，好多蜜汁shell都被杀了，搞的我自己也想开发这么一个斩马刀，顺便当作毕设来做了。 未知攻，焉知防。我们先来看看shell们都是怎么躲过查杀的：加密、变形、回调、隐藏关键字……总之就是一句话，让自己变得没有特征，这样就可以躲过狗和盾的查杀。但是万变不离其宗，无论怎么变形，最终都会回到类似这样的格式： $_GET($_POST) 分为执行数据部分($_GET)和传递数据的部分($_POST)，也就是说，无论怎么变形，在执行的过程中都会变成这个样子，接下来还是去执行类似system(), exec(), eval()等等函数，那么我们就直接定位到这里，检测该脚本是否调用危险函数，或是在脚本调用这些函数的时候进行分析，判断该脚本是否为非法用户的shell，会取到很不错的防御效果。 0x01 如何获取先机 既然分析出来了问题的根结，那么下一步就是要控制住这些危险函数的入口点，即system()等函数的底层入口，就像Windows API中hook掉zw系列函数一样，我们也要hook掉system()这些函数。 这里我们需要用到PHP Extension， 即PHP扩展，位于PHP内核zend和PHP应用代码之间，很明显，扩展可以监控应用层代码的执行细节，也可以调用内核提供的ZEND API接口，包括禁用类、禁用函数等等。图为PHP结合其他必要组件的基本结构。 在ZEND中提供了接口供我们进行这样的操作，通过zend_set_user_opcode_handler就可以达到目的，根据某大牛所说，只需要hook三个OPCODE即可。 ZEND_INCLUDE_OR_EVAL 处理eval()、require等 ZEND_DO_FCALL 函数执行system()等 ZEND_DO_FCALL_BY_NAME 变量函数执行 $func = "system";$func();等 以ZEND_DO_FCALL为例，在MINIT中将ZEND_DO_FCALL替换掉： zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL); 接着定义自己的处理函数，这里是LIGHT_DO_FCALL。 static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS) { /* 检测是否为合法调用 */ if (/*非法调用*/) { /*拦截掉，不执行。*/ } else { /*合法调用，继续执行*/ return ZEND_USER_OPCODE_DISPATHC; } } 这样一个骨架就完成了，其余的内容可以自行添加。这样每当调用system()等函数时，都会经过你自己的LIGHT_DO_FCALL，于是就可以对上层的代码执行进行检查了。 0x02 上面的方法太粗暴 上面的方法固然是好，但是误判会很多，甚至一个普通的函数调用都会直接被ban掉。这样的WAF放在业务中一定会被喷出翔的。我们需要换个思路。 之前已经说过了，无论如何变形、加密、隐藏关键字等，最终都需要调用到system()，eval()等函数，终究是逃不过这些的。那么我们把刚才的思路的变一下，不要直接hook那几个OPCODE，向上走一层。 eval()函数在底层是要调用zend_compile_string函数的，那么我们是不是可以在底层重载掉zend_compile_string函数，或是用自己的函数替换掉呢？答案肯定是可以的，我们只需要重写自己的zend_compile_string函数即可。 但是对于system来说有些不一样，上面我们hook掉了ZEND_DO_FCALL，但是太粗暴了，我们这里可以在function_table中删除掉system，并用我们自己的函数注册system函数，这样就可以做到了实时检查，如果为合法调用，可以调用旧的system函数并继续执行。 很明显这样做的话，确实减少了误杀，但是也增加了风险，因为除了system()之外还有很多可以用于执行命令的函数。除了执行命令，还有遍历目录、读敏感文件的问题，需要考虑很多的方面，偏僻的函数都要考虑在内，还有PHP SPL、DirectoryIterator等等各种方面。 0x03 说了这么多我们看看实际效果吧 我们来看看hook三个OPCODE的方式会产生什么样子的效果。首先我们在MINIT函数中将这三个OPCODE的处理函数换成我们自己的。 ZEND_MINIT_FUNCTION(lightWAF) { /* * hook掉ZEND_DO_FCALL * 处理system函数等 */ zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL); /* * hook掉ZEND_DO_FCALL_BY_NAME * 处理$func='system';$func();等类型 */ zend_set_user_opcode_handler(ZEND_DO_FCALL_BY_NAME, LIGHT_DO_FCALL_BY_NAME); /* * hook掉ZEND_INCLUDE_OR_EVAL * 处理eval, require等 */ zend_set_user_opcode_handler(ZEND_INCLUDE_OR_EVAL, LIGHT_INCLUDE_OR_EVAL); return SUCCESS; } LIGHT_DO_FCALL， LIGHT_DO_FCALL_BY_NAME，LIGHT_INCLUDE_OR_EVAL的内容类似，均为判断脚本文件是否在upload目录中，如果在的话就禁止危险函数的执行，否则放行。这里我们用LIGHT_DO_FCALL说明一下： /* LIGHT_DO_FCALL */ static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS) { char *filePath; filePath = zend_get_executed_filename(TSRMLS_C); php_printf("[Debug] filePath: %s\n ", filePath); if (strstr(filePath, "/upload/")) { /* 非法调用，拦截 */ php_printf("[Warning] Execute command via system() etc.\n "); return ZEND_USER_OPCODE_RETURN; } else /* Not Found */ { /* 合法调用，放行 */ return ZEND_USER_OPCODE_DISPATCH; } } 这里我们看到，一旦检测到在upload目录中，就返回ZEND_USER_OPCODE_RETURN，实际作用就是不继续执行危险函数了，如果不在upload目录中，就返回ZEND_USER_OPCODE_DISPATCH，实际作用就是继续执行该函数。 扩展编写完成后，编译即可得到so文件，如果对PHP扩展开发以及编译不熟悉的同学，请参考这个扩展开发教程的第五章：http://www.walu.cc/phpbook/ ，开发方面的知识超出了本文的内容，所以这里不再赘述了。 编译完成后，将得到的lightWAF.so文件复制到php的扩展目录下，我这里是/usr/local/php/lib/php/extensions/，根据自己的实际情况进行调整。接下来修改php.ini，让PHP自动加载我们的扩展，在其中加上一行： extension=/path/to/our/lightWAF.so 我这里是： extension=/usr/local/php/lib/php/extensions/lightWAF.so 根据之前so文件放的位置不同，请自行修改路径。 最后我们重启PHP服务，让设置生效。PHP重启完成后，在终端中执行php -m，来验证扩展是否加载成功，如果在结果中看到了lightWAF，则说明加载成功。（请无视图中的错误信息，那个是我本地环境的问题，对lightWAF以及PHP没什么影响） 成功加载后，我们看一下实际效果，这里有一个上传文件的页面，可以上传任何类型的文件并将上传的文件保存在upload目录中。现在模拟一下getshell的过程。 我们写个小马，内容如下： <?php system($_GET["cmd"]); ?> 进行上传，并访问shell，结果如下： 可以看到被成功拦截了，好吧，我们换只牛逼的马儿，试试反射型的。 <?php $func = new ReflectionFunction("system"); echo $func->invokeArgs(array("$_GET[c]")); ?> 继续上传并访问，看看结果： 依然被拦截了，只不过这次触发的是ZEND_DO_FCALL_BY_NAME这个OPCODE。 接下来我们看看正常的文件会不会被拦截，在lightWAF目录下写入一个test.php，用于模拟正常的业务文件，内容如下： <?php system('ls'); ?> 访问一下看看结果： 可以看到ls命令成功的执行了，也就是说我们的正常文件是不会被拦截的，而只有upload目录中的文件会被拦截，这样做又会引发另一个弊端，倘若攻击者通过某种方法将shell写入正常的文件中，或是与业务结合起来，那么这种防御手段就很难生效了。具体如何防御还要结合其他的特征进行检测，并不是没有办法了，实际应用中不能只依靠检测文件路径这一条规则，需要结合业务进行部署防御方案。 另一种方法与这个hook三个OPCODE的方法类似，无非就是麻烦一点，感兴趣的同学可以围观下面的参考文献：http://security.tencent.com/index.php/blog/msg/19 ，这里描述了hook函数的比较详细的思路。 0x04 不谈业务的安全都是耍流氓 总结一下，拦截的方法大概就是上面两种，但是拦截的依据还没有决定，如何判断一个调用system()的脚本是否为webshell呢？如果我们的WAF放到生产环境，啥都不管乱杀，很有可能造成正常的业务无法工作。我自己总结了几个方法，可能联合起来使用效果更好一些。 根据目录判断 通常情况下，上传的文件一般都有专门的目录进行存放，例如upload/等，正常的业务文件是不在这其中的，于是我们可以简单的只对这个目录进行处理，其他目录的文件一律放行。 根据文件权限判断 这需要网站的维护者对网站的权限进行严格的控制，例如，所有的web文件均是644，且为root:root所有。上传的文件为www:www所有，根据权限的不同进行查杀，对于默认的web文件进行放行，对属于非root:root的文件进行查杀。也可以根据w标志判断，通过未知方式getshell的文件很多是带有w标志的，所以可以根据这些特征进行查杀。 变形检测 如果发现一个文件调用了assert、system、preg_replace /e等等，但是在源文件中没有发现这些关键字，还等什么，这个文件很大的可能就是shell了。（zend_get_executed_filename可以获得文件的标志以及文件的路径，例如出现了assert，说明该脚本使用了assert执行代码。） 黑名单检测 就像传统的杀软一样，总会有那么一部分的特征病毒库，我们也可以建立一部分的webshell特征库，先依靠特征库杀掉一部分，再根据其他的情况进行判断。如果将动态检测和静态检测结合起来，查杀、拦截效果应当都会有显著的改善。 0x05 所以这WAF有啥用 安全需要和业务结合起来进行，不谈业务的安全都是耍流氓。因为是扩展级别的WAF，在部署的时候可能需要重新编译，修改配置文件等等。批量式部署可能显得不是那么方便，而且要根据业务需要进行各种细微的调整。如果仅仅是几台服务器，相信这种WAF还是十分棒的，调整起来也十分方便。 如果能开发出适合批量部署的基于扩展的WAF，那么可能会比较容易普及，毕竟在业务上部署WAF不是一个简单的事情。 参考资料： http://security.tencent.com/index.php/blog/msg/57 http://security.tencent.com/index.php/blog/msg/19 http://www.walu.cc/phpbook/ http://www.php-internals.com/book/?p=index http://www.nowamagic.net/librarys/veda/detail/1543
这篇文章，我们来分析typecho的路由部分，在我看来路由一直是很NB的东西，现在我们一起来分析一下typecho的路由到底是怎么个东西。 相关文件 typecho中的路由相关的文件不多，只存放于/var/Typecho/Router.php和/var/Typecho/Router/文件夹下面。 /var/Typecho/Router.php 一个很普通的Typecho_Router类，看到TODO里写着以后要增加cache，这样速度应该会提高不少吧。 这个类没有构造函数，所以我们就从头开始看吧，有三个变量，分别是$current存储当前的路由名称，$_routeingTable存储解析完成的路由表配置，最后是$_pathInfo存储全路径。 先来看解析路径的函数，match()，它接受两个参数，分别是全路径以及输入参数。 public static function match($pathInfo, $parameter = NULL) { foreach (self::$_routingTable as $key => $route) { if (preg_match($route['regx'], $pathInfo, $matches)) { // 如果在路由表中匹配到了，就将current赋值为该路由的名称 // 猜测$_routingTable的第一维存储的是每个路由的名称，然后有一个'regx'字段存储的是路由规则 // 即$_routingTable['routeName'] // 其中每一项的内容：$routingTable['routeNameA']['regx'] => '/archive/123.html' self::$current = $key; try { /** 载入参数 */ $params = NULL; if (!empty($route['params'])) { // 如果$route['params']非空 unset($matches[0]); // 撤销变量 // 创建一个新的数组，$route['params']作为新数组的key，$matches作为新数组的value $params = array_combine($route['params'], $matches); } // 这里用到了Typecho_Widget的工厂方法widget，这个我们后面在分析Widget的时候再说 // widget是typecho很重要的组成部分，是基本元素，除了抽象出来的类库，其他的几乎所有功能 // 都会通过widget完成。 $widget = Typecho_Widget::widget($route['widget'], $parameter, $params); // 会返回一个关于路由的widget？ return $widget; } catch (Exception $e) { // 没有匹配到的话就返回404吧大概 if (404 == $e->getCode()) { Typecho_Widget::destory($route['widget']); continue; } throw $e; } } } return false; } 我自己的一些理解写到了注释里，可能有些地方不太对，等后面看完了widget的部分再回来修改。 接下来我们看一看dispatch()路由分发函数，这个和上面的match()函数基本一致，唯一不同的是增加了一段关于widget的代码： if (isset($route['action'])) { $widget->{$route['action']}(); } 应该是调用了这个widget的暂时还不知道啥意思，后面再看。 下面会看到一个路由反解析函数，猜测最后应该是将路由解析成URL吧。但是其中遇到了一个Typecho_Common::url，也只能等分析到Common类的时候再看吧。 最后还剩下两个函数，分别是设置路由默认配置，以及获取路由信息的函数，获取路由信息的函数十分简单，没啥好说的了。而在设置路由默认配置中又遇到了Typecho_Router_Parser这个类，下面我们就去分析这个文件。 /var/Typecho/Router/Parser.php 这个类叫做路由器解析器，先从构造函数开始看吧。 public function __construct(array $routingTable) { $this->_routingTable = $routingTable; $this->_defaultRegx = array( 'string' => '(.%s)', 'char' => '([^/]%s)', 'digital'=> '([0-9]%s)', 'alpha' => '([_0-9a-zA-Z-]%s)', 'alphaslash' => '([_0-9a-zA-Z-/]%s)', 'split' => '((?:[^/]+/)%s[^/]+)', ); } 这个函数接受一个参数，为路由器映射表。其实就是给两个本类的变量赋了值。 继续向下看，有一个_match()函数，这个函数的作用是局部匹配并替换正则字符串。这个函数根据获得的参数数量，返回不同的内容，意义还不是很明显，我们结合下面的parse()函数来看（parse()函数中会使用_match函数）。 parse()函数的作用是解析路由表，其中preg_replace_callback的作用是执行一个正则表达式搜索并且使用一个回调进行替换，这里就是使用的_match作为cb函数来使用的，接下来就是一大堆正则的匹配了。 public function parse() { $result = array(); foreach ($this->_routingTable as $key => $route) { $this->_params = array(); // 先将$route['url']字符串中的'[',']',':'替换为'%','%',' '. // 然后在这个字符串中搜索"/%([^%]+)%/"，然后将结果作为_match的参数并调用cb函数 // 这样也就能理解在_match函数中开始的操作了 $route['regx'] = preg_replace_callback("/%([^%]+)%/", array($this, '_match'), preg_quote(str_replace(array('[', ']', ':'), array('%', '%', ' '), $route['url']))); /** 处理斜线 */ // 删除末尾的斜杠 $route['regx'] = rtrim($route['regx'], '/'); // 然后前后加上一部分东西 $route['regx'] = '|^' . $route['regx'] . '[/]?$|'; $route['format'] = preg_replace("/\[([^\]]+)\]/", "%s", $route['url']); $route['params'] = $this->_params; $result[$key] = $route; } return $result; } 一些说明已经写到注释中了，再结合前面的一大堆正则，看的好蛋疼。再回过头来看_match()函数的后半部分，按照参数个数操作的地方，这里用了sprintf，然后刚刚在$_defaultRegx初始化的时候，其中有包含格式化控制符%s，所以这里的第二个参数可以直接替换掉其中%s的地方，例如return sprintf($this->_defaultRegx['char'], '+');，实际上返回的就是([^/]+)，其他的一个道理，只是复杂些。如果想获得详细的变量内容，还是var_dump出来看一下比较好。 到这里就分析完了路由的部分，接下来可能分析一些比较杂乱的部分，由于本系列文章没有按照执行流程来分析，而是一个部分一个部分的分析，所以可以不按照顺序来阅读，不会有影响，等所有的模块都分析完后，我会分析typecho的执行流程等机制。