The Power of Scripting: Finding Morto.A

Here I go on anoth­er vbScript tuto­r­i­al.  You might ask why I’m not doing this in pow­er­shell yet and it is sim­ple: I still run into 2003 and XP envi­ron­ments. Oh yeah, and this works. I don’t care what script­ing lan­guage I’m writ­ing in if it gets the job done; you should­n’t either. My $0.02. If you want to down­load this script, click here: Morto.A Detec­tion Script.

A had to do a lit­tle cleanup on a net­work from the Morto.A worm.  The first thing I want­ed to do was find out how bad things were.  They were report­ing a DDOS across their LAN (most­ly 3389) and a lot of oth­er issues.  It as obvi­ous we were going to need to rebuild a few sys­tems but we want­ed to get a grasp out of what the dam­age was.  This were gen­er­al­ly work­ing: logons, shares, etc.

I’m not going to go over what we did on the fire­wall or local net­work but dis­cuss a quick script I threw togeth­er to scan the net­work for infect­ed sys­tems.  The trick was this net­work had mul­ti­ple domains and sev­er­al com­put­ers that weren’t even domain joined.

So, I put my automa­tion hat on and threw togeth­er a script I will out­line below.  I split this into sec­tions to help you see how you can use script­ing to piece togeth­er a com­pli­cat­ed job in sim­ple tasks.  It is pret­ty basic.  I start by defin­ing what I want to do:

Prob­lem state­ment: I need to check every com­put­er in each domain for infec­tion of the Morto.A worm.  I can split this into three dis­tinct steps:

  1. For Each Domain
  2. Get Each Com­put­er
  3. Check for Morto.A

For Each Domain

This is pret­ty sim­ple.  There are advanced tech­niques for find­ing all the domains in a for­est or read­ing from a text file or even a spread­sheet.  I like to keep it sim­ple and just cre­ate 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 “Com­put­er­sAll” sub-rou­tine.  I call this in the domain script by pass­ing each domain in DNS for­mat.  I used DNS domains in my exam­ple on pur­pose (you could use the Dis­tin­guished­Name for­mat “dc=domain1,dc=domain,dc=local” but I pre­fer DNS names for this pur­pose).  By default, each com­put­er in that domain is going to reg­is­ter itself in DNS (assum­ing you set it up prop­er­ly).  I want to get the com­put­er name out of AD and append the dns domain name.  Once that is done, I want to use my “IsCon­nectible” script to check and make sure it is online.  If it pings, I should be able to car­ry out my busi­ness and find out if it is infect­ed.

The real mag­ic is in the lines: str­Base, strAt­trs and str­Fil­ter.  str­Base sets the search base to the domain you sent the sub-rou­tine.  It isn’t going to go out­side this domain for its search but it will search the whole domain due to its use of sub­tree.  The search a spe­cif­ic OU, you have to use the DN for­mat and that is out of scope of this arti­cle.  The strAt­trs is pret­ty straight for­ward.  This is an array of attrib­ut­es you want returned.  You could do DNS­Do­main­Name but some clients set this improp­er­ly.  It is safe to assume (in most cas­es) that the name and the domain name com­prise the DNS domain name.  The str­Fil­ter isn’t doing much but may look com­plex.  Basi­cal­ly, it is look­ing for com­put­ers in AD that aren’t dis­abled.

The oth­er things to pay atten­tion to is the line after

Do Until ADORecordSet.EOF

.  This checks first if the com­put­er is on the net­work through my “IsCon­nectible” script.  It pass­es the FQDN by append­ing “.” 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 sev­er­al sources and found two dis­tict char­ac­ter­isitcs of this work:

  1. C:windowssystem32sens32.dll
  2. HKLM­Sys­temW­PA
    1. Key: sr
    2. Val­ue: Sens

So, I need to check and see if a file and reg­istry key exists in order to detect if the machine is infect­ed.  There are a cou­ple of caveats with this: 1) when check­ing 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 reg­istry check relies on the remote reg­istry ser­vice.  To over­come these caveats, we put error check­ing in.  If an error occurs, it assumes a man­u­al check is need­ed.  We do this to err on the side of cau­tion.  Of course, on a large net­work, this isn’t desir­able.  Feel free to con­tact me if you need more advanced help.  Here is the sub-rou­tine I came up with to do all of the above.  It is mean and nasty and isn’t exact­ly the kind of finesse I pre­fer but, it works…

First, I try writ­ing a temp file (and delet­ing it if it suc­ceeds). If that works, I should be able to detect if the file exists and con­nect to the remote reg­istry. How­ev­er, per­haps you are an admin that is para­noid and dis­ables the remote reg­istry ser­vice. Well, it will still detect an error and tell you to check man­u­al­ly. Of course, if you don’t have admin rights, it will also tell you to check remote­ly.

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 tak­en sev­er­al scripts and thrown them togeth­er to find a solu­tion. I can reuse this for any­thing. Per­haps I want to use vbscript to find com­put­ers with a cer­tain file on their hard dri­ve. I can reuse this by just tweak­ing the Mor­to sub-rou­tine.