7
\$\begingroup\$

I have written this Active Directory password changer script. Comments and testing is appreciated for things I may have overlooked, since this is my first AD Script.

There are two parts: PHP and PowerShell.

This is being run on an IIS Server with PHP 5.4.x with Fast CGI and PowerShell 2.x.

The IIS_IUSRS is a manager of a group of all people who can change their own password with this method (as I exclude any account with access to the server).

If IIS_IUSRS is not a manager of a group of people you will get "Access Denied" in the log.

My goal with this script is to change passwords of people that cannot locally access domain computers. While I want this script as secure as possible, limiting those that can change their password and not allowing those with elevated permissions to change password. I was unable to find an escape character function and had to make my own.

Feel free to test this script beyond breaking point and comment, as I would like to see how well I have done in scripting something for Active Directory.

Can anyone confirm that I don't need to escape if possible? Post results if you do break it, please.

Here is the PHP script:

<?php setlocale(LC_CTYPE, "en_US.UTF-8"); ?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Pw Changer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<?php
/*
* Note: Errorstate var is changeable client side and should not be trusted
*/

$psScriptPath = ".\Bin\Non-Auth\adpwchange2014.ps1";// Path to the PowerShell script.
$logfile = './Logging/Phpruntime.txt';
$date = date('D, d M Y H:i:s');

if(!empty($_GET["successstate"])){//Achievement Get: PW Changer
    echo '<div class="successstate">'. $_GET["successstate"] .'</div>';
    exit(header('refresh:5; ../index.php'));
    }

if(!isset($_POST["submit"])){
    if(!empty($_GET["errorstate"])){echo '<div class="errorstate">' . $_GET["errorstate"] . '</div><br /><br />';}
    // if there was no submit variable passed to the 
    // script (i.e. user has visited the page without clicking submit), display the form:
    echo '<form name="testForm" class="formbox" id="testForm" action="index.php" method="post" />
    Username: <input type="text" name="username" id="username"/><br />
    Old Password: <input type="password" name="old_password"><br />
    New Password: <input type="password" name="new_password"><br />
    Confirm New Password: <input type="password" name="confirm"><br />
    <input type="submit" name="submit" id="submit" value="submit" />
    </form>';

}elseif(!empty($_POST["username"]) && !empty($_POST["old_password"]) && !empty($_POST["new_password"]) && !empty($_POST["confirm"])){// Else if submit was pressed, check if all of the required variables have a value and then Use PHP to check for risks such as Username in password or useing old password
    $errorstate = '';
    if($_POST["new_password"] != $_POST["confirm"]){
        $errorstate .= 'New Password and Confirm do not match</br>';
        }

    $username = utf8_decode($_POST["username"]);
    $old_password = utf8_decode($_POST["old_password"]);
    $new_password = utf8_decode($_POST["new_password"]);
    $confirm = utf8_decode($_POST["confirm"]);

    if(strlen($new_password) <= 8){//Length Check equal or greater then
        $errorstate .= 'Eight or more charictors needed</br>';
        }

    if(strpos($new_password,$old_password) !== false){//New Password Matches username or old password
        $errorstate .= 'Can not contain your old password</br>';
        }

    if(strpos($new_password, $username) !== false){
        $errorstate .= 'Can not contain your Username</br>';
        }

    $operator = array('\\','#','+','<','>',';','\"','=',',');//Operators that need to be escaped with
    $replace = array('\\\\','\\#','\\+','\\<','\\>','\\;','\\"','\\=','\,');//replacement

    $username = str_replace ($operator, $replace, $username);
    #$new_password = str_replace ($operator, $replace, $new_password);
    #$old_password = str_replace ($operator, $replace, $old_password);

    $check_upper = 0;
    $check_lower = 0;
    $check_digit = 0;
    $check_punct = 0;

    foreach(count_chars($new_password, 1) as $key => $value){//Strength Test Results can be derived from $value
        if(!ctype_upper(chr($key))){$check_upper=1;}//if Upper-case
        if(!ctype_lower(chr($key))){$check_lower=1;}//if Lower-case
        if(!ctype_digit(chr($key))){$check_digit=1;}//if Numeric
        if(!ctype_punct(chr($key))){$check_punct=1;}//if Symbol
        if($check_upper + $check_lower + $check_digit + $check_punct>= 3){}//Save us from checking the entire string
        }

    if($check_upper + $check_lower + $check_digit + $check_punct<= 2){
        $errorstate .= 'Password needs to contain at least 3 of the following criteria: Upper-case, Lower-case, Numeric and/or Symbol</br>';
        }

    if(!empty($errorstate)){//EXIT if error state is set. Do not pass go, do not collect $200.
        exit(header('Location: .?errorstate='.$errorstate));
        }

    $user = $username;
    $username = base64_encode($username); //Transport Layer Base64
    $new_password = base64_encode($new_password); //Transport Layer Base64
    $old_password = base64_encode($old_password); //Transport Layer Base64

    /*
    * The danger happens here as it is sent to powershell.
    */
    $query = shell_exec('powershell.exe -ExecutionPolicy ByPass -command "' . $psScriptPath . '" < NUL -base64_username "' . $username . '" < NUL -base64_oldpassword "' . $old_password . '" < NUL -base64_newpassword "' . $new_password . '" < NUL');// Execute the PowerShell script, passing the parameters
    /*
    *Log the query result
    */

    if(stristr($query, 'Success:') !== false){ //Return True
        $logstr = '========================================'."\r\n";
        $logstr .= '    ' . $date . ' - Success'."\r\n";
        $logstr .= '========================================'."\r\n";
        $logstr .= $_SERVER['REMOTE_ADDR'] . ' - ' . $user .": Attempted Password Change result \r\n";
        $logstr .= $query . "\r\n";
        $logstr .= "\r\n";
        file_put_contents($logfile, $logstr, FILE_APPEND | LOCK_EX);
        $errorstate = '</br>Success: Password was changed</br>';
        exit(header('Location: ./index.php?successstate='.$errorstate));

    }elseif(stristr($query, 'Failed:') !== false){ //Return False
        $logstr = '========================================'."\r\n";
        $logstr .= '    ' . $date . ' - Failed'."\r\n";
        $logstr .= '========================================'."\r\n";
        $logstr .= $_SERVER['REMOTE_ADDR'] . ' - ' . $user .": Attempted Password Change result \r\n";
        $logstr .= $query . "\r\n";
        $logstr .= "\r\n";
        file_put_contents($logfile, $logstr, FILE_APPEND | LOCK_EX);
        $errorstate = '</br>Failed: Password was not changed</br>';
        exit(header('Location: .?errorstate='.$errorstate));

    }else{//someone broke something not that we tell them but we log the entry
        $logstr = '========================================'."\r\n";
        $logstr .= '            ' . $date . ' - Error Warning'."\r\n";
        $logstr .= '========================================'."\r\n";
        $logstr .= $_SERVER['REMOTE_ADDR'] . ' - ' . $user .": Attempted Password Change result \r\n";
        $logstr .= 'powershell.exe -ExecutionPolicy ByPass -command "' . $psScriptPath . '" < NUL -username "' . $username . '" < NUL -oldpassword "' . $old_password . '" < NUL -newpassword "' . $new_password . '" < NUL' . "\r\n";
        $logstr .= $query . "\r\n";
        $logstr .= 'Username: ' .$username . "\r\n";
        $logstr .= 'Old Password: ' .$old_password . "\r\n";
        $logstr .= 'New Password: ' .$new_password . "\r\n";
        $logstr .= "\r\n";
        file_put_contents($logfile, $logstr, FILE_APPEND | LOCK_EX);
        //You could go one step further and ban IP for X time // you could also send an email to yourself
        $errorstate = '</br>Failed: Password was not changed</br>';
        exit(header('Location: .?errorstate='.$errorstate));
        }

}else{// Else the user hit submit without all required fields being filled out:
    $errorstate = 'Please Complete all fields</br>';
    exit(header('Location: .?errorstate='.$errorstate));
    }
