Tests de sécurité automatisés
Les chaînes de CI/CD permettent une industrialisation de la production logicielle en testant, packageant et déployant les applications automatiquement. Dans ces chaînes automatiques, l’aspect sécurité est régulièrement mis de côté et les tests associés sont bien souvent manuels.
Les tests automatiques de sécurité peuvent être catégorisés en 2 familles, le SAST (Static Application Security Testing) qui analyse de manière statique le code produit par les développeurs ou les fichiers de configuration paramétrés par les administrateurs système alors que le DAST (Dynamic Application Security Testing) fait des tests sur une application ou un serveur déployé et accessible. Si le SAST apparait en début de chaîne, sur le poste des développeurs ou sur l’outil d’intégration continue, le DAST intervient quant à lui en fin de chaîne lorsque l’application a été packagée et déployée sur un environnement.
Les outils présentés ici sont dans la catégorie DAST et permettent principalement de valider la non-régression sur certains points spécifiques (ports ouverts, qualité de la configuration TLS, protection contre Heartbleed, etc.). Il ne s’agit pas d’outils de “pentest automatique” se substituant à un pentesteur en recherchant des SQL injections, XSS, etc (même si des outils comme Arachni ou sqlmap sont intégrables).
Les 2 outils présentés sont Gauntlt et RobotFramework.
GAUNTLT
Gauntlt est un outil open source créé en 2012 permettant d’écrire des tests de sécurité grâce au langage Gherkin interprété par l’outil de test Cucumber. Gauntlt fait l’interface entre les outils de sécurité classiques (nmap, sslyze, etc.) et l’outil de test. Les outils de sécurité ne sont pas intégrés à gauntlt et doivent être installés sur la machine exécutant les tests.
Même si le projet ne semble plus très actif, les éléments de base présents sont suffisants pour mettre en place des tests de sécurité.
Gauntlt propose par défaut des adaptateurs pour les outils suivants :
Fonctionnement
Ecriture d’un test
L’exemple suivant utilise l’adaptateur nmap pour vérifier que le port 80 est ouvert sur scanme.nmap.org.
Etapes :
- Vérifier que nmap est installé sur la machine
- Définir les variables (hostname = scanme.nmap.org)
- Définir un scenario de test
- Décrire la commande nmap à lancer
- Vérifier le résultat de la commande avec une expression régulière
@slow
Feature: simple nmap attack (sanity check)
Background:
Given "nmap" is installed
And the following profile:
| name | value |
| hostname | scanme.nmap.org |
Scenario: Verify server is available on standard web port
When I launch an "nmap" attack with:
"""
nmap -p 80 <hostname>
"""
Then the output should match /80.tcp\s+open/
Ce test peut être sauvegardé dans un fichier ayant pour extension .attack
.
A l’exécution du binaire gauntlt, l’outil joue les tests de tous les fichiers .attack
présents dans le dossier courant. Il est également possible de spécifier un fichier directement en argument.
En cas d’erreur :
Utilisation des alias
Cette première méthode de test a pour inconvénient de devoir spécifier pour chaque test la ligne de commande de l’outil sous-jacent (ici nmap). Pour rendre l’écriture de tests plus intuitive et simple, gauntlt propose des alias intégrants directement et à un seul endroit la ligne de commande de l’outil à utiliser.
Le 1er test peut par exemple évoluer en :
@slow
Feature: simple nmap attack
Background:
Given "nmap" is installed
And the following profile:
| name | value |
| host | scanme.nmap.org |
| port | 80 |
Scenario: Test
When I launch a "nmap-single_port" attack
Then the output should match /80.tcp\s+open/
Explications :
nmap-single_port
est un alias permettant d’exécuter la commandenmap -p<port> <host>
.<port>
ethost
sont des variables à définir avant l’appel de l’alias
Les alias définis par défaut sont disponibles à l’adresse suivante : https://github.com/gauntlt/gauntlt/tree/master/lib/gauntlt/attack_aliases
Création d’alias
De nouveaux alias peuvent être créés en modifiant le fichier json correspondant à nmap ./lib/gauntlt/attack_aliases/nmap.json
.
Par exemple :
"nmap-check_80_port" : {
"command" : "nmap -p80 <host>",
"description" : "Check the HTTP default port (80)",
"requires" : [ "<host>" ]
}
Pour les alias, gauntlt prend en compte tous les fichiers json situés dans le dossier /lib/gauntlt/attack_aliases
.
Création d’adaptateur
Pour ajouter le support d’un outil dans gauntlt, il faut décrire la syntaxe d’appel à l’outil de sécurité. Les adaptateurs sont situés dans le dossier ./lib/gauntlt/attack_adapters
.
Dans l’exemple suivant, le support de l’outil ssllabs-scan est ajouté en créant le fichier suivant ./lib/gauntlt/attack_adapters/ssllabs.rb
.
When /^"ssllabs" is installed$/ do
ensure_cli_installed("ssllabs-scan")
end
When /^I launch (?:a|an) "ssllabs" attack with:$/ do |command|
run_with_profile command
end
When /^I launch (?:a|an) "ssllabs-(.*?)" attack$/ do |type|
attack_alias = 'ssllabs-' + type
ssllabs_attack = load_attack_alias(attack_alias)
Kernel.puts "Running a #{attack_alias} attack. This attack has this description:\n #{ssllabs_attack['description']}"
run_with_profile ssllabs_attack['command']
end
Sont définis ici :
- La syntaxe et le test à faire pour vérifier que ssllabs-scan est bien installé
- La syntaxe pour exécuter une commande simple avec ssllabs-scan
- La syntaxe pour utiliser un alias
Un alias est ensuite créé dans ./lib/gauntlt/attack_aliases/ssllabs.json
pour ne récupérer que la note finale de l’analyse (entre F et A) :
{
"ssllabs-grade": {
"command": "ssllabs-scan --quiet --grade <host>",
"description": "Give SSL grade based on Qualys SSL Labs",
"requires": [
"<host>"
]
}
}
Enfin, pour utiliser l’adaptateur, le fichier d’attaque suivant est créé :
@slow
Feature: SSL Grade
Background:
Given "ssllabs" is installed
And the default aruba timeout is 360 seconds
And the following profile:
| name | value |
| host | google.fr |
Scenario: Test with alias
When I launch a "ssllabs-grade" attack
Then the output should match /"A"[\r\n](.*)"A"/
Scenario: Test without alias
When I launch a "ssllabs" attack with:
"""
ssllabs-scan --quiet --usecache --grade <host>
"""
Then the output should match /"A"[\r\n](.*)"A"/
A noter :
- Le but du test est de vérifier que le site testé à la note A
- 2 scenarios sont définis, le 1er utilise l’alias créé alors que le 2ème utilise le mode classique
- L’analyse par ssllabs-scan est assez longue et nécessite l’augmentation du timeout à 360 secondes.
- Le site testé (google.fr) dispose d’une ipv4 et d’une ipv6, ssllabs-scan renvoie dont 2 lignes avec 2 notes
Résultat :
RobotFramework
RobotFramework est un outil répandu permettant d’automatiser toute sorte de tests (IHM, API, etc.) via différents modules. Les tests de sécurité ne font pas exception et sont intégrables via des modules additionnels faisant appel aux outils de sécurité traditionnels (nmap, sslyze, etc.).
La société we45 propose à travers son Github un ensemble de modules pour des outils de sécurité répandus (Sslyze, Burp, Arachni, Nikto, etc.). Le module pour nmap est quant à lui disponible ici. Comme pour gauntlt, les modules ne contiennent pas les outils de sécurité, ces derniers doivent être installés sur la machine exécutant les tests.
Attention la plupart de ces modules sont écrits pour la version 2 de python. Une petite passe de correction peut être nécessaire pour les faire tourner sous python 3.
Écriture d’un test
Par exemple, l’écriture d’un test pour le module Sslyze donne :
*** Settings ***
Library RoboSslyze
*** Variables ***
${TARGET} www.google.com
*** Test Cases ***
Test for SSL
test ssl basic ${TARGET}
test ssl server headers ${TARGET}
Explications :
- Définition des modules à charger
- Définition des variables qui seront prises en entrée des tests
- Appels des tests (se référer à la documentation du module pour connaître les tests disponibles)
Résultat :
Modification d’un module
Le module nmap permet uniquement de lancer des scans (OS, ports, etc.) et renvoie le résultat de la commande dans un fichier texte situé dans le répertoire courant. Une analyse humaine est ensuite nécessaire pour interpréter les résultats. Le résultat dans Robot Framework est quant à lui toujours en succès.
La modification suivante a pour but de comparer la liste des ports ouverts retournée par la commande nmap avec une liste de ports autorisés définis dans les variables du test.
Dans le fichier RoboNmap.py, la méthode concernée par le test nmap all tcp scan
est nmap_all_tcp_scan
:
Remplacer :
def nmap_all_tcp_scan(self, target, file_export = None):
par
def nmap_all_tcp_scan(self, target, authorized_ports, file_export = None):
afin de pouvoir injecter la liste des ports autorisés lors de l’appel au test.
Ensuite dans le corps de la méthode, remplacer :
try:
parsed = NmapParser.parse(nmproc.stdout)
print(parsed)
self.results = parsed
except NmapParserException as ne:
print('EXCEPTION: Exception in Parsing results: {0}'.format(ne.msg))
par
try:
parsed = NmapParser.parse(nmproc.stdout)
logger.info(parsed)
self.results = parsed
for scanned_hosts in self.results.hosts:
for serv in scanned_hosts.services:
port = str(serv._portid)
if port not in authorized_ports and serv.state == 'open':
logger.warn("Port {0} is open".format(port))
except NmapParserException as ne:
print('EXCEPTION: Exception in Parsing results: {0}'.format(ne.msg))
Explications :
- Les résultats obtenus par la commande nmap sont parsés
- Ils sont comparés avec la liste de ports définis dans la variable
authorized_ports
- Seuls les ports avec l’état
open
et étant absents de la liste autorisée sont remontés dans un warning
Dans le fichier de test :
*** Settings ***
Library ./RoboNmap.py
Library Collections
*** Variables ***
${TARGET} mydomain.local
${AUTHORIZED_PORTS} 80 443
*** Test Cases ***
Check headers
http headers scan ${TARGET}
Ici, seuls les ports 80 et 443 sont autorisés avec le statut open
Exécution avant la modification :
Avec la modification
Pour être réellement opérationnel ce module nécessite encore quelques modifications comme une comparaison exacte avec la liste des ports autorisés.
Création d’un module additionnel
Si un module n’est pas disponible, il est possible de le créer. Dans le cas ci-dessous, l’objectif est de faire des vérifications sur les requêtes HTTP pour vérifier la présence de certains headers de sécurité (CSP, HSTS, etc.). Pour cette analyse, aucun outil externe n’est requis, le module requests
de python permettant de faire des requêtes HTTP est suffisant.
Dans un fichier RoboHttp.py :
from robot.api import logger
import requests
class RoboHttp(object):
ROBOT_LIBRARY_SCOPE = 'GLOBAL'
def __init__(self):
logger.info("Http initialized")
self.results = None
def http_headers_scan(self, target):
response = requests.get('https://' + target)
xssProtectionHeaderName = 'X-XSS-Protection'
matches = [x for x in response.headers if x.lower() == xssProtectionHeaderName.lower()]
if not any(matches):
logger.warn('{0} header missing'.format(xssProtectionHeaderName))
else:
logger.info('{0} header found with value "{1}"'.format(xssProtectionHeaderName, response.headers[matches[0]]))
xFrameOptionsHeaderName = 'X-Frame-Options'
matches = [x for x in response.headers if x.lower() == xFrameOptionsHeaderName.lower()]
if not any(matches):
logger.warn('{0} header missing'.format(xFrameOptionsHeaderName))
else:
logger.info('{0} header found with value "{1}"'.format(xFrameOptionsHeaderName, response.headers[matches[0]]))
cspHeaderName = 'Content-Security-Policy'
matches = [x for x in response.headers if x.lower() == cspHeaderName.lower()]
if not any(matches):
logger.warn('{0} header missing'.format(cspHeaderName))
else:
logger.info('{0} header found with value "{1}"'.format(cspHeaderName, response.headers[matches[0]]))
hstsHeaderName = 'Strict-Transport-Security'
matches = [x for x in response.headers if x.lower() == hstsHeaderName.lower()]
if not any(matches):
logger.warn('{0} header missing'.format(hstsHeaderName))
else:
logger.info('{0} header found with value "{1}"'.format(hstsHeaderName, response.headers[matches[0]]))
serverHeaderName = 'Server'
matches = [x for x in response.headers if x.lower() == serverHeaderName.lower()]
if any(matches):
logger.warn('{0} header found with value "{1}"'.format(serverHeaderName, response.headers[matches[0]]))
else:
logger.info('{0} header missing'.format(hstsHeaderName))
Le module ne définit qu’un seul test http headers scan
et donc qu’une seule méthode http_headers_scan
. Cette méthode prend en entrée un paramètre représentant la cible à tester target
. Le module logger
importé permet de piloter le résultat du test. La méthodelogger.warn
est utilisée pour renvoyer des warnings au niveau du test sans pour autant le mettre en échec.
Plus d’informations sur le module logger
sont disponibles ici.
Une fois le module créé, le fichier de test associé ressemble à :
*** Settings ***
Library ./RoboHttp.py
*** Variables ***
${TARGET} mydomain.local
*** Test Cases ***
Check headers
http headers scan ${TARGET}
Résultat :
Conclusion
L’aspect sécurité est identique entre les 2 solutions proposées puisque les outils de sécurité utilisés en arrière plan sont identiques (nmap, sslyze, etc.). D’un point de vue développement, l’approche gauntlt permet d’ajouter le support de nouveaux outils grâce à un langage naturel (Gherkin) sans requérir à des compétences en développement.