Last active
September 23, 2015 02:33
-
-
Save jecxjo/60c1fbb7474c980f0373 to your computer and use it in GitHub Desktop.
BASH CGI Script for managing Unix Passwords
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash - | |
#=============================================================================== | |
# Copyright (c) 2015 Jeff Parent | |
# All rights reserved. | |
# | |
# Redistribution and use in source and binary forms, with or without modification, | |
# are permitted provided that the following conditions are met: | |
# | |
# * Redistributions of source code must retain the above copyright notice, this | |
# list of conditions and the following disclaimer. | |
# * Redistributions in binary form must reproduce the above copyright notice, | |
# this list of conditions and the following disclaimer in the documentation | |
# and/or other materials provided with the distribution. | |
# * Neither the name of the passwd.sh authors nor the names of its contributors | |
# may be used to endorse or promote products derived from this software without | |
# specific prior written permission. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
# | |
# FILE: passwd.sh | |
# | |
# USAGE: ./passwd.sh | |
# | |
# DESCRIPTION: cgi script to modify unix passwords | |
# | |
# OPTIONS: --- | |
# REQUIREMENTS: sudo access to chpasswd | |
# BUGS: --- | |
# NOTES: --- | |
# AUTHOR: jecxjo (jeff@commentedcode.org) | |
# ORGANIZATION: | |
# CREATED: 09/20/15 13:28 | |
# REVISION: 0.0.4 | |
# | |
# CHANGELOG: 0.0.4 - Security holes and cleanup | |
# 0.0.3 - Moved bash_cgi code to file | |
# 0.0.2 - Clean up | |
# 0.0.1 - Initial version | |
# | |
#=============================================================================== | |
# Global Vars | |
URL="https://example.com/cgi-bin/passwd.sh" | |
TITLE="Change Password" | |
EMAIL_FROM_NAME="Webmaster" | |
EMAIL_FROM_ADDRESS="webmaster@example.com" | |
USER_DB="/var/lib/passwd.sh/users.db" | |
RESET_DB="/var/lib/passwd.sh/reset.db" | |
RND_CMD=$(/usr/bin/dd if=/dev/random bs=1 count=32 | \ | |
/usr/bin/base64 | \ | |
/usr/bin/sed 's|+||g' | \ | |
/usr/bin/sed 's|/||g' | \ | |
/usr/bin/sed 's|=||g' | \ | |
/usr/bin/sed 's| ||g') | |
BLACKLIST=(root http nobody) | |
################# | |
# Confirm Reset # | |
################# | |
# Apply new password and output HTML status | |
# 1->user, 2->pass | |
function ResetPass () { | |
local usr=$(IsSaneUser "$1") pass="$2" | |
# write new user:pass to system | |
echo "${usr}:${pass}" | /usr/bin/sudo /usr/bin/chpasswd | |
# Check if password change was successful | |
if [ $? -eq 0 ]; then | |
echo "<b>Success:</b> Password changed successfully<br />" | |
# Remove all instances of reset keys | |
/usr/bin/umask 026 | |
local tmp=$(mktemp /tmp/reset.XXXXXX) | |
sed "/:${usr}$/d" "${RESET_DB}" > "${tmp}" | |
mv "${tmp}" "${RESET_DB}" | |
/usr/bin/chown http:http "${RESET_DB}" | |
else | |
echo "<b>Error:</b> Failed setting password<br />" | |
fi | |
} | |
# Check if Key:User DB and return HTML Form to reset | |
# 1->user, 2->key | |
function ConfirmReset () { | |
local usr=$(IsSaneUser "$1") key="$2" | |
/usr/bin/grep -q "^${key}:${usr}" "${RESET_DB}" | |
# Check if reset code is valid | |
if [ $? -eq 0 ]; then | |
# Create form to enter new password | |
/usr/bin/cat <<EOF | |
<form action="${URL}" method="POST"> | |
<fieldset> | |
<legend>Reset Password</legend> | |
<input type="hidden" name="cmd" id="cmd" value="resetpass" /> | |
<input type="hidden" name="key" id="key" value="${key}" /> | |
<input type="hidden" name="user" id="user" value="${usr}" /> | |
<p><label class="field" for="pass">Password:</label><input type="password" name="pass" id="pass" class="textbox-300" /></p> | |
<p><label class="field" for="passcfm">Confirm:</label><input type="password" name="passcfm" id="passcfm" class="textbox-300" /></p> | |
<input type="submit" value="Submit" /> | |
</fieldset> | |
</form> | |
EOF | |
else | |
echo "<b>Error:</b> Reset code is not valid<br />" | |
fi | |
} | |
# Check if all form data is valid for new password on reset | |
# as generated by ConfirmReset | |
# 1->user, 2->key, 3->pass, 4->cfm | |
function ApplyNewPass () { | |
local usr=$(IsSaneUser "$1") key="$2" pass="$3" cfm="$4" | |
if [ -z "${usr}" ]; then | |
echo "<b>Error:</b> No User entered<br />" | |
elif [ -z "${key}" ]; then | |
echo "<b>Error:</b> No Key<br />" | |
elif [ -z "${pass}" ]; then | |
echo "<b>Error:</b> No New Password<br />" | |
ConfirmReset "${usr}" "${key}" | |
elif [ -z "${cfm}" ]; then | |
echo "<b>Error:</b> No New Password<br />" | |
ConfirmReset "${usr}" "${key}" | |
else | |
grep -q "^${usr}:" /etc/passwd | |
if [ $? -eq 1 ]; then | |
echo "<b>Error:</b> User does not exist<br />" | |
elif [ "${pass}" != "${cfm}" ]; then | |
echo "<b>Error:</b> New Passwords don't match<br />" | |
ConfirmReset "${usr}" "${key}" | |
else | |
ResetPass "${usr}" "${pass}" | |
fi | |
fi | |
} | |
################## | |
# Password Reset # | |
################## | |
# Find Email Address from Contact Info | |
# 1->user | |
function GetAddress () { | |
local usr=$(IsSaneUser "$1") | |
/usr/bin/awk -v search="^${usr}:" '$0 ~ search{split($0,a,":"); print a[2];}' "${USER_DB}" | |
} | |
# Create form to request Reset Email | |
# 1->user | |
function UserResetForm () { | |
local user=$(IsSaneUser "$1") | |
/usr/bin/cat <<EOF | |
<form action="${URL}" method="POST"> | |
<fieldset> | |
<legend>Reset Password</legend> | |
<input type="hidden" name="cmd" id="cmd" value="setreset" /> | |
<p><label class="field" for="user">User:</label><input type="text" name="user" id="user" class="textbox-300" value="${user}" /></p> | |
<input type="submit" value="Submit" /> | |
</fieldset> | |
</form> | |
EOF | |
} | |
# Create Email, send it and then generate HTML status | |
# 1->user | |
function ApplyReset () { | |
local usr=$(IsSaneUser "$1") | |
local key="${RND_CMD}" | |
if [ -z "${usr}" ]; then | |
echo "<b>Error:</b> No User entered<br />" | |
UserResetForm "" | |
else | |
/usr/bin/grep -q "^${usr}:" /etc/passwd | |
if [ $? -eq 1 ]; then | |
echo "<b>Error:</b> User does not exist<br />" | |
UserResetForm "" | |
else | |
/usr/bin/grep -q "^${usr}:" "${USER_DB}" | |
if [ $? -eq 1 ]; then | |
echo "<b>Error:</b> User has no contact info<br />" | |
UserResetForm "" | |
else | |
# Create Email message | |
local subject="Password Reset" | |
local link="${URL}?cmd=cfmreset&user=${usr}&key=${key}" | |
local address=$(GetAddress "${usr}") | |
local message=$(cat <<EOF | |
A request was made to reset the password for ${usr}. If this was in error | |
please ignore this message. Otherwise follow the link to reset your account | |
password: | |
${link} | |
Thank you | |
EOF) | |
local mail="subject:${subject}\nfrom:${EMAIL_FROM_ADDRESS}\n\n${message}" | |
echo -e "${mail}" | /usr/bin/sendmail -F "${EMAIL_FROM_NAME}" -f "${EMAIL_FROM_ADDRESS}" "${address}" | |
if [ $? -eq 0 ]; then | |
echo "<b>Success:</b> Email sent<br />" | |
# Write key to database | |
echo "${key}:${usr}" >> "${RESET_DB}" | |
else | |
echo "<b>Error:</b> Failed sending email<br />" | |
fi | |
fi | |
fi | |
fi | |
} | |
################ | |
# Set Password # | |
################ | |
# Create form to change password | |
# 1->user 2->old_pass | |
function UserPassForm () { | |
local user=$(IsSaneUser "$1") | |
local old_pass=$2 | |
/usr/bin/cat <<EOF | |
<form action="${URL}" method="POST"> | |
<fieldset> | |
<legend>Change Password</legend> | |
<input type="hidden" name="cmd" id="cmd" value="setpass" /> | |
<p><label class="field" for="user">User:</label> <input type="text" name="user" id="user" value="${user}" /></p> | |
<p><label class="field" for="oldpass">Old Password:</label><input type="password" name="oldpass" id="oldpass" value="${old_pass}" /></p> | |
<p><label class="field" for="pass">New Password:</label> <input type="password" name="pass" id="pass" /></p> | |
<p><label class="field" for="passcfm">Confirm Password:</label> <input type="password" name="passcfm" id="passcfm" /></p> | |
<input type="submit" value="Submit" /> | |
</fieldset> | |
</form> | |
EOF | |
} | |
# Apply new password to user and generate HTML status | |
# 1->user, 2->old pass, 3->new pass | |
function SetPass () { | |
local user=$(IsSaneUser "$1") pass=$2 new=$3 | |
local out=$(echo -e "${pass}\n${pass}\n${new}\n${new}" | /usr/bin/su -c 'if /usr/bin/passwd; then echo "SUCCESS"; fi ' "${user}") | |
echo "${out}" | /usr/bin/grep -q "SUCCESS" | |
if [ $? -eq 0 ]; then | |
echo "<b>Success:</b> Password Changed<br />" | |
else | |
echo "<b>Error:</b> Failed changing password[${out}]<br />" | |
fi | |
} | |
# Validate form data generated by UserPassForm | |
# 1->user, 2->old, 3->newa, 4->newb | |
function ApplyPass () { | |
local usr=$(IsSaneUser "$1") | |
local old="$2" | |
local newa="$3" | |
local newb="$4" | |
if [ -z "${usr}" ]; then | |
echo "<b>Error:</b> Invalid User<br />" | |
UserPassForm "" "" | |
elif [ -z "${old}" ]; then | |
echo "<b>Error:</b> No Old Password<br />" | |
UserPassForm "${usr}" "" | |
elif [ -z "${newa}" ]; then | |
echo "<b>Error:</b> No New Password<br />" | |
UserPassForm "${usr}" "${old}" | |
elif [ -z "${newb}" ]; then | |
echo "<b>Error:</b> No New Password<br />" | |
UserPassForm "${usr}" "${old}" | |
else | |
/usr/bin/grep -q "^${usr}:" /etc/passwd | |
if [ $? -eq 1 ]; then | |
echo "<b>Error:</b> User does not exist<br />" | |
UserPassForm "" "" | |
elif [ "${newa}" != "${newb}" ]; then | |
echo "<b>Error:</b> New Passwords don't match<br />" | |
UserPassForm "${usr}" "${old}" | |
else | |
SetPass "${usr}" "${old}" "${newa}" | |
fi | |
fi | |
} | |
################ | |
# Contact Info # | |
################ | |
# Create form to update Contact Info | |
# 1->user, 2->email | |
function UserContactForm () { | |
local user=$(IsSaneUser "$1") email=$(IsSaneEmail "$2") | |
/usr/bin/cat <<EOF | |
<form action="${URL}" method="POST"> | |
<fieldset> | |
<legend>Change Contact Info</legend> | |
<input type="hidden" name="cmd" id="cmd" value="setcontact" /> | |
<p><label class="field" for="user">User:</label><input type="text" name="user" id="user" value="${user}" /></p> | |
<p><label class="field" for="user">Password:</label><input type="password" name="pass" id="pass" /></p> | |
<p><label class="field" for="user">Email:</label><input type="email" name="email" id="email" value="${email}" /></p> | |
<input type="submit" value="Submit" /> | |
</fieldset> | |
</form> | |
EOF | |
} | |
# Apply new contact info and generate HTML status | |
# 1->user, 2->password, 3->email | |
function SetContact () { | |
local usr=$(IsSaneUser "$1") pass=$2 email=$(IsSaneEmail "$3") | |
local f="/tmp/${usr}" | |
local str="${usr}:${email}" | |
# Touch file as user, requires correct password | |
local out=$(echo -e "${pass}\n" | /usr/bin/su -c "/usr/bin/touch \"${f}\"" - "${usr}") | |
# if su worked, user/pass was valid | |
if [ -e "${f}" ]; then | |
# Remove old contact info and add new | |
/usr/bin/umask 026 | |
TMP=$(/usr/bin/mktemp /tmp/contact.XXXXXX) | |
/usr/bin/sed "/^${usr}:/d" "${USER_DB}" > "${TMP}" | |
echo "${usr}:${email}" >> "${TMP}" | |
mv "${TMP}" "${USER_DB}" | |
/usr/bin/chown http:http "${USER_DB}" | |
echo "<b>Success:</b> Contact Info Updated<br />" | |
# cleanup touched file | |
local out=$(echo -e "${pass}\n" | /usr/bin/su -c "/usr/bin/rm \"${f}\"" - "${usr}") | |
else | |
echo "<b>Error:</b> Failed to update DB, No file[${f}]<br />" | |
fi | |
} | |
# Validate form data generated by UserContactForm | |
# 1->user, 2->pass, 3->email | |
function ApplyContact () { | |
local usr=$(IsSaneUser "$1") pass="$2" email=$(IsSaneEmail "$3") | |
if [ -z "${usr}" ]; then | |
echo "<b>Error:</b> Invalid Username<br />" | |
UserContactForm "" "${email}" | |
elif [ -z "${pass}" ]; then | |
echo "<b>Error:</b> Invalid Password<br />" | |
UserContactForm "${usr}" "${email}" | |
elif [ -z "${email}" ]; then | |
echo "<b>Error:</b> Invalide Email<br />" | |
UserContactForm "${usr}" "" | |
else | |
grep -q "^${usr}:" /etc/passwd | |
if [ $? -eq 1 ]; then | |
echo "<b>Error:</b> User does not exist<br />" | |
UserContactForm "" "${email}" | |
else | |
SetContact "${usr}" "${pass}" "${email}" | |
fi | |
fi | |
} | |
#################### | |
# Main Application # | |
#################### | |
# Switch on URL argument "cmd" to generate correct page | |
# $1->cmd | |
function Body () { | |
echo "<body>" | |
cgi_getvars BOTH cmd | |
case "${cmd}" in | |
resetpass) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH key | |
cgi_getvars BOTH pass | |
cgi_getvars BOTH passcfm | |
ApplyNewPass "${user}" "${key}" "${pass}" "${passcfm}" | |
;; | |
cfmreset) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH key | |
ConfirmReset "${user}" "${key}" | |
;; | |
setreset) | |
cgi_getvars BOTH user | |
ApplyReset "${user}" | |
;; | |
setpass) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH oldpass | |
cgi_getvars BOTH pass | |
cgi_getvars BOTH passcfm | |
ApplyPass "${user}" "${oldpass}" "${pass}" "${passcfm}" | |
;; | |
setcontact) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH pass | |
cgi_getvars BOTH email | |
ApplyContact "${user}" "${pass}" "${email}" | |
;; | |
resetform) | |
cgi_getvars BOTH user | |
UserResetForm "${user}" | |
;; | |
contactform) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH email | |
UserContactForm "${user}" "${email}" | |
;; | |
passform) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH oldpass | |
UserPassForm "${user}" "${oldpass}" | |
;; | |
*) | |
cgi_getvars BOTH user | |
cgi_getvars BOTH oldpass | |
UserPassForm "${user}" "${oldpass}" | |
;; | |
esac | |
echo "<br />" | |
echo "<a href=\"/cgi-bin/passwd.sh?cmd=passform\">Password</a>" | |
echo "<a href=\"/cgi-bin/passwd.sh?cmd=contactform\">Contact</a>" | |
echo "<a href=\"/cgi-bin/passwd.sh?cmd=resetform\">Reset Password</a>" | |
echo "</body>" | |
} | |
# START bash_cgi | |
# Created by Philippe Kehl | |
# http://oinkzwurgl.org/bash_cgi | |
# (internal) routine to store POST data | |
function cgi_get_POST_vars() | |
{ | |
# check content type | |
# FIXME: not sure if we could handle uploads with this.. | |
[ "${CONTENT_TYPE}" != "application/x-www-form-urlencoded" ] && \ | |
echo "bash.cgi warning: you should probably use MIME type "\ | |
"application/x-www-form-urlencoded!" 1>&2 | |
# save POST variables (only first time this is called) | |
[ -z "$QUERY_STRING_POST" \ | |
-a "$REQUEST_METHOD" = "POST" -a ! -z "$CONTENT_LENGTH" ] && \ | |
read -n $CONTENT_LENGTH QUERY_STRING_POST | |
# prevent shell execution | |
local t | |
t=${QUERY_STRING_POST//%60//} # %60 = ` | |
t=${t//\`//} | |
t=${t//\$(//} | |
t=${t//%24%28//} # %24 = $, %28 = ( | |
QUERY_STRING_POST=${t} | |
return | |
} | |
# (internal) routine to decode urlencoded strings | |
function cgi_decodevar() | |
{ | |
[ $# -ne 1 ] && return | |
local v t h | |
# replace all + with whitespace and append %% | |
t="${1//+/ }%%" | |
while [ ${#t} -gt 0 -a "${t}" != "%" ]; do | |
v="${v}${t%%\%*}" # digest up to the first % | |
t="${t#*%}" # remove digested part | |
# decode if there is anything to decode and if not at end of string | |
if [ ${#t} -gt 0 -a "${t}" != "%" ]; then | |
h=${t:0:2} # save first two chars | |
t="${t:2}" # remove these | |
v="${v}"`echo -e \\\\x${h}` # convert hex to special char | |
fi | |
done | |
# return decoded string | |
echo "${v}" | |
return | |
} | |
# routine to get variables from http requests | |
# usage: cgi_getvars method varname1 [.. varnameN] | |
# method is either GET or POST or BOTH | |
# the magic varible name ALL gets everything | |
function cgi_getvars() | |
{ | |
[ $# -lt 2 ] && return | |
local q p k v s | |
# prevent shell execution | |
t=${QUERY_STRING//%60//} # %60 = ` | |
t=${t//\`//} | |
t=${t//\$(//} | |
t=${t//%24%28//} # %24 = $, %28 = ( | |
QUERY_STRING=${t} | |
# get query | |
case $1 in | |
GET) | |
[ ! -z "${QUERY_STRING}" ] && q="${QUERY_STRING}&" | |
;; | |
POST) | |
cgi_get_POST_vars | |
[ ! -z "${QUERY_STRING_POST}" ] && q="${QUERY_STRING_POST}&" | |
;; | |
BOTH) | |
[ ! -z "${QUERY_STRING}" ] && q="${QUERY_STRING}&" | |
cgi_get_POST_vars | |
[ ! -z "${QUERY_STRING_POST}" ] && q="${q}${QUERY_STRING_POST}&" | |
;; | |
esac | |
shift | |
s=" $* " | |
# parse the query data | |
while [ ! -z "$q" ]; do | |
p="${q%%&*}" # get first part of query string | |
k="${p%%=*}" # get the key (variable name) from it | |
v="${p#*=}" # get the value from it | |
q="${q#$p&*}" # strip first part from query string | |
# decode and evaluate var if requested | |
[ "$1" = "ALL" -o "${s/ $k /}" != "$s" ] && \ | |
eval "$k=\"`cgi_decodevar \"$v\"`\"" | |
done | |
return | |
} | |
#cgi_getvars BOTH ALL | |
# END of bash_cgi | |
################ | |
# Sanitization # | |
################ | |
# Checks if username is a sane username | |
# 1->user | |
function IsSaneUser () { | |
local user=$(echo "$1" | /usr/bin/grep "^[0-9A-Za-z-]\+$") | |
if [ ! -z "${user}" ]; then | |
local count = 0 | |
while [ "x${BLACKLIST[count]}" != "x" ] | |
do | |
if [ "${user}" == "${BLACKLIST[count]}" ]; then | |
return | |
fi | |
count=$(( ${count} + 1 )) | |
done | |
fi | |
echo "${user}" | |
} | |
# Checks if email is sane | |
# 1->email | |
function IsSaneEmail () { | |
echo "$1" | /usr/bin/grep -E -o "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" | |
} | |
################# | |
# CLI Functions # | |
################# | |
function PrintUsage () { | |
/usr/bin/cat <<EOF | |
usage: passwd.sh [OPTION] | |
-h | --help Print this output | |
-c | --cron Trigger cleanup of expired entries | |
This application is a CGI script that generates a Unix account password | |
manager. The script should be executed as a non-root user and the user | |
should be given sudo access to /usr/bin/chpasswd. | |
To allow password reset requests to expire, this script should be run as | |
a cron job with the -c flag. | |
EOF | |
} | |
function CronMode () { | |
echo "TODO: Create Cron Mode" | |
} | |
################### | |
# HTML Generation # | |
################### | |
function Header() { | |
/usr/bin/cat <<EOF | |
<head> | |
<title>${TITLE}</title> | |
<style> | |
fieldset { | |
width: 500px; | |
} | |
legend { | |
font-size: 20px; | |
} | |
label.field { | |
text-align: right; | |
width: 200px; | |
float: left; | |
font-weight: bold; | |
} | |
label.textbox-300 { | |
width: 300px; | |
float: left; | |
} | |
fieldset p { | |
clear: bloth; | |
padding: 5px; | |
} | |
</style> | |
</head> | |
EOF | |
} | |
# Cron mode, trigger cleanup of reset requests | |
CRON_MODE=0 | |
# Print usage and quit | |
HELP_MODE=0 | |
while [[ $# > 1 ]] | |
do | |
local key = $1 | |
case ${key} in | |
-c|--cron) | |
CRON_MODE=1 | |
;; | |
-h|--help) | |
HELP_MODE=1 | |
;; | |
*) | |
;; | |
esac | |
shift # next arg | |
done | |
if [ ${HELP_MODE} -eq 1 ]; then | |
PrintUsage | |
elif [ ${CRON_MODE} -eq 1 ]; then | |
CronMode | |
else | |
/usr/bin/cat <<EOF | |
Content-type: text/html | |
<!DOCTYPE html> | |
<html> | |
$(Header) | |
$(Body) | |
</html> | |
EOF | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment