Keystone getting oauth access token by brute-forcing oauth_verifier code

Bug #1236675 reported by Phuong Cao
32
This bug affects 3 people
Affects Status Importance Assigned to Milestone
OpenStack Identity (keystone)
Fix Released
Medium
Matthieu Huin

Bug Description

Title: Keystone getting oauth access token by brute-forcing
oauth_verifier code
Reporter: Phuong Cao
Products: openstack/keystone
Affects: keystone/master branch as of Oct 7th 2013

Description:
Phuong Cao reported a vulnerability in OAuth SQL backend of
keystone/master branch.

How does the attack work?
By creating many access token requests with oauth_verifier code selected
from the range 1000 to 9999,
an attacker can request a valid access token to a role and a project,
overriding a user who actually request access to the role and the project.

Before describing in detail how the attack works, this is how OAuth
works (summarized from RFC5849)

1. Alice registers as a consumer with the Openstack admin.
2. Alice asks the Openstack admin a token with a specified role and a
project on behalf of Bob (Bob is the owner of the project).
3. The Openstack admin returns to Alice a request token key.
4. Alice sends the request token key to Bob to ask for permissions to
access the project.
5. Bob authorizes Alice's request token to have access to the project
with the specified role.
6. Bob generates an oauth_verifier code ranging from 1000 to 9999, then
sends back to Alice an oauth_verifier code.
7. Alice use the oauth_verifier code and the request token key to ask
the Openstack admin for the access token to the project.

This is how the attack works:
At step 4, assuming an attacker can sniff Alice's request token key.
This can be done by acting as a man in the middle if Alice interacts
with Keystone using HTTP requests,
or acting as a local user to list the process arguments if Alice is
interacts with Keystone using openstack commandline tools (this case is
similar to CVE 2013-2013).

Now the attacker has Alice's request token key, he/she need to wait for
Bob to authorizes Alice's request token (step 5), then repeatedly
brute-forcing Keystone with the pair(oauth_verifier, Alice's request
token key) using oauth_verifier from the range 1000 to 9999. Since the
oauth_verifier is in a short range, and Openstack/Keystone doesn't have
any mechanism to limit number of requests, the attacker can bruteforce
for the valid oauth_verifier key until the request token expires.

A more aggressive way is to keep brute-forcing Keystone until Bob
authorizes Alice's request token, by doing this the attacker will have
more chance getting the access token key before Alice.

Where are the vulnerable code locations?
Line 210 of sql.py file:
https://github.com/openstack/keystone/blob/master/keystone/contrib/oauth1/backends/sql.py#L210

In OAuth SQL backend of keystone/master branch, the oauth_verifier code,
a fundamental part of OAuth1 protocol, is generated using random numbers
from 1000 to 9999.
This is a small range of numbers and it is easy to be guessed/brute-forced.
This attack is classified as "CWE-330: Use of Insufficiently Random
Values" (http://cwe.mitre.org/data/definitions/330.html).

What are the possible fixes?
We suggest using a long random string (e.g., 32-bit or 64-bit). Using
os.urandom() is a good one, it has been recommended for generating
random number for cryptographic purposes.
A patch is attached in the attached file (please note: we haven't tested
this patch).

Where is the exploit code?
We attach a snippet code that we modify from test_bad_verifier()
Keystone test case.
The snippet is a sketch of how oauth_verifier code brute-forcing can be
implemented.

What is the affected version?
The keystone/master on github as of Oct 7th 2013 is affected.

