Vous êtes ici

Analyse et correctif du 0day Zimbra

Portrait de ivan

Quand quelqu’un balance sauvagement un 0day sur ExploitDB, sans aucune concertation avec l’éditeur, ça me fait généralement marrer. Mais ce matin, quand j’ai ouvert les yeux et découvert qu’une faille Zimbra était tombée, j’avoue avoir traversé un instant de panique.
Puis j’ai rigolé.
Puis j’ai re-paniqué.

UPDATE (08 décembre) : Contrairement à ce qu'a annoncé l'auteur, il apparaît que le 0day n'en est pas un : le bug avait été corrigé dans les versions 7.2.2 et 8.0.2.

Il se trouve que j’utilise justement Zimbra pour gérer mon serveur de mail personnel. M’attendant à des attaques en masse durant le week-end, j’ai préféré prendre les devants et regarder sous le capot ce qui clochait.

La première chose à dire, c’est que l’exploit fonctionne de manière stable. Comme je préfère le Python, j’ai redéveloppé une preuve de concept basée sur celle de rubina119 pour faire mes tests. Je l’aurais bien offerte en téléchargement, mais les lois sur la régulation des codes numériques dits "à double usage" sont en train de se durcir et j’aime mieux ne pas tendre le bâton pour me faire battre. Voyons plutôt comment fonctionne l’exploit.

Analyse du 0day

Il s’agit d’une bête LFI qui permet d’inclure un fichier arbitraire au milieu d’un fichier de propriétés. L’URL vulnérable est http://[hostname.tld]/zimbra/res/AjxTemplateMsg.js?skin=[path]%00. On n’obtient pas d’exécution de code par ce biais, mais ce n’est pas nécessaire. Le fichier /opt/zimbra/conf/localconfig.xml contient des informations de configuration de Zimbra cruciales, dont des identifiants utilisables sur l’API d’administration. Une fois ceux-ci récupérés, ils permettent d’obtenir une clef API avec laquelle on peut effectuer des opérations de maintenance parfaitement légitimes sur l’interface SOAP de l’application. Comme par exemple, créer un utilisateur privilégié.

Pour résumer de manière limpide :

  • On utilise la LFI pour récupérer des identifiants LDAP de l’utilisateur zimbra
  • Ces identifiants permettent de récupérer un token API via une requête SOAP légitime.
  • Grâce à une poignée de requêtes bien choisies envoyées sur /service/admin/soap avec le token, on crée un utilisateur privilégié.
  • Enfin, il ne reste plus qu’à se connecter au panneau d’administration du webmail attaqué. On peut alors lire les mails des utilisateurs, peut-être uploader du code hostile, etc…

Zimbra possède deux interfaces : une pour les utilisateurs du webmail sur les ports 80 et/ou 443, et une réservée aux administrateurs sur le port 7071. Si la faille LFI est présente sur les deux, l’API SOAP n’est pas accessible quand on a pris le soin de ne pas exposer le panneau d’administration aux inconnus. Si cette interface n’est pas accessible publiquement sur votre installation, vous pouvez souffler.
Mais même si le code actuel ne permet pas de s’introduire sur votre serveur, la lecture arbitraire de fichiers persiste. Et ça fait désordre.

J’ai constaté que personne ne s’était ému de ce 0day sur le forum officiel de Zimbra. En prime, j’ai cru comprendre que les développeurs sont en plein changement de système de versionning, ce qui me laisse penser que la faille ne sera pas corrigée immédiatement. J’ai donc décidé de prendre le problème à bras le corps et de patcher héroïquement mon serveur.

Correction du bug

La base de code de Zimbra étant gigantesque, retrouver le code coupable n’a pas été une mince affaire. Après une bonne heure et demie à faire des grep hasardeux dans le dossier de l’application, j’ai fini par trouver la fonction à abattre.

// File /com/zimbra/kabuki/servlets/Props2JsServlet.java
 
protected void load(HttpServletRequest req, DataOutputStream out,
				Locale locale, List<List<String>> basenamePatterns,
				String basedir, String dirname, String classname) throws IOException {
String basename = basedir + classname;
 
out.writeBytes("// Basename: " + getCommentSafeString(basename) + '\n');
for (List<String> basenames : basenamePatterns) {
	try {
		ClassLoader parentLoader = this.getClass().getClassLoader();
		PropsLoader loader = new PropsLoader(parentLoader, basenames,
				basedir, dirname, classname);
 
		// load path list, but not actual properties to prevent caching
		ResourceBundle.getBundle(basename, locale, loader,  new ResourceBundle.Control()
		{
			// […snip…]
		});
		for (File file : loader.getFiles()) {
			Props2Js.convert(out, file, classname); // /!\ The file is included here
		}
	} catch (MissingResourceException e) {
		out.writeBytes("// properties for " + classname + " not found\n");
	} catch (IOException e) {
		out.writeBytes("// properties error for " + classname +
				" - see server log\n");
		error(e.getMessage());
	}
}
}

