Exploiting CVE-2025-40629: Path Traversal in PNETLab (v4.2.10)
TL; DR
While testing a PNETLab instance (v4.2.10), I stumbled upon CVE-2025-40629 - a path traversal vulnerability. No public PoC of the vulnerability was available, and there was no indication whether the exploit requires authentication. After diving into the source code in Github (probably unofficial), I found that it's an authenticated path traversal in the /api/export endpoint that allows arbitrary file read. To exploit it, you need to supply at least one valid .unl lab file first, then you can traverse anywhere on the filesystem.
Intro
So there I was, poking around a PNETLab installation during a security assessment. For those unfamiliar, PNETLab is a fork of EVE-NG - a popular network emulation platform used for building virtual labs with Cisco, Juniper, and other networking gear. Pretty useful for CCNA/CCNP prep and network testing.
A quick CVE search for the target version turned up CVE-2025-40629 - described simply as a "path traversal vulnerability" that can lead to potentially accessing sensitive files.
I also noticed that, as of the moment, the vulnerable version is the latest available on PNETLab website, meaning there's probably no patch for it as of the moment.

I took the source code and started digging. An hour later, I found the root cause and developed a working exploit.
Vulnerability Analysis and Exploitation
Finding the Vulnerable Endpoint
I started mapping out the API endpoints and noticed /api/export - used for exporting lab files as ZIP archives. The API endpoint definition is straightforward: File: api.php (lines 1640-1676)
$app->post('/api/export', function () use ($app) {
$indent = new \indentify();
list($user, $tenant, $output) = $indent->authorization($app->getCookie('token'));
if ($user === False) {
$app->response->setStatus($output['code']);
$app->response->setBody(json_encode($output));
return;
}
try {
checkPermission(USER_PER_EXPORT_LAB);
$event = json_decode($app->request()->getBody());
$p = json_decode(json_encode($event), True);;
$output = apiExportLabs($p);
$app->response->setStatus($output['code']);
$app->response->setBody(json_encode($output));
} catch (ResponseException $e) {
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = $e->getMessage();
$output['error_code'] = $e->getCode();
$output['data'] = $e->getData();
$app->response->setStatus($output['code']);
$app->response->setBody(json_encode($output));
return;
}catch (Exception $e) {
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = get($GLOBALS['messages'][$e->getMessage()], $e->getMessage());
$output['error_code'] = $e->getCode();
$app->response->setStatus($output['code']);
$app->response->setBody(json_encode($output));
return;
}
});As shown, it's authenticated. The juicy stuff happens in apiExportLabs().
Dissecting the Vulnerable Function
Here's where things get interesting. The apiExportLabs() function is supposed to validate paths and create a ZIP file of lab exports: File: includes/api_labs.php (lines 186-267):
function apiExportLabs($p)
{
$export_url = '/Exports/pnetlab_export-' . date('Ymd-His') . '.zip';
$export_file = '/opt/unetlab/data' . $export_url;
if (is_file($export_file)) {
unlink($export_file);
}
if (checkFolder(BASE_LAB . $p['path']) !== 0) {
// Path is not valid
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = $GLOBALS['messages'][80077];
return $output;
}
if (!chdir(BASE_LAB . $p['path'])) {
// Cannot set CWD
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = $GLOBALS[80072];
return $output;
}
foreach ($p as $key => $element) {
if ($key === 'path') {
continue;
}
// Using "element" relative to "path", adding '/' if missing
$relement = substr($element, strlen($p['path']));
if ($relement[0] != '/') {
$relement = '/' . $relement;
}
if (is_file(BASE_LAB . $p['path'] . $relement)) {
// Adding a file
$cmd = 'zip ' . $export_file . ' ".' . $relement . '"';
secureCmd($cmd);
exec($cmd, $o, $rc);
if ($rc != 0) {
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = $GLOBALS['messages'][80073];
return $output;
}
}
if (checkFolder(BASE_LAB . $p['path'] . $relement) === 0) {
// Adding a dir
$cmd = 'zip -r ' . $export_file . ' ".' . $relement . '"';
secureCmd($cmd);
exec($cmd, $o, $rc);
if ($rc != 0) {
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = $GLOBALS['messages'][80074];
return $output;
}
}
}
// Now remove UUID from labs
$cmd = BASE_DIR . '/scripts/remove_uuid.sh "' . $export_file . '"';
secureCmd($cmd);
exec($cmd, $o, $rc);
if ($rc != 0) {
if (is_file($export_file)) {
unlink($export_file);
}
$output['code'] = 400;
$output['status'] = 'fail';
$output['message'] = $GLOBALS['messages'][$rc];
return $output;
}
$output['code'] = 200;
$output['status'] = 'success';
$output['message'] = $GLOBALS['messages'][80075];
$output['data'] = $export_url;
return $output;
}The issue: The function validates the base path parameter (line 194) but completely ignores validation of individual file elements in the loop (lines 210-246). Each element can contain path traversal sequences, and they're used directly to construct file paths.
The Exploit
Here's the important part. If you send just traversal payloads like:
{
"path": "/",
"0": "/../../etc/passwd"
}The secureCmd() function catches the .. and blocks it. The file gets added to the ZIP, but then the remove_uuid.sh script fails because /etc/passwd isn't a valid .unl lab file, and the entire ZIP gets deleted (line 254).
The Solution: Include a valid .unl lab file first! The script at line 249 processes the ZIP to remove UUIDs from lab files. If there's at least one valid .unl file in the ZIP, the script succeeds, and our traversed files come along for the ride.
Here's a working PoC request:

/etc/passwd from the target serverFully automated exploit code can be found in this Github repository.
Conclusion
CVE-2025-40629 is an authenticated path traversal vulnerability in PNETLab's lab export functionality. The root cause is incomplete input validation - while the base path gets validated, individual file parameters in the export request are not checked at all. Exploitation is straightforward - include a valid .unl lab file first, then add your traversal targets. This enables an authenticated attacker to read arbitrary files from the server filesystem. Once the export completes, the returned ZIP archive should contain both legitimate lab files and the traversed targets, making this a straightforward information disclosure vulnerability.