PHP文件上传流量层面WAF绕过
2022-7-4 15:51:6 Author: xz.aliyun.com(查看原文) 阅读量:39 收藏

[TOC]

简介

  • PHP文件上传实现规范为RFC1867
  • 实验环境为 PHP 7.3.4 + nginx 1.20.1,关于上传部分的相关源码在github,PHP解析multipart/form-data http请求体的入口函数SAPI_POST_HANDLER_FUNC
  • PHP源码调试环境参考
  • PHP示例代码
<?php
var_dump($_FILES);
?>
  • 文件解析的简要流程如下

TRICKS

前向截断

  • \/会对文件名进行前向截断,类似info.txt/info.php的文件名经php处理后会变成info.php

  • 调用栈如下

  • 其中有一段注释如下,其本意是为了解决IE上传文件时传递全路径名的问题
/* The \ check should technically be needed for win32 systems only where
     * it is a valid path separator. However, IE in all it's wisdom always sends
     * the full path of the file on the user's filesystem, which means that unless
     * the user does basename() they get a bogus file name. Until IE's user base drops
     * to nill or problem is fixed this code must remain enabled for all systems. */
  • 关键函数在php_ap_basename,该函数会寻找\/字符最后出现的位置,并从该位置截断字符串,从而造成了前向的截断
static char *php_ap_basename(const zend_encoding *encoding, char *path)
{
    char *s = strrchr(path, '\\');
    char *s2 = strrchr(path, '/');

    if (s && s2) {
        if (s > s2) {
            ++s;
        } else {
            s = ++s2;
        }
        return s;
    } else if (s) {
        return ++s;
    } else if (s2) {
        return ++s2;
    }
    return path;
}

后向截断

  • 00会对文件名进行后向截断,类似info.php(00)xxx的文件名经php处理过后会变成info.php

  • 在解析header时候,仅对内存进行了copy,内存视图如下

  • 后续解析filename时候使用strlen获取filename长度,strlen原型如下,在遇到\0,即内存中的00时,认为字符串结束了,也就造成了截断
头文件:#include <string.h>

strlen()函数用来计算字符串的长度,其原型为:
    unsigned int strlen (char *s);

【参数说明】s为指定的字符串

strlen()用来计算指定的字符串s 的长度,不包括结束字符"\0"
  • 同样的,00可以对$_POST变量名也可以进行截断,对$GET$_COOKIE等变量名添加00会导致400错误

    • 示例代码
    • 正常请求

    • $_POST变量名中添加00后,可以看到postxxx变为了post

    • 但是在$_POST变量值中添加00则不受影响,但是会使得字符串长度加1

文件名末尾的\字符会被忽略

  • 如图所示

  • 关键函数在php_ap_getword,当出现\+quote这样相连的两个字符时,会忽略\只取quote的值
static char *php_ap_getword(const zend_encoding *encoding, char **line, char stop)
{
    char *pos = *line, quote;
    char *res;

    while (*pos && *pos != stop) {
        if ((quote = *pos) == '"' || quote == '\'') {
            ++pos;
            while (*pos && *pos != quote) {
                // 此处会忽略 \ 字符
                if (*pos == '\\' && pos[1] && pos[1] == quote) {
                    pos += 2;
                } else {
                    ++pos;
                }
            }
            if (*pos) {
                ++pos;
            }
        } else ++pos;
    }
    if (*pos == '\0') {
        res = estrdup(*line);
        *line += strlen(*line);
        return res;
    }

    res = estrndup(*line, pos - *line);

    while (*pos == stop) {
        ++pos;
    }

    *line = pos;
    return res;
}

;可以影响文件名解析的结果

  • 类似filename=info.php;.txt;这样的字符串经过PHP处理后,会变成info.php注意filename的值没有用双引号包裹,用双引号包裹会导致失败

  • 在解析Content-Disposition时,会先使用;符号进行分词,然后使用=进行分词。所以,类似filename=info.php;.txt;这样的字符串第一次分词后结果为filename=info.php/txt,第二次分词时就将filename解析为了info.php,大致流程如下
SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
{
    //...
    // 使用 ; 进行分词
    while (*cd && (pair = getword(mbuff->input_encoding, &cd, ';')))
    {
        //...
        // 按照 = 进行解析
        if (strchr(pair, '=')) {
         // ...   
        }
        // ...
    }
    // ...
}

双写filename

  • php解析Content-Disposition时,按照从前到后的顺序,如果后面有相同的变量名,则会进行值的覆盖,关键代码

失败的上传 - 1

  • filename首字符为00时,上传会失败。如下所示,在filename首字符前插入00,导致上传失败

if (filename[0] == '\0') {
#if DEBUG_FILE_UPLOAD
                sapi_module.sapi_error(E_NOTICE, "No file uploaded");
#endif
                cancel_upload = UPLOAD_ERROR_D;
            }

失败的上传 - 2

  • name首字符为]时,也会导致上传失败,如下所示

  • 关键代码,当*tmp == ']'时,skip_upload = 1,导致了后续处理时,忽略了上传的文件
while (*tmp) {
    if (*tmp == '[') {
        c++;
    } else if (*tmp == ']') {
        c--;
        if (tmp[1] && tmp[1] != '[') {
            skip_upload = 1;
            break;
        }
    }
    if (c < 0) {
        skip_upload = 1;
        break;
    }
    tmp++;
}

总结

  • 实战时,以上各种技巧可以灵活组合
  • 以上的tricks基于y4tacker文章以及php源码得来,相信深读源码的话,会有更多的tricks

参考


文章来源: https://xz.aliyun.com/t/11486
如有侵权请联系:admin#unsafe.sh