返回介绍

5.1 认证

发布于 2024-10-10 22:16:32 字数 37673 浏览 0 评论 0 收藏 0

认证是指通过某些方法验证系统用户身份的行为。Web 应用程序里使用的认证方法除了在第 3 章里已经介绍过的 HTTP 认证之外,还有使用 HTML Form 的用户名和密码的认证方式,以及利用客户端的 SSL 证书的认证方式等。本书将主要针对 HTML Form 认证进行说明。

这一节将从以下几个方面来说明当认证功能存在漏洞时将会面临的威胁以及可以采取的对策:

  • 登录(Login)功能
  • 针对暴力破解攻击的对策
  • 密码的保存方式
  • 自动登录
  • 登录表单(Form)
  • 错误消息
  • 退出登录功能

5.1.1 登录功能

用户登录功能可以称为是认证处理中的核心,即通过对照用户输入的 ID、密码是否和数据库一致,若信息一致即认证成功。本书中把这种用户本人合法性验证的功能称为登录功能。

登录功能通常是通过执行类似下面的 SQL 语句,从数据库中检索满足用户 ID 和密码条件的记录,如果找到了相应的用户记录,就可以认为是登录成功了。

SELECT * FROM usermaster WHERE id=? AND password=?

针对登录功能的攻击

如果攻击者成功攻破了登录功能,就可以伪装成正常用户使用系统了。本书中将把这种攻击称为非法登录。认证功能的攻击有如下几种比较典型的案例。

  • 通过 SQL 注入攻击来跳过登录功能

    如果登录页面存在 SQL 注入漏洞,攻击者即使不知道用户的密码,也可以利用漏洞,跳过登录功能从而成功登录。关于 SQL 注入的内容在 4.4.1 节已经介绍过了,本章不会再进行更深入的讨论。

  • 通过 SQL 注入攻击获取用户密码

    同样,如果应用中存在 SQL 注入漏洞,则保存在数据库中的用户 ID 或者密码有可能被盗取。攻击者一旦拿到了这些用户的 ID 和密码,就可能冒充用户登录。

    不过即使攻击者利用 SQL 注入漏洞盗取了用户的密码,我们也有办法让攻击者无法利用这些数据进行攻击。具体对策我们将在 5.1.3 节中说明。

  • 在登录页面进行暴力破解

    还有一种攻击方法是在登录页面不断地尝试使用各种用户 ID 和密码的组合来进行登录,具体方法包括暴力破解和字典破解等。

    暴力破解(Brute Force Attack)使用的方法是利用所有的字符组合作为密码来进行尝试。

    字典破解是事先准备一个“字典”,其中都是常被用户作为密码使用的字符组合,然后按顺序尝试字典里保存的密码组合(见图 5-1)。

    图 5-1 反复尝试各种用户 ID 和密码组合进行攻击

    不管利用上面哪种攻击方法,都需要尝试大量的用户 ID 或者密码组合,所以我们可以在登录功能中检测这种攻击并采取相应的预防措施,具体内容将会在 5.1.2 节中详细说明。

  • 通过社会化攻击得到用户密码

    社会化攻击(也称为社会化工程攻击,Social Hacking),指的是并不对计算机或者软件发起攻击,而是通过对欺骗用户,获取重要信息的攻击方法。典型的方法比如冒充领导或者服务器管理员给用户打电话,欺骗用户说“由于某项业务需要,请告知密码”,从而骗取用户的密码的行为。

    此外,还有一种攻击方法是通过偷窥用户在输入密码时的页面或者键盘敲打来盗取密码,叫作 Shoulder Hack,这也是社会化攻击的一种。

    Shoulder Hack 如果从字面意思来看的话有从用户背后探头窥视的意思,但是实际上即使不用伸出头,如果采用其他方法能窥视到的话,也能得到用户的密码。在本书中,我们将把通过偷窥得到用户密码的攻击行为统一称为 Shoulder Hack。应对 Shoulder Hack 攻击,可以采取遮盖密码输入框等方法,详细的内容请参考 5.1.5 节。

    从 Web 应用程序本身来说,对于 Shoulder Hack 以外的其他社会化攻击就显得无能为力了。我们可以通过对员工、用户进行教育强化的方法来应对攻击,但这超出了本书的范围,就不在这里详述了。

  • 通过钓鱼方法获取密码

    钓鱼(Phishing)是指通过搭建和真实网站非常相像的山寨网站,诱骗用户输入密码等来获取个人信息的方法。这也是社会化工程(Social Engineering)的一种。在国外,频繁报道了大规模的钓鱼事件,在日本也报道过用户在山寨 Yahoo!JAPAN 和银行等钓鱼网站上受骗的案例。

    预防钓鱼本首先是用户需要提高警惕,同时作为 Web 网站也应采取相应对策,我们将在 7.2 节里说明。

登录功能被破解后的影响

如果攻击者攻破了 Web 应用程序以他人名义非法登录,那么攻击者就拥有并且可使用用户的所有权限,比如阅读信息、修改、删除以及购物、转账、发帖等。

非法登录带来的破坏性与会话劫持是一样的,如果密码被攻击者知道了,有一些需要密码再次输入确认的功能都能被攻击者恶意使用(会话劫持则不能达到此目的)。

另外,会话劫持攻击一般都是被动攻击,攻击时需要用户发起某些活动,攻击者才能参与进来。而非法登录是一种主动攻击,不需要用户的参与。因此非法登录会对更多的用户产生影响。

综上所述,非法登录的影响比远超于会话劫持,属于重大安全隐患,需要制定万全的对策。

如何防止非法登录

在使用表单认证(或者叫密码认证)的应用里,为了防止非法登录,需要做到以下两点。

  • 确保系统不存在 SQL 注入等安全性 Bug
  • 使用难以猜测的密码

下面依次对这两点进行说明。

  • 确保系统中不存在 SQL 注入等安全性 Bug

    用户登录功能容易存在的安全隐患有以下几种 1 :

    (A)SQL 注入(4.4.1 节)

    (B)固定会话 ID(4.6.4 节)

    (C)Cookie 的安全属性设置不完善(4.8.2 节)

    (D)自由重定向漏洞(4.7.1 节)

    (E)HTTP 消息头注入(4.7.2 节)

    (A)的 SQL 注入漏洞之所以容易发生,是因为在一般的用户登录实现中都需要利用 SQL 在数据库中进行用户名密码比对。

    (B)和(C)是用户登录认证后,在 Cookie 里保存会话 ID 时存在安全问题时所带来的安全隐患。

    (D)和(E)虽然和用户认证没有直接关系,但是用户登录后,多数应用需要跳转到登录前的页面,结果导致登录功能经常出现此类安全隐患。

    下面介绍一下有关密码预测难度的问题。

  • 设置难以猜测的密码

    密码认证的前提是“知道此密码的人只有合法用户”。基于这个前提,可以判断“只要某个人知道了密码,即可认为他就是合法用户”,但是如果其他人可以推测出此密码,那这个前提就不存在了。

    所以,最基本的是我们需要确保用户的密码不能被其他人猜测到。比如在 4.6 节里提到的那样,如果使用类似密码学级别的伪随机数生成器的话,基本可以生成不能被猜测到的密码。

    但是密码是需要用户自己输入的,程序生成的随机密码不容易记住,输入也很麻烦,所以实际上用户更多的是选择即好记也方便输入的字符串作为密码。

    一般来说,用户使用便捷性(好记、输入方便)和系统安全强度(猜测的困难程度)如图 5-2 那样成反比关系。如果用户能在选择密码时能深思熟虑的话,应该可以选出密码安全度高,又能兼顾到用户使用方便性的密码。

    图 5-2 密码的使用便捷性和安全强度的关系

  • 密码的字符种类和长度要求

    在设置一个不易被他人猜到的密码时,最基本的要素就是密码所使用的字符种类以及密码的长度。因为字符种类和密码长度决定了可以作为密码使用的字符串的总数量。

    密码组合总数 = 字符种类的总数 ^ 密码位数

    这里“^”是幂乘运算符。字符种类的数量指的是可以使用的字符的总数量,比如只用数字就是 10,只用小写英文字母就是 26 个等。表 5-1 显示的是根据可使用的字符种类及密码长度得到的各种可能的密码组合的总数。

    表 5-1 密码总数

    字符种类数量4 位6 位8 位
    10 种(数字)1 万100 万1 亿
    26 种(小写英文字母)约 46 万约 3 亿约 2000 亿
    62 种(大小写英文加数字)约 1500 万约 570 亿约 220 兆
    94 种(字母、数字加上各种符号)约 7800 万约 6900 亿约 6100 兆

    从上面的表中可以看出,字符种类和密码位数即使只是稍微增加某一项的值,密码组合的总数都将会大幅增加。

  • 密码的使用现状

    然而现实中用户使用的密码并没有表 5-1 说明的那样多,其原因就是用户更愿意使用好记和好输入的密码。也就是说,用户更倾向于使用如图 5-2 中右下角所显示的密码类型。

    媒体已经报道了很多能佐证这种倾向的统计数据,我们介绍其中的一些报道 2 。下面的报告都是基于非法获取的用户密码数据作出的统计分析。

    通过这些报告我们很容易看出用户在满足密码限制条件的前提下,更愿意使用简单的密码。估计在密码限制条件为“长度在 10 个字符以上,大写字母、小写字母、数字和符号至少包含一个以上”的网站里,用户使用最多的密码就是“Password1!”吧。

    在这种密码使用情况下,如何让用户设置更安全的密码,这正是网站运营方需要彰显智慧的地方。

  • 应用程序设计中关于密码的需求

    这一节我们将整理一下在应用程序设计中需要考虑的和密码相关的需求问题。

    设置安全密码的最终责任在于用户本人,对应用程序来说最低需求是“不能妨碍用户选择安全的密码”。换句话说,就是不要超出实际需求,设置过于严格的字符种类和密码位数的限制。

    应用程序关于密码中使用的字符种类和长度要满足最低需求,典型的有下面一些组合:

    • 字符种类:英文字母和数字组合(区分大小写)
    • 位数:最多可输入 8 个字符

    但是有人可能觉得上面的限制太局限了,实际上我们也没有必要必须采用这样严格的限制,所以可以考虑下面的组合:

    • 字符种类:所有 ASCII 字符(0x20~0x7E)
    • 位数:128 位以内

    如果放宽密码的字符种类和位数的限制,那么用户可能不只是使用简单的密码,还可能会使用密码短语(Passphrase)。密码短语取代简单的单词,使用若干的词组(Phrase)组成比较长的短句作为密码使用。

    以上我们所说明的内容,是对密码的“容器”的要求。即应用程序准备了一个大的容器(即可使用的字符种类和密码位数),用户自己负责,自由地选择自己的密码。但是,现实中广泛使用的密码很多都是比较容易猜测和攻破的密码,因此越来越多的网站除了限制密码使用的字符和位数外,还对具体的密码内容进行检查、核对。

  • 严格的密码检查原则

    为了预防用户密码被攻击,Web 应用应该积极采取密码检查功能。其基本原则,有以下几种:

    • 关于字符种类的检查(比如字母、数字、符号至少各一个)
    • 关于密码位数(比如至少 8 位以上)
    • 禁止使用和用户 ID 一样的密码
    • 禁止使用密码词典里有的词汇做密码

    Twitter 就使用了基于密码字典的密码可用性检查,比如图 5-3 是 Twitter 用户修改密码的页面 3 ,在新密码输入框里面输入“password”后的截图。

    图 5-3 Twitter 修改密码界面

    页面里显示了“密码过于简单”的错误信息,这时如果坚持点击“修改”按钮的话,则会出错。

    也许这样的检查有点过于严格,甚至引起人们质疑其违反了“密码选择是用户的责任”这一原则,但是反过来说这一措施有效避免了用户使用过于简单的密码。

1 当然,这句话的意思并不是说系统就不存在其他安全隐患。

2 这些报道都是笔者在 2010 年 10 月 20 日查阅过的。

3 https://twitter.com/settings/password

5.1.2 针对暴力破解攻击的对策

针对在线暴力破解攻击,账号锁定是一种有效的对抗方式。我们身边账号锁定最常见的例子就是银行卡,如果交易时连续 3 次输入错误密码,卡片就会被冻结。这样可以有效地防止银行卡被盗或者被别人捡到后非法使用。账号密码也一样,如果输错密码超过了一定次数,该账号也应该被锁定。

初步认识账号锁定

在 Web 应用程序里基本的账号锁定功能可以这样来实现:

  • 记录每个用户 ID 的密码连续错误次数
  • 如果密码错误次数超过了一定上限,则锁定此账号 ;被锁定的账号不能再次登录
  • 账号被锁定后,通过邮件等方式通知该用户和系统管理员
  • 用户正常登录后,清除之前记录的密码错误计数器

如果和 ATM 一样,最多只允许用户输入 3 次错误密码,可能有点太少,会导致用户账号频繁被锁定。所以这个次数设为 10 次比较合适 4

4 在面向信用卡加盟商的安全标准 PCI DSS 2.0( https://zh.pcisecuritystandards.org/minisite/en/pci-dss-supporting-docs-v20.php )7.5.13 节规定了“在尝试 6 次后锁定用户,阻止用户反复尝试访问”。像 PCI DSS 一样,如果系统需要遵守的标准明确规定了账号锁定策略的话,我们需要按照该策略规定去实现。

另外,如何给被锁定的账号解除锁定,可以参考下面的规则:

  • 账号被锁定后 30 分钟 5 ,自动给该用户解锁
  • 管理员利用某些方法对用户进行验证后给该用户解锁

5 在 PCI DSS 2.0 标准里也规定为 30 分钟(7.5.14)。

之所以选择 30 分钟后给用户解锁,为的就是能让正常用户能尽早登录进来。也许有人会觉得 30 分钟有点太过短暂,但实际上 30 分钟是比较合理有效的。

尝试 10 次输入密码错误后再经过 30 分钟等待解锁,这样攻击者为了验证 100 个密码需要四个半小时以上的时间,而且还会给系统管理员发送 10 次账号被锁定的系统通知。在这段时间里,管理员可以详细调查用户被锁定的情况,且根据需要,甚至可以封掉攻击来源的 IP。

暴力破解攻击的检测和对策

目前暴力破解攻击的变种有以下几种攻击方法。

  • 字典攻击

    字典攻击不是尝试所有理论上可能的密码组合,而是只选取使用频率较高的密码按顺序进行尝试。由于现实中很多人在使用比较简单且危险的密码,所以这个方法比单纯的暴力破解效率更高。

    和暴力破解一样,针对字典攻击采用账号锁定是比较有效的。

    图 5-4 字典攻击的例子

  • Joe 账号检索

    用户 ID 和密码相同的账号称为 Joe 账号(Joe account),如果在应用程序里不禁止这种用户 / 密码组合,那么系统里就可能存在一定比例的 Joe 账号。Joe 账号检索的例子可以参考图 5-5。

    单纯的账号锁定不能解决 Joe 账户检索攻击,具体对策将在后面进行讲解。

    图 5-5 Joe 账号检索的例子

  • 逆向暴力破解

    通常的暴力破解是针对固定的用户 ID,采用不同的密码尝试登录。与此相反,逆向暴力破解(Reverse Brute Force Attack)则是固定密码,轮换不同的用户 ID 组合进行尝试。图 5-6 是使用固定密码“password1”进行逆向暴力破解的例子。

    针对逆向暴力破解,账号锁定对策也无能为力。关于其对策我们会在下面一节进行说明。

    图 5-6 逆向暴力破解的例子

  • 针对变种暴力破解的对策

    在 Joe 账号攻击和逆向暴力破解攻击面前,单纯的账号锁定功能没有效果。但是,对这种攻击必须要采取相应对策。

    比如,通过统计 MySpace 的密码后发现,用户使用最多的密码是 password1,占统计对象总数的 0.22%。这个数字看起来很小,但是如果用 password1 做密码进行逆向暴力破解的话,尝试 1000 个用户平均就能成功登录 2 次。这种攻击的成功率是非常高的 6 。

    所以,必须要对这种攻击采取必要的措施。但是就现实情况来说,还没有什么特别有效的对策。下面列出一些辅助措施。

    • 严格检查密码

      前面我们已经说明过了,在用户注册时根据字典检查用户输入的密码,如果密码是很多用户常用的密码,或者密码和用户 ID 一样,就拒绝用户注册。这样就能完全杜绝 Joe 账号问题了。另外,即使攻击者使用逆向暴力破解,由于其采用的密码都是平常被大量使用的密码,而如果这样的密码在系统中被禁用的话,攻击者的成功率也会大大降低。

    • 隐藏登录 ID

      这种方法是指系统中除了保存对外公开的昵称之外,还另外保存非公开的用于登录的用户 ID。具体例子包括将用户的电子邮箱地址作为登录 ID。SNS 巨头 mixi 或 GREE、facebook、MySpace、EC 巨头 Amazon 等,都使用电子邮件作为用户的登录 ID7 。另一方面,Twitter 除了支持使用电子邮件登录之外,也支持用公开的用户 ID 登录。所以 Twitter 不能有效地预防逆向暴力破解。

      使用电子邮件作为登录 ID 时,需要考虑到用户变更邮箱地址的需求,所以最好在内部保存一个唯一的 ID 来标识一个用户。

    • 监视登录失败率

      发生密码暴力破解攻击时,登录失败率(单位时间内登录失败次数 ÷ 尝试登录总次数)一般都会激增。所以如果定时检测登录失败率,管理员就可以在失败率激增的时候调查其原因。如果是遇到攻击了,管理员可以通过封掉远程 IP 地址等必要的措施进行处理,这也是一种有效对策。

    • 各种对策方法的比较

      下面总结一下到目前为止讲过的各种对策的优缺点。

      表 5-2 解决变种暴力破解攻击的各种对策的优缺点

       优点缺点
      严格检查密码实现、部署简单需要花费精力去创建和维护密码字典 / 不能算作完美的对策
      隐藏登录 ID实现、部署简单已有的应用需要变更服务设计,实现起来有一定难度。
      监视登录失败率对所有密码攻击都有效果需要有监视人员,运行成本较高 / 有可能不能做到即时响应。

      上面所列举的这些措施能有效地提高系统的安全性,但不一定能有效应对其漏洞。在项目规划阶段,需要综合网站性质、对安全性的要求、项目成本等方面综合考虑,再决定是否需要实施这些对策。

6 也许会有人觉得这种攻击很简单,且成功率很高,所以也想尝试一下,但是即使不是出于恶意,只要拿着别人的账号密码登录了,就是触犯法律的行为,请不要在非实验环境做这样的测试。

7 不确定是否考虑到安全上的问题才这样设计。

5.1.3 密码保存方法

在这一节里,我们将讲解为什么需要在保存密码时对其进行加密保护,以及可以采用的具体方法等。

保护密码的必要性

如果由于某些原因导致用户密码泄露,那么就有可能导致用户密码被恶意使用,从而给用户造成损失。一旦密码泄露,很可能导致其他机密信息也泄露出去,甚至会导致信息泄露之外的损失。

  • 利用该用户权限进行购物、转账等
  • 利用该用户权限进行信息发布、篡改、删除
  • 如果用户在多个网站使用同一密码的话,损失也会波及到其他网站

因此,为了防止网站因为 SQL 注入漏洞等导致数据库信息泄露时不让攻击者能恶意使用保存在数据库中的密码,就需要对密码采取保护措施。

典型的密码保护方法包括加密和信息摘要(Message Digest)(也称密码学级别的散列值,Cryptographic Hash)

下面将讲解安全的密码保存方法。

利用加密方式进行密码保护及其注意事项

一般来说用来开发 Web 应用的编程语言都会提供用来加密的库,密码的加密、解密从编码学的角度来说都不是什么困难的事情。但是,实际进行加密的时候,有以下几个问题需要考虑。

  • 选择安全的加密算法
  • 如何生成 key
  • 如何管理 key
  • 加密算法退化(Compromise)后的再次加密 8

8 加密算法的退化是指加密算法被破解,或者随着计算机性能的提升,暴力破解等变得更容易实现等情况。这里所说的再加次加密是指先用之前已经退化的算法进行解密,再使用新的安全的加密算法进行加密。

这其中最难的问题是 key 的保管方法。由于每次登录都需要 key,所以只把 key 锁在安全的保险箱里是不可行的。而且既要确保 Web 应用能正常使用 key,还要确保 key 不会被盗取,这样的系统本身很难实现。退一步说,如果能找到理想的管理 key 的方法,那么也可以用这种方法直接来管理密码。

所以,现实中几乎不采用可逆加密的方式来保护密码,更多情况下是采用下面将要说明的信息摘要的方式。

专栏:数据库加密和密码保护

现在市场上有能将整个数据库进行加密的产品。其中大部分的产品都可以称为“透明数据加密”(TDE,Transparent Data Encryption),即应用程序开发可以不用考虑加密功能的存在。

使用 TDE 的时候,应用程序只是使用普通的 SQL 语句,数据库引擎则将数据加密后进行存储。使用 SELECT 等语句进行数据检索时,TDE 会自动对加密的数据进行解密操作。

使用 TDE 数据库虽然很简单,但是它并不适合进行密码保护。其原因是它不能防御类似 SQL 注入这样的攻击。因为 TDE 的透明加密的关系,SQL 注入后得到的数据都是被解密后的明文字符串了。

TDE 数据库在数据库的文件、备份存档等被盗的情况下,可以有效保护数据库内容不会泄露。

利用信息摘要来进行密码保护及其注意事项

这一节我们将会对采用信息摘要进行密码保护的方法进行说明。

  • 什么是信息摘要

    能将任意长度的数据(bit 数组)压缩为固定长度(信息摘要,或者叫作散列值)的函数叫作散列函数,满足安全上要求(参考后面的专栏)的散列函数叫作密码学级别的散列函数(Cryptographic Hash Function)。在后面的章节中我们将简称为散列函数。

    我们下面来看一下几个信息摘要的例子。手头有 SSH 客户端软件的用户可以登录本书中实验用的虚拟机,然后输入下面带下划线部分的命令。输入命令行的下一行白底黑字的内容是 MD5 散列函数的输出结果。

    程序示例 使用 md5sum 进行散列值计算

    其中“echo -n”是在 echo 内容后不输出回车符,md5sum 是用来对给定文件或者标准输入进行散列值计算的命令。

    上面的例子分别对“password1”和“password2”做了散列值计算,从计算结果可以看出,虽然这两个字符串只有一个字符不一样,但是计算出来的结果却相差甚远。

    专栏:密码学级别的散列函数需要满足的要求

    • 原像计算困难性(Pre-image Resistance)

      原像计算困难性是指在现实的可接受时间内从散列值反推出原内容的困难程度。原像计算困难性也叫作单向性。

    • 第 2 原像计算困难性(Second Pre-image Resistance)

      第 2 原像计算困难性是指给定原数据,在现实的可接受时间内找出相同散列值的其他数据的困难程度。第 2 原像计算困难性也称为弱耐冲突性(Weak Collision Resistance)。

    • 冲突困难性(Collision Resistance)

      冲突困难性是指找出拥有相同散列值的两个不同数据的困难程度。原数据之间并没有什么关 联,条件是散列值相同即可。冲突困难性也称为强耐冲突性(Strong Collision Resistance)。

      广泛使用至今的 MD5 散列函数已经被证明是不满足强耐冲突性性的,可以说弱耐冲突性被攻破也只是时间的问题。但是如果仅用作保护密码安全的话,能保证原像计算困难性已经足够了。也就是说,MD5 散列函数还是能继续作为保护密码安全之用的。

      但是根据目的去选择合适的散列函数可能会比较困难,如果选择不当,还可能带带来安全隐患。所以我们可以不用考虑具体的使用场景,而是选择那些通用的、安全的散列算法就可以了。比如 SHA-256 就是一个不错的选择。

  • 利用信息摘要保护密码

    图 5-7 简单说明了使用信息摘要的密码保存和验证的方法。如图所示,数据库中保存的不再是密码原文而是其散列值,登录时验证的也是原密码的散列值。

    图 5-7 利用散列保存和验证密码

    之所以对密码原文采用信息摘要能保护密码安全,是因为散列函数具有下面的特性。单向性和冲突困难性的详细定义请参考之前的专栏。

    • 不能从散列值倒推出明文密码(单向性)
    • 不同的密码生成相同的散列值的概率非常低(冲突困难性)

    尽管散列函数满足安全性上的那些需求,但是由于密码的字符种类和长度都是有限的,所以还是有一些方法能实现根据散列值得到原来的密码。这里我们选择其中的 3 种方法来介绍一下。

  • 威胁 1:离线暴力破解

    在这之前我们已经说过了散列函数不能从散列值得到原来的数据,但是那只适用于一般情况,对于密码来说就不合适了。由于在密码中使用的字符种类有限,且长度也有限,所以有时候通过暴力破解是可以得到原密码的。

    另外,对散列函数还有一个要求就是处理速度要足够快,因为散列函数的典型利用场景是为 DVD-ROM 等巨大 ISO 文件做信息摘要,计算其散列值的。考虑到我们会频繁地使用散列函数来计算散列值,如果计算过程花费时间过长的话,甚至可能会对系统性能产生影响。所以散列函数处理速度是越快越好。

    但是散列函数速度过快对密码做信息摘要来说有可能是一种灾难,因为处理速度提高了,也使得暴力破解的效率变高,增加暴力破解成功的可能性。

    这种攻击在从散列值反推出原文的时候并不需要连接到服务器(Offline),所以也叫作离线暴力破解攻击。

    下面介绍下笔者做的一个小实验的结果。在实验中使用了 md5brute9 这个用来从 MD5 散列值查找原文的工具。在长度是 8 位的小写英文字母这一测试条件下,查找“zzzzzzzz”的散列值。按字母顺序排列的话,这个字符串排在最后。

    运行实例 从散列值倒推出原字符串的例子

    在系统配置为 Pentium Dual-Core 2GHz 的机器上,如果只使用单核(Core)进行测试的话,大概只需要花费 40 个小时就能成功地查找到该散列值对应的密码原文。平均算下来大概一个小时能进行 138 万次散列值计算。

    基于此实验数据,如果用大小写英文字母加上数字作为密码,长度为 8 位的话,需要大概 5 年才能找到原文。5 年看上去时间很长了,但是如果使用 676 核的集群 10 的话,只需要 3 天就能破解出原文了。

    也就是说,如果密码长度在 8 位以下的话,以现在的 CPU 能力来说,还是可以在可接受范围内从散列值得到密码原文的。而且破解并没有利用 MD5 的漏洞等,其他的散列函数(SHA-1 或 SHA-256)也存在同样的问题。

    上面只是暴力破解的例子,利用字典攻击也能得到散列值的原字符串,如果该字符串已经在字典里存在的话,甚至可以瞬间(1 秒以内)破解。

    后来,为了更高效的从散列值得到原文,有人发明了利用彩虹表进行破解的方法。

  • 威胁 2:彩虹破解(Rainbow Crack)

    我们还可以考虑另一种办法来提高从散列值得到密码原文的效率,即预先使用暴力破解的方法生成一个散列值查找表,解析原密码的时候如果能查询到这个表的话,就能实现高速的密码解析工作。但实际上由于密码组合数量庞大,基本上创建这样一个查找表是很很困难的。

    然而到了 2003 年,一种基于彩虹表(Rainbow Table)的方法出现了,它使得创建一个可接受大小的查找表成为可能。后面的“参考:彩虹表原理”小节有针对彩虹表的详细说明,各位读者可以参考。这里我们通过实验来看一下彩虹表破解是如何利用彩虹表来解析出密码原文的。

    彩虹表需要为不同的字符种类和字符串长度创建不同的查找表,如果字符种类或字符串长度增加,彩虹表大小也会急速变大。笔者手头上的彩虹表是适用于密码为 7 位以下的小写字母加数字的密码,我们将使用这个彩虹表来进行实验。这里使用的工具 rcrack.exe 可以从 RainbowCrack Project11 下载,

    运行实例 Rainbow Crack 的例子

    从上面的例子可以看出,Rainbow Crack 只用了 45 秒就把密码原文给解析出来了。相比之下同样的散列值,之前介绍的 md5brute 用了 997 分钟,使用彩虹的表速度足足是 md5brute 的 1300 倍之多。不过这个实验对 md5brute 来说有点不公平(我们使用的明文字符串排在所有字符组合的最后),实际用起来应该不会有这么大的差别,但是这也充分说明彩虹表的高效性和实用性了。

    RainbowCrack Project 的主页也在出售彩虹表数据,比如用户 MD5 算法的彩虹表,我们可以买到 8 位以下所有 ASCII 字符,以及 10 位以下小写字母加数字的彩虹表 12 。从理论上说,如果密码只是简单地进行散列处理后保存的话,我们用这些彩虹表就可以在很短的时间内破解出原密码。13

    抵御彩虹表攻击的最简单的方法就是增加密码长度,现在能获取的彩虹表支持的最大长度都只有 10 位左右,如果我们把密码长度设置为 20 个字符以上,就可以预防用目前的彩虹表破解原密码的问题。但是强制用户使用 20 多位的密码也有点不太现实,所以可以采用后面将要介绍的加 salt 取散列值的对策。

  • 威胁 3:在用户数据库里创建密码字典

    也许我们会觉得如果使用攻击者未知的散列函数,攻击者应该就没有办法计算出原密码了,但是实际上即使攻击者不知道保存密码时所使用的散列函数,也有其他方法能解析出原密码。

    这种方法就是通过在被攻击目标系统里注册大量的僵尸(Dummy)用户,在系统的数据库里制作一个“密码字典”出来。图 5-8 是这种攻击的大概流程。

    图 5-8 在 Web 应用数据库中创建密码字典

    如图 5-8 说明的那样,攻击者首先在攻击对象系统中注册大量的虚假账号(①),然后再利用其他方法(比如 SQL 注入攻击等),盗取系统的用户数据库(②)。在取得的用户数据库里,查看保存散列密码的那一列的数据,寻找具有和在①里注册的用户相同散列值的记录,在图 5-8 的例子里,用户 saburo 和 evil2 的密码散列值相同(③),因为 evil2 的密码是 123456,所以可以断定 saburo 的密码也是 123456(④)。

    针对这种攻击,加盐也是一种有效的防御手段。

  • 如何防止散列值被破解

    很多人认为将密码作为散列值的形式保存起来就安全了,但实际上有各种各样的方法可以破解散列密码,在上面我们已经介绍过了。之前介绍的方法都是恶意利用特定的散列函数(比如 MD5)的特点及漏洞等。只要是使用算法公开的散列函数,基本上都会面临同样的问题。

    之前介绍的方法,都是密码组合模式数量不是特别大的情形下发生的破解,如果使用 20 位以上的随机数的话,我们可以认为基本上密码不会被破解。但是这样的密码使用起来非常地不便,现实中也不可能被采用,现实中使用最多的密码长度在 8 位左右,所以我们要寻找防止散列值被破解的方法。

    基本的防止散列值被破解方法有下面两种:

    • salt(加盐)
    • stretching(延展计算)
  • 对策 1:salt(加盐)

    salt 指的是在原本要散列的数据后面追加的内容。加上了 salt,除了看上去密码字符串会变长之外,还因为每个用户的 salt 都不一样,所以即使两个用户的密码相同,也能为这两个用户的密码生成不同的散列值。

    安全的使用 salt 需要满足以下条件:

    • 确保有一定的长度
    • 每个用户使用不同的 salt

    这其中“一定长度”的说法可能有些模棱两可,实际上考虑到对抗彩虹表攻击,salt 和密码加起来的长度至少要保证在 20 位以上。

    不同用户使用不同 salt 的原因,是让使用相同密码的用户也能生成不同的散列值。为不同用户生成不同的 salt 有两种方法。

    • 使用随机数作为 salt
    • 使用以用户 ID 为输入参数的函数来生成 salt

    很多教材中都推荐使用随机数作为 salt 使用,因为使用随机数作为 salt 的话,必须将 salt 也保存在数据库里。如果不知道 salt 的话,就不能验证密码是否正确了。

    另一种方法,如果使用以用户 ID 为输入参数的函数的话,就不需要保存 salt 了,这是该方法的一个优点,和随机数比较起来,该方法没有明显的缺点。因此本书里比较推荐使用基于用户 ID 的函数来产生 salt 值。salt 的实现例子可以参考后面的实现示例。

  • 对策 2:stretching(延展计算)

    即使使用 salt,也不能降低暴力破解带来的危险。因为即使加上 salt,也不会影响计算散列值所需要的时间。为了对抗暴力破解,需要让散列计算处理速度变慢。

    stretching(延展计算)是一种利用现有的 MD5 或者 SHA-1、SHA-256 等散列函数,想办法增加计算散列值所需要时间的一种方法。它通过反复递归的调用散列函数来增加计算时间。具体的实现方法请参考下一小节。

  • 实现示例

    下面的脚本是在上文的基础上,用来计算散列值的一个示例。

    代码清单 /51/51-001.php

    <?php
      // FIXEDSALT 要根据实际情况进行修改
      define('FIXEDSALT', 'bc578d1503b4602a590d8f8ce4a8e634a55bec0d');
      define('STRETCHCOUNT', 1000);
     
      // 生成 salt
      function get_salt($id) {
        return $id . pack('H*', FIXEDSALT);  // 将用户的 ID 和固定字符串连接起来
      }
    
      function get_password_hash($id, $pwd) {
        $salt = get_salt($id);
        $hash = '';  // 默认的散列值
        for ($i = 0; $i < STRETCHCOUNT; $i++) {
          $hash = hash('sha256', $hash . $pwd . $salt); // stretching
        }
      return $hash;
    }
    // 调用示例
     
    var_dump(get_password_hash('user1', 'pass1'));
    var_dump(get_password_hash('user1', 'pass2'));
    var_dump(get_password_hash('user2', 'pass1'));
    
    

    执行结果

    string(64) "a44812a099b40ee49ffe2bd6c5de7403a1854e009ba9e2b417b9770d4ffac54b"
    string(64) "cc2c26c9a22d7318f48ed99e8915c6861559ade98e4df3dab64e51c7ea476389"
    string(64) "3fca4aab6f7bf9ed2ac855dbc0e22c148e7e23a137c497777e1e9269902571c8"
    
    

    get_salt 方法的输入参数为用户 ID,返回值是用于散列计算的 salt。例子里只是简单地将用户 ID 和固定字符串用 pack 函数从十六进制转换为二进制数据后连接在一起。通过使用二进制数据,可以达到增加字符种类的效果。

    get_password_hash 方法里面将密码原文和 salt 连起来后用 SHA-256 算法进行了 1000 次的计算 14 ,这里之所以使用 SHA-256,是因为它是目前来说比较安全的用在密码保存及其他领域的散列函数之一。

    如果每次调用 get_password_hash 方法时传递的 $id$pwd 参数的值都是一样的话,这个方法的返回值也会每次都一样,所以不需要在数据库里再额外保存 salt 的值。

    stretching 次数(这里是 1000 次)越高的话,对暴力破解等攻击的抵抗能力就越强。当然它也有不利的一面,就是与此同时它也会给服务器带来更高的负荷。如果负荷过高,则会给正常业务带来影响,甚至可能被人利用来发动 DoS 攻击。所以这个 stretching 次数需要一边观察服务器负荷情况一边进行调整,最后选择一个合适的值。

9 http://www.vector.co.jp/soft/unix/util/se365582.html

10 这个数字是 2010 年象棋电脑程序“阿迦罗 2010”战胜日本女棋王时的 CPU 的核数。

11 http://project-rainbowcrack.com/

12 http://project-rainbowcrack.com/buy.php

13 根据 RainbowCrack Project 的测试数据,破解一个密码最长也只需要 202 秒。

14 关于 stretching 方法的更多内容请参考 Cryptography Engineering [1]。

专栏:密码泄露途径

密码数据泄漏途径很多,除了前面我们已经介绍的 SQL 注入攻击、密码尝试、社会化攻击、钓鱼攻击等,这里我们再介绍一下其他可能造成密码泄露的行为。

  • 备份数据被盗、丢失

    备份数据里可能会包含密码等机密信息,如果这些媒体(硬盘、磁带)丢失或者流出到外部的话,就会造成密码泄露。

  • 硬盘等被盗、丢失

    如果服务器、硬盘等从数据中心被偷走的话,也会造成信息泄漏。虽然机器从机房被偷走有点令人难以置信,但是硬盘被盗的先例在现实世界中确实是发生过的。针对硬盘被盗的情况,之前介绍过的 TDE 型数据库是一种很好的解决对策。

  • 内部操作人员将数据带出

    在数据中心机房或者办公室里的内部操作人员,可以通过数据库管理工具等把数据抽出,再用 USB 或者光盘之类的媒体带到公司外部,导致信息泄露。此类事件频有发生,媒体中也经常报道。

5.1.4 自动登录

现在很多 Web 应用都会在登录页面提供“自动登录”或者“保持登录状态”这样的复选框(如图 5-9),如果用户选中自动登录,那么即使重启浏览器,系统也会自动进行登录,而不必再次输入用户名和密码。

图 5-9 自动登录复选框示例

从系统安全的角度考虑,一直以来大家都认为自动登录是对安全不利的。比如会话持续时间变长的话,就会增加受到类似 XSS 等被动攻击的概率。

但是笔者认为,根据目前网站的现状及自身的特点,是可以选择提供自动登录功能的。其原因有以下几点。

  • 随着 Web 应用的深入普及,需要保持登录状态的服务增加了(比如 Google 等)
  • 频繁的登录、退出操作,容易迫使用户选择更加简单的密码,反而使得系统的危险系数增加

下面我们说一下怎么才能安全地实现自动登录功能。但是在这之前,作为反例,我们先看看自动登录中一些比较危险的实现方式。

危险的实现方式示例

下面是一个实现方式不正确的网站的例子,它把用户名和自动登录标识都保存到 Cookie 里(例子中 Expires 的实际日期是在 30 天之后)了。

Set-Cookie: user=yamada

; expires=Wed, 27-Oct-2010 06:20:55 GMT
Set-Cookie: autologin=true

; expires=Wed, 27-Oct-2010 06:20:55 GMT

在这个例子中用户名和自动登录标识都用明文保存在 Cookie 中,但是因为用户本人可以修改 Cookie,加入把 user=yamada 篡改为 user=tanaka 的话,就可以冒充别人登录到系统里面了。

虽然这个例子看起来很极端,但是现实中确实是存在着具有类似安全隐患的 Web 网站或者软件的。

下面的方法虽然针对上面的问题作了改良,但是仍然算不上一个好的方案。

Set-Cookie: user=yamada

; expires=Wed, 27-Oct-2010 06:20:55 GMT
Set-Cookie: passwd=5x23AwpL

; expires=Wed, 27-Oct-2010 06:20:55 GMT
Set-Cookie: autologin=true

; expires=Wed, 27-Oct-2010 06:20:55 GMT

这里虽然增加了对密码的验证,使得攻击者不能很轻易地通过上面的方法来冒充别人。但是如果攻击者知道了被攻击用户的密码的话,完全可以堂堂正正地从登录页面登录系统了,就没有必要攻击自动登录功能了。

而且一旦在 Cookie 里存放了敏感信息的话,那么一旦网站出现 XSS 漏洞的时候,就存在密码被盗取的可能性,从而可能带来更大的损失。因此这种方法仍然算不上是理想的实现方式。

下面开始我们介绍如何来实现一个安全的自动登录功能。

安全的自动登录实现方式

要实现保持用户登录状态的功能,可以采用下面的三种方式之一。

  • 延长会话有效期
  • 使用令牌(Token)
  • 使用认证票(Ticket)

下面按顺序介绍一下这三种方法。

  • 延长会话有效期

    如果所用的编程语言或者框架支持设置 Cookie 的 Expires(过期时间)属性的话,这种方法则最简单。

    如果使用的是 PHP 的话,可以使用以下方法实现延长会话有效期。

    • 通过使用 session_set_cookie_params 方法设置 Cookie 的 Expires 属性值
    • 在文件 php.ini 中将 session.gc_maxlifetime 设置为一周左右(默认为 24 分钟)

    但是这样做的话不想保持登录状态的用户的会话过期时间也会被延长到一周,会增加这些不想使用自动登录功能的用户受到 XSS 等被动攻击的概率。

    解决这个问题,可以在应用程序里限制会话过期时间。如以下的脚本解说。

    首先是 php.ini 文件中进行一些设置的例子,下面这个设置是保持会话的有效期至少为一星期。15

    session.gc_probability = 1
    session.gc_divisor = 1000        604800 = 7*24*60*60
    
    
    session.gc_maxlifetime = 604800──┘
    

    之后,是在验证用户密码成功后设置登录信息的地方,加入如下逻辑。这里假设用户选择了自动登录的话,浏览器会将参数 autologin 赋值为 on 后传给服务端。

    代码清单 /51/51-002.php

        <?php
          // 假设在此之前用户密码验证已经通过且成功登录
          $autologin = @$_GET['autologin'] == 'on';
          $timeout = 30 * 60;
          if ($autologin) { // 自动登录的场景
            $timeout = 7 * 24 * 60 * 60;  // 会话有效期设为一星期
            session_set_cookie_params($timeout);  // Cookie 的 Expires 属性
          }
          session_start();
          session_regenerate_id(true);  // 固定会话 ID 对策
          $_SESSION['id'] = $id;     // 登录中的用户 ID
          $_SESSION['timeout'] = $timeout;  // 超时时间(时长)
          $_SESSION['expires'] = time() + $timeout;  // 超时时间(时刻)
        ?>
        <body>
        login successful<a href="51-003.php">next</a>
        </body>
    
    

    在这个例子里自动登录后会依次进行下面的处理。

    • 会话超时时间设为 1 星期(默认为 30 分钟)
    • 将含有会话 ID 的 Cookie 的 Expires 属性设置为 1 星期后

    不管是否是自动登录,都会执行下面这两步

    • 将会话超时时长保存到 $_SESSION['timeout']
    • 将会话超时时刻保存到 $_SESSION['expires']

    下面是判断用户是否处于登录状态的代码。下面这部也会确认之前设置的会话超时相关的属性值。

    代码清单 /51/51-003.php

        <?php
          session_start();
          function islogin() {
            if (! isset($_SESSION['id'])) {  // 还没设置 id 时
              return false;  // 没有登录
            }
            if ($_SESSION['expires'] < time()) {  // 该会话已经超时
              $_SESSION = array(); // 取消 $_SESSION 变量
              session_destroy();    // 放弃会话(退出登录)
              return false;
            }
            // 更新超时时刻
            $_SESSION['expires'] = time() + $_SESSION['timeout'];
            return true;  // 用户处于登录状态,即返回 true
          }
          if (islogin()) {
            // 用户登录中的处理内容(省略了后面的内容)
    
    

    islogin 方法用来判断用户是否处于登录状态。该处理中通过判断保存在会话里的超时时刻和现在时刻来判断用户会话是否已经超时了。

  • 使用令牌实现自动登录

    根据所使用的编程语言不同,有时候不能对保存着会话 ID 的 Cookie 的 Expires 属性进行设置,这时候如果浏览器一关掉的话对应的会话也自动被销毁了,也就不能通过编程语言在会话的管理机制上实现保持用户登录状态的功能了。

    在这种情况下要实现同样的功能的话,则可以考虑使用 4.6.4 节里介绍过的令牌来实现保持用户的登录状态。

    • 用户登录时创建令牌

      在用户登录成功的时候,创建令牌并保存到 Cookie 中。Cookie 的 Expires 属性可以设置为 1 周左右,令牌的值使用伪随机数。最好同时设置 HttpOnly 属性,另外如果是 HTTPS 连接的话,需要同时设置 secure 属性。

      令牌的值只是个随机数,每个登录用户的信息,可以用下图那样的结构保存到数据库中进行管理。

      令牌(唯一)用户 ID有效期

      图 5-10 自动登录用户信息

      如上图所示,通过令牌的值可以知道哪个用户在什么时间之内能进行自动登录。

      令牌是在用户登录时候创建的(只有在用户选择了自动登录的时候才会创建),下面的伪代码展示了该方法的大致思路。

      代码清单 自动登录令牌创建过程(伪代码)

      function set_auth_token($id, $expires) {
        do {
          $token = 随机数 ;
          准备查询 ('insert into autologin values(?, ?, ?)');
          执行查询 ($token, $id, $expires);
          if ( 查询成功 )
            return $token;
        } while( 数据重复错误 );
        die(' 访问数据库错误 ');
      }
       
      $timeout = 7 * 24 * 60 * 60;  // 认证的有效期(1 周)
      $expires = time() + $timeout;  // 认证的有效期
      $token = set_auth_token($id, $expires);  // 设置令牌
      setcookie('token', $token, $expires);  // 将令牌的值保存到 Cookie
      
      

      set_auth_token 方法用来生成令牌,输入参数为用户 ID 和令牌的有效期间,生成令牌的值后将这些信息保存到数据库中,并返回生成的令牌值给调用函数。如果生成令牌值的过程中发生了值重复问题,则需要继续尝试直到生成不重复的令牌值为止。

      在上面的示例代码里最后,也展示了如何调用生成令牌函数。这里将表示自动登录的有效期参数设为一周后调用 set_auth_token ,并将返回的令牌保存到了 Cookie。

    • 判断用户的登录状态和执行自动登录

      接着我们再看看如何实现判断用户是否处在登录中状态,以及如何实现自动登录。处理逻辑见下面的伪代码。

      代码清单 判断用户是否已登录以及如何实现自动登录(伪代码)

      function check_auth_token($token) {
        准备查询 ('select * from autologin where token = ?');
        执行查询 ($token);
        取得 $id 和有效期 ;
        if ( 不存在相应记录 )
          return false;
        if ( 有效期 < 现在时刻 ) {
          放弃旧认证令牌 ;
          return false;
        }
        return $id;
      }
       
      function islogin($token)
        if ( 会话中是否有认证信息 )
          return 认证成功 ;   // 用户已经是登录中状态了
        // 从下面开始是会话已经超时,开始自动登录处理
        $id = check_auth_token($token);
        if ($id !== false) {
          将认证信息放到会话 ;
          放弃旧认证令牌 ;
          设置新的认证令牌(及新的有效期间);
          return 认证成功 ;
        }
        return 认证失败 ;  // 自动登录失败的时候
      }
      // 需要编写批处理程序来定期从数据库删除已经过期的认证令牌记录
      
      

      islogin 函数先判断现在会话里是否有用户的登录信息,如果有的话就认为用户已经登录了,将直接返回成功。如果会话里不存在用户的登录信息的话,则继续调用 check_auth_token 函数,尝试进行自动登录。在 check_auth_token 方法里根据传过来的令牌信息来查找用户的自动登录数据,如果数据库中存在此令牌数据,且有效期间没有过期的话,则返回登录中的用户 ID。

      如果执行自动登录成功的话,需要先删掉旧的令牌信息,然后再生成新的令牌并保存到数据库中。之所以选择删除原来的令牌再创建新的,而不是简单地更新原令牌的有效期间,是因为如果原令牌如果由于某些原因被泄露的话,有可能被别人拿来进行恶意攻击,所以最好采取删除原令牌的方法。同样的原因,在用户修改密码后,也应该对令牌信息做类似的处理。

    • 退出登录

      在进行退出登录处理的时候,需要以用户 ID 为查询条件,在用户自动登录数据表里把这个用户对应的令牌信息删除。考虑到用户可能在不同的终端上登录并且启用了自动登录,所以这里删除用户的自动登录数据的时候,需要以用户 ID 为条件而不能用令牌。

      代码清单 退出登录处理(伪代码)

      $_SESSION = array();  // 销毁 $_SESSION 变量
      session_destroy(); // 销毁会话(退出登录)
      // 根据用户 ID 删除该用户所有自动登录数据
      准备删除语句 ('delete from autologin where id=?');
      执行删除语句  ($id);
      
  • 基于认证票的自动登录方式

    认证票是为了在不同的服务器之间共享用户的认证数据(用户名,有效期间)而设计的。为了防止伪造或者信息泄露,认证票可能会需要电子署名或者加密。Windows 所采用的 Kerberos 认证,以及 ASP.NET 的表单认证(From 认证)等都采用了认证票的方法。

    认证票方法的优点是可以跨服务器共享用户的认证信息。但是实现认证票功能的话需要比较专业的加密或者安全方面的知识,尽量避免自己去实现。

    如果想在在多台服务器之间共享认证数据的话,推荐采用第三方的单点登录系统(SSO),或者使用 OpenID 等开放的认证平台产品。

  • 三种方法的比较

    关于如何实现用户自动登录功能,上面我们介绍了延长会话超时时间、令牌和认证票等三种方法。这之中,使用令牌方式是比较好的选择。令牌方式的优点有如下几条。

    • 对不想使用自动登录功能的用户不构成任何影响
    • 针对使用多终端登录的用户可以实现一次全部退出登录
    • 管理员可以强制使指定的用户退出登录
    • 在客户端不额外保存敏感信息,不存在被恶意利用的风险

15 服务器在收到客户端请求时,会以 session.gc_probability / session.gc_divisor 的概率进行会话回收工作,清理存活时间已经超过 session.gc_maxlifetime 的会话,所以会话被清理的时机不是实时的,会有一定的延迟。

如何降低自动登录带来的风险

自动登录带来的问题是用户的认证状态会持续很长时间,这会增加用户遭受 XSS 或 CSRF 等被动攻击的风险。

针对此问题,可以在关键操作,比如查看重要信息(个人信息等)、重要的操作(购物、转账、修改密码)之前加入密码确认步骤。亚马逊就是采用这种方法的网站之一,通常情况下用户访问亚马逊是在登录状态下进行的,但是用户如果想下单或者查看历史订单等重要操作前就会被要求再次输入密码进行验证。

5.1.5 登录表单

这一节我们将对登录表单(用户输入 ID 和密码的页面)的安全性要求做一下说明。实现登录表单的时候最基本的原则如下。

  • 密码输入框需要掩码(Mask)显示
  • 如果应用需要使用 HTTPS 的话,从登录表单显示页面开始就应该使用 HTTPS

所谓输入密码的掩码显示,是指使用类型是 password 的输入控件,使用这种控件后,输入的密码在页面上显示时会以“*”的符号显示出来。这样就能降低因 shoulder hack 导致密码被盗取的风险。

下面说一下为什么需要在登录表单显示页面就要使用 HTTPS。在输入密码的页面和进行登录处理的页面中,如果把后面的页面放在 HTTPS 下的话,用户输入的密码会被加密后传递给服务器,这样就能防止密码被盗取了。但是如果在输入密码的页面不使用 HTTPS 的话,就可能会存在如下风险。

  • 登录表单被篡改,跳转目的地址被设为攻击者的地址
  • 如果遭受 DNS 欺骗(DNS cache poisoning 或 DNS spoofing)攻击的话,用户可能会被转到攻击者的网站

如果我们在显示登录表单的页面开始使用 HTTPS 的话,就能防止该页面的内容被篡改。即使用户被诱骗到其他网站,浏览器也会显示出错信息,提醒用户注意(用户需要确认域名是否正确)。

所以说,如果要使用 HTTPS 的话,请一定在登录显示页面开始就使用 HTTPS。

专栏:密码确实需要掩码显示吗

现在编写登录页面时,将密码输入框做掩码处理应该算是最基本的常识了,但是笔者却对此有些疑问。如果对输入框进行了掩码设置,那么输入包含符号、大小写字母的密码会变得稍微有点麻烦,用户可能会有选择更简单的密码,这对系统安全性来说反而是一个不利因素,这也是笔者对掩码处理持有疑问的原因。

美国关于 Web 使用方便性的权威 Jakob Nielsen 在 2009 年 6 月发表了一篇“请停止隐藏密码显示”16 的专栏文章,这篇文章里列举了很多隐藏密码明文显示的缺点,当时出现了很多反对的意见,但是 SANS 的博客里却对此表示了赞同的看法 17 。这篇文章也成了当时大家关注的话题。

把目光转向 Web 网站以外,可以发现在桌面端应用程序里面,有很多软件都提供了在密码的明文显示和掩码显示之间的切换功能。比如下面的图显示的就是 Windows Vista 的 VPN 设置画面,画面里有一个“显示字符”的复选框。

图 5-11 密码显示 / 不显示的例子

密码认证的最大的威胁是跨越网络的暴力破解,而预防抗暴力破解的最好的方法就是选择安全的密码。以后提供“显示密码字符”复选框的网站数量也许会增加。但是如果使用浏览器的密码自动保存功能的话,有可能画面一显示出来就把明文密码都暴露了,会被别人看见,这是个比较麻烦的问题,所以“显示密码字符”的复选框需要默认为不被选中。

16 http://www.nngroup.com/articles/stop-password-masking/

17 https://blogs.sans.org/appsecstreetfighter/2009/06/28/response-to-nielsens-stop-password-masking/

5.1.6 如何显示错误消息

本节将说明一下如何在用户登录界面正确的显示错误消息。

不管什么错误消息,其原则都是不能留给攻击者任何提示。在登录功能里像下面的两个错误消息都是对安全不利的:

“指定的用户不存在”

“密码不正确”

因为攻击者从这些消息里就可以知道到底是用户 ID 错了还是密码错了,使得暴力破解将会更有针对性。图 5-12 是在知道用户 ID/ 密码哪个错误和不知道用户 ID/ 密码哪个错误这两种情况下,攻击所需要成本的对比。

图 5-12 如果知道登录 ID 或者密码哪个错了的话破解将会变容易

如果是用户 ID 不正确(不存在)的话,如左图,首先攻击者会一直尝试使用不同的用户 ID,直到找到了系统里存在的用户 ID,然后再开始进行猜测密码,这样的话最多需要尝试 2 万次就够了。如果攻击者不知道登录 ID 或者密码哪个不正确的话,就需要尝试所有的用户 ID 和密码的组合,也就是至少需要尝试 1 万 ×1 万,即 1 亿次才行。可以看出,错误消息可以在很大程度上对攻击者的攻击效率产生影响。

同样,有时候攻击者也可以通过用户账号被锁定时显示的错误信息来推测用户 ID 是否存在,所以推荐在发生账号被锁定的时候使用类似下面那样的消息(如果系统支持账号锁定功能的话):

“ID 或者密码错误,账号已被锁定”

尽管这样会让真正的用户不知道到底是自己的用户 ID 错了还是密码错了,但我们在 5.1.2 节提到了,用户账号被锁定的时候,最好同时给用户发送邮件通知。所以如果在上面弹出的错误消息后面加上类似下面这段文字的话,用户体验会更好一些。

※ 账号锁定时将发送邮件通知账号所有者,如果有什么疑问,请查看邮件内容进行确认。

5.1.7 退出登录功能

比较安全的退出登录处理的做法是销毁会话对象。另外,有时候需要在退出登录的时候加入防止 CSRF 漏洞的逻辑,但是如果由第三方强制退出登录没有什么大的不利影响的话,也可以考虑省略预防 CSRF 的处理。

下面是在退出登录需要做的事。

  • 退出登录处理有副作用所以用 POST 提交退出登录请求
  • 在退出登录处理中销毁会话对象
  • 根据需要选择是否加入 CSRF 处理

发起退出登录请求的页面实现示例请参考下面的代码。

代码清单 /51/51-011.php

【前面省略】// 这里假设已经执行了 session_start();
<form action="51-012.php" method="POST">
<-- 下面是防止 CSRF 的令牌  -->
<input type="hidden" name="token" value="<?php echo
  htmlspecialchars(session_id()); ?>">
<input type="submit" value=" 退出登录  ">
</form>

这段代码通过 POST 方式向执行退出操作的脚本(51-012.php)发起请求,作为预防 CSRF 的措施,同时传一个 hidden 的参数 token,token 直接使用了会话 ID 的值。

执行退出登录的处理如下所示。

代码清单 /51/51-012.php

<?php
  $token = $_POST['token'];
  session_start();
  // 进行令牌验证
  if ($token != session_id()) {
    die(' 点击退出登录按钮退出 ');
  }
  // 清空 $_SESSION 变量
  $_SESSION = array();
  // 销毁 session
  session_destroy();
?>

在这段代码的前半部分是进性预防 CSRF 的令牌检查,关于 CSRF 对策的详细内容可以参考 4.5.1 节。

上面脚本的后半部分是执行退出登录处理的中心内容,先对 $_SESSION 变量进行了清空操作,然后又销毁了会话。如果只是退出登录的话,不必对 $_SESSION 进行清空操作,但是为了防止以后在退出登录后增加逻辑时不小心发生其他问题,安全起见对这个变量进行了清空处理。

5.1.8 认证功能总结

这一节我们介绍了几种能增强系统认证功能安全性的一些方法。用户 / 密码认证作为现在主流的认证方式,可以采用下面的方法来提高其安全性。

  • 密码可用字符种类和长度的要求
    • 请参考 5.1.1 节
    • 请参考 5.1.1 节
  • 针对暴力破解的对策
    • 请参考 5.1.2 节
  • 密码保存方法
    • 请参考 5.1.3 节
  • 输入页面和错误信息的需求
    • 请参考 5.1.5 节
    • 请参考 5.1.6 节

此外,本节还介绍了如何安全的实现自动登录和退出登录功能。

参考:彩虹表原理

即使攻击者得到了密码的散列值去暴力破解,时间成本还是非常高的,于是有人提出了用反向查找表来提高查询速度的方法。这里所说的反向查找表是指把密码的散列值作为查找表的键(Key),而把密码原文作为查找表的值(Value)存储。表 5-3 是一个的反向查找表例子。

表 5-3 散列值反向查找表示例

散列值

密码

098f6bcd46

test

5f4dcc3b5a

password

900150983c

abc

d16fb36f09

xyz

但是如果直接去生成这样一个查找表的话,其大小将会过于庞大。所以有人又设想如果能按照某些规则,做出一个密码 1 →散列值 1 →密码 2 →散列值 2 →密码 3 →散列值 3 →…这样的链表的话,那么我们只需要记住链表的表头和表尾就可以了。这就是彩虹表的最显著特征。

为了建立这样一个链表,就需要知道如何从一个密码的散列值得到其后面的密码,实现这个功能的函数被称为还原函数(Reduction Function)。还原函数的功能就是从给定的散列值,生成一个符合密码规范(可使用字符种类及密码长度)的新密码字符串。在链表里的不同位置需要使用不同的还原函数,而不能全部使用同一个还原函数。在图 5-13 的例子里,由于一共进行了 3 次散列值还原操作,所以相应地也就需要 3 个还原函数。

图 5-13 彩虹表的链表结构

为了能多保存一些密码组合,首先需要选择链表的第一个元素,然后通过计算得到整个链表。在把彩虹表数据保存到文件的时候,只需要记录这个链表的头和尾的元素即可。图 5-14 是彩虹表在文件中保存的大概样子。

图 5-14 彩虹表的保存方法示例

下面我们以散列值 a48927 为例来看看如何利用彩虹表来计算出该散列值对应的原密码。最开始检索的时候由于不知道这个散列值在链表的哪个位置上,所以需要对每个位置进行验证。首先,使用还原函数和散列函数分别计算出其在链表各个位置时的链表末尾元素的值,得到的结果是 lookie、abcxyz、root00 这三个值,如图 5-15 所示。

图 5-15 使用彩虹表进行检索的过程

接着,依次查找这些产生的密码是否在彩虹表链表的最尾部,就找到了以 root00 为最末元素的链表。然后,在彩虹表里找到相应的链表的头元素,即 system。

以 system 开始,依次使用散列函数和还原函数进行链表计算,就能发现 system 的散列值就是 a48927,即需要检索的密码原文就是 system。

彩虹表的数据文件里只保存了每个链表的开头和结尾的元素,其大小只由组成密码的字符种类和密码长度决定,跟具体的散列算法无关。而且彩虹表的算法和具体的散列函数(比如 MD5)本身无关,可以针对任何散列函数创建彩虹表,比如已经有公开的适用于 SHA-1 的彩虹表了。预计今后也会创建针对 SHA-256 的彩虹表。

参考文献

[1]Niels Ferguson, Bruce Schneier, Tadayoshi Kohno. (2010). Cryptography Engineering . Wiley Publishing, Inc.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文