首页
社区
课程
招聘
[分享]pwnable.kr input day6
2021-1-7 22:20 8040

[分享]pwnable.kr input day6

2021-1-7 22:20
8040

题目

解题过程

1. 查看文件列表


flag 只对创建者 input2_pwn 和 root 可读,而我们的登录用户是 input2,无读权限。input2_pwn 与 input2 用户对 input 文件有读和执行权限,且权限里含有 s,因此 input2 用户在执行该文件时,会被赋予 root 权限。
此题调试过程较复杂,先将 input 文件复制到本地,再生成一个本地的 flag 便于测试
scp -P 2222 input2@pwnable.kr:/home/input2 ./
echo "This is a test!" > flag

2. 查看 input.c 文件

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char* argv[], char* envp[]){
        printf("Welcome to pwnable.kr\n");
        printf("Let's see if you know how to give input to program\n");
        printf("Just give me correct inputs then you will get the flag :)\n");
 
        // argv
        if(argc != 100) return 0;
        if(strcmp(argv['A'],"\x00")) return 0;
        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
        printf("Stage 1 clear!\n");
 
        // stdio
        char buf[4];
        read(0, buf, 4);
        if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
        read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
        printf("Stage 2 clear!\n");
 
        // env
        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
        printf("Stage 3 clear!\n");
 
        // file
        FILE* fp = fopen("\x0a", "r");
        if(!fp) return 0;
        if( fread(buf, 4, 1, fp)!=1 ) return 0;
        if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
        fclose(fp);
        printf("Stage 4 clear!\n");
 
        // network
        int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);
        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );
        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }
        listen(sd, 1);
        int c = sizeof(struct sockaddr_in);
        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }
        if( recv(cd, buf, 4, 0) != 4 ) return 0;
        if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
        printf("Stage 5 clear!\n");
 
        // here's your flag
        system("/bin/cat flag");
        return 0;
}

我们的目标是执行 system("/bin/cat flag") 需满足 5 个条件。

条件 1 argv

基础知识

main(int argc, char* argv[], char* envp[])
argc 代表程序的参数个数,程序名也作为参数之一,所以参数个数最少为 1。
argv[ ] 以 null 结尾的字符串数组。第一个字符串(argv[0])是程序名,后面的每个字符串都是从命令行传递给程序的参数。最后一个字符串(argv[argc])为 null。
envp[ ] 指向系统的环境变量数组的指针,"名称=值"的形式,以NULL结束。
int execve(const char filename,char const argv[ ],char * const envp[ ]);

c 源码

1
2
3
4
5
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");

要求

程序在执行时输入 100 个参数,且第 65 个参数应该是 ”\x00”,第 66 个参数应该是 ”\x20\x0a\x0d”。(65、66 分别是 A、B 的 ASCII 码)
而且 "\x00" 代表 NULL,"\x20" 代表空格 "\0a" 代表换行 "\x0d" 代表回车,这些参数无法在命令行中直接输入,此处采用 execve() 来传参。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
 
int main() {
    char *argv[100];
    argv[0] = "/home/whoami/pwn/input/input";
    for (int i = 1; i < 100; i++)
        argv[i] = "A";
    argv[65] = "\x00";
    argv[66] = "\x20\x0a\x0d";
    argv[100] = NULL;
    char* envp[] = {0,NULL};
    execve("/home/whoami/pwn/input/input",argv,envp);
}

执行结果

条件 2 stdio

基础知识

ssize_t read(int fd, void * buf, size_t count);
fd 是文件描述符,在 fd 一题中遇到过。fd == 0 是标准输入 stdin,fd ==1 是标准输出 stdout,fd == 2 是标准错误输出 stderr
int pipe(int fd[2]);
管道两端可分别用描述字 fd[0] 以及 fd[1] 来描述,fd[0] 端只能用于读,称其为管道读端;fd[1] 端则只能用于写,称其为管道写端。
int dup2(int odlfd, int newfd);
dup2()用来复制参数 oldfd 所指的文件描述词, 并将它拷贝至参数 newfd 后一块返回。

c 源码

1
2
3
4
5
6
7
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");

要求

第一个 read 中的 fd 为 0,表示标准输入,"\x00\x0a\x00\xff" 无法通过命令行输入,第二个 read 中的 fd 为 2,表示标准错误输出,"\x00\x0a\x02\xff" 也没有办法从命令行输入。此处考虑借助管道实现 I/O 重定向,最终实现将这些特殊字符输入到对应 buf 中。

exp

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main() {
 
    /* stage 1 */
    char *argv[100];
    argv[0] = "/home/whoami/pwn/input/input";
    for (int i = 1; i < 100; i++)
        argv[i] = "A";
    argv[65] = "\x00";
    argv[66] = "\x20\x0a\x0d";
    argv[100] = NULL;
 
    char* envp[] = {0,NULL};
 
 
    /* stage 2 */
    int pipe_stdin[2], pipe_stderr[2];
    pid_t pid_child;
 
    if(pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
        perror("Cannot create pipe!");
        exit(-1);
    }
 
    if((pid_child = fork()) < 0) {
        perror("Cannot create child process!");
        exit(-1);
    }
 
    if(pid_child == 0) {
        /* 子进程先等待父进程重定向管道读,然后关闭不使用的管道读
        ** 继而向两个管道写对应的字符串,父进程此时已经把管道读重定向到 stdin 和 stderr
        ** 最终由 input 程序接收
        */
        sleep(1);
        close(pipe_stdin[0]);   
        close(pipe_stderr[0]);
        write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
        write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);
    }
    else {
        //父进程无需管道写操作,首先 close 掉,然后把两个管道读重定向到 stdin 和 stderr 即 0 2
        close(pipe_stdin[1]);
        close(pipe_stderr[1]);
        dup2(pipe_stdin[0], 0);
        dup2(pipe_stderr[0], 2);
 
        execve("/home/whoami/pwn/input/input", argv, envp);
    }
}

执行结果

条件 3 env

基础知识

char getenv(const char name);
该函数返回一个以 null 结尾的字符串,该字符串为被请求环境变量的值。如果该环境变量不存在,则返回 NULL。

c 源码

1
2
3
4
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef")))
    return 0;
printf("Stage 3 clear!\n");

要求

使环境变量名 "\xde\xad\xbe\xef" 对应的值为 "\xca\xfe\xba\xbe" 即可。
利用 execve 传参。

exp

char *envp[]={0,NULL};修改为char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};即可

执行结果

条件 4 file

基础知识

size_t fread(void buffer, size_t size, size_t count, FILE stream);
buffer 为接收数据的地址,size 为一个单元的大小,count 为单元个数,stream 为文件流。
fread() 函数每次从 stream 中最多读取 count 个单元,每个单元大小为 size 个字节,将读取的数据放到 buffer;文件流的位置指针后移 size count 字节。
返回值为实际读取的单元个数。如果小于 count,则可能文件结束或读取出错;可以用 ferror() 检测是否读取出错,用 feof() 函数检测是否到达文件结尾。
size_t fwrite(void
buffer, size_t size, size_t count, FILE stream);
buffer 为数据源地址,size 为每个单元的字节数,count 为单元个数,stream 为文件流指针。
fwrite() 函数每次向 stream 中写入 count 个单元,每个单元大小为 size 个字节;文件流的位置指针后移 size
count 字节。
返回值为成功写入的单元个数。如果小于 count,则说明发生了错误,文件流错误标志位将被设置,随后可以通过 ferror() 函数判断。

c 源码

1
2
3
4
5
6
7
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

要求

读文件”\x0a”,前四个字节是”\x00\x00\x00\x00”即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
/* stage 4 */
FILE *fp = NULL;
char buf[] = "\x00\x00\x00\x00";
 
fp = fopen("\x0a","wb");
if(!fp) {
    perror("Cannot open file.");
    exit(-1);
}
fwrite(buf,4,1,fp);
fclose(fp);
fp = NULL;

执行结果

条件 5 network

基础知识

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
struct  sockaddr_in{
  sa_family_t   sin_family;  //地址族,"AF_INET"(IPV4), "AF_INET6"(IPV6); 
  uint16_t         sin_port;     //16位TCP/UDP端口号
  struct  in_addr  sin_addr;    //32位IP地址
  cha         sin_zero[8]   //不使用
};
 
struct  in_addr{
  in_addr_t    s_addr;      //32位IPV4地址
}
 
 
atoi() 将字符型转化为整型
htons(),将主机字节顺序(大、小端字节序)转换为网络字节顺序(大端字节序),为了解决兼容性问题。
 
int bind(int sockfd, struct sockaddr * my_addr, int addrlen);
/* sockfd 表示 socket 函数创建的通信文件描述符
** my_addr 表示 struct sockaddr 的地址,用于设定要绑定的 ip 和端口
** addrlen 表示所指定的结构体变量的大小
** 返回值:成功则返回 0, 失败返回 -1
** 功能:让套接字文件在通信时使用固定的IP和端口号
*/
 
int listen(int s, int backlog);
/* listen() 用来等待参数 s 的 socket 连线。
** 参数 backlog 指定同时能处理的最大连接要求。
** 返回值:成功则返回 0, 失败返回 -1
*/
 
int accept(int s, struct sockaddr * addr, int * addrlen);
/* accept() 用来接受参数 s 的 socket 连线。
** 参数 s 的 socket 必需先经 bind()、listen() 函数处理过, 当有** 连线进来时 accept() 会返回一个新的 socket 处理代码, 往后的数** 据传送与读取就是经由新的 socket 处理, 而原来参数 s 的 socket ** 能继续使用 accept() 来接受新的连线要求。
** 连线成功时, 参数 addr 所指的结构会被系统填入远程主机的地址数据, ** 参数 addrlen 为 scokaddr 的结构长度。
** 返回值:成功则返回新的socket 处理代码, 失败返回-1
*/
 
int recv(int s, void *buf, int len, unsigned int flags);
/* recv() 用来接收远端主机经指定的 socket 传来的数据, 并把数据存** 到由参数 buf 指向的内存空间, 参数 len 为可接收数据的最大长度。
*/
 
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
/* connect() 用来将参数 sockfd 的 socket 连至参数 serv_addr 指** 定的网络地址。
** 参数addrlen 为sockaddr 的结构长度。
** 返回值:成功则返回0, 失败返回-1
*/

c 源码

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
// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;    // Address to accept any incoming messages.
saddr.sin_port = htons( atoi(argv['C']) );   // C 的 ASCII 为 67
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
        return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

要求

C 的 ASCII 码为 67,所以条件 1 中第 67 个参数转换为整型后为接收端口号,我们需利用 socket 往该端口发送 "\xde\xad\xbe\xef"

exp

在条件 1 处添加 argv[67] = "12345";后,添加下列代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* stage 5 */
sleep(5);
int sockfd;
char buf1[] = "\xde\xad\xbe\xef";
struct sockaddr_in saddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1) {
    perror("Cannot create socket!");
    exit(-1);
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_port = htons(12345);
if(connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) {
    perror("Cannot connect to server!");
    exit(-1);
}
write(sockfd, buf1, 4);
close(sockfd);

完整 exp 上传到附件中了。

执行结果

本地测试通过

3. 上传 exp 并执行

上传编译后的 exp 记得将代码中的 argv[0] 修改为服务器的路径
scp -P 2222 ./exp_server input2@pwnable.kr:/tmp/test2021
红框内为 flag

上传 exp 遇到的几个问题

  1. input2 用户没有 /home/input2 文件的写权限,所以不能将 exp 直接上传到此目录,需上传到 /tmp 目录。
  2. 将 exp 上传到 /tmp 目录后因为有与 exp,flag 同名文件,所以我们必须在此目录下重新创建一个属于我们自己的文件夹,将 exp 转移到该文件夹中
  3. 新创建的文件夹里无 flag,需建立一个软链接ln -s /home/input2/flag ./才能读到 flag

参考文献

  • https://blog.csdn.net/m0_37925202/article/details/78944329
  • https://blog.csdn.net/qzwujiaying/article/details/6027570
  • https://r00tk1ts.github.io/2018/03/06/input/
  • https://bbs.pediy.com/thread-260204.htm
  • http://c.biancheng.net/cpp/html/2516.html
  • http://c.biancheng.net/cpp/html/2517.html

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 350
活跃值: (240)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mllaopang 2021-1-10 07:53
2
0

谢谢分享
游客
登录 | 注册 方可回帖
返回