Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

Hide DBI password in scripts

by danielgr (Novice)
on Jan 12, 2018 at 20:37 UTC ( #1207165=perlquestion: print w/replies, xml ) Need Help??
danielgr has asked for the wisdom of the Perl Monks concerning the following question:

We are using a shared Linux account to run database scripts and CGI programs. I thought of the following method of hiding the $password in a script, but was not able to google its viability:

1. $password is obfuscated in a compiled C program that breaks up the password so it won't be visible via "strings".
2. The C program will only return the password to registered calling programs or scripts - and checks the registered inode value to ensure it was not altered.

Some code fragments follow to show how some of this information is gathered from C:

parentpid = (int) getppid(); printf("%d\n", parentpid); sprintf(a,"cat /proc/%d/cmdline;echo", parentpid); sprintf(b,"cat /proc/%d/comm;echo", parentpid);

Most of the posts I've seen say it is impossible to achieve unbreakable security in this respect. However, I was wondering if anyone has tried this technique or similar for Perl scripts?

Replies are listed 'Best First'.
Re: Hide DBI password in scripts
by afoken (Abbot) on Jan 12, 2018 at 23:41 UTC
    Most of the posts I've seen say it is impossible to achieve unbreakable security in this respect.

    Whatever you do, you will end with a program (C, Perl, whatever) that contains two parts:

    1. an encrypted or obfuscated secret (password or username and password)
    2. all code required to decrypt or de-obfuscate the secret

    In pseudo-code:

    my $secret="(binary garbage here)"; sub checkSecurity; sub decodeSecret; sub reencode; if (checkSecurity()) { # <--- bypass the function call or make the fu +nction return TRUE my $plaintext=decodeSecret($secret); # breakpoint here say $plaintext; # or, if you like: say reencode($plaintext); } else { die "Insecure condition found, won't tell you the secret!\n"; }

    It should be obvious that this can be broken quite easily. Start the program under control of a debugger and let it run to the place where it has decrypted or de-obfuscated the secret, break and dump the secret. Yes, there are ways to make exactly this harder. Search the web for anti-debugging techniques and how to bypass them. You can and should include the security checks in the decoder. But again, you can bypass those checks.

    But if you use an external program from a perl script, it is even easier: Just make the perl script print the secret. Everything needed to make the external program happy must be present in the perl script. For a DBI application, just replace DBI->connect() with die. You don't even have to modify the perl script, a tiny module like the following, combined with perl -MDBIspy victim.pl should be sufficient.

    package DBIspy; use strict; use warnings; use feature 'say'; use DBI; no warnings 'redefine'; sub DBI::connect { say for @_; die 'BROKEN'; } 1;

    Demo:

    > perl -MDBIspy -E 'use DBI; DBI->connect("dbi:SQLite:","user","passwo +rd")' DBI dbi:SQLite: user password BROKEN at DBIspy.pm line 12. >

    There are several password managers available, commonly bundled with the Linux desktop environment you use. Whatever interface it uses, guess what happens when you need to call DBI->connect() with a secret from the password manager. Right, you need code that finally extracts the secret as plain text. And guess what happens when you use that ridiculously trivial DBIspy module. Your secret is no longer secret.

    Alexander

    --
    Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

      Not sure if I misunderstood:

      1. In order to use the C debugger, you would need access to the source. The source can be stashed away after the executable is created. - sorry this is a misstatement. It has been a long time since I've used gdb, so the source may not be required. Ahh! Just realized you were talking about the Perl debugger.

      2. The C program would not relinquish the password if the inode of the registered script has changed, meaning the script has changed - so a hacker can't add a "die" or just print the password.

        Not sure if I misunderstood:

        1. In order to use the C debugger, you would need access to the source. The source can be stashed away after the executable is created. - sorry this is a misstatement. It has been a long time since I've used gdb, so the source may not be required. Ahh! Just realized you were talking about the Perl debugger.

        No, I was not talking specifically about the perl debugger, but of course, the perl debugger usually has source access (except for XS code).

        Source code is not needed to debug a compiled program. You won't have nice C source in the debugger output, but you can step through the disassembled code and set breakpoints, even if the debug information has been stripped from the binary.

        Semi-automatic disassemblers like IDA can be very helpful to find places to set breakpoints at runtime. Just reading the relevant parts of the disassembly can be sufficient, if the de-obfuscation / decryption code is a single function. And yes, IDA can disassemble executables for many different processors and operating systems.

        2. The C program would not relinquish the password if the inode of the registered script has changed, meaning the script has changed - so a hacker can't add a "die" or just print the password.

        I can make the perl program print out the secret and stop without ever touching the main script. Have a look at how I injected the DBIspy module.

        I can have a second perl script modify the script and restore all meta-data in the inode (see lstat and especially utime). File size won't change for an injected die, I can easily pad the file with some commented-out garbage to the old size.

        I can use environment variables to modify @INC and thus load modified modules. Your code surely uses strict somewhere. Guess how hard it would be to include DBIspy or similar in a modifed copy of strict.pm.

        I can use LD_PRELOAD to load an additional library into the perl interpreter. Code in the library would be executed before perl's main(), would call the magic password program and abort the program before perl's main() is reached.


        Update: strace-demo

        /tmp>cat secret-keeper.c #include <stdio.h> int main(int argc, char ** argv) { // note: security checks omitted fputs("find me, I'm the secret",stdout); return 0; } /tmp>make secret-keeper cc secret-keeper.c -o secret-keeper /tmp>strip secret-keeper /tmp>cat secret-reader.pl #!/usr/bin/perl use strict; use warnings; my $secret=qx(./secret-keeper); print "The secret is <$secret>\n"; /tmp>perl secret-reader.pl The secret is <find me, I'm the secret> /tmp>strace -o trace perl secret-reader.pl The secret is <find me, I'm the secret> /tmp>grep -C5 find trace /dev/null trace-read(5, "", 4) = 0 trace-close(5) = 0 trace-ioctl(3, TCGETS, 0x7ffeddbf80a0) = -1 ENOTTY (Inappropria +te ioctl for device) trace-lseek(3, 0, SEEK_CUR) = -1 ESPIPE (Illegal see +k) trace-fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 trace:read(3, "find me, I'm the secret", 8192) = 23 trace-read(3, "", 8192) = 0 trace---- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12034, + si_uid=1001, si_status=0, si_utime=0, si_stime=0} --- trace-fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 trace-close(3) = 0 trace-wait4(12034, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = + 12034 trace:write(1, "The secret is <find me, I'm the "..., 40) = 40 trace-rt_sigaction(SIGHUP, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGINT, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGQUIT, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGILL, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGTRAP, NULL, {SIG_DFL, [], 0}, 8) = 0 /tmp>

        Alexander

        --
        Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
        The C program would not relinquish the password if the inode of the registered script has changed, meaning the script has changed - so a hacker can't add a "die" or just print the password.
        Oh, woe is me! I can't modify the Perl script to print out the parameters it passes to DBI::connect!

        Guess I'll edit DBI.pm instead and have the connect method print @_ before doing anything else.

        As afoken said, if you have both the hidden secret and the means of revealing the secret in the same place at the same time, then an attacker will be able to learn the secret, whether by subverting your decoding process or by learning how it works and applying it himself. You can make this more difficult, but you can never make it impossible, as has been repeatedly demonstrated by the ongoing wars between digital content providers and digital content pirates. The content providers have thrown boatloads of money at it and hired people far smarter than you or me, and the pirates invariably crack each new scheme in far less time than it took to devise and implement the scheme in the first place (often within a single day).

      We can eliminate the DBIspy threat by copying in the text of the DBI::connect sub to the caller program:

      sub DBI::connect { # code from original DBI::connect }


      This of course would have to be updated manually if a new version of DBI was installed.
      And, we can assume that the DBI.pm module is write-protected from all except root. (We can assume root is not compromised).
      Also, instead of the inode we can use checksums.

        We can eliminate the DBIspy threat by copying in the text of the DBI::connect sub to the caller program:

        sub DBI::connect { # code from original DBI::connect }

        Yes, sure. And I can read it from the script to be executed and look for functions called from the copied DBI::connect. Most trivially, the next function to attack would be the DBD's connect method. A few lines of code should be sufficient to load and patch the DBD before the script starts, very similar to my DBIspy hack.

        This of course would have to be updated manually if a new version of DBI was installed.

        So you are creating a maintainance nightmare here, for little or no gain.

        And, we can assume that the DBI.pm module is write-protected from all except root.

        Only if you use the system perl or a local perl installed by root. It won't be protected if perl is installed by the user running the script.

        (We can assume root is not compromised).

        Now, that's a very bold statement.

        With physical access to the machine, root is compromised within minutes on a standard installation of many Linux distributions. Simply because the boot loader does not prevent me from temporarily adding init=/bin/sh rw to the kernel's command line. After that, I have a root shell without a password prompt and can modify any file on the system. Or, I could reboot and run my own Linux from a CD or a USB stick to modify any file on the system. (So yes, you should prevent physical access, add BIOS and boot loader passwords, and encrypt all disks, if secret data is stored on a machine.)

        Without physical access, there are still many ways to get root access. Linux has security problems, like any other system. To make things worse, CPU bugs like Meltdown und Spectre hardware bugs can help gaining root access.

        But luckily, I don't need root access.

        Also, instead of the inode we can use checksums.

        Yes, but they won't help you against LD_PRELOAD. The perl script is unmodified, but I will still get the secret.

        Heck, I could even break checksum tests from within perl, with a perl loader script that has the following behaviour:

        1. Create a backup copy of victim.pl, including a-time and m-time (lstat).
        2. Completely replace the content of victim.pl with an attack script.
        3. exec("victim.pl",@ARGV), running the attack script.

        The attack script does the following:

        1. Restore victim.pl content from the backup copy. Note: at this point, perl has completely read the attack script from victim.pl and no longer cares about the content of victim.pl.
        2. Restore the original a-time and m-time of victim.pl (utime). Now, victim.pl looks sane and unmodified again, and it is. It will pass any checksum test. But perl is running my attack code.
        3. Remove the backup copy.
        4. Run any code from victim.pl required to execute secret-keeper (e.g. set up environment variables, file handles, ...)
        5. Run the unmodified, secret-revealing binary, and print out the secret revealed: print qx(./secret-keeper);

        secret-keeper has no chance to detect that attack. My loader script is no longer running. Its last action, exec("victim.pl"), removed all traces from memory. It will look like victim.pl was started directly by my shell. victim.pl's inode and content seem to be unmodified. This is also true for any module victim.pl might load. Whatever action the original victim.pl did before running secret-keeper, my attack script has done the same in the fourth step.

        Alexander

        --
        Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: Hide DBI password in scripts
by afoken (Abbot) on Jan 13, 2018 at 00:20 UTC
    The C program will only return the password to registered calling programs or scripts - and checks the registered inode value to ensure it was not altered.

    Uh, I did not see that. Creative, but breakable. There are editors that can edit files without altering the inode number, vi is the most common one.

    For perl scripts, that does not help much, see my previous posting. You would also have to check all modules that the script may load, including the ones loaded at runtime, and the *.so for XS modules. And you would have to check that no "evil" modules are loaded. The perl debugger could be used to bypass the normal flow of operation, and of course to read the secret, so you need to check for that, too. To make things interesting, the security checks should allow updating perl modules via CPAN or distrubution packages.

    /proc/$PID/comm can be modifed at runtime. I can run any program that I want and change that value so that it passes the check. And it is specific for Linux, not available elsewhere, and not on all Linux versions.

    /proc/$PID/cmdline is read-only, so you could check for some tricks played on the command line. But you also have to check for several environment variables, see perlrun. You also have to check for sitecustomize.pl. And that's just perl. Other scripting languages have similar tricks.

    LD_PRELOAD can do very evil stuff to any dynamically linked program, see ld.so. If I can inject a shared object that calls the secret-emitting program even before main() runs, your secret is no longer secret. So you definitively have to check the environment.

    What happens for strace -o /tmp/trace /home/shared/bin/shared-program? I would expect to find the secret in /tmp/trace.


    Just for fun, read suEXEC Security Model. All of these paranoid checks are there just to safely run CGIs as a different user, this program is not trying to keep anything secret.

    Alexander

    --
    Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

      We can probably solve the perl debugger problem with something like:

      $db=DBI->connect("DBI:Oracle:sid=$DBSID;host=$DBHOST;port=$DBPORT;","$ +DBUSER",`/path/shared_C_program`) || ($err=$db->errstr);


      Thank you for the wealth of information.

        We can probably solve the perl debugger problem with something like:

        $db=DBI->connect("DBI:Oracle:sid=$DBSID;host=$DBHOST;port=$DBPORT;","$ +DBUSER",`/path/shared_C_program`) || ($err=$db->errstr);

        I don't need a debugger for that. strace is sufficient:

        /tmp>cat password-keeper.c #include <stdio.h> int main(int argc, char ** argv) { // note: security checks omitted // note: deobfuscation omitted fputs("postgres",stdout); return 0; } /tmp>make password-keeper cc password-keeper.c -o password-keeper /tmp>strip password-keeper /tmp>cat victim.pl #!/usr/bin/perl use strict; use warnings; use DBI; my $dbh=DBI->connect('dbi:Pg:dbname=postgres','postgres',`./password-k +eeper`,{ RaiseError => 1 }); my $sth=$dbh->prepare('select 42 as answer'); $sth->execute(); $sth->dump_results(); /tmp>strace -f -o trace.txt -e trace=write,process perl victim.pl 42 1 rows /tmp>cat trace.txt 26962 execve("/usr/local/bin/perl", ["perl", "victim.pl"], [/* 40 vars + */]) = 0 26962 arch_prctl(ARCH_SET_FS, 0x7f04ca5ce700) = 0 26962 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETT +ID|SIGCHLD, child_tidptr=0x7f04ca5ce9d0) = 26963 26963 execve("./password-keeper", ["./password-keeper"], [/* 40 vars * +/]) = 0 26963 arch_prctl(ARCH_SET_FS, 0x7f7d4ad21700) = 0 26963 write(1, "postgres", 8) = 8 26963 exit_group(0) = ? 26963 +++ exited with 0 +++ 26962 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=26963, + si_uid=1001, si_status=0, si_utime=0, si_stime=0} --- 26962 wait4(26963, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = + 26963 26962 write(1, "42\n1 rows\n", 10) = 10 26962 exit_group(0) = ? 26962 +++ exited with 0 +++ /tmp>

        Alexander

        --
        Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: Hide DBI password in scripts
by nysus (Priest) on Jan 12, 2018 at 22:24 UTC
Re: Hide DBI password in scripts
by karlgoethebier (Monsignor) on Jan 13, 2018 at 13:12 UTC

    It just came to my mind if this isn't more an issue for your network department:

    ----------------- | bad world | ----------------- | ----------------- | gateway/fw | ----------------- | ----------------- | reverse proxy | ----------------- | ----------------- | app server | ----------------- | ----------------- | db server | -----------------

    Update: See NGINX REVERSE PROXY.

    Update 2: If these bastions are overrun i guess that you have a different problem.

    Just an idea. Best regards, Karl

    «The Crux of the Biscuit is the Apostrophe»

    perl -MCrypt::CBC -E 'say Crypt::CBC->new(-key=>'kgb',-cipher=>"Blowfish")->decrypt_hex($ENV{KARL});'Help

Re: Hide DBI password in scripts
by aitap (Curate) on Jan 15, 2018 at 09:47 UTC

    If you absolutely have to (++)store the secret and the means of revealing the secret on the same machine, you might want to consider Trusted Computing, where the key is sealed inside a special chip (TPM) and the application has to attest itself (measure its state at various stages to prove that it has not been tampered with) to obtain the key back from the TPM.

    The problem is that the trust chain has to wind from as far as BIOS and bootloader (otherwise the attackers will tamper with things you're not attesting1) and that it's somewhat hard to define the state of your application which both is reproducible and proves the absence of tampering. If the former fails, you won't get the correct key even if no attack has happened. If the latter fails, the attackers will find something that's not measured and coerce the application to give up the key. Others have provided lots of examples of state of Perl applications (PERL5LIB, all of %INC, LD_PRELOAD and other dynamic library hacks...) you would have to find and make reproducible if you go down this road. Despite the difficulties, TPM has its uses, for example, in BitLocker, where Windows is able to skip some checks and get the partition encryption key automatically most of the time, but has to ask the user for password if TPM reports that some boot settings do not match their checksums.

    Given the requirement of OS and hardware support and the amount of work to ensure state reproducibility, I wouldn't do this myself but perhaps for you it's a more viable option. Instead, I would separate the ciphertext and the encryption key and make it relatively easy for the user to give the key to the app on startup, while also preventing the part of memory from being written out to swap or core dump (some libraries provide abstractions for that). Yes, that means that my app would have to keep running and require user interaction on each restart.

    1A working example of that is KonBoot which boots before Windows, modifies its parts in RAM and hands the control back to the boot loader. The modifications make it possible to log in as administrator without knowing the original credentials.

Re: Hide DBI password in scripts
by Anonymous Monk on Jan 14, 2018 at 22:21 UTC

    A few specific examples:

    MySQL 5.5 Reference 6.5.1.4:

    As of MySQL 5.5.16, MySQL Enterprise Edition supports an authentication method that enables MySQL Server to use PAM (Pluggable Authentication Modules) to authenticate MySQL users. PAM enables a system to use a standard interface to access various kinds of authentication methods, such as Unix passwords or an LDAP directory. (...) PAM authentication enables MySQL Server to accept connections from users defined outside the MySQL grant tables and that authenticate using methods supported by PAM. (...) PAM authentication can return to MySQL a user name different from the login user, based on the groups the external user is in and the authentication string provided. This means that the plugin can return the MySQL user that defines the privileges the external PAM-authenticated user should have. For example, a user named joe can connect and have the privileges of the user named developer.

    Microsoft: Authentication Methods to SQL Server ....

    Oracle authentication services for operating systems

    When you are developing applications that are meant to face the internet, you typically have to roll-your-own authentication and authorization infrastructure.   But, in an intranet environment, as I said previously, these practices are usually not allowed.   Developers often develop applications that will run against production databases which they don’t have any access to.   These databases are not secured using user-names and passwords:   they are secured with rules that are centrally managed.   Best-practices and an increasing body of laws throughout the world mandate these requirements.   The enterprise centrally manages the dual concerns of authentication and authorization, and does not permit any installed application or subsystem to use alternate means.

      as I said previously

      who are you and why should we trust you?

Re: Hide DBI password in scripts
by sundialsvc4 (Abbot) on Jan 14, 2018 at 14:55 UTC

    There is a credible alternative that is available in corporate environments:   LDAP (OpenDirectory), or Kerberos.   As expected, Perl (CPAN) has full support for this, as do all other modern languages, web/application servers, and so forth.

    These technologies make it possible for central security-management personnel to identify the applications and users who are to be authenticated to have to the database (and other resources), and to control what access they are authorized to have.   This is commonly used to provide “single sign-on” to internal websites (who now do not have to ask the user to “log in,” because they already know who you are and what you are to be allowed to do), but it can apply to databases as well.

    Now, the application does not have any secrets to tell.   And, management of the entire setup can be done at the central security consoles of the enterprise by authorized personnel – not necessarily the application developers.   In many corporations you are required to do this, not to “roll your own” or to embed any passwords in any source-code (obfuscated or not).

      There is a credible alternative that is available in corporate environments: LDAP (OpenDirectory), or Kerberos.

      Well, let's see what we can make of this bold statement:


      PostgreSQL can in fact authenticate against an LDAP server. This is documented in https://www.postgresql.org/docs/9.6/static/auth-methods.html (Note: Intentionally linking to 9.6, not current). But let's have a look at the documented details (emphasis mine):

      20.3.7. LDAP Authentication

      This authentication method operates similarly to password except that it uses LDAP as the password verification method. LDAP is used only to validate the user name/password pairs. Therefore the user must already exist in the database before LDAP can be used for authentication.

      LDAP authentication can operate in two modes. In the first mode, which we will call the simple bind mode, the server will bind to the distinguished name constructed as prefix username suffix. [...]

      In the second mode, which we will call the search+bind mode, the server [...] performs a search for the user trying to log in to the database. [...] Once the user has been found in this search, the server disconnects and re-binds to the directory as this user, using the password specified by the client, to verify that the login is correct. This mode is the same as that used by LDAP authentication schemes in other software, such as Apache mod_authnz_ldap and pam_ldap. This method allows for significantly more flexibility in where the user objects are located in the directory, but will cause two separate connections to the LDAP server to be made.

      Or, summarized: PostgreSQL's LDAP authentication uses username and password passed to DBI->connect(). You have to provide username and password, no matter how you configure LDAP authentication.

      This is how LDAP works, nicely summarized, and independent from the relational database engine used by DBI. In other words: LDAP can not help here. You have to provide a secret password to use LDAP.


      Now, Kerberos. Quoting the same page:

      20.3.3. GSSAPI Authentication

      GSSAPI is an industry-standard protocol for secure authentication defined in RFC 2743. PostgreSQL supports GSSAPI with Kerberos authentication according to RFC 1964. GSSAPI provides automatic authentication (single sign-on) for systems that support it. [...]

      GSSAPI support has to be enabled when PostgreSQL is built; see Chapter 16 for more information.

      [...] Some Kerberos implementations might require a different service name, such as Microsoft Active Directory which requires the service name to be in upper case (POSTGRES).

      [...]

      So, yes, Kerberos can be used to avoid a password if the database supports it.


      But: Does your database support it?

      MySQL: LDAP, PAM, but no Kerberos. Windows authentication requires plugins on client and server, and won't help with non-Windows system. (And please don't make me think about clever ideas like "client-side cleartext authentication".) So, Kerberos won't help you with MySQL. FAIL.

      MS SQL Server: Either pure Windows authentication, or Windows authentication alternatively to username/password stored in SQL server. Not even LDAP, and no trace of Kerberos. So: FAIL.

      Oracle: LDAP, Kerberos, and many other. I did not expect less. PASS.

      DB2: LDAP is supported, and Kerberos is, too. PASS.

      PostgreSQL: see above. PASS.

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

        Quick question: Can you authenticate in Kerberos only for "registered" scripts within a single Linux user?

      Some code examples showing what you're talking about, like afoken provided, would be welcome. Otherwise skepticism may persist since you have demonstrated some confusion in this area.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://1207165]
Approved by Perlbotics
Front-paged by haukex
help
Chatterbox?
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others chanting in the Monastery: (4)
As of 2018-06-19 23:13 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    Should cpanminus be part of the standard Perl release?



    Results (116 votes). Check out past polls.

    Notices?