Created
December 15, 2021 12:59
-
-
Save johnhpatton/fd43a9cae9301693c8e39f4c42e9e08e to your computer and use it in GitHub Desktop.
Nginx + Lua to mitigate CVE-2021-44228
This file contains 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: 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 = '%$%{[^:]+:[jndi:]%}' | |
-- validate captured XXX group for substring patterns | |
-- ${::-j} | |
-- ${::-n} | |
-- ${::-d} | |
-- ${::-i} | |
-- ${::-:} | |
local cve_2021_44228_more_badness = '%$%{[^:]*:[^:]*:%-?[jndi:]%}' | |
-- 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_heders) 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 first, last = 0 | |
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 | |
first, last = request:find(cve_2021_44228_group, first+1) | |
if not first then break end | |
if string.match(request:sub(first, last), cve_2021_44228_badness) | |
or string.match(request:sub(first, last), cve_2021_44228_more_badness) | |
or string.match(request:sub(first, last), 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 |
This file contains 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
# /etc/nginx/conf.d/default.conf | |
server { | |
listen 80 default_server; | |
server_name localhost; | |
### CALL LUA BLOCK FUNCTION ### | |
### Add to top of server block ### | |
set $captured_request_headers ""; | |
set $captured_request_body ""; | |
set $cve_2021_44228_log ""; | |
rewrite_by_lua_block { | |
cve_2021_44228.block_cve_2021_44228() | |
} | |
### END CALL LUA BLOCK FUNCTION ### | |
location / { | |
root /usr/share/nginx/html; | |
index index.html index.htm; | |
} | |
# redirect server error pages to the static page /50x.html | |
# | |
error_page 500 502 503 504 /50x.html; | |
location = /50x.html { | |
root /usr/share/nginx/html; | |
} | |
} |
This file contains 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
# /etc/nginx/conf.d/lua.conf | |
# GLOBAL LUA -- HTTP BLOCK LEVEL | |
# Lua locations | |
# $prefix is the server prefix ( -p {server_prefix} ) passed in | |
# to nginx at startup, or the default build prefix (/etc/nginx/). | |
lua_package_path "${prefix}conf.d/?.lua;/usr/local/lib/lua/?.lua;;"; | |
lua_package_cpath "/usr/lib64/lua/5.1/?.so;;"; | |
# Disable logging cosocket lua TCP socket read timeout. | |
lua_socket_log_errors off; | |
# initialize lua globals | |
init_by_lua_block { | |
cve_2021_44228 = require("cve_2021_44228") | |
} |
This file contains 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
# /etc/nginx/nginx.conf | |
# nginx plus implementation for RHEL hosts, see documentation | |
# for other distributions. | |
### MODULES REQUIRED ### | |
load_module modules/ndk_http_module.so; | |
load_module modules/ngx_http_lua_module.so; | |
### END MODULES REQUIRED ### | |
error_log /var/log/nginx/error.log error; | |
pid /var/run/nginx.pid; | |
lock_file /var/lock/nginx.lock; | |
worker_processes auto; | |
events { | |
worker_connections 1024; | |
} | |
http { | |
include /etc/nginx/mime.types; | |
default_type application/octet-stream; | |
### OPTIONAL ADJUSTED LOG FORMAT ### | |
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' | |
'$status $body_bytes_sent "$http_referer" ' | |
'"$http_user_agent" "$http_x_forwarded_for" ' | |
'"$cve_2021_44228_log"'; | |
### END ADJUSTED LOG FORMAT ### | |
access_log /var/log/nginx/access.log main; | |
sendfile on; | |
#tcp_nopush on; | |
keepalive_timeout 65; | |
#gzip on; | |
include /etc/nginx/conf.d/*.conf; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment