CVE-2023-37378: Insufficient Information (NVD-CWE-noinfo)
value: Nullsoft Scriptable Install System (NSIS) before 3.09 mishandles access control for an uninstaller directory.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.3
Published: 2023-07-03T20:15Z
Repository: https://github.com/kichik/nsis
Language: C
Files changed: Docs/src/history.but, Source/build.cpp, Source/exehead/Main.c, Source/exehead/util.c
Commit date: 2023-05-21 16:15:16 +0000
Commit message: Use isolated uninstaller temp directory


git-svn-id: https://svn.code.sf.net/p/nsis/code/NSIS/trunk@7388 212acab6-be3b-0410-9dea-997c60f758d6

Diff: diff --git a/Docs/src/history.but b/Docs/src/history.but
index 310dbfd5..2f7bfd1b 100644
--- a/Docs/src/history.but
+++ b/Docs/src/history.but
@@ -8,10 +8,14 @@ Released on ????, 20??
 
 ????
 
 \S1{v3.09-cl} Changelog
 
+\S2{} Major Changes
+
+\b Use isolated uninstaller directory for every instance
+
 \S2{} Minor Changes
 
 \b Added \R{assert}{!assert}
 
 \b Fixed \cw{!uninstfinalize} bug on large uninstallers (\W{http://sf.net/p/nsis/bugs/1279}{bug #1279})
diff --git a/Source/build.cpp b/Source/build.cpp
index 90e6dd0b..2f64a4ca 100644
--- a/Source/build.cpp
+++ b/Source/build.cpp
@@ -2324,12 +2324,12 @@ void CEXEBuild::PrepareInstTypes()
 void CEXEBuild::AddStandardStrings()
 {
 #ifdef NSIS_CONFIG_UNINSTALL_SUPPORT
   if (uninstall_mode)
   {
-    cur_header->str_uninstchild = add_asciistring(_T("$TEMP\\Un_$1.exe"));
-    cur_header->str_uninstcmd = add_asciistring(_T("\"$TEMP\\Un_$1.exe\" $0 _?=$INSTDIR\\"));
+    cur_header->str_uninstchild = add_asciistring(_T("$TEMP\\Un.exe"));
+    cur_header->str_uninstcmd = add_asciistring(_T("\"$TEMP\\Un.exe\" $0 _?=$INSTDIR\\"));
   }
 #endif//NSIS_CONFIG_UNINSTALL_SUPPORT
 #ifdef NSIS_SUPPORT_MOVEONREBOOT
   cur_header->str_wininit = is_target_64bit() ? 0 : add_asciistring(_T("$WINDIR\\wininit.ini"));
 #endif//NSIS_SUPPORT_MOVEONREBOOT
diff --git a/Source/exehead/Main.c b/Source/exehead/Main.c
index 3e0d417b..78ff558c 100644
--- a/Source/exehead/Main.c
+++ b/Source/exehead/Main.c
@@ -346,64 +346,73 @@ EXTERN_C void NSISWinMainNOCRT()
         goto end;
       }
     }
     else
     {
-      int x, admin = UserIsAdminGrpMember();
-
-      mystrcat(state_temp_dir,_T("~nsu"));
-      if (admin) mystrcat(state_temp_dir,_T("A")); // Don't lock down the directory used by non-admins
-      mystrcat(state_temp_dir,_T(".tmp"));
-
-      // check if already running from uninstaller temp dir
-      // this prevents recursive uninstaller calls
-      if (!lstrcmpi(state_temp_dir,state_exe_directory))
-        goto end;
-
-      admin ? CreateRestrictedDirectory(state_temp_dir) : CreateNormalDirectory(state_temp_dir);
-      SetCurrentDirectory(state_temp_dir);
-
-      if (!(*state_install_directory))
-        mystrcpy(state_install_directory,state_exe_directory);
+      int admin = UserIsAdminGrpMember();
+      WORD tries; // 0xfffe attempts or bust
+      size_t cchtmpslash = mystrlen(state_temp_dir);
+      LPTSTR unexe = g_usrvars[5], unexecmd = g_usrvars[6];
 
       mystrcpy(g_usrvars[0], realcmds);
-      SET2CHAR(g_usrvars[1], _T("A\0"));
-
-      for (x = 0; x < 26; x ++)
+      if (!(*state_install_directory))
       {
-        static TCHAR buf2[NSIS_MAX_STRLEN];
-
-        GetNSISString(buf2,g_header->str_uninstchild); // $TEMP\Un_$1.exe
+        mystrcpy(state_install_directory, state_exe_directory);
+      }
 
-        DeleteFile(buf2); // clean up after all the other ones if they are there
+      for (tries = 0; ++tries != 0;)
+      {
+        DWORD retry = 0, ec;
+retry_un_dir:
+        wsprintf(state_temp_dir + cchtmpslash, _T("~nsu%X.tmp"), tries);
+        GetNSISString(unexe, g_header->str_uninstchild); // '$TEMP\Un.exe'
+        if (admin)
+        {
+          ec = CreateRestrictedDirectory(state_temp_dir);
+        }
+        else
+        {
+          ec = CreateNormalDirectory(state_temp_dir);
+        }
 
-        if (m_Err) // not done yet
+        if (ec)
         {
-          // copy file
-          if (CopyFile(state_exe_path,buf2,TRUE))
+          // Delete previous uninstaller
+          if (DeleteFile(unexe))
+          {
+            myDelete(state_temp_dir, DEL_DIR|DEL_RECURSE);
+            if (!retry++) goto retry_un_dir;
+          }
+        }
+        else
+        {
+          HANDLE hProc;
+          SetCurrentDirectory(state_temp_dir);
+#ifdef NSIS_SUPPORT_MOVEONREBOOT
+          MoveFileOnReboot(state_temp_dir, NULL);
+#endif
+          if (CopyFile(state_exe_path, unexe, TRUE))
           {
-            HANDLE hProc;
 #ifdef NSIS_SUPPORT_MOVEONREBOOT
-            MoveFileOnReboot(buf2,NULL);
+            MoveFileOnReboot(unexe, NULL);
 #endif
-            GetNSISString(buf2,g_header->str_uninstcmd); // '"$TEMP\Un_$1.exe" $0 _?=$INSTDIR\'
-            hProc=myCreateProcess(buf2);
+            GetNSISString(unexecmd, g_header->str_uninstcmd); // '"$TEMP\Un.exe" $0 _?=$INSTDIR\'
+            hProc = myCreateProcess(unexecmd);
             if (hProc)
             {
               CloseHandle(hProc);
-              // success
-              m_Err = 0;
+              m_Err = 0; // Success
+            }
+            else if (!retry++ && !file_exists(unexe))
+            {
+              // Another instance deleted us between CopyFile and CreateProcess
+              goto retry_un_dir;
             }
           }
+          break; // We called CreateProcess; success or failure, we are done.
         }
-        (*(((NSIS_STRING *)g_usrvars)[1]))++;
       }
-
-#ifdef NSIS_SUPPORT_MOVEONREBOOT
-      MoveFileOnReboot(state_temp_dir,NULL);
-#endif
-
       goto end;
     }
   }
 #endif//NSIS_CONFIG_UNINSTALL_SUPPORT
 
diff --git a/Source/exehead/util.c b/Source/exehead/util.c
index 59c5a5f6..ba682f6f 100644
--- a/Source/exehead/util.c
+++ b/Source/exehead/util.c
@@ -66,18 +66,14 @@ const UINT32 g_restrictedacl[] = {
   0x00000101, 0x01000000, 0x00000000 // ACCESS_ALLOWED_ACE:SID (WORLD\Everyone)
 };
 
 DWORD NSISCALL CreateRestrictedDirectory(LPCTSTR path)
 {
-  const SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION|GROUP_SECURITY_INFORMATION|DACL_SECURITY_INFORMATION|PROTECTED_DACL_SECURITY_INFORMATION;
   PSID admingrpsid = GetAdminGrpSid();
   SECURITY_DESCRIPTOR sd = { 1, 0, SE_DACL_PRESENT, admingrpsid, admingrpsid, NULL, GetAdminGrpAcl() };
   SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), &sd, FALSE };
-  DWORD ec = CreateDirectory(path, &sa) ? ERROR_SUCCESS : GetLastError();
-  if (ERROR_ALREADY_EXISTS == ec)
-    ec = SetFileSecurity(path, si, &sd) ? ERROR_SUCCESS : GetLastError();
-  return ec;
+  return CreateDirectory(path, &sa) ? ERROR_SUCCESS : GetLastError();
 }
 DWORD NSISCALL CreateNormalDirectory(LPCTSTR path)
 {
   return CreateDirectory(path, NULL) ? ERROR_SUCCESS : GetLastError();
 }

