如何跳转到影子小程序

0 阅读5分钟

在对微信小程序进行安全测试时,我们常遇到一个现象:明明某企业有小程序,却无法通过微信搜索找到它。这并非小程序不存在,而是由于以下原因被“隐藏”:

  • 小程序尚在审核中;开发者关闭了“可被搜索”选项;
  • 内容涉及敏感领域,被平台限制收录;
  • 仅为内部员工或特定用户开放。

这类未公开、不可搜索但实际运行的小程序,我们称之为 “影子小程序”

AppID 是每个微信小程序的唯一身份标识,如同“身份证号”。对于影子小程序而言,即便其在微信搜索中不可见、未对外公开,只要处于运行状态,就必然拥有一个有效的 AppID。 

通过 AppID,测试人员可以绕过搜索限制,直接定位到影子小程序的入口,进而发现其可能暴露的管理后台、调试接口等安全隐患。因此,AppID是发现和评估影子小程序风险的关键线索。

每一个小程序都有自己的appid,通过小程序前端文件夹名称、无影、或者等小程序反编译平台、burpsuite抓包,都能看到该小程序的appid。

当我们获取到一个小程序 appid 后,如何访问该 appid 对应的小程序呢?

我们可以直接搭建一个小程序来进行跳转:

注册账号

先去注册一个自己的小程序:mp.weixin.qq.com/wxopen/ware…

image.png

image.png

这里正常注册,小程序类目填 其他,主体类型填 个人 即可。

注册好了之后登陆进来小程序平台,填写小程序信息和小程序类目。

image.png

正常按照要求填就行了,类目哪里我填的是 工具 > 信息查询工具, 其实填啥无所谓,这个小程序不需要上线的,所以也就不需要备案。

image.png

然后点击开发管理,拿到你自己的 AppID 即可。

然后去下载一个微信开发者工具,下载自己平台的即可,我是 MAC 这里就下载 MAC 了。 developers.weixin.qq.com/miniprogram…

image.png

然后打开微信开发者工具,将自己的 AppID 填进去,注意是你自己的。然后注意点不使用云服务。

image.png

然后接下来我们只需要编写三个文件的代码即可,直接复制我下面的代码就行了。

image.png

index.scss:

// 1. 定义变量,方便统一管理颜色和尺寸
$color-primary: #667eea;
$color-primary-end: #764ba2;
$color-bg: #f5f5f5;
$color-white: #fff;
$color-text-main: #333;
$color-text-regular: #666;
$color-text-placeholder: #999;
$color-error: #ff4d4f;
$color-disabled-bg-start: #e0e0e0;
$color-disabled-bg-end: #d0d0d0;

$border-radius-lg: 16rpx;
$border-radius-sm: 8rpx;
$spacing-unit: 40rpx;

// 2. 定义混入 (Mixin),复用通用样式
@mixin flex-center {
  display: flex;
  align-items: center;
}

@mixin button-reset {
  &::after {
    border: none;
  }
}

// 3. 使用嵌套和变量重写样式
page {
  background-color: $color-bg;
  min-height: 100vh;
}

.container {
  padding: $spacing-unit;
}

// 头部
.header {
  text-align: center;
  margin-bottom: 60rpx;

  .title {
    display: block;
    font-size: 48rpx;
    font-weight: bold;
    color: $color-text-main;
    margin-bottom: 20rpx;
  }

  .subtitle {
    display: block;
    font-size: 28rpx;
    color: $color-text-placeholder;
  }
}

// 表单
.form {
  background-color: $color-white;
  border-radius: $border-radius-lg;
  padding: $spacing-unit;
  margin-bottom: $spacing-unit;

  .form-item {
    margin-bottom: $spacing-unit;

    &:last-child {
      margin-bottom: 0;
    }
  }

  .label {
    @include flex-center;
    margin-bottom: 20rpx;
    font-size: 28rpx;
    color: $color-text-main;

    .required {
      color: $color-error;
      margin-right: 8rpx;
      font-weight: bold;
    }

    .optional {
      margin-left: 8rpx;
      font-size: 24rpx;
      color: $color-text-placeholder;
    }
  }

  .input {
    width: 100%;
    height: 80rpx;
    padding: 0 24rpx;
    background-color: $color-bg;
    border-radius: $border-radius-sm;
    font-size: 28rpx;
    box-sizing: border-box;
  }
}

// 按钮组
.button-group {
  display: flex;
  flex-direction: column;
  gap: 24rpx;
  margin-bottom: $spacing-unit;
}

.jump-button {
  width: 100%;
  height: 96rpx;
  line-height: 96rpx;
  background: linear-gradient(135deg, $color-primary 0%, $color-primary-end 100%);
  color: $color-white;
  border-radius: $border-radius-lg;
  font-size: 32rpx;
  font-weight: bold;
  border: none;
  box-shadow: 0 8rpx 24rpx rgba($color-primary, 0.3);
  transition: all 0.3s ease;
  @include button-reset;

  &[disabled] {
    background: linear-gradient(135deg, $color-disabled-bg-start 0%, $color-disabled-bg-end 100%);
    color: $color-text-placeholder;
    box-shadow: none;
  }
}

