65.9K
CodeProject 正在变化。 阅读更多。
Home

安全问题 - 概述和示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.49/5 (12投票s)

2015年11月13日

CPOL

17分钟阅读

viewsIcon

16758

最近我经常处理安全问题,而且我一直都在思考安全问题,所以我决定写这篇帖子。但是,别指望我会谈论最新的案例。那么,首先,什么才算安全问题?我没有一个完美的答案,但也许我们可以这样说,如果应用程序中的任何东西以意想不到的方式使用,并且可能导致您损失金钱或以某种方式给您带来麻烦,那么它就是一个安全问题。请注意,这个问题不一定直接影响您,因为给您的客户带来问题的问题,如果他们决定起诉您,也会成为您的问题。

最近我经常处理安全问题,而且我一直都在思考安全问题,所以我决定写这篇帖子。但是,别指望我会谈论最新的案例。

那么,首先,什么才算安全问题?

我没有一个完美的答案,但也许我们可以这样说,如果应用程序中的任何东西以意想不到的方式使用,并且可能导致您损失金钱或以某种方式给您带来麻烦,那么它就是一个安全问题。请注意,这个问题不一定直接影响您,因为给您的客户带来问题的问题,如果他们决定起诉您,也会成为您的问题。

我知道,这样的通用描述并没有解释安全问题的任何技术细节……但实际上这就是最好的特点:重要的不是技术上出了什么问题。而是后果有多严重。

第一个案例

我 13 岁时,人们使用付费的BBS,其中一些 BBS 有游戏,你可以在游戏中投注你剩余的时间/下载次数,如果运气好,你将获得投注的额外时间/下载次数。如果不走运,好吧,你就会失去它们。

有一个特别的游戏是四匹马赛跑,考虑到任何相对较好的随机化,人们预计会输掉 3/4 的时间,只赢 1/4 的时间,但“不知何故”,许多人开始大部分时间都赢。

我甚至不记得我怎么和其中一个 BBS 的老板聊天的,当他告诉我那个游戏以及人们赢钱的事情时,我决定自己试试。他和一些了解情况的人认为,一个精心制作的黑客应用程序可以预览随机化或类似的东西……但我不需要玩三次就知道哪里出错了。

我玩了一次公平游戏并输了。然后我做了一次我认为不公平的游戏……我实际上赢了比赛,但因为我的不公平尝试而输了。我已经知道哪里错了,但我又玩了一次不公平的游戏,我的马输了,我赢了!

那么,我在做什么?

我的投注是负值。

所以,每次我选择正确的马时,我实际上“输了”。每次我选择错误的马时,我实际上“赢了”。考虑到赔率,通过一直选择同一匹马并大部分时间“输掉”,我实际上是在“赢”。

重要的观察点

  1. 错误只是缺少了一个 `if (value <= 0)` 条件;
  2. 人们自然会想到更复杂的攻击类型。然而,最简单的攻击却击中了他们;
  3. 如果这是一个普通的游戏,这个错误就不会是安全问题。它仍然是一个 bug,但它之所以成为安全问题,是因为通过这种方式获胜的用户不再为那些时间/下载次数付费。

第二个案例

第二个案例是我后来看到的。在 OneDrive 和“云”出现很久之前,我们就已经有了允许我们存储一些文件(用于备份或发布自己的网站)的网站。

在这个特定的案例中,该网站允许用户免费存储最多 1MB 的文件(当时这已经很多了)。要使用超过这个容量,用户需要付费。

一个用户不应该读取或写入其他用户的文件。在列出文件时,人们会访问一个类似于 `http://www.somesite.com/list.asp?folder=/MyDocuments` 的链接,该链接实际上映射到服务器上的一个文件夹,路径类似于 `C:\SiteUsers\LoggedUser\MyDocuments`。

该网站没有使用真正的 Windows 用户或其他东西,但由于用于访问网站的登录信息被用作路径的一部分,开发人员认为他只限制用户查看自己的文件夹。

也就是说,如果我有一个用户名为 `PauloZemek` 的账户,我应该只看到 `C:\SiteUsers\PauloZemek` 下面的文件。如果提供了一个像 `/Documents/SecretDocuments` 这样的文件夹,路径将被转换为 `C:\SiteUsers\PauloZemek\Documents\SecretDocuments`。到目前为止,一切看起来都很好。

用户根本无法访问 `C:\SiteUsers` 这个字符串,他们也无法在访问任何路径时删除自己的登录名,所以一切都很安全,对吧?我(PauloZemek 登录)无法访问 SomeOtherUser 的文件,对吧?

