The Problem

In the CCDC competition, the default passwords handed to you are often insecure, and it’s good practice to immediately change all passwords on the AD server. We’re then required to send a CSV file with all the updated passwords for each user.

Solution: a PowerShell script that runs on any Windows Server version 2012 R2 or newer.

Requirements for the script:

  1. Change all AD user passwords
  2. Generate secure passwords
  3. Create a CSV file with username and new password
  4. Work on at least Windows Server 2012

Getting the AD Accounts

We can use Get-ADUser to pull a list of users from a specific domain:

$users = Get-ADUser -Filter * -SearchBase $domain -Properties DistinguishedName

Then, to change each password, we loop through every user:

foreach ($user in $users) { ... }

I ran into an issue here: the $user variable contained all the information I needed, but I couldn’t use it directly to change the password. Instead, I had to use the DistinguishedName to reset it. I could have used the GUID instead, but the DistinguishedName is easiest to read at a glance.

Setting Account Passwords

The command to reset a user’s password is Set-ADAccountPassword:

foreach ($user in $users) {
    $name = $user | Select-Object -ExpandProperty DistinguishedName
    Set-ADAccountPassword -Identity $name -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $password -Force)
}
  • Identity selects the user by DistinguishedName.
  • Reset is the flag that tells AD to force the password change.
  • NewPassword supplies the new value.

Password Generator

I based the design on this approach, with one change: I stripped *&$/ out of the special character set to avoid issues with how Windows AD interacts with Linux tooling elsewhere in the environment.

$uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()
$lowercase = "abcdefghijklmnopqrstuvwxyz".ToCharArray()
$number    = "0123456789".ToCharArray()
$special   = "%()=?}{@#+!".ToCharArray()

A 2026 Update: the original character sets quietly dropped I and J from uppercase and just q from lowercase. I cannot remember why this was unless I had forgotten my alphabet at 1am when I was writing this. It excludes capital I but not lowercase l, drops J but keeps j, and skips q but not Q. If the goal had been avoiding visually ambiguous characters (common practice — I/l/1, O/0), it would have needed to be consistent about it, and it wasn’t. So the letter racism has been removed and all letters are equal again.

Inside the foreach loop, I pull a random number of characters from each array and stitch them together, then shuffle the result so it isn’t just “all the capitals, then all the lowercase, then numbers, then symbols”:

foreach ($user in $users) {
    $name = $user | Select-Object -ExpandProperty DistinguishedName

    # Build the password from each character class
    $password  = ($uppercase | Get-Random -Count $UCount) -join ''
    $password += ($lowercase | Get-Random -Count $LCount) -join ''
    $password += ($number    | Get-Random -Count $NCount) -join ''
    $password += ($special   | Get-Random -Count $SCount) -join ''

    # Scramble the password so characters aren't bunched up by type
    $passArray = $password.ToCharArray()
    $password  = ($passArray | Get-Random -Count $passArray.Count) -join ''

    Add-Content -Path $csvPasswordFile -Value ('"' + $name + '",' + $password)
}

CSV File

I start by grabbing the directory the script is running from, so it works no matter where it’s copied to:

$csvPasswordFile = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$csvPasswordFile += "\UsersNewPasswords.csv"
New-Item $csvPasswordFile -ItemType File

Then, back inside the foreach loop, each new password gets appended as a line with Add-Content as seen in the loop above.

Closing Notes

This project ended up being less difficult than I expected, but two things that I didn’t cover in detail above:

  • Setting the color scheme. This matters because it looks cooler when its green text on a black screen. Looking like a hacker is 50% of the battle.

  • Self-elevating to admin. AD password resets need an elevated session, the fix is a short block at the top of the script:

    $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $isAdmin) {
        Start-Process powershell -Verb RunAs -ArgumentList "-File `"$PSCommandPath`""
        exit
    }
    

    It checks the current session’s role, and if it isn’t elevated, relaunches itself with -Verb RunAs and exits the non-elevated copy. It’s a bit denser than the rest of the script, but that’s the whole trick.

If you’d like to see the full script, it’s on my GitHub.

2026 Update

Five years on, and I still love this script. A few use cases I have found for it:

Decoupling generation from assignment. Instead of generating passwords inline with the character-array approach above, I now feed the script a pre-generated list from passgen (or an equivalent generator) and have it only handle the assignment half: matching each entry to an AD user, calling Set-ADAccountPassword, and logging the result. Splitting generation from assignment means the password policy (length, character rules, entropy target) lives in one tool and the AD-specific plumbing lives in another.

Reuse as a rotation and recovery tool. The original use case was “kill the insecure defaults before the round starts.” The same script turns out to be just as useful in the opposite situation: after a suspected compromise, when you need to rotate every credential in the domain fast and get your own team back into a known good state. The mechanics are the same: enumerate users, generate or assign new credentials, log the result. Especially when the environment is still isolated as final remediation is being completed.