前言
前面分享了两篇关于Hyperf框架中RPC 同语言框架跨语言框架的调用,今天来点有意思,具有实战意义的技术分享,hyperf框架和laravel非常相似的PHP框架,按理来说下面的思路可以应用于任何语言框架。
最近有一些薅羊毛的羊毛党盯上了我们公司的支付宝小程序的抽奖产品,主要的逻辑是在前端判断用户是否关注的该小程序,如果关注了就给用户一次抽奖机会,然后请求后端接口直接抽奖发放奖励,运营人员发现奖金消耗过快,然后后台分析,没有几个人关注小程序却有很多人抽奖,分析过后发现是某些别有用心的人抓到了我们的接口进行更换请求参数然后跳过了前端判断是否关注小程序进行直接抽奖请求(这里是直接更换了支付宝用户ID参数达到了直接给支付宝用户直接发放奖励的效果)。
粗略方案
经过前后端讨论,解决的办法就是header头增加几个参数,例如时间戳,随机字符串,然后把这些内容加上请求参数进行混淆生成一个签名header头参数。把这些参数传给后端进行童谣的混淆签名验证并检查时间戳。
以上的方案就可防止一些薅羊毛的羊毛党通过简单的抓包更换参数来实现重放攻击(重放攻击就是拿着一样参数或者修改某些参数再次请求获取资源),减少了损失。
详细方案
参数简要说明
在header中增加3个签名参数:
-
sign 最终签名字符串
-
time-stamp 请求时的时间戳
-
nonce-str 随机字符串
混淆逻辑
前端流程
graph TD A[定义一个数组] --> B[Get请求参数加入数组] B[Post参数数组数组] --> C[Get参数加入数组] C -->D[获取时间戳time_stamp加入数组] D -->E[生成随机字符串nonce_str加入数组] E -->F[假如有鉴权参数TOKEN就加入数组] F -->G[依据数组的键进行ASCII码排序] G -->H[数组转字符串] H -->I[数组base64加密转大写] I -->J[MD5混淆生成sign参数放在请求header头]
用于接口调试,我在postman上实现了完美与前端一样的混淆:Postman Pre-request Script
request_time_stamp =Math.round(new Date() / 1000);// 获取秒级时间戳 token = pm.environment.get("sign-token")//读取环境变量,这里的环境变量应该在登录接口的Tests里面设置 pm.environment.set('sign-time-stamp',request_time_stamp) //读取设置环境变量,共所有接口使用 nonce_str = randomString(32)pm.environment.set('sign-nonce-str',nonce_str)// 设置随机字符串环境变量 var params_args = pm.request.url.query.members;// 获取当前请求所有Get参数及其值 var body_args = request.data; // 获取当前请求所有Post参数及其值 for(var i=0;i<params_args.length;i++){body_args[params_args[i].key] = params_args[i].value;// 合并Get参数Post参数的键和值 } body_args['time_stamp'] = request_time_stamp; body_args['nonce_str'] = nonce_str body_args['token'] = token body_args = objectsort(body_args)//所有参数合并排序 console.log(body_args); body_args_base64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(body_args)).toUpperCase()//base64混淆字母转大写 // console.log(body_args_base64); sign = CryptoJS.MD5(body_args_base64).toString()// MD5混淆 // console.log(sign); sign_type = (request_time_stamp % 5) % 2;//当前时间进行取余操作,判断奇偶数 // console.log(sign_type); if(sign_type==1){//根据时间戳求余奇偶数来进行混淆拼接,得出最后的signnew_sign =sign + token }else{new_sign =token + sign } console.log(new_sign); pm.environment.set('sign-sign',new_sign) //设置sign参数,供全局接口使用 function objectsort(obj){let arr = new Array();let num = 0;for (let i in obj) {arr[num] = i;num++;}const sortArr = arr.sort();//自定义排序字符串let str = "";for (let i in sortArr) {str += sortArr[i] + "=" + obj[sortArr[i]] + "&";}//去除两侧&符号const char = "&";str = str.replace(new RegExp("^\\" + char + "+|\\" + char + "+$", "g"), "");return str; } /* 生成随即字符串 */ function randomString(len) {len = len || 32;const $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz-';const maxPos = $chars.length;let res = '';for (let i = 0; i < len; i++) {res += $chars.charAt(Math.floor(Math.random() * maxPos));}return res; }
Postman接口工具的代码语法跟Javascript一样的,增加一些环境变量读写的操作,直接在postman中设置环境变量:
在请求发送之前会执行上面那段代码,把所有的参数进行计算生成sign然给header头中的环境变量(sign-nonce-str)赋值。
后面找资料发现也可以不设置环境变量控制,使用下面的方法设置添加或更新header头:
pm.request.headers.upsert({ key: 'sign-sign', value: sign-sign }) pm.request.headers.upsert({ key: 'sign-time-stamp', value: request_time_stamp }) pm.request.headers.upsert({ key: 'sign-nonce-str', value: nonce_str })
后端逻辑
先简要梳理一下基本逻辑,再看详细的代码
graph TD A[接收header头中time_stamp,sign,nonce-str] --> B[首先对time_stamp时间戳验证,与服务器时间对比验证] B --> C[时间戳求余分开token和sign] C -->D[接收请求中Post和Get参数和header头验签参数组成数组进行ASCII排序] D -->E[排序完成的数组进行base64加密转大写字符串] E -->F[生成的字符串进行MD5混淆与header头sign对比验签]
后端中间件的代码,相对于前端较为简单,主要是框架做了一些封装,post脚本都是底层逻辑代码app/Middleware/AuthMiddleware.php:
<?php declare(strict_types=1); /** * This file is part of Hyperf. * * @link https://www.hyperf.io * @document https://hyperf.wiki * @contactgroup@hyperf.io * @licensehttps://github.com/hyperf/hyperf/blob/master/LICENSE */ namespace App\Middleware; use App\Exception\BusinessException; use App\Tool\Token; use Hyperf\Utils\context; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class AuthMiddleware implements MiddlewareInterface {public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {if (! $request->hasHeader('sign') || ! $request->hasHeader('time-stamp') || ! $request->hasHeader('nonce-str')) {throw new BusinessException(3002, '登录失败');}$request_sign_str = $request->getHeader('sign')[0];$request_time_stamp = $request->getHeader('time-stamp')[0];$nonce_str = $request->getHeader('nonce-str')[0];$time_stamp = time();$difference = $time_stamp - $request_time_stamp;if ($difference && $difference > 180 || $difference && $difference < -180) {throw new BusinessException(3004, '密钥已过期');}$sign_type = ($request_time_stamp % 5) % 2;if ($sign_type) {$request_sign = substr($request_sign_str, 0, 32);$request_token = substr($request_sign_str, 32);} else {$request_sign = substr($request_sign_str, -32);$request_token = substr($request_sign_str, 0, -32);}$sign_info = ['time_stamp' => $request_time_stamp,'nonce_str' => $nonce_str,'token' => $request_token,];$all_params = array_merge($sign_info, $request->getQueryParams(), $request->getParsedBody());$sort_string = $this->sort_ascii($all_params);$sign_string = strtoupper(base64_encode($sort_string));// base64转后转大写$sign = md5($sign_string);if ($request_sign != $sign) {throw new BusinessException(3005, '签名错误');}$token = new Token();$token_info = $token->get($request_token);if (! $token_info) {throw new BusinessException(3003, '页面已过期,请重新操作');}$querys = $request->getQueryParams();if (isset($querys['openid'])) {if ($querys['openid'] != $token_info['openid']) {throw new BusinessException(3006, '非法请求!');}}$parsed = $request->getParsedBody('openid');if (isset($parsed['openid'])) {if ($parsed['openid'] != $token_info['openid']) {throw new BusinessException(3006, '非法请求!');}}Context::set(ServerRequestInterface::class, $request);return $handler->handle($request);}/*ascii码从小到大排序 * @param array $params * @return bool|string */private function sort_ascii($params = []) {if (! empty($params)) {$p = ksort($params);if ($p) {$str = '';foreach ($params as $k => $val) {$str .= $k . '=' . $val . '&';}return rtrim($str, '&');}}return false;} }
在config/autoload/listeners.php
启用,设置为全局中间件
<?php declare(strict_types=1); /** * This file is part of Hyperf. * * @link https://www.hyperf.io * @document https://hyperf.wiki * @contactgroup@hyperf.io * @licensehttps://github.com/hyperf/hyperf/blob/master/LICENSE */ use App\Middleware\AuthMiddleware; return ['http' => [ AuthMiddleware::class], ];
也可以在控制器类或者某个方法用注解使用单独使用这个中间件:
use App\Middleware\AuthMiddleware; use App\Tool\Token; use Hyperf\Di\Annotation\Inject; .../** * @PostMapping(path="test") * @Middleware(AuthMiddleware::class) */public function test() {return $this->request->input('id');} ...
这样访问接口的时候后端就会验证必须携带这几个header混淆参数,并且需要混淆计算准确。
总结
混淆方法可以根据自己的需求更改,可以加上AES 带秘钥的加解密,数组排序换个排序方式,md5和base64加密顺序互换等等,我这里只是提供一个思路。主要的流程是把前端请求参数和随机字符字符串加上时间戳(随机字符串和时间戳也要放在header头)进行混淆生成一个header混淆参数,后端以同样的方式把请求参数和从header头获取到的验签参数进行计算与前端传入的混淆参数进行对比验签,同时进行时间戳时间范围验证。
这样修改某个请求参数,不修改header头中的验签参数去请求,后端中间件验证一定不会通过。破解之法就是使用关键参数进行在前端代码中Debug,完全模拟生成关键参数吗,这样的人太少了,太难了,这样的混淆应该会过滤95%以上的薅羊毛技术党。
网络安全入门学习路线
其实入门网络安全要学的东西不算多,也就是网络基础+操作系统+中间件+数据库,四个流程下来就差不多了。
1.网络安全法和了解电脑基础
其中包括操作系统Windows基础和Linux基础,标记语言HTML基础和代码JS基础,以及网络基础、数据库基础和虚拟机使用等...
别被这些看上去很多的东西给吓到了,其实都是很简单的基础知识,同学们看完基本上都能掌握。计算机专业的同学都应该接触了解过,这部分可以直接略过。没学过的同学也不要慌,可以去B站搜索相关视频,你搜关键词网络安全工程师会出现很多相关的视频教程,我粗略的看了一下,排名第一的视频就讲的很详细。 当然你也可以看下面这个视频教程仅展示部分截图:
学到http和https抓包后能读懂它在说什么就行。
2.网络基础和编程语言
3.入手Web安全
web是对外开放的,自然成了的重点关照对象,有事没事就来入侵一波,你说不管能行吗! 想学好Web安全,咱首先得先弄清web是怎么搭建的,知道它的构造才能精准打击。所以web前端和web后端的知识多少要了解点,然后再学点python,起码得看懂部分代码吧。
最后网站开发知识多少也要了解点,不过别紧张,只是学习基础知识。
等你用几周的时间学完这些,基本上算是具备了入门合格渗透工程师的资格,记得上述的重点要重点关注哦! 再就是,要正式进入web安全领域,得学会web渗透,OWASP TOP 10等常见Web漏洞原理与利用方式需要掌握,像SQL注入/XSS跨站脚本攻击/Webshell木马编写/命令执行等。
这个过程并不枯燥,一边打怪刷级一边成长岂不美哉,每个攻击手段都能让你玩得不亦乐乎,而且总有更猥琐的方法等着你去实践。
学完web渗透还不算完,还得掌握相关系统层面漏洞,像ms17-010永恒之蓝等各种微软ms漏洞,所以要学习后渗透。可能到这里大家已经不知所云了,不过不要紧,等你学会了web渗透再来看会发现很简单。
其实学会了这几步,你就正式从新手小白晋升为入门学员了,真的不算难,你上你也行。
4.安全体系
不过我们这个水平也就算个渗透测试工程师,也就只能做个基础的安全服务,而这个领域还有很多业务,像攻防演练、等保测评、风险评估等,我们的能力根本不够看。
所以想要成为一名合格的网络工程师,想要拿到安全公司的offer,还得再掌握更多的网络安全知识,能力再更上一层楼才行。即便以后进入企业,也需要学习很多新知识,不充实自己的技能就会被淘汰。
从时代发展的角度看,网络安全的知识是学不完的,而且以后要学的会更多,同学们要摆正心态,既然选择入门网络安全,就不能仅仅只是入门程度而已,能力越强机会才越多。
尾言
因为入门学习阶段知识点比较杂,所以我讲得比较笼统,最后联合CSDN整理了一套【282G】网络安全从入门到精通资料包,需要的小伙伴可以点击链接领取哦! 网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!