董沛然
(国家开发银行信息科技部,北京 100032)
软件的注入类漏洞,是风险等级很高的一类漏洞,在CVE、OWASP等权威信息安全漏洞库中,注入类漏洞长期位居前10名之列。它可以导致系统运行错误、或泄露客户信息等严重事故。
在所有注入类漏洞中,尤以SQL注入最为常见,危害也最大。
SQL注入,简单地说就是利用程序代码漏洞,绕过程序权限,将SQL命令插入到用户请求的查询字符串或者输入域进行攻击,其结果轻则获得敏感信息和数据,重则控制服务器[1]。
攻击者(黑客)在利用SQL漏洞实施攻击时,通常会选择直接注入或二次注入两种方式。在直接SQL注入方式中,直接将代码插入到用户输入变量,该变量与SQL命令串联在一起;在二次SQL注入方式中,将恶意字符串通过报文植入数据库表中,未来在程序读取该库表字段时,被动态拼接为SQL命令,并执行。
下面以一个简单的直接注入SQL漏洞为例,说明SQL注入的原理。以下代码动态地构造并执行了一个SQL查询,该查询可以搜索与指定客户名称相匹配的交易记录。
假设有如下C语言代码段:
Receive_Input(&szCUSTOM_NAME); //接收用户输入的查询条件——注入点
上述代码的逻辑是:接收用户在界面输入的姓名,将其作为查询条件拼接到SQL语句中,再在数据库中完成查询操作。
正常情况下,当用户输入客户名称David时,拼接成的SQL语句为:
select * from TABLE_TRANSACTION where CUSTOM_NAME='David';
此时程序将产生正确的输出。
但是当这段代码被黑客攻击时,如果黑客输入的内容为:
这样一来,将会有两条SQL语句被执行,虽然其中前一条SQL语句是安全的,但后一条却可以查询出库表中的所有记录,造成严重的信息泄露。
这里涉及一些概念。在这个例子中,程序从用户输入获取了szCUSTOM_NAME字段,在未经过滤的情况下,将其值直接以字符串拼接的方式拼接到一个SQL语句里面,造成了SQL注入漏洞。
在这段程序中,直接从用户输入获取信息的代码行Receive_Input(&szCUSTOM_NAME)称为这个SQL注入漏洞的注入点。执行危险SQL语句的代码行Execute(szSQL)称为这个SQL注入漏洞的爆发点。从注入点到爆发点之间,所有涉及危险字符串szCUSTOM_NAME信息赋值的语句,称为这个SQL注入漏洞的传递链。
命令注入漏洞,是指通过提交恶意构造的参数破坏命令语句的结构,达到非法执行命令的手段。命令注入漏洞常发生在具有执行系统命令的Web应用中。
下面举一个命令注入漏洞的例子。以下代码在服务器上动态地为用户建立专用目录,路径名是用户输入的客户名称。
假设有如下PHP语言代码段:
如此看来上述代码也是存在很大风险的。
在本例中,同样涉及注入点、爆发点、数据流的概念。从用户输入获取cu st om Na me的代码行$customName = $_POST["customName"]称为这个命令注入漏洞的注入点。执行这个危险命令的代码行system($command)称为这个命令注入漏洞的爆发点。从注入点到爆发点之间,所有涉及危险字符串customName信息赋值的语句,称为这个命令注入漏洞的传递链。
从上面两个例子可以看出,注入类漏洞的本质是,黑客通过引号、分号、斜杠、点号等特殊字符,闭合了原有的程序逻辑,使得用户提交的参数和程序本来的逻辑相互干扰。这样一来,黑客提交的参数就不仅仅只起到参数的作用,而且还进入到程序逻辑里来了。黑客就是通过这样的方式,把恶意代码嵌入到正常代码里来的。
根据注入类漏洞的可利用性和危害性,目前业界一般将其分为三类。
(1)高危漏洞:可以直接被利用的漏洞,并且利用难度较低。利用之后可能对网站或服务器的正常运行造成严重影响、对用户财产及个人信息造成重大损失。
(2)中危漏洞:利用难度极高,或满足严格条件才能实现攻击的漏洞,或漏洞本身无法被直接攻击,但能为进一步攻击起较大帮助作用的漏洞。
(3)低危漏洞:无法直接实现攻击,但可能造成一些非重要信息的泄露,这些信息可能让攻击者更容易找到其他安全漏洞。
从上节可见,注入漏洞的本质是利用程序代码缺陷,绕过程序的权限,将恶意代码插入到用户请求的查询字符串或者输入域进行攻击。
为了防范注入类漏洞,业界主流的做法是加强对用户输入内容的校验。通常在各种大型信息系统的界面中,诸如时间、日期等大多数输入框的格式是固定的,黑客无法通过这些输入框注入攻击信息。但是对于姓名、备注栏等较为开放的输入框,系统一般不对输入内容做过多校验,并且此类输入框的长度一般较长,足够黑客输入可运行的恶意内容。
有鉴于此,出于防范注入类型攻击的目的,笔者认为进行大型信息系统的开发时应做到以下5点。
在编码层面,编写安全性高的源代码,避免SQL拼接,改用占位符方式实现SQL组装。重视源代码安全检查,因为源代码安全检查可以暴露一部分明显的安全隐患(如SQL拼接等)[2]。
在前面的案例中,造成SQL注入攻击的根本原因在于攻击者可以改变SQL查询的上下文,使本应作为数据解析的数值,被篡改为命令了。为避免这种问题,以占位符形式给SQL语句传参是一种十分有效的方法。
使用占位符生成SQL语句的示例代码段如下:
在上面的示例代码段中,问号表示占位符。在程序编译时将参数传入SQL语句,生成合法的SQL语句。如此一来,非程序自身的数据不参与SQL语句逻辑的构成,那么黑客精心设计的危险字符串也就不会奏效了。
对所有开放型输入框,进行恶意关键字的校验,如前面案例中的delete、rm、分号、斜杠等字符串应尽量过滤掉。危险字符串可用穷举或正则表达式的方式识别[3]。过滤时,可以考虑去掉这些可能隐藏恶意攻击意图的字符串,或用星号替换。
坚持最小展现原则,客户端的展示信息不宜过细。这是因为黑客在利用注入手段攻击系统时,往往免不了“猜”和“试”的过程。黑客通常通过反复试探,并借助系统的各种返回信息、消息、日志内容来猜测其开发语言、后台架构、甚至库表字段名称等细节信息[4]。因此,我们在软件开发中,要尽量避免给用户暴露系统细节(如应用程序信息、数据库信息、或其他容易暴露后台逻辑的信息)[5]。不但要避免在客户端上显式地展示这些信息,也不宜写在消息或日志里返回给客户端,因为黑客可以利用流量抓包软件抓取到这些消息或日志信息。
系统的每一个进程、每一次数据库操作,应该使用可以完成该任务的最小权限运行。任何需要提权的操作,都应尽可能只保持最短的时间,一旦任务完成,应该立即收回权限,这样可以减少攻击者在高权限的条件下执行恶意代码的机会。
系统在进行恶意关键字的过滤时,不仅需要考虑本系统所用的开发语言,还应考虑有关联关系的其他下游系统。一个大型复杂系统,往往是由众多小型子系统构成的。如图1所示。
图1 大型复杂系统模型(无输入校验)
其中,接入渠道子系统A主要负责接收用户的输入信息,并做一些简单的处理。而后,根据一定的逻辑,A将数据流输入到后台处理子系统B和后台处理子系统C,进行一些更为复杂的处理。
如前所述,由于系统遭受的注入类攻击主要来自于用户输入,所以接入渠道系统一般拥有比较完善的校验规则,如下图所示,接入渠道子系统A的输入校验模块应该可以过滤针对本子系统A的恶意字符。而后台处理子系统B和C由于并不直接面向用户,所以在开发时,往往这类子系统的安全性校验就不那么完整和严谨了。如图2所示。
图2 大型复杂系统模型(在接入渠道子系统A中加入输入校验)
但是,经过接入渠道子系统A处理过的数据会流入后台处理子系统B和C中,并且B、C所用的开发语言、数据库、部署的操作系统可能与接入渠道系统完全不同,黑客的恶意内容完全有可能躲过了前面的校验,而待转入后面的系统后发起攻击。这就如同地铁线路中,一颗炸弹一旦从某站流入地铁,就可以畅通无阻地流窜到任意线路、任意站点。所以要想保证所有线路的安全,就要确保全市任一地铁站,与重要站点的安检同样严格。因此更好的做法是,在每个子系统中,都针对本子系统的技术特点,进行有针对性的校验。如图3所示。
图3 大型复杂系统模型(在每个子系统中都加入输入校验)
当然,这种各个子系统分别校验的方式容易形成“各自为政”,可能接入渠道子系统A中已经实现了的输入校验,在后台处理子系统B和C中又重复实现了,这必然会降低系统的开发效率和运行效率。
有鉴于此,最好的方式是在接入渠道子系统A的前面,建立专门的校验系统,涵盖针对所有子系统的潜在危险字符校验,供整个系统群使用。如图4所示。
图4 大型复杂系统模型(在接入渠道子系统A前加入专门的输入校验模块)
这样不仅达到了全面校验的效果,并且也避免了重复校验的危害,充分保证系统群的整体运行效率。不仅如此,从开发人员安排上,可以请专门的信息安全人员编写此安全模块(或子系统),在保证专业性的同时,也有利于其他子系统的开发人员将精力集中于业务处理上。
本文首先介绍了注入类安全缺陷的定义和原理,然后,从安全架构、安全编码的角度,提出了5点防范注入类缺陷的措施:以占位符形式给SQL语句传参、严格进行恶意关键字的校验、客户端的展示信息不宜过细、坚持最小权限原则、遵循系统的整体安全架构模型。最后,从安全测试的角度,提出了基于复合引擎的自动化代码安全检查平台的构建方法,并阐述了其优势、原理和具体算法。■