Tag: vbScript

Retrieving Password from Application Pool

I came across an undocumented app the other day. For a number of reasons, we needed to restore the password but it wasn’t documented anywhere. Luckily, the service account was setup in an app pool. In IIS 7.0 or 7.5, APPCMD can be used to recover the password. In 6.0, adsutil.vbs can be used.

cscript.exe /nologo adsutil.vbs GET W3SVC/AppPools/AppPoolName/WAMUserPass

However, I wanted to write my own little script. Having a little tidbit makes it easy to reuse later for other clients. For example, I could search AD for SPNs starting with “HTTP”, loop through each of their app pools and document the username and passwords for all service accounts used in this fashion. So, here is the little tidbit I threw together.

Option Explicit

Call GetAppPoolUserAndPass("localhost", "ApplicationPoolName")

Private Sub GetAppPoolUserAndPass (byVal strComputer, byVal strAppPool)
	Dim appPool
	On Error Resume Next
	Set appPool = GetObject("IIS://" & strComputer & "/w3svc/AppPools/" & strAppPool)
	If Err Then
		wscript.echo "Error connecting to " & chr(34) & strAppPool & chr(34) & " on " & strComputer
		wscript.echo strAppPool & vbTab & appPool.WAMUserName & vbTab & appPool.WAMUserPass
	End If
	On Error GoTo 0
End Sub

Here is an example of just what I mentioned above. YMMV but this should discover IIS boxes and report all the accounts used in their app pools. Note: Pools using built-in accounts will show up with blank passwords; this is normal; the password isn’t actually blank.

Option Explicit

' Determine DNS domain name.
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Determine DNS domain name.
Dim objRootDSE, strDNSDomain
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory.
Dim adoCommand, adoConnection
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = adoConnection

' Build Query
Dim strBase, strFilter, strAttributes, strQuery
strBase = ""
strFilter = "(servicePrincipalName=HTTP*)" 'Search for SPN starting w/ HTTP (case insensitive)
strAttributes = "name"
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False

Dim adoRecordset
Set adoRecordset = adoCommand.Execute

If (adoRecordset.EOF = True) Then
    Wscript.Echo "No SPNs Found matching HTTP*"
End If

Wscript.Echo "Computer Name" & vbTab & "AppPool Name" & vbTab & "User Name" & vbTab & "User Password"

Do Until adoRecordset.EOF
    Call GetApplicationPools(adoRecordset.Fields("name").Value & "." & strDNSDomain)

' Clean up.