References:
Link to oauth1 file and vulnerable code location (sql.py, line #210):
https://github.com/openstack/keystone/blob/master/keystone/contrib/oauth1/backends/sql.py#L210
CWE-330: Use of Insufficiently Random Values:
(http://cwe.mitre.org/data/definitions/330.html).
RFC5849: http://tools.ietf.org/html/rfc5849
os.urandom(): http://docs.python.org/2/library/os.html
OAuth in keystone tutorial:
http://www.unitedstack.com/blog/oauth-in-keystone/

# Patch
--- /tmp/keystone/keystone/contrib/oauth1/backends/sql.py 2013-10-07 17:06:04.170603933 -0500
+++ /home/vagrant/keystone/keystone/contrib/oauth1/backends/sql.py 2013-10-07 17:01:39.124008733 -0500
@@ -17,6 +17,8 @@
 import datetime
 import random
 import uuid
+import os
+import binascii

 from keystone.common import sql
 from keystone.common.sql import migration
@@ -207,7 +209,7 @@
             token_ref = self._get_request_token(session, request_token_id)
             token_dict = token_ref.to_dict()
             token_dict['authorizing_user_id'] = user_id
- token_dict['verifier'] = str(random.randint(1000, 9999))
+ token_dict['verifier'] = binascii.b2a_hex(os.urandom(16))
             token_dict['role_ids'] = jsonutils.dumps(role_ids)

             new_token = RequestToken.from_dict(token_dict)

# Test brute force
class MaliciousOAuth1Tests(OAuth1Tests):

    # modified from test_bad_verifier()
    def test_bruteforce_verifier(self):

        # create consumer for oauth
        consumer = self._create_single_consumer()
        consumer_id = consumer.get('id')
        consumer_secret = consumer.get('secret')
        consumer = oauth1.Consumer(consumer_id, consumer_secret)

        url, headers = self._create_request_token(consumer,
                                                  self.project_id)
        # get request token
        content = self.post(url, headers=headers)
        credentials = urlparse.parse_qs(content.result)
        request_key = credentials.get('oauth_token')[0]
        request_secret = credentials.get('oauth_token_secret')[0]
        request_token = oauth1.Token(request_key, request_secret)

        # authorize request token
        url = self._authorize_request_token(request_key)

        body = {'roles': [{'id': self.role_id}]}
        resp = self.put(url, body=body, expected_status=200)
        verifier = resp.result['token']['oauth_verifier']
        self.assertIsNotNone(verifier)

        # we are not going to use received oauth_verifier here, instead, we brute-force to find the valid oauth_verifier
        for i in range(1000,10000):
            request_token.set_verifier(str(i))
            url, headers = self._create_access_token(consumer,
                                                     request_token)
            # We expect 401 status code for most of requests since most oauth_verifier code
            # that we try will be invalid.
            # The test will crash at the valid oauth_verifier code when returned status = 201,
            # which is different from the expected 401 status.
            r = self.post(url, headers=headers, expected_status=401)

            # Print out oauth_verifier, and raw response request that contains access token.
            if (r == 201): # We have found correct oauth_verifier code
                print 'oauth_verifier: {}, raw access_token response request: {}'.format(str(i), str(r))

We are looking forward hearing from you.

Thank you.

Best,

Phuong Cao
Research Assistant
DEPEND group
Coordinated Science Laboratory
University of Illinois at Urbana Champaign
Urbana, IL 61801, USA

Revision history for this message
Phuong Cao (pcao3) wrote :
Revision history for this message
Phuong Cao (pcao3) wrote :
Revision history for this message
Dolph Mathews (dolph) wrote :

> At step 4, assuming an attacker can sniff Alice's request token key.

If you can sniff for this, then you can also sniff for access tokens or just regular tokens, so I don't see this as imminently exploitable and I think this can be opened publicly.

The intent of the short OAuth verifier codes is to make it easy for Alice to type them manually, in case they are delivered to Alice via an alternative transport, such as SMS or a paper napkin. 32 bytes of entropy encoded in hex makes for a poor user experience in that scenario...

I'm happy to see the entropy increased here for Havana (perhaps we can also make it configurable?), but I don't want to require Alice to type a 32 character string (by default). Using 4 bytes of entropy instead of 32 (rendering 8 character hex strings) would produce 4,294,967,296 possible values instead of the existing 8999, which I think is sufficient to close this as an attractive attack vector.

In the future, we can pursue encoding with a higher base (for example, using base 36) but produce the same encoded string length, thus increasing the entropy further. Alternatively, we can disable request tokens after they've failed one or more verification attempts.

Revision history for this message
Jeremy Stanley (fungi) wrote :

Given that analysis, this sounds like a security hardening improvement in Icehouse or later. I agree this probably ought to be disclosed with no advisory needed and put through the normal code review workflow. The OSSG might be interested in releasing a note about it... subscribing Robert for input/counterargument before we potentially open this.

Revision history for this message
Phuong Cao (pcao3) wrote :

@Dolph:

Your point about usability of the short OAuth verifier code is right. However, when using OAuth programmatically, increasing the length of the OAuth verifier code has no effect on usability. For example, both Google and Yahoo! are using long OAuth verifier code for their OAuth implementation:

https://developers.google.com/accounts/docs/OAuth_ref
http://www.flickr.com/services/api/auth.oauth.html

Revision history for this message
Steve Martinelli (stevemar) wrote :

@Phuong,

Just a quick clarification, you mentioned Yahoo, but posted a link to Flickr.
Yahoo does use a short (6 character) verifier: http://developer.yahoo.com/oauth/guide/oauth-accesstoken.html

Adam Young (ayoung)
Changed in keystone:
status: New → Confirmed
Revision history for this message
Adam Young (ayoung) wrote :

What happens if a request token is never approved, and the user submits "validator"=none in JSON. Will that match, and the user be able to erroneously get a valid token?

Revision history for this message
Dolph Mathews (dolph) wrote :

@stevemar: Yahoo also owns Flickr! :)

I don't believe the OAuth verifier is intended to be used programmatically at all, at least in OAuth 1. I'm not as familiar with OAuth 2, or perhaps my perspective on OAuth 1 is outdated?

Revision history for this message
Steve Martinelli (stevemar) wrote :

@ayoung,

the access token would not have any approved roles either.

Revision history for this message
Adam Young (ayoung) wrote :

Steve, yes, I realize that roles are assigned later. Keystone also prevents using a token to get a token. Still, a token would be issued, and any Keystone operations that just required a token would still accept it.

Revision history for this message
Thierry Carrez (ttx) wrote :

Based on the above analysis (not really exploitable except if you have the power to sniff tokens in which case it's game over already), I think it makes sense to open this bug publicly and discuss strengthening options in the open.

Please confirm that you don't mind this private bug to become publicly accessible and I'll make it happen.

Changed in ossa:
status: New → Incomplete
Revision history for this message
Phuong Cao (pcao3) wrote :

Yes please.

Revision history for this message
Dolph Mathews (dolph) wrote :

(reversing myself) Phuong Cao- after this is opened, can you submit your patch to gerrit as-is? We can allow the length to be relaxed via configuration later on.

Thierry Carrez (ttx)
information type: Private Security → Public
no longer affects: ossa
tags: added: security
tags: added: havana-rc-potential
Thierry Carrez (ttx)
tags: added: havana-backport-potential
removed: havana-rc-potential
Dolph Mathews (dolph)
Changed in keystone:
importance: Undecided → Medium
Revision history for this message
Matthieu Huin (mhu-s) wrote :

I

Revision history for this message
Matthieu Huin (mhu-s) wrote :

I agree that a 8 character long hex string is a good compromise. Shall this be pushed on ?

Revision history for this message
Dolph Mathews (dolph) wrote :

Matthieu: yes, please!

Matthieu Huin (mhu-s)
Changed in keystone:
assignee: nobody → Matthieu Huin (mhu-s)
Revision history for this message
Openstack Gerrit (openstack-gerrit) wrote : Fix proposed to keystone (master)

Fix proposed to branch: master
Review: https://review.openstack.org/89612

Changed in keystone:
status: Confirmed → In Progress
Revision history for this message
Openstack Gerrit (openstack-gerrit) wrote : Fix merged to keystone (master)

Reviewed: https://review.openstack.org/89612
Committed: https://git.openstack.org/cgit/openstack/keystone/commit/?id=fd02a9c3d03dde51e7ec808552256dea7fef865c
Submitter: Jenkins
Branch: master

commit fd02a9c3d03dde51e7ec808552256dea7fef865c
Author: Matthieu Huin <email address hidden>
Date: Tue Apr 22 17:14:25 2014 +0200

    More random values for oAuth1 verifier

    The oAuth1 verifier was generated as a random number ranging from
    1000 to 9999. This small range of numbers is vulnerable to
    brute-force attacks as described in CWE-330. The verifier is now
    a 8-character long alphanumerical string, a good compromise between
    security against guessing and ease of use.

    SecurityImpact
    Change-Id: Ibe4a2e57a02c261d85ba6c0d61696f134c54443e
    Closes-Bug: #1236675

Changed in keystone:
status: In Progress → Fix Committed
Thierry Carrez (ttx)
Changed in keystone:
milestone: none → juno-1
status: Fix Committed → Fix Released
Thierry Carrez (ttx)
Changed in keystone:
milestone: juno-1 → 2014.2
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Duplicates of this bug

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.