Skip to content

Instantly share code, notes, and snippets.

@johnhpatton
Created December 15, 2021 12:59
Show Gist options
  • Save johnhpatton/fd43a9cae9301693c8e39f4c42e9e08e to your computer and use it in GitHub Desktop.
Save johnhpatton/fd43a9cae9301693c8e39f4c42e9e08e 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 = '%$%{[^:]+:[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
# /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;
}
}
# /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")
}
# /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