Post

How to: automate session rotation in BurpSuite with mitmproxy

Intro

Ever got that gnarly web app pentest with a session lifetime of 2 minutes, thinking it would take at most 15 minutes to set up the session rotation and then spent 4 hours trying to make five separate Burp plugins work to no avail? Well, me too.

There is a way more elegant solution that is going to work 100% of the time and only requires a bit of Python knowledge. Today, I will share my trusty setup that I’ve been using for over a year to streamline the session rotation for pretty much every web hacking tool that supports HTTP proxy.

We’re going to use mitmproxy scripts!

Setup

The overall setup looks like this:

Setup Scheme

We will define a mitmproxy user script that stores its own session and replaces it in any incoming request from the tools using the HTTP proxy. The logic is trivial and, more importantly, a lot more customizable than BurpSuite macros that, to this day, do not know how to extract a JWT from the response JSON body.

An example setup with BurpSuite would look something like this:

Burp Setup

Examples

Here are some mitmproxy script examples for three common scenarios: basic JWT session with access tokens, JWT session with refresh tokens, and cookie-based session. To use these script harnesses, you must redefine the refresh_session, login, and is_session_valid functions to fit your application. As you can see, the script files are short and shouldn’t take long to get working.

You can also find all scripts from this post on my Github.

JWT - Access Token Only

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from mitmproxy import http
import datetime
import logging
import requests
import jwt

refresh_url = 'https://your.app.com/login'

def refresh_session() -> str:
    response_json = requests.post(
        refresh_url,
        json={
            'username': 'test',
            'password': 'test'
        },
        verify=False
    ).json()

    access_token = response_json['access_token']

    logging.warning(f'Got new access token: {access_token}...')
    return access_token

def is_session_valid(access_token: str) -> bool:
    parsed_access_token = jwt.decode(access_token, options={'verify_signature': False})
    now = int(datetime.datetime.now().timestamp())
    # return true if 'exp' timestamp is still in the future
    return now < parsed_access_token['exp']

logger = logging.getLogger(__name__)
access_token = refresh_session()

def request(flow: http.HTTPFlow) -> None:
    global access_token
    if not is_session_valid(access_token):
        access_token = refresh_session()
    flow.request.headers['Authorization'] = f'Bearer {access_token}'

JWT - Access & Refresh Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from mitmproxy import http
import datetime
import logging
import requests
import jwt

login_url = 'https://your.app.com/login'
refresh_url = 'https://your.app.com/token/rotate'

def login() -> tuple[str,str]:
    response_json = requests.post(
        login_url,
        json={
            'username': 'test',
            'password': 'test'
        },
        verify=False
    ).json()
    access_token = response_json['access_token']
    refresh_token = response_json['refresh_token']
    logging.warning(f'Got new access access token: {access_token}...')
    logging.warning(f'Got new access refresh token: {refresh_token}...')
    return refresh_token, access_token

def refresh_session(refresh_token: str) -> tuple[str,str]:
    response_json = requests.post(
        refresh_url,
        json={
            'refresh_token': refresh_token
        }
    )
    access_token = response_json['access_token']
    refresh_token = response_json['refresh_token']
    logging.warning(f'Got new access access token: {access_token}...')
    logging.warning(f'Got new access refresh token: {refresh_token}...')
    return refresh_token, access_token

def is_session_valid(access_token: str) -> bool:
    parsed_access_token = jwt.decode(access_token, options={'verify_signature': False})
    now = int(datetime.datetime.now().timestamp())
    # return true if 'exp' timestamp is still in the future
    return now < parsed_access_token['exp']

logger = logging.getLogger(__name__)
refresh_token, access_token = refresh_session()

def request(flow: http.HTTPFlow) -> None:
    global refresh_token, access_token
    if not is_session_valid(access_token):
        refresh_token, access_token = refresh_session(refresh_token)
    flow.request.headers['Authorization'] = f'Bearer {access_token}'

Cookies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from mitmproxy import http
import datetime
import logging
import requests

refresh_url = 'https://your.app.com/login'

def refresh_session() -> str:
    response = requests.post(
        refresh_url,
        json={
            'username': 'test',
            'password': 'test'
        },
        verify=False
    )
    cookies = response.cookies.get_dict()
    logging.warning(f'Got new cookies: {cookies}...')
    return cookies

def is_session_valid(response: http.Response) -> bool:
    return response.status_code != 401

logger = logging.getLogger(__name__)
cookies = refresh_session()

def response(flow: http.HTTPFlow) -> None:
    global cookies
    if not is_session_valid(flow.response):
        cookies = refresh_session()

def request(flow: http.HTTPFlow) -> None:
    global cookies
    flow.request.cookies.update(cookies)

This post is licensed under CC BY 4.0 by the author.