你好,我是王昊天。

英文里有一句谚语你一定听说过,“Curiosity killed the cat”,翻译成中文是“好奇害死猫”。这句话是想告诉人们,不要为那些不必要的尝试去冒不可预知的风险,提醒人们不要过分好奇,否则会给自己带来伤害。但是在网络安全和编程领域,这句话却并不正确,因为正是对未知的好奇与探索,我们才能深入底层原理,写出更优雅的代码,构建出更安全的系统。

要知道,当处于对某种事物的属性出现认知空白时,动物往往想要弥补这份空白,是一种的非常本能心理,好奇心也由此产生。可以想象这样一个场景:当你构建了一个崭新的系统出现在用户面前,如果他们对这个系统充满了探索欲,那么这对于你来说无疑是非常愉快的体验。但是作为系统的构建者,你一定希望用户按照预先设定的规则、在预先搭建好的框架下开始他的旅程。而现实往往并非如此……

你会发现,如果一个系统不存在有效的访问控制,那么这个系统一旦向用户开放使用,你将面临一场“灾难”。

访问控制

那么什么是访问控制呢?访问控制是一种策略,在这种策略的控制下,用户的操作不能逾越预设好的权限边界。而访问控制一旦失效通常会导致未认证信息泄露、内部数据篡改、数据删除和越权操作等后果。访问控制失效型问题通常有以下几种类型:

  1. 系统在实现过程中违背了“最小权限原则” 或 “默认拒绝原则”,在这种情况下用户可以获得一些特殊权限,而这些特殊权限原本只应该授权给特定的用户或角色;
  2. 通过修改URL地址、内部程序状态、HTML页面,或者使用Cyber工具修改API请求的方式绕过访问控制;
  3. 通过提供唯一ID的方式预览或者修改其他账户信息及数据;
  4. 未经过访问控制地通过POST、PUT和DELETE方法访问API;
  5. 通常意义上的提权,比如未登录状态下的用户操作,或者常规用户登录状态下的管理员操作;
  6. 元数据操纵,比如重放或者修改JWT(JSON Web Token)访问控制令牌,或者通过操纵Cookie的方式进行提权;
  7. CORS误配置,可以导致来自未认证源的API访问。

这里我们首先来看几种简单攻击场景。

  1. 这个应用在SQL调用中直接使用了未经验证的数据,并利用该数据进行信息查询:
pstmt.setString(1, request.getParameter("acct"));
ResultSet results = pstmt.executeQuery();

对于一个攻击者而言,只需要简单地在浏览器地址栏中修改acct参数,即可对SQL语句进行操纵,而在未经验证的情况下,该攻击者可以访问到其他账户的信息。

https://example.com/app/accountInfo?acct=notmyacct
  1. 一个攻击者可以很轻松地修改URL地址,尝试去访问他的目标链接,比如这里攻击者试图通过URL地址修改直接访问admin页面:
https://example.com/app/getappInfo
https://example.com/app/admin_getappInfo

如果攻击者成功访问了第二个链接,那么说明系统在权限设计和访问控制上就是存在问题的。

  1. 由于实现过程中未对用户访问参数设置边界,导致了很多越权问题的发生:
https://example.com/order/?order_id=2021102617429999

攻击者可以尝试修改上述API接口中的order_id参数,使其在程序接口上的输入合法,但是对于用户而言却是越权行为。

  1. HTTP PUT方法最早目的用于文件管理操作,可以对网站服务器中的文件实现更改删除的更新操作,该方法往往可以导致各种文件上传漏洞,造成严重的网站攻击事件:
put /root/Desktop/shell.php

上述代码在支持PUT方法的环境中,上传Webshell进行提权;在实际运用中,若必须启用该方法,则需要对该方法涉及文件资源做好严格的访问权限控制。

  1. Web应用将身份认证结果直接存储在Cookie中,并未施加额外的保护措施:
Cookie: role=user --> Cookie: role=admin

通过在Web前端拦截Cookie,并进行Cookie内容修改即可提权。

  1. 有些开发者为了方便,直接在Access-Control-Allow-Origin中反射请求的Origin值:
add_header "Access-Control-Allow-Origin" $http_origin;
add_header “Access-Control-Allow-Credentials” “true”;

这是一个错误的Nginx配置示例,这样的配置意味着信任任何网站,攻击者网站可以直接跨域读取其资源内容,窃取隐私数据。

案例实战

通过刚才几个简单的场景,相信你对访问控制已经有了一个初步的认知。接下来我们再来看一个更为复杂的攻击场景。

