写在前面
个人赛分三段,阶段性放题,只有一道web,但是挺有意思,记录一下,附件我放文末了
CloudPulse
附件就两个,前端是python,后端是go

通信渠道是这个函数:
def forward_to_backend(payload: dict):try:resp = requests.post(BACKEND_URL,json=payload,headers={"Content-Type": "application/json"},timeout=10)return jsonify({"status": "success", "data": resp.text})except requests.RequestException as e:return jsonify({"status": "error", "message": "Backend service unavailable"}), 503
从env中拿到BACKEND_URL和后端通信
了解完大致的结构后,开始找sink点:

找完了也只发现了这一个sink点,虽然限定了curl方法,但是也可以用来文件读取,这里想到的是file://读取flag
往回推,看看能不能实现:

参数是可添加的,基本坐实了sink点,接着往回看:
target由此处传入,看看waf:

validateTarget:

要求以http://或https://开头,这里其实对我们file伪协议没什么影响,因为curl是可以接收多个url的,接着看:
MatchString:

对target和customHeaders设了黑名单,这里过滤了file://,看似走不通了,其实不然,file:/flag也是可以的:
scheme:file 加绝对路径 /flag,curl 接受这种写法,而且它允许冒号后面出现 1 到 3 个斜杠
接着走,看看哪里调用了performFetch:

这里的aa函数是啥呢,看看定义:

分别对应ops、httpcheck、fetch,先看switch怎么写的:

原来是对传入的Ops字段的值进行匹配,那这里我们就知道要使得req.Ops的值为fetch了,这个函数的其他部分对我们的payload暂时没什么作用,先不看,找找这个函数在哪里被调用:

找到了main函数,go文件基本就分析到头了,现在我们知道,要获得flag,需要满足如下条件:
1.req.Ops为fetch
2.req.Target要以http://或https://开头,且包含file:/flag("http://www.amoda.com file:/flag"这样就行)
下一步就是分析pyhon文件了:
回到我们开始讲的通信函数:

这里的BACKEND_URL在第8行有定义:
BACKEND_URL = os.environ.get("BACKEND_URL", "http://backend:8080/api/monitor")
直接将json数据发给/api/monitor路由,看看函数调用:

我们发给后端的数据是这个normalized,倒推原始内容,post发给这个路由就行了:
从头开始看:
首先函数只接收json格式,然后转换成数组给到_sanitize_payload函数:
_sanitize_payload:

遍历键值对,要求键不得有特殊符号:

然后又把key交给_normalize_key(确实很绕。。):

其实就是k.lower
然后回到90行,接着调用一个函数:

没什么好说的,限制开头和黑名单
然后就到了重点:


翻译成代码就是:
normalized["Ops"] = "httpcheck"
normalized["target"] = target
这下真难办了,既要Ops等于httpcheck,又要等于fetch,这也是这题最大的考点:
json.Unmarshal的折叠匹配算法
解释如下:
根据 Go 官方文档和源码逻辑,json.Unmarshal 在将 JSON 数据填充到结构体时,遵循以下匹配优先级:
- 精确匹配:JSON 的 key 必须与结构体字段的
json标签或字段名完全一致(区分大小写)。 - 大小写折叠匹配:如果精确匹配失败,Go 会尝试忽略大小写进行匹配。
所以我们就是要通过折叠算法覆盖掉原本的Ops达到进入判断的目的
历史上存在过很多 s 的变体,比如:
ſ (U+017F):拉丁小写字母长 S。在古英语或德语(如 ſt 写作 ſt)中常见。
K (U+212A):开尔文符号。
我们这里用ſ
ſ (U+017F):在 Unicode 的“大小写折叠”规则中,它被定义为 s 的等价字符。
我们回到go文件,找到这个函数:

json.Unmarshal 是 Go 语言标准库 encoding/json 中用于反序列化(解码)的核心函数
当他发现精确匹配无法匹配Ops时,就会调用折叠匹配,把我们构造的Opſ 当作Ops,覆盖原来的httpcheck
(其实我感觉很像python中的全角字符绕过)
那我们就可以构造payload如下:
{
"ops":"amoda",
"op\u017f":"fetch",
"target":"http://amoda.com file:/flag"
}
再套个requests发给/api/probe路由即可:
import requests
url ="xxxx/api/probe"
payload={
"ops":"amoda",
"op\u017f":"fetch",
"target":"http://amoda.com file:/flag"
}re = requests.post(url,json=payload)
print(re.text)
通过网盘分享的文件:e76edf210e6e4b86827c99bccade6518.zip
链接: https://pan.baidu.com/s/13YDsxGGORGoZBKzLguI5aQ 提取码: 8c49
--来自百度网盘超级会员v3的分享
完结撒花
