Toc
  1. boundary值处理
  2. body处理
    1. filename的特殊处理
Toc
0 results found
白帽酱
php在流量层面绕waf的一些姿势-php文件上传处理流程简单分析
2022/06/24 PHP WAF PHP WAF bypass

测试环境 PHP 7.1.9

php文件上传处理在 main/rfc1867.c 中的 rfc1867_post_handler函数

boundary值处理

if (SG(post_max_size) > 0 && SG(request_info).content_length > SG(post_max_size)) {
sapi_module.sapi_error(E_WARNING, "POST Content-Length of " ZEND_LONG_FMT " bytes exceeds the limit of " ZEND_LONG_FMT " bytes", SG(request_info).content_length, SG(post_max_size));
return;
}

/* Get the boundary */
boundary = strstr(content_type_dup, "boundary");//查找是否包含boundary
if (!boundary) {
int content_type_len = (int)strlen(content_type_dup);
char *content_type_lcase = estrndup(content_type_dup, content_type_len);

php_strtolower(content_type_lcase, content_type_len);
boundary = strstr(content_type_lcase, "boundary");
if (boundary) {
boundary = content_type_dup + (boundary - content_type_lcase);
}
efree(content_type_lcase);
}

if (!boundary || !(boundary = strchr(boundary, '='))) {//判断boundary值起始位置
sapi_module.sapi_error(E_WARNING, "Missing boundary in multipart/form-data POST data");
return;//传入畸形的boundary会抛出警告 (可以用来检测目标php是否开启错误回显)
}

boundary++;
boundary_len = (int)strlen(boundary);

if (boundary[0] == '"') {//引号包裹
boundary++;
boundary_end = strchr(boundary, '"');
if (!boundary_end) {
sapi_module.sapi_error(E_WARNING, "Invalid boundary in multipart/form-data POST data");
return;
}
} else {//非引号包裹
/* search for the end of the boundary */
boundary_end = strpbrk(boundary, ",;");//截止字符
}
if (boundary_end) {
boundary_end[0] = '\0';
boundary_len = boundary_end-boundary;
}

/* Initialize the buffer */
if (!(mbuff = multipart_buffer_new(boundary, boundary_len))) {
sapi_module.sapi_error(E_WARNING, "Unable to initialize the input buffer");
return;
}

首先,php先从Content-Type是否包含boundary字符串
之后寻找等号的位置作为起始位置,获取boundary值。
boundary的值有引号包裹和无引号包裹两种情况
在无引号包裹时,可以使用逗号或分号作为终止字符。
到这里就可以总结出有效boundary格式了
{任意字符}boundary{除等号外任意字符}=[“]{boundary内容}[“][[,;]{任意字符}]


static multipart_buffer *multipart_buffer_new(char *boundary, int boundary_len)
{
multipart_buffer *self = (multipart_buffer *) ecalloc(1, sizeof(multipart_buffer));

int minsize = boundary_len + 6;
if (minsize < FILLUNIT) minsize = FILLUNIT;

self->buffer = (char *) ecalloc(1, minsize + 1);
self->bufsize = minsize;

spprintf(&self->boundary, 0, "--%s", boundary);

self->boundary_next_len = (int)spprintf(&self->boundary_next, 0, "\n--%s", boundary);

self->buf_begin = self->buffer;
self->bytes_in_buffer = 0;

if (php_rfc1867_encoding_translation()) {
php_rfc1867_get_detect_order(&self->detect_order, &self->detect_order_size);
} else {
self->detect_order = NULL;
self->detect_order_size = 0;
}

self->input_encoding = NULL;

return self;
}

在获取boundary值之后
调用multipart_buffer_new来生成multipart的缓冲区
函数内定义了body中的分界线
–boundary值

结合上面几个特性构造一个比较极端的例子:
图片.png

body处理

下面开始body的处理

if (!multipart_buffer_headers(mbuff, &header)) { //获取multipart头
goto fileupload_done;
}
if ((cd = php_mime_get_hdr_value(header, "Content-Disposition"))) {
char *pair = NULL;
int end = 0;
while (isspace(*cd)) {
++cd;
}
while (*cd && (pair = getword(mbuff->input_encoding, &cd, ';'))) {
char *key = NULL, *word = pair;
while (isspace(*cd)) {
++cd;
}
if (strchr(pair, '=')) {
key = getword(mbuff->input_encoding, &pair, '=');
if (!strcasecmp(key, "name")) {
if (param) {
efree(param);
}
param = getword_conf(mbuff->input_encoding, pair);
if (mbuff->input_encoding && internal_encoding) {
unsigned char *new_param;
size_t new_param_len;
if ((size_t)-1 != zend_multibyte_encoding_converter(&new_param, &new_param_len, (unsigned char *)param, strlen(param), internal_encoding, mbuff->input_encoding)) {
efree(param);
param = (char *)new_param;
}
}
} else if (!strcasecmp(key, "filename")) {
if (filename) {
efree(filename);
}
filename = getword_conf(mbuff->input_encoding, pair);
if (mbuff->input_encoding && internal_encoding) {
unsigned char *new_filename;
size_t new_filename_len;
if ((size_t)-1 != zend_multibyte_encoding_converter(&new_filename, &new_filename_len, (unsigned char *)filename, strlen(filename), internal_encoding, mbuff->input_encoding)) {
efree(filename);
filename = (char *)new_filename;
}
}
}
}
if (key) {
efree(key);
}
efree(word);
}
static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header)
{
char *line;
mime_header_entry entry = {0};
smart_string buf_value = {0};
char *key = NULL;

/* didn't find boundary, abort */
if (!find_boundary(self, self->boundary)) {//匹配分界线
return 0;
}

/* get lines of text, or CRLF_CRLF */

while ((line = get_line(self)) && line[0] != '\0') {//获取下一行字符串 [1]
/* add header to table */
char *value = NULL;

if (php_rfc1867_encoding_translation()) { //[2] 判断编码 之后处理字符串会用到 好像默认禁用 写死了返回0??
self->input_encoding = zend_multibyte_encoding_detector((const unsigned char *) line, strlen(line), self->detect_order, self->detect_order_size);
}

/* space in the beginning means same header */
if (!isspace(line[0])) {
value = strchr(line, ':');
}

if (value) {
if (buf_value.c && key) {
/* new entry, add the old one to the list */
smart_string_0(&buf_value);
entry.key = key;
entry.value = buf_value.c;
zend_llist_add_element(header, &entry);
buf_value.c = NULL;
key = NULL;
}

*value = '\0';
do { value++; } while (isspace(*value));

key = estrdup(line);
smart_string_appends(&buf_value, value);
} else if (buf_value.c) { /* If no ':' on the line, add to previous line */
smart_string_appends(&buf_value, line); //[2] 如果没有冒号就作为上一行的值
} else {
continue;
}
}

if (buf_value.c && key) {
/* add the last one to the list */
smart_string_0(&buf_value);
entry.key = key;
entry.value = buf_value.c;
zend_llist_add_element(header, &entry);
}

return 1;
}

这个过程有几个值得注意的点

[1] multipart中换行可以不是CLRF,只需要包含一个\n就会判断为新行。

static char *get_line(multipart_buffer *self)
{
char* ptr = next_line(self);

if (!ptr) {
fill_buffer(self);
ptr = next_line(self);
}

return ptr;
}

图片.png
图片.png
[2] 如果当前行没有冒号就和上一行合并
这个就好玩了 直接一个字符一行 秒杀低端waf x)
图片.png
[3] multipart前后可以填充垃圾数据

filename的特殊处理

为了兼容老旧的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. */
s = _basename(internal_encoding, filename);
if (!s) {
s = filename;
}

PHPAPI zend_string *php_basename(const char *s, size_t len, char *suffix, size_t sufflen)
{
char *c;
const char *comp, *cend;
size_t inc_len, cnt;
int state;
zend_string *ret;

comp = cend = c = (char*)s;
cnt = len;
state = 0;
while (cnt > 0) {
inc_len = (*c == '\0' ? 1 : php_mblen(c, cnt));

switch (inc_len) {
case -2:
case -1:
inc_len = 1;
php_mb_reset();
break;
case 0:
goto quit_loop;
case 1:
#if defined(PHP_WIN32)
if (*c == '/' || *c == '\\') {
#else
if (*c == '/') {
#endif
if (state == 1) {
state = 0;
cend = c;
}
#if defined(PHP_WIN32)
/* Catch relative paths in c:file.txt style. They're not to confuse
with the NTFS streams. This part ensures also, that no drive
letter traversing happens. */
} else if ((*c == ':' && (c - comp == 1))) {
if (state == 0) {
comp = c;
state = 1;
} else {
cend = c;
state = 0;
}
#endif
} else {
if (state == 0) {
comp = c;
state = 1;
}
}
break;
default:
if (state == 0) {
comp = c;
state = 1;
}
break;
}
c += inc_len;
cnt -= inc_len;
}

quit_loop:
if (state == 1) {
cend = c;
}
if (suffix != NULL && sufflen < (size_t)(cend - comp) &&
memcmp(cend - sufflen, suffix, sufflen) == 0) {
cend -= sufflen;
}

len = cend - comp;

ret = zend_string_init(comp, len, 0);
return ret;
}

本文作者:白帽酱
版权声明:本文首发于白帽酱的博客,转载请注明出处!