意外的代理访问

如果一个应用从输入流组件接受到了外部的请求、消息或者命令,并且这些请求、消息或者命令没有经过有效的请求源处理就被转发到应用控制范围外的某个资产中,这时就会诱发一种访问控制失效风险。

想象一下,当你还在读书的时候,在学校里同学给了你一份蛋糕,希望你回家后可以转送给你的妹妹,而你到家之后由于疏忽直接把蛋糕给了妹妹并且没有任何说明,这时妹妹会以为这份蛋糕是你买给她的礼物,这就造成了一种误解。

回到我们所描述的技术场景,这个应用就是“你”的身份,而请求被转发的目标资产就是“你的妹妹”,这个应用此时成为了一种代理或者中间人的角色,使得不应该被访问到的资产被意外访问到了。

再稍微拓展开讲一讲,如果一个攻击者不能直接访问目标,但是一个应用可以,这时攻击者可以发送请求到应用,再让应用转发请求到最终目标。这种情况下,攻击请求看起来像是来自应用的访问,而非真实攻击者。这种攻击的效果很直观,可以直接绕过访问控制(如防火墙)或者隐蔽恶意请求源信息。

我们不仅需要知道这种风险的威力,还需要了解这种风险在什么情况下会出现。由于类似的消息转发或者代理功能大部分情况下都是正常功能,因此我们不能因噎废食,仅仅是因为有可能存在安全风险就抛弃正常功能。这就需要我们去了解,在什么情况下这种功能会变成一种漏洞:

  1. 应用本身的权限和用户可以操纵的输入流组件所属的权限不同;(条件A)
  2. 攻击者并不能够直接发送请求到最终目标资产;(条件B)
  3. 攻击者能够创建一个可以被转发的请求,这个请求可能:

用简单的公式来描述的话,就是只有在“A && B && ( C || D )”的情况下,消息转发或者代理功能才会成为一种安全风险或者安全漏洞。

CVE-2010-1637漏洞

接下来我们看一个非常有趣的漏洞,它的编号是CVE-2010-1637。这个漏洞会影响SquirrelMail 1.4.20以及更早的版本,漏洞主要的发生点是Mail Fetch组件,由于该组件是SquirrelMail的默认组件,因此该漏洞影响力还是很大的。如果该漏洞存在,那么经过认证的远程用户就可以得到允许绕过防火墙限制,将SquirrelMail作为一个代理(proxy)进一步扫描内网。

mail_fetch是在SquirrelMail 1.4.20版本的一个默认组件:

hunter@HunterdeiMac > ~/Downloads/squirrelmail-1.4.20/plugins > tree mail_fetch
mail_fetch
├── README
├── class.POP3.php
├── fetch.php
├── functions.php
├── index.php
├── options.php
└── setup.php
 
0 directories, 7 files

mail_fetch的主要功能是通过使用fsockopen()这个PHP函数来模拟POP3协议,并且仅支持了POST方式的认证,并没有对IP以及端口号进行检查:

...
if (!isset($port) || !$port) {$port = 110;}
    if(!empty($this->MAILSERVER))
    $server = $this->MAILSERVER;
 
    if(empty($server)){
      $this->ERROR = "POP3 connect: " . _("No server specified");
      unset($this->FP);
      return false;
    }
    // 老师加的注释
    // 此处缺乏对于服务器IP及端口号的检查
    $fp = @fsockopen("$server", $port, $errno, $errstr);
 
    if(!$fp) {
      $this->ERROR = "POP3 connect: " . _("Error ") . "[$errno] [$errstr]";
      unset($this->FP);
      return false;
    }
...

经过简单的分析,可以发现该代码段符合“A && B && ( C || D )”的漏洞存在条件,该处应该存在失效的访问控制。

找到了切入点,接下来你就可以通过下面三个步骤实现汽车变成汽车人的精彩过程——将SquirrelMail变成Nmap扫描器

  1. 服务端先返回消息的Service,比如SSH这种,通过对TCP服务的Banner信息抓取,可以了解目标资产提供的Service;
  2. 客户端先发送消息的Service,比如HTTP这种,POP3对象在建立完TCP三次握手之后会进入阻塞状态,通过fgets()函数设置硬编码超时时间,可以判断目标端口是否开放;
  3. 由于仅仅支持POST方式认证,因此请求是以账户为前置条件去发送的,所以Cookie也是需要的。

那么到现在为止,我们已经有了“汽车人”变形之后的完整功能设计图:

