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
|