?>

</body>
</html>

And here is the PowerShell:

#*=============================================================================
#* Script Name: adpwchange2014.ps1
#* Created: 2014-10-07
#* Author:
#* Purpose: This is a simple script that queries AD users.
#* Reference Website: http://theboywonder.co.uk/2012/07/29/executing-powershell-using-php-and-iis/
#* 
#*=============================================================================

#*=============================================================================
#* PARAMETER DECLARATION
#*=============================================================================
param(
[string]$base64_username,
[string]$base64_newpassword,
[string]$base64_oldpassword
)

#*=============================================================================
#* IMPORT LIBRARIES
#*=============================================================================

if ((Get-Module | where {$_.Name -match "ActiveDirectory"}) -eq $null)
{
#Loading module
Write-Host "Loading module AcitveDirectory..."
Import-Module ActiveDirectory
}
#*=============================================================================
#* PARAMETERS
#*=============================================================================
$username = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_username))
$newpassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_newpassword))
$oldpassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_oldpassword))

#*=============================================================================
#* INITIALISE VARIABLES
#*=============================================================================
# Increase buffer width/height to avoid PowerShell from wrapping the text before
# sending it back to PHP (this results in weird spaces).
$pshost = Get-Host
$pswindow = $pshost.ui.rawui
$newsize = $pswindow.buffersize
$newsize.height = 1000
$newsize.width = 300
$pswindow.buffersize = $newsize

#*=============================================================================
#* EXCEPTION HANDLER
#*=============================================================================

#*=============================================================================
#* FUNCTION LISTINGS
#*=============================================================================

    Function Test-ADAuthentication  {
        Param($Auth_User, $Auth_Pass)
        $domain = $env:USERDOMAIN

        Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        $ct = [System.DirectoryServices.AccountManagement.ContextType]::Domain
        $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($ct, $domain)
        $pc.ValidateCredentials($Auth_User, $Auth_Pass).ToString()
        }

    Function Set-ADAuthentication{
        Param($Auth_User,$Auth_OldPass, $Auth_NewPass)
        $domain = $env:USERDOMAIN
        $Auth_NewPass = ConvertTo-SecureString $Auth_NewPass -AsPlainText -Force
        $Auth_OldPass = ConvertTo-SecureString $Auth_OldPass -AsPlainText -Force
        #Running -whatif to simulate results
        #Therefore we expect "Failed: Password change" as it was not changed
        Set-ADAccountPassword -Identity $Auth_User -NewPassword $Auth_NewPass -OldPassword $Auth_OldPass -PassThru
        $authentication = Test-ADAuthentication $username $newpassword
        if ($authentication -eq $TRUE) {
            Write-Output "Success: Password Changed"
            }elseif ($authentication -eq $FALSE) {
            Write-Output "Failed: Password Change"
            }else {
                Write-Output "Error: EOS"
                EXIT
                NUL
                Stop-Process -processname powershell*
                }
        }

#*=============================================================================
#* Function: function1
#* Purpose: This function does X Y Z
#* =============================================================================

#*=============================================================================
#* END OF FUNCTION LISTINGS
#*=============================================================================

#*=============================================================================
#* SCRIPT BODY
#*=============================================================================
Write-Output $PSVersionTable
Write-Output "  "
$authentication = Test-ADAuthentication "$username" "$oldpassword"
if ($authentication -eq $TRUE) {
    Set-ADAuthentication $username $oldpassword $newpassword
    }elseif ($authentication -eq $FALSE) {
    Write-Output "Failed: Validation"
    }else {Write-Output "Error: EOS"
    EXIT
    NUL
    Stop-Process -processname powershell*
    }