.clear-button {
  width: 100%;
  height: 96rpx;
  line-height: 96rpx;
  background-color: $color-white;
  color: $color-text-regular;
  border: none;
  border-radius: $border-radius-lg;
  font-size: 30rpx;
  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
  transition: all 0.3s ease;
  @include button-reset;
}

// 提示信息
.tips {
  background-color: $color-white;
  border-radius: $border-radius-lg;
  padding: $spacing-unit;

  .tips-title {
    font-size: 28rpx;
    font-weight: bold;
    color: $color-text-main;
    margin-bottom: 20rpx;
  }

  .tips-item {
    font-size: 26rpx;
    color: $color-text-regular;
    line-height: 44rpx;
    margin-bottom: 12rpx;
  }
}

index.ts:

// index.ts
Component({
  data: {
    appId: '',
    path: '',
    extraData: '',
  },
  methods: {
    // 输入 AppID
    onAppIdInput(e: any) {
      this.setData({
        appId: e.detail.value.trim()
      })
    },

    // 输入页面路径
    onPathInput(e: any) {
      this.setData({
        path: e.detail.value.trim()
      })
    },

    // 输入额外参数
    onExtraDataInput(e: any) {
      this.setData({
        extraData: e.detail.value.trim()
      })
    },

    // 跳转到其他小程序
    navigateToMiniProgram() {
      const { appId, path, extraData } = this.data

      if (!appId) {
        wx.showToast({
          title: '请输入目标小程序 AppID',
          icon: 'none'
        })
        return
      }

      // 构建完整路径
      let fullPath = path || ''
      if (fullPath && extraData) {
        fullPath += (fullPath.includes('?') ? '&' : '?') + extraData
      }

      wx.navigateToMiniProgram({
        appId: appId,
        path: fullPath,
        success: () => {
          console.log('跳转成功')
        },
        fail: (err) => {
          console.error('跳转失败:', err)
          let errorMsg = '跳转失败'
          
          if (err.errMsg.includes('navigateToMiniProgram:fail invalid appid')) {
            errorMsg = 'AppID 无效或目标小程序不存在'
          } else if (err.errMsg.includes('navigateToMiniProgram:fail cancel')) {
            errorMsg = '用户取消跳转'
          } else if (err.errMsg.includes('path')) {
            errorMsg = '页面路径错误'
          }

          wx.showModal({
            title: '跳转失败',
            content: errorMsg + '\n\n请确保:\n1. 目标小程序 AppID 正确\n2. 目标小程序已发布\n3. 页面路径正确(如有填写)',
            showCancel: false
          })
        }
      })
    },

    // 清空表单
    clearForm() {
      this.setData({
        appId: '',
        path: '',
        extraData: ''
      })
    }
  }
})

index.wxml:

<!--index.wxml-->
<view class="container">
  <view class="header">
    <text class="title">小程序跳转工具</text>
    <text class="subtitle">输入目标小程序信息进行跳转</text>
  </view>

  <view class="form">
    <view class="form-item">
      <view class="label">
        <text class="required">*</text>
        <text>目标小程序 AppID</text>
      </view>
      <input 
        class="input" 
        placeholder="请输入目标小程序的 AppID" 
        value="{{appId}}"
        bindinput="onAppIdInput"
      />
    </view>

    <view class="form-item">
      <view class="label">
        <text>目标页面路径</text>
        <text class="optional">(可选)</text>
      </view>
      <input 
        class="input" 
        placeholder="例如: pages/index/index" 
        value="{{path}}"
        bindinput="onPathInput"
      />
    </view>

    <view class="form-item">
      <view class="label">
        <text>页面参数</text>
        <text class="optional">(可选)</text>
      </view>
      <input 
        class="input" 
        placeholder="例如: id=123&name=test" 
        value="{{extraData}}"
        bindinput="onExtraDataInput"
      />
    </view>
  </view>

  <view class="button-group">
    <button class="jump-button" bindtap="navigateToMiniProgram" disabled="{{!appId}}">
      跳转小程序
    </button>
    <button class="clear-button" bindtap="clearForm">
      清空
    </button>
  </view>

  <view class="tips">
    <view class="tips-title">使用说明:</view>
    <view class="tips-item">1. AppID 为必填项,填写要跳转的目标小程序 AppID</view>
    <view class="tips-item">2. 页面路径为可选,如: pages/index/index,不填则跳转到目标小程序首页</view>
    <view class="tips-item">3. 页面参数为可选,格式: key1=value1&key2=value2</view>
    <view class="tips-item">4. 从2020年4月起,跳转其他小程序无需配置白名单</view>
    <view class="tips-item">5. 需要用户点击按钮触发跳转,会弹窗询问用户是否跳转</view>
  </view>
</view>

然后还需要改一下 app.json,只需要改里面一小段代码即可,这个文件里的其他代码都不动:

  "window": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "小程序跳转工具",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationStyle": "custom"
  },

编写完代码,保存代码,然后点击预览就会出现一个二维码,手机扫码就可以使用这个小程序了:

image.png

image.png