RSS Feed
更好更安全的互联网
  • 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/

    作者:Elfinx | 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/

    作者:Elfinx | 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/

    作者:Elfinx | 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是第二个全局变量