访问控制是一种策略,在这种策略的控制下,用户的操作不能逾越预设好的权限边界。而访问控制一旦失效通常会导致未认证信息泄露、内部数据篡改、数据删除和越权操作等后果。访问控制失效型问题通常有以下几种类型:
用户轻松的修改URL的参数内容,从而轻而易举的访问到admin页面。一下是两个假设URL,第一个是普通用户,第二个是管理员用户页面,如果普通用户访问第二个页面成功,那就说明访问控制策略存在问题。
https://example.com/app/getappInfo
https://example.com/app/admin_getappInfo
由于实现过程中未对用户访问参数设置边界,导致了很多越权问题的发生,下方示例URL中,攻击者可以尝试修改 API 接口中的 order_id 参数,使其在程序接口上的输入合法,但是对于用户而言却是越权行为。
https://example.com/order/?order_id=2021102617429999
HTTP PUT 方法最早目的用于文件管理操作,可以对网站服务器中的文件实现更改删除的更新操作,该方法往往可以导致各种文件上传漏洞,造成严重的网站攻击事件,下方示例代码,在支持 PUT 方法的环境中,上传 Webshell 进行提权;在实际运用中,若必须启用该方法,则需要对该方法涉及文件资源做好严格的访问权限控制。
put /root/Desktop/shell.php
Web 应用将身份认证结果直接存储在 Cookie 中,并未施加额外的保护措施,下方示例中,攻击者通过web前端拦截cookie,实现对cookie的修改,从而达到未授权访问的效果:
Cookie: role=user --> Cookie: role=admin
有些开发者为了方便,直接在 Access-Control-Allow-Origin 中反射请求的 Origin 值。比如如下配置,意味着nginx相信任何网站,允许所有访问跨域读取器资源,造成隐私数据被窃取。
add_header "Access-Control-Allow-Origin" $http_origin;
add_header “Access-Control-Allow-Credentials” “true”;
优先开始设计访问控制体系
访问控制不仅是应用安全设计的一项主要事务,而且应当被设置在非常优先的位置,因为往往访问控制的设计在起步阶段是相对简单的,但是会很快随着功能点的增多快速复杂化。所以,如果你考虑使用成熟的软件框架来完成访问控制,一定要确保其能够满足你未来的应用定制化需求。
强制所有请求优先经过访问控制检查
可以这种方法开发一个访问控制检查层(Layer),然后确保所有请求都在某种程度上经过这个检查层。以 Java 的 filter 为例,许多自动化的请求处理机制都是能够帮助我们实现这种需求的技术形态。
默认拒绝
这是非常简单但是有效的策略,所谓默认拒绝是指,只要一个请求没有被指明是被允许的,那么它就是被拒绝的。
记录所有的访问控制类事件
所有的访问控制失效都应该有完整的记录,因为这些事件很可能成为恶意用户尝试寻找系统漏洞的线索。
简单来说,你所构建的系统中有一个功能组件使用外部输入来构建文件名,而这个文件名会用来定位一个在受限目录的文件,如果文件名中既包含一些特殊元素,又没有进行合理的过滤处理,就会导致路径被解析到受限文件夹之外的目录。
扩展开讲一讲,很多系统内部的文件操作都希望被限制在特定目录中进行。通过使用…以及/符号,攻击者可以进行文件路径逃逸。其中最常见的符号组合是…/,这种符号组合在操作系统中会被解析为上级目录,这种漏洞被称为相对路径穿越。绝对路径穿越是另一种类型的路径穿越,比如/usr/local/bin就是典型的例子。
实例1:
一种典型的社交网络应用代码,每个用户的配置文件都被存储在单独的文件中,所有文件被集中到一个目录里:
$dataPath = "/users/example/profiles";
$username = param("user");
$profilePath = $dataPath . "/" . $username;
// 可以发现并没有对用户传入的username参数进行验证
open( $fh, "<$profilePath") || ExitError("profile read error: $profilePath");
print "\n"
;
while(<$fh>) {
print "$_ \n";
}
print "\n";
当用户尝试访问我们的的配置文件的时候,会组成如下路径:
/users/example/prfiles/hunter
但是这里并没有对我们的输入的访问文件名进行过滤,攻击者如果抓包修改访问文件问../../../../../../etc/passwd
这样的文件路径,完整路径如下:
/users/example/prfiles/../../../../../../etc/passwd ==》/etc/passwd
从而导致了路径穿越。
实例2:
下面这个代码在编写过程中考虑到输入的不安全性,采用了黑名单方式,过滤掉了输入中包含的../
字符,但是可见并没有抵用-g来进行全局参数匹配:
$username = GetUntrustedInput();
// 这里是老师写的注释
// 黑名单方式过滤
// 对username的过滤不严格
$username = ~ s/\.\.\///;
$filename = "/home/user/" . $username;ReadAndSendFile($filename);
所以被黑名单过滤了第一个../
之后,后续的../
还是会继续拼接到路径中造成路径穿越。
实例3:
如下代码,虽然采取了白名单,但是依然存在路径穿越的问题:
String path = getInputPath();
// 这里是老师写的注释
// 白名单方式过滤
// 对path的限制不够严格
if (path.startsWith("/safe_dir/"))
{
File f = new File(path);
f.delete()
}
当攻击者的攻击payload是/safe_dir/../../../../../../../etc/passwd
这种形式的时候,依然后造成路径穿越。
简单来说,如果我们的应用系统向一个未得到访问授权的用户暴露了敏感信息,那么这就是一种敏感数据泄露风险。
敏感数据泄露即使在今天仍然是相当严重且普遍存在的一个风险点,主要原因是数据泄露并非一个纯粹的技术性问题,很多时候与业务流程、功能设计都息息相关。
单纯从漏洞危害程度来看,敏感数据泄露主要分为两种,一种是业务敏感数据泄露,另一种是技术敏感信息泄露。业务敏感数据的泄露危害性是巨大的,会直接影响到公司的品牌和业务运行;技术敏感信息泄露往往不能对应用系统安全性产生直接威胁,但配合其他漏洞的综合利用可以实现 1+1>2 的效果。
由于数据泄露风险的特殊性质,主要是很多时候风险来源是逻辑层面、设计层面或者权限层面的安全问题,导致传统的防御方案和扫描器等很难发挥作用,也正因如此导致其成为了排名第一的风险种类。
在开发及设计阶段,可以考虑引入威胁建模及审计工具,来协助我们发现在逻辑层面以及设计层面引入的安全风险。通过对系统功能以及业务流程的威胁建模,可以帮助我们消除许多常见的安全隐患。
权限不合理简单来说,是不合理的权限赋予、权限处理以及权限管理过程。这里所说的权限,指的是终端角色的一种属性。
那么什么是终端角色呢?你可以理解为,用户就是一个终端角色。与权限相关的赋予、处理以及管理过程,我们主要通过权限管理来统一实现。
权限管理就是能够赋予终端执行某种特殊操作的权利,比如在某些运维场景下,运维人员能够获得系统维护的权限,这其中就包括重启服务器权限——我们都知道服务器重启可不是常规操作权限。
场景一:高权限运行服务
在安装和运行组件的过程中,某些程序组件的运行环境设置的权限过高,导致低权限应用通过服务调用关系可以完成提权操作。比如我们的web服务器,如果直接一root权限或者administrator权限运行,一旦web应用出现漏洞,被getshell之后,就会导致root权限被直接获取。这一类的问题大多数发生在运维层面。
场景二:降权异常
实例代码:
def makeNewUserDir(username):
...
try:
raisePrivileges()
os.mkdir('/home/' + username)
lowerPrivileges()
except OSError:
return False
...
上述代码包含了一次短暂提权,开发者在完成目标操作后立即进行了降权,但要注意的是 username 作为一个外部输入的参数,可能由于各种原因(输入不合法、安全过滤不严格等)导致 mkdir 函数报错进而抛出异常,一旦触发这种情况 lowerPrivileges 函数就无法得到执行,程序将持续以高权限状态运行,可能会为后续漏洞利用过程提供舒适的环境。
针对权限相关的安全问题,在三个不同的阶段,我们分别有不同的方式加以防御和检测。
CSRF 的全名是 Cross-Site Request Forgery,中文名称是跨站点请求伪造,简单来说,就是让 Web 应用程序不能有效地分辨一个外部的请求,是否真正来自发起请求的用户,虽然这个请求可能是构造完整、并且输入合法的。
当一个 Web 应用在设计过程中没有充分考虑来自客户端请求的验证机制时,就可能会遇到 CSRF 问题。站在攻击者的视角来看,他可以通过一个 URL、图片加载或者 XMLHttpRequest 等方式,让用户触发一个自动化请求发送行为,这个请求在 Web Server 接受时会被认为是合法的。
如下代码,是让用户更新自己的信息:
<form action = "/url/profile.php" method = "post">
<input type = "text" name = "firstname" />
<input type = "text" name = "lastname" />
<br/>
<input type = "text" name = "email" />
<input type = "submit" name = "submit" value = "Update" />
form>
其中profile.php包含如下代码:
// initial the seesion in order to validate sessions
session_start();
// if the session is registered a valid user the allow update
if ( !session_is_registered("username") )
{
echo "invalid session detected!";
// Redirect user to login page
...
exit;
}
// The user session is valid, so process the request
// and update the information
update_profile();
这里的 PHP 代码中是包含了一些保护措施的,结合我们前面几节课程学到的内容来看,它包含了用户身份的有效性认证,阻止了越权访问。但是上述代码并不能够有效地防止 CSRF 攻击,如果攻击者可以构建下面这段代码,并且将它托管到某个站点,那么当用户保持登录状态并且访问攻击代码页面时,就会触发攻击代码:
<script>
function attack()
{
form.email = "[email protected]"
form.submit();
}
<script>
<body onload = "attack()">
// ...
body>
可以看到,上述攻击代码包含了用户在使用浏览器时不可见的内容,当攻击代码在浏览器中加载时,会触发 attack 函数。如果用户在访问受害网站时保持的登录状态,受害网站就会收到来自用户的请求,请求内容是将 E-mail 更新为攻击者的邮件地址.
通过上述典型的攻击代码,我们可以总结出几点 CSRF 攻击特征:
同源策略
该防御策略的产生主要为了针对 CSRF 攻击的第一个特征——跨域场景,它的设计思路主要是禁止外域(或者不受信任的域名)对 Web Server 发起请求。在 HTTP 协议中,有两个 Header 字段可以用来帮助我们判断来源域:Origin Header 和 Referer Header。这两个字段在浏览器发送请求时会自动携带,并且不能由前端修改。
Token
CSRF Token 如何实现呢?为每一个 form 表单生成唯一的 token,并且在 form 提交时验证 token,就是 CSRF Token 的实现思路,但是 token 需要保证不可预测。在代码实现上主要有 2 种思路。
第一种是在用户访问页面时,由服务器生成 Token,将生成的 Token 存放于 Session 中,一般 Token 生成时会通过加密算法实现,输入一般包括随机字符串、时间戳等.
第二种是通过JS遍历DOM树结构来插入token。
安全的接口设计
现在的防御方案,主要考虑的是如何防止跨域的 CSRF。因为攻击者无法获取到 Token,所以大家会普遍认为,本域发生的 CSRF 暂时是安全的。但是,如果 XSS 和 CSRF 问题同时在本域发生,由于 XSS 可以让攻击者获取 Token,CSRF 的防御就宣告失效。因此我们需要在 Web 应用设计和开发过程中,严格过滤用户的输入,确保用户不能够输入我们不希望出现的内容,这样可以同时规避掉 XSS 和 CSRF 安全风险。
双重cookie
在 Web 应用开发中新增 CSRF_Token 机制还是稍有些麻烦,那么我们该如何通过现有的组件,来实现 CSRF 防御方案呢?答案是双重 Cookie。
当用户访问 Web 网站时,Web 应用为用户随机生成一个新的 Cookie 值,当 Web 应用每次执行表单提交操作时都需要携带这个 Cookie 值;由于同源策略的保护,攻击者无法获取或者修改这个 Cookie 项,因此实现了 CSRF 的保护。