Scan anti-malware de flux XML avec ModSecurity

Rédigé par Nicolas Sulek Aucun commentaire
Classé dans : Logiciel, Système Mots clés : SOAP, XML, , Apache, clamav, ModSecurity

Quand on cherche sur Internet comment analyser les fichiers envoyés vers un serveur Web (généralement Apache), on ne trouve qu'une gestion des envois en POST, ce qui suffit dans la majorité des cas.

Généralement, ça consiste à rajouter le module ModSecurity à Apache et ClamAV ou autre antivirus.

Le traitement est alors le suivant, ModSecurity :

  • intercepte le fichier et le copie dans un répertoire temporaire
  • lance un script faisant appel à l'antivirus
  • bloque ou autorise la requête en fonction du résultat de l'antivirus.

Pour mon usage, il était nécessaire de scanner également les fichiers envoyés en SOAP, c'est-à-dire encapsulé au sein d'un fichier XML avec d'autres informations..
Pour info, ça ressemble à ça :


<soap-env:envelope soap-env:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/">
 <soap-env:body>
 <ns4:storeresource>
 <encodedfile xsi:type="xsd:string">dG90bwo=</encodedfile>
 <data soap-enc:arraytype="SOAP-ENC:Struct[5]" xsi:type="SOAP-ENC:Array">
 <item><column xsi:type="xsd:string">type_id</column>
<value xsi:type="xsd:string">102</value>
 <type xsi:type="xsd:string">integer</type>
</item> <item><column xsi:type="xsd:string">scan_location</column>
<value xsi:type="xsd:string">VILLE</value>
 <type xsi:type="xsd:string">string</type></item>
 <item><column xsi:type="xsd:string">destination</column>
<value xsi:type="xsd:string">AGFCPUB</value> 
<type xsi:type="xsd:string">string</type></item> 
<item><column xsi:type="xsd:string">is_ingoing</column>
<value xsi:type="xsd:string">Y</value>
 <type xsi:type="xsd:string">string</type></item>
 <item><column xsi:type="xsd:string">typist</column>
<value xsi:type="xsd:string">scanner</value>
 <type xsi:type="xsd:string">string</type></item>
 </data> 
<collid xsi:type="xsd:string">letterbox_coll</collid>
<table xsi:type="xsd:string">
<tbody><tr><td>res_letterbox</td></tr></tbody>
</table>
<fileformat xsi:type="xsd:string">pdf</fileformat>
<status xsi:type="xsd:string">ATT</status> 
</ns4:storeresource>
 </soap-env:body> 
</soap-env:envelope>

On a donc un fichier XML qui contient différents champs, mais celui qui nous intéresse est encodedFile, contenant le fichier envoyé vers le serveur Web codé en base64.
Les champs sont bien parsés par ModSecurity qui va vérifier la cohérence des valeurs entrées avec le schéma XML défini, mais le fichier stocké entre les balises encodedFile n'est pas vérifié.

Du coup, j'ai mis en place le fonctionnement suivant (qui n'est pas parfait, mais à le mérite de fonctionner), inspiré fortement de l'analyse des fichiers envoyés en POST :

  • ModSecurity parse le XML
  • ModSecurity récupére le contenu du champ encodedFile et le passe à un script
  • le script écrit le contenu de la chaîne de caractères passée en argument dans un fichier temporaire et lance l'antivirus sur ce fichier
  • ModSecurity bloque ou autorise la requête en fonction du résultat de l'antivirus.

Configuration de ModSecurity

Il faut utiliser la version 2.9 de ModSecurity.
Dans la configuration générale de ModSecurity (/etc/modsecurity/modsecurity.conf), il faut activer le parser XML:

SecRule REQUEST_HEADERS:Content-Type "(?:text|application)/xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" 

Ensuite, on déclare une règle spécifique pour lancer notre script :

SecRule "XML://encodedFile/text()" "@inspectFile /usr/local/bin/runav.lua" "phase:2,t:none,log,deny,msg:'Virus found in uploaded file',id:'950116'" 

La variable XML est alimentée par le parser XML de ModSecurity. Elle contient donc l'ensemble de notre fichier XML précédent, en fait l'arbre XML complet.
XML://encodedFile/text() permet de récupérer la valeur de encodedFile, quelque soit sa position dans l'arbre.
@inspectFile /usr/local/bin/runav.lua passe la valeur de XML://encodedFile/text() en argument du script /usr/local/bin/runav.lua .

Analyse du fichier

Pour le moment, ModSecurity a extrait notre fichier sous forme de chaîne de caractères en base 64. Il reste à l'analyser.

Le script suivant est grandement inspiré de celui de angeloxx :

function main(s)

-- chemin vers les executables
local clamdscan = "/usr/bin/clamdscan"
local clamscan = "/usr/bin/clamscan"

-- recuperation de l identifiant de la transaction pour nommer le fichier temporaire de maniere unique
local identifiant = assert(m.getvar("UNIQUE_ID"), "Empty ID")

-- failoverOnClamdFailure: bascule vers clamscan si clamdscan est en erreur
local failoverOnClamdFailure = true

-- echec et blocage si clamdscan et clamscan echouent
local failOnError = true

local agent = "clamdscan"

-- on evite de scanner les chaines de caracteres vides
len = assert(string.len(s), "Length error")
if len == 0 then
m.log(1, "[scanav skipped, soap xml size is zero]")
return nil
end
-- ecriture de la chaine de caracteres passee en argument dans un fichier temporaire
local filename = "/var/cache/modsecurity/tmp/" .. identifiant
local fileHandle = assert(io.open(filename,'w'), "Erreur ouverture")
assert(fileHandle:write(s), "Erreur ecriture")
assert(fileHandle:close(), "Erreur fermeture")

local cmd = clamdscan .. " --fdpass --stdout --no-summary"

-- lancement de la commande et recuperation du resultat
local f = assert(io.popen(cmd .. " " .. filename), "Erreur clamdscan")
local l = assert(f:read("*a"), "Read error")
assert(f:close(), "Close error")

-- on verifie si FOUND ou ERROR sont presents dans la sortie du scan
local isVuln = string.find(l, "FOUND")
local isError = string.find(l, "ERROR")

-- si clamdscan a echoue et que clamscan est utilise en bascule
if isError and failoverOnClamdFailure then
m.log(1, "[clamdscan fails (" .. l .. "), failover to clamscan]")
agent = "clamscan"
cmd = clamscan .. " --stdout --no-summary"
f = assert(io.popen(cmd .. " " .. filename .. " || true"), "Erreur clamscan")
l = assert(f:read("*a"), "Erreur lecture")
assert(f:close(), "Erreur fermeture")
isVuln = string.find(l, "FOUND")
isError = string.find(l, "ERROR")
end

-- suppression du fichier temporaire
assert(os.remove(filename), "Erreur effacement")

if isVuln then
m.log(1, "[" .. agent .. " scanner message: " .. l .. "]")
return "Virus Detected"
elseif isError and failOnError then
-- is a error (not a virus) a failure event?
m.log(1, "[" .. agent .. " scanner message: " .. l .. "]")
return "Error Detected"
else
m.log(1, "[" .. agent .. " scanner message: " .. l .. "]") return nil
end
end

Les commentaires sont fermés.