0

I'm cleaning up a hacked website running a PHP-based CMS. Every PHP file on the site has had the following string inserted at the beginning of the file's first line:

<?php /**/ eval(base64_decode("aWYoZnVuY3Rpb25"));?>

(I've truncated the base64 string for clarity.)

My goal is to remove this string via bash script. I first made sure that I could loop through all files.

#!/bin/bash
# de-malware-ifier

for i in $(find ~/Sites/www.domain.com -name '*.php'); do
  echo "file $i"
done

This works as expected, printing out the filenames of the several hundred infected files.

I then tried to modify the bash script to replace-in-place the evil string for each of these files:

#!/bin/bash
# de-malware-ifier

for i in $(find ~/Sites/www.domain.com -name '*.php'); do
  echo "file $i"
  evil='<?php /**/ eval(base64_decode("aWYoZnVuY3Rpb25"));?>'
  sed 's/$evil//'
done

However, running this script hangs on the first file. Why is this script hanging, and how should I modify this script to give me the result I want?

I am on Mac OSX.

unruthless
  • 507
  • 5
  • 8
  • In general, reversing the changes *that you are aware of* is not enough to ensure that you have regained control of the server. The attackers may have installed a backdoor (or several) that'll allow them right back in after you have "cleaned" the system. The best approach after a server compromise is to rebuild it from scratch, or restore from a backup from before the intrusion happened. See [this ServerFault question and answers](http://serverfault.com/questions/6190/reinstall-after-a-root-compromise). – Gordon Davisson May 17 '14 at 00:24
  • 2
    Beware about the `for i in $(command)` ... you should avoid this syntax. See this answer to get more details : http://stackoverflow.com/questions/19606864/ffmpeg-in-a-bash-pipe/19607361#19607361 – Idriss Neumann May 17 '14 at 07:15
  • @GordonDavisson Thanks, that's a helpful tip for others reading this question. In my specific case, www.domain.com was overdue to be taken offline anyway, which I've done. I'm treating this as a learning exercise in basic bash scripting rather than hacked server recovery. – unruthless May 19 '14 at 03:34

4 Answers4

1

The reason it's hanging is because you're not giving sed a filename, so it's waiting for input on stdin.

To edit your file, you should use:

sed -i bak 's/foo/bar/' "$i"

Note that this is not sufficient to fix your script. Other problems include:

  1. Your pattern contains a lot of characters that are special to sed. You'd have to escape them. See if you can use fgrep -v instead.
  2. $evil won't expand in single quotes. Use double quotes.
that other guy
  • 116,971
  • 11
  • 170
  • 194
  • `fgrep -v` will remove the entire line that matches, not just the matching portion of the line. – Barmar May 16 '14 at 23:56
  • Can you explain what `bak` does in the context of that command? I'm new to sed and bash scripting in general. – unruthless May 17 '14 at 00:25
  • 1
    From `man sed`: `-i extension` Edit files in-place, saving backups with the specified extension. If a zero-length extension is given, no backup will be saved. It is not recommended to give a zero-length extension when in-place editing files, as you risk corruption or partial content in situations where disk space is exhausted, etc. – that other guy May 17 '14 at 00:50
0

Sed is missing input.

Try this:

#!/bin/bash
# de-malware-ifier

for i in $(find ~/Sites/www.domain.com -name '*.php'); do
   echo "file $i"
   evil='<?php \/\*\*\/ eval(base64_decode("aWYoZnVuY3Rpb25"));?>'
   sed  -i "s/$evil//" $i
done

PS: I am not sure if you need to escape something else on "$evil".

Idriss Neumann
  • 3,760
  • 2
  • 23
  • 32
Tiago Lopo
  • 7,619
  • 1
  • 30
  • 51
  • Beware about the `for i in $(command)` ... you should avoid this syntax. See this answer to get more details : http://stackoverflow.com/questions/19606864/ffmpeg-in-a-bash-pipe/19607361#19607361 – Idriss Neumann May 17 '14 at 07:15
  • 1
    Never use `for i in` like that, never try to use sed when you DON'T want an RE in the search space (you missed escaping `?`s and maybe more), and always quote your variables (`"$i"`) to avoid word splitting and globbing issues. – Ed Morton May 17 '14 at 13:36
0

As others pointed out, you were missing the file name for the sed command, but don't try to use sed for this as sed cannot operate on a string, only an RE. Instead of wasting their time on the cosmetic -i option for sed, the GNU guys would've done a LOT more good if they could have provided a flag to tell sed to treat it's search pattern as a string instead of a regexp.

Anyway - try this instead:

tmp="/usr/tmp/tmp$$"
trap 'rm -f "$tmp"; exit' 0
find ~/Sites/www.domain.com -name '*.php' |
while IFS= read -r i; do
  echo "file $i"
  evil='<?php /**/ eval(base64_decode("aWYoZnVuY3Rpb25"));?>'
  awk -v evil="$evil" 's=index($0,evil){$0 = substr($0,1,s-1) substr($0,s+length(evil)} 1' "$i" > "$tmp" $$ mv "$tmp" "$i"
done

I also fixed your loop on file names. Never use for i in $(...) as it will fail for file names that contain any white space. The loop I posted will only fail if you have file names that contain newlines.

GNU awk has a -i inplace flag if you want to avoid manually specifying the tmp file.

Ed Morton
  • 188,023
  • 17
  • 78
  • 185
0

Goal:

Remove <?php /**/ eval(base64_decode("aWYoZnVuY3Rpb25"));?> from the beginning of every PHP file's first line using the stream editor, sed.

Discussion:

The stream editor has implicit and explicit line addressing. If you omit line addresses (numeric, regular expressions, or a combination of both), then the entire file is processed.

Point 1:

If you only want to target the first line, then you should specify it explicitly.

sed -i '1s/<pattern>/<substitution>/' <filename>

However, since you are trying to rid your files of "evil", you probably want to remove "evil" anywhere (globally) it is found on the first line.

sed -i '1s/<pattern>/<substitution>/g' <filename>

Point 2:

The "evil" you are dealing with uses non-alpha numeric characters, so you must be wary of using it as input in various contexts. In order to use a regular expression to search for regular expression meta-characters (?, +, *, [, ], ., et al), you must either:

  1. Escape the meta-characters with backslashes to avoid pattern collisions (Example: \?), or

  2. Change the regular expression pattern delimiter to avoid a pattern collision, or

  3. Both (This is what you should do in this case).

In sed, you can change the regex pattern delimiter by escaping a character before your pattern begins.

Example:

sed -i '1s\#<pattern>#<substitution>#g' <filename>

Point 3:

You can search for strings as a <pattern> with regular expressions in sed! By definition, the most basic pattern is a sequence of characters. However, you must adhere to point number two above and escape any regex meta-characters, or the default pattern delimiter, /, if necessary.

Solution 1:

Your evil, I mean regex pattern, has regex meta-characters and the default pattern delimiter embedded in it!

<?php /**/ eval(base64_decode("aWYoZnVuY3Rpb25"));?>

I would prescribe the following. Notice that I am now using double quotes because I want the shell to do variable interpolation before executing sed. Also, because I changed the regex pattern delimiter to #, I did not need to escape the two forward slashes associated with that micro block quote. :-)

#!/bin/bash

function evilRemover ()
{
    pattern='\<\?php /\*\*/ eval\(base64_decode\("aWYoZnVuY3Rpb25"\)\);\?\>'
    local IFS="\n"

    for filename in "$@"; do
        sed -i "1s\#${pattern}##g" "$filename"
    done
}

evilRemover $(find ~/Sites/www.domain.com -name '*.php' -print)

Note: I will go out on a limb and say that anyone that puts white spaces in their file names should consider using the underscore, _, instead.

Mr. @Ed Morton above is trying to warn against the possibility of word splitting, but "$@" should prevent it if you pass your list into a function like above.

Hidden, non-printing characters in file names can be hard to deal with, but this specific solution should work for your problem to a high degree of certainty (99.9999%).

Solution 2:

More generically:

#!/bin/bash

function deleteWordsFromLine ()
{
    lineNumber=$1
    pattern=$2
    local IFS="\n"

    shift 2

    for filename in "$@"; do
        sed -i "${lineNumber}s\#${pattern}##g" "$filename"
    done
}

targetLine=1
word='\<\?php /\*\*/ eval\(base64_decode\("aWYoZnVuY3Rpb25"\)\);\?\>'
filenames=$(find ~/Sites/www.domain.com -name '*.php' -print)

deleteWordsFromLine $targetLine $word $filenames

Solution 3:

In the event that it would be better to delete the first line of all the files ...

#!/bin/bash

function deleteLine ()
{
    lineNumber=$1
    local IFS="\n"

    shift 1

    for filename in "$@"; do
        sed -i "${lineNumber}d" "$filename"
    done
}

targetLine=1
filenames=$(find ~/Sites/www.domain.com -name '*.php' -print)

deleteLine $targetLine $filenames

Final Note:

Be sure to execute this solution with enough permissions, or else the find command will return messages to stderr in the following format.

find: '/some/dir/file.php': Permission denied
Anthony Rutledge
  • 6,980
  • 2
  • 39
  • 44