Last active
December 19, 2021 17:19
-
-
Save johnhpatton/e7980957c41d798975c3fc659f457a72 to your computer and use it in GitHub Desktop.
Nginx + Lua to mitigate CVE-2021-44228
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- -*- location: /etc/nginx/conf.d/cve_2021_44228.lua; -*- | |
-- -*- mode: lua; -*- | |
-- -*- author: John H Patton; -*- | |
-- -*- email: [email protected]; -*- | |
-- -*- 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 |
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
There is a mistake L89
if isempty(ngx.var.captured_request_heders) then
. There is a missinga
atheaders