0

I found lot's of close questions, but not the exact fit to my use case, so had to open a new one.

Guess a system that has a password consisting of all lowercase and uppercase letters, numbers and special characters (a-z,A-Z,0-9, #) in multiple configuration files (the same password is used by a web-application on main domain, its sub-domains and all the relevant databases). Example of such file with a password would be:

$databases = array (
  'default' =>
  array (
    'default' =>
    array (
      'database' => 'database_name',
      'username' => 'datbase_user',
      'password' => 'yv[48k2)KMGx{g1b',
      'host' => 'localhost',
      'port' => '',
      'driver' => 'mysql',
      'collation' => 'utf8mb4_general_ci',
      'charset' => 'utf8mb4',
      'prefix' => '',
    ),
  ),
);

I am trying to write a bash-code that takes in automatically generated new password (with the same algorithm - all lowercase and uppercase letters, numbers and special characters), finds all occurrences of the old password in home directory of respective parent domain and its subdomains and replaces with the new password. The following one-liner works fine:

grep -rlF "$old_pass" /path/to/directory | xargs sed -i "s/$old_pass/$new_pass/g"

for passwords consisting of only letters and digits (e.g. qVYYgB5bRxUYpHg). However, while the grep part works fine, the sed part fails as soon as the system adds special characters to a generated password (e.g.aN#zDU>lve15Uwqn) giving:

sed -i "s/$old_pass/$new_pass/g" password.php
sed: -e expression #1, char 37: unterminated `s' command

What is the best way to replace all occurrences of an existing password that consists of all the letters, numbers and special characters stored in different variables in multiple files?

Nick
  • 207
  • 3
  • 11
  • 1
    what you've posted now is a very different problem with a very different correct solution to any of the answers posted so far (hint: you would not search for the original password). I don't want to go around again but hopefully others will. Good luck. – Ed Morton Nov 06 '18 at 00:22
  • Sorry, Ed, I first thought the problem is just `sed` part so decided to concentrate attention around it, but when people started to suggest to operate the given file, I had to explain I don't have specific files, but rather outputs of grep search. – Nick Nov 06 '18 at 08:30
  • Do the configuration files have a common name (e.g. `config.txt`) or common suffix (e.g. `foo,cfg`, `bar.cfg`, etc.) or any other way to distinguish them by their name from every other file? – Ed Morton Nov 06 '18 at 12:45
  • The end-user's application can have any kind of file, so unfortunately I can not rely on any pattern in file names, but search with grep and then operate files. – Nick Nov 06 '18 at 13:07
  • It's such a flawed approach though - you're relying on the old password string not being part of some longer string and not existing in any other file. You could badly corrupt your files doing this. You should at least be testing for the `'` delimiters and the word `password`. I'd be searching for `'password' => 'yv[48k2)KMGx{g1b',` at least, not just `yv[48k2)KMGx{g1b` or I'd probably actually be searching for `'password' => '[^']+',` (and maybe `$databases = array` to make sure I was in the right file type) unless the actual old password is necessary. – Ed Morton Nov 06 '18 at 13:15
  • We have quite long 32 character passwords, so if there is a slight chance that that long password can happen to be part of another string in the system, then I'll take my chances. However, I see your points, they are legitimate and ideally most possible variables like, for example, `password`, `pass`, `pswd`, `pwd` should be checked. But then some applications could easily use completely occasional variables, especially if written by non-English speakers. So there is no absolute solution to this problem anyway. – Nick Nov 06 '18 at 18:29

3 Answers3

0

Your sed expression is wrong because you need to escape the square bracket [:

sed -i 's/yv\[48k2)KMGx{g1b/aN#zDU>lve15Uwqn/g' password.php

To avoid shell parameter expansion, use single in quote the sed expression.

If you want to match all letters, numbers and special characters (assuming punctuation), you can use class characters:

sed 's/[[:punct:][:alnum:]]*/NEWPASSWORKD/g' password.php
oliv
  • 12,690
  • 25
  • 45
  • Sorry I missed to mention the passwords are actually stored in variables, so I have to use double quotes. `sed "s/[[:punct:][:alnum:]]*yv[48k2)KMGx{g1b/aN#zDU>lve15Uwqn/g" password.php` didn't work. – Nick Nov 05 '18 at 14:51
  • As I already mentioned, if you use litteral square bracket `[`, you must escape the character with a backslash. – oliv Nov 05 '18 at 15:01
  • Your example misses the old password completely. Please give a working answer. – Nick Nov 05 '18 at 20:22
0

I think the possible permutations of autogenerated passwords may be beyond the scope of what you are going to be able to easily accommodate with a sed, but the crux of the matter is protecting the metacharacters. For this specific example:

$: echo "$b"              # the desired string
aN#zDU>lve15Uwqn
$: echo "$a"              # the current string, with embedded metachars
yv[48k2)KMGx{g1b
$: echo "$a" |            # pipe it to a sed to clean it
>    sed 's/\W/\\&/g'     # backquote all non-word chars
yv\[48k2\)KMGx\{g1b
$: echo "$a" | sed 's/\W/\\&/g'>a # save to a file
$: cat a
yv\[48k2\)KMGx\{g1b
$: echo "sed -E 's/$(<a)/$b/g'">x # save the sed to be run to a file
$: cat x
sed -E 's/yv\[48k2\)KMGx\{g1b/aN#zDU>lve15Uwqn/g'
$: echo $a | . x          # source it
aN#zDU>lve15Uwqn

All this saving to a file is not entirely necessary, but it makes it a little easier to manage your quoting. As Ed mentioned below, you'd basically have to do the same thing for the new password.

So your actual script:

 echo "$old_pass" | sed 's/\W/\\&/g'> old_pat
 echo "$new_pass" | sed 's/\W/\\&/g'> new_pat
 echo "sed -E 's/$(<old_pat)/$(<new_pat)/g'" > src # save the command to a file
 echo "$old_pass" | . src                        # source it

I didn't include the -i to do an inplace edit, as you'll want to vet the process first.

Hope this helps someone, but generally, this is one of those cases even I would probably use awk... or more likely, to be honest, just refactor the logic. :)

...you aren't hardcoding all those passwords in that php file, are you? :D

Paul Hodges
  • 13,382
  • 1
  • 17
  • 36
  • 1
    You need to sanitize `b` too to accommodate `&`, `\1`, `/`, etc. and you'd need to add code to handle `'`s (maybe map it to some octal value or use `"` delimters so you CAN escape those but then you'd need to double up the escapes everywhere else or us `[]`?). There may be others. Or to put it another way - don't even.... :-). – Ed Morton Nov 05 '18 at 16:52
  • I'm inclined to agree, Ed. This just isn't something I would do in sed. A solution to the example given isn't going to handle everything. – Paul Hodges Nov 05 '18 at 16:55
  • You can handle everything in sed (see https://stackoverflow.com/a/29626460/1745001) but it's just not worth the trouble when you can simply use literal strings in awk. I've often wished sed had a "use literal strings" option but oh well... – Ed Morton Nov 05 '18 at 16:56
  • 1
    Yep. A 'just don't parse this' option would be really nice, lol – Paul Hodges Nov 05 '18 at 16:59
  • 1
    Right - `sed --mc-hammer ...`. – Ed Morton Nov 05 '18 at 17:02
  • 1
    LOL!! I almost fell out of my chair. :) – Paul Hodges Nov 05 '18 at 17:06
  • Thanks for all ideas. I don't have to stick with `sed`. Could you please re-read my question edited with additional details and give the working example code for my sue case. Using `awk` would be ok. – Nick Nov 05 '18 at 20:32
0

Better to use perl here due to it's support of /\Q...\E/ regex pattern that treats all letters within these tags literally (no special regex meaning).

Here is an example:

cat file

abc123
pass=yv[48k2)KMGx{g1b
foobar

Now use this perl command:

old_pass='yv[48k2)KMGx{g1b'
new_pass='aN#zDU>lve15Uwqn'

perl -pe "s~\\Q${old_pass}\\E~\\Q$new_pass~g" file

abc123
pass=aN\#zDU\>lve15Uwqn
foobar

If you don't have perl, then you may use this awk that uses sting search without using any regex:

awk -v old="$old_pass" -v new="$new_pass" 'n=index($0, old) { 
   $0 = substr($0, 1, n-1) new substr($0, n + length(old)) } 1' file
anubhava
  • 761,203
  • 64
  • 569
  • 643
  • That awk script will fail if the passwords contain backslashes, hence [my use of ENVIRON](https://stackoverflow.com/a/53158368/1745001). I wouldn't normally quibble but since the question is specifically about variables containing "special" chars.... – Ed Morton Nov 05 '18 at 16:43
  • 1
    Oh yes that's right, some backslash might create issues with `-v` – anubhava Nov 05 '18 at 18:17
  • The example with perl outputted the desired result, however file isn't saved. How could I edit with perl in place? Appending the `> new_file` and then overriding the old file works, however I hope there is a shorter version. – Nick Nov 05 '18 at 22:56
  • Please note that I have to append perl command to grep like `grep -rlF "$old_pass" | xargs perl -pe "s~\\Q${old_pass}\\E~\\Q$new_pass~g"`, so no file names can be provided on command line. – Nick Nov 05 '18 at 23:09
  • After digging little bit further on this, I've found out I could use `perl -pi -e` to edit found files in place. However, with `old_pass="*DGB9Twq7WTwz@wR"` and `new_pass="tDx6U&ShRv}E3Mdb"` set using the `grep -rlF "${old_pass}" | xargs perl -pi -e "s~\\Q${old_pass}\\E~\\Q$new_pass~g"` command results in `tDx6U\&ShRv\}E3Mdb@wR` instead of just `tDx6U&ShRv}E3Mdb`. And I need to replace any kind of special characters. – Nick Nov 05 '18 at 23:31
  • Your awk example outputted the right result, however since I have GNU Awk 4.0.2 it doesn't have in place editing option, so hitting the wall with it too. – Nick Nov 05 '18 at 23:52
  • 1
    Your answer was closest to then narrowing the desired effect on https://stackoverflow.com/a/53170714/5797450 Thanks! – Nick Nov 06 '18 at 13:10