前言
给你下面一段代码,你能否看出该代码中出现的问题?
function checkSignature($signature)
{
try {
$decoded = base64_decode($signature, true);
if ($decoded === false) {
throw new Exception("Invalid base64 encoding");
}
global $Secret_key;
return $decoded === $Secret_key;
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
}
function verifySignature($headers)
{
if (!isset($headers['X-Signature'])) {
return false;
}
$validSignature = $headers['X-Signature'];
if (checkSignature($validSignature) === false) {
return false;
}
return true;
}
if (!verifySignature(getallheaders())) {
http_response_code(403);
?>
如果不能的话,我认为你很有必要接着往下看😂
起因是前几天在微信上看到了微步的一个推送:漏洞通告 | Gitblit 身份认证绕过漏洞,其中这一段对于漏洞的描述勾起了我许多的回忆:

触发Gitblit签名验证失败导致验证流程回退至基于密码的身份认证流程并提前返回 true,从而绕过身份认证
在我的印象中,这类由于触发失败或者报错导致在实际项目中的绕过有很多很多,和我们熟知的编程语言特性导致的安全漏洞不同的是,这类问题是完全由开发者在程序逻辑的设计不完备导致的。于是我好奇的问了问gemini这类问题叫什么名字,它告诉我叫做fail-open问题,于是本文我们就用fail-open来称呼这类逻辑漏洞了XD

不过在网上我其实没怎么搜到对于这类安全问题的严格定义或者研究啥的,只是找到了一个类似科普的文章提了提对于fail-open的定义:故障打开(Fail Open)

大概的意思就是这是一种架构层的设计,为了保证某些更加重要的功能在危机关头能正常使用,于是保持系统在故障的情况下不选择关闭而是让其正常运行。当然,我认为这与本文要讨论的内容是不太一样的,因为我们这里讨论的 fail-open 问题大部分是由于开发者安全意识不到位导致程序逻辑设计失误,而不是因为他们正是出于这类特殊的目标而专门设计的。
那么再回到最开始我提到的代码,想必你已经对于如何绕过这个鉴权有了一点思路了,事实上这就是一个典型的 fail-open 问题,由于触发程序故障导致的程序逻辑绕过。这里的 checkSignature 函数以及 verifySignature 函数的验证配合存在一个严重的逻辑缺陷,checkSignature 只是在能正常解码的时候把签名和 $Secret_Key 进行比较,返回真或者假,而 verifySignature 只有在 checkSignature 返回假的时候才退出,否则默认返回真,那么这里我们其实只需要构造一个错误的base64编码,比如@@@,让 checkSignature 解码错误,那么该函数就会抛出错误,不会返回任何东西,自然也不会返回假,verifySignature 也能正常通过:

上面的代码来自于我之前出过的一个ctf题目:MOCSCTF2025 ez-write&&ez-injection,你可能认为世界上不会真的有人这么写代码,但实际上这是某知名互联网公司线上的真实代码逻辑,虽然我已经不太记得代码的细节了,但大致的逻辑就是如上,只需要构造一个会导致程序报错的编码,就可以神奇的绕过权限检测。
真实世界中的fail-open问题
fail-open问题的定义
这里我们来举两个真实项目中的 fail-open 漏洞,一个来自于java,另一个来自于php,可以发现这是一个与语言无关的纯设计层面的问题。由于这个漏洞名字似乎都是我取的,所以我对这类问题做一个粗略的定义:
fail-open 问题,是指当程序出现故障(如鉴权失败、抛出报错等)时,流程并没有终止,而是继续运行,导致漏洞产生的现象。
dataease jwt 鉴权绕过
这个漏洞来自于Le1a师傅:https://github.com/dataease/dataease/security/advisories/GHSA-xx2m-gmwg-mf3r
最早在2024年的时候其实就有师傅提到过DataEase存在伪造JWT令牌漏洞:https://github.com/dataease/dataease/security/advisories/GHSA-45v9-gfcv-xcq6,Le1a师傅的这个漏洞其实是该补丁的一个bypass,当时就有师傅提到了,由于 JWT 密钥是硬编码在代码中的,UID 和 OID 也是硬编码的,所以攻击者可以直接伪造一个 JWT 并接管服务器,当时开发者采用的修复commit为:https://github.com/dataease/dataease/commit/e755248d59543bcd668ace495f293ff735fa82e9
在旧代码里用户 ID 和组织 ID 被写死为 1,JWT 的签名密钥就是一个硬编码的 MD5 字符串,所以可以被攻击者轻松伪造,而在新代码里 JWT 签名密钥不再写死,而是从管理员实际输入的密码动态生成,并且 JWT 校验时会校验 uid 和 oid claim,必须和 TokenUserBO 匹配,Token 必须基于当前密码的 secret 签发,Token 内的 uid 和 oid 必须与用户实际信息一致,因此至少从开发者的视角里修复了 JWT 的伪造问题。
那么Le1a师傅是怎么绕过的呢?从报告里可以看出来,当 secret 验证失败并且设置了状态码和响应后,流程并没有终止,而是继续进入 filterChain.doFilter(servletRequest, servletResponse),所以 secret验证完全就没有成功生效,导致攻击者可以继续使用任意secret来签发符合格式的 JWT,以此绕过鉴权。

可以看出来这就是一个 fail-open 问题,当程序出现故障(鉴权失败)时,流程并没有终止,而是继续运行,导致了漏洞的产生。
禅道鉴权绕过
这篇文章我是看的huamang哥哥的博客:Zentaopms前台权限绕过+后台远程命令执行漏洞分析
当用户未授权访问网站时,检测到无授权会进入deny,于是会进入end,抛出异常:

但是异常处理这里,程序并没有退出,而是echo了一句报错(是不是和我在文章开头举例的代码十分的相似),导致流程继续执行,攻击者可以进一步未授权的利用,导致rce。显然,这也是一个典型的 fail-open 问题,当程序出现故障时,流程并未终止而是继续执行,导致了漏洞的产生。

如何自动化的检测fail-open问题
作为一个负责任的hacker,在提出问题之后自然应当顺便提出如何解决问题,首先最自然的思路应该是用fuzz,不断的输入一些奇奇怪怪的字符触发报错,看看能不能绕过程序的权限校验,但一个致命的缺陷是,一个项目里可能的接口太多了,可能导致报错的执行路径也有很多,但可能只有在某个特定的点触发程序故障才可能导致fail-open问题,能不能fuzz到就看天了。而作为一个之前搞过一段时间静态分析的人,直觉告诉我这类问题其实完全能从静态分析的角度解决,毕竟这就是一个纯粹的控制流的问题,如果程序的控制流在执行的过程中遇到故障没有终止而是流向了sink点,那么就说明了系统中可能存在 fail-open 问题。
和我大哥yulate交流了之后,他狠狠的拷打了我,说逻辑上的控制流检测这类漏洞自然没啥问题,关键是去哪儿找source和sink点呢?首先我感觉sink点这一块可能就得对程序进行建模了,比如对于spring类的项目,鉴权的放行点可能就是 dofilter,当然可能有些程序会是 return true 之类的,需要具体问题具体分析。source点我认为不应该选取一般的常见source点,比如那些读取用户输入的函数,而是选择可能出现”程序故障”的函数,因为正常的鉴权通过的执行流程自然是不会经过”故障”点的,既然是 fail-open 问题,那么我们自然要从可能导致出现”程序故障”的角度进行搜索,分析在出现故障时控制流能不能流向sink点。
个人感觉可以采取从sink到source的角度进行逆向搜索,从sink一个一个流向可能出现”故障”的source,判断用户能否到达这个source点,如果能自然就说明系统中存在问题,等后面有空了我可能会写个小demo,敬请期待XD
orz