在研发人员眼中,编码开发的目的是实现相关功能逻辑可用,无明显功能 bug。而实际上,在安全人员眼中,很多这样看似没有功能问题的代码,却可以利用来进行安全漏洞攻击。虽然这在很多研发人员眼中是看似天方夜谭,但很不幸,通过以往的无数重大安全事件的验证,这个事实客观存在。
本文主要针对三类最有代表性、安全威胁等级最高的安全漏洞进行着重分析,从安全角度介绍看似合理的功能实现代码是如何被 “攻破” 的。
一、SQL注入
所谓 SQL 注入,就是通过把 SQL 命令插入到 Web 表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。具体来说,它是利用现有应用程序,将(恶意的)SQL 命令注入到后台数据库引擎执行的能力,它可以通过在 Web 表单中输入(恶意)SQL 语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行 SQL 语句。
由于用户的输入, 也是 SQL 语句的一部分, 所以攻击者可以利用这部分可控制内容, 注入自己定义的语句, 改变SQL语句执行逻辑, 让数据库执行任意自己需要的指令. 通过控制部分SQL语句, 攻击者可以查询数据库中任何自己需要的数据, 利用数据库的读写文件等特性, 可以直接获取数据库服务器的系统权限。
举例来说,以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
...
String userName = ctx.getAuthenticatedUserName();
String itemName=request.getParameter("itemName"); String query = "SELECT * FROM items WHERE owner = '"+ userName + "' AND itemname = '" + itemName + "'";
ResultSet rs = stmt.execute(query);
...
这一代码所执行的查询遵循如下方式:
SELECT * FROM items
WHERE owner =
AND itemname = ;
但是,由于这个查询是动态构造的,由一个不变的基查询字符串和一个用户输入字符串连接而成,因此只有在 itemName 不包含单引号字符时,才会正确执行这一查询。如果一个用户名为 wiley 的攻击者为 itemName 表单字段输入字符串 “name' OR 'a'='a”,那么构造的查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。
再更为极端一些,如果这个用户名为 wiley 的攻击者在itemName这个表单字段输入字符串 “name'; DELETE FROM items; --”,那么构造成的查询语句将会变为两个:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name';
DELETE FROM items;
--'
第二个语句会造成什么结果,不言自明。
针对此类 SQL 注入攻击,较为有效的方式是使用参数化查询。
参数化查询 (Parameterized Query 或 Parameterized Statement) 是指在设计与数据库链接并访问数据时,在需要填入数值或数据的地方,使用参数 (Parameter) 来给值,这个方法目前已被视为最有效可预防SQL注入攻击 (SQL Injection) 的攻击手法的防御方式。
举例来说,我们可以把上面存在SQL注入攻击的例子改成如下代码:
...
String userName = ctx.getAuthenticatedUserName();
String itemName =request.getParameter("itemName");
#p#分页标题#e#String query = "SELECT * FROM items WHERE itemname=? AND owner=?";
PreparedStatement stmt =conn.prepareStatement(query);
stmt.setString(1, itemName);
stmt.setString(2, userName);
ResultSet results = stmt.execute();
...
通过这种参数绑定方式,无论攻击者在 itemName、userName 表单字段填入任何内容,这一代码所执行的查询永远遵循如下方式:
SELECT * FROM items
WHERE owner =
AND itemname =;
即通过参数化查询的方式将后台 SQL 查询语句固化,防止攻击者构造恶意参数拼接 SQL 语句导致的 SQL 语句篡改实现的SQL注入攻击问题。
二、跨站脚本 (XSS)
跨站脚本 (cross-site scripting,XSS) 是一种安全攻击,其中,攻击者在看上去来源可靠的链接中恶意嵌入译码。它允许恶意用户将代码注入到网页上,由于动态网页的 web 应用对用户提交的请求中的参数未做充分的检查过滤,允许攻击者在提交的数据中加入 HTML、JS 代码,未加编码地输出到第三方用户的浏览器,并最终导致攻击者构造的恶意脚本在用户浏览器中执行。
跨站脚本攻击危害十分严重,如可以窃取用户 cookie,伪造用户身份登录、可控制用户浏览器、结合浏览器及其插件漏洞,下载病毒木马到浏览者的计算机、衍生 URL 跳转漏洞、蠕虫攻击、钓鱼欺骗等。
XSS 最为常见的两类攻击分别为反射型 XSS 和存储型 XSS,接下来我们将分别介绍。
以下 JSP 代码片段可从 HTTP 请求中读取雇员 ID eid,并将其显示给用户。
<% String eid = request.getParameter("eid"); %>
...
Employee ID: <%= eid %>
如果 eid 只包含标准的字母或数字文本,这个例子中的代码就能正确运行。如果 eid 里有包含元字符或源代码中的值,那么 Web 浏览器就会像显示 HTTP 响应那样执行 eid 里的代码。
起初,这个例子似乎是不会轻易遭受攻击的。毕竟,有谁会输入导致恶意代码的 URL,并且还在自己的电脑上运行呢?真正的危险在于攻击者会创建恶意的 URL,然后采用电子邮件或者社会工程的欺骗手段诱使受害者访问此 URL 的链接。当受害者单击这个链接时,他们不知不觉地通过易受攻击的网络应用程序,将恶意内容带到了自己的电脑中。这种对易受攻击的 Web 应用程序进行盗取的机制通常被称为反射型 XSS。
再来看看存储型 XSS 的例子,以下 JSP 代码片段可在数据库中查询具有给定 ID 的雇员,并输出相应雇员姓名。
<%...
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from emp where id="+eid);
if (rs != null) {
rs.next();
String name = rs.getString("name");
}
%>
Employee Name: <%= name %>
如同上一个例子,如果对 name 的值处理得当,该代码就能正常地执行各种功能;如若处理不当,就会对代码的攻击行为无能为力。同样,这段代码暴露出的危险较小,因为 name 的值是从数据库中读取的,而且显然这些内容是由应用程序管理的。然而,如果 name 的值是由用户提供的数据产生,数据库就会成为恶意内容沟通的通道。如果不对数据库中存储的所有数据进行恰当的输入验证,那么攻击者便能在用户的 Web 浏览器中执行恶意命令。这种类型的攻击即成为存储型 XSS,即攻击者利用手段将恶意代码存入数据库,一旦该恶意代码被从数据库读取,相关指令就会被执行。该手法极其阴险狡猾,因为数据存储导致的间接性使得辨别威胁的难度增大,而且还提高了一个攻击影响多个用户的可能性。
#p#分页标题#e#一般存储型 XSS 攻击会从访问提供留言板、评论区等提供输入字段的表单的网站开始。攻击者会在这些留言板、评论区表单条目中嵌入 JavaScript,若后台未经验证就将其存入数据库,接下来所有访问该留言板、评论区的用户都会执行这些恶意代码。
针对 XSS 的防护,主要手段在于用户输入数据的验证,包括:
严格校验用户输入的数据,必须对所有输入中的 script、iframe 等字样进行严格的检查和 html escape 转义。这里的输入不仅仅是用户可以直接交互的输入接口,也包括 HTTP 请求中的 cookie 中的变量,HTTP 请求头部中的变量等。
校验数据类型,验证其格式、长度、范围和内容。
客户端,服务端进行双重校验。
对输出的数据也要检查,因为数据库里的值有可能会在一个大网站的多处都有输出,所以即使在输入做了编码等操作,在各处的输出点时也要进行安全检查。
三、任意命令执行
任意命令执行漏洞指的是 Web 应用程序未检测用户输入的合法性,直接传入程序中调用系统命令的函数中如: system(),eval(),exec(),从而可能会导致攻击者通过构造恶意参数,在服务器上执行任意命令。
攻击者通过构造恶意代码,执行任意命令可获取服务器权限,导致服务器上的重要数据,如:程序代码、数据库信息、文档资料等泄露。
举例来说,下面这段来自系统实用程序的代码根据系统属性 APPHOME 来决定其安装目录,然后根据指定目录的相对路径执行一个初始化脚本。
...
String home = System.getProperty("APPHOME");
String cmd = home + INITCMD;
java.lang.Runtime.getRuntime().exec(cmd);
该代码使得攻击者可通过修改系统属性 APPHOME 从而控制 INITCMD 的路径指向,从而提高自己在应用程序中的权限,继而随心所欲地执行命令。由于程序不会验证从环境中读取的值,所以如果攻击者能够控制系统属性 APPHOME 的值,他们就能欺骗应用程序去运行恶意代码从而取得系统控制权。
再看一个例子,下面的代码来自一个管理 Web 应用程序,旨在使用户能够使用一个围绕 rman 实用程序的批处理文件封装器来启动 Oracle 数据库备份,然后运行一个 cleanup.bat 脚本来删除一些临时文件。脚本 rmanDB.bat 接受单个命令行参数,该参数指定了要执行的备份类型。由于访问数据库受限,所以应用程序执行备份需要具有较高权限的用户。
...
String btype = request.getParameter("backuptype");
String cmd = new String("cmd.exe /K
\"c:\\util\\rmanDB.bat "+btype+"&&c:\\util\\cleanup.bat\"")
System.Runtime.getRuntime().exec(cmd);
...
这里的问题是:程序没有对读取自用户的 backuptype 参数做任何验证。通常情况下 Runtime.exec() 函数不会执行多条命令,但在这种情况下,程序会首先运行 cmd.exe shell,从而可以通过调用一次 Runtime.exec() 来执行多条命令。一旦调用了该 shell,它即会允许执行用两个与号分隔的多条命令。如果攻击者传递了一个形式为 "&& del c:\\dbms\\*.*" 的字符串,那么应用程序将随程序指定的其他命令一起执行此命令。由于该应用程序的特性,运行该应用程序需要具备与数据库进行交互所需的权限,这就意味着攻击者注入的任何命令都将通过这些权限得以运行。
为防止任意命令执行漏洞,应当禁止用户直接控制由程序执行的命令。在用户的输入会影响命令执行的情况下,应将用户输入限制为从预定的安全命令集合中进行选择。如果输入中出现了恶意的内容,传递到命令执行函数的值将默认从安全命令集合中选择,或者程序将拒绝执行任何命令。
#p#分页标题#e#有时还可以执行其他检验,以检查这些来源是否已被恶意篡改。例如,如果一个配置文件为可写,程序可能会拒绝运行。如果能够预先得知有关要执行的二进制代码的信息,程序就会进行检测,以检验这个二进制代码的合法性。如果一个二进制代码始终属于某个特定的用户,或者被指定了一组特定的访问权限,这些属性就会在执行二进制代码前通过程序进行检验。
四、总结&解决方案
上述三类安全漏洞,无一例外是在代码功能正常的前提下进行的,可见功能可用不代表安全可靠。而为解决这些问题,更多的是需要在研发过程中各环节介入安全能力,实现对上述各类漏洞的上线前检出以及修复,降低项目上线安全隐患。
企业应该将赋能服务贯穿需求分析、架构设计、研发、测试回归以及发布迭代全流程,通过赋能将专业安全能力赋予研发各环节人员,并在各环节提供不同工具(STAC、SAST、IAST、常态化安全运营)使赋能知识真实应用落地,最终以统一平台展示、分析、回归、闭环安全问题,并向安全部提供 SIEM,根据各流程频现的漏洞类型、研发人员知识盲区等再次提供针对性培训,最终针对性制定规章制度,实现制度精准逆推落地。
需求和架构阶段:基于业务场景的威胁建模 (STAC),以威胁建模赋能方式教会需求分析和架构审计人员对项目内场景潜在场景风险进行识别和剥离,通过威胁建模针对性提出安全方案,用于后续研发等环节的解决或规避。
软件编码阶段:静态应用安全测试 (SAST),通过与 git、svn 等代码仓库联动,自动化拉取全量或增量代码进行代码安全检查,以波谷时间检测方式在上班时间前根据提交历史以邮件形式同时相关责任人,降低对相关人员工作方式更改。
软件测试阶段:交互式安全测试 (IAST),IAST 通过代理、VPN 或者服务端 Agent 方式无感知获取功能测试人员测试交互流量,基于模糊测试 (fuzz) 思想对流量进行攻击代码随机插入和攻击流量构建,并自动化对被测程序进行安全测试,同时可准确确定漏洞所在的代码文件、行数、函数及参数。
上线迭代阶段:常态化安全运营,对项目上线后所在的服务器资产、中间件以及项目本身进行 7*24 小时周期性安全检查,相当于有一个安全团队或渗透测试工程师全天候管理线上资产、站点以及中间依赖的安全问题,有效确保安全健壮性。
【本文是51CTO专栏作者“李少鹏”的原创文章,转载请通过安全牛(微信公众号id:gooann-sectv)获取授权】
下一篇:特征工程之加密流量安全检测