ClamAV LUA script per ModSecurity

Aprile 16, 2017 technews

Ho cominciato ad utilizzare ModSecurity nel 2004 quando ho avuto bisogno di progettegere applicazioni come PhpMyAdmin oppure SquirrelMail dai classici attacchi di SQL Injection che erano popolari in quegli anni. Assieme a fail2ban, monit ed alcuni script fatti in casa per correlare log e attacchi ha sempre fatto parte dell’insieme degli strumenti utilizzati per garantire i servizi erogati dai miei web server.

Ultimamente sono di nuovo alle prese con ModSecurity perché sto aggiornando la mia infrastruttura ad una versione decisamente più aggiornata, sia per quanto riguarda il modulo che il set di regole core (CRS); per questo motivo ho deciso di avventurarmi nella scansione con antivirus degli upload.

In passato ho già usato questa funzionalità da qualche cliente, utilizzando uno script Perl che chiamava clamdscan (o clamscan in caso di fallimento) ma questa funzionalità ha lo svantaggio di richiedere ad Apache di avviare ogni volta una shell con un interprete Perl (una operazione che occupa tempo e memoria) che a sua volta lancia un’altra shell per effettuare la scansione del/dei file.

Ad oggi ModSecurity supporta un linguaggio di programmazione embedded chiamato Lua per ridurre la latenza introdotta dalla shell esterna ed usa LuaJIT per compilare ed eseguire lo script; è quindi ora di convertire lo script Perl di virusscan in Lua.

Qui trovi la versione aggiornata:

#!/usr/bin/lua
--[[
   This script can be used to inspect uploaded files for viruses
   via ClamAV. To implement, use with the following ModSecurity rule:

   SecRule FILES_TMPNAMES "@inspectFile /opt/modsecurity/bin/modsec-clamscan.lua" "phase:2,t:none,log,deny"

   Author: Angelo Conforti (based on Josh Amishav-Zlatin code)
   Requires the clamav-server and clamav-scanner
   
   If you use SELinux on RHEL base distro:
   setsebool -P antivirus_can_scan_system 1
   
   And remember that CentOS ClamAV distribution has some issue 
   with permission in the "default" configuration. Use Debian and
   you'll be happy :)

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
]]--


function fsize(filename)
   file = io.open(filename,"r")
   local current = file:seek()
   local size = file:seek("end")
   file:seek("set",current)
   file:close()
   return size
end

function main(filename)
   -- Configure paths
   local clamdscan  = "/usr/bin/clamdscan"
   local clamscan  = "/usr/bin/clamscan"
   
   -- failoverOnClamdFailure: failover to clamscan if clamdscan report an error
   local failoverOnClamdFailure = true
   
   -- fail (and block) if clamdscan (and clamscan) fails
   local failOnError = false
   
   -- local var
   local agent = "clamdscan"

   -- Skip empty items because if clamd is not working and you
   -- use the clamscan agent an empty file can take about 12 secs 
   -- to be analyzed
   if fsize(filename) == 0 then
     m.log(1, "[scanav skipped, file " .. filename .." size is zero]")
     return nil
   end

   -- The system command we want to call with fdpass flag to 
   -- do not incur in a permission issue
   local cmd = clamdscan .. " --fdpass --stdout --no-summary"

   -- Run the command and get the output
   local f = io.popen(cmd .. " " .. filename .. " || true")
   local l = f:read("*a")
   f:close()

   -- Check the output for the FOUND or ERROR strings which indicate
   -- an issue we want to block access on
   local isVuln = string.find(l, "FOUND")
   local isError = string.find(l, "ERROR")

   -- If clamdscan fails and you want failover to the traditional clamscan...
   if isError and failoverOnClamdFailure then
     -- Try to use the clamscan program
     m.log(1, "[clamdscan fails (" .. l .. "), failover to clamscan]")
     agent = "clamscan"
     cmd = clamscan .. " --stdout --no-summary"
     f = io.popen(cmd .. " " .. filename .. " || true")
     l = f:read("*a")
     f:close()
     isVuln = string.find(l, "FOUND")
     isError = string.find(l, "ERROR")
   end

   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
     return nil
   end
end

Lo script ha alcune variabili che possono servire a configurare il funzionamento secondo le proprie esigenze:

  • failoverOnClamdFailure: il motore di scansione di default è clamdscan che usa clamd per velocizzare la procedura; clamd infatti carica le definizioni di virus una sola volta all’avvio e clamdscan passa al demone il filehandle (tramite –fdpass, per evitare guai con selinux) in modo da effettuare la scansione in meno di un secodno. Se clamdscan fallisce perché clamd non sta funzionante puoi scegliere se passare a clamscan (decisamente più lento, circa 12 secondi nel test) o fallire
  • failOnError: dcidi cosa succede se clamdscan (e clamscan, se è stato configurato il failover) fallisce: invia un errore defitivo a modsecurity, il che signifiva che la richiesta sarà rifiutata, ooppure ignora l’errore e permetti l’upload di un contenuto non verificato

Questo post è disponibile anche in: English