Poking Around DNS Scavenging Settings with Powershell

I've been working toward getting DNS scavenging turned on in a domain.  DNS scavenging, as you may know, takes a good deal of patience and forethought.  It's not something you want to just blindly enable without doing any reconnaissance first.  First off, since I'm new to this environment, let me scan all the domain controllers (which are also the DNS servers in this case) and see what the scavenging and aging settings currently look like:
 
#
$Servers = @()
ForEach ($DC In Get-ADDomainController -Filter *)
{
  $Server = New-Object PSObject -Property @{ 
     Hostname   = $DC.HostName
     Scavenging = $((Get-DnsServerScavenging `
                        -ComputerName $DC.HostName).ScavengingState)
     Aging      = $((Get-DnsServerZoneAging `
                        -Name 'acme.com' `
                        -ComputerName $DC.HostName).AgingEnabled)
    }
  $Servers += $Server
}

$Servers | FT -AutoSize
#
Hostname Scavenging Aging
-------- ---------- -----
DC01     False      True
DC02     False      True
DC03     False      True
DC04     False      True
DC05     False      True

So record aging is already turned on for the zone.  All that's left to do is enable scavenging on one of the DNS servers. (I don't like having all of the domain controllers scavenging - just one.)  But before I do that, I want to wait a while (like, a couple weeks) and see what hosts are updating their DNS records and which ones aren't. Lucky us - DNS server has a WMI provider.

#
$Records = Get-WmiObject 
              -Namespace 'Root\MicrosoftDNS' 
              -Query 'SELECT * FROM MicrosoftDNS_ResourceRecord WHERE Timestamp != 0'

$Records | Select TextRepresentation, `
           @{n='Timestamp'; e={([DateTime]'1/1/1601').AddHours($_.Timestamp)}} `
         | Where Timestamp -LT (Get-Date).AddDays(-30) | FT -AutoSize

The only tough bit is that the record's timestamp comes as a 32-bit integer that represents the number of hours elapsed since January 1st, 1601.  So you'd want to convert that into a meaningful date.  Now we can see which resource records in DNS aren't refreshing themselves on a regular basis.  After checking that list for sanity and correcting any problems, we can turn on scavenging. 

Get-Quotation: Something For Your Powershell Profile

I was playing around with my Powershell profile (again,) and I wanted to put a message-of-the-day style gizmo in there.  Now, a random quotation greets me every time I open Powershell:

  • Where do the quotes come from?

The function downloads them from www.quotationspage.com using Invoke-WebRequest.

  • What if I am not connected to the internet?

The function locally stores each unique quotation that it downloads. In the event that you are disconnected from the internet, the function will simply draw one of the locally cached quotes at random.

  • Where is my Powershell profile stored?

$Env:HOMEPATH\Documents\WindowsPowershell\Microsoft.Powershell_profile.ps1

  • Can I use this outside of my Powershell profile?

Sure.  It is just a function named Get-Quotation.

Here it is:

(Edit: A couple hours later, added wordwrap)

#
Function Get-Quotation
{
    Set-StrictMode -Version Latest

    $Form = @{'number'='1'; 
              'collection[0]'  = 'devils';
              'collection[1]'  = 'mgm';
              'collection[2]'  = 'motivate';
              'collection[3]'  = 'classic';
              'collection[4]'  = 'coles';
              'collection[5]'  = 'lindsly';
              'collection[6]'  = 'poorc';
              'collection[7]'  = 'altq';
              'collection[8]'  = '20thcent';
              'collection[9]'  = 'bywomen';
              'collection[10]' = 'contrib'}
    
    [String[]]$FormattedQuote = @()
    [Int]$MaxWidth = 0
    If ($Host.Name -EQ 'ConsoleHost')
    {
        $MaxWidth = $Host.UI.RawUI.WindowSize.Width
    }
    Else
    {
        $MaxWidth = 80
    }

    Try
    {
        $Page = Invoke-WebRequest http://www.quotationspage.com/random.php3 -Method Post -ContentType 'application/x-www-form-urlencoded' -Body $Form -ErrorAction Stop -TimeoutSec 5 -MaximumRedirection 0

        Foreach ($Element In $Page.AllElements)
        {
            If ($Element.tagName -EQ 'DL')
            {
                [String[]]$PreFormattedQuote = $Element.outerText -Split [Environment]::NewLine                

                For ($Index = 0; $Index -LT $PreFormattedQuote.Count; $Index++)
                {
                    If (($PreFormattedQuote[$Index].Length -GT 0) -AND -Not($PreFormattedQuote[$Index].Contains('More quotations on:')))
                    {
                        $FormattedQuote += $PreFormattedQuote[$Index]
                    }                    
                }
                $FormattedQuote[-1] = "`t-- $($FormattedQuote[-1])"                
            }
        }

        $Hasher = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
        $Hashed = $Hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($FormattedQuote[0]))
        [String]$HashString = [BitConverter]::ToString($Hashed).Replace('-', $Null)

        If (-Not(Test-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') -PathType Container))
        {
            New-Item (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') -ItemType Directory | Out-Null
        }

        If (-Not(Test-Path (Join-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') $HashString) -PathType Leaf))
        {            
            $FormattedQuote | Out-File (Join-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') $HashString)
        }
    }
    Catch
    {
        Write-Warning "Failed to get quotation from www.quotationspage.com. ($($_.Exception.Message))"

        $FormattedQuote = Get-Content ((Get-ChildItem (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') | Get-Random).FullName)
    }

    # Word wrap!
    [Int]$Column = 0
    Foreach ($Line in $FormattedQuote)
    {
        If ($FormattedQuote.IndexOf($Line) -EQ ($FormattedQuote.Count -1))
        {
            Write-Host "`n$Line" -ForegroundColor DarkGray
            Continue
        }
        
        [String[]]$Words = $Line -Split ' '
        Foreach ($Word In $Words)
        {
            # Strip any control characters from the word.
            $Word = $Word.Replace('`r', $Null).Replace('`n', $Null).Replace('`t', $Null)

            $Column += $Word.Length + 1   
            If ($Column -GT ($MaxWidth - 8))
            {
                Write-Host
                $Column = 0
            }
            Write-Host "$Word " -NoNewline -ForegroundColor DarkCyan            
        }
        
    }
    Write-Host
}

Get-Quotation

TinyWebRedirector

I wrote something this weekend.

TinyWebRedirector v1.0 - Redirects HTTP Requests
Copyright (C) 2015 Joseph Ryan Ries
www.myotherpcisacloud.com

Usage:
Install:   TinyWebRedirector -install
Uninstall: TinyWebRedirector -uninstall

I wrote this micro web server for all the sysadmins out there who have an internal Active Directory that shares the same DNS name as their public domain name. Let's say your internal AD domain name is contoso.com. Your public website is also contoso.com. In this scenario, internal employees at the office cannot reach your public website by entering http://contoso.com into their web browsers, because contoso.com internally resolves to the IP address of one of your AD domain controllers. This has lead to messy solutions, such as installing IIS on each domain controller, for the sole purpose of redirecting requests on port 80 to www.contoso.com. But installing IIS on your domain controllers is not a great idea.

TinyWebRedirector is more suited to this purpose because:

  • It is tiny. The image file is 110KB, and runs with a ~2.6MB working set. It requires no redistributable DLLs.
  • It does one thing and one thing only. This translates to a much smaller potential attack surface than a large web server such as IIS.
  • It is written in C, and so does not require .NET. Will run on any Windows machine Vista/2008 or greater.
  • The listening port (default 80) and the URL to redirect visitors to is configurable in the registry at HKLM\SYSTEM\CurrentControlSet\Services\TinyWebRedirector. Restart the service for changes to take effect.
  • The service runs as Local Service. This is a much safer configuration than services that run as Local System.

Please let me know if you find any bugs or weaknesses.

Github repository is here.

The compiled and signed x64 binary is here:

TinyWebRedirector.exe (112.8KB)

The Basics of DLL Injection Part I

Hello... sorry for not posting in a while.  I've been going through some recent career changes and haven't really come across any IT-related inspiration lately.  I thought I'd post a simple, bare-bones technique of injecting a DLL into a running process on a Windows computer.  It's really simple.

  1. Verify that the PID supplied is valid.
  2. Get a handle to the remote process.
  3. Allocate memory in the remote process.
  4. Write the DLL path into the remote process's memory.
  5. Call CreateRemoteThread to induce LoadLibrary in the remote process. 

As long as the process does not already have the DLL loaded, the remote process will automatically call DllMain (the injected DLL's entry point,) which can contain all sorts of fun code.  For instance, now that you have injected a DLL into the remote process, you are well on your way to performing something like API hooking, for example. You could modify the Import Address Table of the remote process... but I'm getting ahead of myself.  First things first.  Without further ado:

// InjectDLL.cpp
// Joseph Ryan Ries - 2015
// Injects a DLL into another process on the system.

#include 
#include 
#include 

// Returns true if the supplied file path exists and is a file.
// Returns false if the supplied path does not exist, or is a directory.
BOOL FileExists(LPCTSTR FilePath)
{
	DWORD FileAttributes = GetFileAttributes(FilePath);

	return (FileAttributes != INVALID_FILE_ATTRIBUTES && !(FileAttributes & FILE_ATTRIBUTE_DIRECTORY));
}

// Entry point. Returns 0 at the end if everything was successful.
// Returns 1 if something failed. Outputs messages to console.
int wmain(int argc, wchar_t *argv[])
{
	if (argc != 3)
	{
		wprintf_s(L"\nUsage: %s C:\\Temp\\MyDLL.dll 1337\n", argv[0]);
		wprintf_s(L"\nUse the full path to the DLL you want to inject and supply the\n");
		wprintf_s(L"process ID (PID) of the process you want to inject it in to.\n");
		return(1);
	}

	if (!FileExists(argv[1]))
	{
		wprintf_s(L"\nERROR: Unable to find file %s.\n", argv[1]);
		return(1);
	}

	wchar_t DLLPath[MAX_PATH] = { 0 };

	wcscpy_s(DLLPath, argv[1]);

	DWORD Pid = _wtoi(argv[2]);

	if (Pid == 0)
	{
		wprintf_s(L"\nERROR: Unable to interpret the supplied process ID.\n");
		return(1);
	}

	wprintf_s(L"Searching for PID %d.\n", Pid);

	DWORD CurrentProcesses[2048] = { 0 };
	DWORD ProcessListSize = 0;
	DWORD ProcessCount = 0;

	if (EnumProcesses(CurrentProcesses, sizeof(CurrentProcesses), &ProcessListSize) == 0)
	{
		wprintf_s(L"\nERROR: Unable to enumerate currently running processes!\nLastError: 0x%x.\n", GetLastError());
		return(1);
	}

	ProcessCount = ProcessListSize / sizeof(DWORD);

	wprintf_s(L"%d processes currently on the system.\n", ProcessCount - 1);

	BOOL ProcessFound = FALSE;

	for (unsigned int ProcessCounter = 0; ProcessCounter < ProcessCount; ProcessCounter++)
	{
		if (CurrentProcesses[ProcessCounter] == 0)
		{
			continue;
		}
		if (CurrentProcesses[ProcessCounter] == Pid)
		{
			ProcessFound = TRUE;
			break;
		}
	}

	if (ProcessFound == FALSE)
	{
		wprintf_s(L"\nERROR: A process with PID %d could not be found.\n", Pid);
		return(1);
	}

	HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);

	if (ProcessHandle == NULL)
	{
		wprintf_s(L"\nERROR: Unable to open handle to process %d. LastError 0x%x.\n", Pid, GetLastError());
		return(1);
	}

	wprintf_s(L"Process with PID %d successfully opened.\n", Pid);

	void* RemoteDLLPathMemory = NULL;		// The DLL entry address in the remote process

	// VirtualAlloc will probably not give us less than 4k. Whatever. We only need ~520 bytes for MAX_PATH * 2.
	RemoteDLLPathMemory = VirtualAllocEx(ProcessHandle, NULL, (MAX_PATH * sizeof(wchar_t)), MEM_COMMIT, PAGE_READWRITE);

	if (RemoteDLLPathMemory == NULL)
	{
		wprintf_s(L"\nERROR: Unable to allocate memory in remote process %d. LastError: 0x%x.\n", Pid, GetLastError());
		return(1);
	}

	wprintf_s(L"Memory allocated in process %d.\n", Pid);

	SIZE_T BytesWritten = 0;

	if (WriteProcessMemory(ProcessHandle, RemoteDLLPathMemory, (void*)DLLPath, sizeof(DLLPath), &BytesWritten) == 0)
	{
		wprintf_s(L"\nERROR: Unable to write DLL path into remote process memory. LastError: 0x%x.\n", GetLastError());
		return(1);
	}

	wprintf_s(L"%I64d bytes written into the memory of process %d.\n", BytesWritten, Pid);
	
	HANDLE ThreadHandle = CreateRemoteThread(
		ProcessHandle,
		NULL,
		NULL,
		(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"),
		RemoteDLLPathMemory,
		NULL,
		NULL);

	if (ThreadHandle == NULL)
	{
		wprintf_s(L"\nERROR: Unable to start remote thread in process %d. LastError: 0x%x.\n", Pid, GetLastError());
		return(1);
	}

	wprintf_s(L"Successfully loaded %s into process %d.\n", DLLPath, Pid);

	wprintf_s(L"Note: DllMain will only run if the DLL has not already been loaded by the process.\n");
	
	return(0);
}

Ignore the very last line (</psapi.h></windows.h></stdio.h>), the code editor wigged out.