下面进入本案例的一个核心,即图片上传模块。此部分的表单部分是通过upload.asp文件显示的,实际的上传和将图片信息写入数据库是通过页面upfile.asp文件处理的。
upload.asp文件用于显示供用户上传使用的表单,在这个表单里面,用户可以选择要上传的照片所属的类别,同时可以填写照片名称、介绍、TAG等内容,通过使用表单文件域选择文件,还可以预览所选择的文件。
upload.asp文件的访问效果如图8.12所示。
图8.12
在这里,整体的表单基本都是由静态HTML代码显示的,但是在选择“您要把图片上传到哪个相册”的部分,则是由ASP读取当前登录的用户所创建的所有相册,并动态生成的,我们先来看这部分的代码。
<p>
<label for="AlbumID">您要把图片上传到哪个相册:</label>
<select name="AlbumID" id="AlbumID">
<%
'选择当前用户的所有相册
sql = "SELECT * FROM Album WHERE UserID = " & CLng(Session("weshare_UserID"))
'打开记录集
oRs.Open sql,oConn,1,1
'遍历记录集,显示相册
For i = 1 To oRs.RecordCount%>
<option value="<%=oRs("AlbumID")%>"><%=oRs("AlbumName")%></option>
<%oRs.MoveNext
Next%>
</select>
<a href="javascript:showModalBox();">新建相册</a>
</p>
在这里,首先生成SQL语句,这里根据用户Session中所保存的用户ID值来选取Album表中由当前用户所建立的所有相册记录。
而后程序打开记录集,循环以相应格式输出记录集中的数据。
这里你可能会有疑问,如果没有登录的用户访问了此页面,是否会出错呢。是的,因为如果没有登录,则Session("weshare_UserID")的值为空,此时将会出错,因此这里我们在本页面的头部对用户是否登录做出了检测:
<%@LANGUAGE="VBSCRIPT" CODEPAGE="936"%>
<%Option Explicit%>
<!--#include file="conn.html" -->
<!--#include file="include/function.html" -->
<%
'判断用户是否登录
If IsLogined()= False Then
ShowError "请先登录!"
Response.End
End If
'……省略无关代码
%>
以上是显示用户相册的选择域的代码,我们再来看实现用户在文件域中选择文件后,预览用户所选图片的代码,这部分实际是JavaScript代码进行处理的,其效果如图8.13所示。
图8.13
实现此功能,首先我们需要在页面的相应部位预留显示所选图片的区域:
<div id="picture-preview">
</div>
而后我们为文件域定义onchange属性,即当此文件域的值发生变化时,触发JavaScript函数picturePreview:
<p>
<label for="PictureFile">选择一张本地图片:</label>
<input type="file" name="PictureFile" id="PictureFile" onchange="JavaScript:picturePreview()" />
</p>
picturePreview函数完成显示图片缩略图的功能,其代码如下:
<script type="text/javascript">
function picturePreview()
{
var sPicLocalUrl = $F("PictureFile");
//通过正则判断所选文件扩展名来判断用户选择的是否是图片文件
if(!(/^.+\.(gif|jpg|jpeg|png|bmp)$/i.test(sPicLocalUrl.toLowerCase())))
{
alert("对不起,您只能选择一个图片上传!");
return false;
}
$("picture-preview").innerHTML = '<img id="prev" src="' + sPicLocalUrl + '" width="180" />'
}
</script>
此函数首先获得文件域PictureFile的值(这里$F是Prototype框架中的函数,用于获取表单域的值)。而后使用正则表达式对其进行测试,通过扩展名来判断所选的是否是图片文件。如果不是图片文件,则给出提示,否则将picture-preview元素的innerHTML属性设置为显示图片缩略图的HTML代码。
显示缩略图的功能十分实用,它可以让用户在上传之前看到自己要上传的图片,从而避免错误上传,这样可以提高用户体验的小功能,希望你在自己的程序中多加使用。
[NextPage]
细心的你会注意到,在上一节显示用户相册列表的部分,有这样一段HTML代码:
<a href="javascript:showModalBox();">新建相册</a>
这里当点击此链接后,将打开一个模态窗口,来提供让用户新建相册的功能,这里实际上是调用diag.Open的JavaScript方法来实现打开mod_Album.asp页面的,如图8.14所示。
图8.14
Mod_Album.asp文件与我们以往的表单处理文件类似,都是将显示表单和处理表单合并在同一个页面中的,这里我们主要来看当用户输入要建立的相册名称,并提交表单后的处理代码:
<%
If PostBack() Then
Dim sql,oRs
Dim sAlbumName,lAlbumID
'获取相册名称
sAlbumName = Server.HTMLEncode(Trim(Request.Form("AlbumName")))
'建立sql语句
sql = "SELECT * FROM [Album] WHERE AlbumName = '" & sAlbumName & "' AND UserID = " & Session("weshare_UserID")
'打开记录集取出相应用户的相应名称的相册
Set oRs = Server.CreateObject("ADODB.RecordSet")
oRs.Open sql,oConn,1,3
'如果不存在,则加入相册到数据库
If oRs.EOF AND oRs.BOF Then '如果记录集为空,即不存在同名相册
oRs.AddNew() '新增记录
'设定新增的记录各字段的值
oRs("AlbumName") = sAlbumName
oRs("UserID") = Session("weshare_UserID")
'更新记录集
oRs.Update
'获得新增的相册编号
oRs.MoveLast
lAlbumID = oRs("AlbumID")
Response.Write("<script type=""text/javascript"">var opt=new Option('" & sAlbumName & "','" & lAlbumID & "');window.opener.document.getElementById('AlbumID').options.add(opt);opt.selected=true;window.opener.diag.Close()</script>")
Else '如果存在同名相册,则提示错误
Response.Write("<script type=""text/javascript"">alert('您已经有这个名字的相册了!');window.opener.diag.Close()</script>")
End If
'关闭和清空记录集和数据连接,回收资源
oRs.Close
Set oRs = Nothing
oConn.Close
Set oConn = Nothing
Response.End()
End If
%>
程序首先生成SQL语句,用以从表Album选取当前用户的要建立的相册名称的记录,并打开记录集,而后进行判断,如果记录集为空,那么则添加一条记录,同时输出一段HTML代码,用以设定upload.asp文件中选取域,以加入新增的相册,如果记录集不为空,则说明当前用户已经有这个名称的相册了,此时给出提示。
[NextPage]
在以往的章节中,我们似乎都没有或者很少涉及到ASP对文件的操作。实际上,对于一个网站来说,所使用的数据交互,除了数据库之外,还有很大一部分是和服务器文件系统上的文件进行交互。而将客户端的文件上传到服务器,则又是这其中最常用的操作之一。
本案例中,我们将通过图片上传处理页面upfile.asp为例来讲解在ASP中上传文件的一种方法,即通过无组件上传类来上传文件。
ASP不像其他网站编程语言,内置了上传文件的处理模块,而是通过第三方组件来对此功能进行扩展的,这样做自然符合ASP的特点—简单的脚本操作配合复杂的组件以实现完整的网站功能。但是有时在服务器上由于种种限制却不能够安装这些处理上传的组件,这时我们就使用无组件上传类来解决这一问题。
对文件上传的处理,实质上来说是分析客户端发过来的数据包,对其内容进行分析,将数据包中文件的二进制代码提取出来并保存到服务器的文件系统上的过程。现在网络上已经有很多成熟的无组件上传类,本案例介绍的是稻香老农的无组件上传类,你可以在include/lib/ upload_5xsoft.inc文件中找到其源代码。
具体来说,当你在一个网站表单中,加入文件域,同时设定Form标签的enctype,即编码方式属性为“multipart/form-data”时(如果你使用Dreamweaver,在加入文件域时,软件会自动修改编码方式属性,如果你直接在记事本手写代码,请务必记得加入或修改这个属性,否则将造成无法上传),当你提交表单,浏览器就会以特殊的方式发送数据包,而并非以传统的“名称=值”这样的参数对了,他以一个特定字符串分隔开各个参数对,并将文件的二进制代码一并发送,而我们的ASP在服务器端所做的就是首先获得这个原始的请求信息包,然后分隔开各个参数对,并从中找出文件的那一部分,顺次的一点一点的读取文件的代码,并将其写入到服务器上的硬盘中,以完成文件的上传。
这个过程看起来着实非常繁琐,不过你不用担心,这并非需要你来实现的东西,现在网上已经有很多成熟的无组件上传类了,我们下面将介绍稻香老农的无组件上传类的使用方法。
从理论回到我们的upfile.asp文件中来。
在这个文件中,相比一般的获得表单内容并保存到数据库当中的操作,我们实际上只是多了一个步骤,那就是对上传来的文件进行保存的过程。而这个保存的过程实际上又可以分为三步:第一步是生成上传的文件在服务器上保存的全路径,第二步是对此路径做出判断,如果路径中指定的目录(文件夹)不存在,则创建之,第三步则是调用上传类的相应方法来实现文件的保存。
我们来具体看upfile.asp文件的代码:
<%@LANGUAGE="VBSCRIPT" CODEPAGE="936"%>
<%Option Explicit%>
<!--#include file="conn.html" -->
<!--#include file="include/function.html" -->
<!--#include file="include/lib/upload_5xsoft.inc" -->
<%
'登录后才能上传文件,因此先做登录检测
If IsLogined() = False Then
ShowError "请先登录!"
Response.End()
End If
'……省略变量定义
'获得当前用户编号
lUserID = Trim(Session("weshare_UserID"))
'设定脚本超时时间为1000秒
Server.ScriptTimeout = 1000
'建立上传对象
Set oUpload = New upload_5xSoft
'建立文件对象
Set oFile = oUpload.File("PictureFile")
'建立FSO对象
Set oFSO = Server.CreateObject("Scripting.FileSystemObject")
'取上传文件的文件名和扩展名
sFilename = Trim(oFile.FileName)
sFileExt = Mid(sFilename, InStrRev(sFilename, "."), Len(sFilename))
'生成保存在服务器上的路径和文件名
sNewFilePath = "UserUpload/" & lUserID & "/" & Year(Date()) & Right("00" & Month(Date()), 2)
sNewFileName = Clng(Timer * 100) & sFileExt
'将文件按照用户“编号/年月”存放,如果不存在目录,则创建之
If Not oFSO.FolderExists(Server.MapPath("UserUpload/" & lUserID)) Then
oFSO.CreateFolder(Server.MapPath("UserUpload/" & lUserID))
End If
If Not oFSO.FolderExists(Server.MapPath(sNewFilePath)) Then
oFSO.CreateFolder(Server.MapPath(sNewFilePath))
End If
'如果文件扩展名合法
If CheckExt(sFileExt) = True Then
'首先保存文件
oFile.Saveas Server.MapPath(sNewFilePath & "/" & sNewFileName)
'打开空记录集
sql = "SELECT * FROM [Photo] WHERE 1=0"
Set oRs = Server.CreateObject("ADODB.RecordSet")
oRs.Open sql,oConn,1,3
oRs.AddNew() '增加新纪录
'设定新纪录各字段的值
oRs("UserID") = lUserID
oRs("AlbumID") = Trim(oUpload.Form("AlbumID"))
oRs("PhotoName") = Server.HtmlEnCode(Trim(oUpload.Form("PhotoName")))
oRs("PhotoContent") = Server.HtmlEnCode(Trim(oUpload.Form("PhotoContent")))
oRs("PhotoSourceUrl") = sNewFilePath & "/" & sNewFileName
oRs("PhotoSize") = oFile.FileSize
oRs("PhotoPermission") = Trim(oUpload.Form("PhotoPermission"))
'注意下面对标签进行了一定的格式化后才存入数据库
oRs("PhotoTags") = "," & Trim(oUpload.Form("PhotoTags")) & ","
oRs("PhotoClick") = 0
oRs("PhotoVoteScore") = 3 '设定默认的得分为3分
oRs("PhotoVoteTotal") = 1 '设定默认的投票人数为1人
oRs("PhotoAddTime") = Now()
'更新记录集
oRs.Update
'调用SaveTags过程保存标签
SaveTags Trim(oUpload.Form("PhotoTags"))
oRs.Close
Set oRs = Nothing
ShowSuccess "文件上传成功!"
Else
ShowError "文件上传失败!"
End If
'清空各对象,回收资源
Set oUpload = Nothing
Set oFile = Nothing
Set oFSO = Nothing
CloseDb()
%>
在这个文件中,首先判断用户是否登录,如果没有登录,则提示错误。而后程序获得当前登录的用户的编号,保存在变量lUserID中以备使用。
而后程序进行了一个设定脚本超时时间的操作:Server.ScriptTimeout = 1000,这样可以避免由于上传大文件,所需处理时间较长,而脚本超时时间很短,从而导致不能完成上传的操作。
而后程序分别建立上传类的实例oUpload、指向客户端上传文件的对象指针oFile和FileSystemObject(即文件系统对象)的实例oFSO。
而后程序通过oFile的相关属性获得所上传文件的文件名sFilename,并得到其扩展名sExt。
接下来我们就要生成文件所保存的路径了,这里规定,用户所上传的图片均以UserUpload/用户编号/年月(YYMM)/文件名格式保存,这样我们生成保存的路径sNewFilePath,而后根据当前时间生成文件名sNewFileName。
这时万事具备,只欠东风,我们就要进行实际的保存工作了。
在保存之前,程序调用函数CheckExt对签名获得的文件扩展名进行判断,仅当其为图片文件的扩展名,才进行保存工作,否则提示错误。
实际的保存工作是通过oFile.saveas方法来将文件保存在服务器上的文件系统,而后获取用户填写的图片名称等信息并保存在数据库中,这部分与我们以前的代码类似,这里不再详述,最后我主要讲解下处理图片Tag的过程SaveTags。
SaveTags过程代码如下:
Sub SaveTags(strTags)
If strTags = "" Then Exit Sub
Dim aTags,sTagElement
Dim oRsTag,sqlTag
aTags = Split(strTags,",")
'建立记录集对象的实例oRsTag
Set oRsTag = Server.CreateObject("ADODB.RecordSet")
'针对每个标签遍历集合
For Each sTagElement In aTags
'打开记录集,判断是否存在当前标签
sqlTag = "SELECT * FROM [Tag] WHERE TagName = '" & sTagElement & "'"
oRsTag.Open sqlTag,oConn,1,3
If oRsTag.EOF AND oRsTag.BOF Then '如果不存在当前标签
'则创建它
oRsTag.AddNew()
oRsTag("TagName") = sTagElement
oRsTag("TagCount") = 1
Else '如果存在当前标签
'则为其使用次数加1
oRsTag("TagCount") = oRsTag("TagCount") + 1
End If
'更新和关闭记录集
oRsTag.Update
oRsTag.Close
Next
Set oRsTag = Nothing
End Sub
你应该还记得我们在设计数据库时,对此部分的处理,我们将图片的Tags单独保存在一个表中,这个过程首先将以逗号分割的字符串转换为数组,而后遍历数组的每一个元素—单个Tag,在遍历过程中,打开记录集,判断Tag表中是否有相应的Tag记录,如果没有,则添加一个,如果有,则为其使用次数加一。
[NextPage]
上传漏洞是ASP安全中的一个重要话题。他的危害相比其他漏洞来说,可以说是致命的。在上传处理的代码中,如果没有对用户所上传的文件的路径、扩展名进行很好的处理,则可能导致用户可以任意的上传asp文件到服务器上,从而危害服务器的安全。
一般对于上传漏洞的概念定义如下:由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。打个比方来说,如果你使用Windows服务器并且以asp作为服务器端的动态网站环境,那么在你的网站的上传功能处,就一定不能让用户上传asp类型的文件,否则他上传一个WebShell,你服务器上的文件就可以被他任意更改了。
我们知道,在Web中进行文件上传的原理是通过将表单设为multipart/form-data,同时加入文件域,而后通过HTTP协议将文件内容发送到服务器,服务器端读取这个分段(multipart)的数据信息,并将其中的文件内容提取出来并保存的。通常,在进行文件保存的时候,服务器端会读取文件的原始文件名,并从这个原始文件名中得出文件的扩展名,而后随机为文件起一个文件名(为了防止重复),并且加上原始文件的扩展名来保存到服务器上。
而这时程序员可能有如下的疏忽:
1. 完全没有处理。
完全没有处理的情况不用我说,看名字想必大家都能够了解,这种情况是程序员在编写上传处理程序时,没有对客户端上传的文件进行任何的检测,而是直接按照其原始扩展名将其保存在服务器上,这是完全没有安全意识的做法,也是这种漏洞的最低级形式,一般来说这种漏洞很少出现了,程序员或多或少的都会进行一些安全方面的检查。
2. 将“asp”等字符替换。
我们再看一些程序员进阶的做法,程序员知道asp这样的文件名是危险的,因此他写了个函数,对获得的文件扩展名进行过滤,如:
Function checkExtName(strExtName)
strExtName = lCase(strExtName) '转换为小写
strExtName = Replace(strExtName,"asp","") '替换asp为空
strExtName = Replace(strExtName,"asa","") '替换asa为空
checkExtName = strExtName
End Function
使用这种方式,程序员本意是将用户提交的文件的扩展名中的“危险字符”替换为空,从而达到安全保存文件的目的。粗一看,按照这种方式,用户提交的asp文件因为其扩展名asp被替换为空,因而无法保存,但是仔细想想,这种方法并不是完全安全的。
突破的方法很简单,只要我将原来的WebShell的asp扩展名改为aaspasp就可以了,此扩展名经过checkExtName函数处理后,将变为asp,即a和sp中间的asp三个字符被替换掉了,但是最终的扩展名仍然是asp。
因此这种方法是不安全的。如何改进呢,请接着往下看。
3. 不足的黑名单过滤。
知道了上面的替换漏洞,你可能已经知道如何更进一步了,对了,那就是直接比对扩展名是否为asp或者asa,这时你可能采用了下面的程序:
Function checkExtName(strExtName)
strExtName = lCase(strExtName) '转换为小写
If strExtName = "asp" Then
checkExtName = False
Exit Function
ElseIf strExtName = "asa" Then
checkExtName = False
Exit Function
End If
checkExtName = True
End Function
你使用了这个程序来保证asp或者asa文件在检测时是非法的,这也称为黑名单过滤法,那么,这种方法有什么缺点呢。
黑名单过滤法是一种被动防御方法,你只可以将你知道的危险的扩展名加以过滤,而实施上,你可能不知道有某些类型的文件是危险的,就拿上面这段程序来说吧,你认为asp或者asa类型的文件可以在服务器端被当作动态脚本执行,事实上,在Windows 2000版本的IIS中,默认也对cer文件开启了动态脚本执行的处理,而如果此时你不知道,那么将会出现问题。
实际上,不只是被当作动态网页执行的文件类型有危险,被当作SSI处理的文件类型也有危险,例如shtml、stm等,这种类型的文件可以通过在其代码中加入<!--#include file="conn.html"-->语句的方式,将你的数据库链接文件引入到当前的文件中,而此时通过浏览器访问这样的文件并查看源代码,你的conn.asp文件源代码就泄露了,入侵者可以通过这个文件的内容找到你的数据库存放路径或者数据库服务器的链接密码等信息,这也是非常危险的。
那么,如果你真的要把上面我所提到的文件都加入黑名单,就安全了吗,也不一定。现在很多服务器都开启了对asp和php的双支持,那么,我是不是可以上传php版的WebShell呢,所以说,黑名单这种被动防御是不太好的,因此我建议你使用白名单的方法,改进上面的函数,例如你要上传图片,那么就检测扩展名是否是bmp、jpg、jpeg、gif、png之一,如果不在这个白名单内,都算作非法的扩展名,这样会安全很多。
4. 表单中传递文件保存目录。
上面的这些操作可以保证文件扩展名这里是绝对安全的,但是有很多程序,譬如早期的动网论坛程序,将文件的保存路径以隐藏域的方式放在上传文件的表单当中(譬如用户头像上传到UserFace文件夹中,那么就有一个名为filepath的隐藏域,值为userface),并且在上传时通过链接字符串的形式生成文件的保存路径,这种方法也引发了漏洞。
FormPath=Upload.form("filepath")
For Each formName in Upload.file ''列出所有上传了的文件
Set File=Upload.file(formName) ''生成一个文件对象
If file.filesize<10 Then
Response.Write "请先选择你要上传的图片 [ <a href=# onclick=history.go(-1)>重新上传</a> ]"
Response.Write "</body></html>"
Response.End
End If
FileExt=LCase(file.FileExt)
If CheckFileExt(FileExt)=false then
Response.Write "文件格式不正确 [ <a href=# onclick=history.go(-1)>重新上传</a> ]"
Response.Write "</body></html>"
Response.End
End If
Randomize
ranNum=Int(90000*rnd)+10000
FileName=FormPath&year(now)&month(now)&day(now)&hour(now)&minute(now)&second(now)&ranNum&"."&FileExt
大家可以看出这段代码,首先获得表单中filepath的值,在最后将其拼接到文件的保存路径FileName中。
在这里就会出现一个问题。
问题的成因是一个特殊的字符:chr(0),我们知道,二进制为0的字符实际上是字符串的终结标记,那么,如果我们构造一个filepath,让其值为filename.asp■(这里■表示空字符,即字符串终结标记),这个时候会出现什么状况呢,FileName的值就变成了filename.asp,再进入下面的保存部分,所上传的文件就以filename.asp文件名保存了,而无论其本身的扩展名是什么。
黑客通常通过修改数据包的方式来修改filepath,将其加入这个空字符,从而绕过了前面所有的限制来上传可被执行的网页,这也是我们一般所指上传漏洞的原理。
那么,如何防护这个漏洞呢,很简单,尽量不在客户端指定文件的保存路径,如果一定要指定,那么需要对这个变量进行过滤,如:
FormPath = Replace(FormPath,chr(0),"")
5. 保存路径处理不当。
经过以上的层层改进,从表面上来说,我们的上传程序已经很安全了,事实上也是这样的,从2004年的动网上传漏洞被指出后,其他程序纷纷改进上传模块,因此上传漏洞也消失了一段时间,但是最近,另种上传漏洞被黑客发掘了出来,即结合IIS6的文件名处理缺陷而导致的一个上传漏洞。
在该系统中,用户上传的文件将被保存到其以用户名为名的文件夹中,上传部分做好了充分的过滤,只可以上传图片类型的文件,那么,为什么还会出现漏洞呢。
IIS6在处理文件夹名称的时候有一个小问题,就是,如果文件夹名中包含.asp,那么该文件夹下的所有文件都会被当作动态网页,经过ASP.dll的解析,那么此时,在有这样的漏洞的系统中,我们首先注册一个名为test.asp的用户名,而后上传一个WebShell,在上传时将WebShell的扩展名改为图片文件的扩展名,如jpg,而后文件上传后将有可能会保存为test.asp/20070101.jpg这样的文件,此时使用firefox浏览器访问该文件(IE会将被解析的网页文件当作突破处理,因为其扩展名为图片),此时会发现我们上传的“图片”又变成了WebShell。
这个漏洞其实是十分有趣的,他不只是简单的asp漏洞,而是结合了IIS的一个缺陷,的确非常的隐蔽。
当然,防御这个漏洞也是很简单的,如果没有必要,那么不要将突破按照用户名分目录保存,如果一定要这样,那么需要检测用户名中是否有非法字符,例如“.”等。
如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