File Access


File system interaction is integral to most applications and provides a popular target for attackers to exploit dangerously written code. Safe file-handling code requires developers to program defensively because attackers take advantage of the nuances and flexibility of the file access APIs and file systems. Windows OSs in particular offer a lot of flexibility and convenience for developers. Unfortunately, these capabilities can lead to serious security issues when developers aren't aware of subtle aspects of the file system and file I/O APIs.

Windows OSs control access to files through the object security mechanisms you have already explored. That is, files on the file system are treated as objects, so they are manipulated by handles to file objects. Unanticipated file accesses might produce unexpected results in several ways, however, and consequently, an application might perform in a manner other than what was intended. The following sections explore the ins and outs of file accesses and what problems might arise when attempting to open files.

File Permissions

As mentioned, files are treated by the system as objects (of the File type), so object permissions describe the permissions for the physical file the object represents. Files have a number of specific access rights that allow granular control over who can access a file and the manner in which they can access it. These access rights, taken from the MSDN, are shown in Table 11-7.

Table 11-7. File Access Rights

Access Right

Meaning

FILE_ADD_FILE

For a directory, the right to create a file in the directory.

FILE_ADD_SUBDIRECTORY

For a directory, the right to create a subdirectory.

FILE_ALL_ACCESS

All possible access rights for a file.

FILE_APPEND_DATA

For a file object, the right to append data to the file; for a directory object, the right to create a subdirectory.

FILE_CREATE_PIPE_INSTANCE

For a named pipe, the right to create a named pipe.

FILE_DELETE_CHILD

For a directory, the right to delete a directory and all files it contains, including read-only files.

FILE_EXECUTE

For a native code file, the right to run the file (given to scripts, might cause the script to be executable, depending on the script interpreter).

FILE_LIST_DIRECTORY

For a directory, the right to list the directory's contents.

FILE_READ_ATTRIBUTES

The right to read file attributes.

FILE_READ_DATA

For a file object, the right to read the corresponding file data; for a directory object, the right to read the corresponding directory data.

FILE_READ_EA

The right to read extended file attributes.

FILE_TRAVERSE

For a directory, the right to traverse the directory.

FILE_WRITE_ATTRIBUTES

The right to write file attributes.

FILE_WRITE_DATA

For a file object, the right to write data to the file; for a directory object, the right to create a file in the directory.

FILE_WRITE_EA

The right to write extended attributes.

STANDARD_RIGHTS_READ

Includes READ_CONTROL, which is the right to read information in the file or directory object's security descriptor.

STANDARD_RIGHTS_WRITE

Includes WRITE_CONTROL, which is the right to write to the directory object's security descriptor.


These file permissions can be applied when creating the file with the CreateFile() function. When you're auditing code that creates new files, it's important to correlate the permissions applied to the new file with what entities should have permission to read and/or modify that file. The lack of correct permissions can result in unintentional disclosure of information and possibly rogue users modifying sensitive files that alter how the program works. As an example, a program is generating sensitive information about employees, including salary summaries and so forth. If relaxed permissions are applied to the file object when it's created, any other employee might be able to discover their coworkers' salaries.

The File I/O API

The Windows File I/O API provides access to files through object handles, so all file-manipulation functions use handles to perform operations on a file. The API provides a basic set of functionality for creating, opening, reading, and writing to files as well as performing more advanced operations. This functionality is exposed through a large number of functions; however, the main ones you'll deal with daily are just CreateFile(), ReadFile(), WriteFile(), and CloseHandle(). These functions are responsible for the basic operations performed on files in most applications. As a code auditor, your primary focus is the CreateFile() routine because it's the most likely place for things to go awry, so this section primarily covers this function.

Note

There's also an OpenFile() function just for opening files, but it's for 16-bit Windows applications and is no longer used.


The CreateFile() function is used for both creating and opening files and has the following prototype:

HANDLE CreateFile(LPCSTR lpFileName, DWORD dwDesiredAccess,                   DWORD dwSharedMode,                   LPSECURITY_ATTRIBUTES                   lpSecurityAttributes,                   DWORD dwCreationDisposition,                   DWORD dwFlagsAndAttributes,                   HANDLE hTemplateFile)


As you can see, this function takes quite a few parameters. These parameters are briefly described in the following list:

  • lpFileName This parameter is the name of the file to open or create.

  • dwDesiredAccess This parameter is the access the application requires to the file: read access, write access, or both.

  • dwSharedMode This parameter describes what access is allowed by other processes while the returned handle remains open.

  • lpSecurityAttributes This parameter describes the object access rights for the file if a new one is being created. It also describes whether the file handle is inheritable.

  • dwCreationDisposition This flag affects whether to create a new file and what to do if a file of the same name already exists. A value of CREATE_ALWAYS always creates a new file, overwriting another file if it already exists. A value of CREATE_NEW creates a new file or causes the function to fail if a file with the same name exists. A value of OPEN_ALWAYS causes the function to open an existing file if one exists; otherwise, it creates a new one. A value of OPEN_EXISTING causes the function to fail if none exist, and a value of trUNCATE_EXISTING causes the function to fail if the file doesn't exist but truncates the file to 0 bytes if it does exist.

  • dwFlagsAndAttributes This parameter describes certain attributes of the file being created. Relevant values are described as they come up in the following sections.

  • hTemplateFile This parameter provides a handle to a template file; its file attributes and extended attributes are used to establish the attributes of a new file being created. If an existing file is being opened, this parameter is ignored.

You can see there are a lot of possibilities for determining how files are created or opened.

File Squatting

In the discussion on objects, you learned about object namespace squatting. It's applicable to files as well, if the CreateFile() function is used incorrectly. Sometimes it's possible to cause an application to act as if it has created a file when it has actually opened an existing file. This error causes several parameters to be ignored, thus potentially tricking the application into exposing sensitive data or allowing users to control data in a file they shouldn't be able to control. A file-squatting vulnerability occurs when these conditions are met:

  • An application should create a new file, not open an existing file, but the dwCreationDisposition parameter is set incorrectly. Incorrect settings are any setting except CREATE_NEW.

  • The location where the file is being created is writeable by potentially malicious users.

If both conditions are met, a vulnerability exists in the application whereby attackers would be able to create a file of the same name first and give the file arbitrary security attributes, ignoring the ones that have been supplied. In addition, because this file squatting also causes the supplied file attributes to be ignored, it might be possible to make the application function incorrectly by creating a file with different attributes. For example, consider the following call:

BOOL CreateWeeklyReport(PREPORT_DATA rData, LPCSTR filename) {     HANDLE hFile;     hFile = CreateFile(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,         FILE_ATTRIBUTE_ARCHIVE, NULL);     if(hFile == INVALID_HANDLE_VALUE)         return FALSE;     ... write report data ... }


This code is meant to mark the report it generates for archiving, presumably so that it can be backed up periodically. However, if attackers create a file with the same name before the application, this file attribute is ignored. Therefore, attackers can read potentially sensitive data that gets written to the report file and omit FILE_ATTRIBUTE_ARCHIVE from the file's attributes, resulting in the report not being backed up as intended.

Note

It may seem that the CREATE_ALWAYS parameter would prevent file squatting attacks because it will overwrite an existing file. However, if a file already exits, the CREATE_ALWAYS parameter will cause CreateFile() to retain the DACL and attributes of the overwritten file and ignore the DACL supplied in the security descriptor.


Canonicalization

Canonicalization is the process of turning a pathname from one of several different relative forms into its simplest absolute form. It was covered in depth in Chapter 8, "Strings and Metacharacters," but is discussed again here because it holds special significance in Windows. Generally, it's risky to use untrusted data to construct relative pathnames. Why? Because it gives attackers the opportunity to specify an absolute path, if they are able to control the initial part of the filename argument. A simple example of a vulnerable call is shown:

char *ProfileDirectory = "c:\\profiles"; BOOL LoadProfile(LPCSTR UserName) {     HANDLE hFile;     if(strstr(UserName, ".."))         die("invalid username: %s\n", UserName);     SetCurrentDirectory(ProfileDirectory);     hFile = CreateFile(UserName, GENERIC_READ, 0, NULL,         OPEN_EXISTING, 0, NULL);     if(hFile == INVALID_HANDLE_VALUE)         return FALSE;     ... load profile data ... }


When auditing code, it's important to train yourself to spot bad use of canonical pathnames, as in this example. The developer assumes that by setting the current working directory and ensuring that no directory traversal double-dot combinations exist, any file access can only be for a file in the specified profile directory. Of course, because UserName is given as the initial part of the path segment, attackers could simply select a username that's an absolute path and access any file outside the current directory.

In addition, CreateFile() canonicalizes any directory traversal components before validating whether each path segment exists. So you can supply nonexistent paths in the filename argument as long as they are eliminated during canonicalization. For example, CreateFile() will open C:\blah.txt if you specify a filename such as C:\nonexistent\path\..\..\blah.txt; it doesn't matter that C:\nonexistant\path\ does not exist. This canonicalization issue might be relevant when a path is prepended to user input. Here's a modified version of the previous example that demonstrates this issue.

char *ProfileDirectory = "c:\profiles"; BOOL LoadProfile(LPCSTR UserName) {     HANDLE hFile;     char buf[MAX_PATH];     if(strlen(UserName) >        MAX_PATH  strlen(ProfileDirectory)  12)         return FALSE;     _snprintf(buf, sizeof(buf), "%s\\prof_%s.txt",               ProfileDirectory, UserName);     hFile = CreateFile(buf, GENERIC_READ, 0, NULL,         OPEN_EXISTING, 0, NULL);     if(hFile == INVALID_HANDLE_VALUE)         return FALSE;     ... load profile data ... }


This example doesn't check for directory traversal, although it allows you to control only part of the filename. It makes no difference, however, because you can specify nonexistent path components. Therefore, you can still perform a directory traversal attack by using \..\..\..\test or another similar pathname.

Filelike Objects

Several other types of objects can be opened via CreateFile() and treated as regular files. They aren't files that appear in the file system hierarchy but objects that appear in the object namespace. These objects have a special filename format to indicate that they aren't regular files:

\\host\object


The host component is any host that can be reached from the target machine; the local host is indicated by using a period (.). The object component should be familiar if you've ever opened a file on a remote Windows share. In that case, the object is just the share name and fully qualified path to the file. However, the format of the object component actually depends on which type of object is being opened. CreateFile() can open several different types of objects: pipes, mailslots, volumes, and tape drives.

Pipes and mailslots are IPC mechanisms that you explore more in Chapter 12, but for now, it's necessary to know how they can be opened as files.

For these object types, the object component of the name uses the following format:

type\name


The type component is the class of object, such as pipe or mailslot. The name component is the name of the object. So you can open the stuff pipe on myserver by using the following string:

\\myserver\pipe\stuff


In Chapter 12, you see that Windows authentication and impersonation can make the capability to open one of these IPC mechanisms a vulnerability in and of itself because this capability gives attackers the opportunity to steal client privileges. Tape and volume accesses can also be achieved; however, a volume can't be read from and written to with the regular File API. So an incorrect open will likely become apparent to the application when it tries to perform operations on the file handle.

To access these objects, attackers must control the first segment of the pathname. Being able to achieve this control isn't common, but it happens from time to time. For instance, the example from the previous section would be able to specify some of these objects, which might afford attackers the opportunity to perform an impersonation-style attack.

Device Files

Device files are special entities that reside in the file hierarchy and allow a program to have access to virtual or physical devices. In UNIX, this access is typically handled by storing special device files in a common directory (usually /dev). In Windows, it's handled a bit differently. Device files in Windows don't have inode entries on the file system volume, as they do in UNIX; in fact, Windows devices don't exist on the file system at all! Instead, they're represented by file objects in the object namespace. The CreateFile() function checks when a file access is made to see whether a special device file is requested; if so, it returns a handle to the device object rather than a handle to a regular file. This process happens transparently to the application. The following special device names can be opened by applications:

  • COM1-9

  • LPT1-9

  • CON

  • CONIN$

  • CONOUT$

  • PRN

  • AUX

  • CLOCK$

  • NUL

The CreateFile() function searches the filename argument for these devices by looking at the filename component and ignoring the pathname components. Therefore, a device name can be appended to any file path, and it opens a device rather than a regular file. This behavior is somewhat hard to combat in applications because it introduces unexpected attack vectors. Specifically, if part of the filename parameter is user supplied, a device can be accessed by using any of the listed filenames.

Note

There's an exception: Console devices are treated specially by CreateFile(), so CONIN$, CONOUT$, and CON can't be appended to arbitrary paths to access a console device. Any of the other listed devices, however, exhibit the described behavior.


Accessing devices in this way might cause an application to unexpectedly hang or read and write data to and from devices that it didn't intend to. Consider the following example:

HANDLE OpenProfile(LPCSTR UserName) {     HANDLE hFile;     char path[MAX_PATH];     if(strstr(UserName, ".."))         die("Error! Username %s, contains illegal characters\n",             UserName);     _snprintf(path, sizeof(path), "%s\\profiles\\%s",               ConfigDir, UserName);     hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ,                        NULL, OPEN_EXISTING, 0, NULL);     if(hFile == INVALID_HANDLE_VALUE)         die("opening file: %s\n", path);     return hFile; }


Assume that UserName contains untrusted data. Although path traversal attacks have been taken into account, there is no provision for the username specifying a device file.

Another point about reserved device names is that they can also have any file extension appended, and they are still considered a device. For example, the file c:\COM1.txt still opens the COM1 device. Therefore, any code that appends a file extension to a filename might still be vulnerable to attacks, resulting in the application unwittingly opening a device rather than a regular file.

File Types

No parameter can be passed to CreateFile() to ensure that the file being opened is a regular file, so you might be wondering how any call to CreateFile() can be secure from attack without a lot of messy string-matching code to test for device names. The answer is that several functions can be used to determine whether the file in question is a regular file. Specifically, application developers can use GetFileAttributes() and GetFileAttributesEx() to retrieve file attributes and GetFileType() to get the type of a file.

In addition, you can do something in the CreateFile() call to prevent it from opening device files and special files: Use the Universal Naming Convention (UNC) form and prefix the filename with \\?\. Putting this sequence at the beginning of a filename has several effects on how CreateFile() parses the filename; essentially, it minimizes the amount of parsing performed on the filename, which causes it to skip certain checks, including whether the file is a DOS device or a special file.

The caveat of the UNC form is that it changes the way the filename is handled and might create pathnames that are inaccessible via the traditional DOS-style path. This happens because the DOS naming convention is limited to 260 characters for a fully qualified path. However, NTFS supports a maximum path length of 32,767, but these names can be accessed only by using a UNC pathname provided to the Unicode version of the CreateFile() function.

File Streams

NTFS supports the notion of file streams, also known as alternate data streams (ADSs). A file stream is simply a named unit of data associated with a file. Each file is composed of one or more file streams. The default file stream is nameless, and any operations performed on a file are implicitly assumed to be dealing with the unnamed file stream, unless another file stream is specified. A fully qualified file stream name has the following format:

filename:file stream name:file stream type


You're no doubt already familiar with the format of filenames, so you can move on to file stream names. The file stream name has the same format as a filename (without the pathname component). It can contain nearly any character, including spaces. Finally, the file stream type member (which is often omitted) specifies a file stream attribute. Although several attributes exist, the only valid choice is $DATA.

For code auditors, file streams can introduce vulnerabilities in certain contexts, particularly when filenames are being constructed based on user input, and those filenames are expected to be of a certain format and have a specific extension. For example, a Web application has a user profiles directory in the Web root where each user's profile is kept in a text file. The following code opens the user profiles directory:

BOOL OpenUserProfile(LPCSTR UserName) {     HANDLE hProfile;     char buf[MAX_PATH];     if(strlen(UserName) >= MAX_PATH  strlen(ProfilesDir) - 4)         return FALSE;     if(strstr(UserName, ".."))         return FALSE;     _snprintf(buf, sizeof(buf), "%s\\%s.txt", ProfilesDir,               UserName);     hProfile = CreateFile(buf, GENERIC_ALL, FILE_SHARE_READ,                           NULL, CREATE_ALWAYS, 0, NULL);     if(hProfile == INVALID_HANDLE_VALUE)         return FALSE;     ... load or create profile ... }


The intention of this code is to create a text file in the user profiles directory; however, you can create a file with any extension you please by specifying a username such as test.asp:hi. This username would cause the code to create the test.asp file with the file stream hi.txt. Although you could create arbitrary files in this example, accessing the alternate file streams where you're writing data might prove to be more complicated, depending on the Web server being used to serve files.

Attacks of this nature tend to work on Web-related technologies because filenames are often completely user controlled, and how the filename appears to the Web server makes a big difference in how it's processed and served to users. For example, the file extension might cause a file to be handled by a certain filter or Web server extension, as in IIS. In fact, default installations of IIS 4 and earlier had a vulnerability involving file streams that took advantage of this situation. By appending ::$DATA to an ASP script file, it was possible to read the source of the file remotely instead of having it run the contents as script code because IIS didn't correctly identify it as an ASP file and hand it off to the ASP ISAPI extension for processing. So a request such as the following could allow the contents of the login.asp script on a Web server to be revealed:

GET /scripts/login.asp::$DATA


Note that when using ADS notation to specify alternate data streams, the only way to represent the unnamed stream is by using ::$DATA. You can't omit the $DATA extension. The filenames C:\test.txt: and C:\test.txt:: are illegal as far as CreateFile() is concerned, and attempting to create or open files with these names results in an error.

Extraneous Filename Characters

CreateFile() has a few more idiosyncrasies that don't belong in any other category, so they are mentioned here. First, CreateFile() performs special handling of trailing spaces in file names. Any trailing spaces in the filename argument are silently stripped out, which introduces some possible vulnerabilities. This behavior might be a useful method of stripping out trailing path data, thus allowing attackers to choose an arbitrary file extension, as shown in this example:

BOOL OpenUserProfile(LPCSTR UserName) {     char buf[MAX_PATH];     HANDLE hFile;     if(strstr(UserName, ".."))         return FALSE;     _snprintf(buf, sizeof(buf), "%s\\%s.txt", ProfileDirectory,               Name);     buf[sizeof(buf)-1] = '\0';     hFile = CreateFile(buf, GENERIC_ALL, FILE_SHARE_READ, NULL,                        CREATE_NEW, 0, NULL);     if(hFile == INVALID_HANDLE_VALUE)         return FALSE;     ... more stuff ... }


This code is intended to create a text file and enforces this behavior by appending a .txt extension. However, if users specify a filename that's close to MAX_PATH bytes, this .txt file extension might get cut off. By specifying a filename with an arbitrary extension followed by a large number of spaces, users could create any type of file they like.

Having arbitrary trailing spaces might also cause an application to incorrectly identify files with special names or file extensions and use them incorrectly. For example, consider the following code:

HANDLE GetRequestedFile(LPCSTR requestedFile) {     if(strstr(requestedFile, ".."))         return INVALID_HANDLE_VALUE;     if(strcmp(requestedFile, ".config") == 0)         return INVALID_HANDLE_VALUE;     return CreateFile(requestedFile, GENERIC_READ,                       FILE_SHARE_READ, NULL, OPEN_EXISTING, 0,                       NULL); }


This simple example checks whether users are requesting a special file .config, and if they are, doesn't allow them to access it. However, by specifying a filename such as ".config", users can still gain access to this file.

Note

Users would also be able to access the file by requesting .config::$DATA.


Spaces trailing the filename might also pose a threat when files are supposed to be unique, but the call to CreateFile() uses the CREATE_ALWAYS value for dwCreationDisposition instead of CREATE_NEW. Returning to the user profiles example, imagine you have an administrative user with special privileges. You might be able to steal the administrator's credentials by creating an account with a username such as "admin". Selecting this username might make it possible to read administrative profile data or even overwrite it.

Spaces aren't the only extraneous characters stripped from filename arguments. Another interesting behavior of CreateFile() is that it strips trailing dots from the filename in much the same way it strips spaces. Any number of trailing dots are silently stripped off the end of a filename before the file is created, introducing opportunities for creating or opening unexpected files in much the same way using spaces does. So creating a file named "c:\test.txt.........." creates the c:\test.txt file. As an interesting variation, both spaces and dots can be intermingled in any order, and CreateFile() silently strips both spaces and dots. For example, passing the filename "c:\test.txt . .. ..." to CreateFile() also creates the C:\test.txt file. This behavior isn't well known and isn't obvious to developers, so attackers can use this suffix combination to trick applications into opening files. This is especially true of Web-based applications and Web servers because filename extensions often determine how they handle files. In fact, appending dots or spaces to filenames has resulted in several instances of being able to view the source for script code.

One other behavior of these trailing characters is that they aren't stripped if an ADS stream follows the filename. For example, if you pass the name c:\test.txt. to CreateFile(), the trailing dot is stripped and the c:\test.txt file is created. However, if you pass the name c:\test.txt.:stream to CreateFile(), the trailing dot isn't stripped, and the c:\test.txt. file is created (with an ADS named stream). The same happens if you have an unnamed ADS following the file extension, such as ::$DATA. However, if you have dots and/or spaces following the ADS component of the filename, they are truncated. So the string "C:\\test.txt::$DATA ......... . . . ..." creates the c:\test.txt file and writes to the default unnamed file stream.

As a final note, DOS device names might end with a colon character (:) that's silently stripped out, and the device is accessed as normal. They might also contain additional characters after the colon, and the function still succeeds. However, an ADS isn't created for the device; the extraneous data is just ignored.

Case Sensitivity

One thing that distinguishes Windows filenames from UNIX filenames is that NTFS and FAT filenames aren't case sensitive. Therefore, bypassing filename and path checks by mixing case when accessing files is possible sometimes. If you look at the previous example, the GetrequestedFile() function is intended to block people from accessing the .config file in any directory. You saw a method for gaining access to the file by using extraneous trailing characters, but another method you could use is requesting the file with some or all of the characters in uppercase. Therefore, by requesting .CONFIG, you can retrieve the contents of a file that's supposed to be hidden from you. Any file accesses in Windows need to be assessed for case-mixing when validating filenames or file extensions. SPI Dynamics discovered precisely this type of bug in the Sun ONE Web server. The Sun ONE Web server determined how to process files based on the server extension, yet it treated the filenames as case sensitive because it was originally built for UNIX systems. Therefore, if a JSP page was requested with an uppercase extension (hello.JSP as opposed to hello.jsp), the server would mistakenly list the file's source code rather than run the script. A description of this bug is available at http://sunsolve.sun.com/search/document.do?assetkey=1-26-55221-1.

DOS 8.3 Filenames

In early versions of Windows and DOS, filenames were represented in the 8.3 format. This term refers to a filename composed of up to eight letters, followed by a dot, followed by a three-letter file extension. The introduction of Windows NT and 95 allowed using longer filenames, filenames containing spaces, and filenames without extensions. To retain compatibility with earlier Windows versions, these newer file systems store a long filename and an 8.3 filename for every file. This 8.3 filename is generally composed of the first six letters of the long filename followed by a tilde(~) and a number, and then the dot and the first three letters of the extension. The number after the tilde differentiates between long filenames that have the first six letters of their names in common. For example, the thisisalongfilename.txt filename can usually be referred to as thisis~1.txt.

This format can become a bit of a security problem for filenames that are more than eight characters, not including the dot or file extension. This issue is relevant when certain files aren't allowed to be accessed or data is kept in separate files distinguished by a key that's meant to be unique. For example, refer to the user profile code used to demonstrate some file handling vulnerabilities so far. In applications such as this one, it might be possible to steal other users' credentials by creating a username that's the same initial six letters followed by a ~1. Assume the application is managing users, one of whom is an administrator with the username administrator. Creating a new user with the name admini~1 might allow an attacker to access that user's profile due to the equivalence of the two names.

When auditing code for bugs of this nature, be mindful that it may be possible to circumvent filename restrictions if a requested filename is larger than eight characters. However, this issue can be prevented by prepending the UNC path identifier (\\?\) to disable DOS filename parsing when calling CreateFile().

Auditing File Opens

The flexibility of the CreateFile() function can cause a number of problems. You can formalize these problems as an ordered list of things to check to determine whether a file open is safe. This summary has been divided into tables based on what part of the filename users can control: the beginning, the middle, or the end. Some potential vulnerabilities fit into more than one of these categories, so there's also a table summarizing attacks that are possible when users control any part of the filename. This section is a summary of all the attacks discussed thus far in file openings, so it is intended as a reference for code auditors when encountering file opens. These tables simply list attacks made possible by the file APIs and don't explain when they could be used to compromise an application because you have already covered that ground. These summaries are just based on generic file open problems that might occur; applications might, of course, contain context-specific logic flaws in the way they open files (such as not adequately checking file permissions when running in an elevated context), and these flaws aren't summarized. Finally, these rules don't apply if untrusted data is not used to compose any part of the pathname.

Controlling the Beginning of a Filename

Table 11-8 summarizes potential vulnerabilities to check for when users can control the beginning of a filename argument.

Table 11-8. Controlling the Beginning of a Filename

Attack

Vulnerable If

Specifying an absolute path

There's no check for path separators.

Specifying a named pipe

The code fails to check that the file being accessed is a regular file (has the attribute FILE_ATTRIBUTE_NORMAL) using GetFileAttributes() or is a disk file (FILE_TYPE_DISK) according to GetFileType().

Specifying a mailslot

Same as for named pipes.


Controlling the Middle of a Filename

Table 11-9 summarizes potential problems when malicious users can specify part of the filename, but there's constant data both before and after the user-controlled string.

Table 11-9. Controlling the Middle of a Filename

Attack

Vulnerable If

Directory traversal attack

The code fails to check for directory traversal characters (..).

DOS 8.3 filenames

The code does static string comparisons on potentially long filenames and makes policy decisions based on that comparison. Also, the filename must be passed to CreateFile() without being prefixed with \\?\.


Controlling the End of a Filename

Table 11-10 summarizes vulnerabilities that might arise in an application when users can control the end of a filename. In many instances, it might be the intention that users control just the middle of a filename, but they can control the end by using up the entire amount of space in a buffer. For example, in the following line, if user_input is large enough, the .txt extension will be cut off:

_snprintf(buf, sizeof(buf), "%s.txt", user_input);


Table 11-10. Controlling the End of a Filename

Attack

Vulnerable If

Directory traversal attack

The code fails to check for directory traversal characters (..).

Adding extraneous trailing characters

Some checks are made on the file extension or filename without taking into account the silent truncation of spaces and dots.

DOS 8.3 filenames

The code does static string comparisons on potentially long filenames and makes policy decisions based on that comparison. Also, the filename must be passed to CreateFile() without being prefixed with \\?\.


Controlling Any Part of the Filename

Table 11-11 summarizes generic attacks that might be available to attackers, no matter what part of the filename they control.

Table 11-11. Controlling Any Part of a Filename

Attack

Vulnerable If

Specifying a device

The code fails to check that the file being accessed is a regular file (has the attribute FILE_ATTRIBUTE_NORMAL) using GetFileAttributes() or is a disk file (FILE_TYPE_DISK) according to GetFileType(). Also, vulnerable only if the pathname isn't prefixed with \\?\.

Specifying ADS

The code fails to check for the ADS separator (:).

Filename squatting

The code intends to create new files but doesn't use the CREATE_NEW flag to CreateFile(), and users are able to write files into the relevant directory.

Case sensitivity

The code does checks on a filename assuming case sensitivity (more common in code ported from UNIX to Windows).


Links

Links provide a mechanism for different file paths to point to the same file data on disk. Windows provides two mechanisms for linking files: hard links and junction points. Hard links in Windows are similar to those in UNIX; they simply allow a file on disk to have multiple names. Junction points enable a directory to point to another directory or volume attached to the system. They apply to directories only; there's no soft link parallel in Windows, with the exception of Windows shortcut files. The presence of these special files might allow attackers to trick applications into accessing files in unauthorized locations, thus potentially undermining the security of the application. The following sections discuss how to identify problems that result from encountering these types of special files.

Hard Links

Creating a hard link simply assigns an additional name to the linked file so that the file can be referred to by either name. A file object on disk keeps track of how many names refer to it so that when a link is deleted, the file is removed from the system only when no more names refer to it. A hard link can be created programmatically by using the CreateHardLink() function. Hard links can be applied only to files, not directories, and the original file and the new hard link must reside on the same volume; you can't create a link to a file where the target name resides on a separate volume or a remote location specified by a UNC path name. Finally, the user creating the hard link must have appropriate access to the destination file.

Junction Points

Junction points are special directories that are simply pointers to another directory; the target directory can be located on the same volume or a different volume. In contrast to hard links, junction points can point only between directories; files can't be used as the source or target of a junction point.

Note

Actually, you can create directory junction points that point to files, but attempts to open them always fail with ERROR_ACCESS_DENIED.


Apart from this limitation, junction points are similar to the symbolic links discussed already in the UNIX chapters. Junctions are available only on volumes formatted as NTFS 5 and later, as they use reparse point functionality in those NTFS versions.

Reparse Points

Junctions are implemented through the use of NTFS reparse points. NTFS files and directories can set the FILE_ATTRIBUTE_REPARSE_POINT attribute to indicate that a file system driver needs to intervene in file-handling operations. The file system driver then performs special parsing on a reparse data buffer associated with the file. Every file system driver that implements reparse points has a unique tag registered in the kernel. When a file with a reparse point is encountered, the reparse data buffer is compared against each registered tag value, and then passed off to the appropriate driver when a match is found. If no match is found, the file access fails.

Junctions are one implementation of reparse points. They apply only to directories, which must be emptya constraint of reparse points applied to directories. Their data buffer contains a pointer to the target location the directory is intended to point to. The driver can then use this information to find the real target file an application is attempting to access.

At the time of this writing, there's no publicly exposed API to manipulate reparse points easily. However, users can construct and examine reparse data buffers by using the DeviceIoControl() function. Mike Nordell explains in more detail how to create and manipulate reparse points at www.codeproject.com/w2k/junctionpoints.asp.


Because junction points are dynamicmeaning they can point anywherewhere the junction points can change at any time. Their presence represents some potential issues for applications trying to access files securely. These vulnerabilities fall into two primary categories, explained in the following sections:

  • Unintentional file access outside a particular subdirectory structure

  • File access race conditions

Arbitrary File Accesses

Often an application should restrict access to a confined region of the file system. For example, an FTP server might export only a specific subdirectory, or an application that manages user profiles might access user data in only a certain subdirectory.

Say a privileged service is accessing files in c:\temp, which a normal user can also write to. Attackers might be able to cause the service to access system files that it shouldn't. The following example shows some vulnerable code:

BOOL WriteToTempFile(LPCSTR filename, LPCSTR username,                      LPVOID data, size_t length) {     char path[MAX_PATH], ext[8];      HANDLE hFile;     if(strchr(filename, '\\') != NULL        || strstr(filename, "..") != NULL)         return FALSE;     generate_temporary_filename_extension(ext);     snprintf(path, sizeof(path)-1, "c:\\temp\\%s_%s_%s.txt",              user, filename, ext);     path[sizeof(path)-1] = '\0';     hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ,                        NULL, CREATE_ALWAYS, 0, NULL);     if(hFile == INVALID_HANDLE_VALUE)         return FALSE;     ... write data ... }


There are several problems with the way this code is written, but assume attackers can provide the filename, but not the username; the username is determined when they log in. By creating a junction with the same name as the file being created, attackers can have this filename written to anywhere on the file system. Furthermore, a large number of spaces (as discussed earlier) can be used to remove the extension and create a completely predictable file.

To perform this attack, users (say bob) could create a junction in c:\temp pointing to C:\Windows\system32 and named bob_dirname. Attackers would then specify a filename with enough spaces to cut off the trailing data, so the resulting path would translate to any arbitrary file under the main 32-bit system directory. Assuming the application is running with sufficient privileges, this allows the attacker to replace executables or libraries used by services and administrative users.

In this example, users need to be able to supply a file separator. The code checks for \\, not /, which allows them to supply one. Because junctions can be linked successfully only between two directories, path separators are always an additional consideration when determining whether a bug is exploitable through the use of junctions. If a path separator can't be specified, exploitation is possibly more limited. As always, exploitability of a bug of this nature depends on how the pathname is built and whether the file is written to or read from. Still, there is the potential for a vulnerability any time attackers can potentially circumvent an application's file access restrictions to affect arbitrary parts of the file system.

It can also be dangerous to read a file controlled by less privileged users. A malicious user might be able to perform some nasty tricks, particularly by using junctions. To understand this problem, take a look at a simple example:

int LoadUsersSettings(LPCSTR User, LPCSTR SettingsFileName) {     char path[MAX_PATH];     HANDLE hFile;     _snprintf(path, sizeof(path)-1, "%s\\appdata\\%s",               get_home_directory(User),         SettingsFileName);     path[sizeof(path)-1] = '\0';     hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ,                        NULL, OPEN_ALWAYS, 0, NULL);     If(hFile == INVALID_HANDLE_VALUE)         return -1;     ... read the file ... }


This code seems innocent enough, assuming the get_home_directory() function works as expected. However, attackers could create a junction named appdata that points to an arbitrary location on the file system. If they can then specify the SettingsFileName argument, they could use junctions to arbitrarily read any file on the system.

File Access Race Conditions

When a privileged process needs to access an object on the file system on behalf of a less privileged user, there are two basic ways to do so. The first way is to impersonate the user and attempt to access the file as normal; the second way is to retrieve information about the file and then decide whether to proceed based on file attributes and related security rights. The second approach carries some inherent dangers because the file system isn't a static entity and neither are the objects residing on it. Therefore, the state of the file could change between the time file attributes are examined and when the file is actually operated on. This situation is referred to as a race condition. You have examined race conditions already on UNIX file systems, and race conditions on Windows file systems are quite similar.

TOCTTOU

As in UNIX, race conditions primarily occur as a result of the time of check to time of use (TOCTTOU) variance in an application. This vulnerability occurs when a security check is done on a file (examining the owner or other properties of the file), and then the file is accessed later, assuming the security check passes. During the intervening period, the file could be modified so that its attributes change, resulting in the privileged application accessing a file it didn't intend to. The scope of this attack tends to be more limited in Windows because the File APIs are designed in such a way that they're less conducive to attacks of this nature. For example, in UNIX, TOCTTOU attacks could happen by using access() and then open(). There's no direct correlation of that code sequence in Windows; the API encourages checks to be done as the file is being opened. However, being able to change attributes between a call to GetFileAttributes() and CreateFile() could have consequences that lead to a vulnerability.




The Art of Software Security Assessment. Identifying and Preventing Software Vulnerabilities
The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities
ISBN: 0321444426
EAN: 2147483647
Year: 2004
Pages: 194

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net