Unauthentifizierte SQL-Injection über GPrintDownload (GX4, weiterhin offen)
Schwachstellenmeldung · nachweislich ausgenutzt
Ein öffentlich erreichbarer AJAX-Endpunkt in Gambio GX4 lässt sich ohne Anmeldung für beliebige Datenbankabfragen missbrauchen. Ursache ist eine Escaping-Funktion, die sich mit einem doppelten Backslash umgehen lässt. In GX5 ist die Stelle bereits behoben — im GX4-Zweig steht der Fix bis heute aus.
Das Problem in zwei Sätzen
request_port.php?module=GPrintDownload&key=… reicht den
key-Parameter durch gm_prepare_string() und verkettet
ihn anschließend direkt in eine SQL-Abfrage. Die Funktion überspringt ihr eigenes
Escaping, sobald der Wert bereits maskiert aussieht — was ein Angreifer
trivial vortäuschen kann.
gm_magic_check() soll erkennen, ob ein String schon escaped ist.
Sie prüft dafür, ob vor einem Anführungszeichen ein Backslash steht. Bei der
Eingabe 1\\' steht dort einer — also gilt der String als „schon
escaped“ und mysqli_real_escape_string() wird nicht
aufgerufen. MySQL wertet \\ danach als literalen Backslash, das
Anführungszeichen schließt den String, und die Injection steht.
Der verwundbare Code
// system/classes/customizer/GPrintDownloadAjaxHandler.inc.php
function get_permission_status($p_customers_id = NULL)
{
return true; // ← keinerlei Zugriffsprüfung
}
function proceed()
{
$c_download_key = gm_prepare_string($this->v_data_array['GET']['key']);
$t_get_file_data = xtc_db_query("SELECT filename, encrypted_filename
FROM " . TABLE_GM_GPRINT_UPLOADS . "
WHERE download_key = '" . $c_download_key . "'");
Die Ursache eine Ebene tiefer
// gm/inc/gm_prepare_string.inc.php
function gm_magic_check($string){
if(preg_match('/(^"|[^\\\\]{1}")/', $string) == 1) return false;
if(preg_match('/(^\'|[^\\\\]{1}\')/', $string) == 1) return false;
else return true; // ← „sieht escaped aus“
}
function gm_prepare_string($string, $strip = false){
if(!$strip){
if(ini_get('magic_quotes_gpc') == 0 /* … */){
if(!gm_magic_check($string)) // ← false = escapen, true = überspringen
$string = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $string);
}
}
return $string;
}
Nachgerechnet mit dem Originalcode:
| Eingabe | gm_magic_check | Folge |
|---|---|---|
| 1' | false | wird escaped — korrekt |
| 1\' | true | Escaping übersprungen |
| 1\\' | true | Escaping übersprungen — so lief der Angriff |
Der Endpunkt ist ohne Anmeldung erreichbar: get_permission_status()
gibt unbedingt true zurück.
Tragweite
Zwei Dinge machen den Fund größer als einen einzelnen Endpunkt.
-
Kein Login nötig. Der Angriff läuft anonym über das Shop-Frontend.
In der Praxis wurden damit
customers_id,customers_email_addressund vor allempassword_request_keyausgelesen. -
Die Escaping-Funktion ist nicht auf diesen Handler beschränkt.
Im untersuchten Shop wird
gm_prepare_string()an 79 Stellen in 26 Dateien aufgerufen. Überall dort, wo das Ergebnis ebenfalls in einen SQL-String verkettet wird, greift derselbe Bypass. Diese Stellen sollten mitgeprüft werden.
password_request_key das eigentliche Ziel war
Der Angreifer forderte im Shop einen Passwort-Reset an, las den frisch erzeugten Token per SQL-Injection direkt aus der Datenbank und konnte damit das Passwort setzen — ohne je Zugriff auf das Postfach zu haben. Dieser Ablauf ist in den Zugriffsprotokollen elfmal nachweisbar. Auf dem betroffenen Shop lagen 738 gültige Reset-Tokens in der Tabelle, weil Gambio sie nach Gebrauch nicht verwirft.
Nachweis der Ausnutzung
Rekonstruiert aus den Apache-Zugriffsprotokollen. Angreifer-IP
176.105.204.211, 211 Anfragen, automatisiert
(SQLMap-Signatur 0x71786b7671).
-
Vorab-Sondierung Test auf vorhandene Webshells:
/admin/403WebShell.php,/media/content/gmo.php— beide 404. -
Normaler Shop-Besuch Startseite, Session wird aufgebaut.
-
SQL-Injection beginnt
ORDER BY 1…10zur Spaltenermittlung, dannUNION ALL SELECT, Abfrage der DB-Version. -
Reset-Tokens ausgelesen
SELECT customers_id, customers_email_address, password_request_key FROM customers WHERE customers_status = 0 -
Passwort-Reset angefordert
POST /password_double_opt.php?action=first_opt_in— erzeugt einen frischen Token in der Datenbank. -
Neuen Token abgegriffen Zweite SQLi-Runde, gezielt auf
password_request_key. Der Zyklus wiederholt sich bis 15:30. -
Webshell abgelegt
media/content/gmo.php, 78.729 Bytes. -
Webshell aktiv HTTP 200. Danach Dateibrowser (
?loknya=…) und zwei POST-Aktionen. -
Letzter Zugriff Danach kein weiterer Aufruf der Shell.
-
Entdeckt und neutralisiert Shell umbenannt und auf
chmod 000gesetzt.
Beteiligte Adressen
| IP | Herkunft | Netz | SQLi-Anfragen | Rolle |
|---|---|---|---|---|
| 176.105.204.211 | Ukraine, Poltava | RPC HomeNet Ltd. — Privatanschluss | 97 | Einbruch + Webshell |
| 169.150.0.0 | anonymisiert | im Log gekürzt | 354 | nur Sondierung |
| 188.137.245.134 | Deutschland | Podaon SIA — germany-1.vps.ac | 8 | nur Sondierung |
User-Agent durchgehend Chrome/151.0.0.0 auf Windows. Keine dieser
Adressen taucht in Bestellungen oder Kundendaten des Shops auf.
Patch-Stand bei Gambio
Der Fix existiert bereits — er ist nur nie in den GX4-Zweig zurückgeflossen. Das ist der eigentliche Grund, warum diese Meldung noch relevant ist.
| Fassung | gm_prepare_string | Handler | Status |
|---|---|---|---|
| GX5 v5.0.2.0 | escaped bedingungslos,gm_magic_check nicht mehr aufgerufen |
nutzt xtc_db_input() |
behoben |
| GX4 + Security Update 2026-08 | unverändert — Bypass wirkt | nutzt gm_prepare_string() |
offen |
Security Update 2026-08 v1.0 (freigegeben für v4.7.2.0 bis v4.9.5.0) enthält
genau vier Dateien. Alle vier ersetzen unsicheres Escaping durch
xtc_db_input() bzw. (int)-Casts:
inc/xtc_display_banner.inc.php → xtc_db_input($identifier), (int)$identifier
inc/xtc_format_price.inc.php → xtc_db_input($_SESSION['currency'])
inc/xtc_update_whos_online.inc.php → xtc_db_input(...)
gm/classes/GMTracker.php → xtc_db_input($_SERVER['REMOTE_ADDR'])
GPrintDownloadAjaxHandler.inc.php ist nicht dabei,
obwohl es dasselbe Muster aufweist, ohne Anmeldung erreichbar ist und in GX5
bereits umgestellt wurde. Ein Shop mit aktuellem Security Update ist an dieser
Stelle weiterhin angreifbar.
Der betroffene Shop lief auf einer Version aus dieser Liste. Das erklärt, warum ein gepflegtes System kompromittiert werden konnte.
Sofortmaßnahme für Betreiber
Wirkt ohne Core-Änderung und übersteht ein Update. Sinnvoll überall dort, wo
GPrint nicht genutzt wird — im untersuchten Shop existierte die Tabelle
gm_gprint_downloads nicht einmal, und in 30 Tagen gab es keinen
einzigen legitimen Aufruf.
# .htaccess im Shop-Root, ganz oben
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)module=GPrintDownload [NC]
RewriteRule ^request_port\.php$ - [F,L]
</IfModule>
Zusätzlich sollten alle bestehenden Reset-Tokens verworfen werden — welche davon abgeflossen sind, lässt sich nachträglich nicht feststellen:
UPDATE customers SET password_request_key = NULL
WHERE password_request_key IS NOT NULL;
Vorschlag für den Patch
Der eigentliche Fix gehört in den Handler, nicht in die Escaping-Funktion: Der Download-Schlüssel ist ein Bezeichner, kein Freitext.
function proceed()
{
$t_key = (string)($this->v_data_array['GET']['key'] ?? '');
// Whitelist statt Escaping — der Schluessel ist ein Hash-Bezeichner
if ($t_key === '' || !preg_match('/^[A-Za-z0-9_-]{1,64}$/', $t_key)) {
$this->v_output_buffer = 'Error: File does not exist!';
return true;
}
// Gebundener Parameter statt Verkettung
$t_get_file_data = StaticGXCoreLoader::getDatabaseQueryBuilder()
->query('SELECT filename, encrypted_filename FROM '
. TABLE_GM_GPRINT_UPLOADS . ' WHERE download_key = ?', [$t_key]);
// …
Unabhängig davon empfiehlt sich, gm_magic_check() zu entfernen.
Die Funktion stammt aus der Zeit von magic_quotes_gpc, das seit
PHP 5.4 nicht mehr existiert. Ihre Heuristik kann Angreifereingaben von echtem
Escaping nicht unterscheiden — sie schwächt jede Stelle, die sie schützt.
Analyse auf Basis der Apache-Zugriffsprotokolle, der Shop-Datenbank und des Dateisystems eines produktiven GX4-Shops. Zeitangaben in MEZ/MESZ (UTC+2). Die Webshell selbst wurde nicht ausgeführt und liegt gesichert vor.