Repository: CTFd/CTFd

Issue:
Log failed login attempts
During CTF competitions, mischievous participants or rogue strangers may try to exploit CTFd by brute forcing the login page or attempt SQL injections, etc...

It'll be useful if we could add the failed login attempts into the existing /opt/CTFd/CTFd/logs/logins.log or maybe a new file named failed-logins.log.


Discussion:

Resolving patch:
diff --git a/CTFd/auth.py b/CTFd/auth.py
--- a/CTFd/auth.py
+++ b/CTFd/auth.py
@@ -21,6 +21,7 @@ def confirm_user(data=None):
         # If the CTF doesn't care about confirming email addresses then redierct to challenges
         return redirect(url_for('challenges.challenges_view'))
 
+    logger = logging.getLogger('logins')
     # User is confirming email account
     if data and request.method == "GET":
         try:
@@ -33,9 +34,13 @@ def confirm_user(data=None):
         team = Teams.query.filter_by(email=email).first_or_404()
         team.verified = True
         db.session.commit()
-        logger = logging.getLogger('regs')
-        logger.warn("[{0}] {1} confirmed {2}".format(time.strftime("%m/%d/%Y %X"), team.name.encode('utf-8'), team.email.encode('utf-8')))
         db.session.close()
+        logger.warn("[{date}] {ip} - {username} confirmed their account".format(
+            date=time.strftime("%m/%d/%Y %X"),
+            ip=utils.get_ip(),
+            username=team.name.encode('utf-8'),
+            email=team.email.encode('utf-8')
+        ))
         if utils.authed():
             return redirect(url_for('challenges.challenges_view'))
         return redirect(url_for('auth.login'))
@@ -53,6 +58,12 @@ def confirm_user(data=None):
                 return redirect(url_for('views.profile'))
             else:
                 utils.verify_email(team.email)
+                logger.warn("[{date}] {ip} - {username} initiated a confirmation email resend".format(
+                    date=time.strftime("%m/%d/%Y %X"),
+                    ip=utils.get_ip(),
+                    username=team.name.encode('utf-8'),
+                    email=team.email.encode('utf-8')
+                ))
             return render_template('confirm.html', team=team, infos=['Your confirmation email has been resent!'])
         elif request.method == "GET":
             # User has been directed to the confirm page
@@ -80,6 +91,11 @@ def reset_password(data=None):
         team.password = bcrypt_sha256.encrypt(request.form['password'].strip())
         db.session.commit()
         db.session.close()
+        logger.warn("[{date}] {ip} -  successful password reset for {username}".format(
+            date=time.strftime("%m/%d/%Y %X"),
+            ip=utils.get_ip(),
+            username=team.name.encode('utf-8')
+        ))
         return redirect(url_for('auth.login'))
 
     if request.method == 'POST':
@@ -104,6 +120,7 @@ def reset_password(data=None):
 
 @auth.route('/register', methods=['POST', 'GET'])
 def register():
+    logger = logging.getLogger('regs')
     if not utils.can_register():
         return redirect(url_for('auth.login'))
     if request.method == 'POST':
@@ -149,9 +166,12 @@ def register():
                 if utils.can_send_mail() and utils.get_config('verify_emails'):  # Confirming users is enabled and we can send email.
                     db.session.close()
                     logger = logging.getLogger('regs')
-                    logger.warn("[{0}] {1} registered (UNCONFIRMED) with {2}".format(time.strftime("%m/%d/%Y %X"),
-                                                                                     request.form['name'].encode('utf-8'),
-                                                                                     request.form['email'].encode('utf-8')))
+                    logger.warn("[{date}] {ip} - {username} registered (UNCONFIRMED) with {email}".format(
+                        date=time.strftime("%m/%d/%Y %X"),
+                        ip=utils.get_ip(),
+                        username=request.form['name'].encode('utf-8'),
+                        email=request.form['email'].encode('utf-8')
+                    ))
 
                     utils.verify_email(team.email)
 
@@ -162,8 +182,12 @@ def register():
 
         db.session.close()
 
-        logger = logging.getLogger('regs')
-        logger.warn("[{0}] {1} registered with {2}".format(time.strftime("%m/%d/%Y %X"), request.form['name'].encode('utf-8'), request.form['email'].encode('utf-8')))
+        logger.warn("[{date}] {ip} - {username} registered with {email}".format(
+            date=time.strftime("%m/%d/%Y %X"),
+            ip=utils.get_ip(),
+            username=request.form['name'].encode('utf-8'),
+            email=request.form['email'].encode('utf-8')
+        ))
         return redirect(url_for('challenges.challenges_view'))
     else:
         return render_template('register.html')
@@ -171,6 +195,7 @@ def register():
 
 @auth.route('/login', methods=['POST', 'GET'])
 def login():
+    logger = logging.getLogger('logins')
     if request.method == 'POST':
         errors = []
         name = request.form['name']
@@ -187,20 +212,35 @@ def login():
                 session['nonce'] = utils.sha512(os.urandom(10))
                 db.session.close()
 
-                logger = logging.getLogger('logins')
-                logger.warn("[{0}] {1} logged in".format(time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8')))
+                logger.warn("[{date}] {ip} - {username} logged in".format(
+                    date=time.strftime("%m/%d/%Y %X"),
+                    ip=utils.get_ip(),
+                    username=session['username'].encode('utf-8')
+                ))
 
                 if request.args.get('next') and utils.is_safe_url(request.args.get('next')):
                     return redirect(request.args.get('next'))
                 return redirect(url_for('challenges.challenges_view'))
+
             else:  # This user exists but the password is wrong
+                logger.warn("[{date}] {ip} - submitted invalid password for {username}".format(
+                    date=time.strftime("%m/%d/%Y %X"),
+                    ip=utils.get_ip(),
+                    username=team.name.encode('utf-8')
+                ))
                 errors.append("Your username or password is incorrect")
                 db.session.close()
                 return render_template('login.html', errors=errors)
+
         else:  # This user just doesn't exist
+            logger.warn("[{date}] {ip} - submitted invalid account information".format(
+                date=time.strftime("%m/%d/%Y %X"),
+                ip=utils.get_ip()
+            ))
             errors.append("Your username or password is incorrect")
             db.session.close()
             return render_template('login.html', errors=errors)
+
     else:
         db.session.close()
         return render_template('login.html')


Test patch: