如果测试网络应用程序的安全问题就像写文章那样简单,那么世界会变得多么美好。最近,有人要求我给出一个网络应用程序常见安全错误的清单。这项工作很简单,下面是我常见到的“陷阱(gotchas)”:
1. 盲目信任通过cookie传递的信息以及通过URL传递的参数
2. 不检查屏幕输入
3. 预验证(Pre-validation)帐号
4. 无约束的用户登录
5. 网络文件夹访问权限设置错误
6. 缓冲敏感信息
7. 安装Web server demo
8. 忘记修改后端数据库的默认口令
9. 忘记安装安全补丁
10. 开放网络管理端口
下面是前五个常见错误的简要介绍:
信任,但也要验证
“信任,但也要验证(Trust but verify)”是我最喜欢的座右铭之一。网络应用程序设计师和程序员应该很好的实践这句座右铭。尽管cookie和URL参数大大简化了开发者的工作,但我们不应该忘记验证它们所传递数据的有效性。
许多网络商务平台从臭名昭著的“购物车漏洞(shopping cart vulnerability)”学会了这个,网上小偷可以通过这个漏洞来修改购物车中的商品价格。而购物车本身不过是一个基于文本的cookie而已。服务器在检验之后,会合计保存在cookie中的条目价格。设想一下:客户完全控制了价格,这种情况会有多可怕。更糟糕的是,服务器没有验证数据的手段。我敢肯定,许多网络商务经历了更严重的冲击。
检查这个问题的最好方法就是清除所有的cookie,运行应用程序,并看看写到磁盘中的cookie。我总是看看cookie的内容,这样可以避免敏感的信息(例如任务(role)甚至用户id和口令)没有保存到cookie中。
命令可以等同于控制
我曾经看到过这样一个系统,它通过URL传递的参数来进行程序控制。当我查看它的源代码后,我注意到了一个普通线程,系统级的命令用如下形式嵌入到URL中:“action=’do something’”。
在测试过程中,我编写了一对自定义URL来看看系统是如何处理它们的。不出所料,通过我在URL中嵌入的命令(“action=’cat xxx/etc/passwd’”)我获得了系统的控制权。
概括的说:如果你通过URL栏来传递参数,那么你至少应当解析它的无效内容和恶意内容。你最好对URL参数设置约束,这样当传递来的参数值在意料之外时,你的程序依然可以处理它。测试你的程序是否存在这个问题也很容易――修改URL栏中的地址,然后看看你的应用程序是如何处理这个数据的。
千万不要忘了验证数据有效性
在测试网络应用程序的过程中,我经常会发现不验证输入数据有效性的字段,它们简直就是缓冲区溢出和SQL injection攻击者的金矿。测试时,我会打开记事本并创建一个长度超过500的字符串,然后把它剪切拷贝到口令输入框中。如果系统没有限制输入字符串的长度,那么它往往会挂起或者崩溃。
然后我将测试验证规则,我会把一个总是等于“真”(例如“OR ‘x’=’x’”)的条件语句附加到口令输入框中。由于通过这种方法,SQL语句已经建立,因此许多系统都会被操控,从而允许未授权的访问――许多系统都会被这种“OR TRUE”语句所欺骗。下面是可以用来控制系统的SQL语句的例子:
Select userid, passwd from USERS where userid = :uid_entered and passwd = pwd_entered
假设用户在useid字段中键入“admin”,在口令栏中键入“OR ‘x’=’x’”,那么这个SQL语句就会解释为“select userid, passwd from USERS where userid=admin and passwd=password OR 'x'='x'”,设计者可能会没有预料到这种情况的出现。
欢迎毯下的钥匙
另一个常见的错误就是把系统帐号作为应用程序数据库的预验证帐号,我常常对这中错误出现的频率之高感到吃惊。许多网络应用程序把用户凭证(也就是用户名和口令)保存到自己的数据库中。在验证用户凭证的有效性之前,你必须登录到数据库,因此,系统一般用一种我称之为“预验证登录帐号”的方法来处理验证;例如,当以“admin/admin”登录系统时,就会验证屏幕输入与数据库中保存的用户名和口令是否相匹配。
值得注意的是,我发现每个预验证登录帐号总是管理员类型的帐号,这些帐号具有最大权限。使得情况更具有风险性的是:为了让网络应用程序可以访问帐号和口令,数据库一般保存到Webroot目录中或者起始页(start page)下的目录中。无论是上述两种情况下的哪一种,恶意用户都能很容易获得口令。这种情况非常像把房门钥匙藏在欢迎毯下,或者把备用的车钥匙放在汽车的遮阳板上。这个错误很严重,它的存在有利于恶意用户闯入网络程序。
右拐还是左拐
我还喜欢做这样一种测试:以管理员的身份合法登录,打开任何管理页面(例如,“添加新用户页”),然后注销登录。当会话终止(通过打开一个浏览器来确认)后,点击这个书签。令人吃惊的是,应用程序往往自动授以我管理员权限。
另一个技术就是查看以及被注释掉但还没有从基线(baseline)中删除的死代码。在这种情况下,我会以客户(或者任何权力有限的用户)身份登录,并尝试浏览死代码。重复一次,许多死代码依然保存在基线中。
开发者常常会在开发阶段建立一个起始页面(这个页面不会被配置)――它略去了登录过程并建立起一个测试环境。在发布系统的时候,网络程序员一般会把注释掉这个初始调用,或者把这个测试页面重命名后依然把它留在Webroot目录下。
我会检查这些代码来判定是否存在多个登录也就是启动页面,并看看其中是否有一个可以让我以管理员身份访问系统而无需提供任何凭证。我还会尝试在controls外围登录,特别是在开发者提供导航指南的情况下。如果不清除Internet临时文件的话,它们就提供了有用信息。如果应用程序清楚的希望我往右走,我就试着往左走,看看它是否有防止用户背道而驰的机制。
权限设置
总的来说,开发者不需要为访问权限设置错误负责――除非应用程序构建在访问权限基础上。例如,如果一个网络应用程序要求有一个可以进行全局可写的特定的目录(更糟糕的情况是这个目录的权限设置为所有人都是可读、可写和可执行),这样应用程序提供一个极好的可以隐藏(也有可能是触发)恶意逻辑的场所。
许多应用程序都有存储临时报告的目录。我常常会修改URL来浏览该目录,这样我就知道了该目录的权限设置。如果应用程序提供了ad hoc查询功能(为了存储查询结果,这些目录一般是全局可写的),我就会尝试传送一个可执行文件到该目录,然后通过浏览器来调用它,看看它是否执行。
如果应用程序提供了任何上载功能,我就会检查执行权限。应该很少人有网络目录下文件的执行权限;用户也不应该获得服务器上可运行程序的执行权限。如果我能在应用程序外shell(这种情况常常发生),那么任何产生的过程都属于某个优先帐号(例如“oracle”、“root”或者“system”)并拥有它所属帐号的权限。如应用程序应当适当控制对上载数据的访问,否则就会潜在产生问题。另一个常见错误就是对上载目录的权限设置太弱。
避免漏洞
尽管我列举的清单没有包含所有情况,但它包含开发者在构建基于网络的应用程序的过程中的常见错误。网络上有许多非常棒的资源,开发者和测试者可以从中获得关于常见漏洞的更多信息。我强烈建议所有的开发者都阅读OWASP2004报告。你也应该读读SANS的TOP20列表。尽管它并没有针对网络应用,但它会教给开发者防范漏洞的意识。在这些知识的武装下,你应该能够避免绝大部分常见的陷阱。