wxvl/doc/2024-11/【翻译】0day 代码审计 CyberPanel v2.3.6 RCE.md

8.3 KiB
Raw Permalink Blame History

【翻译】0day 代码审计 CyberPanel v2.3.6 RCE

DreyAnd 安全视安 2024-11-01 21:29

声明
:该公众号分享的安全工具和项目均来源于网络,仅供安全研究与学习之用,如用于其他用途,由使用者承担全部法律及连带责任,与工具作者和本公众号无关。

本文为翻译文章,需要阅读原文请访问https://dreyand.rs/code/review/2024/10/27/what-are-my-options-cyberpanel-v236-pre-auth-rce

引言

2024年10月27日

 

CodeReview

几个月前,我被分配进行一次针对运行 CyberPanel 的目标的渗透测试。它似乎是由一些 VPS 提供商默认安装的,且还得到了 Freshworks 的赞助。

我最初对如何攻陷目标感到无从下手,因为功能有限,于是我决定另辟蹊径,寻找一个 0day 漏洞 ¯
_
(ツ)_/¯。

这导致我在最新版本(截至目前为 2.3.6)中发现了一个 0-click 预认证根 RCE 漏洞。它目前仍然“未修补”,因为维护者已被通知,补丁已经完成,但仍在等待 CVE 及修复进入主发布版。截止到 10 月 30 日,两个 CVE 已被分配:

  • CVE-2024-51567

  • CVE-2024-51568

同时,维护者发布了安全公告。

你可以在此链接找到补丁提交:
补丁提交

我还对漏洞奖励项目进行了大规模扫描,发现了一些受影响的主机——感谢 iustin 的帮助!

我觉得这篇文章还记录了我在审计各种项目时的思维过程,因此如果你是一个有创造力的初学者,想要开始代码审查,我强烈建议你阅读这篇博客。

代码库结构

这实际上是一个相当简单的 Django Web 应用。它的实际目的是在 VPS 上设置各种系统服务(例如 FTP、SSH、SMTP、IMAP 等)。

当我们进入主页时,我们只看到一个登录功能,所以似乎没有太多可以操作的内容 :/

但这只是冰山一角。

像任何 Django 项目一样,在查看实际项目之前,我们应该先了解框架的工作方式,模式如下:

  • X/urls.py -> 此文件将包含功能 X 的所有 API 路由。

  • X/views.py -> 此文件将包含映射到功能 X 的路由的所有控制器。

  • X/views -> 动态生成页面 HTML 的模板。

  • X/static -> 静态文件和其他东西…

因为这些通常包含身份验证等逻辑,这是我自然开始检查的第一件事。我立刻注意到他们对每个路由逐一进行身份验证检查。

我首先想知道——为什么?你会期望有人使用身份验证中间件或其他方法,而不必自己在每个路由上写身份验证检查。

接下来我想到的是:“如果我是写这样的代码,我肯定会遗漏几个路由的身份验证”——而且,没错,这正是这里发生的事 :)

N-day 漏洞分析

通常当我想更深入了解目标时,我会阅读之前漏洞的写作/利用/文档,这对我了解目标非常有帮助。

我注意到在 2.3.5 版本中发布了以下安全更新:
CyberPanel v2-3-5

文件管理器的上传功能中的身份验证绕过:文件管理器上传功能中的一个漏洞,由于人为错误被修正。

“由于人为错误”……这让我想到了。

这让我有了分析这个补丁以深入了解代码库的想法。

让我们看看补丁之前的提交在 filemanager/views.py 中:
文件管理器视图

仔细看看,我们需要绕过的两个检查是:

  • userID = request.session['userID']

  • admin = Administrator.objects.get(pk=userID)

第一个检查从 Django 的内部会话对象获取用户 ID第二个则是调用 Django 的 ORM 来获取我们是否是管理员的信息。

令我惊讶的是,这两个操作实际上都会抛出异常,因为第一个操作试图访问一个不存在的键,而第二个的
get()
则是 Django ORM 的默认行为:

如果没有匹配查询的结果,
get()
将引发
DoesNotExist
异常。

好吧,我们需要一个未认证的漏洞,所以这两者都将失败,但代码中明显存在逻辑问题,因为
fm.upload()

try/except
之外,仍然会工作,哈哈。找到这个漏洞的人真是有眼光!

让我们看看
upload()
方法:

哦,看来我们现在使用子进程读取文件!这对我们来说是个好消息,我想,这个信息值得记下!

你可以猜到这个漏洞的本质:通过
completePath
进行简单的命令注入,通过
ProcessUtilities.outputExecutioner()
函数。

注意:由于我们的 ORM 检查将失败,因此我认为不能通过
domainName
来利用这个漏洞。

我还做了一个快速的 PoC我不知道这是否是漏洞的新变体因为在任何地方都没有提到 RCE但我想这个补丁也是修复了它

无论如何,让我们总结一下我们到目前为止获得的知识:

  • 身份验证检查是通过
    request.session['userID']
    和 Django 的 ORM 逐路由进行的。

  • 他们喜欢将内容通过子进程传输。

  • 他们喜欢搞乱事物的顺序。

  • 他们喜欢忘记事情。

FYI许多端点仅通过传递
userID=1
进行交互,允许您未认证地使用它们。希望这能给大家一个提示,如果他们想找更多漏洞 :)

找到 0day

在这一点上,我使用 Semgrep 搜索潜在的有趣代码,立刻发现了一段代码:

这段代码显然没有任何身份验证检查,且最近刚被添加。有人肯定是疏忽了,否则不会如此简单。

让我们尝试触发一个 PoC 来验证这一点。

绕过 secMiddleware

代码较长,因此我附上了简化版:

在这一点上,我开始对各种字符和技巧进行模糊测试,以尝试绕过此端点的检查,但没有成功。

所以,我开始逻辑性地思考,发现了一个有趣的绕过方法,这需要一点创造力,而不需要对复杂的 Linux 技巧有深入了解。

注意到middleware 仅在 POST 请求方法时进行命令注入检查,而我们看到的
upgrademysqlstatus()
路由是通过
json.loads(request.body)
加载 POST 数据的。

根据 Django 文档,
body
属性的原始 HTTP 请求体会以字节串的形式返回。这对于处理与常规 HTML 表单不同的数据很有用。

你注意到了吗?这意味着我们可以以
OPTIONS/PUT/PATCH
等其他
HTTP
方法发送请求,从而完全绕过安全中间件。

于是,我们可以实现:

(这很有意义,因为这个项目用于管理系统上的所有服务。)

漏洞利用

我写了一个简单的交互式利用脚本,你可以使用,尽情享用!

挑战

希望你阅读这篇文章时感到愉快 :)

既然你已经读到了这里,我给你一个挑战,去找出这个代码中的另一个漏洞:

我的朋友发现了这个确切漏洞的另一种变体,你能找到吗?(可以解决)

这更多是如果你想找另一个 0day请检查
restoreStatus
路由:

看起来这里又是一个简单的命令注入案例——问题在于,
os.path.exists
需要返回 True同时路径仍然要包含命令注入的有效负载。我们可能需要一个任意文件创建的 gadget。是的我知道 Python 中的
os.path.join
技巧,不,它在这里没有帮助。)


backupStatus
中似乎也存在类似的情况:

这里唯一的问题是,如果
backupDomain
不存在,我们会遇到 ORM 异常。再次需要一个网站/文件名创建的漏洞。

祝你好运!