summaryrefslogtreecommitdiffstats
path: root/scripts/http-virustotal.nse
blob: 2a2cc8fafc3eedafe3fe50bef11a07e0771d344e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
local http = require "http"
local io = require "io"
local json = require "json"
local stdnse = require "stdnse"
local openssl = stdnse.silent_require "openssl"
local tab = require "tab"
local table = require "table"

description = [[
Checks whether a file has been determined as malware by Virustotal. Virustotal
is a service that provides the capability to scan a file or check a checksum
against a number of the major antivirus vendors. The script uses the public
API which requires a valid API key and has a limit on 4 queries per minute.
A key can be acquired by registering as a user on the virustotal web page:
* http://www.virustotal.com

The scripts supports both sending a file to the server for analysis or
checking whether a checksum (supplied as an argument or calculated from a
local file) was previously discovered as malware.

As uploaded files are queued for analysis, this mode simply returns a URL
where status of the queued file may be checked.
]]

---
-- @usage
-- nmap --script http-virustotal --script-args='http-virustotal.apikey="<key>",http-virustotal.checksum="275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"'
--
-- @output
-- Pre-scan script results:
-- | http-virustotal:
-- |   Permalink: https://www.virustotal.com/file/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f/analysis/1333633817/
-- |   Scan date: 2012-04-05 13:50:17
-- |   Positives: 41
-- |   digests
-- |     SHA1: 3395856ce81f2b7382dee72602f798b642f14140
-- |     SHA256: 275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f
-- |     MD5: 44d88612fea8a8f36de82e1278abb02f
-- |   Results
-- |     name                  result                          date      version
-- |     AhnLab-V3             EICAR_Test_File                 20120404  2012.04.05.00
-- |     AntiVir               Eicar-Test-Signature            20120405  7.11.27.24
-- |     Antiy-AVL             AVTEST/EICAR.ETF                20120403  2.0.3.7
-- |     Avast                 EICAR Test-NOT virus!!!         20120405  6.0.1289.0
-- |     AVG                   EICAR_Test                      20120405  10.0.0.1190
-- |     BitDefender           EICAR-Test-File (not a virus)   20120405  7.2
-- |     ByteHero              -                               20120404  1.0.0.1
-- |     CAT-QuickHeal         EICAR Test File                 20120405  12.00
-- |     ClamAV                Eicar-Test-Signature            20120405  0.97.3.0
-- |     Commtouch             EICAR_Test_File                 20120405  5.3.2.6
-- |     Comodo                Exploit.EICAR-Test-File         20120405  12000
-- |     DrWeb                 EICAR Test File (NOT a Virus!)  20120405  7.0.1.02210
-- |     Emsisoft              EICAR-ANTIVIRUS-TESTFILE!IK     20120405  5.1.0.11
-- |     eSafe                 EICAR Test File                 20120404  7.0.17.0
-- |     eTrust-Vet            the EICAR test string           20120405  37.0.9841
-- |     F-Prot                EICAR_Test_File                 20120405  4.6.5.141
-- |     F-Secure              EICAR_Test_File                 20120405  9.0.16440.0
-- |     Fortinet              EICAR_TEST_FILE                 20120405  4.3.392.0
-- |     GData                 EICAR-Test-File                 20120405  22
-- |     Ikarus                EICAR-ANTIVIRUS-TESTFILE        20120405  T3.1.1.118.0
-- |     Jiangmin              EICAR-Test-File                 20120331  13.0.900
-- |     K7AntiVirus           EICAR_Test_File                 20120404  9.136.6595
-- |     Kaspersky             EICAR-Test-File                 20120405  9.0.0.837
-- |     McAfee                EICAR test file                 20120405  5.400.0.1158
-- |     McAfee-GW-Edition     EICAR test file                 20120404  2012.1
-- |     Microsoft             Virus:DOS/EICAR_Test_File       20120405  1.8202
-- |     NOD32                 Eicar test file                 20120405  7031
-- |     Norman                Eicar_Test_File                 20120405  6.08.03
-- |     nProtect              EICAR-Test-File                 20120405  2012-04-05.01
-- |     Panda                 EICAR-AV-TEST-FILE              20120405  10.0.3.5
-- |     PCTools               Virus.DOS.EICAR_test_file       20120405  8.0.0.5
-- |     Rising                EICAR-Test-File                 20120405  24.04.02.03
-- |     Sophos                EICAR-AV-Test                   20120405  4.73.0 TP
-- |     SUPERAntiSpyware      NotAThreat.EICAR[TestFile]      20120402  4.40.0.1006
-- |     Symantec              EICAR Test String               20120405  20111.2.0.82
-- |     TheHacker             EICAR_Test_File                 20120405  6.7.0.1.440
-- |     TrendMicro            Eicar_test_file                 20120405  9.500.0.1008
-- |     TrendMicro-HouseCall  Eicar_test_file                 20120405  9.500.0.1008
-- |     VBA32                 EICAR-Test-File                 20120405  3.12.16.4
-- |     VIPRE                 EICAR (v)                       20120405  11755
-- |     ViRobot               EICAR-test                      20120405  2012.4.5.5025
-- |_    VirusBuster           EICAR_test_file                 20120404  14.2.11.0
--
-- @args http-virustotal.apikey an API key acquired from the virustotal web page
-- @args http-virustotal.upload true if the file should be uploaded and scanned, false if a
--       checksum should be calculated of the local file (default: false)
-- @args http-virustotal.filename the full path of the file to checksum or upload
-- @args http-virustotal.checksum a SHA1, SHA256, MD5 checksum of a file to check
--


author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories={"safe", "malware", "external"}


local arg_apiKey = stdnse.get_script_args(SCRIPT_NAME .. ".apikey")
local arg_upload = stdnse.get_script_args(SCRIPT_NAME .. ".upload") or false
local arg_filename = stdnse.get_script_args(SCRIPT_NAME .. ".filename")
local arg_checksum = stdnse.get_script_args(SCRIPT_NAME .. ".checksum")

prerule = function() return true end

local function readFile(filename)
  local f = io.open(filename, "r")
  if ( not(f) ) then
    return false, ("Failed to open file: %s"):format(filename)
  end

  local str = f:read("a")
  f:close()
  if ( not(str) ) then
    return false, "Failed to read file contents"
  end
  return true, str
end

local function requestFileScan(filename)
  local status, str = readFile(filename)
  if ( not(status) ) then
    return false, str
  end

  local shortfile = filename:match("^.*[\\/](.*)$")
  local boundary = "----------------------------nmapboundary"
  local header = { ["Content-Type"] = ("multipart/form-data; boundary=%s"):format(boundary) }
  local postdata = ("--%s\r\n"
  .. 'Content-Disposition: form-data; name="apikey"\r\n\r\n'
  .. "%s\r\n"
  .. "--%s\r\n"
  .. 'Content-Disposition: form-data; name="file"; filename="%s"\r\n'
  .. "Content-Type: text/plain\r\n\r\n%s\r\n--%s--\r\n"):format(boundary, arg_apiKey, boundary, shortfile, str, boundary)

  local host = "www.virustotal.com"
  local port = { number = 80, protocol = "tcp" }
  local path = "/vtapi/v2/file/scan"

  local response = http.post( host, port, path, {any_af = true, header = header }, nil, postdata )
  if ( not(response) or response.status ~= 200 ) then
    return false, "Failed to request file scan"
  end

  local status, json_data = json.parse(response.body)
  if ( not(status) ) then
    return false, "Failed to parse JSON response"
  end

  return true, json_data
end

local function getFileScanReport(resource)

  local host = "www.virustotal.com"
  local port = { number = 80, protocol = "tcp" }
  local path = "/vtapi/v2/file/report"


  local response = http.post(host, port, path, {any_af=true}, nil, { ["apikey"] = arg_apiKey, ["resource"] = resource })
  if ( not(response) or response.status ~= 200 ) then
    return false, "Failed to retrieve scan report"
  end

  local status, json_data = json.parse(response.body)
  if ( not(status) ) then
    return false, "Failed to parse JSON response"
  end

  return true, json_data
end

local function calcSHA256(filename)

  local status, str = readFile(filename)
  if ( not(status) ) then
    return false, str
  end
  return true, stdnse.tohex(openssl.digest("sha256", str))
end

local function parseScanReport(report)
  local result = {}

  table.insert(result, ("Permalink: %s"):format(report.permalink))
  table.insert(result, ("Scan date: %s"):format(report.scan_date))
  table.insert(result, ("Positives: %s"):format(report.positives))
  table.insert(result, {
    name = "digests",
    ("SHA1: %s"):format(report.sha1),
    ("SHA256: %s"):format(report.sha256),
    ("MD5: %s"):format(report.md5)
  })

  local tmp = {}
  for name, scanres in pairs(report.scans) do
    local res = ( scanres.detected ) and scanres.result or "-"
    table.insert(tmp, { name = name, result = res, update = scanres.update, version = scanres.version })
  end
  table.sort(tmp, function(a,b) return a.name:upper()<b.name:upper() end)

  local scan_tbl = tab.new(4)
  tab.addrow(scan_tbl, "name", "result", "date", "version")
  for _, v in ipairs(tmp) do
    tab.addrow(scan_tbl, v.name, v.result, v.update, v.version)
  end
  table.insert(result, { name = "Results", tab.dump(scan_tbl) })

  return result
end

local function fail(err) return stdnse.format_output(false, err) end

action = function()

  if ( not(arg_apiKey) ) then
    return fail("An API key is required in order to use this script (see description)")
  end

  local resource
  if ( arg_upload == "true" and arg_filename ) then
    local status, json_data = requestFileScan(arg_filename, arg_apiKey)
    if ( not(status) or not(json_data['resource']) ) then
      return fail(json_data)
    end
    resource = json_data['resource']

    local output = {}
    table.insert(output, "Your file was successfully uploaded and placed in the scanning queue.")
    table.insert(output, { name = "To check the current status visit:", json_data['permalink'] })
    return stdnse.format_output(true, output)
  elseif ( arg_filename ) then
    local status, sha256 = calcSHA256(arg_filename)
    if ( not(status) ) then
      return fail("Failed to calculate SHA256 checksum for file")
    end
    resource = sha256
  elseif ( arg_checksum ) then
    resource = arg_checksum
  else
    return
  end

  local status, response

  local status, response = getFileScanReport(resource)
  if ( not(status) ) then
    return fail("Failed to retrieve file scan report")
  end

  if ( not(response.response_code) or 0 == tonumber(response.response_code) ) then
    return fail(("Failed to retrieve scan report for resource: %s"):format(resource))
  end

  return stdnse.format_output(true, parseScanReport(response))
end