Meraklısı için Wordpress Content Injection Zafiyeti (4.7.0 & 4.7.1)
Wordpress 4.4 sürümü ile birlikte REST API desteğini eklemişti. 4.7.0 ve 4.7.1 sürümlerinde ise varsayılan olarak aktif hale getirdi. Getirilen bu imkan, söz konusu versiyonlarda bugünlerde herkesin konuştuğu Content Injection zafiyetini ortaya çıkardı. Zafiyetin ayrıntılarını ve etkilerini, hakkında yazılan ilk Türkçe kaynaktan öğrenmek için yazımızı okuyabilirsiniz.
Wordpress, milyonlarca web sitesi tarafından kullanılan bir blog sistemi. Yalnızca bu kadar da değil. Sunduğu jenerik yapısı sayesinde kolaylıkla bir e-ticaret, bir magazin ya da herhangi bir siteye dönüşebilir. Aktif ve genişleyen topluluk desteği ile her geçen gün hangi amaçla kullanmak istiyorsanız, Wordpress yüklemenizi o amaca evrilteceğiniz bir template, bir plugin sistemi yayınlanıyor.
Sunduğu kullanım kolaylığını, REST API desteği ile de güçlendirmek isteyen Wordpress Aralık 2015 4.4 sürümü ile birlikte REST API desteğini ekledi. Sonrasında da 4.7.0 ve 4.7.1 versiyonlarında REST desteğini varsayılan olarak aktif hale getirdi. Bugün tüm güvenlik camiasının konuştuğu Content Injection zafiyeti de böylece ortaya çıkmış oldu. Bu zafiyet dolayısıyla saldırgan hiçbir yetkiye gereksinimi olmadan ilgili sitedeki blog postları güncelleyebilir.
REST bilindiği üzere, HTTP isteklerini işleyen bir tür servis. Bu servis sayesinde HTTP'nin standart işleçlerini kullanarak web kaynağı üzerinde değiştirme, güncelleme, silme gibi işlemler yapılabilmektedir. Bugün pek çok mobil uygulama veri katmanı ile ilişkilerini REST API'ler ile sağlamaktadır. REST API ile ilgili ayrıntılı bir yazı için tıklayınız.
Wordpress REST API'sindeki bu zafiyete sebep olan noktalar kısaca, kod bütünlüğünde sağlanamayan tutarlılık ve uygulama altyapısının bilinmeyen yahut geliştirme aşamasında hesaba katılmayan özellikleri. Şimdi bunlara sıra ile değinelim.
Wordpress REST API'sini incelemeye bu istekleri karşılamak üzere set edilen route tanımları ile başlayalım.
Şekil 1
1 numaralı noktada REST API tarafından karşılanacak URL'lerin tanımları yapılıyor. Buna göre REST URL'i (ki bu URL zafiyet kapsamında wp-json/wp/v2/posts/XXX şeklinde çözümlenmektedir.) eğer path alanının sonunda en az bir basamaklı bir tamsayı barındırıyor ise, bu route bloğunda tanımlanan handler’lardan biri tarafından karşılanacaktır.
Peki Ama Hangi Handler?
WP_Rest_Server (class-wp-rest-server.php) sınıfında tanımlanan sabitlerde tanımlandığı şekliyle, READABLE değeri GET metoduna, CREATABLE değeri POST metoduna, EDITABLE ise PUT,POST ya da PATCH değerlerinden birini temsil etmektedir.
Şekil 2
2 ve 3 numaralı alanlardan görüldüğü üzere, EDITABLE yani POST, PUT ya da PATCH metodlarından biri ile yapılan bir istek, URL'in path kısmının son bölümünde tamsayı tipinde bir değer içeriyorsa öncelikle yetki doğrulaması için permission_callback
alanında belirtilen update_item_permission_check
isimli fonksiyon çağrılacak. Buradaki yetki doğrulamasından onay alındığı takdirde de esas callback olan update_item
fonksiyonu çağrılacaktır.
URL : http://netsparkertest.com/wordpress/index.php/wp-json/wp/v2/posts/4
Burada dikkatleri update_item_permission_check
fonksiyonunda yoğunlaştırmakta fayda var.
Şekil 3
Kırmızı ile işaretlediğimiz noktadan anlaşılacağı üzere, bu REST fonksiyonunun kapsamına girmemiz, path parametresinde yollanan tamsayı değeri sayesinde olmasına rağmen, fonksiyon içerisinde id değeri $request isimli koleksiyondan alınmakta. $request koleksiyonu, PHP kullanıcılarının aşina olduğu $_REQUEST koleksiyonunu döndürmektedir. $_REQUEST koleksiyonu hem Query string hem de POST istek body’sinden gönderilen değerlerin tümünü kapsar.
Fonksiyonda dikkatimizi çeken bir başka özellik ise tüm fonksiyon gövdesine hakim olan blacklist yaklaşımıdır. Bu yaklaşıma göre değil (!) yargılarıyla bir takım karşılaştırmaların yapılıp, bu kara listeye dahil edilmeyen yaklaşımların bu fonksiyonu atlatma olasılığıdır.
Bu karalistelerden birine yakalanmayan fonksiyon nihayet return true satırına varacak ve böylece update_item_permission_check
fonksiyonu "true" değer döndürecektir.
Aşağıdaki gibi bir URL ile istek yaptığımızı varsayalım:
http://netsparkertest.com/wordpress/index.php/wp-json/wp/v2/posts/4?id=4abcdefg
URL ile Rest API'nın kapsamına girdik ama query string alanından da id değerini manipüle ettik.
Dolayısıyla Şekil 3'te kırmızı renkle işaretlenen alanda query string'den gelen id parametresi alınmış olacaktır. Bu değer ise, 4abcdefg değeridir.
Şekil 3'te 1 numara ile işaretlenen kontrol'de && (ve) operatörü kulllanılmıştır. Yani eşitliğin her iki tarafının da doğru olması beklenmektedir. Sistemde 4abcdefg id’sine sahip bir blog post olmadığından $post değeri peşinen false'dur. Dolayısıyla bu fonksiyon kapsamına girilmeyecek ve ilk kısım atlatılmış olacaktır.
2 numara ile işaretlenen kontrolde ise yine üç farklı kontrol için && (ve) operatörü kullanılmış ve koşulun tüm elemanlarının true değeri döndürmesi beklenmiştir. Bu mukayesede author değerinin POST body’sinde ya da URL'in query kısmında gönderilmesi bekleniyor. Eğer bu alan POST ya da GET olarak gönderilmedi ise !empty($request["author"])
değeri sağlanamayacağından bu alana da girmeyecek ve bir nokta daha atlatılmış olacaktır.
3 numara ile işaretlenen blokta yine && (ve) operatörü ile tüm koşullardan true dönmesi beklenmektedir. Bu şartlardan biri de POST ya da Query ile gönderilen sticky alanıdır. Bu alan da gönderilmediği takdirde, bu koşulun bloguna da girilmeyecektir.
4 numaralı son koşul ise yine POST ya da Query'de gönderilmesi beklenen term değerleri ile ilgilidir. Term ve taxonomy kavramlarını şu örnekle açıklayabiliriz. Bir blog post'un kategorisi taxonomy; bu kategorinin bileşenleri -örneğin ürünler, resimler ise- term olarak adlandırılmaktadır.
Zafiyetin istismarı için ise term değerini göndermek mecburi değildir. Bu değeri göndermediğimizde de bu blok da atlatılacak ve nihayet return true
satırı ile update_item_permission_check
fonksiyonundan true yanıtı dönecektir. Tüm bu kontrollerden sonra sıra update_item
fonksiyonunda.
Şekil 4
Şekil 4'te update_item
fonksiyonu görülmektedir. Kırmızı ile işaretlenen alanda görülmektedir ki id değeri $request koleksiyonundan alınmakta ve type casting ile $id isimli değişkene aktarılmaktadır.
Tam bu noktada PHP'nin bir özelliğinden bahsetmemek olmaz. PHP katı bir tip yönetimi dayatan bir dil değildir. Değişkenlerin tipleri, kendilerine atanan değerlerce belirlenir ve betik boyunca değişebilir.
Tipler birbiri arasında değişik şartlarda dönüşebilmektir. Örneğin saldırı esnasında blog post id'si olarak atanan 4abcdefg değeri type casting ile integer tipine dönüştürüldüğünde, dönen yanıt şu olacaktır.
$id = (int)"4abcdefg" => $id değeri 4 olacaktır.
$garabet = 3 * "10 adet masa" = > $garabet değişkeninin değeri 30 olacaktır.
PHP'de tip dönüşümleri ile ilgili ayrıntılı bilgiyi PHP.net'de yer alan Type Juggling sayfasında ayrıntıları ile bulabilir, PHP ile ilgili diğer garabetleri ise lolphp subreddit sayfasından okuyabilirsiniz.
Artık id değeri 4abcdefg değerinden type casting olarak 4 'e dönüştüğünde, 4 numaralı blog postu editleyememek için hiçbir neden kalmadı.
Sayfanın saldırı öncesi görüntüsü:
Aşağıda saldırı için hazırlanan bir POST isteği bulunmakta:
POST http://netsparkertest.com/wordpress/index.php/wp-json/wp/v2/posts/4?id=4abcdefg HTTP/1.1
User-Agent: Fiddler
Content-Length: 70
Content-Type: application/json
Accept: */*
Host: netsparkertest.com
{"content":"I think you got the answer now!","title":"You're hacked!"}
HTTP/1.1 200 OK
Date: Thu, 02 Feb 2017 19:35:52 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.19
X-Robots-Tag: noindex
Link: <http://192.168.1.31/wordpress/index.php/wp-json/>; rel="https://api.w.org/"
X-Content-Type-Options: nosniff
Access-Control-Expose-Headers: X-WP-Total, X-WP-TotalPages
Access-Control-Allow-Headers: Authorization, Content-Type
Allow: POST, PUT, PATCH, DELETE
Content-Length: 1963
Content-Type: application/json; charset=UTF-8
{"id":4,"date":"2017-02-02T15:27:04","date_gmt":"2017-02-02T15:27:04","guid":{"rendered":"http:\/\/192.168.1.31\/wordpress\/?p=4","raw":"http:\/\/192.168.1.31\/wordpress\/?p=4"},"modified":"2017-02-02T19:35:52","modified_gmt":"2017-02-02T19:35:52","password":"","slug":"wordpress-content-injection","status":"publish","type":"post","link":"http:\/\/192.168.1.31\/wordpress\/index.php\/2017\/02\/02\/wordpress-content-injection\/","title":{"raw":"You're hacked!","rendered":"You’re hacked!"},"content":{"raw":"I think you got the answer now!","rendered":"<p>I think you got the answer now!<\/p>\n","protected":false},"excerpt":{"raw":"","rendered":"<p>I think you got the answer now!<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"_links":{"self":[{"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/4"}],"collection":[{"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=4"}],"version-history":[{"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/4\/revisions"}],"wp:attachment":[{"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=4"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=4"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/192.168.1.31\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=4"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}
İsteğin Fiddler’da yapılmasına dair ekran görüntüsü:
Gönderilen istek ve yanıt:
Saldırıdan sonra sayfanın görüntüsü:
Çözüm
Wordpress uygulamanızı ivedilikle 4.7.2 veya üzeri bir sürüme güncellemelisiniz.