I have php website for prevent hacker modify any file and inject backdoor.
I want to use git
as a file tracking solution. This is my shell script:
#!/bin/sh
msg_i(){
echo "[I] 1ドル"
}
msg_e(){
echo "[E] 1ドル"
}
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
msg_e "Not in a git repository."
exit 1
fi
GIT_DIR=$(git rev-parse --git-dir)
export GIT_DIR="$GIT_DIR"
BRANCH_NAME=main
AUTOBACKUP_BRANCH_NAME=main-autobackup
if ! git show-ref --quiet "refs/heads/${AUTOBACKUP_BRANCH_NAME}"; then
if git branch "${AUTOBACKUP_BRANCH_NAME}"; then
msg_i "Created new branch ${AUTOBACKUP_BRANCH_NAME}."
else
msg_e "Failed to create branch ${AUTOBACKUP_BRANCH_NAME}."
fi
fi
git add .
AUTOBACKUP_TREE=$(git write-tree)
AUTOBACKUP_PARENT_COMMIT=$(git show-ref --hash --heads $AUTOBACKUP_BRANCH_NAME)
AUTOBACKUP_DIFF_INDEX=$(git diff-index "${AUTOBACKUP_PARENT_COMMIT}" --name-status)
if [ -z "$AUTOBACKUP_DIFF_INDEX" ]; then
msg_i "No changes detected in branch $AUTOBACKUP_BRANCH_NAME."
else
DIFF_INFORMATION=$(echo "$AUTOBACKUP_DIFF_INDEX" | awk '{status=1ドル; file=substr(0,ドル index(0,ドル2ドル)); if (status == "A") desc="added"; else if (status == "C") desc="copied"; else if (status == "D") desc="deleted"; else if (status == "M") desc="modified"; else if (status == "R") desc="renamed"; else if (status == "T") desc="type changed"; else if (status == "U") desc="unmerged"; else if (status == "X") desc="unknown"; else if (status == "B") desc="pairing broken"; else desc=status; printf "%s %s\n", desc, file}')
AUTOBACKUP_COMMIT_MESSAGE=$(echo "$DIFF_INFORMATION" | awk '{print 1ドル}' | sort | uniq -c | awk '{print 2ドル ": " 1ドル}' | paste -sd " " -)
AUTOBACKUP_COMMIT_HASH=$(git commit-tree "${AUTOBACKUP_TREE}" -p "${AUTOBACKUP_PARENT_COMMIT}" -m "${AUTOBACKUP_COMMIT_MESSAGE}" -m "${DIFF_INFORMATION}")
echo "${AUTOBACKUP_COMMIT_HASH}" | xargs git branch -f $AUTOBACKUP_BRANCH_NAME
DIFF_OUTPUT=$(git diff-tree --no-commit-id --name-status -r "$(git show-ref --hash --heads $AUTOBACKUP_BRANCH_NAME)~1" "$(git show-ref --hash --heads $AUTOBACKUP_BRANCH_NAME)" --)
if [ -z "$DIFF_OUTPUT" ]; then
msg_i "Last commit in $AUTOBACKUP_BRANCH_NAME is empty, deleting last commit."
git update-ref refs/heads/$AUTOBACKUP_BRANCH_NAME "${AUTOBACKUP_COMMIT_HASH}~1"
else
msg_i "Changes detected in branch $AUTOBACKUP_BRANCH_NAME."
fi
fi
if [ "$(git rev-list --count "${AUTOBACKUP_BRANCH_NAME}..${BRANCH_NAME}")" -gt 0 ]; then
msg_i "Merging ${BRANCH_NAME} into ${AUTOBACKUP_BRANCH_NAME}."
MERGE_BASE=$(git merge-base "${BRANCH_NAME}" "${AUTOBACKUP_BRANCH_NAME}")
git merge-tree "${MERGE_BASE}" "${BRANCH_NAME}" "${AUTOBACKUP_BRANCH_NAME}"
MERGE_COMMIT=$(git commit-tree "${AUTOBACKUP_TREE}" -p "${AUTOBACKUP_BRANCH_NAME}" -p "${BRANCH_NAME}" -m "update branch" -m "Silently merged ${BRANCH_NAME} into ${AUTOBACKUP_BRANCH_NAME}.");
echo "${MERGE_COMMIT}" | xargs git branch -f $AUTOBACKUP_BRANCH_NAME
fi
git push origin $AUTOBACKUP_BRANCH_NAME
This script idea based on this https://stackoverflow.com/a/25520876/12716228 and add some modification.
This script result is:
Did I miss something before I run this on server with systemd timers or cron?
-
\$\begingroup\$ Don't use all upper case names for non-exported variables, see the answers at correct-bash-and-shell-script-variable-capitalization for why it matters. \$\endgroup\$Ed Morton– Ed Morton2024年10月31日 22:44:25 +00:00Commented Oct 31, 2024 at 22:44
2 Answers 2
security
prevent hacker modify any file
If you feel your server setup leaks like a sieve and has likely vulnerabilities, that's worth pursuing on its own, perhaps with an OWASP checklist. Asking the current script to shut the barn door after the cows got out is attending to matters on the late side.
Being able to rebuild your production webserver from scratch is a very good goal, and necessary when recovering from a known breach. We store the source code somewhere that is less exposed to attack, and trust those source files when creating a new webserver. Consider using a Dockerfile to do that. A great many cloud providers would be happy to run your docker image for you.
Of course, if there's some vuln in that image which the hacker knows about, merely rewriting a backdoored server with a fresh image won't do much good, since the hacker can quickly re-exploit it in the same way. So detection and fixing vulnerabilities are still important.
remote server
BRANCH_NAME=main
AUTOBACKUP_BRANCH_NAME=main-autobackup
By hypothesis, you view the webserver as p0wned. So an attacker could alter both branches, and indeed could replace the repo or rewrite history to cover his tracks. Putting the autobackup "source of truth" on an assumed compromised server seems unwise.
shell
#! /bin/sh
Consider running under bash
instead.
My concern is that your interactive shell is different from the target shell, but it's pretty close, so you will test things interactively and then be surprised by some subtle behavior difference in production.
stderr
This is nice:
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
Possibly it was copy-n-pasted from somewhere.
Consider allowing stderr to be displayed.
We expect it will get zero lines of output,
but if it does they will be interesting.
I view the intent of the dev-null redirect
as simply discarding the expected "true".
In the error case we're going to have some [E]
chatter anyway.
fatal error
msg_e "Failed to create branch ${AUTOBACKUP_BRANCH_NAME}."
That looks fatal to me.
You should probably bail at that point,
similar to the exit 1
up above in the case that there's no repo.
add vs remove
git add .
This grabs any new or edited files, great!
I note in passing that on the main branch we might have done git rm foo
,
and the autobackup branch won't track such changes.
single column
The assignment to DIFF_INFORMATION turns a single-letter text column
into a multi-word column which cannot be reparsed.
In particular the awk
in the following line makes
references to 1ドル
and 2ドル
which are clearly incorrect.
Filenames can contain SPACE characters, and the
"pairing broken"
description contains a SPACE character.
Prefer e.g. "pairing_broken"
.
The very long one-liner awk
script is inconvenient.
Consider breaking it up, making it a separate script file,
or using /usr/bin/join
in its place.
undo
This check seems to appear a bit late:
if [ -z "$DIFF_OUTPUT" ]; then
msg_i "Last commit in $AUTOBACKUP_BRANCH_NAME is empty, deleting last commit."
git update-ref ...
Instead of an unconditional git diff-tree
,
why not do that just in the case where we have diffs?
history
Aha! It looks like this is what tries to thwart an attacker who rewrites history:
git push origin $AUTOBACKUP_BRANCH_NAME
The OP script is somewhat long. If I was an attacker who compromised the server, I think I would just alter the OP script so it seems to be sending "happy happy!" autobackups to the origin server, while ignoring the trojan binaries.
Consider adopting this alternate approach:
- A secure internal auditing server uses
rsync -a --checksum webserver:/opt/website .
to efficiently grab a current copy of production files - Audit server then performs diffing, change tracking, auditing and alerting on its local copy
This forces the attacker to rewrite the production rsync
, which is certainly feasible.
A little better would be a webserver that is an NFS client which mounts /opt/website, and the audit server syncs from the NFS server, which presumably may have had its data files but not its code altered.
assumptions
Did I miss something
The original question you cited was just looking to make a backup which is pushed to another server. Your security-oriented use case is quite different, as you assume an attacker can do arbitrary writes to the webserver's filesystem. Running the scheduler, the diffing, and the alerting on a server which the attacker is assumed to control is not appropriate. Prefer to put as much auditing functionality as feasible on a separate audit server that is deliberately designed to have a tiny attack surface.
-
\$\begingroup\$ Thankyou for verry clear explanation, I know git is non standard way for backup, I just try follow my idea, until found best way for monitoring website, my plan using this script is for managing simple wordpress website, I have setup standard backup all file each day, and this git backup for secondary backup (with bare repo and run in root level, my main origin branch also protected, origin git access only autobackup branch), I just imagine someday evil inject
function.php
with@eval($_POST);
and I didnt notice that. \$\endgroup\$Anas– Anas2024年09月13日 19:08:39 +00:00Commented Sep 13, 2024 at 19:08
Readablility
I whole-heartedly agree with the previous answer regarding the DIFF_INFORMATION
assignment line. Even on my wide monitor, I need to scroll horizontally a lot. This makes the line hard to read. Since it is hard to read, it is hard to understand. Therefore, it will be hard to debug (if there are problems) and maintain (if you need to add or remove functionality). Although you may understand this code because it is fresh in your mind, you (and others) will struggle 6 months from now.
It is crucial for code to be readable to humans. This line must be refactored.
Layout
I like the functions at the top of the code, and the code has consistent indentation. It would be nice to add a little more vertical whitespace. For example, add a blank line after each function and before and after each if/fi
construct:
#!/bin/sh
msg_i(){
echo "[I] 1ドル"
}
msg_e(){
echo "[E] 1ドル"
}
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
msg_e "Not in a git repository."
exit 1
fi
GIT_DIR=$(git rev-parse --git-dir)
Documentation
Add header comments to the top of the code to describe its purpose:
#!/bin/sh
# Use git as a file tracking solution to
# prevent hacker modify any file and inject backdoor...
# Add a summary of what the code does here.
-
\$\begingroup\$ Thankyou for your comment, I will rewrite my code to be readable to humans and add documentation ( I'am always miss this ). \$\endgroup\$Anas– Anas2024年09月13日 19:12:20 +00:00Commented Sep 13, 2024 at 19:12