• Vulnerability: Second Order SQL Injection, Reflected XSS, Path Traversal, Function Execution
  • Affected Software: ZenPhoto
  • Affected Version: 1.4.8 (probably also prior versions)
  • Patched Version: 1.4.9
  • Risk: Medium
  • Vendor Contacted: 2015-05-18
  • Vendor Fix: 2015-07-09
  • Public Disclosure: 2015-07-10

ZenPhoto is an open-source CMS written in PHP with a focus on hosting images. There are multiple vulnerabilities in version 1.4.8, including SQL injection and XSS vulnerabilities.

Second Order Error Based SQL Injection

There are multiple second order error based SQL injections into the ORDER BY keyword in the admin area.


Example Exploitation: Getting Users

id,extractvalue(1,concat(0x7e,(SELECT substring(concat(user,0x3a,pass),1,30) FROM zenphotoadministrators limit 6,1)))%23
id,extractvalue(1,concat(0x7e,(SELECT substring(concat(user,0x3a,pass),29,60) FROM zenphotoadministrators limit 6,1)))%23


Use prepared statements in all places, even for data that comes from the database.

Additionally, it might be a good idea to check if the custom field is an actual column of the database before setting it. This is not ideal as the only protection, but it would definitely increase the user experience (otherwise, if they enter invalid data they will get an error and not know why).


With reflected XSS it is possible to execute JavaScript in the context of the victims browser. This allows to bypass any CSRF protection, and thus to execute any action the victim can execute if they click on a link once. This means that an attacker could gain code execution via the theme editor or exploit the SQL injection without having admin access. Additionally, it is possible to steal cookies, display the login page and log the entered data, or inject a javascript key logger. All of this can happen without the victim being aware of it and – apart from clicking a link once – without their interaction.




There are a couple of different sanitation functions. The one most often used is sanitize_string, which does not adequately protect against any attacks. In some places html_encode – just a wrapper around htmlspecialchars – is used additionally resulting in secure code.

bypassing sanitize_string($string):

  1. html entities encode: <script>alert(1)</script>
  2. url encode: %26lt%3Bscript%26gt%3Balert(1)%26lt%3B%2Fscript%26gt%3B


The sanitize_string function basically does this:

  $content = preg_replace('~<script.*?/script>~is', '', $content);
  $content = preg_replace('~<style.*?/style>~is', '', $content);
  $content = preg_replace('~<!--.*?-->~is', '', $content);
  $content = strip_tags($content);
  $content = str_replace('&nbsp;', ' ', $content);
  $content = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
  return $content;

The first three lines can be easily bypassed (eg <<script></script>script>alert(1)<script></script></script> or by using an image tag: <img src="no" onerror="alert(1)">, the fourth line actually does protect against some XSS attacks (but is not really recommended), but the last line makes these attacks possible again (and also makes the bypassing of the first three lines unnecessary).

sanitize_string is eg used in admin-upload.php:141:

            if (isset($_GET['error'])) {
                $errormsg = sanitize($_GET['error']);
                <div class="errorbox fade-message">
                    <h2><?php echo gettext("Upload Error"); ?></h2>
                    <?php echo (empty($errormsg) ? gettext("There was an error submitting the form. Please try again.") : $errormsg); ?>

or in backup_restore.php:477:

    $compression_handler = sanitize($_GET['compression']);
    $messages = '
    <div class="messagebox fade-message">
    if ($compression_handler == 'no') {
        $messages .= (gettext('Restore completed'));
    } else {
        $messages .= sprintf(gettext('Restore completed using %s compression'), $compression_handler);


Use html_encode instead of sanitize_string. If there is not a good reason to have sanitize_string, think about removing it in the future.

Directory Traversal

For an admin, it is possible to view and edit any PHP or inc files, not just the ones inside the theme directory.




Use realpath and check if the result is inside the allowed theme directory. Do not rely on sanitize_path, because it allows to travers at least one directory upwards, and filtering is not the recommended way to prevent directory traversal.

In practice, it doesn’t matter, because an attacker can just edit a legitimate PHP theme file and inject <?php passthru($_GET['c']) ?>, which would then allow them to edit any file anyways. It is still a good idea to fix this, as users might disable the file edit functionality themselves to increase security.

It might be a good idea to add an easy way to forbid file edits to prevent code execution for attackers that managed to gain access to the admin area (or attackers that were able to exploit an XSS vulnerability).



http://localhost/zenphoto-security-fixes/zp-core/admin.php?action=external&error=" onmouseover="alert('xsstest')" foo="bar&msg=hover over me!

Execute Function




An admin user can execute any function they want via this URL (there is also no CSRF protection for it):



I’m reporting this because as defense in depth, it’s a good idea to not allow execution of arbitrary functions. I have not found a way to actually exploit it currently, but if there is eg code like this somewhere:

class Foo {
    function bar() {
        $var = $_GET['var'];
        // do interesting things with var (eg update user var to admin)

if ($validCSRF) {

the CSRF protection can be bypassed via localhost/zenphoto-security-fixes/zp-core/admin.php?action[]=Foo&action[]=bar&var=somevar.

So with the presence of call_user_func, some control over the control flow of the site is given up.


  • 2015-05-18: Initial Report
  • 2015-05-18: Vendor Confirmation
  • 2015-06-05: Asking for Progress Update
  • 2015-06-05: Vendor Progress Update, Asking for Confirmation of Fixes
  • 2015-06-07: Confirmation of Fixes, Report of Addition Issues
  • 2015-06-07: Vendor Confirmation
  • 2015-06-14: Suggested Disclosure Date (2015-06-28)
  • 2015-06-15: Vendor Confirmation
  • 2015-06-25: Vendor asking for extention
  • 2015-06-27: Suggested new Disclosure Date (2015-07-07)
  • 2015-06-27: Vendor Announced Release
  • 2015-07-09: Vendor Released Fix
  • 2015-07-10: Disclosed