Posts Tagged ‘ VBScript

How to manage GPOs with vbScript?

You can do really really cool stuff with gpos in vbScript. I will show you how to export reports and give you some examples what else can be done going the vbScript way…
The Group Policy Management console in Windows offers you the possibility to export reports about group policy object’s settings to html files – this, for example, is an excerpt of my default domain controllers policy:

You can do this (and much more) by script too. Here is how you can do it… You can choose to save this report in a variable to do further processing in your script or you can save it to a file, just like the console does.

Function getGPOHTMLReport(strDomain, strGPOCN)
  Set objGPM = CreateObject("GPMgmt.GPM")
  Set objGPMConstants = objGPM.GetConstants()
  Set objGPMDomain = objGPM.GetDomain(strDomain, "", objGPMConstants.UseAnyDC)
  Set objGPO = objGPMDomain.GetGPO(strGPOCN)
  Set objGPMReport = objGPO.GenerateReport(objGPMConstants.ReportHTML)
  getGPOHTMLReport = objGPMReport.result
End Function

Wscript.echo getGPOHTMLReport("normanbauer.com", "{6AC1786C-016F-11D2-945F-00C04fB984F9}") 'Default Domain Controllers Policy
Sub exportGPOHTMLReport(strDomain, strGPOCN, strOutFilename)
  Set objGPM = CreateObject("GPMgmt.GPM")
  set objGPMConstants = objGPM.GetConstants()
  set objGPMDomain = objGPM.GetDomain(strDomain, "", objGPMConstants.UseAnyDC)

  Set objGPO = objGPMDomain.GetGPO(strGPOCN)
  objGPO.GenerateReportToFile objGPMConstants.ReportHTML, strOutFilename
End Sub

exportGPOHTMLReport "normanbauer.com", "{6AC1786C-016F-11D2-945F-00C04fB984F9}", "C:\temp\export.html" 'Default Domain Controllers Policy

Functions used in the scripts above:

The function above generates the report of the specified gpo (you can find the cn of the gpo ["Unique ID"] in the Group Policy Management console on the details tab of a gpo, or in the System\Policies Container in Active Directory) and returns the html formatted result. The sub does almost the same but does not return the result but saves it to a file specified in strOutFilename.

You can do much more with the GPMgmt.GPM object – almost everything what the console can do, like creating, deleting and copying gpos, get and set wmi filters and set the gpo to be enabled or disabled on computer and/or user accounts.

How to change BitLocker recovery password with vbScript?

Related to my last post about how to change BitLocker recovery password from an elevated command prompt here is how you can achieve the same result with vbScript and WMI. This script is from Microsoft TechNet: BitLocker Drive Encryption Operations Guide: Recovering Encrypted Volumes with AD DS.

' Target drive letter
strDriveLetter = "c:"

' Target computer name
' Use "." to connect to the local computer
strComputerName = "."

' --------------------------------------------------------------------------------
' Connect to the BitLocker WMI provider class
' --------------------------------------------------------------------------------

strConnectionStr = "winmgmts:" _
                 & "{impersonationLevel=impersonate,authenticationLevel=pktPrivacy}!\\" _
                 & strComputerName _
                 & "\root\cimv2\Security\MicrosoftVolumeEncryption"

On Error Resume Next 'handle permission errors

Set objWMIService = GetObject(strConnectionStr)

If Err.Number <> 0 Then
     WScript.Echo "Failed to connect to the BitLocker interface (Error 0x" & Hex(Err.Number) & ")."
     Wscript.Echo "Ensure that you are running with administrative privileges."
     WScript.Quit -1
End If

On Error GoTo 0

strQuery = "Select * from Win32_EncryptableVolume where DriveLetter='" & strDriveLetter & "'"
Set colTargetVolumes = objWMIService.ExecQuery(strQuery)

If colTargetVolumes.Count = 0 Then
    WScript.Echo "FAILURE: Unable to find BitLocker-capable drive " &  strDriveLetter & " on computer " & strComputerName & "."
    WScript.Quit -1
End If

' there should only be one volume found
For Each objFoundVolume in colTargetVolumes
    set objVolume = objFoundVolume
Next

' objVolume is now our found BitLocker-capable disk volume

' --------------------------------------------------------------------------------
' Perform BitLocker WMI provider functionality
' --------------------------------------------------------------------------------

' Add a new recovery password, keeping the ID around so it doesn't get deleted later
' ----------------------------------------------------------------------------------

nRC = objVolume.ProtectKeyWithNumericalPassword("Recovery Password Refreshed By Script", , sNewKeyProtectorID)

If nRC <> 0 Then
     WScript.Echo "FAILURE: ProtectKeyWithNumericalPassword failed with return code 0x" & Hex(nRC)
     WScript.Quit -1
End If

' Removes the other, "stale", recovery passwords
' ----------------------------------------------------------------------------------

nKeyProtectorTypeIn = 3 ' type associated with "Numerical Password" protector

nRC = objVolume.GetKeyProtectors(nKeyProtectorTypeIn, aKeyProtectorIDs)

If nRC <> 0 Then
     WScript.Echo "FAILURE: GetKeyProtectors failed with return code 0x" & Hex(nRC)
     WScript.Quit -1
End If

' Delete those key protectors other than the one we just added.

For Each sKeyProtectorID In aKeyProtectorIDs
     If sKeyProtectorID <> sNewKeyProtectorID Then
          nRC = objVolume.DeleteKeyProtector(sKeyProtectorID)
          If nRC <> 0 Then
               WScript.Echo "FAILURE: DeleteKeyProtector on ID " & sKeyProtectorID & " failed with return code 0x" & Hex(nRC)
               WScript.Quit -1
          Else
               ' no output
               'WScript.Echo "SUCCESS: Key protector with ID " & sKeyProtectorID & " deleted"
          End If
     End If
Next

WScript.Echo "A new recovery password has been added. Old passwords have been removed."

' - some advanced output (hidden)
'WScript.Echo ""
'WScript.Echo "Type ""manage-bde -protectors -get " & strDriveLetter & " -type recoverypassword"" to view existing passwords."

How to display Active Directory stored user account pictures in Windows?

This topic is little more complex and we’ll need different steps to accomplish this feature.
First at all you need the pictures for your user objects in the directory. You can use ADSI Edit, Powershell or 3rd party software to put pictures in AD. My blog post “How to save a user picture in Active Directory with vbScript?” will do this for you using vbScript.
Second you’ll need a small application that sets a picture on your harddrive as the user account picture. I tested this only with Windows 7 – so there is no guarantee that this works with other versions too. Why an application? Because there is no documented way of setting an individual user account picture programmatically in vbScript – neither via registry nor via file system.
My blog post “How to set the Windows 7 user account picture programmatically?” covers that small application.
Now that we have the pictures in Active Directory and developed a small application that sets an image file as user account picture we just need one more step between both. We need a vbScript that runs at logon, loads the picture from AD, puts it on the disk and calls our application. Here we go:

Function LoadPictureFromAD(szADsPath, szSaveFileName)
	Dim objUser, bytesRead, adoStreamWrite
	Const adTypeBinary = 1, adSaveCreateOverWrite = 2

	Set objUser = GetObject(szADsPath)
	bytesRead = objUser.Get("thumbnailPhoto")

	Set adoStreamWrite = CreateObject("ADODB.Stream")
	adoStreamWrite.Type = adTypeBinary
	adoStreamWrite.Open
	adoStreamWrite.Write(bytesRead)
	adoStreamWrite.SaveToFile szSaveFileName, adSaveCreateOverWrite
	adoStreamWrite.Close
End Function

Set wshShell = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")

workingdir = Replace(wscript.scriptfullname, Wscript.scriptname, "")

Set wshNetwork = WScript.CreateObject("WScript.Network")
username = wshNetwork.UserDomain  & "\" & wshNetwork.UserName
Set objSysInfo = CreateObject("ADSystemInfo")
strUserName = objSysInfo.UserName
dn = "LDAP://" & strUserName

path = wshShell.ExpandEnvironmentStrings("%temp%") & "\"
filename = path & "uap.jpg"

LoadPictureFromAD dn, filename
wshshell.run workingdir & "useraccountpicture.exe " & username & " " & filename, 0, true

This script contains the function LoadPictureFromAD which expects the user’s distinguished name and a filename where the resulting picture will be saved to. Inside the function we connect to AD, get the user’s object, read the attribute thumbnailPhoto, copy it to a previously created stream object and save it in a file identified by szSaveFileName.
The script itself assumes that the useraccountpicture.exe application resists in the same location as the script. So we set the working directory to the scripts location. After that we need the user’s name, it’s domain and it’s distinguished name. Then we create the filename for the image, this is simply the temp folder with “uap.jpg” as filename. With the distinguished name and the filename we can call the LoadPictureFromAD function and finally we run the useraccountpicture application with the username and the filename as parameters.

Save the script as .vbs – run it – have a look at the start menu – that’s all…

Now you can use this script as a logon script within group policies and all your users can find their photos in the start menu and on the lock screen.

How to save a user picture in Active Directory with vbScript?

Active Directory offers the possibility to save pictures in a user’s object. These pictures can then be used in Outlook, Sharepoint or even self-written applications. Here is how you can do it:

Function SavePictureToAdFromUrl(szADsPath, szUrl)
	Dim objUser, bytesRead, adoStreamRead
	Const adTypeBinary = 1

	Set xml = CreateObject("Microsoft.XMLHTTP")
	xml.Open "GET", szUrl, False
	xml.Send

	If xml.status = 200 Then
		Set adoStreamRead = CreateObject("ADODB.Stream")
		adoStreamRead.Type = adTypeBinary
		adoStreamRead.Open
		adoStreamRead.Write xml.responseBody
		adoStreamRead.Position = 0
		bytesRead = adoStreamRead.Read()
		adoStreamRead.Close

		Set objUser = GetObject(szADsPath)
		objUser.Put "thumbnailPhoto", bytesRead
		objUser.SetInfo
	End If
End Function

This function will read a picture resource from a url (szUrl) and append it to the thumbnailPhoto attribute in the user object identified by szADsPath.
First we open the url and see if we’ll get a status 200 (OK). If so a binary stream object is created and the picture data from the url is written into it. After that we transfer the data from the stream object into a variable, then we create an object from the given szADsPath and finally put the content of the bytesRead variable into the thumbnailPhoto attribute of the user object.
If you do not have a website providing the picture you can do this also with files on a file system.

Function SavePictureToAD(szADsPath, szLoadFileName)
	Dim objUser, bytesRead, adoStreamRead
	Const adTypeBinary = 1

	Set adoStreamRead = CreateObject("ADODB.Stream")
	adoStreamRead.Type = adTypeBinary
	adoStreamRead.Open
	adoStreamRead.LoadFromFile szLoadFileName
	bytesRead = adoStreamRead.Read()
	adoStreamRead.Close

	Set objUser = GetObject(szADsPath)
	objUser.Put "thumbnailPhoto", bytesRead
	objUser.SetInfo
End Function

This one is almost the same function, but it does not expect an url as parameter but a filename – the rest of the function stays the same.

When using one of these functions please make sure that the resource identified by either url or filename is a valid picture file like jpeg or png. Neither Active Directory nor my functions will validate the files!
Please also note that the attribute thumbnailPhoto is for thumbnails – therefor you should use small pictures. Recommended are jpeg pictures with dimensions of 128×128 pixels an a size not exceeding 10KB. The limit of the thumbnailPhoto attribute is 100KB.

How to connect to and read data from Mailboxes, Calendars, … via Exchange Web Services and vbScript?

I offen wondered how I can programmatically read items from mailboxes stored within Microsoft Exchange Server. Some time ago I used Outlook with some macros that accessed the information I needed. But what if Outlook does not run? Or what if the computer crashed – who would logon and start Outlook? Sure you can find workarounds for every problem. But this never seemed to be a ‘cool’ solution to me. In the past few weeks – after I got my Exchange 2010 MCITP – I started to have a look at EWS – Exchange Web Services (available since Microsoft Exchange Server 2007). This one seemed to be pretty simple and also a stable solution to all my needs. Here is how I do it all now with vbscript…

There is not much you’ll need to connect to Exchange, just a http connection to your Client Access Server, a user with credentials and access to the mailbox you want to read, a soap xml request and some lines of code that will parse the response for you.

First, you’ll need to know the dns name or the ip of your Client Access Server – in this example I’ll you EXCAS.domain.tld.
Now we create some objects and define the url to the CAS and username and password to access it:

Dim objXmlHttp, xmlDoc

Const strUrl = "https://EXCAS.domain.tld/ews/exchange.asmx" 'URL to CAS
Const strUser = "domain.tld\myexchangeuser" 'Domain and Username to authenticate with
Const strPassword = "myExchangeUSeRsP4ssw0rd"
Const strEmailAddress = "mailboxtoreadfrom@domain.tld" 'Mailbox that you want to read data from

Set objXmlHttp = CreateObject("Microsoft.XMLHTTP") 'this will send the request to EWS
Set objXmlDoc = CreateObject("MSXML2.DOMDocument") 'this will parse the response for us

Now it is time to decide what we want to do, or better: we need to create the request telling EWS what to do. In this example I want to know about every calendar item in a certain time span. But you can do much more:

Since we want to crawl for many items we will use the FindItem Operation. Following this link you’ll get a complete overview how the request will look like. In common every operation that can be used with Exchange Web Service is really well documented!
As you can see the basic xml soap request is:



  
    
      
        IdOnly
      
      
        
      
    
  

We just need to make some modifications to this example – we want to read all properties in the calendar for example, we do want to connect to another mailbox and we do want to use a certain time span not the whole calendar. So the soap request would look like this (changes marked bold):



  
    
      
        AllProperties
      
      
      
        
          
            mailboxtoreadfrom@domain.tld
          
        
      
    
  

Now we need to connect to EWS, post this request, wait for the answer and parse it. Since this is a complex xml structure we just need to break it down to that level the information is stored in – in this example it is the “t_Items” node.

objXmlHttp.open "POST", strUrl, False, strUser, strPassword
objXmlHttp.setRequestHeader "Content-Type", "text/xml"
objXmlHttp.send strXmlData 'this is the soap request from above
If objXmlHttp.Status = "200" Then 'if the request was successful go on
  If objXmlDoc.loadXML(objXmlHttp.responseText) Then
    Set soap_Body = objXmlDoc.documentElement.childNodes.item(1) 'soap_body is the second node below the document root
    Set m_FindItemResponse = soap_Body.childNodes.item(0) 'm_FindItemResponse is the first node below soap_body
    Set m_ResponseMessages = m_FindItemResponse.childNodes.item(0) 'and so on...
    Set m_FindItemResponseMessage = m_ResponseMessages.childNodes.item(0)
    Set m_RootFolder = m_FindItemResponseMessage.childNodes.item(1)
    Set t_Items = m_RootFolder.childNodes.item(0)

    For Each calendarItem In t_Items.childNodes
      Dim t_Subject, t_Start, t_End, t_LegacyFreeBusyStatus, t_Location, t_IsRecurring, t_CalendarItemType, t_MyResponseType, t_Organizer
      t_ItemId = calendarItem.getElementsByTagName("t:ItemId").item(0).getAttribute("Id")
      t_Subject = calendarItem.getElementsByTagName("t:Subject").item(0).text
      t_Start = calendarItem.getElementsByTagName("t:Start").item(0).text
      t_End = calendarItem.getElementsByTagName("t:End").item(0).text
      t_LegacyFreeBusyStatus = calendarItem.getElementsByTagName("t:LegacyFreeBusyStatus").item(0).text
      t_Location = calendarItem.getElementsByTagName("t:Location").item(0).text
      t_IsRecurring = calendarItem.getElementsByTagName("t:IsRecurring").item(0).text
      t_CalendarItemType = calendarItem.getElementsByTagName("t:CalendarItemType").item(0).text
      t_MyResponseType = calendarItem.getElementsByTagName("t:MyResponseType").item(0).text
      t_Organizer = calendarItem.getElementsByTagName("t:Organizer").item(0).getElementsByTagName("t:Mailbox").item(0).getElementsByTagName("t:Name").item(0).text

      'Here you can do anything you like with all this information
    Next
  End If
Else
  wscript.echo "Error: " & objXmlHttp.Status & " - " & objXmlHttp.statusText 'If status is not 200
End If

Documentation for functions, methods and properties used in this post:

Certificate problems with vbscript and xml http calls

May be some of you know the problem with xml http calls, vbscript and secured websites with certificate problems. Better known as error 80072F0D in msxml3.dll: “The certificate authority is invalid or incorrect”.
When opening such a page in Internet Explorer you will see that the browser wants to prevent you from opening that page. But you can go on by selecting “Continue to the website…”. In vbscript this is also possible. Here you’ll need to add an option to your xml http request like this:

Set objXmlHttp = CreateObject("Msxml2.ServerXMLHTTP")
objXmlHttp.setOption 2, 13056
objXmlHttp.open "GET", "https://urlwithcertificateerror", False
objXmlHttp.send
wscript.echo objXmlHttp.responseText
Set objXmlHttp = Nothing

The second line tells the xmlhttp object to ignore any certificate errors and to continue downloading the page. There are some more options you can set using the setoption function, e.g. overriding the codepage or change the handling of % characters.
Please note that the value of 13056 means that ALL errors regarding certificates are ignored. There are some more values you can set to get more control on what will be ignored and what won’t, e.g.:

SXH_SERVER_CERT_IGNORE_UNKNOWN_CA = 256
Unknown certificate authority

SXH_SERVER_CERT_IGNORE_WRONG_USAGE = 512
Malformed certificate such as a certificate with no subject name.

SXH_SERVER_CERT_IGNORE_CERT_CN_INVALID = 4096
Mismatch between the visited hostname and the certificate name being used on the server.

SXH_SERVER_CERT_IGNORE_CERT_DATE_INVALID = 8192
The date in the certificate is invalid or has expired.

SXH_SERVER_CERT_IGNORE_ALL_SERVER_ERRORS = 13056
All certificate errors.

More information about setting and getting options, about these values and options not mentioned here can be found at http://msdn.microsoft.com/en-us/library/ms753798(v=vs.85).aspx

How to do ‘replaceall’ in VBScript?

As you might know the vbscript replace function does always search only “one round”. Here is an example, what exactly I mean:

Lets assume you want to correct the following string by replacing all “double-l’s” by “single-l’s”:

str = "Littllllle worllllllld!"

The code would be:

wscript.echo replace(str, "ll", "l")

But everything this line does is replace all existing “ll” by “l”. So lets have a look what’s left – the output is: Litllle worlllld!
Why? The original string contained 5 l in Little and 7 l in world. The function used replaces every ll, in Little there are 2 ll and 1 l, in world 3 ll and 1 l. After replacing all doubles you still have 3 l left in Little and 4 in world. As you can see replace only replaces existing strings, the lls that occur by replacing will not be touched… So you need to run a second round to replace more lls by ls…

Since I need this one very often, for example when replacing multiple new lines in html, I wrote a little function doing all the work for me. Here it is:

Function ReplaceAll(str, find, replacewith)
While InStr(str, find) > 0
str = Replace(str, find, replacewith)
Wend
ReplaceAll = str
End Function

How to access WMI namespaces on remote computers that require encryption?

When you have a look at my vbscript bitlocker post and try to use it on remote machines you may not get any results but an application eventlog entry similar to this one here:

Event Source: WinMgmt
Event ID: 5605
Access to the [...] namespace was denied. The namespace is marked with RequiresEncryption but the client connection was attempted with an authentication level below Pkt_Privacy. Re try the connection using Pkt_Privacy authentication level.

When using security related namespaces on remote machines you need to connect to wmi using a higher authentication level:

strComputer = "remotemachine"
Set objWMIService = GetObject("winmgmts:{authenticationLevel=pktPrivacy}\\" & strComputer & "\root\CIMV2\Security\MicrosoftVolumeEncryption")

You can use one of these authentication levels:

Name/value Description
WbemAuthenticationLevelDefault

0

Moniker: Default

WMI uses the default Windows Authentication setting. This is the recommended setting that allows WMI to negotiate to the level required by the server returning data. However, if the namespace requires encryption, use WbemAuthenticationLevelPktPrivacy.

WbemAuthenticationLevelNone

1

Moniker: None

Uses no authentication.

WbemAuthenticationLevelConnect

2

Moniker: Connect

Authenticates the credentials of the client only when the client establishes a relationship with the server.

WbemAuthenticationLevelCall

3

Call

Authenticates only at the beginning of each call when the server receives the request.

WbemAuthenticationLevelPkt

4

Moniker: Pkt

Authenticates that all data received is from the expected client.

WbemAuthenticationLevelPktIntegrity

5

Moniker: PktIntegrity

Authenticates and verifies that none of the data transferred between client and server has been modified.

WbemAuthenticationLevelPktPrivacy

6

Moniker: PktPrivacy

Authenticates all previous impersonation levels and encrypts the argument value of each remote procedure call. Use this setting if the namespace to which you are connecting requires an encrypted connection.

Source: MSDN Library

How to get some information on Bitlocker using VBScript and WMI?

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\CIMV2\Security\MicrosoftVolumeEncryption")
Set colItems = objWMIService.ExecQuery("SELECT * FROM Win32_EncryptableVolume",,48)

Dim arEncryptionMethod
arEncryptionMethod = Array("None", "AES 128 With Diffuser", "AES 256 With Diffuser", "AES 128", "AES 256")

Dim arProtectionStatus
arProtectionStatus = Array("Protection Off", "Protection On", "Protection Unknown")

Dim arConversionStatus
arConversionStatus = Array("Fully Decrypted", "Fully Encrypted", "Encryption In Progress", "Decryption In Progress", "Encryption Paused", "Decryption Paused")

Dim arLockStatus
arLockStatus = Array("Unlocked", "Locked")

For Each objItem in colItems
  Dim EncryptionMethod
  Dim ProtectionStatus
  Dim ConversionStatus
  Dim EncryptionPercentage 'Percentage of the volume that is encrypted
  Dim VolumeKeyProtectorID
  Dim LockStatus

  objItem.GetEncryptionMethod EncryptionMethod
  objItem.GetProtectionStatus ProtectionStatus
  objItem.GetConversionStatus ConversionStatus, EncryptionPercentage
  objItem.GetKeyProtectors 0,VolumeKeyProtectorID
  objItem.GetLockStatus LockStatus

  WScript.Echo "DeviceID: " & objItem.DeviceID
  Wscript.Echo "DriveLetter: " & objItem.DriveLetter
  Wscript.Echo "EncryptionMethod: " & arEncryptionMethod(EncryptionMethod)
  Wscript.Echo "ProtectionStatus: " & arProtectionStatus(ProtectionStatus)
  Wscript.Echo "ConversionStatus: " & arConversionStatus(ConversionStatus)
  Wscript.Echo "EncryptionPercentage: " & EncryptionPercentage & "%"
  Wscript.Echo "LockStatus: " & arLockStatus(LockStatus)

  For Each objId in VolumeKeyProtectorID
    Dim VolumeKeyProtectorFriendlyName
    objItem.GetKeyProtectorFriendlyName objId, VolumeKeyProtectorFriendlyName
    If VolumeKeyProtectorFriendlyName <> "" Then
      Wscript.Echo "KeyProtectors: " & VolumeKeyProtectorFriendlyName
    End If
  Next
Next

Documentation for functions, methods and properties used in this post: