Saved Passwords

Viscosity Menu Icon Packs, Two-Factor Scripts, & OpenVPN Config Tool


Posts: 2
Joined: Tue Jul 27, 2021 11:00 pm

Post by frankiem » Tue Jul 27, 2021 11:15 pm
Greetings everyone,

Famously I think a lot of people have searched and found a post talking about the command(s) to disable viscosity from allowing users to save passwords. I didn't want to resurrect an old post, but I'm going to put some information here that will hopefully save some people time.

Note: If you are looking to package these options, please see the following links:

The following can be used to enable or disable the save password option from a powershell script:
Code: Select all
#Disables (and appears to clear) the ability to save password 
cmd.exe /c "`"C:\Program Files\Viscosity\Viscosity.exe`" SetPref PasswordStorageSupport false"

#Enables the ability to save the password
cmd.exe /c "`"C:\Program Files\Viscosity\Viscosity.exe`" SetPref PasswordStorageSupport true"
^^ be sure to restart the viscosity client after running these commands.

How you deploy the powershell is up to you. In my particular case I am deploying the script using Microsoft's InTune. So I needed a way to get the script to run without using a scheduled task. This will run on user login and then every N minutes. I found Jos had a script I could use and then tack on the above.

So here is the full script:
( ... 39cdabf052)
Code: Select all
#Module name:      Invoke-asIntuneLogonScript
#Author:           Jos Lieben
#Author Blog:
#Date:             11-06-2019
#Purpose:          Using this code in ANY Intune script 
#Requirements:     Windows 10 build 1803 or higher
#License:          Free to use and modify non-commercially, leave headers intact. For commercial use, contact me
#Thanks to:
#@michael_mardahl for the idea to remove the script from the registry so it automatically reruns
#@Justin Murray for a .NET example of how to impersonate a logged in user

#FrankieM Note: Keeping all credits to Jos above, adding this is a modified version of code at the bottom to disable Viscosity from saving passwords.

$autoRerunMinutes = 60 #If set to 0, only runs at logon, else, runs every X minutes AND at logon, expect random delays of up to 5 minutes due to bandwidth, service availability, local resources etc. I strongly recommend 0 or >60 as input value to avoid being throttled
$visibleToUser = $False

#Uncomment for debug logs:
#Start-Transcript -Path (Join-Path $Env:temp -ChildPath "intuneRestarter.log") -Append -Confirm:$False
    $runningAsSystem = $True
    Write-Output "Running as SYSTEM"
    $runningAsSystem = $False
    Write-Output "Running as $($env:USERNAME)"

[email protected]"
using System;
using System.Runtime.InteropServices;

namespace murrayju
    public static class ProcessExtensions
        #region Win32 Constants

        private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
        private const int CREATE_NO_WINDOW = 0x08000000;

        private const int CREATE_NEW_CONSOLE = 0x00000010;

        private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
        private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;


        #region DllImports

        [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            String lpApplicationName,
            String lpCommandLine,
            IntPtr lpProcessAttributes,
            IntPtr lpThreadAttributes,
            bool bInheritHandle,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            String lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation);

        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
        private static extern bool DuplicateTokenEx(
            IntPtr ExistingTokenHandle,
            uint dwDesiredAccess,
            IntPtr lpThreadAttributes,
            int TokenType,
            int ImpersonationLevel,
            ref IntPtr DuplicateTokenHandle);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

        [DllImport("userenv.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hSnapshot);

        private static extern uint WTSGetActiveConsoleSessionId();

        private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);

        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern int WTSEnumerateSessions(
            IntPtr hServer,
            int Reserved,
            int Version,
            ref IntPtr ppSessionInfo,
            ref int pCount);


        #region Win32 Structs

        private enum SW
            SW_HIDE = 0,
            SW_SHOWNORMAL = 1,
            SW_NORMAL = 1,
            SW_SHOWMINIMIZED = 2,
            SW_SHOWMAXIMIZED = 3,
            SW_MAXIMIZE = 3,
            SW_SHOWNOACTIVATE = 4,
            SW_SHOW = 5,
            SW_MINIMIZE = 6,
            SW_SHOWMINNOACTIVE = 7,
            SW_SHOWNA = 8,
            SW_RESTORE = 9,
            SW_SHOWDEFAULT = 10,
            SW_MAX = 10

        private enum WTS_CONNECTSTATE_CLASS

        public struct PROCESS_INFORMATION
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;

            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3,

        private struct STARTUPINFO
            public int cb;
            public String lpReserved;
            public String lpDesktop;
            public String lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public uint dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;

        private enum TOKEN_TYPE
            TokenPrimary = 1,
            TokenImpersonation = 2

        private struct WTS_SESSION_INFO
            public readonly UInt32 SessionID;

            public readonly String pWinStationName;

            public readonly WTS_CONNECTSTATE_CLASS State;


        // Gets the user token from the currently active session
        private static bool GetSessionUserToken(ref IntPtr phUserToken, int targetSessionId)
            var bResult = false;
            var hImpersonationToken = IntPtr.Zero;
            var activeSessionId = INVALID_SESSION_ID;
            var pSessionInfo = IntPtr.Zero;
            var sessionCount = 0;

            // Get a handle to the user access token for the current active session.
            if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
                var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
                var current = pSessionInfo;

                for (var i = 0; i < sessionCount; i++)
                    var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
                    current += arrayElementSize;

                    if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive && si.SessionID == targetSessionId)
                        activeSessionId = si.SessionID;

            // If enumerating did not work, fall back to the old method
            if (activeSessionId == INVALID_SESSION_ID)
                activeSessionId = WTSGetActiveConsoleSessionId();

            if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0)
                // Convert the impersonation token to a primary token
                bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                    (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                    ref phUserToken);


            return bResult;

        public static PROCESS_INFORMATION StartProcessAsCurrentUser(int targetSessionId, string appPath, string cmdLine = null, bool visible = true)
            var hUserToken = IntPtr.Zero;
            var startInfo = new STARTUPINFO();
            var procInfo = new PROCESS_INFORMATION();
            var procInfoRes = new PROCESS_INFORMATION();
            var pEnv = IntPtr.Zero;
            int iResultOfCreateProcessAsUser;

            startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));

                if (!GetSessionUserToken(ref hUserToken, targetSessionId))
                    throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken for session "+targetSessionId+" failed.");

                uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW);
                startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE);
                startInfo.lpDesktop = "winsta0\\default";

                if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false))
                    throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed.");

                if (!CreateProcessAsUser(hUserToken,
                    appPath, // Application Name
                    cmdLine, // Command Line
                    null, // Working directory
                    ref startInfo,
                    out procInfo))
                    iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
                    throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.  Error Code " + iResultOfCreateProcessAsUser);
                procInfoRes = procInfo;
                iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
                if (pEnv != IntPtr.Zero)

            return procInfoRes;


$scriptPath = $PSCommandPath

    Write-Output "Running in system context, script should be running in user context, we should auto impersonate"
    #Generate registry removal path
    $regPath = "HKLM:\Software\Microsoft\IntuneManagementExtension\Policies\$($scriptPath.Substring($scriptPath.LastIndexOf("_")-36,36))\$($scriptPath.Substring($scriptPath.LastIndexOf("_")+1,36))"
    #get targeted user session ID from intune management log as there is no easy way to translate the user Azure AD to the local user
    $logLocation = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log"
    $targetUserSessionId = (Select-String -Pattern "$($scriptPath.Substring($scriptPath.LastIndexOf("_")-36,36)) in session (\d+)]" $logLocation | Select-Object -Last 1).Matches[0].Groups[1].Value

    $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters
    $compilerParameters.GenerateInMemory = $True
    Add-Type -TypeDefinition $source -Language CSharp -CompilerParameters $compilerParameters
        $res = [murrayju.ProcessExtensions]::StartProcessAsCurrentUser($targetUserSessionId, "c:\windows\system32\WindowsPowerShell\v1.0\powershell.exe", " -WindowStyle Normal -nologo -executionpolicy ByPass -Command `"& '$scriptPath'`"",$True)
        $res = [murrayju.ProcessExtensions]::StartProcessAsCurrentUser($targetUserSessionId, "c:\windows\system32\WindowsPowerShell\v1.0\powershell.exe", " -WindowStyle Hidden -nologo -executionpolicy ByPass -Command `"& '$scriptPath'`"",$False)
    Sleep -s 1

    #get new process info, we could use this in a future version to await completion
    $process = Get-WmiObject Win32_Process -Filter "name = 'powershell.exe'" | where {$_.CommandLine -like "*$scriptPath*"}

    #start a seperate process as SYSTEM to monitor for user logoff/logon and preferred scheduled reruns
    start-process "c:\windows\system32\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle Hidden -ArgumentList "`$slept = 0;`$script:refreshNeeded = `$false;`$sysevent = [microsoft.win32.systemevents];Register-ObjectEvent -InputObject `$sysevent -EventName `"SessionEnding`" -Action {`$script:refreshNeeded = `$true;};Register-ObjectEvent -InputObject `$sysevent -EventName `"SessionEnded`"  -Action {`$script:refreshNeeded = `$true;};Register-ObjectEvent -InputObject `$sysevent -EventName `"SessionSwitch`"  -Action {`$script:refreshNeeded = `$true;};while(`$true){;`$slept += 0.2;if((`$slept -gt ($autoRerunMinutes*60) -and $autoRerunMinutes -ne 0) -or `$script:refreshNeeded){;`$slept=0;`$script:refreshNeeded=`$False;Remove-Item $regPath -Force -Confirm:`$False -ErrorAction SilentlyContinue;Restart-Service -Name IntuneManagementExtension -Force;Exit;};Start-Sleep -m 200;};"    
    #set removal key in case computer crashes or something like that
    $runOnceEntries = (Get-ItemProperty -path "HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce")
    if([Array]@($runOnceEntries.PSObject.Properties.Name | % {if($runOnceEntries.$_ -eq "reg delete $regPath /f"){$_}}).Count -le 0){
        New-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce" -Name $(Get-Random) -Value "reg delete $regPath /f" -PropertyType String -Force -ErrorAction SilentlyContinue
    start-sleep -s 10


cmd.exe /c "`"C:\Program Files\Viscosity\Viscosity.exe`" SetPref PasswordStorageSupport false"

Throw "Bye" #final line needed so intune will not stop rerunning the script
viewtopic.php?t=2249 ... schedules/
Credit for the larger script to Jos Lieben:


User avatar
Posts: 1146
Joined: Sun Jan 03, 2010 3:27 am

Post by Eric » Wed Jul 28, 2021 8:22 am
Hi frankiem,

Thanks for your contribution, we do appreciate it!

There is another alternative though that may be easier than needing to run a script like this. Viscosity will pull preferences via GPO. You can push these via your own policies using the ADMX files provided here - ... vironment/

If you wish to do this manually however, you can set these in registry. For example, disabling password support for all users would look like this:
Code: Select all
Windows Registry Editor Version 5.00


The same key can be applied per-user in HKEY_CURRENT_USER.

If you don't like to use software policies, you can also apply this at SOFTWARE\SparkLabs\Viscosity\Preferences instead.

Eric Thorpe
Viscosity Developer



Posts: 2
Joined: Tue Jul 27, 2021 11:00 pm

Post by frankiem » Wed Jul 28, 2021 11:56 pm
Hi Eric,

Thanks for pointing out the ADMX files for use in AD. I think the registry changes are defiantly a better way to handle the settings than what I mentioned above. I'm going to look into using the registry changes in way that can be utilized for those that only use Azure AD and MEM/InTune. :idea:
3 posts Page 1 of 1