那么,如果我向 list.aspx 提供了以下路径 `../SomeOtherUser/` 会发生什么?

我将访问哪个文件夹?

页面会将该文件夹转换为 `C:\SiteUsers\PauloZemek\..\SomeOtherUser\`,但实际的文件系统会将其转换为 `C:\Users\SomeOtherUser\`。发生这种情况是因为“..”实际上表示“父文件夹”。所以,从我的用户那里,我将要求进入父文件夹(包含所有用户),然后我可以选择去哪个用户。

请注意,这里的问题比仅仅访问其他用户的文件更糟糕。如果我只提供 .. 路径,我将能够看到所有用户。如果我使用 ../.. 则会更糟,因为我将能够访问 `C:\` 文件夹,从而访问整个驱动器。

我只讨论了 `list.aspx`,它列出了文件夹内容,但请记住,该网站的目的是存储文件。因为在所有操作(如读取、写入和删除)中都犯了这种错误,所以我能够读取、写入和删除任何地方的文件,甚至能够接管整个网站或销毁操作系统中的重要文件。

也许验证路径是否包含“..”来阻止它是一种解决方案,但有许多方法可以解决问题,而验证字符串本身是最糟糕的方法之一。就个人而言,我认为操作系统应该只允许“..”在路径的开头工作,而不是在中间或结尾。尽管如此,我们不能指望这种行为会改变。所以,在这种情况下,我们必须意识到,无论我们在路径前加上什么“前缀”,人们总是可以“向后”移动这个前缀。所以,需要另一种验证(也许使用不同的操作系统用户来访问这些文件夹将大大提高安全性)。

SQL 注入

前面的案例不是 *SQL 注入*,但它与 SQL 注入有共同的根源。在导航文件夹时,开发人员确信通过以 `C:\Users\UserName\` 开头,不可能访问其他用户的东西,但 **..** 有特殊含义。它并没有深入文件夹树,而是回退了一级。*SQL 注入* 存在类似的问题,某些字符具有特殊含义,如果您只是将用户写的内容添加到自己的字符串中,可能会发生糟糕的事情。特别是,`'` 字符在 SQL 中是问题。例如,开发人员可能会执行此查询

SELECT Name FROM Users WHERE Name LIKE 'user provided string here';

而用户提供的字符串可能是

'; DROP TABLE Sales; SELECT Name FROM Users WHERE NAME LIKE '

这将变成

SELECT Name FROM Users WHERE Name LIKE ''; DROP TABLE Sales; SELECT Name FROM Users WHERE Name LIKE '';

这将是一个有效的 SQL 命令。它将执行 SELECT,然后删除一个名为 Sales 的表,然后执行另一个 SELECT。第二个 SELECT 只是为了确保整个命令被视为有效,因为在 DROP 语句的情况下以 '; 结尾将不起作用,并且会使整个语句无效。

对于 `'` 字符,有一个简单的补丁。只需将单个 `'` 替换为两个,例如 `''`。在字符串中,`''` 被读取为单个真实的 `'` 而不是字符串结束分隔符。但是,还有其他字符可能会引起问题,因此最好的方法是使用命名参数,例如

SELECT Name FROM Users WHERE Name LIKE @Name

然后,`@Name` 由实际编程语言中的另一个语句填充,而 `'` 将始终被视为 `'` 而不是字符串分隔符。

信息泄露

如果您不了解 SQL,上面的示例可能很难理解。然而,由于它非常普遍,我决定展示它。

实际上,*SQL 注入* 通常需要攻击者了解数据库的某些信息。一个人如何删除一个他不知道存在的表?

在文件夹的情况下,所有使用过“cd”命令的人都知道 **..** 文件夹。在 *SQL 注入* 的情况下,了解 SQL 并不意味着您会了解数据库结构。但是,有两种方法可以做到

  1. 猜测。有些数据库结构非常简单,很容易猜测(例如,Person、User 或类似的表非常常见);
  2. 信息泄露。这可能是由于 Web 服务器配置错误或向用户提供完整的错误消息。例如,在那个错误的 SELECT 中,用户可以提供字符串 **'a**,数据库可能会抛出类似以下内容的错误

    无效的标记,第 42 列:SELECT Name FROM Users WHERE Name LIKE ''a';

    很好。通过这个,攻击者就知道存在一个名为 Users 的表。如果表名为 U1_Users,那么攻击者就会得到它。

事实上,有很多方法可以造成信息泄露。*SQL 注入* 本身可以用来读取不同的表和字段,这可能包括密码,例如,造成信息泄露……但是,一些错误消息可能会显示如此详细的问题,以至于攻击者可以从中受益。所以,一方面收到“发生了一些错误”这样的通用消息很糟糕,另一方面,给攻击者提供如此完整的错误消息以帮助他们改进攻击也很糟糕。

网络通信

即使我到目前为止给出的所有示例都发生在服务器上并且涉及通信,它们也不是由通信本身引起的。如果这些应用程序/网站可以在本地使用,您也可以在本地利用它们的错误,而不管后果有多严重。

嗯,许多攻击实际上并不关心应用程序本身,它们试图利用正在使用的通信方式中的漏洞。例如,如果一个网站在登录时使用 HTTP,有人可以“嗅探”网络(在这个过程中,他们会接收网络上发送的所有数据包),他们将轻松看到发送到服务器的登录名和密码。我需要说这有多糟糕吗?

但是,通信本身还会发生更多可能出错的事情,而这些事情会变成问题,而且很多时候,那些有助于“更容易、更快”地编写东西的框架是许多漏洞的根源,无论应用程序的主要逻辑是否存在任何问题。所以,让我们看一些

反序列化问题

造成 DoS(拒绝服务)的简单方法是让正在读取的数据请求创建一个非常大的数组(例如 1GB),并且这些数据实际上非常、非常慢地传输(可能每 50 秒一个字节)。同时打开一些连接,执行一些这些请求,直到您开始收到内存不足的异常……并保持您的连接处于活动状态。普通用户将无法再使用该应用程序/网站,因为所有内存都被这些攻击请求占用了。

这种问题会发生在对 TCP/IP(或类似协议)进行序列化/反序列化时,对通信框架的幼稚实现上。

对于不知道的人来说,序列化是将编程对象转换为字节的过程。反序列化是将这些字节转换回编程对象的过程。并非所有通信都需要对象,但许多现代通信框架通过对象来实现这一点,因为这样更简单。

实际上,问题是由于通常的良好实践(SRP - 单一职责原则,SoC - 关注点分离等)的组合造成的

  • 应用程序告诉通信框架哪些方法/函数可以通过远程调用来调用;
  • 通信框架不断请求反序列化对象,这些对象应该指示要调用哪些函数;
  • 这些对象实际上是从从传输/字节流(如 TCP/IP)读取的数据中反序列化的;
  • 该字节流本应只接收字节,仅此而已。它不知道将要创建哪些对象或将调用哪些方法/事件。

实际上,字节流通常有一个可配置的超时时间,但如果严格遵循“关注点分离/单一职责原则”,即使是超时时间也会作为独立的“层”叠加在流之上。

所以,问题是这样的

  • 通常为每个数据包设置 60 秒的超时时间,并且数据包的大小限制为很小的尺寸(例如 8KB)。数据包是否包含预期的对象并不重要。只要有数据包进来(一个字节就可能是一个数据包),连接就会保持活动状态;
  • 反序列化器不关心数据包。它读取字节,并且通常根据读取的第一个字节来确定要创建的对象的类型和大小(因此,攻击者可以发送指示创建任意大小的字节数组的字节)。传输的数据包大小限制为 8KB 并不重要,反序列化器实际上会继续读取新的数据包,直到它可以填充数组;
  • 将要使用这些对象的代码可能根本不支持数组。然而,它只是要求反序列化一个对象,然后将该对象转换为特定的接口,或者有一个 switch(或类似)代码来决定如何处理刚反序列化的对象。

实际上,当发生最后一步时,已经太晚了。攻击的目的是在发送数据的同时非常缓慢地分配大量内存。当对象完全反序列化时,可能会引发错误,内存可能会被回收,但这只会在服务器已经宕机很长时间之后才会发生。而且,请求一个新的巨大数组作为下一步很容易。

可能的解决方案是什么?

  • 在反序列化之前,必须设置可以反序列化的对象类型,以及可以消耗的最大数组/内存大小。不要反序列化整个对象然后才发现它不正确,甚至不要为不应该反序列化的对象分配内存;
  • 超时时间必须针对整个反序列化设置,而不是每个数据包。也就是说,它不应该在字节流之上,而应该在反序列化器之上。

最大的问题是,大多数反序列化器不允许您验证即将创建的对象或指定有效类型。它们做好自己的工作,您不应该干涉(实际上我们可以说它们正确地遵循了单一职责原则)。