Le paramètre vulnérable de l’URL se retrouve dans la liste de chaînes basenames. Les valeurs attendues sont de type skins/carbon/messages/${name}. Mais l’attaquant contrôle la valeur centrale via l’URL, ce qui permet d’insérer (par exemple) ce chemin : skins/../../[…]/../etc/password%00/messages/${name}. Pour une raison qui ne m’apparaît pas clairement, la méthode ResourceBundle.getBundle est appelée avec un classloader modifié (PropsLoader) qui en profite pour voler la liste des fichiers dans l’extrait suivant :

// File /com/zimbra/kabuki/servlets/Props2JsServlet.java / nested class PropsLoader
 
public InputStream getResourceAsStream(String rname) 
{
	String filename = rname.replaceAll("^.*/", "");
	Matcher matcher = RE_LOCALE.matcher(filename);
	String locale = matcher.matches() ? matcher.group(1) : "";
	String ext = rname.replaceAll("^[^\\.]*", "");
	for (String basename : this.patterns) {
		basename = basename.replaceAll("\\$\\{dir\\}", this.dir);
		basename = basename.replaceAll("\\$\\{name\\}", this.name);
		basename = replaceSystemProps(basename);
		basename += locale + ext;
		File file = new File(this.dirname+basename); // /!\ Unchecked!
		if (!file.exists()) {
			file = new File(basename);
		}
		if (file.exists())
		{
			files.add(file);
			return new ByteArrayInputStream(new byte[0]);
		}
	}
	return super.getResourceAsStream(rname);
}

Cela ressemble à un hack un peu sale mais qu’importe, c’est ici que le mal est fait : sans aucune vérification préalable, la ligne File file = new File(this.dirname+basename); ajoute des fichiers qui seront inclus dans la page (ladite inclusion est visible dans l’extrait de code précédent).

Connaissant mal la base de code de Zimbra, je n’ai pas voulu risquer de causer des effets de bords en appliquant une expression régulière un peu sévère au nom de fichier final. Qui sait, peut-être que l’application a parfois besoin de remonter légitimement dans l’arborescence. En revanche, j’ai estimé qu’il était raisonnable de considérer que le fichier devrait se trouver dans dirname. La minuscule modification suivante résout le bug :

public InputStream getResourceAsStream(String rname) {
	// [...snip...]
	for (String basename : this.patterns) {
		// [...snip...]
		if (file.exists())
		{
			// BUGFIX: Prevents LFI
			if (is_child(file, new File(dirname))) {
				files.add(file);
			}
			return new ByteArrayInputStream(new byte[0]);
		}
	}
	return super.getResourceAsStream(rname);
}

Il y a probablement moyen de faire un correctif plus fin – je laisse ce soin aux développeurs du projet. En attendant, il y a au moins une rustine.

Application du patch

Vous pouvez télécharger mes classes modifiées. Il suffit de remplacer celles qui se trouvent dans :

  • /opt/zimbra/jetty/webapps/zimbra/WEB-INF/classes/com/zimbra/kabuki/servlet/
  • /opt/zimbra/jetty/webapps/zimbraAdmin/WEB-INF/classes/com/zimbra/kabuki/servlet/

Faites ensuite un petit zmcontrol restart et le tour est joué.
Attention : ces classes ont été compilées pour la version de Zimbra 8.0.0, avec le zimbracommons.jar de ma machine… Il est possible qu’ils ne soient pas compatibles avec votre installation ! Je ne saurais que trop vous encourager à faire une copie des .class originaux avant de les remplacer.

Les classes à écraser sont :

  • Props2Js$1PropertyPrinter.class
  • Props2Js.class
  • Props2JsServlet$1.class
  • Props2JsServlet$PropsLoader.class
  • Props2JsServlet.class

Si jamais le patch ne fonctionnait pas pour vous, vous pouvez également compiler les classes vous-même. Pour effectuer cette modification, j’ai créé un projet minimal contenant les deux fichiers Props2JsServlet.java et Props2Js.java. La compilation n’est possible qu’en linkant avec zimbracommon.jar de mon serveur.
Vous trouverez le vôtre sans peine en allant dans /opt/zimbra et en faisant un petit find -name "zimbracommons.jar".

Il ne reste plus qu’à tester manuellement si la faille est toujours présente.
Le tour est joué, et pour les quelques jours qui viennent, vous pourrez vous gausser d’avoir l’un des Zimbras les plus sûrs du monde réparé votre serveur tout seul, comme un grand.

Commentaires

J'imagine qu'il faut changer tous les mots de passes du localconfig après avoir été pris ..., est-ce que tu aurais des infos sur la procédure à suivre ?

Portrait de ivan

Effectivement, ça ne peut pas faire de mal... Mais je n'ai aucune idée de la manière de procéder.
Le plus important, c'est en tout cas de s'assurer que l'interface d'administration (sur le port 7071) n'est pas accessible depuis l'extérieur. A ma connaissance, il n'est pas possible d'exploiter la faille quand c'est le cas.

Ajouter un commentaire

(If you're a human, don't change the following field)
Your first name.
(If you're a human, don't change the following field)
Your first name.
(If you're a human, don't change the following field)
Your first name.

Plain text

  • Aucune balise HTML autorisée.
  • Les adresses de pages web et de courriels sont transformées en liens automatiquement.
  • Les lignes et les paragraphes vont à la ligne automatiquement.
To prevent automated spam submissions leave this field empty.
CAPTCHA
Prouve que tu n'es pas un script en résolvant l'énigme suivante :