summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE25
-rw-r--r--Makefile11
-rw-r--r--README.md93
-rwxr-xr-xlrpb227
4 files changed, 356 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..62c3ae8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2022, Evgeny Zinoviev
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. 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.
+
+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 HOLDER 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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b5a3e46
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+PREFIX = /usr/local
+INSTALL = /usr/bin/env install
+
+all:
+ @echo "run \"make install\" to install to $(PREFIX)"
+
+install:
+ $(INSTALL) lrpb $(PREFIX)/bin
+
+.PHONY: all install
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..465945c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,93 @@
+
+# lrpb
+
+Means "last resort primitive backdoor".
+
+For when you can't log in via ssh but something's still alive...
+
+
+## Requirements
+
+- bash 4+
+- GNU tar
+- curl or wget
+- signify from openbsd
+- rsync
+- ssh
+
+
+## Installation
+
+ make install
+
+
+## Client side usage
+
+First, create a pair of keys:
+
+ signify -G -p myname.pub -s myname.sec
+
+Then write a script you want to be launched on remote side:
+
+ #!/bin/sh
+ touch ~/helloworld-it-works
+
+Write lrpb client config to `client.conf`:
+
+ # ssh config
+ upload_host=mydomain.org
+ upload_port=22
+ upload_user=user
+ upload_path=/home/user/www/lrpb
+
+ name=myname
+
+ signify_path=/bin/signify
+ seckey_path=./myname.sec
+
+Finally, sign and upload it to some remote server you control:
+
+ lrpb upload -c ./client.conf -f ./script.sh
+
+On remote server, set up some http server (nginx, lighttpd, apache, whatever) that serves directory you upload to.
+The script will upload archive name `myname.tar.gz` and it must be accessible by http.
+
+
+## Server side usage
+
+Copy public key to `/etc/lrpb.pub` on the server (or anywhere you
+want, just make sure to set correct path in the config).
+
+Write lrpb server config and save it to `/etc/lrpb.conf`:
+
+ url="https://mydomain.org/lrpb/"
+ pubkey_path=/etc/lrpb.pub
+ name=myname
+ signify_path=/usr/bin/signify-openbsd
+ cwd=/var/lrpbfsq
+ cache_file=/var/lrpbfs/cache
+
+> Optionally, make `/var/lrpbfs` a tmpfs mountpoint. Add to `/etc/fstab`:
+>
+> ```
+> tmpfs /var/lrpbfs tmpfs size=1M,mode=1755,uid=1000,gid=1000 0 0
+> ```
+>
+> Then mount it:
+> ```
+> mount /var/lrpbfs
+> ```
+
+Test that it works:
+
+ lrpb exec
+
+Add cron task (`crontab -e`):
+
+ 0,30 * * * * /usr/local/bin/lrpb exec
+
+
+## License
+
+BSD-2c
+
diff --git a/lrpb b/lrpb
new file mode 100755
index 0000000..00e2517
--- /dev/null
+++ b/lrpb
@@ -0,0 +1,227 @@
+#!/bin/bash
+
+set -e
+#set -x
+
+PROGNAME="$0"
+BOLD=$(tput bold)
+RST=$(tput sgr0)
+
+file=
+config_file="/etc/lrpb.conf"
+declare -A config=()
+command=
+
+die() {
+ echoerr "error: $@"
+ exit 1
+}
+
+echoerr() {
+ >&2 echo "$@"
+}
+
+usage() {
+ cat <<EOF
+usage: $PROGNAME OPTIONS COMMAND
+
+Options:
+ -c|--config FILE path to config file (default: $config_file)
+ -f|--file FILE path to script
+ -x set -x
+
+Commands:
+ upload
+ exec
+EOF
+ exit 1
+}
+
+installed() {
+ command -v "$1" > /dev/null
+ return $?
+}
+
+download() {
+ local source="$1"
+ local target="$2"
+
+ if installed curl; then
+ curl -f -s -o "$target" "$source"
+ elif installed wget; then
+ wget -q -O "$target" "$source"
+ else
+ die "neither curl nor wget found, can't proceed"
+ fi
+}
+
+signify() {
+ "${config[signify_path]}" "$@"
+}
+
+read_config() {
+ local n=0
+ local failed=
+
+ while read line; do
+ n=$(( n+1 ))
+
+ # skip empty lines or comments
+ if [ -z "$line" ] || [[ "$line" =~ ^#.* ]]; then
+ continue
+ fi
+
+ if [[ $line = *"="* ]]; then
+ config[${line%%=*}]=${line#*=}
+ else
+ echoerr "config: invalid line $n"
+ failed=1
+ fi
+ done < <(cat "$config_file")
+
+ [ -z "$failed" ] || exit 1
+}
+
+check_config() {
+ local failed=
+
+ for key in $1; do
+ if [ -z "${config[$key]}" ]; then
+ echoerr "config: ${BOLD}${key}${RST} is missing"
+ failed=1
+ fi
+ done
+
+ [ -z "$failed" ] || exit 1
+}
+
+delete_unpacked_script() {
+ local path=
+ for f in script.sh script.sh.sig; do
+ path="${config[cwd]}/$f"
+ if [ -f "$path" ]; then rm "$path"; fi
+ done
+}
+
+do_upload() {
+ local file="$1"
+ local dir=$(mktemp -d)
+
+ pushd "$dir" >/dev/null
+ cp "$file" .
+
+ local bname="$(basename "$file")"
+
+ signify -S -s "${config[seckey_path]}" -m "$bname" || \
+ die "signify failed: $?"
+
+ local archive="${config[name]}.tar.gz"
+
+ tar --owner=0 --group=0 \
+ --transform="flags=r;s|$bname|script.sh|" \
+ --transform="flags=r;s|$bname.sig|script.sh.sig|" \
+ -czf "$archive" "$bname" "$bname.sig" >/dev/null
+
+ rsync -p --chmod=u+rw,g+r,o+r -e "ssh -p${config[upload_port]}" \
+ $archive \
+ "${config[upload_user]}"@"${config[upload_host]}":"${config[upload_path]}" || \
+ die "rsync failed: $?"
+
+ popd >/dev/null
+ rm -rf "$dir"
+}
+
+do_exec() {
+ pushd "${config[cwd]}" >/dev/null || die "failed to change to ${config[cwd]}"
+
+ local archive="${config[name]}.tar.gz"
+ download "${config[url]}$archive" "$archive" || exit 0
+
+ local checksum
+ local same=0
+ local f
+
+ checksum=$(md5sum "$archive" | awk '{print $1}')
+ if [ -f "${config[cache_file]}" ] && [ "$(cat "${config[cache_file]}")" = "$checksum" ]; then
+ same=1
+ fi
+
+ if [ "$same" = "0" ]; then
+ delete_unpacked_script
+ tar --no-same-owner -xzf "$archive"
+
+ signify -V -p "${config[pubkey_path]}" -m script.sh >/dev/null || die "signify failed: $?"
+ echo "$checksum" > "${config[cache_file]}"
+
+ chmod +x script.sh
+ sh script.sh || echoerr "script.sh exited with $?"
+ else
+ echoerr "archive have not changed, skipping"
+ fi
+
+ rm "$archive"
+ delete_unpacked_script
+
+ popd >/dev/null
+}
+
+case $BASH_VERSION in
+ ''|[0-3].*)
+ die "bash 4.0 is required"
+ ;;
+esac
+
+[[ $# -lt 1 ]] && usage
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ upload|exec)
+ command="$1"
+ shift
+ ;;
+
+ -c|--config)
+ config_file="$2"
+ shift; shift
+ ;;
+
+ -f|--file)
+ file="$2"
+ shift; shift
+ ;;
+
+ -x)
+ set -x
+ shift
+ ;;
+
+ *)
+ die "unrecognized option $1"
+ exit 1
+ ;;
+ esac
+done
+
+[ -z "$config_file" ] && die "config is not specied"
+[ -f "$config_file" ] || die "config file (${BOLD}${config}${RST}) not found"
+read_config
+
+[ -z "$command" ] && die "command not specified"
+case "$command" in
+ upload)
+ [ -z "$file" ] && die "file is not specified"
+ check_config "upload_host upload_port upload_user upload_path name signify_path seckey_path"
+ do_upload "$(realpath "$file")"
+ ;;
+
+ exec)
+ check_config "url name pubkey_path signify_path cwd cache_file"
+ [ -d "${config[cwd]}" ] || die "invalid ${BOLD}cwd${RST}: no such directory"
+ do_exec
+ ;;
+
+ *)
+ die "invalid command '$command'"
+ ;;
+esac
+