Private Sub GetApplicationPools (byVal strComputer)
	Dim objWMIService, colItems, objItem
	On Error Resume Next
	Set objWMIService = GetObject("winmgmts:{authenticationLevel=pktPrivacy}\" & strComputer & "rootmicrosoftiisv2")
	Set colItems = objWMIService.ExecQuery("Select * from IIsApplicationPoolSetting")
	If Err Then
		wscript.echo "Error connecting to " & strComputer
		For Each objItem in colItems
			Wscript.Echo strComputer & vbTab & objItem.Name & vbTab & objItem.WAMUserName & vbTab & objItem.WAMUserPass
	End If
End Sub

The Power of Scripting: Finding Morto.A

Here I go on another vbScript tutorial.  You might ask why I’m not doing this in powershell yet and it is simple: I still run into 2003 and XP environments. Oh yeah, and this works. I don’t care what scripting language I’m writing in if it gets the job done; you shouldn’t either. My $0.02. If you want to download this script, click here: Morto.A Detection Script.

A had to do a little cleanup on a network from the Morto.A worm.  The first thing I wanted to do was find out how bad things were.  They were reporting a DDOS across their LAN (mostly 3389) and a lot of other issues.  It as obvious we were going to need to rebuild a few systems but we wanted to get a grasp out of what the damage was.  This were generally working: logons, shares, etc.

I’m not going to go over what we did on the firewall or local network but discuss a quick script I threw together to scan the network for infected systems.  The trick was this network had multiple domains and several computers that weren’t even domain joined.

So, I put my automation hat on and threw together a script I will outline below.  I split this into sections to help you see how you can use scripting to piece together a complicated job in simple tasks.  It is pretty basic.  I start by defining what I want to do:

Problem statement: I need to check every computer in each domain for infection of the Morto.A worm.  I can split this into three distinct steps:

  1. For Each Domain
  2. Get Each Computer
  3. Check for Morto.A

For Each Domain

This is pretty simple.  There are advanced techniques for finding all the domains in a forest or reading from a text file or even a spreadsheet.  I like to keep it simple and just create an array.

Dim arrDomains
arrDomains = Array("domain1.domain.local", "domain2.domain.local", "domain3.domain.local")
For Each Domain in arrDomains
	Call ComputersAll(Domain)

Get Each Computer

Now, I need to build the “ComputersAll” sub-routine.  I call this in the domain script by passing each domain in DNS format.  I used DNS domains in my example on purpose (you could use the DistinguishedName format “dc=domain1,dc=domain,dc=local” but I prefer DNS names for this purpose).  By default, each computer in that domain is going to register itself in DNS (assuming you set it up properly).  I want to get the computer name out of AD and append the dns domain name.  Once that is done, I want to use my “IsConnectible” script to check and make sure it is online.  If it pings, I should be able to carry out my business and find out if it is infected.

The real magic is in the lines: strBase, strAttrs and strFilter.  strBase sets the search base to the domain you sent the sub-routine.  It isn’t going to go outside this domain for its search but it will search the whole domain due to its use of subtree.  The search a specific OU, you have to use the DN format and that is out of scope of this article.  The strAttrs is pretty straight forward.  This is an array of attributes you want returned.  You could do DNSDomainName but some clients set this improperly.  It is safe to assume (in most cases) that the name and the domain name comprise the DNS domain name.  The strFilter isn’t doing much but may look complex.  Basically, it is looking for computers in AD that aren’t disabled.

The other things to pay attention to is the line after

Do Until ADORecordSet.EOF

.  This checks first if the computer is on the network through my “IsConnectible” script.  It passes the FQDN by appending “.” and the domain name to the name attribute from the query.

Const adUseClient = 3
Private Sub ComputersAll(byVal strDomain)
	Set adoCommand = CreateObject("ADODB.Command")
	Set adoConnection = CreateObject("ADODB.Connection")
	adoConnection.Provider = "ADsDSOObject"
	adoConnection.cursorLocation = adUseClient
	adoConnection.Open "Active Directory Provider"
	adoCommand.ActiveConnection = adoConnection

	strBase   =  ";"
	strFilter = "(&(objectClass=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2));"
	strAttrs  = "name;"
	strScope  = "subtree"
	strQuery = strBase & strFilter & strAttrs & strScope
	adoCommand.CommandText = strQuery
	adoCommand.Properties("Page Size") = 5000
	adoCommand.Properties("Timeout") = 30
	adoCommand.Properties("Cache Results") = False

	Set adoRecordset = adoCommand.Execute

	If adoRecordset.RecordCount > 0 Then
		' wscript.echo "Computers for " & strDomain & ": " & adoRecordset.RecordCount
		' Loop through every single computer in the domain
		Do Until adoRecordset.EOF
			If IsConnectible(adoRecordset.Fields("name").Value & "." & strDomain) = "Online" Then Call DetectMorto(adoRecordset.Fields("name").Value & "." & strDomain)
	End If
End Sub

Check For Morto.A

This is the tricky part. I used several sources and found two distict characterisitcs of this work:

  1. C:windowssystem32sens32.dll
  2. HKLMSystemWPA
    1. Key: sr
    2. Value: Sens

So, I need to check and see if a file and registry key exists in order to detect if the machine is infected.  There are a couple of caveats with this: 1) when checking if a file exists, the default response is true on error, 2) you need local admin rights to query the c$ share, and 2) the registry check relies on the remote registry service.  To overcome these caveats, we put error checking in.  If an error occurs, it assumes a manual check is needed.  We do this to err on the side of caution.  Of course, on a large network, this isn’t desirable.  Feel free to contact me if you need more advanced help.  Here is the sub-routine I came up with to do all of the above.  It is mean and nasty and isn’t exactly the kind of finesse I prefer but, it works…

First, I try writing a temp file (and deleting it if it succeeds). If that works, I should be able to detect if the file exists and connect to the remote registry. However, perhaps you are an admin that is paranoid and disables the remote registry service. Well, it will still detect an error and tell you to check manually. Of course, if you don’t have admin rights, it will also tell you to check remotely.

Private Sub DetectMorto(byVal strComputer)
	' Detects morto via several methods...
	Dim blnInfected : blnInfected = False
	' Requires admin rights to properly detect. Check for rights
	Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")
	On Error Resume Next
	Dim TempFile : Set TempFile = objFSO.CreateTextFile("\" & strComputer & "c$WINDOWStempfile.deleteme", True)
	TempFile.WriteLine("This is a test.")
	Set TempFile = Nothing
	objFSO.DeleteFile("\" & strComputer & "c$WINDOWStempfile.deleteme")	
	If Err Then bnlInfected = "Error"
	On Error GoTo 0
	' Check for registry key placed by Morto...
	Dim strKeyPath, strEntryName, objReg, strValue
	strKeyPath = "SYSTEMWPA"
	strEntryName = "sr"
	On Error Resume Next
	Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\" & strComputer & "rootdefault:StdRegProv")
	objReg.GetStringValue HKEY_LOCAL_COMPUTER, strKeyPath, strEntryName, strValue

	If Err Then blnInfected = "Error"
	If strValue = "Sens" Then blnInfected = True
	' Check for file indicating morto infection
	Dim strMortoSens32
	strMortoSens32 = "\" & strComputer & "c$WINDOWSsystem32Sens32.dll"
	If objFSO.FileExists(strMortoSens32) Then blnInfected = True
	' If the Infected flag is set, it is infected...
	If blnInfected = True Then
		wscript.echo "*******MORTO INFECTION: " & strComputer
	ElseIf blnInfected = "Error" Then
		wscript.echo "Error (Check Manually): " & strComputer
		wscript.echo "Clean: " & strComputer
	End If
End Sub


So, I have taken several scripts and thrown them together to find a solution. I can reuse this for anything. Perhaps I want to use vbscript to find computers with a certain file on their hard drive. I can reuse this by just tweaking the Morto sub-routine.

IsConnectible: My vbScript Ping Method

Whenever I am doing large sweeps of the network that require connecting to a large number of workstations (e.g. file copy, wmi query, etc.), I prefer to check to see if I can even see the system. This avoids waiting for (WMI) timeouts and also aids in troubleshooting failures. If the file copy failed, why? Well, if I can’t ping it or it can’t be resolved, I would like to know right away and move on to the next host.

Of course, there are a couple downsides to this method. It does add overhead to the script because it too has a timeout. However, depending on the purpose of the script, this may be acceptable for the flexibility you gain. The other caveat is that the systems you run this against must allow ICMP on their local firewall or the script will just ignore them and move on to the next host.

There are several methods for pinging hosts but I’ve found this to be the most reliable since it works against any system that allows ICMP, even Linux or Macs. This is adapted from Richard Mueller’s ping script. This method will return three possible values: “Online”, “No Ping Reply”, or “No DNS/WINS Entry”. You can also tweak the ping command options to your liking.

Here is an example of how to call the function:

Dim computers(2), computer, pingable
computers(0) = "pc-100.domain.local"
computers(1) = "pc-200.domain.local"
comptuers(2) = "pc-300.domain.local"
For Each computer In computers
	Select Case IsConnectible(computer)
		Case "Online"
			wscript.echo computer & " is online"
		Case "No Ping Reply"
			wscript.echo computer & " is offline or firewall blocks ICMP"
		Case "No DNS/WINS Entry"
			wscript.echo computer & " cannot be found in DNS/WINS"
		Case "Host Unreachable"
			wscript.echo computer & " is unreachable"
	End Select

Here is the function:

Private Function IsConnectible(ByVal strComputer)
	' Uses ping.exe to check if computer is online and connectible.
	' Adapted from http://www.rlmueller.net/Programs/Ping1.txt
	Dim objShell, objExecObject, strText
	Set objShell = CreateObject("Wscript.Shell")
	' use ping /? to find additional values for ping command; see -n and -w
	Set objExecObject = objShell.Exec("%comspec% /c ping -n 2 -w 750 " & strComputer)
	Do While Not objExecObject.StdOut.AtEndOfStream
		strText = strText & objExecObject.StdOut.ReadLine()
	If InStr(strText,"could not find host") > 0 Then
		IsConnectible = "No DNS/WINS Entry"
	ElseIf (InStr(strText,"Reply from ") > 0) And (InStr(strText,": bytes=") > 0) Then
		IsConnectible = "Online"
	ElseIf InStr(strText,"Destination host unreachable") > 0 Then
		IsConnectible = "Host Unreachable"		
		IsConnectible = "No Ping Reply"
	End If
End Function

vbScript: Adding and Removing a Domain Group to a Local Group

Yes, I still use vbscript. Someday, I’ll get to work in an environment where everything is upgraded. Until then, I have to rely on the tried and true vbscript.

One of the most common uses of a Group Policy startup script is for adding users to the local admin group. Just google it and you will find hundreds of scripts doing just that, batch files, posh, vbscript, perl, etc. I wrote the script below because I wanted the flexibility to reuse this script at any client and for any group (not just Administrators but Remote Desktop Users or Power Users).

The config section takes three arguements: Action, strLocalGroup, strDomainGroup.

  • Action: Can be either “Add” or “Remove”. It will either add the domain group to the local group or remove it.
  • strLocalGroup: The name of the local group (e.g. Administrators, Power Users, etc.). I tested w/ all the standard built-in groups.
  • strDomainGroup: The name of the domain group to add to the local group. Note: The workstation has to be a member of the same domain the group resides in.

Download the script (zipped): add-remove-domain-group-to-local-group

' VBScript Source File
' NAME: Add/Remove Domain Group to Local Group
' AUTHOR: Andrew J Healey
' DATE  : 2011.07.08
' COMMENT: Will add or remove the domain group specified 
'	  to/from the local group specified.
' USAGE: Modify the config section to match your env. 
'	The "Action" can be "Remove" or "Add"

Option Explicit
Dim strDomainGroup, strLocalGroup, Action

'--------- START CONFIG SECTION ---------
Action = "Add" ' or Remove
strLocalGroup = "Administrators"
strDomainGroup = "Local-Workstation-Admins"
'--------- END CONFIG SECTION ---------

' Enable error handling routine to ensure startup
' script doesn't throw error at users
On Error Resume Next

Dim strDomain, strComputer
Dim objNetwork, objLocalGroup, objDomainGroup

Set objNetwork = CreateObject("WScript.Network") 
strDomain = objNetwork.UserDomain
strComputer = objNetwork.ComputerName

Set objLocalGroup = GetObject("WinNT://" & _
		 strComputer & "/" & strLocalGroup) 
Set objDomainGroup = GetObject("WinNT://" & _
		 strDomain & "/" & strDomainGroup)

' Do Work
Select Case Action
	Case "Remove"
	Case "Add"
End Select

' Clean up objects
Set objDomainGroup = Nothing
Set objLocalGroup = Nothing 
Set objNetwork = Nothing

vbScript: Tweaking Power Settings (disabling hibernate and standby)

As is often the case in IT, when you need to push out that software package or migrate that computer to a new domain, it isn’t on the network.  This has come up several times in the past year and I wanted to share my solution.  Now, this isn’t the “greenest” solution because this will ensure your clients never go into a power saving mode.  However, it can be a temporary fix for a project.  It can also be adapted to force standby or hibernate at specific thresholds.

While Windows 7 and Vista make this task simple, XP is still a reality in most enterprises and SMB’s and therefore, must be taken into account.  The script below does just that.  Some additional examples can be found at the US Government’s Energy Star website.

Note: Copying the script from the webpage may cause formatting issues.  You can download the script here: tweak-power-settings.vbs
Read more

.Net Classes within VBScript: Doing Randomness and Arrays the Easy Way

.Net and VBScript can play together
Back in 2007, the Microsoft Scripting Guys posted a article titled “Hey, Scripting Guy! Be Careful What You Say.” This article changed everything in the way I scripted because it should how simply you can access some .Net classes through COM callable wrappers. The two they focus on are “System.Random” and “System.Collections.ArrayList”.

Set objRandom = CreateObject("System.Random")
Set objArrList = CreateObject("System.Collections.ArrayList")

Read more

vbScript: Quickly determine architecture

I’ve been using a routine to determine 64-bit v 32-bit workstations for some time checking the registry for the PROCESSOR_ARCHITECTURE in the HKLMSYSTEMCurrentControlSetControlSession ManagerEnvironment path. However, this was proving to be error prone. So, I just gave up that method altogether since all Windows x64 editions have a “%SystemDrive%Program Files (x86)” directory. This makes it just a quick and easy call the folderexists method of the filesystemobject.

The only downside is that can’t be used remotely but since most of my scripts are used in local policies, this shouldn’t be an issue.


Private Function is64bit()
	Dim filesys : Set filesys = CreateObject("Scripting.FileSystemObject")
	Dim bln64bit : bln64bit = False
	If filesys.FolderExists("C:Program Files (x86)") then bln64bit = True
	is64bit = bln64bit
End Function

Installing Java via Script and Group Policy

Due to some software requirements, there was a need to get JRE 1.5.0_09 rolled out across our enterprise. The requirements were pretty straight forward:

  • Only install on client operating systems (Windows 2000, Windows XP, Windows Vista and Windows 7)
  • Detect the versions of Java installed. If 1.5.0_09 is installed, exit.  If 1.5.0_08 or less was installed, install this version.  If it has a newer version, do nothing.

The best way of determining the Java versions is to look in %program files%.  On 64-bit machines, this is “C:program files (x86)Java”.  On 32-bit, this is “C:program filesJava”.  The script accounts for this.

I wanted to post this because several of the functions used are very useful.  The share hosting the jre runtime needs to have wide open read-only access so the Local System account can access share (Domain Computers).  This script can then be applied to machine accounts in group policy as a startup script.  If you want to test this, just comment out line 111.

Download Compressed (.zip) script

' VBScript Source File
' NAME: Java Runtime Environment Installation
' AUTHOR: Andrew J Healey
' DATE  : 2010.07.15
' COMMENT: This script will install the jre references based on processor, existing 
' 				   installations, and operating system.  This script is to be run at startup
'				   under the Local System account. No user interaction is required for 
'				   this script to work properly.

Option Explicit

If isClientOperatingSystem = False Then wscript.quit

Dim jreVerMajor, jreVerMinor
Dim strCommand, strPathToInstall, strInstallFile, strArguments

'============== BEGIN CONFIGURATION SECTION =================
jreVerMajor = "jre1.5.0_" 'As string
jreVerMinor = 9 'As Integer for <> operations
strPathToInstall = "\servernameSoftwareJava" 'Point to share \servernamesharefolder
strInstallFile = "jre-1_5_0_09-windows-i586-p.exe"
strArguments = "/s /v /qn ADDLOCAL=jrecore,extra IEXPLORER=1 REBOOT=Suppress JAVAUPDATE=0 SYSTRAY=0 WEBSTARTICON=0"
strCommand = strPathToInstall & strInstallFile & " " & strArguments
'============== END CONFIGURATION SECTION =================

If checkForJRE(jreVerMajor, jreVerMinor) = False Then
	Call InstallJava(strCommand)
End If

Private Function checkForJRE(ByVal jreVerMajor, ByVal jreVerMinor)
	Dim jrePath
	Dim blnMajorFound : blnMajorFound = False
	Dim blnMinorFound : blnMinorFound = False
	If is32bit Then
		jrePath = "C:Program FilesJava"
		jrePath = "C:Program Files (x86)Java"
	End If
	On Error Resume Next
		Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")
		Dim objFolder : Set objFolder = objFSO.GetFolder(jrePath)
		Dim colSubfolders : Set colSubfolders = objFolder.Subfolders
		Dim objSubfolder
		For Each objSubfolder in colSubfolders
			If Left(objSubfolder.Name,Len(jreVerMajor)) = jreVerMajor Then
				blnMajorFound = True
				If CInt(Right(objSubfolder.Name,2)) >= jreVerMinor Then
					blnMinorFound = True
				End If
			End If
		If Err.Number <> 0 Then
			chechForJRE = True
			Exit Function
		End If
		If blnMajorFound = False And blnMinorFound = False Then
			checkForJRE = False
			checkForJRE = True
		End If
	On Error GoTo 0
	Set objSubfolder = Nothing
	Set colSubfolders = Nothing
	Set objFolder = Nothing
	Set objFSO = Nothing
	jrePath = Empty
	blnMajorFound = Null
	blnMinorFound = Null
	jreVerMajor = Empty
	jreVerMinor = Empty
End Function 

Private Function is32bit()
	'Get processor architecture; do not use remotely
	const HKEY_LOCAL_MACHINE = &H80000002
	Dim oReg,strKeyPath,strValueName
	Dim strValue
	On Error Resume Next
		Set oReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\.rootdefault:StdRegProv")

		strKeyPath = "SYSTEMCurrentControlSetControlSession ManagerEnvironment"
		oReg.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue

		If Err.Number <> 0 or strValue = "x86" Then
			is32bit = True
			is32bit = False
		End If
	On Error GoTo 0
	Set oReg = Nothing
	strKeyPath = Empty
	strValueName = Empty
End Function 

Private Function InstallJava(ByVal strCommand)
	On Error Resume Next
		Dim objWshShell, intRC

		Set objWshShell = WScript.CreateObject("WScript.Shell")
		intRC = objWshShell.Run(strCommand, 0, True)

		If intRC <> 0 Or Err.Number <> 0 Then
			InstallJava = "Failed"
			InstallJava = "Success"
		End If
	On Error GoTo 0
	Set objWshShell = Nothing
	intRC = Empty
End Function 

Private Function isClientOperatingSystem()
	Dim objWMIService, objItem, colItems
	Dim strOS

	On Error Resume Next
		' WMI Connection to the object in the CIM namespace
		Set objWMIService = GetObject("winmgmts:\.rootcimv2")

		' WMI Query to the Win32_OperatingSystem
		Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

		' For Each... In Loop (Next at the very end)
		For Each objItem in colItems
			strOS = objItem.Caption
		If InStr(strOS,"Windows 7") <> 0 Or InStr(strOS,"XP") <> 0 Or InStr(strOS,"2000 Professional") <> 0 Or InStr(strOS,"Vista") <> 0 Then
			isClientOperatingSystem = True
			isClientOperatingSystem = False
		End If
		If Err.Number <> 0 Then isClientOperatingSystem = False
		strOS = Empty
		Set objItem = Nothing
		Set colItems = Nothing
		Set objWMIService = Nothing
	On Error GoTo 0
End Function 

Part 1: Blocking Bad Hosts – Finding Them, Easily

Download Script: get-bad-hosts.zip

While troubleshooting some issues on an OWA Front-End server, I went over to the security log to see if the authentication attempts were getting past this box. The problem I found was the log was so full of failed logon attempts it was difficult to filter out what I was looking for. In a twelve hour period, there were thousands of 529 events in the security log. Now, I know this is nothing new, but I found a few patterns. I manually exported the log to a CSV, parsed out all the source ip addresses and opened it up in Excel. What I found was that 98.7% of failed logon attempts were made by just four different ip addresses.  (I recommend using MaxMind’s GeoIP Address Locator for help in determining where the source addresses are located.) Read more

Logon Script: Move Local PST Files To Network Share

Download Script: move-pst-to-network.zip

So, my buddy (and former co-worker) called me yesterday for some help with a script he put together.  His script checked the local profile in Outlook for any PST files that were stored locally.  If it found any, it would them move them to the users home space.  We tried and tried to get the script to work properly but it never seemed to work 100%.  Being that he is a good friend and this would be useful at work, I decided to take the work he had put in and get the thing working. Read more