Skip to content

Instantly share code, notes, and snippets.

@johnhpatton
Last active December 19, 2021 17:19
Show Gist options
  • Save johnhpatton/e7980957c41d798975c3fc659f457a72 to your computer and use it in GitHub Desktop.
Save johnhpatton/e7980957c41d798975c3fc659f457a72 to your computer and use it in GitHub Desktop.
Nginx + Lua to mitigate CVE-2021-44228
-- -*- location: /etc/nginx/conf.d/cve_2021_44228.lua; -*-
-- -*- mode: lua; -*-
-- -*- author: John H Patton; -*-
-- -*- email: jhpattonconsulting@gmail.com; -*-
-- -*- license: MIT License; -*-
--
-- Copyright 2021 JH Patton Consulting, LLC
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this
-- software and associated documentation files (the "Software"), to deal in the Software
-- without restriction, including without limitation the rights to use, copy, modify,
-- merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to the following
-- conditions:
--
-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
-- OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.
-- Version: 1.0
local _M = {}
-- pattern to match beginning of jndi command in a matcher
local cve_2021_44228 = '%$%{jndi:'
-- pattern to capture XXX from a ${XXX} group in a matcher
local cve_2021_44228_group = '%$%b{}'
-- validate captured XXX group for substring patterns
-- ${lower:j}
-- ${upper:j}
local cve_2021_44228_badness = '%$%{[^:]+:[jndiJNDI:]+%}'
-- validate captured XXX group for substring patterns
-- ${::-j}
-- ${::-n}
-- ${::-d}
-- ${::-i}
-- ${::-:}
local cve_2021_44228_more_badness = '%$%{[^:]*:[^:]*:%-?[jndiJNDI:]+%}'
-- validate captured XXX group for substring patterns
-- looks for nested commands:
-- ${en${lower:v}:ENV_NAME:-j}
-- ${lower:${upper:${lower:${upper:${lower:${upper:${lower:${upper:${lower:${upper:${lower:${upper:${lower:j}}}}}}}}}}}}}
local cve_2021_44228_double_trouble ='%$%{[^$]*%$%{.+%}.+%}'
local function isempty(s)
return s == nil or s == ''
end
-- capture the request body
-- NOTE: set an nginx variable: captured_request_body
local function capture_request_body()
if isempty(ngx.var.captured_request_body) then
ngx.req.read_body()
local request_body = ngx.req.get_body_data()
if isempty(request_body) then
-- body may get buffered in a temp file:
local body_file = ngx.req.get_body_file()
if not isempty(body_file) then
local body_file_handle, err = io.open(body_file, "r")
if body_file_handle then
body_file_handle:seek("set")
request_body = body_file_handle:read("*a")
body_file_handle:close()
else
request_body = ""
ngx.log(ngx.ERR, "failed to open request body file or failed for reading, check system." )
end
else
request_body = ""
end -- if not isempty(body_file)
end -- isempty(request_body) -- file block
ngx.var.captured_request_body = request_body
end -- isempty(ngx.var.captured_request_body)
end -- function
-- capture the request headers
-- NOTE: set an nginx variable: captured_request_headers
function capture_request_headers()
if isempty(ngx.var.captured_request_headers) then
local ngh = ngx.req.get_headers()
if not isempty(ngh) then
local request_headers = ""
for k, v in pairs(ngh) do
if (type(v) == "table") then
for k2, v2 in pairs(v) do
if isempty(request_headers) then
request_headers = '"' .. k2 .. '":"' .. v2 .. '"'
else
request_headers = request_headers .. ',"' .. k2 .. '":"' .. v2 .. '"'
end
end
else
if isempty(request_headers) then
request_headers = '"' .. k .. '":"' .. v .. '"'
else
request_headers = request_headers .. ',"' .. k .. '":"' .. v .. '"'
end
end -- if (type(v)
end -- for k, v
ngx.var.captured_request_headers = request_headers
end -- if not isempty(ngh)
end -- if isempty(ngx.var.captured_request_headers) -- only needs to be captured once
end -- function
function _M.block_cve_2021_44228()
local match = ""
local found, last = 0
local first = 1
capture_request_headers()
capture_request_body()
local request = ngx.var.request .. ';;' .. ngx.var.captured_request_headers
request = ngx.unescape_uri(request)
if not isempty(ngx.var.captured_request_body) then
request = request .. ';;' .. ngx.var.captured_request_body
end
if not isempty(request) then
if string.match(request, cve_2021_44228) then
ngx.log(ngx.ERR, "cve-2021-44228-blocked: " .. string.match(request, cve_2021_44228))
ngx.var.cve_2021_44228_log = "cve-2021-44228-blocked"
ngx.status = ngx.HTTP_FORBIDDEN
ngx.exit(ngx.HTTP_FORBIDDEN)
else
while true do
found, last = request:find(cve_2021_44228_group, first)
if not found then break end
first=found + 2
if string.match(request:sub(first, last - 1), cve_2021_44228_badness)
or string.match(request:sub(first, last - 1), cve_2021_44228_more_badness)
or string.match(request:sub(first, last - 1), cve_2021_44228_double_trouble) then
ngx.log(ngx.ERR, "cve-2021-44228-blocked: ${ ... " .. request:sub(first, last) .. " ... }")
ngx.var.cve_2021_44228_log = "cve-2021-44228-blocked"
ngx.status = ngx.HTTP_FORBIDDEN
ngx.exit(ngx.HTTP_FORBIDDEN)
end -- if string.match
end -- while true
end -- if string.match
end -- if not isempty(request)
end -- function
return _M
-- end: cve_2021_44228.lua
@DisasteR
Copy link

There is a mistake L89 if isempty(ngx.var.captured_request_heders) then. There is a missing a at headers

@johnhpatton
Copy link
Author

johnhpatton commented Dec 15, 2021

Awesome, thanks, @DisasteR. I copy pasted from an older version. I've also adjusted the method a bit to be more accurate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment