reset_password.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. #!/usr/bin/python
  2. #
  3. # Copyright (c) 2017-2019 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. from cleu.config import Config as C
  35. AD_DC = 'dc1-ad.' + AD_DOMAIN
  36. app = Flask('CLEU Password Reset')
  37. def query_user(username, password, target_user):
  38. global AD_DC
  39. try:
  40. adcontainer.ADContainer.from_dn(C.AD_DN_BASE, options={
  41. 'ldap_server': AD_DC, 'username': username, 'password': password})
  42. except Exception as e:
  43. print(e)
  44. return None
  45. try:
  46. q = adquery.ADQuery(
  47. options={'ldap_server': AD_DC, 'username': username, 'password': password})
  48. q.execute_query(attributes=['distinguishedName'], where_clause="sAMAccountName='{}'".format(
  49. target_user), base_dn=C.AD_DN_BASE, options={'ldap_server': AD_DC, 'username': username, 'password': password})
  50. for row in q.get_results():
  51. return row['distinguishedName']
  52. except Exception as e:
  53. print(e)
  54. return None
  55. def check_auth(username, password):
  56. pythoncom.CoInitialize()
  57. if username == C.VPN_USER or username == C.VPN_USER + '@' + C.AD_DOMAIN:
  58. return False
  59. if 'dn' in session:
  60. return True
  61. if not re.search(r'@{}$'.format(C.AD_DOMAIN), username):
  62. username += '@{}'.format(C.AD_DOMAIN)
  63. target_username = username.replace('@{}'.format(C.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; !!!ENTER YOUR AD USERNAME AND PASSWORD!!!"'})
  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. vpnuser = request.form.get('vpnuser')
  108. if new_pw.strip() == '' or new_pw_confirm.strip() == '':
  109. return Response('''
  110. <html>
  111. <head>
  112. <title>Bad Password</title>
  113. </head>
  114. <body>
  115. <p>You must specify a new password.</p>
  116. </body>
  117. </html>''', mimetype='text/html')
  118. if new_pw != new_pw_confirm:
  119. return Response('''
  120. <html>
  121. <head>
  122. <title>Bad Password</title>
  123. </head>
  124. <body>
  125. <p>Passwords did not match</p>
  126. </body>
  127. </html>''', mimetype='text/html')
  128. adu = aduser.ADUser.from_dn(session['dn'], options={
  129. 'ldap_server': AD_DC, 'username': CLEUCreds.AD_ADMIN, 'password': CLEUCreds.AD_PASSWORD})
  130. try:
  131. adu.set_password(new_pw)
  132. except Exception as e:
  133. return Response('''
  134. <html>
  135. <head>
  136. <title>Failed to Reset Password</title>
  137. </head>
  138. <body>
  139. <h1>Password Reset Failed!</h1>
  140. <p>{}</p>
  141. </body>
  142. </html>'''.format(e), mimetype='text/html')
  143. adu.grant_password_lease()
  144. del session['dn']
  145. resp = '''
  146. <html>
  147. <head>
  148. <title>Password Changed Successfully!</title>
  149. </head>
  150. <body>
  151. <h1>Password Changed Successfully!</h1>'''
  152. if vpnuser and vpnuser == 'true':
  153. resp += '\n<h1>Please disconnect your VPN and connect again with your AD credentials.</h1>'
  154. resp += '''
  155. </body>
  156. </html>'''
  157. return Response(resp, mimetype='text/html')
  158. @app.route('/')
  159. @requires_auth
  160. def get_main():
  161. page = '''
  162. <html>
  163. <head>
  164. <title>Password Reset Form</title>
  165. <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  166. <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" />
  167. <meta name="viewport" content="width=device-width, initial-scale=1">
  168. <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
  169. <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>
  170. <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>
  171. <script>
  172. function verify() {
  173. if (!$('#new_pass').val().trim()) {
  174. alert('Please specify a new password.');
  175. return false;
  176. }
  177. if (!$('#new_pass_confirm').val().trim()) {
  178. alert('Please confirm the new password.');
  179. return false;
  180. }
  181. if ($('#new_pass_confirm').val().trim() != $('#new_pass').val().trim()) {
  182. alert('Passwords do not match.');
  183. return false;
  184. }
  185. return true;
  186. }
  187. </script>
  188. </head>
  189. <body>
  190. <div class="container" role="main" style="width: 100%;">
  191. <div class="page-header">
  192. <h3>Password Reset Form</h3>
  193. </div>
  194. <div class="row">
  195. <div class="col-sm-8">
  196. <form method="POST" onSubmit="return verify();" action="/reset-password">
  197. <div class="form-group">
  198. <label for="new_pass">New Password:</label>
  199. <input type="password" name="new_pass" id="new_pass" class="form-control" placeholder="New Password">
  200. </div>
  201. <div class="form-group">
  202. <label for="new_pass_confirm">Confirm New Password:</label>
  203. <input type="password" name="new_pass_confirm" id="new_pass_confirm" class="form-control" placeholder="Confirm New Password">
  204. </div>
  205. <div class="form-group">
  206. <input type="submit" name="submit" value="Reset My Password!" class="btn btn-primary">
  207. <input type="reset" name="reset" value="Start Over" class="btn btn-default">
  208. </div>'''
  209. page += '\n<input type="hidden" name="vpnuser" value="%s"/>' % (
  210. request.args.get('vpnuser'))
  211. page += '''
  212. </form>
  213. </div>
  214. </div>
  215. </div>
  216. </body>
  217. </html>'''
  218. return Response(page, mimetype='text/html')
  219. if __name__ == '__main__':
  220. app.secret_key = CLEUCreds.AD_PASSWORD
  221. app.run(host='10.100.252.25', port=8443,
  222. threaded=True, ssl_context=('chain.pem', 'privkey.pem'))