2024KalmarCTF-WEB复盘
2024KalmarCTF-WEB复盘
Wells前言:
靶场链接:https://kalmarc.tf/challenges
难度太大了根本写不出来
参考大佬的题解最后复盘出来的
复盘题解
Ez ⛳ v2
题目描述
Caddy webserver is AWESOME, using a neat and compact syntax you can do a lot of powerful things, e.g. wanna know if your browser supports HTTP3? Or TLS1.3? etc
Caddy web服务器非常棒,使用简洁紧凑的语法可以做很多强大的事情,例如,想知道你的浏览器是否支持HTTP3?或者TLS1.3?等等。
Flag is located at GET /$(head -c 18 /dev/urandom | base64) go fetch it.
Flag 位于GET /$(head -c 18 /dev/urandom | base64),去获取它吧。
附件:点击下载附件
解题思路
下载完打开附件包发现只有四个文件,Caddyfile
、docker-compose.yml
这两个中有比较有用的信息
docker-compose.yml
中的关键信息
1 | caddy: |
Caddyfile
中为对Caddy的配置文件
1 | (sec_headers) { |
对于没用接触过的Caddy的我只能去官方文档看看其中的配置文件所代表的含义 查了下发现还有中文文档哈哈哈哈哈哈哈哈哈哈
链接:Caddy v2中文文档 (dengxiaolong.com)
不懂的就半查半猜,问问狗屁通,然后有个初步的理解:
Caddy中存在一个概念:片段
1 | (指令名){ |
例如:
1 | (html_reply) { |
片段类似于我们熟悉的概念:函数,可以在被调用
在任何你需要的地方重复使用它:
1 | import 指令名 |
块
块由一对花括号完成的:
1 | ... { |
花括号前写所服务的域名,花括号后填写对应的指令
此外在片段中有一个特别的指令:templates
-模板,类似于python中的Jinja2模板引擎,进行实时渲染
对应的官方文档:Module http.handlers.templates - Caddy Documentation (caddyserver.com)
根据templates
对应的语法规则({{指令}}
)尝试能不能和Python一样进行模板注入
由于修改UA(User-Agent
)比较方便,所以我选择了ua.caddy.chal-kalmarc.tf
,将UA修改为{{7*7}}
发现服务端返回了500
错误,发现这个思路应该是有戏,修改为{{.Host}}
发现最后页面返回了User-Agent: ua.caddy.chal-kalmarc.tf
,发现确实可以进行类似模板注入的操作,而官方文档中有两个可以让我们读到flag
的指令
1.readFile
Reads and returns the contents of another file, as-is. Note that the contents are NOT escaped, so you should only read trusted files.
按原样读取并返回另一个文件的内容。请注意,内容不会被转义,因此您应该只读取受信任的文件。
1 | {{readFile "path/to/file.html"}} |
2.listFiles
Returns a list of the files in the given directory, which is relative to the template context’s file root.
返回给定目录中的文件列表,该列表相对于模板上下文的文件根目录。
1 | {{listFiles "/mydir"}} |
因此我们修改UA为{{listFiles "/"}}
获得flag的文件名: CVGjuzCIVR99QNpJTLtBn9
然后再修改UA为{{readFile "/CVGjuzCIVR99QNpJTLtBn9"}}
,最后成功获取flag:
kalmar{Y0_d4wg_I_h3rd_y0u_l1k3_templates_s0_I_put_4n_template_1n_y0ur_template_s0_y0u_c4n_readFile_wh1le_y0u_executeTemplate}
BadAss Server for Hypertext
强悍的超文本服务器
题目描述
I wrote my own HTTP server. I have to admit: the code is a bit cursed, but it works! So no problem, right?
我自己写了一个HTTP服务器。我必须承认:代码有点诡异,但是它能正常工作!所以没问题,对吗?
前置知识
/proc
目录以及子目录的功能及其作用- shell语言
- 通配符
解题思路
这是一个黑盒测试,点击题目链接后发现就一个按钮可以交互
点击这个按钮,跳转到http://chal-kalmarc.tf:8080/assets/26c3f25922f71af3372ac65a75cd3b11/iceberg.jpg
,没用任何信息。查看初始页面的源码发现被隐藏了另一个按钮
1 | <!-- <a href="assets/f200d055a267ae56160198e0fcb47e5f/try_harder.txt"> |
再次点进去,发现就一句话:Did you think it was this easy? Nah, this isn't the flag.
也没什么实际的价值点(其实后面这两个东东会被利用到),然后就是无头无脑的不断尝试了robots.txt
、抓包再发包
这个过程中发现了一个比较有趣的,响应头中存在这样一个键值:X-Powered-By: Bash
,shell语言做后端??
发现随便输入一个内容到url后如果不存在会输出为:cat: /app/static/1: No such file or directory
,可以大胆推测后端为shell语言了
然后我就开始怀疑是不是有目录创越的漏洞存在了,拦截请求包转到BP进行改包(浏览器的url中直接输入..
会被删除,而并不会直接传会后端)将url修改为/../../../../../etc/passwd
,返回包中出现了passwd
中对应的内容
逻辑漏洞1:
发现目录穿越漏洞确实存在,可以尝试直接读一下/flag
发现根目录下不存在flag
文件,那就只能进/proc
目录找找对应的进程和运行目录了,一般读取1
进程(一般赛题docker中最初的线程号为1)和self
进程(当前指令所属的进程),一通乱尝试cmdline
、environ
、status
等等发现了一些有用的东西
在/../../../../../proc/1/cmdline
尝试读取后端的运行程序
1 | HTTP/1.0 200 OK |
此时我们可以找到我们的后端所属的脚本文件,我们再将它读取出来,尝试看看源码中是否存在漏洞。读取到的源码为:
1 |
|
由于是shell语言这里我想到了一些shell中的一些骚操作例如:
1 | ${变量名} #会将变量的值进行拓展出来 |
拓展:shell语言中的部分特性
特性1:展开
按照展开顺序分为:
- 花括号展开(Brace Expansion):
花括号展开可以用来生成一系列具有相似结构的字符串。例如,使用花括号展开可以生成一组文件名或者一组命令参数。
示例:
1 | $ echo {a,b,c} |
- 波浪线展开(Tilde Expansion):
波浪线展开用于扩展波浪线后面的特殊字符,通常用于表示用户的主目录路径。
示例:
1 | $ echo ~ |
- 参数,变量,算术展开和命令替换:
参数展开用于访问脚本或函数的参数,变量展开用于展开变量的值,算术展开用于进行数学运算,而命令替换用于将命令的输出作为展开结果。
示例:
1 | $ echo $HOME |
- 单词分割(Word Splitting):
单词分割用于将字符串按照特定的分隔符进行拆分,常见的分隔符包括空格、制表符和换行符。
示例:
1 | $ string="Hello World" |
- 文件名展开(Filename Expansion):
文件名展开用于匹配文件系统中的文件名模式,常见的通配符包括星号(*)和问号(?)。
示例:
1 | $ ls *.txt |
特性2:单双引号的区别
单引号:
使用单引号,单引号中的内容一律被视为字符串,不进行转义,无法被扩展,
${}
、$()
、反引号、通配符
无法被使用例如:
1
2
3
4
5
6
7
8
9
10root@ubuntu:/home/ubuntu/Desktop# echo /*
/app /bin /boot /cdrom /dev /etc /home /lib /lib32 /lib64 /libx32 /lost+found /media /mnt /opt /proc /root /run /sbin /snap /srv /swapfile /sys /tmp /usr /var
#通配符不会展开
root@ubuntu:/home/ubuntu/Desktop# echo '/*'
/*
root@ubuntu:/home/ubuntu/Desktop# echo '$(date)'
$(date)双引号:
- 变量展开:双引号内的变量会被展开,即变量的值会替换变量本身。
- 命令替换:双引号内的命令替换(使用
$()
或反引号``)会被执行,其输出会替换命令本身。 - 通配符不会展开:双引号内的通配符(如
*
、?
等)不会被作为通配符处理,而是作为普通字符。 - 转义字符:某些特殊字符(如
$
、反引号
、\
)可以通过反斜线进行转义以表示其字面意义。
例如:
1
2
3
4
5
6
7
8
9
10
11
12#变量展开
root@ubuntu:/home/ubuntu/Desktop# path="*"
root@ubuntu:/home/ubuntu/Desktop# echo "${path}"
*
#命令替换
root@ubuntu:/home/ubuntu/Desktop# echo "$(date)"
2024年 03月 24日 星期日 16:55:37 CST
#通配符不会展开
root@ubuntu:/home/ubuntu/Desktop# echo "*"
*
特性3:变量展开后的字符串
1.进行变量展开后,通配符不受 展开前单双引号性质的影响
只要最后使用变量时,最外层不存在
""
即可展开
例如
1 | test1 |
2.进行变量展开后,变量展开、命令替换受最开始赋值时的单双引号影响
若最开始时使用单引号,无论最后使用变量时,最外层存不存在引号,都无法被展开
例如:
1 | test1 |
但经过尝试,发现无法通过控制$path
这个值变为${}
使最后的cat返回命令替换的结果
预想结果(可以通过cat的报错获取信息,如果这种办法可行剩下的只是空格的绕过):
1 | root@ubuntu:/home/ubuntu/Desktop# url=$(ls) |
实际结果
拓展:shell语言中的部分特性
特性4:read命令处理字符串
使用read命令来读取输入的字符串时,所读取的字符串相当于被单引号包围,具有单引号包裹的字符串的特点
- 变量不展开:单引号内的内容都会被当作普通字符串处理,包括变量,它们不会被展开。
- 命令不替换:单引号内的命令不会被执行,即使使用
$()
或反引号``。 - 通配符不展开:单引号内的通配符同样不会被展开,被视为普通文本。
- 转义字符不工作:单引号内几乎所有的字符都被视为普通字符,包括反斜线(
\
),它不具有转义功能。
例如:
1 | test1 |
此时发现我们可控点只剩下$protocol
变量
1 | protocol=$(echo -n "${request_line[2]}" | sed 's/^\s*//g' | sed 's/\s*$//g') |
逻辑漏洞2:
可控原因:$protocol
变量由于使用echo
命令,是得read读取后的特性消失,使得$protocol
可被拓展
当我们尝试修改HTTP/1.1
为/*
时,发现并没有出现Invalid protocol
拓展:shell语言中的部分特性
特性4:[]的返回值
在 shell 中,[]
符号通常用于条件测试。如 if [ CONDITION ]; then ...
这样的语句。在这种用法中,[]
是 [
命令的简写,这实际上是一个指向 test
命令的链接。因此,当你使用 []
时,实际上是在调用 test
命令来评估一个条件。
test
命令(或其等价的 [
形式)的返回值遵循 shell 命令的通用返回值约定:
- 0:表示测试的条件为真(true)。
- 1:表示测试的条件为假(false)。。
- >1:如果出现错误,如语法错误或使用了无效的选项,
test
命令可能会返回大于 1 的值。比较特别的是,此时即使设置了set -e
也不会退出程序
逻辑漏洞3:
将HTTP/1.1
替换为/*
时,由于通配符展开,最后会出现进行匹配错误,[]
返回大于1的结果,&&
的两侧结果都是大于0,返回1,因此认为是假,此时这个if
条件被判定为假,可以继续执行后面内容的代码
但如果/*
匹配到的结果只有一个时,并不会造成匹配错误,而直接返回0,&&
的任意一侧结果为0会直接返回0,此时这个if
条件被判定为真
启动set -x
时的详细信息如下
1 | + '[' badass_server.sh bash.md character1.sh character2.sh file '!=' HTTP/1.0 ']' |
注意:此时通配符展开并不能认为这个变量是数组,数组直接使用会利用数组第0个进行匹配,而通配符展开会报错
因此我们可以通过这个点,使用glob
通配符[]
写入一个已知的字符,不断修改字符,使其最后能匹配到的结果最后有1个以上,页面不出现:Invalid protocol
,进行对目录或者文件名的一步一步匹配(用这种方法可以确定名称比已知目录少的目录名)
最初的页面给出了两个目录,需要利用这两个目录,选择一个目录作为已知字符
1 | <a href="assets/26c3f25922f71af3372ac65a75cd3b11/iceberg.jpg"> |
以26c3f25922f71af3372ac65a75cd3b11
作为已知字符为例,所以最后的playload的过程为:
1 | GET /../../../../../../etc/passwd /app/static/assets/[29]* |
最后找到隐藏的目录, 9df5256fe48859c91122cb92964dbd66
估计最后是猜的吧,flag最后的位置是:/app/static/assets/9df5256fe48859c91122cb92964dbd66/flag.txt
修改url为../../../../../app/static/assets/9df5256fe48859c91122cb92964dbd66/flag.txt
读出kalmar{17b29adf_bash_web_server_was_a_mistake_374add33}
Is It Down
它是否宕机
题目描述
In an increasingly online world it is nice to know, if you are the only one being offline or if everybody else are having offline too.
在一个日益在线化的世界中,了解自己是唯一离线还是其他人也处于离线状态是很重要的。
We present to you: Is it down!
我们向您介绍:它是否宕机!
Rumour has it, that a flag is stored somewhere on this server.
有传言称,一个标志位被存储在这台服务器的某个地方。
前置知识
/proc
目录以及子目录的功能及其作用- Python语言及特性
__pycache__
文件夹
解题思路
同样的这也是一个黑盒测试,看了看页面发现并没有什么提示,可以根据自己的输入的网址进行判断,网址是否在线或者离线
直接抓包然后repeat看看
会发现页面出现了我们输入网页的源代码,相关的漏洞应该就是SSRF
尝试直接使用file:///etc/passwd
试试看,发现页面回显出了
1 | {"error":"Url must start with 'https://'. We do not want anything insecure here!","success":false} |
发现只能使用https://
开头的链接(是我应该就在这卡住了)
根据大佬的思路,我们可以尝试一个发生重定向的网址, 无语啦 。。。。。。。。好麻烦,还得自己起个https服务
根据实际的测试发现:
- 如果输入的链接网页存在任何内容,不会跟随着网页重定向
- 如果输入的链接网页不存在任何内容,此时会跟随着网页重定向
例如:
我们输入网址的响应包为:
1 | 301 Moved Permanently |
此时,不跟随重定向,返回的内容为
1 | {"content":"<html>\\r\\n<head><title>301 Moved Permanently</title></head>\\r\\n<body bgcolor=\"white\">\\r\\n<center><h1>301 Moved Permanently</h1></center>\\r\\n<hr><center>openresty</center>\\r\\n</body>\\r\\n</html>\\r\\n","online":true,"success":true} |
我们自己起一个后端服务,为了随时更改重定向内容,我们可以设置代码为以下
1 |
|
此时我们使用这个服务,并输入参数?wells=file:///etc/passwd
,发现页面返回出来了passwd
的文件内容
1 | {"content":"root:x:0:0:root:/root:/bin/ash\\n.........","online":true,"success":true} |
此时再次利用/proc
目录进行尝试找到后端服务的文件夹位置以及所对应的运行程序
在/proc/1/cmdline
尝试读取后端的运行程序的相关信息uwsgi\\x00--ini\\x00/etc/uwsgi/uwsgi-custom.ini\\x00
,可以猜到此时的后端应该是flask
,然后再顺藤摸瓜进入到/etc/uwsgi/uwsgi-custom.ini
读取配置信息:(美化后)
1 | [uwsgi] |
比较重要的信息是:wsgi-file = /var/www/keep-dreaming-sonny-boy/app.py
,然后继续顺藤摸瓜找到对应的后端文件读出出来,因为经过了json的转化,变得无敌难看,让狗屁通美化一下最后得到
1 | from flask import Flask, request, send_from_directory, session, abort |
但是这里面requestlib
库文件是我们安装不了的,尝试直接读取一下requestlib.py
查看是不是自己写的库,同样的也是读到了这个文件,经过美化后如下:
1 | from urllib.request import urlopen, HTTPErrorProcessor, build_opener, Request |
漏洞成因
在定义的fetch
函数中follow_redirects
值默认为True
,并且跟随响应包中的Location
键值进行判断是否存在重定向,重定向的目标是何。但由于只限制了第一次输入的网址为https://
,并未进行对重定向的协议进行检查从而造成了SSRF
如何处理重定向
注意:
如浏览器,powershell中的curl再处理重定向时,无法处理从https://
重定向至file://
powershell中的curl发生报错
浏览器会提示页面错误
做到这里,关键其实是为造出一个session,其键值存在admin: True
,但存有session_encryption_key
的config.py
再被调用后就被删除了,需要找到如何复原的办法
1 | @app.route('/flag', methods=['GET']) |
__pycache__
文件夹
在Python工作目录下,如果执行某文件后经常会自动生成一个
__pycache__
文件夹。__pycache__
文件夹正是缓存*.pyc
地方。*.pyc
文件的命名格式是<module>.<interpreter_version>.pyc
。注意,对于被导入(import
)的module才会生成对应的*.pyc
文件详细文档可参考:
【Python】__pycache__文件夹是什么东西? - 知乎 (zhihu.com)
pyhton中__pycache__文件夹的产生与作用_pycache文件夹下的东西是如何产生的-CSDN博客
例子:
以被导入的为
config.py
文件为例,最后生成在__pycache__
文件夹config.cpython-35.pyc
,最开始的config
为被导入的文件名,cpython
代表的是c语言实现的Python解释器,-35
代表的是版本为3.5版。
因此,我们可以在__pycache__
文件夹中找到config的.pyc
文件,再进行逆向
最后测试到路径为:/var/www/keep-dreaming-sonny-boy/__pycache__/config.cpython-311.pyc
,里面的内容为:
1 | \\xa7\\r\\r\\n\\x00\\x00\\x00\\x00\\x86\\x84\\xf7e;\\x00\\x00\\x00\\xe3\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf3\\n\\x00\\x00\\x00\\x97\\x00d\\x00Z\\x00d\\x01S\\x00)\\x02\\xda Rm7GbQJ4uDikyiis6miD7YwsN11rEjfLN)\\x01\\xda\\x16session_encryption_key\\xa9\\x00\\xf3\\x00\\x00\\x00\\x00\\xfa*/var/www/keep-dreaming-sonny-boy/config.py\\xfa\\x08<module>r\\x07\\x00\\x00\\x00\\x01\\x00\\x00\\x00s\\x11\\x00\\x00\\x00\\xf0\\x03\\x01\\x01\\x01\\xd8\\x19;\\xd0\\x00\\x16\\xd0\\x00\\x16\\xd0\\x00\\x16r\\x05\\x00\\x00\\x00 |
由于转化为json后转义字符\
会变成\\
,叫狗屁通写一个小小的脚本把以上数据恢复成二进制数据
1 | data_str = """ |
.pyc
文件的逆向在网上就很多在线的工具可以进行使用,逆向后得到
1 | #!/usr/bin/env python |
再使用session伪造工具flask-session-cookie-manager
为造出一个session出来,指令为:
1 | python flask_session_cookie_manager3.py encode -s 'Rm7GbQJ4uDikyiis6miD7YwsN11rEjfL' -t "{'admin':True}" |
最后得到伪造的session所对应的cookie为:eyJhZG1pbiI6dHJ1ZX0.ZgBMeg.t2OWSLBvEeZpMPUZNBFFcmsFS-o
,手动填入浏览器中,并访问/flag
最后得到flag:kalmar{Rem3Mbr_T0_fl0sh!}
查看Wells的丢人过程
我想着不是伪造出session最后的结果时运行/readflag
这个可执行文件,便尝试读取了一下,转化为二进制,再发给逆向的同学看看能不能找出flag位于哪里命名为什么直接读取出来
也确实逆向出来,找到了flag的位置以及名称
但出题人也估计想到了这一点,位于flag.txt
设置了权限,没法被直接读取出来。。。。。。。
跪谢逆向的同学:L0SJ0K