/* ==================================================================== * * Apache FrontPage module. * * Copyright (c) 1996-1997 Microsoft Corporation -- All Rights Reserved. * * NO WARRANTIES. Microsoft expressly disclaims any warranty for this code and * information. This code and information and any related documentation is * provided "as is" without warranty of any kind, either express or implied, * including, without limitation, the implied warranties or merchantability, * fitness for a particular purpose, or noninfringement. The entire risk * arising out of use or performance of this code and information remains with * you. * * NO LIABILITY FOR DAMAGES. In no event shall Microsoft or its suppliers be * liable for any damages whatsoever (including, without limitation, damages * for loss of business profits, business interruption, loss of business * information, or any other pecuniary loss) arising out of the use of or * inability to use this Microsoft product, even if Microsoft has been advised * of the possibility of such damages. Because some states/jurisdictions do not * allow the exclusion or limitation of liability for consequential or * incidental damages, the above limitation may not apply to you. * * $Revision: 1.3 $ * $Date: 1997/10/15 17:23:46 $ * */ /* * User configurable items. We will not run the server extensions with any * UID/GID less than LOWEST_VALID_UID/LOWEST_VALID_GID. */ #if defined(LINUX) #define LOWEST_VALID_UID 15 #else #define LOWEST_VALID_UID 11 #endif #if defined(HPUX) || defined(IRIX) || defined(SUNOS4) #define LOWEST_VALID_GID 20 #else #if defined(SCO) #define LOWEST_VALID_GID 24 #else #define LOWEST_VALID_GID 21 /* Solaris, AIX, Alpha, Bsdi, *BSD, etc. */ #endif #endif /* * End of user configurable items */ #include "httpd.h" #include "http_config.h" #include "http_conf_globals.h" #include "http_log.h" #include #include #if defined(UWARE7) || UW==700 #define Vstat stat32 #define Vlstat lstat32 #else #define Vstat stat #define Vlstat lstat #endif #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif #ifndef MAXPATHLEN #define MAXPATHLEN 1024 #endif #if (MAXPATHLEN < 1024) #undef MAXPATHLEN #define MAXPATHLEN 1024 #endif #define KEYLEN 128 /* Should be a multiple of sizeof(int) */ static char gszKeyVal[KEYLEN+1]; /* SUID key value used by this module */ static int gfdKeyPipe[2]; /* Pipe to fpexe stub CGI */ static int gbKeyPipeActive = FALSE;/* Pipe to fpexe stub CGI is active */ static int gbEnabled = FALSE; /* TRUE when SUID scheme is enabled */ #if !defined(SHARED_MODULE) static int giInitializeCount = 0; /* FrontPageInit called previously */ #endif static const char* FP = "/usr/local/frontpage/currentversion"; static const char* FPKEYDIR = "/usr/local/frontpage/currentversion/apache-fp"; static const char* KEYFILEXOR = "/usr/local/frontpage/currentversion/apache-fp/suidkey"; static const char* KEYFILE = "/usr/local/frontpage/currentversion/apache-fp/suidkey.%d"; static const char* FPSTUBDIR = "/usr/local/frontpage/currentversion/apache-fp/_vti_bin"; static const char* FPSTUB = "/usr/local/frontpage/currentversion/apache-fp/_vti_bin/fpexe"; static const char* SHTML = "/_vti_bin/shtml.exe"; static const char* SHTML2 = "/_vti_bin/shtml.dll"; static const char* VTI_BIN = "/_vti_bin"; static const char* FPCOUNT = "/_vti_bin/fpcount.exe"; static const char* AUTHOR = "/_vti_bin/_vti_aut/author.exe" ; static const char* ADMIN = "/_vti_bin/_vti_adm/admin.exe" ; MODULE_VAR_EXPORT module frontpage_module; /* * Print a descriptive error in the httpd's error_log. The format string * should be length limited so that it is no longer than 1800 bytes. */ static void LogFrontPageError( server_rec* s, const char* szFormat, const char* szFile, const char* szRoutine, int bIsDisabled) { char szBuf[MAXPATHLEN * 2]; sprintf(szBuf, szFormat, szFile); strcat(szBuf, " in "); strcat(szBuf, szRoutine); strcat(szBuf, "."); if (bIsDisabled) { strcat(szBuf, " Until this problem is fixed, the FrontPage security patch is disabled and the FrontPage extensions may not work correctly."); gbEnabled = FALSE; /* Make double sure we're not enabled */ } ap_log_error(APLOG_MARK, APLOG_ERR, s, szBuf); } /* * Clean up stale keyfiles. Failure to clean up stale keyfiles does not * stop the FrontPage SUID scheme. */ static void FrontPageCleanup(server_rec *s) { DIR *d; struct DIR_TYPE *dstruct; int myPid = getpid(); if (!(d = opendir(FPKEYDIR))) { /* * This should be a rare occurrence, because we're running as root and * should have access to the directory. Stale key files can be * exploited. User recovery: Check that the directory exists and is * properly protected (owned by root, permissions rwx--x--x), and that * there are no stale key files in it (suidkey.*, where * is a * non-existant PID). */ LogFrontPageError(s, "Can't clean stale key files from directory \"%-.1024s\"", FPKEYDIR, "FrontPageCleanup()", FALSE); return; } while ((dstruct = readdir(d))) { if (strncmp("suidkey.", dstruct->d_name, 8) == 0) { /* * Make sure the key file contains a pid number - otherwise * it is harmless and you can ignore it. */ char* pEnd = 0; int pid = strtol(dstruct->d_name + 8, &pEnd, 10); if (!pEnd || *pEnd) continue; /* * Make sure there isn't some other server using this key file. * If the process group isn't alive, then the file is stale * and we want to remove it. */ if (pid == myPid || kill(pid, 0) == -1) { char szBuf[MAXPATHLEN]; sprintf(szBuf, "%-.500s/%-.500s", FPKEYDIR, dstruct->d_name); if (unlink(szBuf) == -1) { /* * This should be a rare occurrence, because we're running * as root and should always have permission to delete the * file. Stale key files can be exploited. User recovery: * delete the offending file. */ LogFrontPageError(s, "Can't unlink stale key file \"%-.1024s\"", szBuf, "FrontPageCleanup()", FALSE); } } } } closedir(d); } /* * Checks that all the permissions are currently correct for the FrontPage * fpexe SUID stub to run correctly. If not, it logs an error and aborts * initialization, effectively disabling the FrontPage SUID scheme. * It checks both the file permissions (owned by root and not writable to * group, other) and that the directory is not writable. */ static int FrontPageCheckup(server_rec *s) { struct stat fs; if (geteuid() != 0) { /* * We need to be root to have the security scheme work correctly. * User recovery: run the server as root. */ LogFrontPageError(s, "Not running as root", 0, "FrontPageCheckup()", TRUE); return (FALSE); } if (Vlstat(FPKEYDIR, &fs) == -1 || /* We can't stat the key dir */ fs.st_uid || /* key dir not owned by root */ (fs.st_mode & (S_IRGRP | S_IROTH)) || /* key dir is readable */ (fs.st_mode & (S_IWGRP | S_IWOTH)) || /* key dir is writable */ !(fs.st_mode & (S_IXGRP | S_IXOTH)) || /* key dir is not executable */ !(S_ISDIR(fs.st_mode))) { /* * User recovery: set directory to be owned by by root with permissions * rwx--x--x. Note you need the execute bit for group and other so * that non-root programs can run apache-fp/_vti_bin/fpexe (even though * non-root cannot list the directory). */ LogFrontPageError(s, "Incorrect permissions on key directory \"%-.1024s\", needs root ownership and perms rwx--x--x", FPKEYDIR, "FrontPageCheckup()", TRUE); return (FALSE); } if (Vlstat(FPSTUBDIR, &fs) == -1 || /* We can't stat the stub dir */ fs.st_uid || /* stub dir not owned by root */ (fs.st_mode & (S_IWGRP | S_IWOTH)) || /* stub dir is writable */ (!S_ISDIR(fs.st_mode))) { /* * User recovery: set directory to be owned by by root with permissions * r*x*-x*-x. */ LogFrontPageError(s, "Incorrect permissions on stub directory \"%-.1024s\", needs root ownership and perms r*x*-x*-x", FPSTUBDIR, "FrontPageCheckup()", TRUE); return (FALSE); } if (Vstat(FPSTUB, &fs) == -1 || /* We can't stat the stub */ fs.st_uid || /* stub not owned by root */ !(fs.st_mode & S_ISUID) || /* stub is not set-uid */ (fs.st_mode & S_ISGID) || /* stub is set-gid */ (fs.st_mode & (S_IWGRP | S_IWOTH)) || /* stub is writable */ !(fs.st_mode & (S_IXGRP | S_IXOTH))) /* stub is not executable */ { /* * User recovery: set stub to be owned by by root with permissions * r*s*-x*-x. */ LogFrontPageError(s, "Incorrect permissions on stub \"%-.1024s\", needs root ownership and perms r*s*-x*-x", FPSTUB, "FrontPageCheckup()", TRUE); return (FALSE); } return (TRUE); } /* * Module-initializer: Create the suidkey file and local value. * Everything needs to be just right, or we don't create the key file, and * therefore, the fpexe SUID stub refuses to run. */ static void FrontPageInit(server_rec *s, pool *p) { int fdPipe[2]; pid_t pid; FILE *f; struct stat fs; int fd; char szKeyFile[MAXPATHLEN]; int iRandom[5]; char* szRandom = (char*)iRandom; struct timeval tp; struct timezone tz; (void)p; /* p is unused */ #if !defined(SHARED_MODULE) /* * Standalone servers call initialization twice: once in main() and again * in standalone_main(). The fully initializing on the the first call is a * waste of time, and a race condition can leave a stale suidkey.pgrpid * file around. */ if (ap_standalone && !giInitializeCount++) return; #endif /* * Disable the suid scheme until everything falls perfectly into place. */ gbEnabled = FALSE; gbKeyPipeActive = FALSE; /* * Clean up old key files before we start */ FrontPageCleanup(s); if (!FrontPageCheckup(s)) return; if (pipe(fdPipe) == -1) { /* * This should be a rare occurrence. User recovery: check to see why * the system cannot allocate a pipe (is the file table full from * run-away processes?), and fix the problem or reboot, then try again. */ LogFrontPageError(s, "pipe() failed", 0, "FrontPageInit()", TRUE); return; } gettimeofday(&tp, &tz); iRandom[0] = tp.tv_sec; iRandom[1] = tp.tv_usec | tp.tv_usec << 20; pid = fork(); if (pid == -1) { /* * This should be a rare occurrence. User recovery: check to see why * the system cannot allocate a process (is the process table full from * run-away processes?), and fix the problem or reboot, then try again. */ LogFrontPageError(s, "fork() failed", 0, "FrontPageInit()", TRUE); return; } if (pid) { /* * I am the parent process. Try to read a random number from the * child process. */ unsigned int npos = (unsigned int)-1; unsigned int v1 = npos, v2 = npos, v3 = npos, v4 = npos; int stat; int iCount; close(fdPipe[1]); if (waitpid(pid, &stat, 0) == -1 || (!WIFEXITED(stat) || WIFEXITED(stat) && WEXITSTATUS(stat))) { /* * This should be a rare occurrence. User recovery: Make sure you * have a /bin/sh, or change the shell location in the execl * command below. Try the commands defined in RAND_CMD in a * /bin/sh session to make sure they work properly. Rebuild this * module and your httpd with the proper commands. */ LogFrontPageError(s, "Random number generator exited abnormally", 0, "FrontPageInit()", TRUE); return; } iCount = read(fdPipe[0], gszKeyVal, KEYLEN); close(fdPipe[0]); if (iCount < 0) { /* * This should be a rare occurrence. See the above comment under * the waitpid failure condition for user recovery steps. */ LogFrontPageError(s, "Could not read random numbers", 0, "FrontPageInit()", TRUE); return; } gszKeyVal[iCount] = 0; sscanf(gszKeyVal, "%u %u %u %u", &v2, &v1, &v4, &v3); if (v1 == npos || v2 == npos || v3 == npos || v4 == npos) { /* * This should be a rare occurrence. See the above comment under * the waitpid failure condition for user recovery steps. */ LogFrontPageError(s, "Could not scan random numbers", 0, "FrontPageInit()", TRUE); return; } iRandom[2] = (v1 << 16) + v2 + (v4 << 12) + v3; } else { /* * I am the child process. Create a random number which shouldn't * be easily duplicated. */ if (dup2(fdPipe[1], 1) == -1) exit(1); /* Parent picks up the error */ close(fdPipe[0]); #ifdef LINUX #define RAND_CMD "/bin/ps laxww | /usr/bin/sum ; /bin/ps laxww | /usr/bin/sum" #else #if defined ( bsdi ) || ( defined ( BSD ) && ( BSD >=199103 )) #define RAND_CMD "/bin/ps laxww | /usr/bin/cksum -o 1 ; /bin/ps laxww | /usr/bin/cksum -o 1" #else #define RAND_CMD "/bin/ps -ea | /bin/sum ; /bin/ps -ea | /bin/sum" #endif #endif execl("/bin/sh", "/bin/sh", "-c", RAND_CMD, NULL); exit(1); } gettimeofday(&tp, &tz); iRandom[3] = tp.tv_sec; iRandom[4] = tp.tv_usec | tp.tv_usec << 20; /* * See if there is an 'suidkey' file to merge into our key. */ if (Vstat(KEYFILEXOR, &fs) == -1) { /* * It's a security violation if the key file is not present. User * recovery: Make sure the key file is present and properly protected * (owned by root, permissions r**------). */ LogFrontPageError(s, "The key file \"%-.1024s\" does not exist", KEYFILEXOR, "FrontPageInit()", TRUE); return; } else { int i, iCount; char szBuf[KEYLEN]; if ((fs.st_mode & (S_IRWXG | S_IRWXO)) || fs.st_uid) { /* * It's a security violation if the key file is not owned by root, * and is not protected from all other group. User recovery: Make * sure the key file is properly protected (owned by root, * permissions r**------). */ LogFrontPageError(s, "The key file \"%-.1024s\" must be owned by root and have permissions r**------", KEYFILEXOR, "FrontPageInit()", TRUE); return; } if ((fd = open(KEYFILEXOR, O_RDONLY)) == -1) { /* * This should be a rare occurrence. User recovery: Make sure * the key file exists, is properly owned and protected, and is * readable. */ LogFrontPageError(s, "Cannot open key file \"%-.1024s\"", KEYFILEXOR, "FrontPageInit()", TRUE); return; } iCount = read(fd, szBuf, KEYLEN); if (iCount < 8) { /* * The keyfile must be at least 8 bytes. If it longer than 128 * bytes, only the first 128 bytes will be used. Any character * value from 0-255 is fine. User recovery: Make sure the key file * is at least 8 bytes long. */ LogFrontPageError(s, "Key file \"%-.1024s\" is unreadable or is too short (must be at least 8 bytes)", KEYFILEXOR, "FrontPageInit()", TRUE); return; } /* * Now generate the effective key we'll be using by XORing your key * with 5 "random" 32-bit integers. The primary security of this * scheme is your key; properly setting it and changing it often keeps * the FrontPage SUID scheme secure. All this work above to generate 5 * random 32-bit integers is soley to make your key somewhat harder to * crack (assuming the key files are properly protected). If you don't * like the algorithm used to generate the 5 random integers, feel free * to substitute as appropriate (check out SGI's Lavarand (TM) at * lavarand.sgi.com). */ for (i = 0; i < KEYLEN; i++) gszKeyVal[i] = szBuf[i % iCount] ^ szRandom[i % sizeof(iRandom)]; close(fd); } #if defined(SUNOS4) pid = getpgrp(0); #else pid = getpgrp(); #endif sprintf(szKeyFile, KEYFILE, (int)pid); fd = creat(szKeyFile, 0600); if (fd < 0) { /* * This should be a rare occurrence, because we're running as root and * should always have permission to create the file. User recovery: * check that you are not out of disk space, or that the file is not * NFS-mounted on a share where you do not have permissions. */ LogFrontPageError(s, "Could not create key file \"%-.1024s\"", szKeyFile, "FrontPageInit()", TRUE); return; } if (write(fd, gszKeyVal, 128) != 128) { /* * This should be a rare occurrence. User recovery: check that you are * not out of disk space. */ close(fd); unlink(szKeyFile); LogFrontPageError(s, "Could not write to key file \"%-.1024s\"", szKeyFile, "FrontPageInit()", TRUE); return; } close(fd); /* * Everything looks OK enough to start the suid scheme. */ gbEnabled = TRUE; /* * Thanks to Scot Hetzel (hetzels@westbend.net) */ ap_add_version_component("FrontPage/3.0.4.3"); } /* * Look for a valid FrontPage extensions scenario and fake a scriptalias if * appropriate. If there are any problems, we silently decline. */ static int FrontPageAlias( request_rec* r, char* szCgi, const char* szFpexe) { int iLen; struct stat webroot; struct stat vti_pvt; struct stat stub; char szBuf[MAXPATHLEN]; char chSave; char szFormat[MAXPATHLEN * 2]; /* * Decline if we cannot run the stub, or it is writable. */ if (Vstat(FPSTUB, &stub) == -1 || !(stub.st_mode & S_IXOTH) || stub.st_mode & (S_IWGRP | S_IWOTH)) { /* * The stub used to be correctly permissioned; what happened? User * recovery: set stub to be owned by by root with permissions * r*s*-x*-x. */ LogFrontPageError(r->server, "Incorrect permissions on stub \"%-.1024s\", must be owned by root with permissions r*s*-x*-x", FPSTUB, "FrontPageAlias()", FALSE); return DECLINED; } chSave = szCgi[1]; szCgi[1] = '\0'; ap_translate_name(r); szCgi[1] = chSave; /* * Zap trailing slash that confuses some OSes. */ iLen = strlen(r->filename); r->filename[--iLen] = 0; if (iLen > MAXPATHLEN - 10) return DECLINED; sprintf(szBuf, "%s/_vti_pvt", r->filename); /* * Decline if webroot and webroot/_vti_pvt don't have the same * user and group or uid < LOWEST_VALID_UID or gid < LOWEST_VALID_GID. */ if (Vstat(szBuf, &vti_pvt) == -1 || vti_pvt.st_uid < LOWEST_VALID_UID || vti_pvt.st_gid < LOWEST_VALID_GID || Vstat(r->filename, &webroot) != 0 || webroot.st_uid != vti_pvt.st_uid || webroot.st_gid != vti_pvt.st_gid) { /* * The webroot and webroot/_vti_pvt don't match. User recovery: fix * the owners and groups of both directories to match, and have both a * uid and gid in the allowable range. */ sprintf(szFormat, "Incorrect permissions on webroot \"\%-.0124s\" and webroot's _vti_pvt directory, the owners and groups must match and have a uid >= %d and gid >= %d", LOWEST_VALID_UID, LOWEST_VALID_GID); LogFrontPageError(r->server, szFormat, szBuf, "FrontPageAlias()", FALSE); return DECLINED; } /* * If the pipe is active, it was because we previously executed a CGI. * That CGI must have finished by now (otherwise we wouldn't be processing * this next request), so we can and should close the pipe to avoid a * resource leak. */ if (gbKeyPipeActive) { close(gfdKeyPipe[0]); gbKeyPipeActive = FALSE; } /* * If we can't get a pipe, that's really bad. We'll log an error, and * decline. This should be a rare occurrence. User recovery: check to see * why the system cannot allocate a pipe (is the file table full from * run-away processes?), and fix the problem or reboot, then try again. */ if (pipe(gfdKeyPipe) == -1) { LogFrontPageError(r->server, "pipe() failed", 0, "FrontPageAlias()", FALSE); return DECLINED; } /* * Note: ap_pstrdup allocates memory, but it checks for out of memory * conditions - it will not return if out of memory. */ r->handler = ap_pstrdup(r->pool, "cgi-script"); ap_table_set(r->notes, "alias-forced-type", r->handler); ap_table_set(r->subprocess_env, "FPEXE", ap_pstrdup(r->pool, szFpexe)); sprintf(szBuf, "%d", webroot.st_uid ); ap_table_set(r->subprocess_env, "FPUID", ap_pstrdup(r->pool, szBuf)); sprintf(szBuf, "%d", webroot.st_gid ); ap_table_set(r->subprocess_env, "FPGID", ap_pstrdup(r->pool, szBuf)); sprintf(szBuf, "%d", gfdKeyPipe[0]); ap_table_set(r->subprocess_env, "FPFD", ap_pstrdup(r->pool, szBuf)); r->execfilename = ap_pstrcat(r->pool, FPSTUB, szCgi + strlen(szFpexe), NULL); r->filename = ap_pstrcat(r->pool, r->filename, szCgi, NULL); if (write(gfdKeyPipe[1], gszKeyVal, 128) != 128) { /* * If we can't write to the pipe, that's really bad. We'll log an * error, and decline. This should be a rare occurrence. User * recovery: check to see why the system cannot write to the pipe (is * the system being choked with too much load?), and fix the problem or * reboot, then try again. */ LogFrontPageError(r->server, "Write to pipe failed", 0, "FrontPageAlias()", FALSE); close (gfdKeyPipe[0]); close (gfdKeyPipe[1]); return DECLINED; } close(gfdKeyPipe[1]); gbKeyPipeActive = TRUE; return OK; } /* * This routine looks for shtml.exe, fpcount.exe, author.exe and admin.exe * in a URI, and if found we call FrontPageAlias() to check for a valid * FrontPage scenario. * * The return value is OK or DECLINED. */ static int FrontPageXlate( request_rec *r) { char *szVti; char *szCgi; /* * Decline if we're improperly initialized. */ if (!gbEnabled) return DECLINED; /* * Check once for anything with _vti_bin. This is much faster than * checking all four paths, because anything without this is definitely * not a FrontPage scenario. */ if (!(szVti = strstr(r->uri, VTI_BIN))) return DECLINED; /* * Test for FrontPage server extenders: * .../_vti_bin/shtml.exe... * .../_vti_bin/shtml.dll... * .../_vti_bin/fpcount.exe... * .../_vti_bin/_vti_aut/author.exe... * .../_vti_bin/_vti_adm/admin.exe... */ if (szCgi = strstr(szVti, AUTHOR )) return FrontPageAlias(r, szCgi, AUTHOR); /* * Convert inadvertent shtml.dll to shtml.exe * Thanks for the idea to Scot Hetzel (hetzels@westbend.net) */ if (szCgi = strstr(szVti, SHTML2 )) { int iShtmlExtPos = strlen(SHTML2) - 3; strncpy(szCgi + iShtmlExtPos, SHTML + iShtmlExtPos, 3); } if (szCgi = strstr(szVti, SHTML )) return FrontPageAlias(r, szCgi, SHTML); if (szCgi = strstr(szVti, ADMIN )) return FrontPageAlias(r, szCgi, ADMIN); if (szCgi = strstr(szVti, FPCOUNT)) return FrontPageAlias(r, szCgi, FPCOUNT); return DECLINED; } /* * Declare ourselves so the configuration routines can find us. */ module MODULE_VAR_EXPORT frontpage_module = { STANDARD_MODULE_STUFF, FrontPageInit, /* initializer */ NULL, /* per-directory config creater */ NULL, /* dir config merger - default is to override */ NULL, /* server config creator */ NULL, /* server config merger */ NULL, /* command table */ NULL, /* [6] list of handlers */ FrontPageXlate, /* [1] filename-to-URI translation */ NULL, /* [4] check/validate HTTP user_id */ NULL, /* [5] check HTTP user_id is valid *here* */ NULL, /* [3] check access by host address, etc. */ NULL, /* [6] MIME type checker/setter */ NULL, /* [7] fixups */ NULL, /* [9] logger */ NULL, /* [2] header parser */ NULL, /* child_init */ NULL, /* child_exit */ NULL /* post read-request */ };