The SCRIPTHASH PHP4 extension

Designed by John Sutton

Implemented by Tom Oram and John Sutton

Copyright SCL Computer Services, December 2000


Updates:

IMPORTANT:

Updated: New Version Available - 24/09/2002
scripthash version 0.6 is now available packaged as either a RedHat Source RPM or a tarball.
The previous versions of scripthash have had a problem when using glibc > 2.1.1. This version now works with newer versions of glibc. The install packages are now tidier but still not perfect, if anyone is willing to tidy up the packages or start to convert the code to the PEAR standard please let us know. As always send any questions/suggestions/bug reports/etc... to tom@scl.co.uk or john@scl.co.uk.

Updated: New Version Available - 04/01/2002
scripthash version 0.5 is now available packaged as either a RedHat Source RPM or a tarball.
The only difference between this version and 0.4 is that this one works with PHP 4.1.1 (probably 4.1.x) whereas 0.4 is for PHP 4.0.5 (probably 4.0.x). As always send any questions/suggestions/bug reports/etc... to tom@scl.co.uk or john@scl.co.uk.

Updated: New Version Available - 07/12/2001
scripthash version 0.4 is now available packaged as either a RedHat Source RPM or a tarball.
This version has many new features and a lot of bugs have been fixed but sadly we have not had the time to update the documentation for this release, we will update it soon but until then you can send any questions/suggestions/bug reports/etc... to tom@scl.co.uk or john@scl.co.uk.

NEW EASY INSTALL: (Replaces topic 5)
To install scripthash version 0.4 and above, all you need to do is download the tarball or RPM, build and install it. Then add the following lines to php.ini
extension_dir=/usr/lib/php/extensions
extension=scripthash.so

then restart Apache. (see old INSTALL section for some other notes)


PLEASE NOTE: This code was written for, and has been tested using, mod_php 4.0.3pl1 running on Apache 1.3.14 running on Linux kernel 2.2.10. YMMV ;-) This code is ALPHA and comes with ABSOLUTELY NO WARRANTY.

  1. OVERVIEW

  2. USAGE

  3. CONFIGURATION DIRECTIVES

  4. SECURITY CONSIDERATIONS

  5. INSTALLATION

  6. SCRIPTHASH CLIENTS

  7. EXAMPLE APPLICATION: phppassd()

  8. EXAMPLE APPLICATION: suexec()

  9. TODO

  10. CHANGELOG

Hit count:16826

1. OVERVIEW

During module initialisation a shared memory segment is created and initialised with a randomly generated secret. This segment remains attached across the subsequent forks which create the Apache children and thus the children have access to the secret. The module implements a single function scripthash() which when called returns an MD5 hash of the secret and various attributes of the call. This hash can be used by external programs (scripthash "clients") to securely return privileged information to the caller.

The scripthash tarball (http://www.scl.co.uk/scripthash/tarball) contains the source code for:

2. USAGE

string scripthash ([int use_path [, string challenge ]])

Returns a scripthash string. A scripthash string is a concatenation of 8 fields seperated by colons:

key:user:group:mode:path:random :challenge:hash

where:

key

the key of the shared memory segment which holds the secret;

user

the user info associated with the user_source (see CONFIGURATION DIRECTIVES below);

group

the group info associated with the user_source (ditto below);

mode

four octal digits representing the mode of the calling script;

path

if use_path is specified and is true, the full path of the calling script i.e. DOCUMENT_ROOT.PHP_SELF. Otherwise, an empty field;

random

base64 encoding of a number of randomly generated bytes (ditto below);

challenge

EXACTLY the second optional argument. Otherwise, if not specified, an empty field.

hash

the md5 hash of key:user:group:mode:path:random:challenge and secret

The challenge argument is for use in challenge-response implementations (e.g., see the example php function suexec()) and so should be randomly generated information. In principle, it can contain characters of any value but the current implemention will certainly choke on ASCII NUL's, so it is advisable to ASCII armour this argument before passing it in.

Scripthash generation can fail in a number of circumstances, in which case a FATAL error will be raised and the script terminated:

"Generated scripthash would violate umask setting". See description of the umask configuration directive below.

"VirtualHost User and/or Group does not match owner/group of script calling scripthash". See description of the user_source configuration directive below.

"Generated scripthash would contain root privilege". See description of the no_root_squash configuration directive below.

Plus two fatal errors which should not arise under normal circumstances:

"Can't get PHP_SELF"

"Can't get DOCUMENT_ROOT"

3. CONFIGURATION DIRECTIVES

All directives are called scripthash.NAME

NAME

TYPE

ACCEPTS

DEFAULT

key

admin_value

integer

0

min_key

admin_value

integer

1

max_key

admin_value

integer

31

rand_bytes

admin_value

integer

4

secret_duration

admin_value

integer

300

user_source

admin_value

stat|vhost|both

both

user_numeric

admin_value

0|1

0

no_root_squash

admin_value

0|1

0

umask

admin_value

integer

0022

key

The shared memory key. If 0, use the first unused key in the range min_key to max_key. Otherwise, use exactly this key and re-use it if it already exists, providing it seems safe to do so.

min_key

See key above. Ignored unless key is 0.

max_key

See key above. Ignored unless key is 0.

rand_bytes

The number of randomly generated bytes to include in each scripthash.

secret_duration

The maximum age in seconds of the secret. After this interval has elapsed since secret generation, the next subsequent call of the scripthash() function triggers re-generation of the secret.

user_source

The source of the scripthash user and group information. stat means stat the script, i.e., PHP_SELF, vhost means use the User and Group settings of the current VirtualHost, and both means evaluate both and if they don't match, declare an error.

user_numeric

If set true, the scripthash user and group information will be the numeric values user uid and group gid, otherwise the user name and group name.

no_root_squash

If set true (DON'T! on a live server), attempting to generate a scripthash containing user and/or group info for the superuser will only generate a warning, otherwise a fatal error.

umask

If any of these bits are set in the mode of the calling script, a fatal error results. If your system uses the Red Hat user/group convention, you might prefer the less restrictive 0002.

Notes:

  1. no_root_squash and user_numeric should be of type admin_flag rather than admin_value but we can't work out how to code that?

  2. umask is interpreted in the usual C fashion so must have a leading zero if you want it interpreted as octal.

  3. On occasions when Apache does not exit cleanly, the shared memory segment will not be removed. This is not a problem if you are using a fixed (i.e. non-zero) value for key, as the same segment will be re-used the next time. If however you are using a key value of 0, the next available key will be used and the previous one will be left unused and unavailable. We supply a utility utils/cleanshm.c to tidy up unused shared memory segments.

4. SECURITY CONSIDERATIONS

It is important to appreciate that a scripthash accepted by a scripthash client to solicit privileged information is essentially equivalent to the privileged information itself. For this reason, the scripthash function will (by default) refuse to generate a scripthash for an "insecure" caller, i.e., if called from a script which is writeable by either group or other, since such a script could have been tampered with to expose the scripthash. A user who writes a script which exposes scripthashes generated by that script might as well just hardcode the privileged information into the script. However, scripthashes are only valid, i.e., will be accepted by a scripthash client, whileever the secret on which they are based is still available and this is at maximum a time of (twice) secret_duration seconds since generation. Challenge-response type implementations can avoid the danger of exposure by only accepting a scripthash which is generated on demand. See phpsuexecd for an example.

To ensure that a scripthash is not transmitted to a bogus client, it is important that all clients are secure. For example, the example client apps use Unix domain sockets to listen for scripthashes. Under Linux (unlike under BSD derived systems?), the security of a domain socket is guaranteed by the permissions of the directory it appears in, so the sockets are created in /var/run/scripthash, a directory which should be set writeable only by root. If they were instead created in /tmp, then if a client failed to start up, a malicious user program could open the domain socket and harvest scripthashes which it could then submit to the "real" client at some later time when the real client *had* been started up. (Of course, if this were later than twice secret_duration seconds after generation, the scripthashes would in any case no longer be valid.)

Guessing here, but intuition would suggest that the size of the automatically generated "random" field in the scripthash should not be set too low. Otherwise a malicious user could mount a brute force attack on the secret by generating enough scripthashes differing by only small changes in the path?

Regarding the security of the secret itself, there are two cases to consider: attack from without and attack from within.

Attack from without: Can another user process running with the same uid as apache (typically nobody) gain sufficient access to apache's memory to be able to scan for the secret? The 2.2.10 kernel source (fs/proc/mem.c):

        if (!(tsk->flags & PF_PTRACED)
                || tsk->state != TASK_STOPPED
                || tsk->p_pptr != current)
                        tsk = NULL;

would seem to suggest (?) that the process must have requested a ptrace, it must be stopped, and the request for access must come from the parent process. Otherwise, the attempt returns ESRCH ("No such process"). Certainly all our attempts to open /proc/{pid}/mem have failed with ESRCH.

Attack from within: can some module running within Apache gain access to the secret. The obvious attack - to attempt to reattach the shared memory segment given that the key is publicly available - will fail with "Permission denied" (EACCES) providing Apache is started as root and it's children run as non-root. If either you do NOT startup Apache as root, or DO run the children as root, then you will need to modify the source of the shm_attach() function to protect yourself against this attack launched from within php. And presumably a similiar attack will also be available from within mod_perl, for example. We do not know of any other function in the standard php4 which would allow access to arbitrary memory addresses. We know next to nothing about mod_perl, so until we know better we intend to play safe and NOT run mod_perl ;-)

5. INSTALLATION

You may want to tweak these values in scripthash.h before building:

#define SCRIPTHASH_ID_STRING            "SCRIPTHASH"
#define SCRIPTHASH_SECRET_LENGTH        8
#define SCRIPTHASH_MIN_RAND_BYTES       "4"

ID_STRING

Used to identify shared memory segments created by the scripthash extension.

SECRET_LENGTH

The length in bytes of the secret.

MIN_RAND_BYTES

The minimum acceptable value of the scripthash.rand_bytes configuration directive.

The supplied spec file mod_php4.spec details the installation procedure. For those masochists who don't use rpm's, here it is:

tar xzf php-4.0.3pl1.tar.gz
cd php-4.0.3pl1/ext
tar xzf scripthash-0.1.tar.gz
cd ..
./buildconf
./configure --enable-scripthash
make
install -m 755 .libs/libphp4.so /usr/lib/apache
cd ext/scripthash/client
make
make install
cd perl
perl Makefile.PL
make
make install
cd ../../apps
make
make install

You'll also need to do:

mkdir /var/run/scripthash
ldconfig

To test your installation, first look at phpinfo() and make sure scripthash support is enabled. Then run a script containing <?php echo scripthash() ?>. If you get the error "Fatal error: Generated scripthash would contain root privilege" then re-read the section "CONFIGURATION DIRECTIVES" above. If you can't fathom this out, you'd probably best not use the scripthash extension ;-(

6. SCRIPTHASH CLIENTS

The shared library libscripthashclient.so contains the code for scripthash_validate():

#include <scripthash.h>

int scripthash_validate(
                        int key_in,
                        const char *scripthash_in,
                        char **uid_out,
                        char **gid_out,
                        int *mode_out,
                        char **path_out,
                        char **challenge_out,
                        char **errmsg_out);

The function returns true if scripthash_in is a scripthash generated with the secret referenced by key_in, otherwise false and an error message in *errmsg_out. If key_in is 0, the key value passed in the scripthash will be used. Each of the 5 **arguments accepts NULL (if you don't need it), otherwise the routine will malloc memory as necessary, which the caller must free. That is, for each non-NULL **argument which you pass, your code should check on return if *arg is not NULL and if so, free it. Actually, if the function returns true then errmsg will NOT have been malloc'd even if passed, and vice versa, if the function returns false, none of the args except errmsg will have been malloc'd.

You do not have to use this shared object - it just makes life easier. It is built using the same #define's etc (in scripthash.h) as the scripthash extension, so guaranteeing a consistent approach to the shared memory segment.

There is a perl binding to the shared object which you use like this:

use scripthash;
if (!scripthash::validate($key, $scripthash, $user, $group, $mode, $path, $challenge, $err))

{
        print "Invalid scripthash: $err\n";
}
else {
        print "user=$user, group=$group, mode=$mode, path=$path, challenge=$challenge\n";

}

7. EXAMPLE APPLICATION: mysql_password()

To get this application going, follow this recipe:

Choose where to put the password file, e.g., /etc/mysqlpassdb, and copy the supplied mysqlpassdb there.

Start up the password server as root: phppassd /var/run/scripthash/mysqlpassd.sock /etc/mysqlpassdb

(See phppassd -h for full usage.)

Copy the file testmysql_password.php (installed in /usr/lib/php/Scripthash if you installed the rpm) to somewhere in the document root and run it. You should get "You don't have access to mysql user john on host localhost".

echo “scripthashuser localhost:john:mYpAsSwOrD” >> /etc/mysqlpassdb and then send the password server a SIGHUP.

where scripthashuser is the unix owner of the testmysql_password.php script. Now run the script again and you should get: "Password is mYpAsSwOrD Don't do this! Instead call mysql_connect(localhost, john, mYpAsSwOrD)"

Each password record has as key the scripthash user, and as value a string:

host:user:password[ host:user:password]...

i.e. one or more triplets, seperated by single spaces, where each triplet specifies the password for a mysql user on a given host. Note that host's and user's cannot contain either spaces or colons and passwords cannot contain spaces.

8. EXAMPLE APPLICATION: suexec()

This is an argument compatible replacement for the standard exec(). The difference is that the command is executed as the scripthash user and group rather than as the webserver user and group.

To get this going, follow this recipe:

Start up the suexec server as root: phpsuexecd -a phpsuexecd.sock

(See phpsuexecd -h for full usage.)

Copy the file testsuexec.php (installed in /usr/lib/php/Scripthash if you installed the rpm) to somewhere in the document root and run it.

If you choose to run phpsuexecd AND set no_root_quash, you are heading for a catastrophe. YOU HAVE BEEN WARNED. Otherwise, and generally speaking, running phpsuexecd will give your users the same capabilites as if they had telnet access to your server.

9. TODO

Call to php_alter_ini_entry() for scripthash.key in scripthash.c not working properly. OK at first but then reverts to 0 (as revealed by phpinfo()).

The call to system() in phpsuexecd sometimes returns -1 even though it has succeeded?

Use SOCK_DGRAM instead of SOCK_STREAM for the servers? Since these are running on Unix domain sockets, this should be reliable and faster?

10. CHANGELOG

0.1 -> 0.2

- phpsuexecd only worked if user_numeric set. Fixed with conditional calls to getpwnam & getgrnam.

- in mysql_password() (in scripthash.php), get scripthash before opening socket to mysqlpassd so that if scripthash fails, we don't leave mysqlpassd with an open stream and in a tail spin! Clearly this needs more attention! No amount of bad behaviour by a client should be able to put the mysqlpassd server into a tailspin.

John Sutton

6th December 2000