为什么忘记密码时只能重设,不把旧密码告诉我?

某天小明在整理他的收藏夹时发现了一个以前很常逛,但已经将近半年多没去的一个论坛。小明想回去看看那边变得怎么样了,于是点进去那个论坛,输入了帐号密码,得到了密码错误的提示。

尝试了几次之后,系统提示小明可以使用「忘记密码」的功能,所以小明填了自己的 email 之后去收件箱里查看,发现系统传来一个「重设密码」的链接。虽然说最后小明成功利用重新设定的密码登入,但有个问题让他百思不得其解:

奇怪呀,为啥要我重新设置密码,把旧的密码发到邮箱里给我不就好了?

应该有许多人都跟小明一样,有过类似的疑惑。把旧密码发给我不是很好吗,干嘛强迫我换密码?

这一个看似简单的问题,背后其实藏了许多信息安全相关的概念,就让我们慢慢寻找问题的答案,顺便学习一些基本的信息安全知识吧!

被偷走的数据库

大家应该很常看到新闻说哪个网站的数据又被偷走了,顾客个人数据全部都泄露出去。像是国外知名的网域代管网站 GoDaddy[1] 就泄露了 120 万笔用户数据,像国内的微博、淘宝等也有过数据泄露的情况发生。

这边我想带大家探讨的两个问题是:

  1. 数据真的这么容易泄露吗?

  2. 数据泄露之后,可能造成什么后果?

我们先来看第一个问题,有很多安全性的漏洞可以造成数据泄露,而有些漏洞的攻击方式,比你想的还简单一百倍。

你想像中的黑客可能像上面那样,打着一大堆不知道在干嘛的指令,画面上出现很多黑底白字或是绿字的画面,完全搞不懂在干嘛,但是做着做着网站就被攻破下来了。

而事实上有些漏洞,可能在地址栏上面改几个字就攻击成功了,就算你不懂任何代码也做得到。

举例来说好了,假设今天有个购物网站,你买了一些东西之后送出订单,订单成立后跳转到订单页面,上面有着一大堆你的个人数据,例如说:姓名、收货地址、联络电话以及 Email 等等。

然后你发现订单页面的网址是 https://shop.xiaoming.cn/orders?id=14597

而正好你的订单编号也是 14597,在好奇心的驱使之下,你就试着把数字改成 14596,然后按下 Enter。

当网站载入完成之后,你竟然还真的能看到编号为 14596 的订单,上面出现一个你不认识的人的姓名、收货地址、联络电话跟 Email。

有些攻击就是这么朴实无华且枯燥,只要改个字就能看到属于其他人的数据。这时候如果你会写程序的话,就可以写个脚本自动去抓 id 是 1 一直到 id 是 15000 的数据,你就拿到了这个购物网站 15000 笔订单的资讯,也就是一万多个顾客的个人数据。

这过程中没有什么黑底白字的画面,也不用一直疯狂打字,唯一需要的只有改数字,个人数据就轻松到手。

这类型的漏洞有个专有名词,称为 IDOR,全名是:Insecure direct object references,大约就是不安全的直接数据存取的意思。漏洞产生的原因就是工程师在开发时,并没有注意到权限控管,因此让使用者能存取到其他人的数据。

有些人看到这边可能以为我只是为了文章浅显易懂,所以才举一个简化的例子,现实生活中的攻击才没这么简单。

这句话算是对了一半,大部分的网站确实都不会有这么明显的一个漏洞,攻击方式会更复杂一点。但可怕的是,还真的有些网站就是这么简单,就是改个数字就可以拿到别人的数据。

例如说这两个就是 IDOR 的真实漏洞:

  1. 享健身 xarefit 任意访问/下载所有会员个人数据[2]

  2. DoorGods 防疫门神实联制系统 IDOR 导致个人数据泄露[3]

对,不要怀疑,就真的只是在网址上改个数字而已这么容易。

以后只要看到网址列上有这种数字,就可以试着去改改看,搞不好不会写程序的你也可以发现 IDOR 的漏洞。

除了这种只要改个东西的漏洞之外,还有另外一个很常见但是需要一点技术能力才能攻破的漏洞,叫做 SQL Injection。

先来讲讲 SQL 是什么,简单来说就是跟数据库查询东西的一种程序语言。既然说是语言那就会有固定语法,若是以中文举例,大概就像是:

去找「订单数据」,给我「id 是 1 的」,按照「建立时间」排序

用「」框起来的部分代表可以变动,而其他关键字例如说「去找」、「给我」这些都是固定的,因为语法要固定才能写程序去解析。

同样以上面假想的购物网站为例,如果网址是 https://shop.xiaoming.cn/orders?id=14597 ,那网站去跟数据库拿数据时,指令大概就是:

去找「订单数据」,给我「id 是 14597 的」

因为网址列上的 id 是 14597 嘛,所以这个 id 就会被放到查询的指令去,如果 id 是别的,那查询的指令也会不一样。

这时候如果我的 id 不是数字,而是「1 的顺便给我使用者数据」,查询就变成:

