书写错误,导致恶意用户构造语句可以写入webshell,进而控制整个服务器。
前几个晚上,把前台文件,只要是数据库调用中的变量都看了一遍。看看是不是有过滤不严的地方,看完后觉得,过滤不严的地方的确不少,但是都已经被单引号保护起来了。在php中,如果magic_qoute_gpc=on(默认的)编译器会自动把单引号等特殊字符转义,而这个时候我们想改变程序的执行流程是非常困难的。这样大大的增加了入侵的难度,在某种程度上,也的确保证了其安全。这也是为什么朋友提出一定要在magic_qoute_gpc为on的时候依然可以利用的要求。如果需要单引号介入才能利用的漏洞,在Discuz!论坛中基本上是没什么用处的。
在把所有文件中数据库连接的地方读了一个遍后,没找到任何有价值的东西。这个时候思路开始有一点乱了,我在犹豫自己是否应该试着从程序员的思维逻辑漏洞下手看看,这就意味着我要去通读所有代码,找到程序员在写程序时考虑不周全的地方,这种漏洞多半是上下文逻辑关系错误,或者是限定不唯一,还有就是一些本来应该注意,但管理员却偏偏忽略的地方。
想到这里,看来没什么能偷懒的了,只能通读代码。因为既然目标是逻辑错误,那么就一定要细看上下文的关系,所以选择从入口点开始读起。入口点就应该是logging.php,因为这是登陆的地方,所有人都会从这里登陆进入论坛。上吧!
老规矩,我们先来看一段代码:
=========codz begin==========
50 $errorlog = "$username\t".substr($password, 0, 2);
......
54 $errorlog .= substr($password, -1)."\t$onlineip\t$timestamp\n";
55 $password = md5($password);
56 $query = $db->query("SELECT m.username as discuz_user, m.password as discuz_pw, m.status, m.styleid AS styleidmem, m.lastvisit,
u.groupid, u.isadmin, u.specifiedusers LIKE '%\t$username\t%' AS specifieduser
FROM $table_members m LEFT JOIN $table_usergroups u ON u.specifiedusers LIKE '%\t$username\t%' OR (u.status=m.status AND ((u.creditshigher='0' AND u.creditslower='0' AND u.specifiedusers='') OR (m.credit>=u.creditshigher AND m.creditWHERE username='$username' AND password='$password' ORDER BY specifieduser DESC");
......
69 if(!$discuz_user)
{
70 @$fp = fopen($discuz_root.'./forumdata/illegallog.php', 'a');
71 @flock($fp, 3);
72 @fwrite($fp, $errorlog);
73 @fclose($fp);
74 showmessage('login_invalid', 'index.php');
}
=========codz endz==============
这段代码我们来一句一句看看,他先纪录输入的用户名和密码,密码只取前两位,然后取密码后一位,并且将ip和时间一起赋过来。然后去密码的md5值,放到数据库中去。如果你认为我们可以改变数据库执行语句的操作流程那就错了,面对单引号我们没有什么可以做的(至少是我做不了什么,除非加密)。后面如果用户名和密码不对则将纪录下错误用户名和密码到illegallog.php中。整个这个记录错误密码的过程中,变量没有经过任何验证,也就是说如果我成心输入错误的用户名,他也不会作检查然后直接记录下来。那么如果我的错误的用户名是一个可执行的代码,他也会记录下来。在他记录下来之后我们去调用这个文件就可以形成一个shell。
到这里你是不是已经兴奋了?对不起,你的兴奋无效。我是一点都兴奋不起来,因为我在前面读第一遍的时候,特别注意过Discuz!对文件句柄的操作,他的确对个别变量没有过滤,但是他在install.php中得初始化的时候,已经给所有用到的以.php结尾的数据文件开始的地方添加了一句:。这是初始化的时候写入的,我们都应该明白这句话的作用。你无法去调用你写入的东西,因为一开始就已经结束了.这样显然是无法成功的。有点烦了,心想不就是5根羊棒么?输就输了!一赌气扔下代码,自己跑到姥姥的那屋,搂着姥姥撒起娇来,(在姥姥面前撒娇、耍赖、捣乱是我最喜欢的事情之一)我在姥姥面前反复咒骂着Discuz!的变态,说我自己如何如何认真的读代码。姥姥并不知道我在说什么,也不在乎我给她捣乱,继续看着自己的电视。过了一会儿,姥姥应了一句:"你这孩子啊,就是粗心,一点都不心细,你看你这什么码(估计是说代码)又坏了吧"。我嘎嘎大声的笑着爬回了自己的屋子。坐在电脑前,喝了杯白开水,冷静了一下。
做安全的人细心,毅力,自学,善于总结是非常必要的。回过头又把logging.php文件重新读一编,看看有没有什么自己忽略的地方。文件并不长,又看了一遍觉得还是没什么问题。既然这里没什么问题,思路就放到那句上,这句话是如何写入的呢?
于是在install.php中翻到了这段代码:
==========codz begin==========
29 function loginit($log) {
30 echo '初始化记录 '.$log;
31 $fp = @fopen('./forumdata/illegallog.php');
32 @fwrite($fp, "\n");
33 @fclose($fp);
34 result();
35 }
......
1389 loginit('karmalog');
1390 loginit('illegallog');
1391 loginit('modslog');
1392 loginit('cplog');
1393 dir_clear('./forumdata/templates');
1394 dir_clear('./forumdata/cache');
==========codz endz==========
很显然,loginit这个函数就是在往illegallog.php中写入这句话。这样我们就算写入了代码也会因为那句话的存在而无法调用执行。我们已经完全进入了一个死胡同。但我似乎觉得这段代码哪里有些问题,再仔细看了一下,那个fopen看着怎么那么不顺眼啊。如果没记错的话fopen的语法格式应该是这样的:
resource fopen ( string filename, string mode [, int use_include_path [, resource zcontext]])
前面两个参数,一个是文件句柄,或者指定打开哪个文件。第二个参数是指定打开的方式,比如读还是写。这里要提醒大家的是这个两个参数是必选的。比如我们写入目录下的Jambalaya.php,我们的语句是这么写的:fopen('./Jambalya.php','w'),后面那个打开方式必须选的,否则就会报错。可是我们注意到,他的代码中只有一个参数,这样程序怎么可能正确执行呢?
我们来做一个试验,创建一个1.php,写入如下代码:
===========codz begin==========
$fp = @fopen('./2.php');
@fwrite($fp, "\n");
@fclose($fp);
echo "success!";
===========codz endz============
在这里做一个和Discuz!中的一样的环境,如果这个程序执行成功,那么会在根目录下产生一个2.php,而2.php的开头一行应该是,并且屏幕上显示success,其实这里的success就是用来让我们了解程序执行所到的位置。我们在url中提交:http://127.0.0.1/myhome/1.php,屏幕上显示了success,这说明已经执行到程序的尾部。但是检查目录时发现并没有生成名为2.php的文件,也就是说我们写入失败了。也许大家会奇怪,写入失败应该有出错信息啊。的确应该有,但是因为在fopen前加上了@,而@在php中是用来抑制所有调用函数产生的错误信息的。换句话说就是即使出错了,也不会报错。
姥姥教训的没错,我太粗心了!
假设一切都是按我的思路走过的话,也就说在安装初始化的时候,因为那个fopen的错误使用,所以discuz/forumdata目录下绝对不会产生一个含有代码的illegallog.php文件,但是因为抑制出错信息,所安装的时候会依然显示初始化成功,其实却并没有初始化,更没有产生illegallog.php。而如果这里没有初始化,也就意味着illegallog.php的产生和初始化将在logging.php中完成。logging.php的初始化并没有对文件写入任何保护语句或者过滤措施来避免用户调用。到这里,一切都明朗了。说得直白一点,也就是因为install.php文件错误的初始化的缘故,我们可以通过logging.php写入恶意代码,然后调用那个文件来产生一个shell控制整个网站。
Exploit代码如下
以下内容可能带有攻击性,仅供安全研究与教学之用。使用者风险自负
不用注册任何账户,到登陆页面,在登陆用户名的地方先输入123456,回车。这里大家可能明白了,密码前两位是显示的,这样illegallog.php里面保存的就是:
*****6 127.0.0.1 1022383175
这就可以查看php的设置了,我们先看一下register_globals这个设置是否为on。(大部分的网站都是on)好,然后我们在登陆口输入,这里最好不用system(),我在做测试的时候很多网站把这个system()函数禁用了。
然后我们调用http://192.168.0.13/forumdata/illegallog.php?cmd=dir
前面出来一堆垃圾信息,往最下面看是不是可以看到目录被列出来了?但是这样写入有点麻烦,因为在实验的时候,大的网站注册用户有10万多人,那么这个文件会大的出奇,打开速度奇慢。
那么我们这里其实还可以这样写入,我们把php的shell改成jpg格式的图片,上传到主机。
在URL中调用:
http://192.168.0.13/forumdata/illegallog.p...chments/Jam.php
然后我们直接提交URL:
http://192.168.0.13/attachments/Jam.php就?...一个shell了吧!