Exfiltrating MyBB Attachments

Recently I accidentally deleted an important SSH key and lost access to a server running MyBB that I manage for a friend. As a result I could not make new backups to download attachments, avatars, and other files I needed from the server. Unfortunately the automated backups to Wasabi Cloud stopped after an API key was deleted from the account, additionally due to bucket lifecycle rules (enforced retention + autodelete), old backups were gone. It was a very bad situation and had I not acted quickly enough I would’ve lost everything. I logged into the MyBB admin panel and downloaded a database backup. I just happened to be learning Python recently and rather than engaging in an expensive data recovery exercise that may or may not find the private key I am looking for I decided to write a program to try and exfiltrate the attachments. When I started this project I had no idea if or how I would pull this off. The alternative was losing everything so I gave it a try.

The VPS Provider’s support would not help me

Unfortunately due to maintenance, a broken user (and likely admin) control panel, and possibly internal privacy policies, the VPS provider was unable to provide me with a disk image or to attach an SSH key. Needless to say after a few hours of chatting back and forth, it seemed to be a lost cause, I was on my own and a sitting duck until I found out a solution or they fixed their tools (if they fix them).

What data needs to be pulled from the VPS before I can delete it and create a new one?

To restore the site I need at a minimum the following data. With the exception of the theme images (which we get from MyBB.com) and a database backup (which we can get from the admin panel), everything has to be downloaded over SSH as the admin panel does not give you a way to download it. As I am locked out of SSH there would be significant data loss if I had taken the database backup and gave up. Here’s a list of what data I needed to get from the server.

  • A copy of MyBB and any plugins (we can get all of these from MyBB.com)
  • A database backup (we have this)
  • All custom images for the theme provided by the developer (we can get those https://community.mybb.com/mods.php?action=view&pid=56)
  • All custom images we added
  • All user attachments (this is split into a .attach file and a thumbnail where applicable)
  • All user avatars
  • All user group star images
  • All custom similes (custom images that act like Emoji)

How was I going to pull this off?

When I noticed became aware of the issue my first attempts were to use the VPS provider’s password reset tool then open the VNC console. Since they’re doing maintenance their panel is broken I had no way to attach a new SSH key. Also Debian package updates ran automatically and OpenSSH was configured to only allow login by SSH key. There is a vsftpd service running, if I could guess the username (it hasn’t been used in years) I might have be able to guess the password programmatically assuming I used Tor or something to get around fail2ban. I could try to generate SSH keys over and over and see if I generate the same key however with our current understanding of the laws of physics cracking an RSA key is impossible to do within my lifetime. This is a case where my own security had backfired. I was locked out and there was nothing I could do to get back in… right? But if there’s a will well there’s Catgirl sitting at their computer screen finding a solution to seemingly impossible problems. OpenSSH and VSFTPD are probably not viable attack surfaces so how do I get the data I need out? I decided to attack what I know the most about, web applications. I decided to look for weaknesses in MyBB and the server configuration and exploit that until I get every last bit of data I need.

To start, I logged into the admin panel and checked the current MyBB version and any plugins we were using and unfortunately all are the newest versions. My initial thoughts were to exploit a security vulnerability in MyBB and install a PHP script that let’s me download files from the server and write to normally restricted locations. Could I find a new vulnerability that’s not public yet? It’s possible but I am on a time crunch and don’t have time to play. So I went ahead and downloaded the database backup incase there was anything of interest there I could pull.

At my workstation I imported the database backup onto a local install of the database software MySQL. This allowed me to freely query a copy of the database. I started looking around and found a few tables of interest.

Table nameContent of interest
mybb_usersFile location of avatar images
mybb_usergroupsFile location of user group star images
mybb_smilesFile location of custom smile images
mybb_attachmentsFile location of attachments (the .attach file) and the file location of the corresponding thumbnail file.

I looked at the structure of each table and found different file paths. Due to how they were inserted and what placeholders existed some parsing of each string would be necessary. Based on row counts were about 9000 attachments and 1000 avatars as well as 5 user group star images and 25 custom similes I needed to download.

I checked if I could access attachment files directly to determine next steps. I found out that even though I was locked out of the server, I could download any file directly if I knew the file location and name and my database backup happens to have this information. Because I can download attachments directly if I know their file paths I do not need to find a Local File Inclusion vulnerability in MyBB to download them. Normally users download a file through attachment.php?aid=123 so they never know the actual path. The MyBB developers didn’t decide to restrict direct access to .attach files which saves me a lot of time. At that point I opened up my text editor and started writing a program to automate the process.

Design decisions

To get a new backup as soon as possible I choose to use Python it would be fast to write. I used the requests library to have an easy way to pull files and the mysql.connector library to have an easy way to directly query the database. Some of the database code is tricky to read as it returns an array of tuples so you’ll have to keep track of the order you requested data in. That said it was fast to write and served it’s purpose without having to do much research. I was able to copy, paste, and edit example MySQL SELECT Statements from a Python MySQL Tutorial without reading it too much. This is a case where speed mattered more than elegance. I needed a backup as soon as humanly possible. After a quick Google search I found an answer on StackOverflow which explained how to create a file and directory if the location and file does not exist. I didn’t see the Python 3.2 solution or I would’ve used it instead as it’s shorter and easier to read. I choose to store each file inside a folder called backup/ and to mirror the layout of the root directory for simplicity. At the end everything was successfully backed up.

The final program

After about four hours of programming my Python program was complete, including a test run, and I was able to pull all of the data I need. It is not perfectly elegant yet but it is reasonably error safe and has user configurable counters in the even that the program freezes during execution. It was enough for my purposes and was written on a time crunch. In it’s current state, it does not support MyBB installations in a subdirectory without a few manual edits.

import mysql.connector
import requests
import os
import errno

def backup_attachments_and_thumbnails():
    db.execute("SELECT aid, attachname, thumbnail FROM " + db_prefix + "attachments")

    myresult = db.fetchall()

    for x in myresult:
        # Download Attachment
        if x[0] > last_attachment:
            filename = "./backup/uploads/" + x[1]
            if not os.path.exists(os.path.dirname(filename)):
                try:
                    os.makedirs(os.path.dirname(filename))
                except OSError as exc:  # Guard against race condition
                    if exc.errno != errno.EEXIST:
                        raise
            url = "https://" + forum_name + "/uploads/" + x[1]
            r = requests.get(url)
            print("Writing file: " + str(x[0]) + " at: " + x[1])
            with open(filename, 'wb') as f:
                f.write(r.content)
                f.close()
        # Download Thumbnail
        if x[0] > last_attachment:
            filename = "./backup/uploads/" + x[2]
            if x[2] != "SMALL" and x[2] != "":
                if not os.path.exists(os.path.dirname(filename)):
                    try:
                        os.makedirs(os.path.dirname(filename))
                    except OSError as exc:  # Guard against race condition
                        if exc.errno != errno.EEXIST:
                            raise
                url = "https://" + forum_name + "/uploads/" + x[2]
                r = requests.get(url)
                print("Writing thumbnail: " + str(x[0]) + " at: " + x[2])
                with open(filename, 'wb') as f:
                    f.write(r.content)
                    f.close()


def backup_avatars():
    db.execute("SELECT uid, avatar FROM " + db_prefix + "users")

    myresult = db.fetchall()

    for x in myresult:
        # Download Avatars
        if x[0] > last_avatar and x[1].startswith("./"):
            # The avatar is stored like "./uploads/avatars/avatar_803.jpg?dateline=1603426821" in SQL
            filename = "./backup/" + x[1][10:].split("?")[0]  # Remove the ?dateline= from filenames
            if not os.path.exists(os.path.dirname(filename)):
                try:
                    os.makedirs(os.path.dirname(filename))
                except OSError as exc:  # Guard against race condition
                    if exc.errno != errno.EEXIST:
                        raise
            url = "https://" + forum_name + x[1][1:]
            r = requests.get(url)
            print("Writing avatar: " + str(x[0]) + " at: " + filename)
            with open(filename, 'wb') as f:
                f.write(r.content)
                f.close()

def backup_smilies():
    db.execute("SELECT sid, image FROM " + db_prefix + "smilies")

    myresult = db.fetchall()

    for x in myresult:
        # Download Smilies
        if x[0] > last_smile:
            filename = "./backup/" + x[1]
            if not os.path.exists(os.path.dirname(filename)):
                try:
                    os.makedirs(os.path.dirname(filename))
                except OSError as exc:  # Guard against race condition
                    if exc.errno != errno.EEXIST:
                        raise
            url = "https://" + forum_name + "/" + x[1]
            r = requests.get(url)
            print("Writing smile: " + str(x[0]) + " at: " + filename)
            with open(filename, 'wb') as f:
                f.write(r.content)
                f.close()

def backup_usergroup_images():
    db.execute("SELECT gid, starimage FROM " + db_prefix + "usergroups")

    myresult = db.fetchall()

    for x in myresult:
        # Download User Group Images
        if x[0] > last_usergroup_image and x[1] != "":
            filename = "./backup/" + x[1]
            if not os.path.exists(os.path.dirname(filename)):
                try:
                    os.makedirs(os.path.dirname(filename))
                except OSError as exc:  # Guard against race condition
                    if exc.errno != errno.EEXIST:
                        raise
            url = "https://" + forum_name + "/" + x[1]
            r = requests.get(url)
            print("Writing usergroup image: " + str(x[0]) + " at: " + filename)
            with open(filename, 'wb') as f:
                f.write(r.content)
                f.close()

if __name__ == '__main__':
    # Setup the database connection
    mybb = mysql.connector.connect(
        host="localhost",
        user="root",
        database="mybb"
    )
    db = mybb.cursor()

    # Domain name and the database prefix
    db_prefix = "mybb_"
    forum_name = "example.com"

    # If for some reason downloading fails you can edit these with the last downloaded ID and restart the program without having to start over
    last_attachment = 0
    last_avatar = 0
    last_usergroup_image = 0
    last_smile = 0

    # Run backup procedures
    backup_attachments_and_thumbnails()
    backup_avatars()
    backup_smilies()
    backup_usergroup_images()

Conclusions

Never give up. There’s usually a way to solve a problem. Even the best of systems have their flaws and if you discover them you can use it to your advantage to solve the most difficult challenges.

One thought on “Exfiltrating MyBB Attachments

  1. This made me check on my SSH key backups. Odd that the hosting provider couldn’t help. Sounds like you were dealing with a bunch of asshats. I’d ditch them.

Comments are closed.