Skip to content

Instantly share code, notes, and snippets.

@kendokan
Last active January 11, 2020 17:14
Show Gist options
  • Save kendokan/d015a8aa6d5199d046184cc10b3bbc34 to your computer and use it in GitHub Desktop.
Save kendokan/d015a8aa6d5199d046184cc10b3bbc34 to your computer and use it in GitHub Desktop.
Wired WPA (802.1x) authentication on a pfSense system, called from cron.
#!/usr/local/bin/php-cgi -f
<?php
/*
Performs 802.1x authentication on a pfSense system.
# php wpa_auth.php --hash testpass123
1efb50581482d84f9e57737b846a32b0
# php wpa_auth.php --interface vmx2 --identity myusername --password 1efb50581482d84f9e57737b846a32b0
# wpa_cli status
Selected interface 'vmx2'
bssid=xx:xx:xx:xx:xx:xx
freq=0
ssid=
id=0
mode=station
pairwise_cipher=NONE
group_cipher=NONE
key_mgmt=IEEE 802.1X (no WPA)
wpa_state=COMPLETED
ip_address=1.2.3.4
address=xx:xx:xx:xx:xx:xx
Supplicant PAE state=AUTHENTICATED
suppPortStatus=Authorized
EAP state=SUCCESS
selectedMethod=25 (EAP-PEAP)
eap_tls_version=TLSv1.2
EAP TLS cipher=ECDHE-RSA-AES256-GCM-SHA384
tls_session_reused=0
EAP-PEAPv1 Phase2 method=MSCHAPV2
eap_session_id=<redacted>
uuid=<redacted>
*/
require_once('interfaces.inc');
$opts = get_options();
$interface = get_interface_by_name($opts['interface']);
// If a carp interface was provided, this system must be master on that interface.
if (array_key_exists('carp', $opts) && !is_carp_master($opts['carp'])) {
if (!$opts['quiet'])
echo('CARP is running on this system, but interface "' . $opts['carp'] . '" is not a master.' . PHP_EOL);
if (get_supplicant_pid($interface)) {
echo('System is not master, terminating supplicant.' . PHP_EOL);
terminate_supplicant($interface);
}
exit();
}
// Stop here and do nothing if already authenticated, unless --force specified.
if (get_supplicant_status($interface) == 'SUCCESS') {
if (!$opts['force']) {
if (!$opts['quiet'])
echo('Already authenticated. Use --force to restart authentication.' . PHP_EOL);
exit();
} else {
echo('Forcing supplicant termination.' . PHP_EOL);
terminate_supplicant($interface);
}
}
// Ensure only one instance of this script is running from this point forward.
$lock = new Lock();
// Any supplicant running at this point isn't doing anything useful for us.
if (get_supplicant_pid($interface)) {
echo('Supplicant running but not authenticated.' . PHP_EOL . 'Terminating supplicant.' . PHP_EOL);
terminate_supplicant($interface);
}
init_supplicant($interface, $opts['identity'], $opts['password']);
echo('Waiting for authorization.' . PHP_EOL);
while (true) {
switch (get_supplicant_status($interface)) {
case 'SUCCESS':
exit('Authorization completed.' . PHP_EOL);
case 'FAILURE':
terminate_supplicant($interface);
die('Authorization FAILED.');
}
sleep(1);
}
function get_interface_by_name ($interface) {
foreach (get_configured_interface_with_descr() as $name => $desc) {
if (strtolower($interface) == strtolower($desc)) {
$arr = get_interface_info($name);
$arr['name'] = $name;
$arr['desc'] = $interface;
}
}
if (is_null($arr))
die('Interface "' . $interface . '" is not valid.' . PHP_EOL);
elseif ($arr['status'] != 'up')
die('Interface "' . $interface . '" is not up.' . PHP_EOL);
else
return $arr;
}
function is_carp_master ($interface) {
if (!get_carp_status())
die('CARP interface "' . $interface . '" specified, but CARP is not running on this system.' . PHP_EOL);
global $config;
$carp_interface = get_interface_by_name($interface);
foreach ($config['virtualip']['vip'] as $vip) {
if ($carp_interface['name'] == $vip['interface'] && $vip['mode'] == 'carp')
$vip_status = get_carp_interface_status("_vip{$vip['uniqid']}");
}
if (!$vip_status)
die('CARP is not configured for interface "' . $interface . '".' . PHP_EOL);
elseif ($vip_status == 'MASTER')
return true;
}
function get_password_hash ($password) {
if (strlen($password) == 32 && ctype_xdigit($password))
return $password;
else
return hash('md4', mb_convert_encoding($password, 'UTF-16LE'));
}
function get_supplicant_pid($interface) {
return exec('pgrep -f "wpa_supplicant .* ' . $interface['hwif'] . '"');
}
function get_supplicant_status($interface) {
if (get_supplicant_pid($interface))
return exec('wpa_cli status | grep "EAP state" | cut -d= -f2');
}
function terminate_supplicant($interface) {
if ($pid = get_supplicant_pid($interface)) {
exec('wpa_cli -i ' . $interface['hwif'] . ' terminate');
return true;
} else {
return false;
}
}
function init_supplicant($interface, $identity, $password) {
$res = shell_exec('wpa_supplicant -D wired -i ' . $interface['hwif'] . ' -C /var/run/wpa_supplicant -B');
// Start wpa_supplicant
if (strstr($res, 'Successfully initialized wpa_supplicant'))
echo('Started supplicant on interface ' . $interface['hwif'] . ' (' . $interface['desc'] . '/' . $interface['name'] . ').' . PHP_EOL);
else
die('Error initiating the supplicant: ' . PHP_EOL . $res . PHP_EOL);
$auth_params = array(
'ap_scan 0',
'add_network 0',
'set_network 0 key_mgmt IEEE8021X',
'set_network 0 eap PEAP',
'set_network 0 eapol_flags 0',
'set_network 0 phase2 \\"auth=MSCHAPV2\\"',
'set_network 0 identity \\"' . $identity . '\\"',
'set_network 0 password hash:' . $password,
'enable_network 0',
);
echo('Loading parameters to supplicant.' . PHP_EOL);
foreach ($auth_params as $param) {
$res = shell_exec('wpa_cli -i ' . $interface['hwif'] . ' ' . $param);
if (!preg_match('/^(OK|0)$/', $res)) {
echo('Error loading parameter:' . PHP_EOL . $res . PHP_EOL);
terminate_supplicant($interface);
die('Supplicant terminated.' . PHP_EOL);
}
}
echo('Finished loading parameters.' . PHP_EOL);
return true;
}
function get_options () {
$help = (PHP_EOL .
'Usage: ' . basename(__FILE__) . ' [-i interface] [-u identity] [-p password]' . PHP_EOL
. PHP_EOL
. 'Performs 802.1x authentication on a pfSense system.' . PHP_EOL
. 'Usage of a hashed password is highly recommended, but not mandatory.' . PHP_EOL
. PHP_EOL
. ' -i, --interface string Interface to authenticate' . PHP_EOL
. ' -c, --carp string Run only if this system is a CARP master for this interface' . PHP_EOL
. ' -u, --identity string Identity (i.e. username)' . PHP_EOL
. ' -p, --password string Password' . PHP_EOL
. ' --hash string Converts plaintext password to a hash' . PHP_EOL
. ' --force Force run, even if already successfully authenticated' . PHP_EOL
. ' -q, --quiet Suppress output' . PHP_EOL
. ' -h, --help Print usage' . PHP_EOL
. PHP_EOL
);
$opts = getopt(
'i:c:u:p:qh',
array(
'interface:',
'carp:',
'identity:',
'password:',
'hash:',
'force',
'quiet',
'help',
)
);
if (array_key_exists('h', $opts) || array_key_exists('help', $opts))
exit($help);
if (array_key_exists('hash', $opts))
exit(get_password_hash($opts['hash']) . PHP_EOL);
if (array_key_exists('i', $opts))
$opts['interface'] = $opts['i'];
if (array_key_exists('c', $opts))
$opts['carp'] = $opts['c'];
if (array_key_exists('u', $opts))
$opts['identity'] = $opts['u'];
if (array_key_exists('p', $opts))
$opts['password'] = $opts['p'];
if (array_key_exists('q', $opts) || array_key_exists('quiet', $opts))
$opts['quiet'] = true;
if (array_key_exists('force', $opts))
$opts['force'] = true;
if (!array_key_exists('interface', $opts))
die('ERROR: interface is a required parameter.' . PHP_EOL . $help);
if (!array_key_exists('identity', $opts))
die('ERROR: identity is a required parameter.' . PHP_EOL . $help);
if (!array_key_exists('password', $opts))
die('ERROR: password is a required parameter.' . PHP_EOL . $help);
else {
$opts['password'] = get_password_hash($opts['password']);
}
// We don't need these anymore.
unset (
$opts['i'],
$opts['c'],
$opts['u'],
$opts['p'],
$opts['q']
);
return $opts;
}
class Lock {
private $fp;
function __construct() {
$this->fp = fopen(__FILE__, 'r');
if (!flock($this->fp, LOCK_EX|LOCK_NB))
die('Another instance of ' . basename(__FILE__) . ' is already running.' . PHP_EOL);
}
function __destruct() {
flock($this->fp, LOCK_UN);
fclose($this->fp);
}
}
?>
@kendokan
Copy link
Author

