An improved and enhanced version of the famous LaunchApp.wsf

by Jeremy Saunders on September 3, 2012

Have you ever wondered why your logon script fails to map network drives when an Administrative user logs onto a computer with User Account Control (UAC) enabled; even though the drive mapping process completes successfully?

To understand this you need to read the section from “Group Policy Scripts can fail due to User Account Control” here: http://technet.microsoft.com/en-us/library/cc766208(WS.10).aspx

And here are a couple of nice blogs on the issue:

Even though there are several versions and examples of this script available, none of them did the job perfectly. So I took the one written by Michael Murgolo from Microsoft and enhanced it with feedback and information from the comments and other scripts, as well as addressing some of the reliability concerns with the code.

This script is currently in place in a large University and working perfectly.

When a user logs in it checks for two well-known SIDs in the elevated user token:

  1. S-1-16-1228 is the SID for High Mandatory Level (ie. an elevated administrative account)
  2. S-1-5-32-544 is the SID for members of the BUILTIN Administrators group

If a user is a member of both groups, a Scheduled Task is created that triggers immediately to execute the main logon (map drives) script in the context of their limited user token. Otherwise it launches the main logon (map drives) script directly. By default, the Scheduled Task will delete itself after 1 minute.

The script must be launched with a parameter, which is the pointer to the main logon (map drives) script.

If you have weakened Windows security by implementing the unsupported EnableLinkedConnections registry value as per KB937624, shame on you! Implement this script and remove the EnableLinkedConnections registry value immediately.

Here is the LaunchApp.cmd Script


@Echo off
cls
Echo Running the LaunchApp process to verify local Admin and UAC state...

:: Map the Drives
cscript.exe //nologo "%~dp0LaunchApp.wsf" "%~dp0LogonScripts.cmd"

Exit /b 0

Here is the Launchapp.wsf script


<job>

<script language="VBScript">

'********************************************************************
'*
'* File: Launchapp.wsf
'* Version: 1.0.4
'* Date: 06/26/2012
'*
'* Modified by Jeremy@jhouseconsulting.com 18th June 2012.
'*
'* Version 1.0.2 of this script written by Michael Murgolo from Microsoft and posted here:
'* http://blogs.technet.com/b/elevationpowertoys/archive/2010/05/25/uac-logon-scripts-and-the-launchapp-wsf-workaround.aspx
'*
'* Main Function: This sample launches the application as interactive user.
'* Originaly from:
'* - Deploying Group Policy Using Windows Vista
'* http://technet.microsoft.com/en-us/library/cc766208(WS.10).aspx
'*
'* Usage: cscript launchapp.wsf <AppPath> <Arguments> <WorkingDirectory>
'* Note that <Arguments> and <WorkingDirectory> are optional.
'*
'* Revisions:
'* 1.0.0 Original script from TechNet.
'* 1.0.1 05/25/2010 Modified using code from:
'* - How to detect UAC elevation from VBScript
'* http://blogs.technet.com/jhoward/archive/2008/11/19/how-to-detect-uac-elevation-from-vbscript.aspx.
'* 1.0.2 08/21/2010 Added logic to handle running on legacy (pre-Vista) OS.
'* http://blogs.technet.com/b/elevationpowertoys/archive/2010/05/25/uac-logon-scripts-and-the-launchapp-wsf-workaround.aspx.
'* 1.0.3 06/18/2012 Enhanced script: Jeremy@jhouseconsulting.com
'* - Reviewed another version of this script written by "Patters" posted here:
'* http://pcloadletter.co.uk/2010/05/15/missing-network-drives/
'* - Added the isTerminalServer function to detect if running in a Terminal Services (RDS) Session.
'* - Added the isRemoteSession function to detect if running in a remote (RDP or ICA) Session.
'* - Cleaned up untidy code.
'* - Note: The .UserId property is not a supported property type of TriggerTypeRegistration. It
'* - is supported by TriggerTypeLogon (runs At log on), Whereas the TriggerTypeRegistration
'* - executes at task creation, so it does not require a UserID.
'* 1.0.4 06/26/2012 Enhanced script based on client feedback: Jeremy@jhouseconsulting.com
'* - Added the blnUseScheduledTaskIfApplicable boolean variable.
'* - Added the OSBuildNumber function.
'* - Removed the redundant GetWmiPropertyValue and WMIDateStringToDate functions.
'* - Enhanced the validation of the IsElevated function.
'* - Cleaned up the instr() logic in the IsElevated function. It was working, but it wasn't setup as per best practice.
'* http://saltwetbytes.wordpress.com/2007/11/20/vbscript-if-instrmytext-searchtext-then/
'* - Added the GetExpiryTime and TwoDigit functions derived from another version of this script written by Jarvis Davis posted here:
'* http://verbalprocessor.com/2008/03/27/vista-logon-scripts-launchappwsf/
'*
'********************************************************************

Option Explicit

Dim intOSBuildNumber, blnDebug, strAppPath, strArguments
Dim strWorkingDirectory, blnUseScheduledTaskIfApplicable
Dim intExpireTaskAfter

'********************** Variables to set ****************************

' Set to True to use a Scheduled Task if applicable
' Set to False to launch directly regardless of the local Administrative permissions and UAC
blnUseScheduledTaskIfApplicable = True

' Set the number of minutes to wait before expiring the task.
' The task will be deleted after it expires.
intExpireTaskAfter = 1

blnDebug = False

'********************************************************************

If WScript.Arguments.Length <> 1 Then
 WScript.Echo "Usage: cscript launchapp.wsf <AppPath> <Arguments> <WorkingDirectory>" & _
 vbcrlf & "Note that <Arguments> and <WorkingDirectory> are optional."
' WScript.Quit(0)
End If

If WScript.Arguments.Length = 1 Then
 strAppPath = WScript.Arguments(0)
 strArguments = ""
 strWorkingDirectory = ""
ElseIf WScript.Arguments.Length = 2 Then
 strAppPath = WScript.Arguments(0)
 strArguments = WScript.Arguments(1)
 strWorkingDirectory = ""
ElseIf WScript.Arguments.Length = 3 Then
 strAppPath = WScript.Arguments(0)
 strArguments = WScript.Arguments(1)
 strWorkingDirectory = WScript.Arguments(2)
End If

intOSBuildNumber = OSBuildNumber()

If intOSBuildNumber >= 6000 Then
 ' Running on Vista or higher.
 ' Check if elevated. ie. Running as an Administrative user and UAC is enabled.
 If IsElevated AND blnUseScheduledTaskIfApplicable Then
 If blnDebug Then
 wscript.echo "Launch As Scheduled Task"
 End If
 LaunchAsScheduledTask strAppPath, strArguments, strWorkingDirectory, intExpireTaskAfter
 Else
 If blnDebug Then
 wscript.echo "Launch Directly"
 End If
 LaunchDirectly strAppPath, strArguments
 End If
Else
 ' Running on legacy OS (XP/2003 or lower).
 If blnDebug Then
 wscript.echo "Launch Directly"
 End If
 LaunchDirectly strAppPath, strArguments
End If

wscript.quit(0)
Sub LaunchAsScheduledTask(strAppPath, strArguments, strWorkingDirectory, intExpireTaskAfter)
 Dim service, strTaskName, rootFolder, taskDefinition
 Dim triggers, trigger, Action, Settings, blnDebug
 Dim objNetwork, strUser, strDomain, dtmExpiryTime

blnDebug = False

Set objNetwork = CreateObject("WScript.Network")
 strUser = objNetwork.UserName
 strDomain = objNetwork.UserDomain

dtmExpiryTime = GetExpiryTime(intExpireTaskAfter)

'***********************************************************
 ' Create Scheduled Task to launch app.
 '***********************************************************
 ' A constant that specifies a registration trigger.
 const TriggerTypeRegistration = 7

' A constant that specifies an executable action.
 const ActionTypeExecutable = 0

' A constant that specifies the flag in RegisterTaskDefinition.
 const FlagTaskCreate = 2

' A constant that specifies an executable action.
 const LogonTypeInteractive = 3

'********************************************************
 ' Create the TaskService object.
 '********************************************************
 Set service = CreateObject("Schedule.Service")
 call service.Connect()
 If NOT isTerminalServer(intOSBuildNumber) Then
 strTaskName = "Launch App As Interactive User"
 Else
 strTaskName = "Launch App As Interactive User - " & strUser & " from the " & strDomain & " domain"
 End If

'********************************************************
 ' Get a folder to create a task definition in.
 '********************************************************
 Set rootFolder = service.GetFolder("\")

'Delete the task if already present
 On Error Resume Next
 call rootFolder.DeleteTask(strTaskName, 0)
 Err.Clear
 On Error Goto 0

'********************************************************
 ' Create the new task
 '********************************************************
 Set taskDefinition = service.NewTask(0)

'********************************************************
 ' Create a registration trigger.
 '********************************************************
 Set triggers = taskDefinition.Triggers

Set trigger = triggers.Create(TriggerTypeRegistration)
 ' Set the task to expire so it can be deleted automatically
 trigger.EndBoundary = dtmExpiryTime

'***********************************************************
 ' Create the action for the task to execute.
 '***********************************************************
 ' Add an action to the task. The action executes the app.
 Set Action = taskDefinition.Actions.Create( ActionTypeExecutable )
 Action.Path = strAppPath
 If strArguments <> "" Then
 Action.Arguments = strArguments
 End If
 If strWorkingDirectory <> "" Then
 Action.WorkingDirectory = strWorkingDirectory
 End If
 If blnDebug Then
 WScript.Echo "Task definition created. About to submit the task..."
 End If

'***********************************************************
 ' Set the settings for the task
 '***********************************************************
 Set Settings = taskDefinition.Settings
 ' Delete the task immediately after the trigger expires
 Settings.DeleteExpiredTaskAfter = "PT0M"

'***********************************************************
 ' Register (create) the task.
 '***********************************************************
 ' Note that the task is created as an XML file under the
 ' %SystemRoot%\System32\Tasks folder
 call rootFolder.RegisterTaskDefinition(strTaskName, taskDefinition, FlagTaskCreate, ,, LogonTypeInteractive)
 If blnDebug Then
 WScript.Echo "Task submitted."
 End If

Set service = Nothing
 Set rootFolder = Nothing
 Set taskDefinition = Nothing
 Set triggers = Nothing
 Set trigger = Nothing
 Set Action = Nothing
 Set Settings = Nothing
 Set objNetwork = Nothing

End Sub
Sub LaunchDirectly(strAppPath, strArguments)
 '***********************************************************
 ' Running as standard user or on legacy OS. Directly launch app.
 '***********************************************************
 Dim oShell, strCommandLine, Return
 ' intWindowStyle for WshShell.Run
 Const WINDOW_STYLE_HIDE = 0
 Const WINDOW_STYLE_ACTIVATE_DISPLAY = 1
 Const WINDOW_STYLE_ACTIVATE_MINIMIZE = 2
 Const WINDOW_STYLE_ACTIVATE_MAXIMIZE = 3
 Const WINDOW_STYLE_RECENT = 4
 Const WINDOW_STYLE_ACTIVATE_CURRENT = 5
 Const WINDOW_STYLE_MINIMIZE_ACTIVATE_NEXT = 6
 Const WINDOW_STYLE_MINIMIZE = 7
 Const WINDOW_STYLE_CURRENT = 8
 Const WINDOW_STYLE_ACTIVATE_RESTORE = 9
 Const WINDOW_STYLE_FROM_PARENT = 10

Set oShell = WScript.CreateObject("WScript.Shell")

If strArguments = "" Then
 strCommandLine = strAppPath
 Else
 strCommandLine = strAppPath & " " & strArguments
 End If

Return = oShell.Run(strCommandLine, WINDOW_STYLE_ACTIVATE_DISPLAY, true)

Set oShell = Nothing

End Sub
'***********************************************************
' Function to detect whether script in running elevated.
'***********************************************************
Function IsElevated()

IsElevated = False

Dim oShell, oExec, szStdOut, cnWshRunning, blnDebug

blnDebug = False
 szStdOut = ""
 Set oShell = CreateObject("WScript.Shell")
 Set oExec = oShell.Exec("whoami /groups")

Do While (oExec.Status = cnWshRunning)
 WScript.Sleep 100
 if not oExec.StdOut.AtEndOfStream then
 szStdOut = szStdOut & oExec.StdOut.ReadAll
 end if
 Loop

select case oExec.ExitCode
 case 0
 if not oExec.StdOut.AtEndOfStream then
 szStdOut = szStdOut & oExec.StdOut.ReadAll
 end if
 ' S-1-16-1228 is the SID for High Mandatory Level (ie. an elevated administrative account)
 ' S-1-5-32-544 is the SID for members of the BUILTIN Administrators group
 ' We check for both SIDs to be 100% sure
 if instr(1,szStdOut,"S-1-16-12288",1) > 0 AND instr(1,szStdOut,"S-1-5-32-544",1) > 0 Then
 If blnDebug Then
 wscript.echo "Elevated"
 End If
 IsElevated = True
 else
 If blnDebug Then
 wscript.echo "Not Elevated"
 End If
 end if
 case else
 if not oExec.StdErr.AtEndOfStream then
 If blnDebug Then
 wscript.echo oExec.StdErr.ReadAll
 End If
 end if
 end select

Set oShell = Nothing
 Set oExec = Nothing

End Function
'***********************************************************
' Function to retrieve The OS BuildNumber.
'***********************************************************
Function OSBuildNumber()
 Dim strComputer, oWMIService, colOSInfo, oOSProperty
 strComputer = "."
 Set oWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
 Set colOSInfo = oWMIService.ExecQuery("Select * from Win32_OperatingSystem",,48)
 For Each oOSProperty in colOSInfo
 OSBuildNumber = oOSProperty.BuildNumber
 Next
 Set oWMIService = Nothing
 Set colOSInfo = Nothing
End Function
'********************************************************
' Function to add time intExpireTaskAfter to the current date/time
' to be used to set an expiration to the scheduled task.
 '********************************************************
Function GetExpiryTime(intExpireTaskAfter)
 Dim objShell, strActiveTimeBias, intoffset, intoffsetHour
 Dim intoffsetMin, stroffset, dtmNewTime
 Set objShell = CreateObject("WScript.Shell")
 strActiveTimeBias = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias"
 ' Get the time offset from the registry and convert it into the hour offset.
 ' This may need modification for non-US locales
 intoffset = objShell.RegRead(strActiveTimeBias) / 60
 intoffsetHour = intoffset Mod 60
 intoffsetMin = intoffset \ 60
 If intoffset < 0 Then
 stroffset = "+" & TwoDigit(intoffsetHour) & ":" & TwoDigit(intoffsetMin)
 Else
 strOffset = "-" & TwoDigit(intoffsetHour) & ":" & TwoDigit(intoffsetMin)
 End If
 ' Add time to the current date and time so we can set the trigger expiration to that
 dtmNewTime = DateAdd("n",intExpireTaskAfter,Now())
 GetExpiryTime = Year(dtmNewTime) & "-" & TwoDigit(Month(dtmNewTime)) & "-" & TwoDigit(Day(dtmNewTime)) & _
 "T" & TwoDigit(Hour(dtmNewTime)) & ":" & TwoDigit(Minute(dtmNewTime)) & ":" & _
 TwoDigit(Second(dtmNewTime)) & strOffset
 Set objShell = Nothing
End Function
'********************************************************
' Function to makes single digit numbers have a leading 0
' and return only positive values
'********************************************************
Function TwoDigit(vExpression)
 If Abs(vExpression) < 10 Then
 TwoDigit = "0" & Abs(vExpression)
 Else
 TwoDigit = Abs(vExpression)
 End If
End Function
'***********************************************************
' Function to detect whether script is running on a Terminal
' (RDS) Server. This will also cover a Citrix XenApp server.
'***********************************************************
Function isTerminalServer(intOSBuildNumber)
 Dim WshShell, strComputer, strNameSpace, objWMIService, colItems
 Dim objItem, blnEventLog, strService, strState
 Const EVENT_SUCCESS = 0
 blnEventLog = False
 set WshShell = WScript.CreateObject("WScript.Shell")
 strComputer = "."
 If intOSBuildNumber >= 6000 Then
 strNameSpace = "\root\cimv2\TerminalServices"
 Else
 strNameSpace = "\root\cimv2"
 End If
 Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & strNameSpace)
 Set colItems = objWMIService.ExecQuery _
 ("Select * from Win32_TerminalServiceSetting",,48)
 For Each objItem in colItems
 Select Case objItem.LicensingType
 Case "1" ' Remote Administration
 isTerminalServer = False
 Case "2" ' Per Device
 isTerminalServer = True
 Case "4" ' Per User
 isTerminalServer = True
 Case "5" ' Not configured yet
 isTerminalServer = True
 Case Else
 isTerminalServer = False
 If blnEventLog Then
' Even if it is a Terminal Server, the LicensingType will return an empty value if the
' "Terminal Services" (TermService) service is in a Stopped state.
 WshShell.LogEvent EVENT_SUCCESS, Wscript.ScriptName & ": The terminal server licensing type is: " & objItem.LicensingType
 strService = "TermService"
 strState = CheckServiceState(strService)
 WshShell.LogEvent EVENT_SUCCESS, Wscript.ScriptName & ": The " & strService & " is : " & strState
 End If
 End Select
 Next
 set WshShell = Nothing
 Set objWMIService = Nothing
 Set colItems = Nothing
End Function
'***********************************************************
' Function to detect whether script is running in an remote
' session. ie. an RDP or ICA Session.
'***********************************************************
Function isRemoteSession()
 Dim objShell, strSessionName, blnDebug
 blnDebug = False
 Set objShell = CreateObject("WScript.Shell")
 strSessionName = objShell.ExpandEnvironmentStrings("%SESSIONNAME%")
 If strComp(strSessionName,"Console",1) = 0 then
 isRemoteSession = False
 If blnDebug Then
 wscript.echo "You are running directly on the console session!"
 End If
 Else
 If instr(1,strSessionName,"RDP-TCP",1) = 1 Then
 isRemoteSession = True
 If blnDebug Then
 wscript.echo "You are in a Remote Desktop (RDP) Session!"
 End If
 End If
 If instr(1,strSessionName,"ICA-TCP",1) = 1 Then
 isRemoteSession = True
 If blnDebug Then
 wscript.echo "You are in a Citrix (ICA) Session!"
 End If
 End If
 End If
 Set objShell = Nothing
End Function
</script>

</job>

Enjoy!

Jeremy Saunders

Jeremy Saunders

Independent Consultant | Contractor | Microsoft & Citrix Specialist | Desktop Virtualization Specialist at J House Consulting
Jeremy is a highly respected, IT Professional, with over 30 years’ experience in the industry. He is an independent IT consultant providing expertise to enterprise, corporate, higher education and government clients. His skill set, high ethical standards, integrity, morals and attention to detail, coupled with his friendly nature and exceptional design and problem solving skills, makes him one of the most highly respected and sought after Microsoft and Citrix technical resources in Australia. His alignment with industry and vendor best practices puts him amongst the leaders of his field.
Jeremy Saunders
Jeremy Saunders
Jeremy Saunders
  • Josh

    It seems to work well in my testing, except I notice that you have isRemoteSession() but you don’t seem to use it. I am seeing in my tests that it works however when using RDP to a win7 machine the drives wont map – the same user on the same machine with a local login will work. Was there special steps to detect an RDP session and treat it differently you didn’t leave in the final version?

    • Jeremy

      Hi Josh,

      That is correct. The isRemoteSession() function is not used unless you actually add it. However, the script works as it should in a local or remote session. I’ve implemented it as is in a couple of Terminal Server/Citrix environments, and it works perfectly.

      1) Let’s see if the Scheduled Task is being created. By default it will not delete itself until 1 minute after logon, which will give you enough time to check for it under the “%SystemRoot%\System32\Tasks” folder. If it’s there, check the time stamp and make sure it’s just been created. If it’s there, it’s either not triggering, or it’s failing on the execution of the task.

      2) The IsElevated function is returning false, so it’s running the script directly. I’m unsure why this would be the case and I’ve not seen this before.

      Once you are logged in via RDP, do the drive mappings work if you run the script manually?

      You’ll need to enable debugging by setting the blnDebug value to True and probably add some lines so that it writes to the Application Event Log:

      Set objShell = CreateObject(“WScript.Shell”)
      objShell.LogEvent 0, “This is the text I want to appear in an Event in the Application Event Log”
      Set objShell = Nothing

      Hope that helps.

      Cheers,
      Jeremy.

      • Josh

        Jeremy,

        I did some playing around with it last night, and with debugging turned on I was able to see the message that was creating the scheduled task – however I wasn’t sure where to look to see if it wasn’t being created or if it wasn’t being run after it was created. I will have to look into that. (So it wasn’t running the script directly)

        And yes, the script works over RDP once logged in. The same user if I remove admin works (because of course the script is running directly at that point).

        I suspect it has to do with the task not being created/not being executed – I will have to look into this. Thanks for the response.

        Josh

Previous post:

Next post: