漏洞挖掘与防范(一)
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:16004488
微信公众号:计算机与网络安全
ID:Computer-network
每类漏洞都有针对性的审计技巧,在我们掌握了这些技巧之后,就可以有针对性地挖掘我们想要的漏洞。漏洞大致分为SQL注入、XSS、文件操作、代码/命令执行、变量覆盖以及逻辑处理,等等,这些都是常见的Web漏洞。
本文介绍最常见的SQL注入、XSS、CSRF漏洞,分析其原理、利用方式,并介绍防范策略。
一、SQL注入漏洞
SQL注入漏洞可能是被人知道最多的漏洞,哪怕再没有接触到安全的程序员,多多少少会对这个词有所耳闻,它也是目前被利用得最多的漏洞。根据笔者维护公司waf时统计的数据,它的攻击次数占总攻击拦截的一半以上。SQL注入漏洞的原理非常简单,由于开发者在编写操作数据库代码时,直接将外部可控的参数拼接到SQL语句中,没有经过任何过滤就直接放入数据库引擎执行。
由于SQL注入是直接面对数据库进行攻击的,所以它的危害不言而喻,通常利用SQL注入的攻击方式有下面几种:
一是在权限较大的情况下,通过SQL注入可以直接写入webshell,或者直接执行系统命令等。
二是在权限较小的情况下,也可以通过注入来获得管理员的密码等信息,或者修改数据库内容进行一些钓鱼或者其他间接利用。
针对SQL注入漏洞的利用工具也是越来越智能,sqlmap是目前被使用最多的注入工具,这是一款国外开源的跨平台SQL注入工具,用Python开发,支持多种方式以及几乎所有类型的数据库注入,对SQL注入漏洞的兼容性也非常强。
既然SQL注入是被利用最多的漏洞,因此它也是被研究最深的漏洞,针对不同的漏洞代码情况和运行环境,有多种的利用方式,如普通注入、盲注、报错注入、宽字节注入、二次注入等,但是它们的原理都是大同小异的,下面会介绍怎么挖掘到这些注入漏洞。
(一)挖掘经验
SQL注入经常出现在登录页面、获取HTTP头(user-agent/client-ip等)、订单处理等地方,因为这几个地方是业务相对复杂的,登录页面的注入现在来说大多是发生在HTTP头里面的client-ip和x-forward-for,一般用来记录登录的IP地址,另外在订单系统里面,由于订单涉及购物车等多个交互,所以经常会发生二次注入。我们在通读代码挖掘漏洞的时候可以着重关注这几个地方。
1、普通注入
这里说的普通注入是指最容易利用的SQL注入漏洞,比如直接通过注入union查询就可以查询数据库,一般的SQL注入工具也能够非常好地利用。普通注入有int型和string型,在string型注入中需要使用单或双引号闭合,下面简单演示普通注入漏洞,后面所有测试SQL注入漏洞的数据表中数据都如下图所示。
测试代码如下:
<?php
$uid=$_GET['id'];
$sql="SELECT * FROM userinfo where id=$uid";
$conn=mysql_connect('localhost','root','123456');
mysql_select_db("test",$conn);
$result=mysql_query($sql,$conn);
print_r('当前SQL语句:'.$sql.'<br />结果:');
print_r(mysql_fetch_row($result));
测试代码中GET id参数存在SQL注入漏洞,测试方法如下图所示。
从截图可以看到原本的SQL语句已被注入更改,使用了union查询到当前用户。
从上面的测试代码中可以发现,数据库操作存在一些关键字,比如select from、mysql_connect、mysql_query、mysql_fetch_row等,数据库的查询方式还有update、insert、delete,我们在做白盒审计时,只需要查找这些关键字,即可定向挖掘SQL注入漏洞。
2、编码注入
程序在进行一些操作之前经常会进行一些编码处理,而做编码处理的函数也是存在问题的,通过输入转码函数不兼容的特殊字符,可以导致输出的字符变成有害数据,在SQL注入里,最常见的编码注入是MySQL宽字节以及urldecode/rawurldecode函数导致的。
(1)宽字节注入
在使用PHP连接MySQL的时候,当设置“set character_set_client=gbk”时会导致一个编码转换的注入问题,也就是我们所熟悉的宽字节注入,当存在宽字节注入漏洞时,注入参数里带入%df%27,即可把程序中过滤的\(%5c)吃掉。举个例子,假设/1.php?id=1里面的id参数存在宽字节注入漏洞,当提交/1.php?id=-1’and 1=1%23时,MySQL运行的SQL语句为select*from user where id=’1\’and 1=1#’很明显这是没有注入成功的,我们提交的单引号被转义导致没有闭合前面的单引号,但是我们提交/1.php?id=-1%df’and 1=1%23时,这时候MySQL运行的SQL语句为:
select * from user where id='1\' and 1=1#'
这是由于单引号被自动转义成\',前面的%df和转义字符\反斜杠(%5c)组合成了%df%5c,也就是“運”字,这时候单引号依然还在,于是成功闭合了前面的单引号。
出现这个漏洞的原因是在PHP连接MySQL的时候执行了如下设置:
set character_set_client=gbk
告诉MySQL服务器客户端来源数据编码是GBK,然后MySQL服务器对查询语句进行GBK转码导致反斜杠\被%df吃掉,而一般都不是直接设置character_set_client=gbk,通常的设置方法是SET NAMES'gbk',但其实SET NAMES'gbk'不过是比character_set_client=gbk多干了两件事而已,SET NAMES'gbk'等同于如下代码:
character_set_connection='gbk',
character_set_results='gbk',
character_set_client=gbk
这同样也是存在漏洞的,另外官方建议使用mysql_set_charset方式来设置编码,不幸的是它也只是调用了SET NAMES,所以效果也是一样的。不过mysql_set_charset调用SET NAMES之后还记录了当前的编码,留着给后面mysql_real_escape_string处理字符串的时候使用,所以在后面只要合理地使用mysql_real_escape_string还是可以解决这个漏洞的,关于这个漏洞的解决方法推荐如下几种方法:
1)在执行查询之前先执行SET NAMES'gbk',character_set_client=binary设置character_set_client为binary。
2)使用mysql_set_charset('gbk')设置编码,然后使用mysql_real_escape_string()函数被参数过滤。
3)使用pdo方式,在PHP5.3.6及以下版本需要设置setAttribute(PDO::ATTR_EMULATE_PREPARES,false);来禁用prepared statements的仿真效果。
如上几种方法更推荐第一和第三种。
下面对宽字节注入进行一个简单测试。
测算代码如下:
<?php
$conn=mysql_connect('localhost','root','123456');
mysql_select_db("test",$conn);
mysql_query("SET NAMES 'gbk'",$conn);
$uid=addslashes($_GET['id']);
$sql="SELECT * FROM userinfo where id='$uid'";
$result=mysql_query($sql,$conn);
print_r('当前SQL语句:'.$sql.'<br />结果:');
print_r(mysql_fetch_row($result));
mysql_close();
当提交/1.php?id=%df'union select 1,2,3,4%23时,成功注入的效果如下图所示。
对宽字节注入的挖掘方法也比较简单,只要搜索如下几个关键字即可:
SET NAMES
character_set_client=gbk
mysql_set_charset('gbk')
(2)二次urldecode注入
只要字符被进行转换就有可能产生漏洞,现在的Web程序大多都会进行参数过滤,通常使用addslashes()、mysql_real_escape_string()、mysql_escape_string()函数或者开启GPC的方式来防止注入,也就是给单引号(')、双引号(")、反斜杠(\)和NULL加上反斜杠转义。如果某处使用了urldecode或者rawurldecode函数,则会导致二次解码生成单引号而引发注入。原理是我们提交参数到WebServer时,WebServer会自动解码一次,假设目标程序开启了GPC,我们提交/1.php?id=1%2527,因为我们提交的参数里面没有单引号,所以第一次解码后的结果是id=1%27,%25解码的结果是%,如果程序里面使用了urldecode或者rawurldecode函数来解码id参数,则解码后的结果是id=1’单引号成功出现引发注入。
测试代码:
<?php
$a=addslashes($_GET['p']);
$b=urldecode($a);
echo '$a='.$a;
echo '<br />';
echo '$b='.$b;
测试效果如下图所示。
既然知道了原理主要是由于urldecode使用不当导致的,那我们就可以通过搜索urldecode和rawurldecode函数来挖掘二次urldecode注入漏洞。
3、espcms搜索注入分析
下面以一个小CMS程序espcms搜索注入的漏洞为例。
漏洞在interface/search.php文件和interface/3gwap_search.php文件in_taglist()函数都存在,一样的问题,以interface/search.php为例说明:
打开文件看到如下代码:
function in_taglist()
{
parent::start_pagetemplate();
include_once admin_ROOT . 'public/class_pagebotton.php';
$page = $this->fun->accept('page','G');
$page = isset($page)?intval($page):1;
$lng =(admin_LNG == 'big5')?$this->CON['is_lancode']:admin_LNG;
$tagkey = urldecode($this->fun->accept('tagkey','R'));
$takey = $this->fun->inputcodetrim($tagkey);
$db_where = ' WHERE lng=\'' . $lng . '\' AND isclass=1';
if(empty($tagkey))
{
$linkURL = $_SERVER['HTTP_REFERER'];
$this->callmessage($this->lng['search_err'],$linkURL,$this->lng ['gobackbotton']);
}
if(!empty($tagkey))
{
$db_where.=" AND FIND_IN_SET('$tagkey',tags)";
}
其中:
$tagkey = urldecode($this->fun->accept('tagkey','R'));
这行代码得到$_REQUEST['tagkey']的值,由于$tagkey变量使用了urldecode,从而可以绕过GPC:
$db_where.=" AND FIND_IN_SET('$tagkey',tags)";
经过判断$tagkey不为空则拼接到SQL语句中,导致产生注入漏洞。
(二)漏洞防范
SQL注入漏洞虽然是目前最泛滥的漏洞,不过要解决SQL注入漏洞其实还比较简单。在PHP中可以利用魔术引号来解决,不过魔术引号在PHP 5.4后被取消,并且gpc在遇到int型的注入时也会显得不那么给力了,所以通常用得多的还是过滤函数和类,像discuz、dedecms、phpcms等程序里面都使用过滤类,不过如果单纯的过滤函数写得不够严谨,也会出现绕过的情况,像这三套程序就都存在绕过问题。当然最好的解决方案还是利用预编译的方式,下面就来看看这三种方式的使用方法。
1、gpc/rutime魔术引号
通常数据污染有两种方式,一种是应用被动接收参数,类似于GET、POST等;还有一种是主动获取参数,类似于读取远程页面或者文件内容等。所以防止SQL注入的方法就是要守住这两条路。通常在开启这两个选项之后能防住部分SQL注入漏洞被利用。为什么说是部分,因为它们只对单引号(')、双引号(")、反斜杠(\)及空字符NULL进行过滤,在int型的注入上是没有多大作用的。
PHP 4.2.3以及之前的版本可以在任何地方设置开启,即配置文件和代码中,之后的版本可以在php.ini、httpd.conf以及.htaccess中开启。
2、过滤函数和类
过滤函数和类有两种使用场景,一种是程序入口统一过滤,像框架程序用这种方式比较多,另外一种是在程序进行SQL语句运行之前使用,除了PHP内置的一些过滤单引号等函数外,还有一些开源类过滤union、select等关键字。
(1)addslashes函数
addslashes函数过滤的值范围和GPC是一样的,即单引号(')、双引号(")、反斜杠(\)及空字符NULL,它只是一个简单的检查参数的函数,大多数程序使用它是在程序的入口,进行判断如果没有开启GPC,则使用它对$_POST/$_GET等变量进行过滤,不过它的参数必须是string类型,所以曾经某些程序使用这种方式对输入进行过滤时出现了绕过,比如只遍历$_GET的值,当时并没有考虑到$_GET的值也是一个数组。我们来看一个例子如下:
<?php
$str?=?"phpsafe";
echo?addslashes($str);
?>
上面的例子输出:phpsafe\'。
(2)mysql_[real_]escape_string函数
mysql_escape_string和mysql_real_escape_string函数都是对字符串进行过滤,在PHP4.0.3以上版本才存在,如下字符受影响【\x00】【\n】【\r】【\】【'】【"】【\x1a】,两个函数唯一不一样的地方在于mysql_real_escape_string接受的是一个连接句柄并根据当前字符集转义字符串,所以推荐使用mysql_real_escape_string。
使用举例:
<?php
$con = mysql_connect("localhost","root","123456");
$id = mysql_real_escape_string($_GET['id'],$con);
$sql="select * from test where id='".$id."'";
echo $sql;
当请求该文件?id=1’时,上面代码输出:select*from test where id='1\''
(3)intval等字符转换
上面我们提到的过滤方式,在int类型注入时效果并不好,比如可以通过报错或者盲注等方式来绕过,这时候intval等函数就起作用了,intval的作用是将变量转换成int类型,这里举例intval是要表达一种方式,一种利用参数类型白名单的方式来防止漏洞,对应的还有很多如floatval等。
应用举例如下:
<?php
$id=intval("1 union select ");
echo $id;
以上代码输出:1
3、PDO prepare预编译
如果之前了解过.NET的SqlParameter或者java里面的prepareStatement,那么就很容易能够理解PHP pdo的prepare,它们三个的作用是一样的,都是通过预编译的方式来处理数据库查询。
我们先来看一段代码:
<?php
dbh = new PDO("mysql:host=localhost;dbname=demo","user","pass");
$dbh->exec("set names 'gbk'");
$sql="select * from test where name =?and password =?";
$stmt = $dbh->prepare($sql);
$exeres = $stmt->execute(array($name,$pass));
上面这段代码虽然使用了pdo的prepare方式来处理sql查询,但是当PHP版本<5.3.6之前还是存在宽字节SQL注入漏洞,原因在于这样的查询方式是使用了PHP本地模拟prepare,再把完整的SQL语句发送给MySQL服务器,并且有使用set names'gbk'语句,所以会有PHP和MySQL编码不一致的原因导致SQL注入,正确的写法应该是使用ATTR_EMULATE_PREPARES来禁用PHP本地模拟prepare,代码如下:
<?php
dbh = new PDO("mysql:host=localhost;dbname=demo","user","pass");
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
$dbh->exec("set names 'utf8'");
$sql="select * from test where name =?and password =?";
$stmt = $dbh->prepare($sql);
$exeres = $stmt->execute(array($name,$pass));
二、XSS漏洞
XSS学名为跨站脚本攻击(Cross Site Scriptings),在Web漏洞中XSS是出现最多的漏洞,没有之一。这种漏洞有两种情况,一种是通过外部输入然后直接在浏览器端触发,即反射型XSS;还有一种则是先把利用代码保存在数据库或文件中,当Web程序读取利用代码并输出在页面上时触发漏洞,也就是存储型XSS。XSS攻当Web程序读取利用代码并输出在页面上时触发漏洞,也就是存储型XSS。XSS攻击在浏览器端触发,大家对其危害认识往往停留在可以窃取cookie、修改页面钓鱼,等等。用一句话来说明该漏洞的危害就是:前端页面能做的事它都能做。
(一)挖掘经验
挖掘XSS漏洞的关键在于寻找没有被过滤的参数,且这些参数传入到输出函数,常用的输出函数列表如下:print、print_r、echo、printf、sprintf、die、var_dump、var_export,所以我们只要寻找带有变量的这些函数即可。另外在代码审计中,XSS漏洞在浏览器环境对利用的影响非常大,所以最重要的还要掌握各种浏览器容错、编码等特性和数据协议。关于XSS漏洞的东西,由于篇幅问题,这些东西就不在这里详细介绍了,推荐阅读邱永华的《XSS跨站脚本攻击剖析与防御》。
XSS漏洞比SQL注入更多,而且在满足业务需求的情况下更加难防御。XSS漏洞经常出现在文章发表、评论回复、留言以及资料设置等地方,特别是在发文章的时候,因为这里大多都是富文本,有各种图片引用、文字格式设置等,所以经常出现对标签事件过滤不严格导致的XSS,同样,评论回复以及留言也是。其次在资料设置的地方,比如用户昵称、签名等,有的应用可能不只一处设置资料的地方,像在注册的地方可以设置、修改资料的地方可以设置,这时候要多留意,不一定所有设置这个资料的地方都过滤严格了。我们在通读代码挖掘的时候可以重点关注这几个地方,这几个地方的XSS也通常都是存储型的。
1、反射型XSS
反射型XSS也就是我们在描述里面说直接通过外部输入然后在浏览器端输出触发的类型,这种类型的漏洞比较容易通过扫描器黑盒直接发现,只需要将尖括号、单双引号等提交到Web服务器,检查返回的HTML页面里面有没有保留原来的特殊字符即可判断。但是白盒审计中,我们只需要寻找带有参数的输出函数,然后根据输出函数对输出内容回溯输入参数,观察有没有经过过滤。
举例一个反射型XSS漏洞的大致形式,代码如下:
//以下是QQ私密接口
if
(
$_GET["openid"]
)
{
//授权成功后,会返回用户的openid
//检查返回的openid是否是合法
id
//echo $_GET["oauth_signature"];
if(!is_valid_openid($_GET["openid"],$_GET["timestamp"],$_GET["oauth_signature"]))
{
showerr('API帐号有误!');
//demo对错误简单处理
echo "###invalid openid\n";
echo "sig:".$_GET["oauth_signature"]."\n";
exit;
}
代码中echo"sig:".$_GET["oauth_signature"]."\n";直接将$_GET["oauth_signature"]的值输出到浏览器中,则可以直接用GET方式注入代码。
2、存储型XSS
存储型XSS,顾名思义也就是需要先把利用代码保存在比如数据库或文件中,当Web程序读取利用代码并输出在页面上时执行利用代码,它的原理图流程图如下图所示。
存储型XSS比反射型要容易利用得多,不用考虑绕过浏览器的过滤,另外在隐蔽性上面也要好得多,特别是在社交网络中的存储型XSS蠕虫能造成大面积的传播,影响非常大,曾经在新浪微博和百度贴吧都爆发过大规模的XSS蠕虫。
同样,要挖掘存储型XSS也是要寻找未过滤的输入点和未过滤的输出函数,这个最终的输出点可能跟输入点完全不在一个业务流上,对于这类可以根据当前代码功能去猜,或者老老实实去追哪里有操作过这个数据,使用表名、字段名去代码里面搜索。
下面的经典案例分析将讲述一个存储型XSS的挖掘过程。
3、骑士cms存储型XSS分析
这里找了一个叫骑士cms的程序看了下,在后台申请友情链接的地方存在XSS漏洞,常规的特殊字符(如尖括号)和标签的事件(如onerror等)大多被过滤,漏洞挖掘过程如下。
安装好骑士cms后,在后台看到一个友情链接管理如下图所示。
前台有一个申请友情链接,根据经验这个申请友情链接的地方应该是一个payload输入的地方,我们先看看/admin/admin_link.php的代码:
$act =!empty($_GET['act'])?trim($_GET['act']):'list';
$smarty->assign('pageheader',"友情链接");
if($act == 'list')
{
get_token();
check_permissions($_SESSION['admin_purview'],"link_show");
require_once(QISHI_ROOT_PATH.'include/page.class.php');
$oederbysql=" order BY l.show_order DESC";
这里是判断访问admin_link.php这个文件的时候有没有act参数,没有就给$act变量赋值为list,即进入到输出友情链接列表的代码:
$offset=($currenpage-1)*$perpage;
$link = get_links($offset,$perpage,$joinsql.$wheresql.$oederbysql);
$smarty->assign('link',$link);
$smarty->assign('page',$page->show(3));
$smarty->assign('upfiles_dir',$upfiles_dir);
$smarty->assign('get_link_category',get_link_category());
$smarty->assign('navlabel',"list");
$smarty->display('link/admin_link.htm');
get_links()函数代码如下:
function get_links($offset,$perpage,$get_sql= '')
{
global $db;
$row_arr = array();
$limit=" LIMIT ".$offset.','.$perpage;
$result = $db->query("SELECT l.*,c.categoryname FROM ".table(
'link')." AS l ".$get_sql.$limit);
while($row = $db->fetch_array($result))
{
$row_arr[] = $row;
}
return $row_arr;
}
很清楚地看到,这是一个从数据库读取友情链接列表的功能:
$link = get_links($offset,$perpage,$joinsql.$wheresql.$oederbysql);
后面的代码则是将读取的内容以link/admin_link.htm为模板显示出来。跟进模板页看看,有一个关键的代码片段如下:
其中:
<spanduokan-code-cn">:#FF6600" title="<img src={#$list.link_logo#} border=0/>" class="vtip">[logo]</span>
这段代码是有问题的,这里直接把显示logo的img标签放在span标签的title里面,当鼠标滑过的时候会调用事件执行显示title即执行img标签,这里的利用点是{#$list.link_logo#}可以是HTML实体编码,从而绕过骑士cms的安全检查。目前我们已经找到一个输出点了,输入点也根据当前代码功能猜到是在前台申请链接的地方,利用过程如下,在前台申请友情链接页面http://localhost/74cms/link/add_link.php的logo字段输入:
1 onerr;
or=aler;
t(1)
来构造代码如下:
<spanduokan-code-cn">:#FF6600" title="<img src=1 onerr;
or=aler;
t(1)
border=0/>" class="vtip">[logo]</span>
执行结果如下图所示。
当管理员在后台查看链接时触发漏洞执行代码,如下图所示。
(二)漏洞防范
由于XSS漏洞在不同浏览器下有不同的利用方式,而且特别是业务上有需求使用富文本编辑器的时候,防御起来就更加复杂,所以在XSS防御这块应该从多个方面入手,尽量减少XSS漏洞。
1、特殊字符HTML实体转码
一般的XSS漏洞都是因为没过滤特殊字符,导致可以通过注入单双引号以及尖括号等字符利用漏洞,比如一个图片标签如下<img src="$_GET['a']"/>,则可以通过输入双引号来闭合第一个单引号利用漏洞,防御这类的XSS漏洞只需要过滤掉相关的特殊字符即可,特殊字符列表如下:
1)单引号(')
2)双引号(")
3)尖括号(<>)
4)反斜杠(\)
5)冒号(:)
6)and符(&)
7)#号(#)
还有两个问题,这些字符应该怎么过滤,什么时候过滤?为了保证数据原始性,最好的过滤方式是在输出和二次调用的时候进行如HTML实体一类的转码,防止脚本注入的问题。
2、标签事件属性黑白名单
上面我们提到过滤特殊字符来防止XSS漏洞,实际上即使过滤了也同样可能会被绕过,比如利用跟宽字节注入一样的方式来吃掉反斜杠,再利用标签的事件来执行js代码,面对这样的情况,我们还得加标签事件的黑名单或者白名单,这里更推荐用白名单的方式,实现规则可以直接用正则表达式来匹配,如果匹配到的事件不在白名单列表,就直接拦截掉,而不是替换为空。
三、CSRF漏洞
CSRF全称为Cross-site request forgery,跨站请求伪造。说白一点就是可以劫持其他用户去进行一些请求,而这个CSRF的危害性就看当前这个请求是进行什么操作了。
而CSRF是怎么一个攻击流程呢?举一个最简单的例子,比如直接请求http://x.com/del.php?id=1可以删除ID为1的账号,但是只有管理员有这个删除权限,而如果别人在其他某个网站页面加入<img src=”http://x.com/del.php?id=1”>再把这个页面发送给管理员,只要管理员打开这个页面,同时浏览器也会利用当前登录的这个管理员权限发出http://x.com/del.php?id=1这个请求,从而劫持了这个账号做一些攻击者没有权限做的事情。
上面举的这个例子只是其中一个场景,更严重的像添加管理员账号、修改网站配置直接写入webshell等等都有很多案例。
(一)挖掘经验
CSRF主要是用于越权操作,所有漏洞自然在有权限控制的地方,像管理后台、会员中心、论坛帖子以及交易管理等,这几个场景里面,管理后台又是最高危的地方,而CSRF又很少被关注到,因此至今还有很多程序都存在这个问题。我们在挖掘CSRF的时候可以先搭建好环境,打开几个有非静态操作的页面,抓包看看有没有token,如果没有token的话,再直接请求这个页面,不带referer。如果返回的数据还是一样的话,那说明很有可能有CSRF漏洞了,这个是一个黑盒的挖掘方法,从白盒角度来说的话,只要读代码的时候看看几个核心文件里面有没有验证token和referer相关的代码,这里的核心文件指的是被大量文件引用的基础文件,或者直接搜"token"这个关键字也能找,如果在核心文件没有,再去看看你比较关心的功能点的代码有没有验证。
Discuz CSRF备份拖库分析
下面我们来分析一个Discuz CSRF可以直接脱裤的漏洞,这个漏洞影响非常大,漏洞在刚公开的时候导致了大量的Discuz论坛被拖库。
漏洞文件在source/admincp/admincp_db.php第30行开始:
if(!$backupdir)
{
$backupdir = random(6);
@mkdir('./data/backup_'.$backupdir,0777);
//文件夹名是六位随机数
C::t('common_setting')->update('backupdir',$backupdir);
/
} else {
//这边也没有做fromhash的验证
DB::query('SET SQL_QUOTE_SHOW_CREATE=0','SILENT');
if(!$_GET['filename']!preg_match('/^[\w\_]+$/',$_GET['filename']))
{
cpmsg('database_export_filename_invalid','','error');
}
/*省略,往下走*/
$backupfilename = './data/'.$backupdir.'/'.str_replace(array('/','\\','.',"'"),'',$_GET['filename']);
//文件名从$_GET['filename'])获取,可控
if($_GET['usezip'])
{
require_once './source/class/class_zip.php';
}
if($_GET['method'] == 'multivol')
{
$sqldump = '';
$tableid = intval($_GET['tableid']);
$startfrom = intval($_GET['startfrom']);
if(!$tableid && $volume == 1)
{
foreach($tables as $table)
{
$sqldump .= sqldumptablestruct($table);
}
}
$complete = TRUE;
for(;$complete && $tableid < count($tables)&& strlen($sqldump)
+ 500 < $_GET ['sizelimit'] * 1000;$tableid++)
{
$sqldump .= sqldumptable($tables[$tableid],$startfrom,strlen($sqldump));
if($complete)
{
$startfrom = 0;
}
}
$dumpfile = $backupfilename."-%s".'.sql';
//$dumpfile为最终导出文件名,下面的代码是写文件
在这个漏洞中,由于表名和文件都是直接GET提交的,目录名由一个固定的backup加上一个六位数字组成,备份成功后可以直接爆破,最终利用可以直接在论坛发帖加入下面代码即可:
<img src="http://127.0.0.1/discuz/admin.php?
action=db&operation=export&setup=1&scrolltop=&anchor=&type=custom&customtables%5B%5D={表名}&method=multivol&sizelimit=2048&extendins=0&sqlcompat=&usehex=1&usezip=0&filename={文件名}&exportsubmit=%CC%E1%BD%BB22">
利用截图,如下图所示。
(二)漏洞防范
防御CSRF漏洞的最主要问题是解决可信的问题,即使是管理员权限提交到服务器的数据,也不一定是完全可信的,所以针对CSRF的防御有以下两点:
1)增加token/referer验证避免img标签请求的水坑攻击,
2)增加验证码。
1、Token验证
Token翻译中文为“标志”,在计算机认证领域叫令牌。利用验证Token的方式是目前使用的最多的一种,也是效果最好的一种,可以简单理解成在页面或者cookie里面加一个不可预测的字符串,服务器在接收操作请求的时候只要验证下这个字符串是不是上次访问留下的即可判断是不是可信请求,因为如果没有访问上一个页面,是无法得到这个Token的,除非结合XSS漏洞或者有其他手段能获得通信数据。
Token实现测试代码如下:
<?php
session_start();
function set_token()
{
$_SESSION['token'] = md5(time()+rand(1,1000));
}
function check_token()
{
if(isset($_POST['token'])&&$_POST['token'] === $_SESSION['token'])
{
return true;
}
else
{
return false;
}
}
if(isset($_SESSION['token'])&&check_token())
{
echo "success";
}
else
{
echo "failed";
}
set_token();
?>
<form method="post">
<input type="hidden" name="token" value="<?=$_SESSION['token']?>">
<input type="submit"/>
</form>
运行结果,如果请求里面的Token值跟服务器端的一致,则输出“success”,否则输出“failed”。
2、验证码验证
验证码验证没有Token那么实用,考虑到用户体验,不可能让用户每个页面都去输入一次验证码,这估计用户得疯掉,所以一般这种方式只用在敏感操作的页面,比如像登录页面,实现方式跟Token差不多,这里就不再详细给出代码。
微信公众号:计算机与网络安全
ID:Computer-network
【推荐书籍】