./squirrel-nmap [Target] [IP] [TCP_PORT] [Cookie]
- Target suqiremail的URL地址,如http://target.com/sqm/
- IP 待扫描的IP地址
- TCP_PORT 尝试探测的TCP端口号
- Cookie 经过认证的Cookie

通过简单的实现,我们来看看实际的战斗威力如何:

./squirre-nmap "http://target.com/squirrelmail-1.4/" 192.168.1.4 22 "key=dTPc00s%3D;SQMSESSID=2600633c256570917fe25d7773eb41b3"
Fetching from 192.168.1.4:22
Oops, POP3 connect: Error [SSH-2.0-OpenSSH_5.1p1 Debian-3]

可以看到能够成功地对内网IP进行Service进行探测。

安全建设方案

通过这次探讨,你可以发现访问控制是授权或拒绝特定用户请求的过程。这个过程只有在应用开发的初始阶段就经过良好的设计,才能避免后续问题的发生。接下来我会提出一些优秀的访问控制设计原则,供你在实践中参考:

1. 优先开始设计访问控制体系
访问控制不仅是应用安全设计的一项主要事务,而且应当被设置在非常优先的位置,因为往往访问控制的设计在起步阶段是相对简单的,但是会很快随着功能点的增多快速复杂化。所以,如果你考虑使用成熟的软件框架来完成访问控制,一定要确保其能够满足你未来的应用定制化需求。

2. 强制所有请求经过访问控制检查
开发一个访问控制检查层(Layer),然后确保所有请求都在某种程度上经过这个检查层。以Java的filter为例,许多自动化的请求处理机制都是能够帮助我们实现这种需求的技术形态。

3. 默认拒绝
这是非常简单但是有效的策略,所谓默认拒绝是指,只要一个请求没有被指明是被允许的,那么它就是被拒绝的。

4. 不要硬编码角色
很多应用框架默认使用用户角色来进行访问控制,以下的代码形态是很常见的:

if (user.hasRole("admin") || user.hasRole("Manager")) {
  deleteAccount();
}

但是你要对这种Role-Based编码模式格外留意,因为它可能会带来以下几种风险:

因此这里我更推荐你使用这种编码方式:

if (user.hasAccess("DELETE_ACCOUNT")) {
  deleteAccount();
}

以属性或者功能为核心的访问控制编码模型,从特性上来讲更易于构建功能丰富的访问控制系统。

5. 记录所有的访问控制类事件
所有的访问控制失效都应该有完整的记录,因为这些事件很可能成为恶意用户尝试寻找系统漏洞的线索。

总结

这节课程我们学习了2021年OWASP TOP 10中排行第一的风险类别——失效的访问控制,访问控制是安全建设的第一关,也是最重要的一环 —— 它不仅和安全建设相关,更是产品功能的一个组成部分。

首先我们对访问控制的概念和范围进行了整体探讨,列举了一些典型的访问控制失效场景:

  1. 系统实现过程中违背最小权限原则;
  2. 修改API参数实现越权;
  3. 提供唯一ID给用户预览数据;
  4. 未对PUT、DELETE防范进行限制;
  5. 元数据(JWT、Cookie)操纵;
  6. CORS误配置。

然后针对这些典型的访问控制失效场景,我们展示了对应的问题代码。

在漏洞挖掘过程中,作为安全或者开发人员,我们要先对待挖掘的漏洞有清晰的认知,对于一些稍复杂的逻辑漏洞最好能够进行清晰的数学描述。

这里我们以“意外的代理访问”类型漏洞为例,抽象出了一个数学模型——“A && B && ( C || D )”用于该类漏洞的判断,并利用模型成功分析了一个2010年真实发生的的0 Day漏洞。

在最后一部分,我们对访问控制层面的安全建设做了分析,并且给出了一些优秀的设计层、编码层的建议:

  1. 在系统构建阶段优先设计访问控制体系;
  2. 强制所有请求经过访问控制检查;
  3. 采用默认拒绝原则;
  4. 采取以权限为核心的编码原则;
  5. 记录所有访问控制类事件。

以我们所探讨的安全建设方案为基础,相信你可以进行优雅的编码并设计出优秀的访问控制系统,构建起你的应用安全的第一道防线。

思考题

除了这节课程我们提到的风险种类,访问控制失效你还能想到哪些风险类型呢?

期待你的思考,也欢迎你在留言中与我交流,或者转发给你的朋友加入讨论。我们下节课再见!