Attacking OpenStack PDF
Attacking OpenStack PDF
COM
Attacking OpenStack
· Aug 5, 2024 · 18 min read
Table of contents
Apply Restrictive File Permissions
Incorrect Example
Writing Files with Python
Correct Example
Secure File Creation in Python
Verify Ownership and Group
Avoid Dangerous File Parsing and Object Serialization Libraries
Incorrect Usage
Correct Usage
Python Pipes to Avoid Shells
Incorrect
Correct
Unvalidated URL redirect
Incorrect
Correct
Validate Certificates on HTTPS Connections to Avoid Man-in-the-Middle Attacks
Incorrect
Correct
Create, Use, and Remove Temporary Files Securely
Python Temporary File Handling
Incorrect
Correct
Restrict Path Access to Prevent Path Traversal
Incorrect
Correct
Use Subprocess Securely
Incorrect
Correct
Parameterize Database Queries
SQLAlchemy
Incorrect
Correct
MySQL
Incorrect
Better
Correct
PostgreSQL (Psycopg2)
Incorrect
Correct
Protect Sensitive Data in Config Files from Disclosure
Incorrect
Correct
Consequences
Example Log Entries
Use Secure Channels for Transmitting Data
Clear Example
Less Obvious Example
Incorrect
Escape User Input to Prevent XSS Attacks
Incorrect
Correct
Resources
Show less
COPY
ls -l secureserv.conf
-rw-rw-rw- 1 secureserv secureserv 6710 Feb 17 22:00
secureserv.conf
The file permissions are set to 666 , allowing read and write access to everyone on
the system. This is insecure because it exposes the sensitive information contained
within the configuration file to all users.
Writing Files with Python
When writing files in a Unix-like operating system using Python, the file permissions
are influenced by the system's umask setting. By default, this may be too permissive
for sensitive data:
COPY
ls -l testfile.txt
-rw-r--r-- 1 user staff 4 Feb 19 10:59 testfile.txt
The file is readable by others, which is not secure for sensitive information.
Correct Example
To correct this, set restrictive permissions to ensure only the owning user can read
and write to the file. Update the file permissions as follows:
COPY
You can securely create a file in Python with appropriate permissions using os.open
with specific flags:
COPY
import os
It is essential to verify the ownership and group of the file, ensuring only the
necessary users have access. Follow the principle of least privilege: if group access is
not needed, do not grant it. This approach minimizes the risk of unauthorized access
and enhances the overall security of sensitive data files.
Incorrect Usage
import yaml
import pickle
conf_str = '''
!!python/object:__main__.AttackerObj
key: 'value'
'''
conf = yaml.load(conf_str)
Using pickle or cPickle with untrusted input can result in arbitrary code execution:
COPY
import pickle
import cPickle
Correct Usage
import yaml
conf_str = '''
- key: 'value'
- key: 'value'
'''
conf = yaml.safe_load(conf_str)
untrusted sources.
Here is a simple function that uses curl to grab a page from a website and pipe it
directly to the wordcount program to tell us how many lines there are in the HTML
source code.
COPY
import subprocess
def count_lines(website):
return subprocess.check_output('curl %s | wc -l' % website,
shell=True)
# >>> count_lines('www.google.com')
# '7\n'
(That output is correct, by the way - the Google HTML source does have 7 lines.)
The function is insecure because it uses shell=True , which allows shell injection. A
user who instructs your code to fetch the website ; rm -rf / can do terrible things to
what used to be your machine.
If we convert the function to use shell=False , it doesn’t work.
COPY
def count_lines(website):
args = ['curl', website, '|', 'wc', '-l']
return subprocess.check_output(args, shell=False)
# >>> count_lines('www.google.com')
# curl: (6) Could not resolve host: |
# curl: (6) Could not resolve host: wc
# Traceback (most recent call last):
# File "<stdin>", line 3, in <module>
# File "/usr/lib/python2.7/subprocess.py", line 573, in check_output
# raise CalledProcessError(retcode, cmd, output=output)
# subprocess.CalledProcessError: Command
# '['curl', 'www.google.com', '|', 'wc', '-l']' returned non-zero exit
status 6
The pipe doesn’t mean anything special when shell=False , so curl tries to download
the website called | . This does not fix the issue; it causes it to be more broken than
before.
If we can’t rely on pipes with shell=False , how should we do this?
Correct
COPY
def count_lines(website):
args = ['curl', website]
args2 = ['wc', '-l']
process_curl = subprocess.Popen(args, stdout=subprocess.PIPE,
shell=False)
process_wc = subprocess.Popen(args2, stdin=process_curl.stdout,
stdout=subprocess.PIPE, shell=False)
# >>> count_lines('www.google.com')
# '7\n'
Rather than calling a single shell process that runs each of our programs, we run them
separately and connect stdout from curl to stdin for wc . We specify
stdout=subprocess.PIPE , which tells subprocess to send that output to the respective
file handler.
Treat pipes like file descriptors (you can actually use FDs if you want); they may block
on reading and writing if nothing is connected to the other end. That’s why we use
communicate() , which reads until EOF on the output and then waits for the process to
terminate. You should generally avoid reading and writing to pipes directly unless you
really know what you’re doing - it’s easy to work yourself into a situation that can
deadlock.
Note that communicate() buffers the result in memory - if that’s not what you want,
use a file descriptor for stdout to pipe that output into a file.
It is common for web forms to redirect to a different page upon successful submission
of the form data. This is often done using a next or return parameter in the HTTP
request. Any HTTP parameter can be controlled by the user and could be abused by
attackers to redirect a user to a malicious site.
This is commonly used in phishing attacks. For example, an attacker could redirect a
user from a legitimate login form to a fake, attacker-controlled login form. If the page
looks enough like the target site and tricks the user into believing they mistyped their
password, the attacker can convince the user to re-enter their credentials and send
them to the attacker.
Here is an example of a malicious redirect URL:
COPY
https://good.com/login.php?next=http://bad.com/phonylogin.php
To counter this type of attack, all URLs must be validated before being used to
redirect the user. This should ensure the redirect will take the user to a page within
your site.
Incorrect
import os
from flask import Flask, redirect, request
app = Flask(__name__)
@app.route('/')
def example_redirect():
return redirect(request.args.get('next'))
Correct
The following is an example using the Flask web framework. It checks that the URL
the user is being redirected to originates from the same host as the host serving the
content.
COPY
def is_safe_redirect_url(target):
host_url = urlparse(request.host_url)
redirect_url = urlparse(urljoin(request.host_url, target))
return redirect_url.scheme in ('http', 'https') and
host_url.netloc == redirect_url.netloc
def get_safe_redirect():
url = request.args.get('next')
if url and is_safe_redirect_url(url):
return url
url = request.referrer
if url and is_safe_redirect_url(url):
return url
return '/'
@app.route('/')
def example_redirect():
return redirect(get_safe_redirect())
The is_safe_redirect_url function checks that the scheme of the redirect URL is
either http or https and that the netloc (network location part) of the redirect URL
matches the host URL. This ensures that the user is only redirected to URLs within
the same site.
The Django framework contains a django.utils.http.is _safe_url function that can
be used to validate redirects without implementing a custom version. This built-in
function provides a secure and efficient way to handle URL redirects, ensuring that
users are only redirected to safe and trusted URLs within the same domain.
import requests
requests.get('https://www.openstack.org/', verify=False)
The example above uses verify=False to bypass the check of the certificate received
against those in the CA trust store.
It is important to note that modules such as httplib within the Python standard
library did not verify certificate chains until it was fixed in the 2.7.9 release. For more
specifics about the modules affected, refer to CVE-2014-9365.
Correct
COPY
import requests
requests.get('https://www.openstack.org/',
verify='/path/to/ca_cert.pem')
The example above uses the CA certificate file to verify that the certificate received
is from a trusted authority.
Use Avoid
tempfile.TemporaryFile tempfile.mktemp
Use Avoid
tempfile.NamedTemporaryFile open
tempfile.SpooledTemporaryFile tempfile.mkstemp
tempfile.mkdtemp
Creating temporary files with predictable paths leaves them open to TOCTOU
attacks:
COPY
import os
import tempfile
import tempfile
open(tempfile.mktemp(), "w")
Correct
Use the secure methods provided by the tempfile module:
COPY
import tempfile
A path traversal attack occurs when an attacker supplies input used to construct a
file path, allowing access to unintended files. Mitigating these attacks involves
restricting file system access and using a restricted file permission profile.
Incorrect
app = Flask(__name__)
@app.route('/')
def cat_picture():
image_name = request.args.get('image_name')
if not image_name:
return 404
return send_file(os.path.join(os.getcwd(), image_name))
if __name__ == '__main__':
app.run(debug=True)
curl http://example.com/?image_name=../../../../../../../../etc/passwd
Correct
Use the following method to restrict access to files within a specific directory:
COPY
import os
if __name__ == "__main__":
import sys
main(sys.argv[1:])
Another approach is to use an indirect mapping between a unique identifier and a file
path:
COPY
localfiles = {
"01": "/var/www/img/001.png",
"02": "/var/www/img/002.png",
"03": "/var/www/img/003.png",
}
def get_file(file_id):
return open(localfiles[file_id])
COPY
def ping(myserver):
return subprocess.check_output(f'ping -c 1 {myserver}',
shell=True)
# >>> ping('8.8.8.8')
# '64 bytes from 8.8.8.8: icmp_seq=1 ttl=58 time=5.82 ms\n'
Correct
COPY
def ping(myserver):
args = ['ping', '-c', '1', myserver]
return subprocess.check_output(args, shell=False)
By passing a list of strings, the shell does not process user input as commands:
COPY
Incorrect
This example uses the built-in parameter substitution mechanism % to insert a value
into the query string. This will perform an unsafe literal insertion and not provide any
escaping.
COPY
import sqlalchemy
connection = engine.connect()
myvar = 'jsmith' # our intended usage
myvar = 'jsmith or 1=1' # this will return all users
myvar = 'jsmith; DROP TABLE users' # this drops (removes) the users
table
query = "select username from users where username = %s" % myvar
result = connection.execute(query)
for row in result:
print("username:", row['username'])
connection.close()
Correct
import sqlalchemy
connection = engine.connect()
myvar = 'jsmith' # our intended usage
myvar = 'jsmith or 1=1' # only matches this odd username
query = "select username from users where username = :name"
result = connection.execute(query, name=myvar)
for row in result:
print("username:", row['username'])
connection.close()
MySQL
Incorrect
Without using any escaping mechanism, potentially unsafe queries can be created.
COPY
import MySQLdb
with con:
cur = con.cursor()
cur.execute(query)
Better
In this example, the query is created using Python's standard, unsafe % operator.
MySQL’s escape_string method is used to perform escaping on the user input string
prior to inclusion in the string.
COPY
import MySQLdb
with con:
cur = con.cursor()
cur.execute(query)
Correct
The correct way to do this using a parameterized query might look like the following:
COPY
import MySQLdb
with con:
cur = con.cursor()
cur.execute(query, (username_value,))
This works because the logic of the query is compiled before the user input is
considered.
PostgreSQL (Psycopg2)
Incorrect
This example uses Python’s unsafe default parameter substitution mechanism to build
a query string. This will not perform any escaping, unlike the correct example below.
The string is processed and passed as a single parameter to execute .
COPY
import psycopg2
Correct
COPY
import psycopg2
In the example below, the password secrets! will be loaded through the
cfg.StrOpt() function that could otherwise be logged and disclosed to anyone with
cfg.StrOpt('password',
help='Password of the host.')
Correct
cfg.StrOpt('password',
help='Password of the host.',
secret=True)
Consequences
If sensitive information options are logged without being marked as secret, that
sensitive information would be exposed whenever the logger debug flag is activated.
Example Log Entries
COPY
OpenStack API calls often contain credentials or tokens that are very sensitive. If they
are sent in plaintext, they may be modified or stolen.
It is very important that API calls are protected from malicious third parties viewing
them or tampering with their content - even for communications between services on
an internal network.
Less Obvious Example
Consider a server process that reports the current number of stars in the sky and
sends the data over the network to clients using a simple webpage. There is no
strong confidentiality requirement for this; the data is not secret. However, integrity is
important. An attacker on the network could alter the communications going from the
server to clients and inject malicious traffic such as browser exploits into the HTTP
stream, thus compromising vulnerable clients.
Incorrect
COPY
cfg.StrOpt('protocol',
default='http',
help='Default protocol to use when connecting to glance.')
Correct
COPY
cfg.StrOpt('protocol',
default='https',
help='Default protocol to use when connecting to glance.')
In the below example, user-supplied data from the query string is written directly into
the HTML document and is presented to the user.
COPY
def index(request):
name = request.GET.get('name')
return f"<html><body>Hello {name}!</body></html>"
Correct
In the below example, the same user-supplied data is escaped before being written
into the document, thus preventing the user from adding HTML elements to the
document.
COPY
def index(request):
name = request.GET.get('name')
safe_name = escape(name)
return f"<html><body>Hello {safe_name}!</body></html>"
In addition to the use of escaping, frameworks and templating engines can also be
useful in preventing XSS attacks.
Resources
https://docs.openstack.org/security-guide/compute/hardening-the-
virtualization-layers.html
Written by
Reza Rashidi Add your bio
Published on
DevSecOpsGuides Add blog description
MORE ARTICLES
Reza Rashidi Reza Rashidi
Reza Rashidi
Attacking Policy
Open Policy Agent (OPA) is a versatile
tool used to enforce policies and ensure
compliance within a …
©2024 DevSecOpsGuides
Archive · Privacy policy · Terms
Write on Hashnode