logo头像

爱分享 爱生活 爱编程

记一次获取网站鉴权的实践操作

在移动端平台(就先简称PF平台)上新应用,一般会经过安全校验。所以每一个应用都会采取一些攻击策略手段来保障应用的安全稳定。一般是 xss 攻击,不过针对今天举例的系统(就先简称为BCP)他做的还是很棒的。还有就是鉴权登录,这也是主要能攻破BCP系统的关键点。

另外本文只分析破解过程,对于原理和一些名词不做过多解释。

工具

  • fillder(抓包工具,这个其实有没有都行,主要是爬取接口的时候,可以比较直观的。)

  • 谷歌浏览器

  • PostMan(用户发送接口请求)

过程分析

xss 诱导攻击

BCP系统这方面考虑的比较全面,暂且不分析了。

最高鉴权获取

这也是本篇文章要重点分析的部分。

起因

打开PF平台上的 BCP 系统,在网络请求中发现,这样一个接口:

  • url:jtsi/UserLogin/login

  • method:POST

  • 作用:用户登录,返回用户登录信息

  • 参数字段:

    字段 取值(爬取发现) 类型 是否必填 说明
    deviceType D string 判断设备类型,貌似没啥用,不断啥就用D这个值就行
    domain BCPWx string 用来判断是否是在 移动端 ,移动端就是 BCPWx,PC端啥也不写
    timestamp 1595656277 number 时间戳,应该是为了标识当前调用的事件
    login_type login_user string 用户登录类型,用 login_user 这个就行
    passwd BcpWx%3c6****efbe84%3E string 用户的密码(为了安全,我进行了脱密处理),下文会重点分析,这个字符串,关键突破点
    t 1595656277720 number 又来一个时间戳,不过这个是 毫秒 的,上面的 timestamp 是秒的。
  • 返回参数:

    {
      "Result": true,
      "ReturnCode": "000",
      "ReturnMessage": {
          "orgName": "******团队", 
          "roleIds": ["-1"],
          "statusMsg": "登录成功",
          "domain": "",
          "sessionId": "5d1116fb-****-****-94ac-96fa19c75d30",
          "userName": "***",
          "userId": "********",
          "orgId": "**********",
          "depts": {
              "num": "1",
              "**********": "****团队"
          },
          "status": 1,
          "token": "5d1116fb-****-****-94ac-96fa19c75d30"
      }
    }

从上面的请求参数开始分析:

  • 第一步:

    BCP 系统的致命漏洞,也是出现在此。用户在经过 PF 平台的 Oauth2 授权后,竟然没有直接返回 token 值,而是在Oauth2 授权拿到 用户id 后,神奇的是又去请求 BCP 系统自带的登录接口(UserLogin/login),那么他就需要账号密码。

  • 第二步:

    通过第一步发现,系统在登陆前,肯定会有一次获取用户密码的请求,果不其然,在进一步抓包处理后(由于只是在手机端简单的注入了一些检测脚本,所以并没有拿到返回密码的接口,或者说在我注入脚本之前,系统就已经完成了获取密码操作,所以我一开始没有拿到这个接口),发现了一个返回用户密码的接口,这个稍后再分析。

  • 第三步:

    第二步说到:会有一个返回密码的接口,那么我们要想获取到最高权限(系统管理员)的密码,就必然得知道管理员(当然我是知道,该系统的管理员的用户id,不知道也没关系,下文会讲解怎么拿)的账号密码。

获取管理员账号1

上文说到,我已经知道管理员的帐号了,因为就是系统负责人,我肯定是知道的,假如说我不知道,怎么办?

大家知道,在公司内部一般的员工编号都是数字或者有规律的,最主要的就是可以根据自己的账号去推演,毕竟破解最多的就是穷举法。

为了更好的解释怎么获取管理员账号,我在下文会提到根据什么原理拿到管理员账号,此处假设我们知道管理员的账号了。

寻求加密突破点

我们再一次回到,在移动端发现的登录接口,分析一下,此处的请求参数:passwd,为了解释简单,我们假设一个账号:12345678,他的密码(当然我是根据他的规律来生成的)是:BcpWx%3c25d55ad283aa400af464c76d713c07ad%3e,根据经验(也是常识),在接口传参数的时候我们不会有%号的出,也是我们是通过抓包拿到的字符串,肯定也会经过URL编码,那么我们反编码一下:BcpWx<25d55ad283aa400af464c76d713c07ad>,这个规律就有了,密码应该是加密过的,加密过后应该是:25d55ad283aa400af464c76d713c07ad,然后开发者又在密码前后分别加了 “BcpWx<” 和 “>”,当然如果你经验够足的话,一眼就能判断 “>” 的url 编码 为 %3c, “< “的编码为 %3e。那么就只剩下 25d55ad283aa400af464c76d713c07ad ,咋一看,这咋解密,无从下手,上文说到,破解、攻击等手段常用的就是穷举法(没想到第一个 BCP 就中了),最常用的就是 md5 加密,不管三七二十一,先来一波md5加密:

字符串 12345678
16位 小写 83aa400af464c76d
16位 大写 83AA400AF464C76D
32位 小写 25d55ad283aa400af464c76d713c07ad
32位 大写 25D55AD283AA400AF464C76D713C07AD

注:因为我知道我得账号是多少,所以我是拿自己的 userid,通过md5 去比对的。

到此返现了惊喜,md5 加密的 32位 小写 结果不就是,我们在接口中发现的字符串吗?其实到这里就已经攻破了,当前这是后来分析,才知道的。假设不知道的时候,我们就需要进一步发现。因为这个只是在移动端登录的接口,即使知道了,操作起来不太方便。接下来我们登录 PC 端看看会有怎样的发现。

获取管理员账号 2

接上上小结的内容,假设你不知道管理员账号怎么办?PC 端登录,是用的公司统一认证平台,当登录到,总共请求了 3(有一个接口请求了两次,重复的,阿西吧) 接口:/jtsi/UserLogin/getHeadImg/jtsi/JTApplicationService/getApplications/jtsi/JTApplicationService/getApplicationMenu

第一个顾名思义,请求头像的不用管,重点部分是后边两个接口,超管也顾名思义,啥都管,那么他的应用和菜单树肯定是最多的,只要根据公司的员工编号,写个批量小脚本,哪个员工的菜单树最多,他肯定就是超管了就没跑了。也有的是 admin 这种。

接口小技巧

根据pc端的接口请求可以看出,BCP 系统鉴权使用的 token (移动端)和 sessionId (PC端),这也就不难理解,为啥在 jtsi/UserLogin/login 的返回结果中,为啥既有 token 还有 sessionId 了,但是他忽略了一点,也是 BCP 系统最致命的。从返回结果我们可以得出两个结论:

  1. 后台生成 token 和 sessionId 的机制一样,只是换了个名称而已
  2. 我们可以利用移动端生成的密码,去获取 token,而此处的 token也即 PC端的 sessionId

为什么要用移动端的接口去生成 token(sessionId ) 呢,上文说到,PC端登录和移动端登录时,是通过 domain 参数来判断的,加入我们不填 domain 参数,你会发现,用 md5 生成的密码对 PC 端的登录接口没用的,这也引发了我继续探寻下去的兴趣。

php 中 的__callStatic()

<?php
/**
 * 魔术方法:__call()和__callStatic()
 * __call($name, $arguments):调用类中不存在的动态方法,自动调用
 * __callStatic($name, $arguments):调用类中不存在的静态方法,自动调用
 * 以上二个魔术方法实现了方法的重载
 */
require 'call_back.php';
class Call_1
{
    //调用动态方法
    public function __call($name, $arguments)
    {
        //打印出方法名和参数,参数是在数组$arguments中
//        return '<p>方法名:'.$name.',参数是:('.implode(',',$arguments).')</p>';
        //跨类调用方法
        //call_user_func_array($method,$array_param);
//        print_r($arguments);
        //call_user_func_array([对象,'方法名',参数数组])
       return call_user_func_array([(new Call_2()),$name],$arguments);
    }
    //调用静态方法
    public static function __callStatic($name, $arguments)
    {
        return call_user_func_array(['Call_2',$name],$arguments);
    }
}
//动态
$obj = new Call_1();
echo $obj->hello(1,2,3);//输出6,成功调用了Call_2类中的方法
echo '<hr>';
//静态
echo $obj->add(1,100);//结果输出5050,成功调用了Call_2类中的add()方法

js 中可以借助 proxy 来高效实现 php中的 __callStatic()方法

// index.js
/**
 * @description: 工厂函数
 * @param {null} 
 * @return: 对应的实例
 */
class Factory {
    static make(){
        const application = require(`./lib/platform/${Factory.staticName}/application`)
        return new application(arguments[0])
    }
}
// 数据劫持
var P = new Proxy(Factory, {
    get: (target, property) => {    
        target.staticName = property;
        return target.make;
    }
});

