测试小站: 处理网 回收帮 培训网 富贵论坛 老富贵论坛

「代码审计」那些代码审计的思路

  前言

  代码审计工具的实现都是基于代码审计经验开发出来用于优化工作效率的工具,我们要学好代码审计就必须要熟悉代码审计的思路。而且代码审计是基于PHP语言基础上学习的,学习代码审计最基本的要求就是能读懂代码。常见的代码审计思路有以下四种:

  根据敏感关键字回溯参数传递过程;

  查找可控变量,正向追踪变量传递过程;

  寻找敏感功能点,通读功能点代码;

  直接通读全文代码。

  敏感函数回溯参数过程

  根据敏感函数来逆向追踪参数的传递过程,是目前使用的最多的一种方式,因为大多数漏洞是由于函数的使用不当造成的。另外非函数使用不当的漏洞,如SQL注入,等以后学习再详细介绍。这种方式的优缺点如下:

  优点:只需搜索相应敏感关键字,即可快速挖掘想要的漏洞,可定向挖掘,高效、高质量;

  缺点:由于没有通读代码,对程序整体架构了解不够深入,在挖掘漏洞时定位利用会花点时间,另外对逻辑漏洞挖掘覆盖不到。

  espcms注入挖掘案例:

  打开seay源代码审计系统,点击左上角新建项目,选择下载的espcms文件夹,点击自动审计,开始审计,得到可能存在漏洞,漏洞文件的路径,和漏洞代码列表。

  我们挑选其中的一条代码

  双击直接定位到这行代码,选中该变量后,可以看到变量的传递过程,在左侧点击parentid函数,在下面详细信息的地方可以看到parentid函数,在下面详细信息的地方可以看到parentid变量获得。

  右键选中这行代码,定位函数主体accept,点击右键,选择定位函数

  可以看到跳转到了class_function.php文件,代码如下:

  可以看到这是一个获取GET、POST、COOKIE参数值得函数,我们传入的变量是parentid和R,则代表在POST、GET中都可以获取parentid参数,最后经过一个daddslashes()函数,实际上是包装的addslashes()函数,对单引号等字符进行过滤。看前面的SQL语句是这样的:

  $sql=“select * from db_table where parentid=dbtablewhereparentid=parentid”;

  并不需要单引号来闭合,可以直接注入。

  在citylist.php文件看到oncitylist()函数在important类中,选中该类名右键点击,选择全局搜索

  可以看到index.php文件有实例化该类,代码如下:

  $archive=indexget(‘archive’, ‘R’);

  $archive=empty($archive) ? ‘adminuser’ : $archive;

  $action=indexget(‘action’, ‘R’);

  $action=empty($action) ? ‘login’ : $action;

  $soft_MOD=array(‘admin’, ‘public’, ‘product’, ‘forum’, ‘filemanage’, ‘basebook’, ‘member’, ‘order’, ‘other’, ‘news’, ‘inc’, ‘cache’, ‘bann’, ‘logs’, ‘template’);

  if (in_array($point, $soft_MOD)) {

  include admin_ROOT . adminfile . “/control/$archive.php”;

  $control=new important();

  $action=‘on’ . $action;

  if (method_exists($control, $action)) {

  $control->$action();

  } else {

  exit(‘错误:系统方法错误!’);

  }

  这里可以看到一个include文件的操作,可惜经过了addslashes()函数无法进行阶段使其包含任意文件,只能包含本地的PHP文件,往下是实例化类并且调用函数的操作,根据代码可以构造出利用EXP:

  127.0.0.1/espcms/upload/adminsoft/index.php?archive=citylist&action=citylist&parentid=-1 union select 1,2,user(),4,5

  通读全文代码

  通读全文代码也有一定的技巧,否则很难读懂Web程序的,也很难理解代码的业务逻辑。首先我们要看程序的大体结构,如主目录有哪些文件,模块目录有哪些文件,插件目录有哪些文件,另外还要注意文件的大小,创建时间,就可以大概知道这个程序实现了那些功能,核心文件有哪些。

  如discuz的主目录如下图所示:

  在看目录结构的时候,特别注意以下几个文件:

  函数集文件

  函数集文件通常命名中包含functions或者common等关键字,这些文件里面是一些公共的函数,提供给其他文件统一调用,所以大多数文件都会在文件头部包含到其他文件。寻找这些文件的一个技巧就是打开index.php或者一些功能性文件。配置文件

  配置文件通常命名中包含config关键字,配置文件包括Web程序运行必须的功能性配置选项以及数据库等配置信息。从这个文件可以了解程序的小部分功能,另外看这个文件的时候注意观察配置文件中参数是用单引号还是双引号,如果是双引号,则很可能会存在代码执行漏洞。安全过滤文件

  安全过滤文件对我们做代码审计至关重要,通常命名中有filter、safe、check等关键字,这类文件主要是对参数进行过滤,比较常见的是针对SQL注入和XSS过滤,还有文件路径、执行的系统命令的参数。index文件

  index是一个程序的入口文件,所以我们只要读一遍index文件就可以大致了解整个程序的架构、运行的流程、包含到的文件。

  骑士cms通读审计案例

  (1)查看应用文件结构

  首先看看有哪些文件和文件夹,寻找名称里有没有带api、admin、manage、include一类关键字的文件和文件夹。可以看到有一个include文件夹,一般比较核心的文件都会放在这个文件夹中。

  (2)查看关键文件代码

  在这个文件夹里可以看多多个数十K的文件,弱common.php就是本程序的核心文件,基础函数基本就在这个文件中实现。一打开文件,立马看多一大堆过滤函数,首先是一个SQL注入过滤函数:

  function addslashes_deep($value)

  {

  if (empty($value))

  {

  return $value;

  }

  else

  {

  if (!get_magic_quotes_gpc())

  {

  $value=is_array($value) ? array_map(‘addslashes_deep’, $value) : mystrip_tags(addslashes($value));

  }

  else

  {

  $value=is_array($value) ? array_map(‘addslashes_deep’, $value) : mystrip_tags($value);

  }

  return $value;

  }

  }

  该函数将传入的变量使用addslashes()函数进行过滤,过滤掉了单引号、双引号、NULL字符以及斜杠,要记住,在挖掘SQL注入漏洞时,只要参数在拼接到SQL语句前,除非有宽字节注入或者其他特殊情况,否则使用了这个函数就不能注入了。

  再往下是一个XSS过滤的函数mystrip_tags(),代码如下:

  function mystrip_tags($string)

  {

  $string=new_html_special_chars($string);

  $string=remove_xss($string);

  return $string;

  }

  这个函数调用了new_html_special_chars()和remove_xss()函数来过滤XSS,代码如下:

  function new_html_special_chars($string) {

  $string=str_replace(array(‘&’, ‘"’, ‘<’, ‘>’), array(‘&’, ‘“‘, ‘<’, ‘>’), $string);

  $string=strip_tags($string);

  return $string;

  }

  function remove_xss($string) {

  $string=preg_replace(‘/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S’, ‘’, $string);

  $parm1=Array('javascript', 'union','vbscript', 'expression', 'applet', 'xml', 'blink', 'link', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base');

  $parm2=Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload','style','href','action','location','background','src','poster');

  $parm3=Array('alert','sleep','load_file','confirm','prompt','benchmark','select','update','insert','delete','alter','drop','truncate','script','eval','outfile','dumpfile');

  $parm=array_merge($parm1, $parm2, $parm3);

  for ($i=0; $i < sizeof($parm); $i++) {

  $pattern='/';

  for ($j=0; $j < strlen($parm[$i]); $j++) {

  if ($j > 0) {

  $pattern .='(';

  $pattern .='(???)?';

  $pattern .='|(?([9][10][13]);?)?';

  $pattern .=')?';

  }

  $pattern .=$parm[$i][$j];

  }

  $pattern .='/i';

  $string=preg_replace($pattern, '****', $string);

  }

  return $string;

  }

  在new_html_special_chars()函数中可以看到,这个函数对&符号、双引号以及尖括号进行了html实体编码,并且使用strip_tags()函数进行了二次过滤。而remove_xss()函数则是对一些标签关键字、事件关键字以及敏感函数关键字进行了替换。

  再往下有一个获取IP地址的函数getip(),是可以伪造IP地址的:

  function getip()

  {

  if (getenv(‘HTTP_CLIENT_IP’) and strcasecmp(getenv(‘HTTP_CLIENT_IP’),’unknown’)) {

  $onlineip=getenv(‘HTTP_CLIENT_IP’);

  }elseif (getenv(‘HTTP_X_FORWARDED_FOR’) and strcasecmp(getenv(‘HTTP_X_FORWARDED_FOR’),’unknown’)) {

  $onlineip=getenv(‘HTTP_X_FORWARDED_FOR’);

  }elseif (getenv(‘REMOTE_ADDR’) and strcasecmp(getenv(‘REMOTE_ADDR’),’unknown’)) {

  $onlineip=getenv(‘REMOTE_ADDR’);

  }elseif (isset($_SERVER[‘REMOTE_ADDR’]) and $_SERVER[‘REMOTE_ADDR’] and strcasecmp($_SERVER[‘REMOTE_ADDR’],’unknown’)) {

  $onlineip=$_SERVER[‘REMOTE_ADDR’];

  }

  preg_match(“/\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}/“,$onlineip,$match);

  return $onlineip=$match[0] ? $match[0] : ‘unknown’;

  }

  很多应用都会由于在获取IP时没有验证IP格式,而存在注入漏洞,不过这里只是可以伪造IP。

  再往下可以看到一个值得关注的地方,SQL查询统一操作函数inserttable()以及updatetable()函数,大多数SQL语句执行都会经过这里,所以我们要关注这个地方是否还有过滤等问题。

  function inserttable($tablename, $insertsqlarr, $returnid=0, $replace=false, $silent=0) {

  global $db;

  $insertkeysql=$insertvaluesql=$comma=‘’;

  foreach ($insertsqlarr as $insert_key=> $insert_value) {

  $insertkeysql .=$comma.’'.$insert_key.'‘;

  $insertvaluesql .=$comma.’\’’.$insert_value.’\’’;

  $comma=‘, ‘;

  }

  $method=$replace?’REPLACE’:’INSERT’;

  // echo $method.” INTO $tablename ($insertkeysql) VALUES ($insertvaluesql)”, $silent?’SILENT’:’’;die;

  $state=$db->query($method.” INTO $tablename ($insertkeysql) VALUES ($insertvaluesql)”, $silent?’SILENT’:’’);

  if($returnid && !$replace) {

  return $db->insert_id();

  }else {

  return $state;

  }

  }

  再往下则是wheresql()函数,是SQL语句查询的Where条件拼接的地方,我们可以看到参数都使用了单引号进行包裹,代码如下:

  function wheresql($wherearr=’’)

  {

  $wheresql=””;

  if (is_array($wherearr))

  {

  $where_set=’ WHERE ‘;

  foreach ($wherearr as $key=> $value)

  {

  $wheresql .=$where_set. $comma.$key.’=”‘.$value.’”‘;

  $comma=‘ AND ‘;

  $where_set=’ ‘;

  }

  }

  return $wheresql;

  }

  还有一个访问令牌生成函数asyn_userkey(),拼接用户名、密码salt以及密码进行一次md5,访问的时候只要在GET参数key的值里面加上生成的这个key即可验证是否有权限,被用在注册、找回密码等验证过程中,代码如下:

  function asyn_userkey($uid)

  {

  global $db;

  $sql=“select * from “.table(‘members’).” where uid=‘“.intval($uid).”‘ LIMIT 1”;

  $user=$db->getone($sql);

  return md5($user[‘username’].$user[‘pwd_hash’].$user[‘password’]);

  }

  (3)查看配置文件

  上面我们介绍到配置文件通常带有“config”这样的关键字,我们只要搜索带有这个关键字的文件名即可:

  在搜索结果中我们可以看到搜索到多个文件,结合经验可以判断config.php以及cache_config.php才是真正的配置文件,打开config.php查看代码:

  

  $dbhost=“localhost”;

  $dbname=”74cms”;

  $dbuser=”root”;

  $dbpass=”123456”;

  $pre=”qs_”;

  $QS_cookiedomain=‘’;

  $QS_cookiepath=“/74cms/“;

  $QS_pwdhash=“K0ciF:RkE4xNhu@S”;

  define(‘QISHI_CHARSET’,’gb2312’);

  define(‘QISHI_DBCHARSET’,’GBK’);

  ?>

  很明显看到,很有可能存在我们之前说过的双引号解析代码执行的问题,通常这个配置是在安装系统的时候设置的,或者后台也有设置的地方。

  看看数据库连接时设置的编码,找到骑士cms连接MySQL的代码在include\mysql.class.php文件的connect()函数,代码如下:

  function connect($dbhost, $dbuser, $dbpw, $dbname=‘’, $dbcharset=‘gbk’, $connect=1){

  $func=empty($connect) ? ‘mysql_pconnect’ : ‘mysql_connect’;

  if(!$this->linkid=@$func($dbhost, $dbuser, $dbpw, true)){

  $this->dbshow(‘Can not connect to Mysql!’);

  } else {

  if($this->dbversion() > ‘4.1’){

  mysql_query( “SET NAMES gbk”);

  if($this->dbversion() > ‘5.0.1’){

  mysql_query(“SET sql_mode=‘’”,$this->linkid);

  mysql_query(“SET character_set_connection=”.$dbcharset.”, character_set_results=”.$dbcharset.”, character_set_client=binary”, $this->linkid);

  }

  }

  }

  if($dbname){

  if(mysql_select_db($dbname, $this->linkid)===false){

  $this->dbshow(“Can’t select MySQL database($dbname)!”);

  }

  }

  }

  这段代码有个关键的地方,有安全隐患。

  代码首先判断MySQL版本是否大于4.1,如果是则执行下面代码:

  mysql_query( “SET NAMES gbk”);

  执行这个语句之后在判断,如果版本大于5则执行下面代码:

  mysql_query(“SET character_set_connection=”.$dbcharset.”, character_set_results=”.$dbcharset.”, character_set_client=binary”, $this->linkid);

  也就是说在MySQL版本小于5的情况下是不会执行这行代码的,

  但是执行了”set names gbk”,我们在之前介绍过”set names gbk”其实干了三件事,等同于:

  SET character_set_connection=’gbk’, character_set_results=’gbk’, character_set_client=’gbk’

  因此在MySQL版本大于4.1小于5的情况下,基本所有跟数据库有关的操作都存在宽字节注入。

  (4)跟读首页文件

  通过对系统文件大概的了解,我们队这套程序的整体架构已经有了一定的了解,但是还不够,需要跟读一下index.php文件,看看程序运行的时候回调用哪些文件和函数。

  打开首页文件index.php可以看到如下代码:

  if(!file_exists(dirname(FILE).’/data/install.lock’))

  header(“Location:install/index.php”);

  define(‘IN_QISHI’, true);

  $alias=”QS_index”;

  require_once(dirname(FILE).’/include/common.inc.php’);

  首先判断安装锁文件是否存在,如果不存在则跳转到install\index.php

  接下来是包含\include\common.inc.php文件,跟进文件查看

  require_once(QISHI_ROOT_PATH.’data/config.php’);

  header(“Content-Type:text/html;charset=”.QISHI_CHARSET);

  require_once(QISHI_ROOT_PATH.’include/common.php’);

  require_once(QISHI_ROOT_PATH.’include/74cms_version.php’);

  \include\common.inc.php文件在开头包含了三个文件,data\config.php为数据库配置文件,include\common.php文件为基础函数库文件,include\74cms_version.php为应用版本文件。

  再看下面的代码:

  f (!empty($_GET))

  {

  $_GET=addslashes_deep($_GET);

  }

  if (!empty($_POST))

  {

  $_POST=addslashes_deep($_POST);

  }

  $_COOKIE=addslashes_deep($_COOKIE);

  $_REQUEST=addslashes_deep($_REQUEST);

  这段代码调用了include\common.php文件里面的addslashes_deep()函数对GET、POST、COOKIE参数进行了过滤。

  再往下可以看到有一个包含文件的操作:

  require_once(QISHI_ROOT_PATH.’include/tpl.inc.php’);

  包含了include pl.inc.php文件,跟进这个文件看看:

  include_once(QISHI_ROOT_PATH.’include/template_lite/class.template.php’);

  $smarty=new Template_Lite;

  $smarty -> cache_dir=QISHI_ROOT_PATH.’temp/caches/‘.$_CFG[‘template_dir’];

  $smarty -> compile_dir=QISHI_ROOT_PATH.’temp/templates_c/‘.$_CFG[‘template_dir’];

  $smarty -> template_dir=QISHI_ROOT_PATH.’templates/‘.$_CFG[‘template_dir’];

  $smarty -> reserved_template_varname=“smarty”;

  $smarty -> left_delimiter=“”;

  $smarty -> force_compile=false;

  $smarty -> assign(‘_PLUG’, $_PLUG);

  $smarty -> assign(‘QISHI’, $_CFG);

  $smarty -> assign(‘page_select’,$page_select);

  首先看到包含了include emplate_lite\class.template.php文件,这是一个映射程序模板的类,继续往下看,可以看到这段代码实例化了这个类对象赋值给¥smarty变量。

  继续跟进则回到index.php文件代码:

  if(!$smarty->is_cached($mypage[‘tpl’],$cached_id))

  {

  require_once(QISHI_ROOT_PATH.’include/mysql.class.php’);

  $db=new mysql($dbhost,$dbuser,$dbpass,$dbname);

  unset($dbhost,$dbuser,$dbpass,$dbname);

  $smarty->display($mypage[‘tpl’],$cached_id);

  }

  else

  {

  $smarty->display($mypage[‘tpl’],$cached_id);

  }

  判断是否已经缓存,然后调用display()函数输出页面。接下来像审计index.php文件一样跟进其他功能入口文件即可完成代码通读。

  根据功能点定向审计

  根据经验我们简单介绍几个功能点会出现的漏洞:

  文件上传功能

  这里说的文件上传在很多功能点都会出现,比如像文章编辑、资料编辑、头像上传、附件上传,这个功能最常见的漏洞就是任意文件上传了,后端程序没有严格地限制上传的格式,导致可以上传或者存在绕过的情况,而除了文件上传功能外,还经常发生SQL注入漏洞。文件管理功能

  在文件管理功能中,如果程序将文件名或者文件路径直接在参数中传递,则很有可能会存在任意文件的操作漏洞,比如任意文件读取等,利用的方法是在路径中使用…/或者…\跳转目录。

  除了任意文件操作漏洞外,还可能会存在XSS漏洞,程序会在页面中输出文件名,而通常会疏忽对文件名进行过滤,导致可以在数据库中存入带有尖括号等特殊符号的文件名,最后在页面显示的时候就会被执行。登录认证功能

  登录认证功能不是指一个过程,而是整个操作过程中的认证,目前的认证方式大多是基于Cookie和Session,不少程序会把当前登陆的用户账号等认证信息放到Cookie中,或许是加密方式。进行操作的时候直接从Cookie中读取当前用户信息,这里就存在一个算法可信的问题,如果这段Cookie信息没有加salt一类的东西,就可以导致任意用户登录漏洞,只要知道用户的不扥信息,即可生成认证令牌,甚至有的程序会直接把用户名放到Cookie中,操作的时候直接读取这个用户名的数据,这也是常说的越权漏洞。找回密码功能

  找回密码虽然看起来不像任意文件上传这种可以危害到服务器安全的漏洞,但是如果可以重置管理员的密码,也是可以间接控制业务权限甚至拿到服务权限的。找回密码功能的漏洞有很多利用场景,最常见的是验证码爆破。目前特别是APP应用,请求后端验证码的时候大多是4位,并且没有限制验证码的错误次数和有效时间,于是就出现了爆破的漏洞。

posted @ 2021-12-17 19:47  linjingyg  阅读(449)  评论(0)    收藏  举报