Vulnerable code:     cur_header->str_uninstchild = add_asciistring(_T("$TEMP\\Un_$1.exe"));
    cur_header->str_uninstcmd = add_asciistring(_T("\"$TEMP\\Un_$1.exe\" $0 _?=$INSTDIR\\"));
      int x, admin = UserIsAdminGrpMember();

      mystrcat(state_temp_dir,_T("~nsu"));
      if (admin) mystrcat(state_temp_dir,_T("A")); // Don't lock down the directory used by non-admins
      mystrcat(state_temp_dir,_T(".tmp"));

      // check if already running from uninstaller temp dir
      // this prevents recursive uninstaller calls
      if (!lstrcmpi(state_temp_dir,state_exe_directory))
        goto end;

      admin ? CreateRestrictedDirectory(state_temp_dir) : CreateNormalDirectory(state_temp_dir);
      SetCurrentDirectory(state_temp_dir);

      if (!(*state_install_directory))
        mystrcpy(state_install_directory,state_exe_directory);
      SET2CHAR(g_usrvars[1], _T("A\0"));

      for (x = 0; x < 26; x ++)
        static TCHAR buf2[NSIS_MAX_STRLEN];

        GetNSISString(buf2,g_header->str_uninstchild); // $TEMP\Un_$1.exe
        DeleteFile(buf2); // clean up after all the other ones if they are there
        if (m_Err) // not done yet
          // copy file
          if (CopyFile(state_exe_path,buf2,TRUE))
            HANDLE hProc;
            MoveFileOnReboot(buf2,NULL);
            GetNSISString(buf2,g_header->str_uninstcmd); // '"$TEMP\Un_$1.exe" $0 _?=$INSTDIR\'
            hProc=myCreateProcess(buf2);
              // success
              m_Err = 0;
        (*(((NSIS_STRING *)g_usrvars)[1]))++;

#ifdef NSIS_SUPPORT_MOVEONREBOOT
      MoveFileOnReboot(state_temp_dir,NULL);
#endif

  const SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION|GROUP_SECURITY_INFORMATION|DACL_SECURITY_INFORMATION|PROTECTED_DACL_SECURITY_INFORMATION;
  DWORD ec = CreateDirectory(path, &sa) ? ERROR_SUCCESS : GetLastError();
  if (ERROR_ALREADY_EXISTS == ec)
    ec = SetFileSecurity(path, si, &sd) ? ERROR_SUCCESS : GetLastError();
  return ec;
Fixed code: \S2{} Major Changes

\b Use isolated uninstaller directory for every instance

    cur_header->str_uninstchild = add_asciistring(_T("$TEMP\\Un.exe"));
    cur_header->str_uninstcmd = add_asciistring(_T("\"$TEMP\\Un.exe\" $0 _?=$INSTDIR\\"));
      int admin = UserIsAdminGrpMember();
      WORD tries; // 0xfffe attempts or bust
      size_t cchtmpslash = mystrlen(state_temp_dir);
      LPTSTR unexe = g_usrvars[5], unexecmd = g_usrvars[6];
      if (!(*state_install_directory))
        mystrcpy(state_install_directory, state_exe_directory);
      }
      for (tries = 0; ++tries != 0;)
      {
        DWORD retry = 0, ec;
retry_un_dir:
        wsprintf(state_temp_dir + cchtmpslash, _T("~nsu%X.tmp"), tries);
        GetNSISString(unexe, g_header->str_uninstchild); // '$TEMP\Un.exe'
        if (admin)
        {
          ec = CreateRestrictedDirectory(state_temp_dir);
        }
        else
        {
          ec = CreateNormalDirectory(state_temp_dir);
        }
        if (ec)
          // Delete previous uninstaller
          if (DeleteFile(unexe))
          {
            myDelete(state_temp_dir, DEL_DIR|DEL_RECURSE);
            if (!retry++) goto retry_un_dir;
          }
        }
        else
        {
          HANDLE hProc;
          SetCurrentDirectory(state_temp_dir);
#ifdef NSIS_SUPPORT_MOVEONREBOOT
          MoveFileOnReboot(state_temp_dir, NULL);
#endif
          if (CopyFile(state_exe_path, unexe, TRUE))
            MoveFileOnReboot(unexe, NULL);
            GetNSISString(unexecmd, g_header->str_uninstcmd); // '"$TEMP\Un.exe" $0 _?=$INSTDIR\'
            hProc = myCreateProcess(unexecmd);
              m_Err = 0; // Success
            }
            else if (!retry++ && !file_exists(unexe))
            {
              // Another instance deleted us between CopyFile and CreateProcess
              goto retry_un_dir;
          break; // We called CreateProcess; success or failure, we are done.
  return CreateDirectory(path, &sa) ? ERROR_SUCCESS : GetLastError();