P.wwlocal('这是一份配置');

//需要导入的包

// ./lib/platform/wwlocal/application
class Application {
    constructor(config){
        console.log('我收到的'+ config );
    }
}

module.exports = Application

在 index.js 中调用 P.wwlocal(‘这是一份配置’), 可以 很方便的导入 ./lib/platform/wwlocal/application,并返回实例,这样可以:

  1. 减少代码量
  2. 调用方便

建议在nodejs高版本的使用,否则可以用Object。property来改造下这个数据劫持方法

php 中 的__callStatic()

<?php
/**
 * 魔术方法:__call()和__callStatic()
 * __call($name, $arguments):调用类中不存在的动态方法,自动调用
 * __callStatic($name, $arguments):调用类中不存在的静态方法,自动调用
 * 以上二个魔术方法实现了方法的重载
 */
require 'call_back.php';
class Call_1
{
    //调用动态方法
    public function __call($name, $arguments)
    {
        //打印出方法名和参数,参数是在数组$arguments中
//        return '<p>方法名:'.$name.',参数是:('.implode(',',$arguments).')</p>';
        //跨类调用方法
        //call_user_func_array($method,$array_param);
//        print_r($arguments);
        //call_user_func_array([对象,'方法名',参数数组])
       return call_user_func_array([(new Call_2()),$name],$arguments);
    }
    //调用静态方法
    public static function __callStatic($name, $arguments)
    {
        return call_user_func_array(['Call_2',$name],$arguments);
    }
}
//动态
$obj = new Call_1();
echo $obj->hello(1,2,3);//输出6,成功调用了Call_2类中的方法
echo '<hr>';
//静态
echo $obj->add(1,100);//结果输出5050,成功调用了Call_2类中的add()方法

js 中可以借助 proxy 来高效实现 php中的 __callStatic()方法

// index.js
/**
 * @description: 工厂函数
 * @param {null} 
 * @return: 对应的实例
 */
class Factory {
    static make(){
        const application = require(`./lib/platform/${Factory.staticName}/application`)
        return new application(arguments[0])
    }
}
// 数据劫持
var P = new Proxy(Factory, {
    get: (target, property) => {    
        target.staticName = property;
        return target.make;
    }
});

P.wwlocal('这是一份配置');

//需要导入的包

// ./lib/platform/wwlocal/application
class Application {
    constructor(config){
        console.log('我收到的'+ config );
    }
}

module.exports = Application

在 index.js 中调用 P.wwlocal(‘这是一份配置’), 可以 很方便的导入 ./lib/platform/wwlocal/application,并返回实例,这样可以:

  1. 减少代码量
  2. 调用方便

建议在nodejs高版本的使用,否则可以用Object。property来改造下这个数据劫持方法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue原理</title>
</head>
<body>
    <script src="./index.js"></script>
    <script>
        let vm = new myVue({
            data: {
                age: 12,
                score: {
                    math: 99,
                    english: 89
                }
            }
        })        
    </script>
</body>
</html>
class myVue {
    constructor(options) {
        this._options = options;
        this.$data = options.data;
        this._initData()
    }
<!-- more -->

    _initData() {
        let data = this.$data
        let keys = Object.keys(data)
        // 数据代理
        // 实现 vm.a 可以拿到 示例中 data的a的值
        for (let index = 0; index < keys.length; index++) {
            Object.defineProperty(this, keys[index], {
                enumerable: true, // 可遍历
                configurable: true, // 可被改写
                get: function proxyGetter() {
                    return data[keys[index]]
                },
                set: function proxySetter(val) {
                    data[keys[index]] = val
                }
            })
        }
        // 数据劫持,data 中的任意数据有变动时 进行劫持
        observe(data)
    }


}
// 判断data是否是基础类型, 递归数data据劫持
function observe(data) {
    // 使用Object.prototype上的原生toString()方法判断数据类型
    // 参考链接:https://blog.csdn.net/u012158998/article/details/86423270
    // 判断 data 属性是否是 数组或者 对象类型
    const type = Object.prototype.toString.call(data)
    if (type !== "[object Array]" && type !== "[object Object]") { // 基础类型无需遍历
        return;
    }

    // 递归处理 数据劫持
    new Observer(data)
}

/**
 * 4. 实现数据劫持工具方法
 * @param {*} obj 要劫持的数据
 * @param {*} objKey 劫持数据的类型
 * @param {*} value  劫持数据的原始值
 */
