reset_password.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. #!/usr/bin/python
  2. #
  3. # Copyright (c) 2017-2018 Joe Clarke <jclarke@cisco.com>
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. # 1. Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. # notice, this list of conditions and the following disclaimer in the
  13. # documentation and/or other materials provided with the distribution.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  19. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  21. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  22. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  23. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  24. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  25. # SUCH DAMAGE.
  26. from pyad import *
  27. import sys
  28. import re
  29. from functools import wraps
  30. from flask import request, Response, session
  31. from flask import Flask
  32. import pythoncom
  33. import CLEUCreds
  34. AD_DN_BASE = 'cn=Users, dc=ad, dc=ciscolive, dc=network'
  35. AD_DOMAIN = 'ad.ciscolive.network'
  36. AD_DC = 'dc1-ad.ad.ciscolive.network'
  37. app = Flask('CLEU Password Reset')
  38. def query_user(username, password, target_user):
  39. global AD_DC, AD_DN_BASE
  40. try:
  41. adcontainer.ADContainer.from_dn(AD_DN_BASE, options={
  42. 'ldap_server': AD_DC, 'username': username, 'password': password})
  43. except Exception as e:
  44. print(e)
  45. return None
  46. try:
  47. q = adquery.ADQuery(
  48. options={'ldap_server': AD_DC, 'username': username, 'password': password})
  49. q.execute_query(attributes=['distinguishedName'], where_clause="sAMAccountName='{}'".format(
  50. target_user), base_dn=AD_DN_BASE, options={'ldap_server': AD_DC, 'username': username, 'password': password})
  51. for row in q.get_results():
  52. return row['distinguishedName']
  53. except Exception as e:
  54. print(e)
  55. return None
  56. def check_auth(username, password):
  57. global AD_DOMAIN
  58. pythoncom.CoInitialize()
  59. if 'dn' in session:
  60. return True
  61. if not re.search(r'@{}$'.format(AD_DOMAIN), username):
  62. username += '@{}'.format(AD_DOMAIN)
  63. target_username = username.replace('@{}'.format(AD_DOMAIN), '')
  64. try:
  65. dn = query_user(username, password, target_username)
  66. if dn is not None:
  67. session['dn'] = dn
  68. return True
  69. else:
  70. try:
  71. dn = None
  72. dn = query_user(CLEUCreds.AD_ADMIN,
  73. CLEUCreds.AD_PASSWORD, target_username)
  74. if dn is None:
  75. return False
  76. adu = aduser.ADUser.from_dn(dn, options={
  77. 'ldap_server': AD_DC, 'username': CLEUCreds.AD_ADMIN, 'password': CLEUCreds.AD_PASSWORD})
  78. obj = adu.get_attribute('pwdLastSet', False)
  79. if password == CLEUCreds.DEFAULT_USER_PASSWORD and int(obj.highpart) == 0 and int(obj.lowpart) == 0:
  80. session['dn'] = dn
  81. return True
  82. except Exception as ie:
  83. print(ie)
  84. return False
  85. except Exception as e:
  86. print(e)
  87. return False
  88. return False
  89. def authenticate():
  90. return Response(
  91. 'Failed to verify credentials for password reset.\n'
  92. 'You have to login with proper credentials.', 401,
  93. {'WWW-Authenticate': 'Basic realm="CLEU Password Reset"'})
  94. def requires_auth(f):
  95. @wraps(f)
  96. def decorated(*args, **kwargs):
  97. auth = request.authorization
  98. if not auth or not check_auth(auth.username, auth.password):
  99. return authenticate()
  100. return f(*args, **kwargs)
  101. return decorated
  102. @app.route('/reset-password', methods=['POST'])
  103. @requires_auth
  104. def reset_password():
  105. new_pw = request.form.get('new_pass')
  106. new_pw_confirm = request.form.get('new_pass_confirm')
  107. if new_pw.strip() == '' or new_pw_confirm.strip() == '':
  108. return Response('''
  109. <html>
  110. <head>
  111. <title>Bad Password</title>
  112. </head>
  113. <body>
  114. <p>You must specify a new password.</p>
  115. </body>
  116. </html>''', mimetype='text/html')
  117. if new_pw != new_pw_confirm:
  118. return Response('''
  119. <html>
  120. <head>
  121. <title>Bad Password</title>
  122. </head>
  123. <body>
  124. <p>Passwords did not match</p>
  125. </body>
  126. </html>''', mimetype='text/html')
  127. adu = aduser.ADUser.from_dn(session['dn'], options={
  128. 'ldap_server': AD_DC, 'username': CLEUCreds.AD_ADMIN, 'password': CLEUCreds.AD_PASSWORD})
  129. try:
  130. adu.set_password(new_pw)
  131. except Exception as e:
  132. return Response('''
  133. <html>
  134. <head>
  135. <title>Failed to Reset Password</title>
  136. </head>
  137. <body>
  138. <h1>Password Reset Failed!</h1>
  139. <p>{}</p>
  140. </body>
  141. </html>'''.format(e), mimetype='text/html')
  142. adu.grant_password_lease()
  143. del session['dn']
  144. return Response('''
  145. <html>
  146. <head>
  147. <title>Password Changed Successfully!</title>
  148. </head>
  149. <body>
  150. <h1>Password Changed Successfully!</h1>
  151. </body>
  152. </html>''', mimetype='text/html')
  153. @app.route('/')
  154. @requires_auth
  155. def get_main():
  156. page = '''
  157. <html>
  158. <head>
  159. <title>Password Reset Form</title>
  160. <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  161. <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.12/css/dataTables.bootstrap.min.css" integrity="sha256-7MXHrlaY+rYR1p4jeLI23tgiUamQVym2FWmiUjksFDc=" crossorigin="anonymous" />
  162. <meta name="viewport" content="width=device-width, initial-scale=1">
  163. <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
  164. <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.12/js/jquery.dataTables.min.js" integrity="sha256-TX6POJQ2u5/aJmHTJ/XUL5vWCbuOw0AQdgUEzk4vYMc=" crossorigin="anonymous"></script>
  165. <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.12/js/dataTables.bootstrap.min.js" integrity="sha256-90YqnHom4j8OhcEQgyUI2IhmGYTBO54Adcf3YDZU9xM=" crossorigin="anonymous"></script>
  166. <script>
  167. function verify() {
  168. if (!$('#new_pass').val().trim()) {
  169. alert('Please specify a new password.');
  170. return false;
  171. }
  172. if (!$('#new_pass_confirm').val().trim()) {
  173. alert('Please confirm the new password.');
  174. return false;
  175. }
  176. if ($('#new_pass_confirm').val().trim() != $('#new_pass').val().trim()) {
  177. alert('Passwords do not match.');
  178. return false;
  179. }
  180. return true;
  181. }
  182. </script>
  183. </head>
  184. <body>
  185. <div class="container" role="main" style="width: 100%;">
  186. <div class="page-header">
  187. <h3>Password Reset Form</h3>
  188. </div>
  189. <div class="row">
  190. <div class="col-sm-8">
  191. <form method="POST" onSubmit="return verify();" action="/reset-password">
  192. <div class="form-group">
  193. <label for="new_pass">New Password:</label>
  194. <input type="password" name="new_pass" id="new_pass" class="form-control" placeholder="New Password">
  195. </div>
  196. <div class="form-group">
  197. <label for="new_pass_confirm">Confirm New Password:</label>
  198. <input type="password" name="new_pass_confirm" id="new_pass_confirm" class="form-control" placeholder="Confirm New Password">
  199. </div>
  200. <div class="form-group">
  201. <input type="submit" name="submit" value="Reset My Password!" class="btn btn-primary">
  202. <input type="reset" name="reset" value="Start Over" class="btn btn-default">
  203. </div>
  204. </form>
  205. </div>
  206. </div>
  207. </div>
  208. </body>
  209. </html>'''
  210. return Response(page, mimetype='text/html')
  211. if __name__ == '__main__':
  212. app.secret_key = CLEUCreds.AD_PASSWORD
  213. app.run(host='10.100.253.25', port=8443,
  214. threaded=True, ssl_context=('chain.pem', 'privkey.pem'))