请注意,这种对通信协议的幼稚实现,在没有人试图攻击应用程序时(例如在本地环境中)工作得很好。可以保证应用程序将始终尽快发送完整的对象,并且只会序列化有效对象。然而,攻击者可以完全忽略对象创建和数据序列化过程,并按其认为合适的方式发送自己的字节。

其他反序列化问题

可能还会发生另外两个反序列化问题,它们都与能够实际加载程序集的反序列化器有关。想想看:当您关闭应用程序时,您会序列化一些代表当前应用程序状态的对象。这些对象及其库都在内存中。但是,当您打开应用程序时,这些对象和库仍然不在内存中,反序列化的目的是恢复先前的状态。在这种情况下,您显然希望加载所需的库。

所以,可能发生的问题是

  1. 攻击者可能会强制您的应用程序加载大量您的应用程序不期望的程序集和类型,导致它使用比预期更多的内存,甚至引起一些冲突,因为许多库和类型都有自己的初始化,当加载相同库的两个版本时尤其成问题;
  2. 一些反序列化器甚至会缓存失败的库/类型。因此,通过请求加载无限列表的库(例如 a.dll、b.dll、c.dll 等)很容易造成拒绝服务。这些库是否存在并不重要,所有内存都将用于存储缓存中的错误。

多种身份验证方法 - 攻击者的选择

许多通信框架允许同时创建多个绑定。很多时候,这是一个简单的决定,比如

  • 为本地通信创建一个二进制和未加密的绑定,因为它最快;
  • 为外部通信创建一个 HTTPS 绑定,因为它更标准和安全。

但有时,人们会忘记一个细节:他们将二进制和未加密的绑定保持打开状态,并允许互联网访问。有些人会找借口

“外部用户不应该使用二进制序列化。如果他们这样做并且有人窃取了他们的凭据,那将是他们的错。”

这是对的。但我们回到了反序列化的问题。攻击者自然可以从外部定位二进制绑定,仅仅因为他知道如何利用它造成*拒绝服务*。所有真正的外部用户只使用 HTTPS 并不重要。黑客仍然有机会使用易受攻击的协议。

透明远程处理与通信框架

也许远程处理框架和通信框架之间没有官方区别,但我通常看到“远程处理”一词用于通信是“透明”的情况。该类不是为远程通信而创建的,但由于远程处理框架,可以做到这一点。

根据这个定义,通信框架是不同的。可以通过通信框架访问的类必须是为了这样使用而创建的。

我必须说,我通常更喜欢远程处理框架。这意味着我可以创建一个在本地运行良好的库,如果我的需求发生变化,我可以在这个库之上创建一个服务,然后使其可供许多不同的服务器访问。也就是说,它将帮助我轻松地从一个简单的库扩展到一个*面向服务的架构*。然而,这很少适用于将对象提供给任何外部网络。

这里的问题很微妙,但事实是大多数库允许开发人员做比通常需要的多得多的事情。尽管如此,一个好的应用程序只会使用有效的调用。但是当你通过远程处理框架公开这样的库时会发生什么?

实际应用程序将继续正确使用该库。然而,该库现在可以被其他应用程序使用。谁能保证其他应用程序(包括由攻击者创建的应用程序)不会做坏事?

通信框架更好吗?

如果透明远程处理不好,通信框架又如何更好呢?

嗯,实际上,它并不是真的“更好”。它是“明确的”。

在透明远程处理中,我们只获取一个类并说:它可以从外部世界使用。您甚至不需要知道该类的所有方法,您只是使其可用。非常容易,但您可能会暴露比需要的多得多的内容。

使用通信框架,您必须明确您想暴露什么。这不能保证您不会暴露太多,但肯定的是,如果您忘记暴露一个方法,它将不可用。更容易发现您忘记暴露一个必需的方法(这将导致您的应用程序失败),而不是发现您暴露了太多(这只有在攻击者实际利用了这种漏洞时才会显现)。

此外,大多数开发人员在创建他们知道将在互联网上公开的类时,与创建将在本地使用的类时,具有不同的心态。“安全”特性对于本地调用通常仅限于验证输入参数和线程安全,而对于暴露给互联网的对象,其安全特性则复杂得多。

暂时就到这里

当我开始写这篇帖子时,我心里想的是更多的“底层”攻击,但就个人而言,我认为这个列表足以说明在谈论安全问题时,事情“多么容易”就会出错。

也许下次我会探索更多底层案例并展示一些实际代码。

希望您喜欢,也希望这篇帖子能让您更多地思考安全问题以及事情是多么容易出错,即使应用程序“工作正常”。

 

© . All rights reserved.