去找「订单数据」,给我「id 是 1 的顺便给我使用者数据」

那整个网站的使用者数据就顺便被我抓下来了。

这个攻击之所以叫做 SQL injection,重点就在于那个 injection,攻击者「注入」了一段文字被当作指令的一部分执行,所以攻击者就可以执行任意查询。

比起上面讲的 IDOR,SQL injection 通常会更为致命,因为不只是订单数据本身,连其他数据也会被一起捞出来。所以除了订单数据,会员数据跟商品数据都有可能一起泄露。

这边也随便找两个公开的案例:

  1. 北一女中网站存在 SQL Injection 漏洞[4]

  2. 桃园高中 网站 SQL injection[5]

而防御方式就是不要把使用者输入的「1 的顺便给我使用者数据」直接当作指令,而是经过一些处理,让整段查询变成:「给我 id 是:『1 的顺便给我使用者数据』的数据」,那因为没有这个 id,所以什么事也不会发生。

个人数据泄漏了,然后呢?

前面我们已经看到了针对那些没有做好防御的网站,个人数据泄露是多么容易的一件事情。

那个人数据泄漏之后,对使用者会有什么影响呢?

大家最感同身受的应该就是诈骗电话吧,例如说某些买书的网站或是订房网站,打过来跟你说什么要分期退款,为了博取你的信任,连你买了哪本书,订了哪个房间,或是你家地址跟姓名全都讲得出来。

这些都是因为数据泄露的缘故,诈骗集团才会知道的这么清楚。

但除了这些个人数据以外,还有两个东西也会泄露,那就是你的帐号跟密码。

也许你会想说:「不就帐号跟密码吗,我就在那个网站上面改密码以后再用就好啦!」

事情也许没有你想的这么简单。如果你没有用密码管理软件的话,我大胆猜测你所有的密码可能都是同一个。因为怕记不起来嘛,所以干脆都用同一个密码。

这时候如果账号和密码泄露,黑客是不是就可以拿这个账号和密码去其他服务试试看?

拿去登你的 Google,登你的 Facebook,这时候用同一个密码的人就会被登进去。所以从表面看只是一个购物网站被入侵,但造成的结果却是你的 Google 还有 Facebook 也一起被盗了。

所以,有时候某个网站被盗帐号可能不是那个网站的问题,而是黑客在其他地方拿到了你的帐号密码,就来这边试试看,没想到就中了。

对于网站的开发者而言,保护好使用者的个人数据是天经地义的事情,保护密码也是,有没有什么好方法可以保护密码呢?

加密吗?把密码用某些算法加密,这样数据库储存的就会是加密后的结果,尽管被偷走了,黑客只要没有解密的方法就解不开。

听起来似乎是最安全的做法了,但其实还有一个问题,那就是网站的开发者还是会知道怎么解密,如果有工程师监守自盗怎么办?他还是可以知道每个使用者的密码是什么,可以把这些资讯拿去卖或者是自己利用。

嗯...似乎我们也不能怎么样,因为无论如何,开发者都需要有方法知道数据库存的密码究竟是多少吧?不然在登入的时候怎么确认帐号密码是对的?

再者,这样听起来应该够安全了,要怎么样才能更安全?难道要连网站的开发者都无法解密,都不知道密码是什么才够安全吗?

Bingo!答对了,就是要这样没错!

没有人知道你的密码,包括网站本身

事实上,网站的数据库是不会储存你的密码的。

或更精确地说,不会储存你的「原始密码」,但会储存密码经过某种运算后的结果,而且最重要的是, 这个运算是无法还原的

直接举例比较快,假设今天有个很简单的算法,可以把密码做转换,转换方式是:「数字不做转换,英文字母把 a 换成 1,b 换成 2...z 换成 26」,以此类推,第几个字母就换成几,大小写不分都一样(先假设不会有符号)。

如果密码是 abc123,转换完就变成 123123。

在使用者注册的时候,网站就把使用者输入的 abc123 转成 123123,然后存到数据库里面。因此数据库存的密码是 123123,而不是 abc123。

当使用者登入时,我们就再把输入的值用同样的逻辑转换,如果输入一样,转换后的结果就会一样对吧?就知道密码是不是正确的。

当黑客把数据库偷走以后,会拿到 123123 这组密码,那一样啊,不是可以推论出原本是 abc123 吗?不不不,没这么简单。

123123、abcabc、12cab3...这些密码转换之后,不也是 123123 吗?所以尽管知道转换规则跟结果,却没有办法还原成「唯一一个密码」,这就是这个算法厉害的地方!

这样的转换就叫做哈希(Hash),abc123 每次 hash 过后的结果都会是 123123,但是从 123123 却无法得到输入一定是 abc123,因为有其他种可能性存在。

这就是 hash 跟加密最大的不同。

加密跟解密是成对的,如果可以加密就一定可以解密,所以你知道密文跟密钥,就可以知道明文。但 hash 不同,你知道 hash 算法的结果,却无法逆推出原本的输入是什么。

而这个机制最常见的应用之一,就在于密码的储存。

在注册时把 hash 过后的密码存进数据库,登入时把输入的密码 hash 过后跟数据库比对,就知道密码是否正确。就算数据库被偷,黑客也不知道使用者的密码是什么,因为逆推不出来。

这就是为什么忘记密码的时候,网站不会跟你讲原本的密码是什么,因为网站本身也不知道啊!

所以不能「找回密码」,只能「重设密码」,因为重设就代表你输入新的密码,然后网站把新的密码 hash 之后存进数据库,未来登入时就会用这组新的 hash 去比对。

有些人可能会注意到这样的储存方式似乎有个漏洞,延续前面的例子,数据库存的是 123123 而我的原始密码是 abc123,这样如果用「abcabc」,hash 过后也是 123123,不就也可以登入吗?这样不太对吧,这不是我的密码嘛

有两个不同的输入却产生出同一个输出,这种状况称为碰撞(hash collision),碰撞一定会发生,但如果算法设计的好,碰撞的机率就超级无敌小,小到几乎可以忽略。

前面提的转换规则只是为了方便举例,真实世界中用的算法复杂许多,就算只有一个字不同,结果都会天差地远,以 SHA256 这个算法为例:

  1. abc123 => 6ca13d52ca70c883e0f0bb101e425a89e8624de51db2d2392593af6a84118090

  2. abc124 => cd7011e7a6b27d44ce22a71a4cdfc2c47d5c67e335319ed7f6ae72cc03d7d63f

类似的输入却产生截然不同的输出。

像我前面举例用的转换就是不安全的 hash 算法,要尽量避免使用或是避免自己设计,尽可能使用密码学家跟专家设计出的算法,像是上面提到的 SHA256。

在使用这些算法的时候,也要特别注意一下是否安全,因为有些算法虽然也是由专家设计,但已经被证明是不安全的,例如说密码用 MD5 来 hash 后储存就是不安全的,可以参考:Is MD5 considered insecure?[6]

储存 hash 后的值就没事了吗?

抱歉,其实只储存密码 hash 过后的值是不够的。

咦,为什么?我刚刚不是说没办法反推出结果吗,那为什么不够?

虽然说没办法反推出结果,但攻击者可以利用「输入一样,输出一定一样」的特性,先建好一个人数据料库。

举例来说,假设有个很常见的密码 abc123,hash 过后的值是 6ca13d,那攻击者就可以先算好,然后把这个关系存在数据库,所以攻击者的数据库里面就可能会有一百万组最常见密码的清单,里面有着每个密码跟它 hash 过后的值。

那接下来只要在 hash 过后的数据库发现 6ca13d,攻击者就可以透过查表的方式,查出原本的密码是 abc123。这不是利用算法反推结果,这只是利用现有数据来查询而已。

为了防御这种攻击,还要做一件事情叫做加盐(Salting),没错,就是盐巴的那个盐。通常会帮每个使用者产生一个独一无二的盐,例如说 5ab3od(实际上会更长,可能 16 或 32 个字以上),接着把我的密码 abc123 加上我的盐,变成 abc1235ab3od,然后用这个加盐过后的结果去做 hash。

为什么要这样做呢?

因为攻击者预先准备好的表格中,比起 abc123,出现 abc1235ab3od 的机率显然更低,同时又因为长度变长了,暴力破解的难度变得更高。如此一来,密码就变得更难破解了。

结语

忘记密码时网站不会把密码发给我,因为网站自己都不知道我的密码是什么。虽然听起来不太可能,但实际状况就是如此。为了安全性,这是必须的手段。

要达成这样的目的,背后最重要的技术原理就是 hash,「同样的密码会产生同样的 hash 值,但从 hash 值没办法对应回原本的密码」就是秘诀所在。

反之,如果你发现有网站可以找回你的密码,那就得要多加注意,有可能网站数据库存的不是 hash 值而是你的密码。在这种状况下,万一有天数据库被入侵,账号和密码被偷走,黑客就能得知你真实的密码,然后去试其他的服务。

有关于密码管理,现在浏览器也有功能可以自动帮你产生密码外加记忆密码,或也可以使用现成的密码管理软件,都可以在不同网站产生不同的密码。

这篇希望能让对这个领域陌生的读者们也能知道一些基本的概念,包括:

  1. 有些网站比你想得脆弱很多,改个网址就可以拿到别人的数据

  2. 对于安全性做得不好的网站,拿到整个人数据料库不是一件难事

  3. 忘记密码只能重设,不能找回,是因为网站也不知道你的密码

  4. 如果有网站可以把旧密码给你,那你得要小心一点

相关推荐
吕彬-前端19 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱22 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
许野平29 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
guai_guai_guai31 分钟前
uniapp
前端·javascript·vue.js·uni-app
Suckerbin36 分钟前
Hms?: 1渗透测试
学习·安全·网络安全
Diamond技术流1 小时前
从0开始学习Linux——网络配置
linux·运维·网络·学习·安全·centos
Spring_java_gg1 小时前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm