CVE-2023-41580: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: Phpipam before v1.5.2 was discovered to contain a LDAP injection vulnerability via the dname parameter at /users/ad-search-result.php. This vulnerability allows attackers to enumerate arbitrary fields in the LDAP server and access sensitive data via a crafted POST request.
CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 7.5
Published: 2023-10-02T13:15Z
Repository: https://github.com/phpipam/phpipam
Language: PHP
Files changed: app/admin/groups/ad-search-group-result.php, app/admin/users/ad-search-result.php, misc/CHANGELOG
Commit date: 2023-02-05 21:45:04 +0000
Commit message: Bugfix: XSS and LDAP injection in ad-search-group-result.php

Reported by Elias Hohl

Diff: diff --git a/app/admin/groups/ad-search-group-result.php b/app/admin/groups/ad-search-group-result.php
index 1a773bfeb..c73788767 100644
--- a/app/admin/groups/ad-search-group-result.php
+++ b/app/admin/groups/ad-search-group-result.php
@@ -4,11 +4,11 @@
  * Script to display usermod result
  *************************************/
 
 /* functions */
 require_once( dirname(__FILE__) . '/../../../functions/functions.php' );
-require( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
+require_once( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
 
 # initialize user object
 $Database 	= new Database_PDO;
 $User 		= new User ($Database);
 $Admin	 	= new Admin ($Database);
@@ -57,17 +57,18 @@ try {
 	$adldap = new adLDAP($options);
 
 	// Use credentials if they've been provided
 	if (isset($params->adminUsername) && isset($params->adminPassword)) {
 		$authUser = $adldap->authenticate($params->adminUsername, $params->adminPassword);
-		if ($authUser == false) {
+		if (!$authUser) {
 			$Result->show("danger", _("Invalid credentials"), true);
 		}
 	}
 
 	//search groups
-	$groups = $adldap->group()->search(adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP,true,"*$_POST[dfilter]*");
+	$esc_dfilter = ldap_escape($_POST["dfilter"], null, LDAP_ESCAPE_FILTER);
+	$groups = $adldap->group()->search(adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP, true, "*$esc_dfilter*");
 
 	//echo $adldap->getLastError();
 }
 catch (adLDAPException $e) {
 	$Result->show("danger", $adldap->getLastError(), false);
@@ -79,51 +80,50 @@ catch (adLDAPException $e) {
 if(sizeof($groups)==0) {
 	print "<div class='alert alert-info'>";
 	print _('No groups found')."!<hr>";
 	print _('Possible reasons').":";
 	print "<ul>";
-	print "<li>"._('Invalid baseDN setting for ' . $server->type)."</li>";
-	print "<li>"._($server->type . ' account does not have enough privileges for search')."</li>";
+	print "<li>"._('Invalid baseDN setting for ') . escape_input($server->type)."</li>";
+	print "<li>".escape_input($server->type). ' '. _('account does not have enough privileges for search')."</li>";
 	print "</div>";
 } else {
 	print _(" Following groups were found").": (".sizeof($groups)."):<hr>";
 
 	print "<table class='table table-top table-td-top  table-striped'>";
 
 	// loop
  	foreach($groups as $k=>$g) {
+		$esc_k = escape_input($k);
+		$esc_g = escape_input($g);
+
+		// search members
+		$groupMembers = $adldap->group()->members($k) ?: [];
+		$esc_members = !empty($groupMembers) ? escape_input(implode(";", $groupMembers)) : '';
+
 		print "<tr>";
-		print "	<td>$k</td>";
-		print "	<td>$g</td>";
+		print "	<td>$esc_k</td>";
+		print "	<td>$esc_g</td>";
 		//actions
 		print " <td style='width:10px;'>";
-		print "		<a href='' class='btn btn-sm btn-default btn-success groupselect' data-gname='$k' data-gdescription='$g' data-members='$members' data-gid='$k' data-csrf_cookie='$csrf'>"._('Add group')."</a>";
+		print "		<a href='' class='btn btn-sm btn-default btn-success groupselect' data-gname='$esc_k' data-gdescription='$esc_g' data-members='$esc_members' data-gid='$esc_k' data-csrf_cookie='$csrf'>"._('Add group')."</a>";
 		print "	</td>";
 		print "</tr>";
 
 		print "<tr>";
 		print "	<td>"._("Members:")."</td>";
 		print "<td colspan='2'>";
-		print "	<div class='adgroup-$k'></div>";
-		// search members
-		$groupMembers = $adldap->group()->members($k);
-		unset($members);
-		if($groupMembers!==false) {
+		print "	<div class='adgroup-$esc_k'></div>";
+
+		if (!empty($groupMembers)) {
 			foreach($groupMembers as $m) {
-				print "<span class='muted'>$m</span><br>";
-				$members[] = $m;
+				print "<span class='muted'>".escape_input($m)."</span><br>";
 			}
-			if(isset($members))
-			$members = implode(";", $members);
 		}
 		else {
-			$members = "";
 			print "<span class='muted'>"._("No members")."</span>";
 		}
 		print "	</td>";
 		print "	</tr>";
 
 	}
 	print "</table>";
 }
-
-?>
\ No newline at end of file
diff --git a/app/admin/users/ad-search-result.php b/app/admin/users/ad-search-result.php
index f2000debc..bab0112ef 100755
--- a/app/admin/users/ad-search-result.php
+++ b/app/admin/users/ad-search-result.php
@@ -4,11 +4,11 @@
  * Script to display usermod result
  *************************************/
 
 /* functions */
 require_once( dirname(__FILE__) . '/../../../functions/functions.php' );
-require( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
+require_once( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
 
 # initialize user object
 $Database 	= new Database_PDO;
 $User 		= new User ($Database);
 $Admin	 	= new Admin ($Database);
@@ -50,17 +50,17 @@ try {
 	// set OpenLDAP flag
 	if($server->type == "LDAP") { $adldap->setUseOpenLDAP(true); }
 
 	//try to login with higher credentials for search
 	$authUser = $adldap->authenticate($params->adminUsername, $params->adminPassword);
-	if ($authUser == false) {
+	if (!$authUser) {
 		$Result->show("danger", _("Invalid credentials")."<br>".$adldap->getLastError(), true);
 	}
 
 	//search for domain user!
-	$escaped_dn = ldap_escape($_POST["dname"], null, LDAP_ESCAPE_DN);
-	$userinfo = $adldap->user()->info("$escaped_dn*", array("*"), false, $server->type);
+	$esc_dname = ldap_escape($_POST["dname"], null, LDAP_ESCAPE_FILTER);
+	$userinfo = $adldap->user()->info("*$esc_dname*", array("*"), false, $server->type);
 
 	//echo $adldap->getLastError();
 }
 catch (adLDAPException $e) {
 	$Result->show("danger", $e->getMessage(), true);
@@ -85,12 +85,11 @@ if(!isset($userinfo['count'])) {
 	unset($userinfo['count']);
 	if(sizeof(@$userinfo)>0 && isset($userinfo)) {
 		// loop
 		foreach($userinfo as $u) {
 			print "<tr>";
-			print "	<td>".escape_input($u['displayname'][0]);
-			print "</td>";
+			print "	<td>".escape_input($u['displayname'][0])."</td>";
 			print "	<td>".escape_input($u['samaccountname'][0])."</td>";
 			print "	<td>".escape_input($u['mail'][0])."</td>";
 			//actions
 			print " <td style='width:10px;'>";
 			print "		<a href='' class='btn btn-sm btn-default btn-success userselect' data-uname='".escape_input($u['displayname'][0])."' data-username='".escape_input($u['samaccountname'][0])."' data-email='".escape_input($u['mail'][0])."' data-server='".escape_input($_POST['server'])."' data-server-type='".$server->type."'>"._('Select')."</a>";
diff --git a/misc/CHANGELOG b/misc/CHANGELOG
index d479dc6a5..a840a6eb1 100755
--- a/misc/CHANGELOG
+++ b/misc/CHANGELOG
@@ -7,10 +7,11 @@
     Security Fixes:
     ----------------------------
     + XSS (reflected) in 'bw-calulator-result.php';
     + XSS (reflected) by invalid email address response;
     + XSS and LDAP injection in ad-search-result.php;
+    + XSS and LDAP injection in ad-search-group-result.php;
     + Restrict find_full_subnets.php to CLI;
     + Ensure confidentiality of database password;
 
 == 1.5.0
 

Vulnerable code: require( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
		if ($authUser == false) {
	$groups = $adldap->group()->search(adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP,true,"*$_POST[dfilter]*");
	print "<li>"._('Invalid baseDN setting for ' . $server->type)."</li>";
	print "<li>"._($server->type . ' account does not have enough privileges for search')."</li>";
		print "	<td>$k</td>";
		print "	<td>$g</td>";
		print "		<a href='' class='btn btn-sm btn-default btn-success groupselect' data-gname='$k' data-gdescription='$g' data-members='$members' data-gid='$k' data-csrf_cookie='$csrf'>"._('Add group')."</a>";
		print "	<div class='adgroup-$k'></div>";
		// search members
		$groupMembers = $adldap->group()->members($k);
		unset($members);
		if($groupMembers!==false) {
				print "<span class='muted'>$m</span><br>";
				$members[] = $m;
			if(isset($members))
			$members = implode(";", $members);
			$members = "";

?>
require( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
	if ($authUser == false) {
	$escaped_dn = ldap_escape($_POST["dname"], null, LDAP_ESCAPE_DN);
	$userinfo = $adldap->user()->info("$escaped_dn*", array("*"), false, $server->type);
			print "	<td>".escape_input($u['displayname'][0]);
			print "</td>";
Fixed code: require_once( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
		if (!$authUser) {
	$esc_dfilter = ldap_escape($_POST["dfilter"], null, LDAP_ESCAPE_FILTER);
	$groups = $adldap->group()->search(adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP, true, "*$esc_dfilter*");
	print "<li>"._('Invalid baseDN setting for ') . escape_input($server->type)."</li>";
	print "<li>".escape_input($server->type). ' '. _('account does not have enough privileges for search')."</li>";
		$esc_k = escape_input($k);
		$esc_g = escape_input($g);

		// search members
		$groupMembers = $adldap->group()->members($k) ?: [];
		$esc_members = !empty($groupMembers) ? escape_input(implode(";", $groupMembers)) : '';

		print "	<td>$esc_k</td>";
		print "	<td>$esc_g</td>";
		print "		<a href='' class='btn btn-sm btn-default btn-success groupselect' data-gname='$esc_k' data-gdescription='$esc_g' data-members='$esc_members' data-gid='$esc_k' data-csrf_cookie='$csrf'>"._('Add group')."</a>";
		print "	<div class='adgroup-$esc_k'></div>";

		if (!empty($groupMembers)) {
				print "<span class='muted'>".escape_input($m)."</span><br>";
require_once( dirname(__FILE__) . "/../../../functions/adLDAP/src/adLDAP.php");
	if (!$authUser) {
	$esc_dname = ldap_escape($_POST["dname"], null, LDAP_ESCAPE_FILTER);
	$userinfo = $adldap->user()->info("*$esc_dname*", array("*"), false, $server->type);
			print "	<td>".escape_input($u['displayname'][0])."</td>";
    + XSS and LDAP injection in ad-search-group-result.php;