#*=============================================================================
#* SCRIPT Exit
#*=============================================================================
EXIT
NUL
Stop-Process -processname powershell*
\$\endgroup\$
4
  • \$\begingroup\$ I had to resort to making this piece because i couldn't find anything like it before. Exchange use to provide such a service but stopped when Windows server 2008 came out unfortunately. And i hope i get constructive criticism so i can improve. \$\endgroup\$
    – Werezwolf
    Nov 20, 2014 at 2:06
  • \$\begingroup\$ Perhaps the escapeshellarg function is worth a look \$\endgroup\$ Nov 20, 2014 at 12:41
  • \$\begingroup\$ Ive tryed that but its suited to command line and not AD thus leaves artifacts in passwords. Thus not helpfull in computer enviroments \$\endgroup\$
    – Werezwolf
    Nov 20, 2014 at 12:57
  • \$\begingroup\$ Rewrote had a small problem that wasent returning properly - also made it live this will change the password. \$\endgroup\$
    – Werezwolf
    Nov 21, 2014 at 3:51

1 Answer 1

3
\$\begingroup\$

I don't really know anything about powershell, so I will only look at your PHP script.

XSS

echo '<div class="successstate">'. $_GET["successstate"] .'</div>';

This is vulnerable to reflected XSS, with which an attacker could execute arbitrary Javascript on a victims computer (and thus steal cookies, deface the website, display a phishing form, etc). Use htmlspecialchars to prevent this (same with errorstate).

Functions

Your code isn't all that long, but for 150 lines, I would extract some code to functions to structure it. For example, displayPasswordChangeForm and processPasswordChangeForm, and checkPasswordStrength.

I would also add a logQueryResult function to avoid duplicate code:

function logQueryResult($queryString, $date, $result, $redirect, $additional) {
    $logstr = '========================================'."\r\n";
    $logstr .= '    ' . $date . ' - ' . $result . ."\r\n";
    $logstr .= '========================================'."\r\n";
    $logstr .= $_SERVER['REMOTE_ADDR'] . ' - ' . $user .": Attempted Password Change result \r\n";
    $logstr .= $query . "\r\n";
    $logstr .= $additional;
    $logstr .= "\r\n";
    file_put_contents($logfile, $logstr, FILE_APPEND | LOCK_EX);
    $errorstate = '</br>' . $result . ': Password was changed</br>';
    exit(header('Location: ' $redirect));
}

// use like this:
logQueryResult($query, $date, 'Success', './index.php?successstate='.$errorstate, '');
logQueryResult($query, $date, 'Failed', '.?errorstate='.$errorstate, '');

$plainUserPass = 'Username: ' .$username . "\r\n";
$plainUserPass .= 'Old Password: ' .$old_password . "\r\n";
$plainUserPass .= 'New Password: ' .$new_password . "\r\n";
logQueryResult($query, $date, 'Error Warning', '.?errorstate='.$errorstate, $plainUserPass);

Misc

  • when building a string, either use all double or all single quote. Code Like $var = 'foo' . $test . "'bar'" . '\n' for example is hard to read.
  • always close your curly brackets at the same time (I would close it where the opening line began, it's easier to see which block it closes that way).
  • personally, I would use more spaces (before {, after } and ,, around ., etc.)
\$\endgroup\$
2
  • \$\begingroup\$ I think I'll have to find another method of displaying the results nicely to the user. The log query Result I am working on to make it into a function is a hard one because the result comes from the query string and I don't think date is a necessary item because its basically when it happens. I'll update the changes based on your recommendation thank you. \$\endgroup\$
    – Werezwolf
    Nov 24, 2014 at 22:35
  • \$\begingroup\$ I'm not sure making functions would help with this as its all to do with sanitizing for Active Directory i barely repeat something more then twice. But i did manage to compress my logging thanks to your recommendation. And i should of fixed the XSS with Session values. \$\endgroup\$
    – Werezwolf
    Nov 25, 2014 at 2:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.