function defineReactive(obj, objKey, value) {
    // 判断下是否可以继续遍历,由内向外的遍历
    observe(obj[objKey])
    Object.defineProperty(obj, objKey, {
        enumerable: true, // 可遍历
        configurable: true, // 可被改写
        get: function proxyGetter() {
            console.log(`${objKey}取值`);
            return value
        },
        set: function proxySetter(val) {
            console.log(`${objKey}发生了改变`);
            value = val
        }
    })
}
// 
class Observer {
    constructor(data) {
        this._walk(data)
    }

    _walk(data) {
        let keys = Object.keys(data)
        for (let index = 0; index < keys.length; index++) {
            // 针对每一层开始遍历数据并劫持
            defineReactive(data, keys[index], data[keys[index]])
        }
    }
}

安装 Java 环境

概述

概念理解:

  • J2SE 标准版

  • J2EE 企业版

  • J2ME 用于移动设备、嵌入式设备

    JRE、JDK、JVM之间的区别与联系

  • JVM :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心,具体的下文会详细说明。

  • JRE :英文名称(Java Runtime Environment),我们叫它:Java 运行时环境。它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库。它相对于 jvm 来说,多出来的是一部分的 Java 类库。

  • JDK :英文名称(Java Development Kit),Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。

显然,这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM

前提

  • win10 专业版

如果安装的是 win10 企业版本呢,由于太纯净了,连应用商店都没有,我们需要自己去下载一下应用商店

  • 开启 Linux 子系统服务

    操作 步骤:

    1. 打开 控制面板
    2. 找到 程序
    3. 点击 启用或关闭 Windows 功能
    4. 在差不多倒数的几个,找到 适用于Linux 的 Windows 子系统打勾,等待安装好后,重启电脑
  • 安装 Linux 子系统

    1. 打开 应用商店,如果没有登陆,需要实现登录 一下
    2. 搜索 Linux
    3. 选择 Ubuntu 18.04 LTS 选择安装,如果下载速度较慢,可以打开传递优化
    4. 等待下载好后,直接启动就好,此处一般时间比较长,请耐心等待,开启后,设置自己的 用户名密码
  • 启动配置 Ubuntu

    这里可以根据自身来配置一些东西,比如替换镜像为国内阿里源,具体的配置还需要,自己去查就好,

    如果要使用 root 权限,直接使用 sudo + commend 就好

安装编辑器

这里可选择 vscode 为例,因为 vsCode 有现成的插件可以很好的在 wsl 中调试代码、共享文件

前言

最近在开发一款基于微信的产品,这里不得不说,超哥wechatSdk 确实好用,节省了不少时间,但是在本地开发总是会用到内网穿透来实现和微信服务器联调的环节。以前也用户过诸如 natapp 类的工具,一个原因是因为,这类工具都是需要费用的,虽然有免费的版本,但是总是会掉线,而且还是限流量。也用过Ngnok,但是如果自己搭建起来比较麻烦,失败率太高了。直接用的话,服务器在国外,在天朝访问你懂的,那是相当的缓慢,一袋烟的功夫过去了,还是个毛都没有,微信接口验证,通过的话,还得看运气。而且每次一个网址只能是用八小时左右,这就需要每次都得到公众号里面重新配置。额……又得等。之前偶然间发现,一个国人开源的神器 frp ,真的是太棒了,这里分下能给大家。具体能实现的功能,文档里面有详细的说明,我就不罗嗦了。给大家中文文档。接下来给大家分享一下搭建步骤,也是一个备忘,有好东西当然是要分享了。
对了,最近开发微信公众平台需要一些测试用户,希望大家帮忙关注一下,不会给您带来困扰的,这里谢谢各位了。

说明

首先说下,什么叫泛域名,假如我有一个域名 sharef.top ,那么主域名就是 www.sharef.top , 假如还想做一个网站,域名为 f.sharef.top ,如果要想 访问这两个网站,那么我们得同时将我们的这两个域名A记录解析到服务器的IP,同样,假如我们需要为这两个网站 申请 ssl 证书,那么我们要申请两个,来配置。但是泛域名解析的话,我们只需要将 * .sharef.top 这一条 A 记录解析到我们的服务器就好了,那么不管我们,想要访问a.sharef.xin,还是 b.sharef.top 都会解析到 我们的服务器。同样 泛域名解析 也是一样,我们只需申请一次的 .sharef.top 证书,我们这里的 二级域名都是可以 用这一个 ssl 证书就可以了。