Can we clean password from PHP memory?

Posted on 2017-11-26 in Programmation

At work in a PHP application, we rely on libsodium to erase a password from $_POST. It may sound like a good idea: once the password is not in memory any more, it can't leak. But the question is: is it really erased from memory? That's the question will answer here.

To do this, we will create a docker container with PHP script to check this. We will dump the memory of the process into a file and we will inspect this file to see if the password is in it.

You can the scripts and the Dockerfile here if you want to run all tests by yourself.

First, let's look at the Dockerfile:

FROM php:7.1-apache

COPY *.php /var/www/html/

# Install libsodium
RUN apt-get update \
    && apt-get install build-essential -y \
    && curl -fsSL 'https://github.com/jedisct1/libsodium/releases/download/1.0.11/libsodium-1.0.11.tar.gz' -o libsodium.tar.gz \
    && mkdir -p libsodium \
    && tar -xf libsodium.tar.gz -C libsodium --strip-components=1 \
    && rm libsodium.tar.gz \
    && cd libsodium && ./configure && make check && make install \
    && cd .. && rm -r libsodium
RUN pecl install libsodium-1.0.6 \
    && docker-php-ext-enable libsodium

# Install packages to dump memory and inspect the dump
RUN apt-get install -y gdb

RUN rm -rf /var/lib/apt/lists/*

Nothing fancy: we start from an image that include Apache and PHP 7.1. We then install libsodium to erase the memory and gdb to have tools to dump the memory and analyse the dump.

Now the scripts. I made 2. The first check-post.php is meant to check that the value in $_POST is correctly erased:

<?php

// Before clean
var_dump($_POST);

Sodium\memzero($_POST['password']);

// After clean.
var_dump($_POST);

The second is meant to dump the memory of the process: we erase the memory then run an infinite loop so we have time to dump the memory of the process:

<?php

Sodium\memzero($_POST['password']);

while(true);

I didn't do var_dump in this one to be sure the password won't be in memory somewhere.

Let's build the image: docker build -t php-clean-password-tests .

Let's run it with docker run -d --name php-clean-password-tests -p 8080:80 php-clean-password-tests

Note: if you use user namespaces, you must start the container with docker run -d --name php-clean-password-tests -p 8080:80 --userns=host --privileged php-clean-password-tests instead, because you need to be the true root to dump the memory of a process.

Let's tests the first script with httpie or good old curl:

http -f POST localhost:8080/check-post.php password=superpasswd
curl -X POST -d 'password=superpasswd' localhost:8080/check-post.php

We have this output (with httpie):

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Length: 81
Content-Type: text/html; charset=UTF-8
Date: Sun, 26 Nov 2017 10:00:07 GMT
Keep-Alive: timeout=5, max=100
Server: Apache/2.4.10 (Debian)
Vary: Accept-Encoding
X-Powered-By: PHP/7.1.11

array(1) {
["password"]=>
string(11) "superpasswd"
}
array(1) {
["password"]=>
NULL
}

So far so good! Before running the second script, let's enter in the container with: docker exec -it php-clean-password-tests /bin/bash

Run http -f POST localhost:8080/wait-memory-dump.php password=superpasswd in your host shell to launch the second script.

In the container, run ps aux to list all running processes. You should see an apache2 command with non zero time, like:

www-data    17  9.5  0.0 173156 11916 ?        R    10:10   0:02 apache2 -DFOREGROUND

That's the process running our script. Get the PID (first number from the left, 17 in this example) and dump the memory with:

gcore 17  # Replace 17 by the PID you got before

You should now have a file named like core.17 in the current directory. This is a binary file we could inspect directly with gdb but to keep things simple, we will convert it to text with objdump:

objdump -s core.17 > core.17.txt

Now, let's grep for our password:

grep pass core.17.txt

Note: given how the file is structured, search direcly for a long string won't work. Try breaking your password into sequence of 3/4 characters to find it.

Here is the result (excerpt):

563aaa079c90 67696e5f 73686132 35365f70 61737377  gin_sha256_passw
7fd64d457030 70617373 776f7264 3d737570 65727061  password=superpa
7fd64d45d090 70617373 776f7264 00000000 00000000  password........
(…)
7ffd7df771b0 70617373 776f7264 3d737570 65727061  password=superpa
7ffd7df78e50 70617373 776f7264 0826f252 d67f0000  password.&.R....
7ffd7df79050 70617373 776f7264 3d737570 65727061  password=superpa

Conclusion: the password is still in memory. So it can still leak and the memzero wasn't that useful (you wouldn't output the password from $_POST anywhere anyway, right?). Where can it come from? Since the script are deliberately small, I suspect it comes from php://input a virtual, read only file where PHP stores the raw input of the request. You can read it with file_get_contents('php://input') and as far as I know, you can't erase it with memzero. Now imagine a true application: you validate the password? Are you sure it is not copied somewhere by your validation code? You use an ORM? Did it copy it somewhere for its internal usage? I guess that to be sure the password is just present in one place and erase it correctly from that one place is a herculean thing to do.

Last question: does it matter? From my point of view no. In modern web development, we don't manage the memory ourselves which makes the risk of leaking it by accident very low (unlike in C if you recall the heartbleed vulnerability). Which means, if you make sure your application core dumps (memory dump generated in cases of same application crash) are disable or not accessible from the outside, you are good. And between you and me, if an attacker can dump memory of a process, (s)he is root on your server, so your security sucks. Period.

So I don't think your application will gain much security if you do try to clean password from memory yourself (and you probably can't do it anyway or at least not without breaking promises from your programming language). The only thing it may do is give you a false sense of security. This doesn't meant you should stick to good password related practices:

  • Hash them with algorithms designed to hash passwords like bcryt or argon2 (and not sha related algorithm or worst md5).
  • Slat them.
  • Don't load the hash from the database unless you need it.
  • Don't put silly restrictions on the password content or length (but check that it is strong).
  • Protect yourself against XSS and CRSF
  • Secure you servers and databases

Further reading: