Yakın zamanda DEFCON 29 CTF elemelerinde Hacking for Soju’yu temsil eden CTF ekibi Norse Code’a katıldım. İnternette bir zorluk vardı, bu yüzden onu çözmek için tüm hızımla ilerledim. Genel olarak zorluk oldukça basittir ve çok da zor değildir, ancak karmaşık JavaScript ile çalışabileceğiniz bir yolu göstermek için bu konuda bir yazı yazmaya karar verdim.
Zorluk, bir web sitesi bağlantısı ve indirebileceğiniz bir Chrome tarayıcı uzantısıyla başlar.
URL: http:// threefactooorx.challenges.ooo:4017
Web sitesiyle etkileşime girdiğimizde bir dosya yüklemesi olduğunu görebiliriz.
Bir HTML dosyası gönderirken aşağıdakileri geri alırız:
Sağlanan sorgulama açıklamasına ve dosyaya dayanarak, gönderdiğimiz HTML’yi uzantının yüklü olduğu bir Chrome tarayıcısının yüklediğini anlayabiliriz. Bu isteğin neye benzediği:
POST /uploadfile/ HTTP/1.1 Host: threefactooorx.challenges.ooo:4017 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------75344376227301306764121513080 Content-Length: 229 Origin: http://threefactooorx.challenges.ooo:4017 Connection: close Referer: http://threefactooorx.challenges.ooo:4017/submit Upgrade-Insecure-Requests: 1 -----------------------------75344376227301306764121513080 Content-Disposition: form-data; name="file"; filename="test.html" Content-Type: text/html test -----------------------------75344376227301306764121513080--
Bu noktada tarayıcı uzantısına dalmamız gerektiğini biliyoruz. Uzantı aşağıdakine benzer:
Aşağıdaki dosyalardan oluşur:
- \3FACTOOORX-public\background_script.js
- \3FACTOOORX-public\content_script.js
- \3FACTOOORX-public\manifest.json
- \3FACTOOORX-public\icons\icon.png
- \3FACTOOORX-public\pageAction\index.html
- \3FACTOOORX-public\pageAction\script.js
- \3FACTOOORX-public\pageAction\style.css
Bir tarayıcı uzantısına ilk kez girdiğinizde bakmak isteyeceğiniz ilk şey manifesttir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
{ "manifest_version": 2, "name": "3FACTOOORX", "description": "description", "version": "0.0.1", "icons": { "64": "icons/icon.png" }, "background": { "scripts": [ "background_script.js" ] }, "content_scripts": [ { "matches": [ " |
Bu size aşağıdaki bilgileri sağlar:
- background_script.js dosyası her zaman tarayıcının arka planında çalışıyor
- content_script.js dosyası hemen yüklenir tüm_url’ler bu, herhangi bir sayfa yükleme anlamına gelir
background_script.js dosyasına baktığımızda aşağıdakileri görüyoruz:
1 2 3 4 5 6 7 8 9 10 |
// Put all the javascript code here, that you want to execute in background. chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension"); if (request.getflag == "true") sendResponse({flag: "OOO{}"}); } ); |
Bu bize, Chrome uzantısı getflag = true içeren bir sendmesaj alırsa, yanıt olarak bayrağın geri gönderileceğini bilmemizi sağlar. Chrome uzantısı bize onunla etkileşim kurmamız için bir yol vermediği sürece bunu JavaScript/XSS ile başarmamızın hiçbir yolu yok. Eğer yapabilseydik şuna benzerdi:
1 |
chrome.runtime.sendMessage({getflag:true}); |
Bunu geçtikten sonra content_script.js dosyasına bakmamız gerekiyor. Not defterinde açtığımızda oldukça tipik, karmaşık bir JavaScript dosyası görüyoruz:
İlk girişimlerim onu aşağıdaki web sitelerine/araçlara atmaktı:
- beautifier.io – Bu, okumayı kolaylaştırmak için JS’yi güzelleştirir.
- JSNice.org – Temel JavaScript karartmalarını çözmede gerçekten iyi olan bir araç.
- de4js – JS gizleme teknikleriyle çalışabilen bir araç.
Ne yazık ki bunların hiçbiri gerçekten yardımcı olmadı.
JavaScript’te manuel olarak gezinmeye çalışırken ilk baktığımız şeylerden biri üstteki veri dizisidir OOO_0x5be3 çünkü bu muhtemelen kodun çoğu tarafından başvurulan dizelerdir.
OOO_0x1e05 işlevi dizelerin gizliliğini kaldırmak için kullanılır ve bunu daha okunaklı hale getirmek için betiğin çoğunu yavaş yavaş dize olarak değiştirmek için kullanabiliriz. Ancak bunu manuel olarak yapmaktan daha iyi bir yaklaşım var; Chrome’daki kesme noktalarını kullanarak, çıktının ne olduğunu görmek için gizlenmiş referansları doğrudan konsolda çağırabiliriz.
Bu noktada uzantıyı Chrome’a yüklemeye ve DOM üzerinde kesme noktalarıyla manuel olarak çalışmaya karar verdim. Yaptığımız ilk şey bir HTML sayfası açmak ve enjekte edilen içerik komut dosyasına bakmak. Bunu Kaynaklar -> İçerik komut dosyaları -> betiği seçerek -> güzelleştirerek yapıyoruz.
Artık bilgi için konsolu inceleyebilir ve kodu takip etmek için DOM’da kesme noktaları ayarlayabiliriz. Bu konuya dalmadan önce ilk gördüğümüz şeylerden biri konsoldaki bir hatadır:
Sağ taraftaki bağlantıya tıkladığımızda bizi doğrudan hatayı veren koda götürecektir:
Kod:
1 |
chilen = _0x1e6746[_0x2ca2fd(-0x22c, -0x1ea, -0x246, -0x1e5) + _0x3126db(-0x283, -0x277, -0x2c2, -0x29c)]('*')[_0x3126db(-0x21d, -0x226, -0x229, -0x1e1)] |
Bu hiçbir şeye benzemiyor, değil mi? Bu yüzden kodun sol tarafında bulunan 477 sayısına tıklayıp kesme noktası belirleyip sayfayı yeniliyoruz.
Kodun üzerinde çalışıp adım adım ilerlemeye çalışmak yerine artık gerçekten basit bir şey yapabiliriz:
Bu dizeleri konsola attığımızda artık satırın şu şekilde olması gerektiğini biliyoruz:
1 |
chilen = _0x1e6746["querySelectorAll"]('*')["length"] |
Şimdi, seçmeye çalıştığı şey hakkında daha fazla bilgi toplamak amacıyla bu işlemi tüm fonksiyon için tekrarlıyoruz. Sonunda şu noktaya geliyoruz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function check_dom() { _0x525a15['KoOZC'] = "#thirdfactooor"; _0x525a15["RkNoD"] = "INPUT"; _0x525a15["QwGfh"] = "QzIrw"; _0x525a15["IKmUR"] = "cunYq"; const _0x2c0eff = _0x525a15; var threeFAElement = document["getElementById"]("3fa"); chilen = threeFAElement["querySelectorAll"]('*')["length"]; maxdepth = 0; total_attributes = threeFAElement["attributes"]["length"]; for (let _0x28c57b of threeFAElement["querySelectorAll"]('*')) { d = _0x2c0eff["wmicU"](getDepth, _0x28c57b); if (d > maxdepth) { maxdepth = d }; if (_0x28c57b['attributes']) { total_attributes += _0x28c57b["attributes"]["length"]; } } specificid = 0; _0x2c0eff["ueJYA"](document['querySelector']("[tost=\"1\"]"), null) && (specificid = 1); token = 0; // if (_0x2c0eff["hJFjw"](document["querySelector"]("#thirdfactooor")["tagName"], "INPUT")) { if(document["querySelector"]("#thirdfactooor")["tagName"] == "INPUT") { if (_0x2c0eff["QwGfh"] !== _0x2c0eff["IKmUR"]) { token = "1337"; }else { function _0x2351ff() { return; } } } return totalchars = threeFAElement["innerHTML"]["length"], _0x2c0eff["TCJdK"](_0x2c0eff["TCJdK"](_0x2c0eff["TCJdK"](_0x2c0eff["TCJdK"](chilen, maxdepth) + total_attributes, totalchars), specificid), token); } |
Burada baktığımız belirli satırlar var:
- 3fa kimliğine sahip bir öğe arıyor.
- İçindeki çocuk unsurlarını ve niteliklerini seçiyor.
- Thirdfactooor kimliğine sahip bir giriş öğesi arıyor.
- Öğelerden birinde “tst=1” niteliğinin olmasını istiyor.
Böylece aşağıdakileri içeren bir html dosyası oluşturuyoruz:
Artık HTML dosyasını yenileyip tekrar yüklediğimizde artık herhangi bir hata almıyoruz. Bu, kodu incelemeye başlayıp bizden ne istediğini anlamaya başlayabileceğimiz anlamına geliyor. yüklendiğini çözdük. kontrol_dom Bu öğeleri yükleme işlevi. Burada birkaç seçeneğimiz var; bunlardan biri, orada bir kod akışı olduğunu bildiğimizden hangi fonksiyonun check_dom’u çağırdığını görmek için geriye doğru çalışabiliriz. Alternatif olarak, sayfa yükleme sırasında neyin yürütüldüğünü inceliyoruz ve görüyoruz.
JavaScript’in altına doğru kaydırırsak aşağıdakileri görürüz:
Acil çıkarımlar şunlardır:
- İşlev geri çağırmayı çağıran DOM değişikliklerini izleyen bir gözlemci yaratıyor.
- Komut dosyası, sayfa yüklendiğinde çalıştırılan bir setTimeout’a (500ms) sahiptir.
- setTimeout içindeki bir fonksiyonun bazı ilginç değişken isimleri vardır: FLAG, nodeadded, nodedeleted, attrcharsadded.
Kesme noktalarını belirleyerek ve daha önce olduğu gibi benzer adımlardan geçerek, betiğin gizliliğini biraz kaldırabiliriz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
setTimeout(function() { _0xd26915["getflag"] = _0x10b2d5["xOsuT"]; chrome["runtime"]["sendMessage"](_0xd26915, function(_0x336e82) { FLAG = _0x336e82["flag"]; // OOO{} console['log'](_0x10b2d5["KShsG"]("flag: ", "OOO{}")); if(nodesadded == 5 && nodesdeleted == 3 && attrcharsadded=23 && domvalue=2188) { document['getElementById']("thirdfactooor")['value'] = "OOO{}"; } const _0x369bcb = document['createElement']("div"); _0x369bcb['setAttribute']('id', 'processed'), document["body"]['appendChild'](_0x369bcb); }); }, 500); |
Değişken adları olduğu gibi bırakıldığı için bundan neredeyse anında çıkarabileceğimiz şey, bayrağı önceden oluşturduğumuz girdiye dökmeden önce aşağıdaki değişkenleri aramaktır:
- eklenen düğümler = 5
- silinen düğümler = 3
- öznitelik eklendi = 23
- baskın değer = 2188
Bu değişiklikleri izleyen bir DOM gözlemcisi olduğunu ve bunun sayfa yüklenirken yürütülmesine ihtiyacımız olduğunu bilerek, bazı değişiklikler oluşturuyoruz ve ne olacağını görmek için yeni kesme noktaları belirliyoruz:
Yaptığımız değişikliklerle, nodeadded’in 2 ve domvalue’nun 1514 olduğunu görebiliyoruz. Dolayısıyla bunu, son HTML dosyasında gereken kriterleri karşılayacak şekilde daha da genişletiyoruz:
Artık istediği değerleri aldığına göre, ne elde ettiğimizi görmek için bunu web sitesi aracılığıyla gönderiyoruz.
Genel olarak, bu oldukça kolay bir görevdi; tüm değişken adlarının anlaşılması zor olsaydı biraz daha zor olurdu. Her iki durumda da, ne beklediğini anlamak için kafa karışıklığı üzerinde çalışırken eğlendim.