kendokan commented Dec 1, 2017

# php wpa_auth.php --help
Usage:  wpa_auth.php [-i interface] [-u identity] [-p password]
        --interface   interface name
        --identity    username or other identity
        --password    hashed password
        --makehash    convert plaintext password to hash
        --force       run even if already successfully authenticated
# php wpa_auth.php --makehash testpass123
hash:1efb50581482d84f9e57737b846a32b0
# php wpa_auth.php --interface vmx2 --identity myusername --password hash:1efb50581482d84f9e57737b846a32b0
wpa: vmx2: Beginning WPA authorization process.
wpa: vmx2: Starting supplicant.
wpa: vmx2: Setting network configuration.
wpa: vmx2: Waiting for authorization.
wpa: vmx2: Authorization complete.
# wpa_cli status
Selected interface 'vmx2'
bssid=xx:xx:xx:xx:xx:xx
freq=0
ssid=
id=0
mode=station
pairwise_cipher=NONE
group_cipher=NONE
key_mgmt=IEEE 802.1X (no WPA)
wpa_state=COMPLETED
ip_address=1.2.3.4
address=xx:xx:xx:xx:xx:xx
Supplicant PAE state=AUTHENTICATED
suppPortStatus=Authorized
EAP state=SUCCESS
selectedMethod=25 (EAP-PEAP)
eap_tls_version=TLSv1.2
EAP TLS cipher=ECDHE-RSA-AES256-GCM-SHA384
tls_session_reused=0
EAP-PEAPv1 Phase2 method=MSCHAPV2
eap_session_id=<redacted>
uuid=<redacted>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment