RSS Feed
更好更安全的互联网
  • 从 0 开始学 Linux 内核之 android 内核栈溢出 ROP 利用

    2019-02-15

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

    最近在研究一个最简单的android内核的栈溢出利用方法,网上的资料很少,就算有也是旧版内核的,新版的内核有了很大的不同,如果放在x86上本应该是很简单的东西,但是arm指令集有很大的不同,所以踩了很多坑

    把上一篇改了一下名字,换成了从0开始学Linux内核,毕竟不是专业搞开发的,所以驱动开发没必要学那么深,只要会用,能看懂代码基本就够用了。本篇开始学Linux kernel pwn了,而内核能搞的也就是提权,而提权比较多人搞的就是x86和arm指令集的Linux系统提权了,arm指令集的基本都是安卓root和iOS越狱,而mips指令集的几乎没啥人在搞,感觉是应用场景少。

    环境准备

    android内核编译

    下载相关源码依赖

    android内核源码使用的是goldfish[1],直接clone下来,又大又慢又久,在git目录下编译也麻烦,所以想搞那个版本的直接下那个分支的压缩包就好了

    本文使用的工具的下载地址:

    PS:git clone速度慢的话可以使用国内镜像加速:s/android.googlesource.com/aosp.tuna.tsinghua.edu.cn/

    修改内核

    学android kernel pwn最初看的是Github上的一个项目[3],不过依赖的是旧内核,估计是android 3.4以下的内核,在3.10以上的有各种问题,所以我自己做了些修改,也开了一个Github源:https://github.com/Hcamael/android_kernel_pwn

    对kernel源码有两点需要修改:

    1.添加调试符号

    首先需要知道自己要编译那个版本的,我编译的是32位Android内核,使用的是goldfish_armv7,配置文件在: arch/arm/configs/goldfish_armv7_defconfig

    但是不知道为啥3.10里没有该配置文件,不过用ranchu也一样:

    给内核添加调试符号,只需要在上面的这个配置文件中添加:CONFIG_DEBUG_INFO=y,如果是goldfish就需要自己添加,ranchu默认配置已经有了,所以不需要更改。

    2.添加包含漏洞的驱动

    目的是研究Android提权利用方法,所以是自己添加一个包含栈溢出的驱动,该步骤就是学习如何添加自己写的驱动

    上面给了一个我的Github项目,把该项目中的vulnerabilities/目录复制到内核源码的驱动目录中:

    修改Makefile:

    导入环境变量后,使用一键编译脚本进行编译:

    PS: 在docker中复现环境的时候遇到一个问题,可以参考:https://stackoverflow.com/questions/42895145/cross-compile-the-kernel

    编译好后的内核在/tmp/qemu-kernel目录下,有两个文件,一个zImage,内核启动镜像,一个vmlinux是kernel的binary文件,丢ida里面分析内核,或者给gdb提供符号信息

    Android模拟环境准备

    内核编译好后,就是搞Android环境了,可以直接使用Android Studio[2]一把梭,但是如果不搞开发的话,感觉Studio太臃肿了,下载也要下半天,不过还好,官方提供了命令行工具,觉得Studio太大的可以只下这个

    PS: 记得装java,最新版的java 11不能用,我用的是java 8

    建一个目录,然后把下载的tools放到这个目录中

    首先需要使用tools/bin/sdkmanager装一些工具

    PS:因为是32位的,所以选择的是armeabi-v7a

    PSS: 我一共测试过19, 24, 25,发现在24,25中,自己写的包含漏洞的驱动只有特权用户能访问,没去仔细研究为啥,就先使用低版本的android-19了

    创建安卓虚拟设备:

    启动:

    去测试下我写的exp:

    编译好了之后运行,记得要用普通用户运行:

    Android 内核提权研究

    环境能跑通以后,就来说说我的exp是怎么写出来的。

    首先说一下,我的环境都是来源于AndroidKernelExploitationPlayground项目[3],但是实际测试的发现,该项目中依赖的估计是3.4的内核,但是现在的emulator要求内核版本大于等于3.10

    从内核3.4到3.10有许多变化,首先,对内核的一些函数做了删减修改,所以需要改改驱动的代码,其次就是3.4的内核没有开PXN保护,在内核态可以跳转到用户态的内存空间去执行代码,所以该项目中给的exp是使用shellcode,但是在3.10内核中却开启了PXN保护,无法执行用户态内存中的shellcode

    提权思路

    搞内核Pwn基本都是一个目的——提权。那么在Linux在怎么把权限从普通用户变成特权用户呢?

    一般提权的shellcode长这样:

    这个shellcode提权的思路有三步:

    1. prepare_kernel_cred(0) 创建一个特权用户cred
    2. commit_creds(prepare_kernel_cred(0)); 把当前用户cred设置为该特权cred
    3. MSR CPSR_c,R3 从内核态切换回用户态(详情自己百度这句指令和CPSR寄存器)

    切换回用户态后,当前程序的权限已经变为root,这时候就可以执行/bin/sh

    再继续深入研究,就涉及到内核的三个结构体:

    每个进程都有一个单独thread_info结构体,我们来看看内核是怎么获取到每个进程的thread_info结构体的信息的:

    有点内核基础知识的应该知道,内核的栈是有大小限制的,在arm32中栈的大小是0x2000,而thread_info的信息储存在栈的最底部

    所以,如果我们能获取到当前进程在内核中运行时的其中一个栈地址,我们就能找到thread_info,从而顺藤摸瓜得到cred的地址,如果能任意写内核,则可以修改cred的信息,从而提权

    总得来说,内核提权其实只有一条路可走,就是修改cred信息,而commit_creds(prepare_kernel_cred(0));不过是内核提供的修改cred的函数罢了。

    我们来通过gdb展示下cred数据:

    通过gdb可以获取到:$sp : 0xd415bf40

    从而计算出栈底地址:0xd415a000

    然后我们就能获取到thread_info的信息,然后得到task_struct的地址:0xd4d16680

    接着我们查看task_struct的信息,得到了cred的地址:0xd4167780

    然后查看cred的信息:

    把uid和gid的十六进制转换成十进制,发现就是当前进程的权限

    使用ROP绕过PXN来进行android提权

    既然我们已经知道了怎么修改权限,那么接下来就研究一下如何利用漏洞来提权,因为是研究利用方式,所以自己造了一个最基础的栈溢出

    因为开了PXN,所以没办法使用shellcode,然后我第一个想到的思路就是使用ROP来执行shellcode的操作

    这里说一下,不要使用ROPgadget,用这个跑内核的ELF,要跑贼久,推荐使用ROPPER[4]

    然后就是找commit_credsprepare_kernel_cred这两个函数的地址,在没有开启kalsr的内核中,我们可以直接把vmlinux丢到ida里面,找这两个函数的地址

    到这里,我们可以构造出如下的rop链:

    在成功修改当前进程的权限之后,我们需要把当前进程从内核态切换回用户态,然后在用户态执行/bin/sh,就能提权成功了

    但是这里遇到一个问题,在shellcode中,使用的是:

    我也很容易能找到gadget: msr cpsr_c, r4; pop {r4, pc};

    但是却没法成功切换回用户态,网上相关的资料几乎没有,我也找不到问题的原因,在执行完msr cpsr_c, r4指令以后,栈信息会发现变化,导致没法控制pc的跳转

    不过后来,我跟踪内核的执行,发现内核本身是通过ret_fast_syscall函数来切换回用户态的:

    经过我测试发现,使用msr SPSR_fsxc, r1可以成功从内核态切换回用户态,但是该指令却只存在于该函数之前,无法找到相关的gadget,之后我想了很多利用该函数的方法,最后测试成功的方法是:

    计算有漏洞的溢出函数的栈和ret_fast_syscall函数栈的距离,在使用ROP执行完commit_creds(prepare_kernel_cred(0));之后,使用合适的gadget来修改栈地址(比如: add sp, sp, #0x30; pop {r4, r5, r6, pc};),然后控制pc跳转到0xc000df90 <ret_fast_syscall+16>:,这样程序就相当于执行完了内核的syscall,然后切换回用户进程代码继续执行,在我们的用户态代码中后续执行如下函数,就能成功提权:

    完整exp可以见我的Github。

    ROP只是其中一种利用方法,后续还会研究其他利用方法和在64位android下的利用。

    参考

    1. https://android.googlesource.com/kernel/goldfish/
    2. https://developer.android.com/studio/?hl=zh-cn#downloads
    3. https://github.com/Fuzion24/AndroidKernelExploitationPlayground
    4. https://github.com/sashs/Ropper

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

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • 多种设备基于 SNMP 协议的敏感信息泄露漏洞数据分析报告

    2019-01-24

    作者:知道创宇404实验室
    English version:https://paper.seebug.org/796/

    1. 更新情况

    2. 事件概述

    SNMP协议[1],即简单网络管理协议(SNMP,Simple Network Management Protocol),默认端口为 161/UDP,目前一共有3个版本:V1,V2c,V3。V3是最新的版本,在安全的设计上有了很大改进,不过目前广泛应用的还是存在较多安全问题的V1和V2c版本。SNMP协议工作的原理简单点来说就是管理主机向被管理的主机或设备发送一个请求,这个请求包含一个community和一个oid。oid就是一个代号,代表管理主机这个请求想要的信息。被管理的主机收到这个请求后,看请求community是否和自己保存的一致,如果一致,则把相应信息返回给管理主机。如果不一致,就不会返回任何信息。所以community相当与一个认证的口令。V1和V2c版本的SNMP协议都是明文传输数据的,所以可以通过抓包嗅探等手段获取认证需要的community。

    2018年12月25日,Seebug 平台收录了多个基于SNMP协议的敏感信息泄露漏洞[2]。在多种厂商提供的网关类设备中,可以使用任意 community非常容易地读取SNMP提供的明文形式的的Web管理系统的用户名和密码、Wi-Fi凭证等信息。也可以使用任意community通过SET协议指令发送配置更新或控制请求,攻击者可以注入恶意的配置,如在Cisco DPC3928SL通过注入SSID造成Web管理系统的XSS(CVE-2018-20379)。

    该漏洞最早于 2017 年 4 月 4 日曝出,CVE编号为CVE-2017-5135,漏洞发现者将该漏洞称之为 Stringbleed[3]。2018年12月22日,时隔一年多,漏洞发现者进行全球探测后提供了一个很全的漏洞影响列表,其中包含23个不同厂商78个不同型号的网关设备,同时申请了多个CVE编号(CVE-2018-20380~CVE-2018-20401)。关于漏洞的成因一直都在争论之中,截止目前依然没有最终定论[4]。该类设备一般由ISP提供,我们暂时没有找到漏洞设备或固件对漏洞原理进行研究。根据社区的讨论结果,产生该漏洞的原因可能有以下几种情况:

    • 这些存在漏洞的设备使用了同一个存在逻辑缺陷的SNMP协议实现,该实现代码没有正确处理 community 字符串认证,导致任意 community 均可以通过认证,进一步导致敏感信息泄露。
    • ISP 配置错误,无效的访问控制规则。

    本文不包含漏洞分析,而是针对全球该类设备漏洞存在情况的数据分析报告。

    3. 漏洞复现

    直接使用 snmpget 命令发送 SNMP GET 请求即可, -c 选项指定任意字符串作为 community 均可通过认证。

    复现结果如下:

    img

    如果目标设备开放了Web服务,则可使用泄露的用户名和密码登陆Web管理系统,如下:

    img

    值得一提的是,用户名和密码存在为空的情况。

    img

    发送 SNMP SET 请求进行配置更新,-c 选项指定任意 community。如下所示,我们通过snmpset修改了 Web 系统用户名。

    img

    4. 漏洞影响范围

    我们通过提取漏洞设备相关的“关键词”,在ZoomEye网络空间搜索引擎[5]上共发现了1,241,510个 IP数据。

    img

    通过使用 zmap 对这 124 万的IP数据进行存活检测,发现约有 23 万的IP 存活。进一步对存活的 23 万IP进行漏洞存在检验,发现有15882 个目标设备存在该敏感信息泄露漏洞,涉及23个厂商的多个型号设备的多个固件版本。

    对这 15882 个漏洞设备的信息进行聚合,得到厂商及版本等统计信息如下(各个型号的ZoomEye dork 为: Vendor +Model +相应型号,如搜索DPC3928SL的语法为:Vendor +Model +DPC3928SL)

    漏洞设备的厂商分布饼图如下(有一点需要说明的是,DPC3928SL网关设备属于受此漏洞影响最严重的网络设备之一,原来属于Cisco公司, 现在属于Technicolor。)

    img

    国家分布前十如下,主要分布在中国、泰国、韩国等国家。

    img

    中国存在漏洞的设备全部分布在广东、台湾两个省份,呈现一定的地域性。其中广东最多,为6318 台。

    img

    进一步分析发现,在原全球124万161/udp 端口的该类设备IP数据中,属于中国的几乎全部分布在广东省和台湾省,其他省份基本上没有探测到公网上该类设备端口开放(运营商禁用了SNMP服务或者没有使用同类设备?)。

    img

    广东省受影响的设备的ISP分布如下,98% 以上归属于 “珠江宽频/联通“ 这个ISP,存在漏洞的设备大部分为Technicolor CWA0101 Wireless Gateway ,version :gz5.0.2。

    img

    台湾的181台漏洞设备都归属于ISP:twmbroadband.com,存在漏洞的设备大部分为Ambit T60C926。结合以上数据分析,我们断定中国存在该漏洞设备的地理分布和当地的ISP有很大关系。

    针对所有存在该漏洞的设备,我们统计了凭证的使用情况,如下:

    常用用户名,主要包含admin、login、user、dlink等。

    img

    常用密码,主要包含 admin、password、dream01、空、br0adband、gzcatvnet、user、Broadcom、dlink、ambit、root等,大部分为常见的弱密码。

    img

    非常有意思的是,我们发现以下使用次数最多的用户名密码组合,和使用该凭证组合最多的漏洞设备,以及漏洞设备所在的国家,都存在一定的关联性。

    (如第一行记录:中国所有含有该漏洞的设备中约有 5502 台都使用了 admin:admin 凭证,且受影响设备型号数量最多的为 Technicolor/CWA0101。)

    5. 总结

    基本可以肯定的是,这不是SNMP协议本身的问题,而是协议的实现代码存在漏洞或者ISP配置错误。该漏洞影响厂商、设备型号非常多,且呈现出一定的区域性。

    路由器、Modem、摄像头等IoT设备的信息泄露漏洞层出不穷,对个人隐私造成极大的威胁,关闭非必要的端口或者使用防火墙限制非法访问是个值得考虑的举措。

    系统的安全性往往取决于最短的那块木板-“木桶效应”,通过SNMP协议泄露HTTP服务凭证很好的说明了这一点。

    用户可根据PoC自行验证设备是否存在漏洞,如果存在漏洞可联系相应的ISP寻求解决方案。

    6. 相关链接

    [1] SNMP 协议
    https://baike.baidu.com/item/SNMP/133378?fr=aladdin

    [2] Seebug 漏洞收录
    https://www.seebug.org/vuldb/ssvid-97741
    https://www.seebug.org/vuldb/ssvid-97742
    https://www.seebug.org/vuldb/ssvid-97736

    [3] Stringbleed
    https://stringbleed.github.io/#

    [4] 关于该漏洞的讨论
    https://www.reddit.com/r/netsec/comments/67qt6u/cve_20175135_snmp_authentication_bypass/

    [5] ZoomEye网络空间搜索引擎
    https://www.zoomeye.org/searchResult?q=MODEL%20%2BVENDOR%20%2Bport%3A%22161%22

    [6] SNMP 历史漏洞参考
    http://drops.the404.me/1033.html


    Paper

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

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • 知道创宇404实验室2018年网络空间安全报告

    2019-01-17

    作者:知道创宇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/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • 从 0 开始学 Linux 驱动开发(一)

    2019-01-08

    作者: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]

    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如下所示:

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

    比如:

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

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

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

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

    加载驱动到内核

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

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

    比如:

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

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

    解决方法用两种:

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

    查看结果

    在/dev下增加设备文件

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

    知识点1 -- 驱动分类

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

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

    知识点2 -- 主次编号

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

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

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

    如上图所示,

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

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

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

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

    上面我举例的代码中:

    我声明了一个该结构体,并赋值,除了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/

    作者:Nanako | Categories:安全研究安全科普 | Tags:
  • Thinkphp5 远程代码执行漏洞事件分析报告

    2018-12-25
    作者:知道创宇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如下:

    继续查看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行添加如下代码:

    0x02 实际攻击分析

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

    2.1 0day在野

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

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

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

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

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

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

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

    2.2 0day曝光后

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

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

    使用的探测拼接参数为:

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

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

    0x03 小结

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


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

    作者:Nanako | Categories:安全研究漏洞通告 | Tags:
  • lucky 勒索病毒分析与文件解密

    2018-12-17
    作者: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

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

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

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

    运行截图

    0x02 病毒流程图

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

    流程图大致如下:

    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 为预设的字符串序列,其值为:

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

    其中 byte_56F800 字符串为:

    5.lucky_crypto_entry()

    文件名格式

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

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

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

    通知服务器

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

    HTTP 数据包格式如下:

    文件筛选

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

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

    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 作为明文对比。

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

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

    其中注意两点:

    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/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • Discuz x3.4 前台 SSRF 分析

    2018-12-10
    作者: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必须是/开头,一个比较常见的技巧。

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

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

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

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

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

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

    前面自动补协议就成了

    这里就涉及到了很严重的问题,就是对于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端口下

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

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

    任意url跳转

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

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

    /source/function/function_core.php:1498

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

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

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

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

    我们抓个包来看看

    成功绕过了各种限制

    利用

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

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

    exp演示


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

    作者:Nanako | Categories:安全研究 | Tags:
  • Code Breaking 挑战赛 Writeup

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

    easy - function

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

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

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

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

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

    函数结构形似

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

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

    exp

    easy pcrewaf

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

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

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

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

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

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

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

    这段代码来说

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

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

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

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

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

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

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

    easy - phpmagic

    题目代码简化之后如下

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

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

    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位一解的,主要不要把第一位打乱掉。

    简单测试一下

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

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

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

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

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

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

    最终poc:

    easy - phplimit

    这个代码就简单多了,简单来说就是只能执行一个函数,但不能设置参数,这题最早出现是在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

    easy - nodechr

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

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

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

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

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

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

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

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

    hard - thejs

    javascript真难....

    关键代码以及注释如下

    由于对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获取其父对象

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

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

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

    回到漏洞

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

    假设对于语句

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

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

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

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

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

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

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

    这种情况下,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/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • LCTF2018 ggbank 薅羊毛实战

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

    分析

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

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

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

    跟着看checkfriend函数

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

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

    然后就是goodluck函数

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

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

    随机数的生成方式为

    另一个的生成方式为

    其实非常简单,这两个数字都是已知的,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,然后交易回滚,交易就失败了。

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

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

    让我们回到checkfriend

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

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

    python脚本解决方案

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

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

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

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

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

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

    脚本如下

    最终效果显著


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

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • 以太坊合约审计 CheckList 之变量覆盖问题

    2018-11-16
    作者:LoRexxar'@知道创宇404区块链安全研究团队
    时间:2018年11月16日
    系列文章:

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

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

    Writeup

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

    fallback函数

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

    guess函数

    lottery函数

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

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

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

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

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

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

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

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

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

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

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

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

    其中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函数,然后返回即可。

    利用合约大致如下

    在题目之后

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

    array

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

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

    mapping

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

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

    mapping + struct

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

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

    对于上面的三种典型结构来说,虽然可以保证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/

    作者:Nanako | Categories:安全研究技术分享 | Tags: